diff --git a/redwood-compose/src/commonTest/kotlin/app/cash/redwood/compose/ChangeListenerTest.kt b/redwood-compose/src/commonTest/kotlin/app/cash/redwood/compose/ChangeListenerTest.kt index 2c39517d04..bc1cb27d55 100644 --- a/redwood-compose/src/commonTest/kotlin/app/cash/redwood/compose/ChangeListenerTest.kt +++ b/redwood-compose/src/commonTest/kotlin/app/cash/redwood/compose/ChangeListenerTest.kt @@ -54,7 +54,6 @@ class DirectChangeListenerTest : AbstractChangeListenerTest() { } class ProtocolChangeListenerTest : AbstractChangeListenerTest() { - @OptIn(RedwoodCodegenApi::class) override fun CoroutineScope.launchComposition( widgetSystem: TestSchemaWidgetSystem, snapshot: () -> T, @@ -101,7 +100,7 @@ abstract class AbstractChangeListenerTest { c.setContent { Button(text, onClick = null) } - assertThat(c.awaitSnapshot()).containsExactly("text hi", "onClick false", "modifier Modifier", "onEndChanges") + assertThat(c.awaitSnapshot()).containsExactly("text hi", "onClick false", "color 0", "modifier Modifier", "onEndChanges") text = "hello" assertThat(c.awaitSnapshot()).containsExactly("text hello", "onEndChanges") @@ -124,7 +123,7 @@ abstract class AbstractChangeListenerTest { Button("hi", onClick = null) Text(text) } - assertThat(c.awaitSnapshot()).containsExactly("text hi", "onClick false", "modifier Modifier", "onEndChanges") + assertThat(c.awaitSnapshot()).containsExactly("text hi", "onClick false", "color 0", "modifier Modifier", "onEndChanges") text = "hello" assertThat(c.awaitSnapshot()).isEmpty() @@ -146,7 +145,7 @@ abstract class AbstractChangeListenerTest { c.setContent { Button("hi", onClick = null, modifier = modifier) } - assertThat(c.awaitSnapshot()).containsExactly("text hi", "onClick false", "modifier Modifier", "onEndChanges") + assertThat(c.awaitSnapshot()).containsExactly("text hi", "onClick false", "color 0", "modifier Modifier", "onEndChanges") modifier = with(object : TestScope {}) { Modifier.accessibilityDescription("hey") @@ -171,7 +170,7 @@ abstract class AbstractChangeListenerTest { c.setContent { Button(text, onClick = null, modifier = modifier) } - assertThat(c.awaitSnapshot()).containsExactly("text hi", "onClick false", "modifier Modifier", "onEndChanges") + assertThat(c.awaitSnapshot()).containsExactly("text hi", "onClick false", "color 0", "modifier Modifier", "onEndChanges") text = "hello" modifier = with(object : TestScope {}) { diff --git a/redwood-compose/src/commonTest/kotlin/app/cash/redwood/compose/ListeningButton.kt b/redwood-compose/src/commonTest/kotlin/app/cash/redwood/compose/ListeningButton.kt index 9cf96ec279..42f9c648d7 100644 --- a/redwood-compose/src/commonTest/kotlin/app/cash/redwood/compose/ListeningButton.kt +++ b/redwood-compose/src/commonTest/kotlin/app/cash/redwood/compose/ListeningButton.kt @@ -39,6 +39,10 @@ class ListeningButton : changes += "onClick ${onClick != null}" } + override fun color(color: UInt) { + changes += "color $color" + } + override fun onEndChanges() { changes += "onEndChanges" } diff --git a/redwood-protocol-guest/api/redwood-protocol-guest.api b/redwood-protocol-guest/api/redwood-protocol-guest.api index 126eb48daa..ee53642f75 100644 --- a/redwood-protocol-guest/api/redwood-protocol-guest.api +++ b/redwood-protocol-guest/api/redwood-protocol-guest.api @@ -7,12 +7,12 @@ public final class app/cash/redwood/protocol/guest/DefaultProtocolBridge : app/c public fun appendModifierChange-z3jyS0k (ILjava/util/List;)V public fun appendMove-HpxY78w (IIIII)V public fun appendPropertyChange-DxQz5cw (IILkotlinx/serialization/KSerializer;Ljava/lang/Object;)V + public fun appendPropertyChange-M7EZMwg (III)V public fun appendPropertyChange-e3iP1vo (IIZ)V public fun appendRemove-HpxY78w (IIIILjava/util/List;)V public fun emitChanges ()V public fun getJson ()Lkotlinx/serialization/json/Json; - public fun getRoot ()Lapp/cash/redwood/protocol/guest/ProtocolWidgetChildren; - public synthetic fun getRoot ()Lapp/cash/redwood/widget/Widget$Children; + public fun getRoot ()Lapp/cash/redwood/widget/Widget$Children; public fun getSynthesizeSubtreeRemoval ()Z public fun getWidgetSystem ()Lapp/cash/redwood/widget/WidgetSystem; public fun initChangesSink (Lapp/cash/redwood/protocol/ChangesSink;)V @@ -28,6 +28,7 @@ public abstract interface class app/cash/redwood/protocol/guest/ProtocolBridge : public abstract fun appendModifierChange-z3jyS0k (ILjava/util/List;)V public abstract fun appendMove-HpxY78w (IIIII)V public abstract fun appendPropertyChange-DxQz5cw (IILkotlinx/serialization/KSerializer;Ljava/lang/Object;)V + public abstract fun appendPropertyChange-M7EZMwg (III)V public abstract fun appendPropertyChange-e3iP1vo (IIZ)V public abstract fun appendRemove-HpxY78w (IIIILjava/util/List;)V public static synthetic fun appendRemove-HpxY78w$default (Lapp/cash/redwood/protocol/guest/ProtocolBridge;IIIILjava/util/List;ILjava/lang/Object;)V diff --git a/redwood-protocol-guest/api/redwood-protocol-guest.klib.api b/redwood-protocol-guest/api/redwood-protocol-guest.klib.api index 0659381bed..a5ecedf53a 100644 --- a/redwood-protocol-guest/api/redwood-protocol-guest.klib.api +++ b/redwood-protocol-guest/api/redwood-protocol-guest.klib.api @@ -13,6 +13,7 @@ abstract interface app.cash.redwood.protocol.guest/ProtocolBridge : app.cash.red abstract fun appendModifierChange(app.cash.redwood.protocol/Id, kotlin.collections/List) // app.cash.redwood.protocol.guest/ProtocolBridge.appendModifierChange|appendModifierChange(app.cash.redwood.protocol.Id;kotlin.collections.List){}[0] abstract fun appendMove(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/ChildrenTag, kotlin/Int, kotlin/Int, kotlin/Int) // app.cash.redwood.protocol.guest/ProtocolBridge.appendMove|appendMove(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.ChildrenTag;kotlin.Int;kotlin.Int;kotlin.Int){}[0] abstract fun appendPropertyChange(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/PropertyTag, kotlin/Boolean) // app.cash.redwood.protocol.guest/ProtocolBridge.appendPropertyChange|appendPropertyChange(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.PropertyTag;kotlin.Boolean){}[0] + abstract fun appendPropertyChange(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/PropertyTag, kotlin/UInt) // app.cash.redwood.protocol.guest/ProtocolBridge.appendPropertyChange|appendPropertyChange(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.PropertyTag;kotlin.UInt){}[0] abstract fun appendRemove(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/ChildrenTag, kotlin/Int, kotlin/Int, kotlin.collections/List = ...) // app.cash.redwood.protocol.guest/ProtocolBridge.appendRemove|appendRemove(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.ChildrenTag;kotlin.Int;kotlin.Int;kotlin.collections.List){}[0] abstract fun emitChanges() // app.cash.redwood.protocol.guest/ProtocolBridge.emitChanges|emitChanges(){}[0] abstract fun initChangesSink(app.cash.redwood.protocol/ChangesSink) // app.cash.redwood.protocol.guest/ProtocolBridge.initChangesSink|initChangesSink(app.cash.redwood.protocol.ChangesSink){}[0] @@ -56,6 +57,7 @@ final class app.cash.redwood.protocol.guest/DefaultProtocolBridge : app.cash.red final fun appendModifierChange(app.cash.redwood.protocol/Id, kotlin.collections/List) // app.cash.redwood.protocol.guest/DefaultProtocolBridge.appendModifierChange|appendModifierChange(app.cash.redwood.protocol.Id;kotlin.collections.List){}[0] final fun appendMove(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/ChildrenTag, kotlin/Int, kotlin/Int, kotlin/Int) // app.cash.redwood.protocol.guest/DefaultProtocolBridge.appendMove|appendMove(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.ChildrenTag;kotlin.Int;kotlin.Int;kotlin.Int){}[0] final fun appendPropertyChange(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/PropertyTag, kotlin/Boolean) // app.cash.redwood.protocol.guest/DefaultProtocolBridge.appendPropertyChange|appendPropertyChange(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.PropertyTag;kotlin.Boolean){}[0] + final fun appendPropertyChange(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/PropertyTag, kotlin/UInt) // app.cash.redwood.protocol.guest/DefaultProtocolBridge.appendPropertyChange|appendPropertyChange(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.PropertyTag;kotlin.UInt){}[0] final fun appendRemove(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/ChildrenTag, kotlin/Int, kotlin/Int, kotlin.collections/List) // app.cash.redwood.protocol.guest/DefaultProtocolBridge.appendRemove|appendRemove(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.ChildrenTag;kotlin.Int;kotlin.Int;kotlin.collections.List){}[0] final fun emitChanges() // app.cash.redwood.protocol.guest/DefaultProtocolBridge.emitChanges|emitChanges(){}[0] final fun initChangesSink(app.cash.redwood.protocol/ChangesSink) // app.cash.redwood.protocol.guest/DefaultProtocolBridge.initChangesSink|initChangesSink(app.cash.redwood.protocol.ChangesSink){}[0] @@ -66,7 +68,7 @@ final class app.cash.redwood.protocol.guest/DefaultProtocolBridge : app.cash.red final val json // app.cash.redwood.protocol.guest/DefaultProtocolBridge.json|{}json[0] final fun (): kotlinx.serialization.json/Json // app.cash.redwood.protocol.guest/DefaultProtocolBridge.json.|(){}[0] final val root // app.cash.redwood.protocol.guest/DefaultProtocolBridge.root|{}root[0] - final fun (): app.cash.redwood.protocol.guest/ProtocolWidgetChildren // app.cash.redwood.protocol.guest/DefaultProtocolBridge.root.|(){}[0] + final fun (): app.cash.redwood.widget/Widget.Children // app.cash.redwood.protocol.guest/DefaultProtocolBridge.root.|(){}[0] final val synthesizeSubtreeRemoval // app.cash.redwood.protocol.guest/DefaultProtocolBridge.synthesizeSubtreeRemoval|{}synthesizeSubtreeRemoval[0] final fun (): kotlin/Boolean // app.cash.redwood.protocol.guest/DefaultProtocolBridge.synthesizeSubtreeRemoval.|(){}[0] final val widgetSystem // app.cash.redwood.protocol.guest/DefaultProtocolBridge.widgetSystem|{}widgetSystem[0] @@ -88,9 +90,3 @@ final val app.cash.redwood.protocol.guest/app_cash_redwood_protocol_guest_Defaul final val app.cash.redwood.protocol.guest/app_cash_redwood_protocol_guest_ProtocolWidgetChildren$stableprop // app.cash.redwood.protocol.guest/app_cash_redwood_protocol_guest_ProtocolWidgetChildren$stableprop|#static{}app_cash_redwood_protocol_guest_ProtocolWidgetChildren$stableprop[0] final val app.cash.redwood.protocol.guest/guestRedwoodVersion // app.cash.redwood.protocol.guest/guestRedwoodVersion|{}guestRedwoodVersion[0] final fun (): app.cash.redwood.protocol/RedwoodVersion // app.cash.redwood.protocol.guest/guestRedwoodVersion.|(){}[0] -// Targets: [js] -final val app.cash.redwood.protocol.guest/app_cash_redwood_protocol_guest_JsArray$stableprop // app.cash.redwood.protocol.guest/app_cash_redwood_protocol_guest_JsArray$stableprop|#static{}app_cash_redwood_protocol_guest_JsArray$stableprop[0] -// Targets: [js] -final val app.cash.redwood.protocol.guest/app_cash_redwood_protocol_guest_JsArrayList$stableprop // app.cash.redwood.protocol.guest/app_cash_redwood_protocol_guest_JsArrayList$stableprop|#static{}app_cash_redwood_protocol_guest_JsArrayList$stableprop[0] -// Targets: [js] -final val app.cash.redwood.protocol.guest/app_cash_redwood_protocol_guest_JsMap$stableprop // app.cash.redwood.protocol.guest/app_cash_redwood_protocol_guest_JsMap$stableprop|#static{}app_cash_redwood_protocol_guest_JsMap$stableprop[0] diff --git a/redwood-protocol-guest/build.gradle b/redwood-protocol-guest/build.gradle index 843935fb95..4a96329297 100644 --- a/redwood-protocol-guest/build.gradle +++ b/redwood-protocol-guest/build.gradle @@ -30,15 +30,6 @@ kotlin { implementation projects.testApp.schema.protocolGuest } } - nonJsMain { - dependsOn(commonMain) - } - jvmMain { - dependsOn(nonJsMain) - } - nativeMain { - dependsOn(nonJsMain) - } } } diff --git a/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/DefaultProtocolBridge.kt b/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/DefaultProtocolBridge.kt index b3419e63db..cceeb4bba8 100644 --- a/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/DefaultProtocolBridge.kt +++ b/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/DefaultProtocolBridge.kt @@ -29,13 +29,15 @@ import app.cash.redwood.protocol.PropertyChange import app.cash.redwood.protocol.PropertyTag import app.cash.redwood.protocol.RedwoodVersion import app.cash.redwood.protocol.WidgetTag +import app.cash.redwood.widget.Widget import app.cash.redwood.widget.WidgetSystem +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.KSerializer import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonPrimitive /** @suppress For generated code use only. */ -@RedwoodCodegenApi +@OptIn(RedwoodCodegenApi::class) public class DefaultProtocolBridge( public override val json: Json = Json.Default, hostVersion: RedwoodVersion, @@ -50,7 +52,7 @@ public class DefaultProtocolBridge( public override val widgetSystem: WidgetSystem = widgetSystemFactory.create(this, mismatchHandler) - public override val root: ProtocolWidgetChildren = + public override val root: Widget.Children = ProtocolWidgetChildren(Id.Root, ChildrenTag.Root, this) public override val synthesizeSubtreeRemoval: Boolean = hostVersion < RedwoodVersion("0.10.0-SNAPSHOT") @@ -100,6 +102,15 @@ public class DefaultProtocolBridge( changes.add(PropertyChange(id, tag, JsonPrimitive(value))) } + @OptIn(ExperimentalSerializationApi::class) + override fun appendPropertyChange( + id: Id, + tag: PropertyTag, + value: UInt, + ) { + changes.add(PropertyChange(id, tag, JsonPrimitive(value))) + } + public override fun appendModifierChange( id: Id, elements: List, diff --git a/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/ProtocolBridge.kt b/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/ProtocolBridge.kt index 91a84a3581..2a6482ccd2 100644 --- a/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/ProtocolBridge.kt +++ b/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/ProtocolBridge.kt @@ -36,8 +36,8 @@ import kotlinx.serialization.json.Json * * This interface is for generated code use only. */ -@RedwoodCodegenApi public interface ProtocolBridge : EventSink { + @RedwoodCodegenApi public val json: Json /** @@ -45,6 +45,7 @@ public interface ProtocolBridge : EventSink { * from the protocol map which leaked any child views of a removed node. We can work around this * on the guest side by synthesizing removes for every node in the subtree. */ + @RedwoodCodegenApi public val synthesizeSubtreeRemoval: Boolean /** @@ -64,13 +65,16 @@ public interface ProtocolBridge : EventSink { public fun emitChanges() + @RedwoodCodegenApi public fun nextId(): Id + @RedwoodCodegenApi public fun appendCreate( id: Id, tag: WidgetTag, ) + @RedwoodCodegenApi public fun appendPropertyChange( id: Id, tag: PropertyTag, @@ -78,17 +82,34 @@ public interface ProtocolBridge : EventSink { value: T, ) + @RedwoodCodegenApi public fun appendPropertyChange( id: Id, tag: PropertyTag, value: Boolean, ) + /** + * There's a bug in kotlinx.serialization where decodeFromDynamic() is broken for UInt values + * larger than MAX_INT. For example, 4294967295 is incorrectly encoded as -1. We work around that + * here by special casing that type. + * + * https://github.com/Kotlin/kotlinx.serialization/issues/2713 + */ + @RedwoodCodegenApi + public fun appendPropertyChange( + id: Id, + tag: PropertyTag, + value: UInt, + ) + + @RedwoodCodegenApi public fun appendModifierChange( id: Id, elements: List, ) + @RedwoodCodegenApi public fun appendAdd( id: Id, tag: ChildrenTag, @@ -96,6 +117,7 @@ public interface ProtocolBridge : EventSink { child: ProtocolWidget, ) + @RedwoodCodegenApi public fun appendMove( id: Id, tag: ChildrenTag, @@ -104,6 +126,7 @@ public interface ProtocolBridge : EventSink { count: Int, ) + @RedwoodCodegenApi public fun appendRemove( id: Id, tag: ChildrenTag, @@ -112,5 +135,6 @@ public interface ProtocolBridge : EventSink { removedIds: List = listOf(), ) + @RedwoodCodegenApi public fun removeWidget(id: Id) } diff --git a/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/ProtocolRedwoodComposition.kt b/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/ProtocolRedwoodComposition.kt index 106a63e257..5e693163ac 100644 --- a/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/ProtocolRedwoodComposition.kt +++ b/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/ProtocolRedwoodComposition.kt @@ -19,7 +19,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.MonotonicFrameClock import androidx.compose.runtime.saveable.SaveableStateRegistry -import app.cash.redwood.RedwoodCodegenApi import app.cash.redwood.compose.LocalWidgetVersion import app.cash.redwood.compose.RedwoodComposition import app.cash.redwood.ui.OnBackPressedDispatcher @@ -31,7 +30,6 @@ import kotlinx.coroutines.flow.StateFlow * @param scope A [CoroutineScope] whose [coroutineContext][kotlin.coroutines.CoroutineContext] * must have a [MonotonicFrameClock] key which is being ticked. */ -@OptIn(RedwoodCodegenApi::class) @Suppress("FunctionName") public fun ProtocolRedwoodComposition( scope: CoroutineScope, diff --git a/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/ProtocolWidgetSystemFactory.kt b/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/ProtocolWidgetSystemFactory.kt index e831cf8619..f9e30fa122 100644 --- a/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/ProtocolWidgetSystemFactory.kt +++ b/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/ProtocolWidgetSystemFactory.kt @@ -15,10 +15,8 @@ */ package app.cash.redwood.protocol.guest -import app.cash.redwood.RedwoodCodegenApi import app.cash.redwood.widget.WidgetSystem -@OptIn(RedwoodCodegenApi::class) public interface ProtocolWidgetSystemFactory { /** Create a new [WidgetSystem] connected to a host via [bridge]. */ public fun create( diff --git a/redwood-protocol-guest/src/commonTest/kotlin/app/cash/redwood/protocol/guest/ProtocolTest.kt b/redwood-protocol-guest/src/commonTest/kotlin/app/cash/redwood/protocol/guest/ProtocolTest.kt index 92e42e4c5c..cb9af77b9e 100644 --- a/redwood-protocol-guest/src/commonTest/kotlin/app/cash/redwood/protocol/guest/ProtocolTest.kt +++ b/redwood-protocol-guest/src/commonTest/kotlin/app/cash/redwood/protocol/guest/ProtocolTest.kt @@ -156,6 +156,8 @@ class ProtocolTest { PropertyChange(Id(1), PropertyTag(1), JsonPrimitive("hi")), // onClick PropertyChange(Id(1), PropertyTag(2), JsonPrimitive(false)), + // color + PropertyChange(Id(1), PropertyTag(3), JsonPrimitive(0u)), ModifierChange(Id(1)), ChildrenChange.Add(Id.Root, ChildrenTag.Root, Id(1), 0), // Button @@ -164,6 +166,8 @@ class ProtocolTest { PropertyChange(Id(2), PropertyTag(1), JsonPrimitive("hi")), // onClick PropertyChange(Id(2), PropertyTag(2), JsonPrimitive(true)), + // color + PropertyChange(Id(2), PropertyTag(3), JsonPrimitive(0u)), ModifierChange(Id(2)), ChildrenChange.Add(Id.Root, ChildrenTag.Root, Id(2), 1), // Button2 @@ -215,6 +219,8 @@ class ProtocolTest { PropertyChange(Id(1), PropertyTag(1), JsonPrimitive("state: 0")), // onClick PropertyChange(Id(1), PropertyTag(2), JsonPrimitive(true)), + // color + PropertyChange(Id(1), PropertyTag(3), JsonPrimitive(0u)), ModifierChange(Id(1)), ChildrenChange.Add(Id.Root, ChildrenTag.Root, Id(1), 0), ), diff --git a/redwood-protocol-guest/src/jsMain/kotlin/app/cash/redwood/protocol/guest/PlatformList.kt b/redwood-protocol-guest/src/jsMain/kotlin/app/cash/redwood/protocol/guest/PlatformList.kt deleted file mode 100644 index 6fd6c3878a..0000000000 --- a/redwood-protocol-guest/src/jsMain/kotlin/app/cash/redwood/protocol/guest/PlatformList.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (C) 2023 Square, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package app.cash.redwood.protocol.guest - -@JsName("Array") -internal external class JsArray { - @JsName("length") - val size: Int - - fun push(element: E) -} - -internal actual typealias PlatformList = JsArray - -internal actual inline fun PlatformList.add(element: E) { - push(element) -} - -internal actual inline fun PlatformList.asList(): List { - return JsArrayList(this) -} - -internal class JsArrayList( - private val storage: JsArray, -) : AbstractList(), - RandomAccess { - override val size: Int get() = storage.size - - override fun get(index: Int): E { - return storage.asDynamic()[index].unsafeCast() - } -} diff --git a/redwood-protocol-guest/src/jsMain/kotlin/app/cash/redwood/protocol/guest/PlatformMap.kt b/redwood-protocol-guest/src/jsMain/kotlin/app/cash/redwood/protocol/guest/PlatformMap.kt deleted file mode 100644 index 151a1933b1..0000000000 --- a/redwood-protocol-guest/src/jsMain/kotlin/app/cash/redwood/protocol/guest/PlatformMap.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (C) 2023 Square, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package app.cash.redwood.protocol.guest - -@JsName("Map") -internal external class JsMap { - operator fun get(key: K): V? - fun set(key: K, value: V) - fun has(key: K): Boolean - fun delete(key: K) -} - -internal actual typealias PlatformMap = JsMap - -internal actual inline operator fun PlatformMap.set(key: K, value: V) { - set(key, value) -} - -internal actual inline operator fun PlatformMap.contains(key: K): Boolean { - return has(key) -} - -internal actual inline fun PlatformMap.remove(key: K) { - delete(key) -} diff --git a/redwood-protocol-guest/src/nonJsMain/kotlin/app/cash/redwood/protocol/guest/PlatformList.kt b/redwood-protocol-guest/src/nonJsMain/kotlin/app/cash/redwood/protocol/guest/PlatformList.kt deleted file mode 100644 index 3f35d24b47..0000000000 --- a/redwood-protocol-guest/src/nonJsMain/kotlin/app/cash/redwood/protocol/guest/PlatformList.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (C) 2023 Square, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package app.cash.redwood.protocol.guest - -@Suppress( - // ArrayList itself aliases to j.u.ArrayList on JVM. - "ACTUAL_TYPE_ALIAS_NOT_TO_CLASS", - // https://youtrack.jetbrains.com/issue/KT-37316 - "ACTUAL_WITHOUT_EXPECT", -) -internal actual typealias PlatformList = ArrayList - -@Suppress("EXTENSION_SHADOWED_BY_MEMBER") // Not true in common. -internal actual inline fun PlatformList.add(element: E) { - add(element) -} - -@Suppress( - // Explicitly trying to be zero-overhead. - "NOTHING_TO_INLINE", - // Inline warning only happens on JVM source set. - "KotlinRedundantDiagnosticSuppress", -) -internal actual inline fun PlatformList.asList(): List { - return this -} diff --git a/redwood-protocol-guest/src/nonJsMain/kotlin/app/cash/redwood/protocol/guest/PlatformMap.kt b/redwood-protocol-guest/src/nonJsMain/kotlin/app/cash/redwood/protocol/guest/PlatformMap.kt deleted file mode 100644 index 0b199d1010..0000000000 --- a/redwood-protocol-guest/src/nonJsMain/kotlin/app/cash/redwood/protocol/guest/PlatformMap.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (C) 2023 Square, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package app.cash.redwood.protocol.guest - -@Suppress( - // LinkedHashMap itself aliases to j.u.LinkedHashMap on JVM. - "ACTUAL_TYPE_ALIAS_NOT_TO_CLASS", - // https://youtrack.jetbrains.com/issue/KT-37316 - "ACTUAL_WITHOUT_EXPECT", -) -internal actual typealias PlatformMap = LinkedHashMap - -internal actual inline operator fun PlatformMap.set(key: K, value: V) { - put(key, value) -} - -internal actual inline operator fun PlatformMap.contains(key: K): Boolean { - return containsKey(key) -} - -@Suppress("EXTENSION_SHADOWED_BY_MEMBER") // Not true in common. -internal actual inline fun PlatformMap.remove(key: K) { - remove(key) -} diff --git a/redwood-testing/src/commonMain/kotlin/app/cash/redwood/testing/toChangeList.kt b/redwood-testing/src/commonMain/kotlin/app/cash/redwood/testing/toChangeList.kt index b4757fd39e..7ff3b464d9 100644 --- a/redwood-testing/src/commonMain/kotlin/app/cash/redwood/testing/toChangeList.kt +++ b/redwood-testing/src/commonMain/kotlin/app/cash/redwood/testing/toChangeList.kt @@ -15,7 +15,6 @@ */ package app.cash.redwood.testing -import app.cash.redwood.RedwoodCodegenApi import app.cash.redwood.protocol.SnapshotChangeList import app.cash.redwood.protocol.guest.DefaultProtocolBridge import app.cash.redwood.protocol.guest.ProtocolWidgetSystemFactory @@ -26,7 +25,6 @@ import kotlinx.serialization.json.Json * Encode this snapshot of widget values into a list of changes which can be serialized and * later applied to the UI to recreate the structure and state. */ -@OptIn(RedwoodCodegenApi::class) public fun List.toChangeList( factory: ProtocolWidgetSystemFactory, json: Json = Json.Default, diff --git a/redwood-testing/src/commonTest/kotlin/app/cash/redwood/testing/ViewRecyclingTester.kt b/redwood-testing/src/commonTest/kotlin/app/cash/redwood/testing/ViewRecyclingTester.kt index 33ab82a044..e9aebdb6d8 100644 --- a/redwood-testing/src/commonTest/kotlin/app/cash/redwood/testing/ViewRecyclingTester.kt +++ b/redwood-testing/src/commonTest/kotlin/app/cash/redwood/testing/ViewRecyclingTester.kt @@ -44,10 +44,10 @@ import kotlinx.coroutines.coroutineScope * Like [TestSchemaTester], but this also hooks up Redwood's protocol mechanism. That's necessary * because view recycling is only implemented as a part of the host-side protocol. */ -@OptIn(RedwoodCodegenApi::class) class ViewRecyclingTester( coroutineScope: CoroutineScope, ) { + @OptIn(RedwoodCodegenApi::class) private val widgetProtocolFactory = TestSchemaProtocolFactory( widgetSystem = TestSchemaWidgetSystem( TestSchema = TestSchemaTestingWidgetFactory(), diff --git a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolGuestGeneration.kt b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolGuestGeneration.kt index 6caa7d3988..ff8a0825e2 100644 --- a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolGuestGeneration.kt +++ b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolGuestGeneration.kt @@ -50,6 +50,7 @@ import com.squareup.kotlinpoet.STRING import com.squareup.kotlinpoet.TypeName import com.squareup.kotlinpoet.TypeSpec import com.squareup.kotlinpoet.UNIT +import com.squareup.kotlinpoet.U_INT import com.squareup.kotlinpoet.joinToCode private val protocolViewType = UNIT @@ -278,21 +279,32 @@ internal fun generateProtocolWidget( when (trait) { is ProtocolProperty -> { val traitTypeName = trait.type.asTypeName() - val serializerId = serializerIds.computeIfAbsent(traitTypeName) { - nextSerializerId++ - } - addFunction( FunSpec.builder(trait.name) .addModifiers(OVERRIDE) .addParameter(trait.name, traitTypeName) - .addStatement( - "this.bridge.appendPropertyChange(this.id, %T(%L), serializer_%L, %N)", - Protocol.PropertyTag, - trait.tag, - serializerId, - trait.name, - ) + .apply { + // Work around https://github.com/Kotlin/kotlinx.serialization/issues/2713. + if (traitTypeName == U_INT) { + addStatement( + "this.bridge.appendPropertyChange(this.id, %T(%L), %N)", + Protocol.PropertyTag, + trait.tag, + trait.name, + ) + } else { + val serializerId = serializerIds.computeIfAbsent(traitTypeName) { + nextSerializerId++ + } + addStatement( + "this.bridge.appendPropertyChange(this.id, %T(%L), serializer_%L, %N)", + Protocol.PropertyTag, + trait.tag, + serializerId, + trait.name, + ) + } + } .build(), ) } diff --git a/redwood-treehouse-guest/build.gradle b/redwood-treehouse-guest/build.gradle index 5b182a653e..aba7f986bc 100644 --- a/redwood-treehouse-guest/build.gradle +++ b/redwood-treehouse-guest/build.gradle @@ -22,5 +22,12 @@ kotlin { api projects.redwoodTreehouseGuestCompose } } + commonTest { + dependencies { + implementation libs.assertk + implementation libs.kotlin.test + implementation projects.testApp.schema.protocolGuest + } + } } } diff --git a/redwood-treehouse-guest/src/commonMain/kotlin/app/cash/redwood/treehouse/ProtocolBridge.kt b/redwood-treehouse-guest/src/commonMain/kotlin/app/cash/redwood/treehouse/ProtocolBridge.kt new file mode 100644 index 0000000000..3470181a14 --- /dev/null +++ b/redwood-treehouse-guest/src/commonMain/kotlin/app/cash/redwood/treehouse/ProtocolBridge.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package app.cash.redwood.treehouse + +import app.cash.redwood.protocol.RedwoodVersion +import app.cash.redwood.protocol.guest.ProtocolBridge +import app.cash.redwood.protocol.guest.ProtocolMismatchHandler +import app.cash.redwood.protocol.guest.ProtocolWidgetSystemFactory +import kotlinx.serialization.json.Json + +internal expect fun ProtocolBridge( + json: Json, + hostVersion: RedwoodVersion, + widgetSystemFactory: ProtocolWidgetSystemFactory, + mismatchHandler: ProtocolMismatchHandler, +): ProtocolBridge diff --git a/redwood-treehouse-guest/src/commonMain/kotlin/app/cash/redwood/treehouse/treehouseCompose.kt b/redwood-treehouse-guest/src/commonMain/kotlin/app/cash/redwood/treehouse/treehouseCompose.kt index 5a2979bec2..c56bb8bd23 100644 --- a/redwood-treehouse-guest/src/commonMain/kotlin/app/cash/redwood/treehouse/treehouseCompose.kt +++ b/redwood-treehouse-guest/src/commonMain/kotlin/app/cash/redwood/treehouse/treehouseCompose.kt @@ -16,11 +16,9 @@ package app.cash.redwood.treehouse import androidx.compose.runtime.saveable.SaveableStateRegistry -import app.cash.redwood.RedwoodCodegenApi import app.cash.redwood.compose.RedwoodComposition import app.cash.redwood.protocol.Change import app.cash.redwood.protocol.EventSink -import app.cash.redwood.protocol.guest.DefaultProtocolBridge import app.cash.redwood.protocol.guest.ProtocolBridge import app.cash.redwood.protocol.guest.ProtocolRedwoodComposition import app.cash.redwood.ui.Cancellable @@ -40,11 +38,10 @@ import kotlinx.coroutines.plus /** * The Kotlin/JS side of a treehouse UI. */ -@OptIn(RedwoodCodegenApi::class) public fun TreehouseUi.asZiplineTreehouseUi( appLifecycle: StandardAppLifecycle, ): ZiplineTreehouseUi { - val bridge = DefaultProtocolBridge( + val bridge = ProtocolBridge( hostVersion = appLifecycle.hostProtocolVersion, json = appLifecycle.json, widgetSystemFactory = appLifecycle.protocolWidgetSystemFactory, @@ -53,7 +50,6 @@ public fun TreehouseUi.asZiplineTreehouseUi( return RedwoodZiplineTreehouseUi(appLifecycle, this, bridge) } -@OptIn(RedwoodCodegenApi::class) private class RedwoodZiplineTreehouseUi( private val appLifecycle: StandardAppLifecycle, private val treehouseUi: TreehouseUi, diff --git a/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/PlatformMap.kt b/redwood-treehouse-guest/src/jsMain/kotlin/app/cash/redwood/treehouse/JsArray.kt similarity index 59% rename from redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/PlatformMap.kt rename to redwood-treehouse-guest/src/jsMain/kotlin/app/cash/redwood/treehouse/JsArray.kt index 3008b51b6f..7396462599 100644 --- a/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/PlatformMap.kt +++ b/redwood-treehouse-guest/src/jsMain/kotlin/app/cash/redwood/treehouse/JsArray.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Square, Inc. + * Copyright (C) 2024 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,14 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package app.cash.redwood.protocol.guest +package app.cash.redwood.treehouse -internal expect class PlatformMap() { - operator fun get(key: K): V? -} +@JsName("Array") +internal external class JsArray { + @JsName("length") + val length: Int -internal expect inline operator fun PlatformMap.set(key: K, value: V) + fun push(element: E) +} -internal expect inline operator fun PlatformMap.contains(key: K): Boolean +internal operator fun JsArray.get(index: Int): E { + return asDynamic()[index].unsafeCast() +} -internal expect inline fun PlatformMap.remove(key: K) +internal fun JsArray.clear() { + asDynamic().splice(0, length) +} diff --git a/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/PlatformList.kt b/redwood-treehouse-guest/src/jsMain/kotlin/app/cash/redwood/treehouse/JsMap.kt similarity index 63% rename from redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/PlatformList.kt rename to redwood-treehouse-guest/src/jsMain/kotlin/app/cash/redwood/treehouse/JsMap.kt index 41fe609c2c..ebb64521ff 100644 --- a/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/PlatformList.kt +++ b/redwood-treehouse-guest/src/jsMain/kotlin/app/cash/redwood/treehouse/JsMap.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Square, Inc. + * Copyright (C) 2024 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,13 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package app.cash.redwood.protocol.guest +package app.cash.redwood.treehouse -@Suppress("unused") // Type parameter used by extensions. -internal expect class PlatformList() { - val size: Int +@JsName("Map") +internal external class JsMap { + operator fun get(key: K): V? + fun set(key: K, value: V) + fun has(key: K): Boolean + fun delete(key: K) } - -internal expect inline fun PlatformList.add(element: E) - -internal expect inline fun PlatformList.asList(): List diff --git a/redwood-treehouse-guest/src/jsMain/kotlin/app/cash/redwood/treehouse/ProtocolBridgeJs.kt b/redwood-treehouse-guest/src/jsMain/kotlin/app/cash/redwood/treehouse/ProtocolBridgeJs.kt new file mode 100644 index 0000000000..f82c25df9c --- /dev/null +++ b/redwood-treehouse-guest/src/jsMain/kotlin/app/cash/redwood/treehouse/ProtocolBridgeJs.kt @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2024 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package app.cash.redwood.treehouse + +import app.cash.redwood.RedwoodCodegenApi +import app.cash.redwood.protocol.Change +import app.cash.redwood.protocol.ChangesSink +import app.cash.redwood.protocol.ChildrenTag +import app.cash.redwood.protocol.Event +import app.cash.redwood.protocol.Id +import app.cash.redwood.protocol.ModifierElement +import app.cash.redwood.protocol.PropertyTag +import app.cash.redwood.protocol.RedwoodVersion +import app.cash.redwood.protocol.WidgetTag +import app.cash.redwood.protocol.guest.ProtocolBridge +import app.cash.redwood.protocol.guest.ProtocolMismatchHandler +import app.cash.redwood.protocol.guest.ProtocolWidget +import app.cash.redwood.protocol.guest.ProtocolWidgetChildren +import app.cash.redwood.protocol.guest.ProtocolWidgetSystemFactory +import app.cash.redwood.widget.WidgetSystem +import app.cash.zipline.asDynamicFunction +import app.cash.zipline.sourceType +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.encodeToDynamic + +internal actual fun ProtocolBridge( + json: Json, + hostVersion: RedwoodVersion, + widgetSystemFactory: ProtocolWidgetSystemFactory, + mismatchHandler: ProtocolMismatchHandler, +): ProtocolBridge = FastProtocolBridge(json, hostVersion, widgetSystemFactory, mismatchHandler) + +@OptIn(ExperimentalSerializationApi::class, RedwoodCodegenApi::class) +internal class FastProtocolBridge( + override val json: Json = Json.Default, + hostVersion: RedwoodVersion, + widgetSystemFactory: ProtocolWidgetSystemFactory, + private val mismatchHandler: ProtocolMismatchHandler = ProtocolMismatchHandler.Throwing, +) : ProtocolBridge { + private var nextValue = Id.Root.value + 1 + private val widgets = JsMap() + private val changes = JsArray() + private lateinit var changesSinkService: ChangesSinkService + private lateinit var sendChanges: (service: ChangesSinkService, args: Array<*>) -> Any? + + override val widgetSystem: WidgetSystem = + widgetSystemFactory.create(this, mismatchHandler) + + override val root: ProtocolWidgetChildren = + ProtocolWidgetChildren(Id.Root, ChildrenTag.Root, this) + + override val synthesizeSubtreeRemoval: Boolean = hostVersion < RedwoodVersion("0.10.0-SNAPSHOT") + + override fun sendEvent(event: Event) { + val node = widgets[event.id.value] + if (node != null) { + node.sendEvent(event) + } else { + mismatchHandler.onUnknownEventNode(event.id, event.tag) + } + } + + override fun initChangesSink(changesSink: ChangesSink) { + val changesSinkService = changesSink as ChangesSinkService + initChangesSink( + changesSinkService = changesSinkService, + sendChanges = changesSinkService.sourceType!!.functions + .single { "sendChanges" in it.signature } + .asDynamicFunction(), + ) + } + + internal fun initChangesSink( + changesSinkService: ChangesSinkService, + sendChanges: (service: ChangesSinkService, args: Array<*>) -> Any?, + ) { + this.changesSinkService = changesSinkService + this.sendChanges = sendChanges + } + + override fun nextId(): Id { + val value = nextValue + nextValue = value + 1 + return Id(value) + } + + override fun appendCreate( + id: Id, + tag: WidgetTag, + ) { + val id = id + val tag = tag + changes.push(js("""["create",{"id":id,"tag":tag}]""")) + } + + override fun appendPropertyChange( + id: Id, + tag: PropertyTag, + serializer: KSerializer, + value: T, + ) { + val id = id + val tag = tag + val encodedValue = value?.let { json.encodeToDynamic(serializer, it) } + changes.push(js("""["property",{"id":id,"tag":tag,"value":encodedValue}]""")) + } + + override fun appendPropertyChange( + id: Id, + tag: PropertyTag, + value: Boolean, + ) { + val id = id + val tag = tag + val value = value + changes.push(js("""["property",{"id":id,"tag":tag,"value":value}]""")) + } + + override fun appendPropertyChange(id: Id, tag: PropertyTag, value: UInt) { + val id = id + val tag = tag + val value = value.toDouble() + changes.push(js("""["property",{"id":id,"tag":tag,"value":value}]""")) + } + + override fun appendModifierChange( + id: Id, + elements: List, + ) { + val id = id + val elements = Json.encodeToDynamic(elements) + changes.push(js("""["modifier",{"id":id,"elements":elements}]""")) + } + + override fun appendAdd( + id: Id, + tag: ChildrenTag, + index: Int, + child: ProtocolWidget, + ) { + check(!widgets.has(child.id.value)) { + "Attempted to add widget with ID ${child.id} but one already exists" + } + widgets.set(child.id.value, child) + + val id = id + val tag = tag + val childId = child.id + val index = index + changes.push(js("""["add",{"id":id,"tag":tag,"childId":childId,"index":index}]""")) + } + + override fun appendMove( + id: Id, + tag: ChildrenTag, + fromIndex: Int, + toIndex: Int, + count: Int, + ) { + val id = id + val tag = tag + val fromIndex = fromIndex + val toIndex = toIndex + val count = count + changes.push(js("""["move",{"id":id,"tag":tag,"fromIndex":fromIndex,"toIndex":toIndex,"count":count}]""")) + } + + override fun appendRemove( + id: Id, + tag: ChildrenTag, + index: Int, + count: Int, + removedIds: List, + ) { + val id = id + val tag = tag + val index = index + val count = count + val removedIds = Json.encodeToDynamic(removedIds) + changes.push(js("""["remove",{"id":id,"tag":tag,"index":index,"count":count,"removedIds":removedIds}]""")) + } + + override fun emitChanges() { + sendChanges(changesSinkService, arrayOf(changes)) + changes.clear() + } + + override fun removeWidget(id: Id) { + widgets.delete(id.value) + } +} diff --git a/redwood-treehouse-guest/src/jsTest/kotlin/app/cash/redwood/treehouse/FastProtocolBridgeTest.kt b/redwood-treehouse-guest/src/jsTest/kotlin/app/cash/redwood/treehouse/FastProtocolBridgeTest.kt new file mode 100644 index 0000000000..0b560df94f --- /dev/null +++ b/redwood-treehouse-guest/src/jsTest/kotlin/app/cash/redwood/treehouse/FastProtocolBridgeTest.kt @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2024 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package app.cash.redwood.treehouse + +import app.cash.redwood.Modifier +import app.cash.redwood.RedwoodCodegenApi +import app.cash.redwood.protocol.Change +import app.cash.redwood.protocol.guest.DefaultProtocolBridge +import app.cash.redwood.protocol.guest.ProtocolMismatchHandler +import app.cash.redwood.protocol.guest.guestRedwoodVersion +import app.cash.redwood.widget.Widget +import assertk.assertThat +import assertk.assertions.isEqualTo +import com.example.redwood.testapp.compose.backgroundColor +import com.example.redwood.testapp.protocol.guest.TestSchemaProtocolWidgetSystemFactory +import com.example.redwood.testapp.widget.TestSchemaWidgetSystem +import kotlin.test.Test +import kotlin.time.Duration +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.descriptors.PrimitiveKind.STRING +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromDynamic +import kotlinx.serialization.modules.SerializersModule + +/** + * Confirm that [FastProtocolBridge] behaves the same as [DefaultProtocolBridge]. + */ +@OptIn(ExperimentalSerializationApi::class, RedwoodCodegenApi::class) +class FastProtocolBridgeTest { + @Test fun consistentWithDefaultProtocolBridge() { + assertChangesEqual { root, widgetSystem -> + val button = widgetSystem.TestSchema.Button() + button.onClick { error("unexpected call") } + root.insert(0, button) + button.text("Click Me") + + val textInput = widgetSystem.TestSchema.TextInput() + root.insert(1, textInput) + textInput.modifier = Modifier.backgroundColor(0xff0000u) + textInput.text("hello") + + root.move(0, 1, 1) + root.remove(0, 2) + } + } + + /** Test our special case for https://github.com/Kotlin/kotlinx.serialization/issues/2713 */ + @Test fun consistentWithDefaultProtocolBridgeForUint() { + assertChangesEqual { root, widgetSystem -> + val button = widgetSystem.TestSchema.Button() + button.color(0xffeeddccu) + } + } + + private fun assertChangesEqual( + block: (Widget.Children, TestSchemaWidgetSystem) -> Unit, + ) { + val json = Json { + useArrayPolymorphism = true + serializersModule = SerializersModule { + contextual(Duration::class, DurationIsoSerializer) + contextual(UInt::class, UInt.serializer()) + } + } + + val fastUpdates = collectChangesFromFastProtocolBridge(json, block) + val defaultUpdates = collectChangesFromDefaultProtocolBridge(json, block) + assertThat(fastUpdates).isEqualTo(defaultUpdates) + } + + private fun collectChangesFromDefaultProtocolBridge( + json: Json, + block: (Widget.Children, TestSchemaWidgetSystem) -> Unit, + ): List { + val bridge = DefaultProtocolBridge( + // Use latest guest version as the host version to avoid any compatibility behavior. + hostVersion = guestRedwoodVersion, + widgetSystemFactory = TestSchemaProtocolWidgetSystemFactory, + json = json, + mismatchHandler = ProtocolMismatchHandler.Throwing, + ) + + val result = mutableListOf() + bridge.initChangesSink( + object : ChangesSinkService { + override fun sendChanges(changes: List) { + result += changes + } + }, + ) + + block( + bridge.root, + bridge.widgetSystem as TestSchemaWidgetSystem, + ) + bridge.emitChanges() + + return result + } + + private fun collectChangesFromFastProtocolBridge( + json: Json, + block: (Widget.Children, TestSchemaWidgetSystem) -> Unit, + ): List { + val bridge = FastProtocolBridge( + // Use latest guest version as the host version to avoid any compatibility behavior. + hostVersion = guestRedwoodVersion, + widgetSystemFactory = TestSchemaProtocolWidgetSystemFactory, + json = json, + mismatchHandler = ProtocolMismatchHandler.Throwing, + ) + + val result = mutableListOf() + bridge.initChangesSink( + changesSinkService = object : ChangesSinkService { + override fun sendChanges(changes: List) { + } + }, + sendChanges = { _, args -> + result += json.decodeFromDynamic>(args.single()) + Unit + }, + ) + + block( + bridge.root, + bridge.widgetSystem as TestSchemaWidgetSystem, + ) + bridge.emitChanges() + + return result + } + + object DurationIsoSerializer : KSerializer { + override val descriptor get() = PrimitiveSerialDescriptor("Duration", STRING) + override fun serialize(encoder: Encoder, value: Duration) = encoder.encodeString(value.toIsoString()) + + override fun deserialize(decoder: Decoder): Duration { + return Duration.parseIsoString(decoder.decodeString()) + } + } +} diff --git a/redwood-treehouse-guest/src/jvmMain/kotlin/app/cash/redwood/treehouse/ProtocolBridgeJvm.kt b/redwood-treehouse-guest/src/jvmMain/kotlin/app/cash/redwood/treehouse/ProtocolBridgeJvm.kt new file mode 100644 index 0000000000..fc4c047df0 --- /dev/null +++ b/redwood-treehouse-guest/src/jvmMain/kotlin/app/cash/redwood/treehouse/ProtocolBridgeJvm.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package app.cash.redwood.treehouse + +import app.cash.redwood.protocol.RedwoodVersion +import app.cash.redwood.protocol.guest.DefaultProtocolBridge +import app.cash.redwood.protocol.guest.ProtocolBridge +import app.cash.redwood.protocol.guest.ProtocolMismatchHandler +import app.cash.redwood.protocol.guest.ProtocolWidgetSystemFactory +import kotlinx.serialization.json.Json + +internal actual fun ProtocolBridge( + json: Json, + hostVersion: RedwoodVersion, + widgetSystemFactory: ProtocolWidgetSystemFactory, + mismatchHandler: ProtocolMismatchHandler, +): ProtocolBridge = DefaultProtocolBridge(json, hostVersion, widgetSystemFactory, mismatchHandler) diff --git a/redwood-treehouse-host/src/commonTest/kotlin/app/cash/redwood/treehouse/FakeZiplineTreehouseUi.kt b/redwood-treehouse-host/src/commonTest/kotlin/app/cash/redwood/treehouse/FakeZiplineTreehouseUi.kt index 0dba8f222d..19f350385d 100644 --- a/redwood-treehouse-host/src/commonTest/kotlin/app/cash/redwood/treehouse/FakeZiplineTreehouseUi.kt +++ b/redwood-treehouse-host/src/commonTest/kotlin/app/cash/redwood/treehouse/FakeZiplineTreehouseUi.kt @@ -54,6 +54,7 @@ class FakeZiplineTreehouseUi( Create(widgetId, WidgetTag(4)), // Button. PropertyChange(widgetId, PropertyTag(1), JsonPrimitive(label)), // text. PropertyChange(widgetId, PropertyTag(2), JsonPrimitive(true)), // onClick. + PropertyChange(widgetId, PropertyTag(3), JsonPrimitive(0u)), // color. ChildrenChange.Add(Id.Root, ChildrenTag.Root, widgetId, 0), ), ) diff --git a/test-app/android-views/src/main/kotlin/com/example/redwood/testapp/android/views/ViewButton.kt b/test-app/android-views/src/main/kotlin/com/example/redwood/testapp/android/views/ViewButton.kt index 879141e536..3460975ec8 100644 --- a/test-app/android-views/src/main/kotlin/com/example/redwood/testapp/android/views/ViewButton.kt +++ b/test-app/android-views/src/main/kotlin/com/example/redwood/testapp/android/views/ViewButton.kt @@ -38,4 +38,7 @@ internal class ViewButton( }, ) } + + override fun color(color: UInt) { + } } diff --git a/test-app/browser/src/commonMain/kotlin/com/example/redwood/testapp/browser/widgets.kt b/test-app/browser/src/commonMain/kotlin/com/example/redwood/testapp/browser/widgets.kt index bd6989a93d..62ffa4615a 100644 --- a/test-app/browser/src/commonMain/kotlin/com/example/redwood/testapp/browser/widgets.kt +++ b/test-app/browser/src/commonMain/kotlin/com/example/redwood/testapp/browser/widgets.kt @@ -48,4 +48,7 @@ class HtmlButton( null } } + + override fun color(color: UInt) { + } } diff --git a/test-app/ios-uikit/TestApp/ButtonBinding.swift b/test-app/ios-uikit/TestApp/ButtonBinding.swift index 920efdd836..3bae0c8b0b 100644 --- a/test-app/ios-uikit/TestApp/ButtonBinding.swift +++ b/test-app/ios-uikit/TestApp/ButtonBinding.swift @@ -36,7 +36,7 @@ class ButtonBinding: Button { // this function will update the bounds and trigger relayout in the parent. root.sizeToFit() } - + func onClick(onClick: (() -> Void)? = nil) { self.onClick = onClick if (onClick != nil) { @@ -45,7 +45,10 @@ class ButtonBinding: Button { root.removeTarget(self, action: #selector(clicked), for: .touchUpInside) } } - + + func color(color: UInt32) { + } + @objc func clicked() { self.onClick?() } diff --git a/test-app/schema/src/main/kotlin/com/example/redwood/testapp/schema.kt b/test-app/schema/src/main/kotlin/com/example/redwood/testapp/schema.kt index e550d6cd3c..88180665ac 100644 --- a/test-app/schema/src/main/kotlin/com/example/redwood/testapp/schema.kt +++ b/test-app/schema/src/main/kotlin/com/example/redwood/testapp/schema.kt @@ -81,6 +81,7 @@ public data class Text( public data class Button( @Property(1) val text: String?, @Property(2) val onClick: (() -> Unit)?, + @Property(3) @Default("0u") val color: UInt, ) /** Like [Button] but with a required lambda. */