Skip to content

Commit

Permalink
Create a widget protocol descriptor type (#2343)
Browse files Browse the repository at this point in the history
This has knowledge of the child IDs for this widget as well as the ability to create instances of it.

In the future this will also hold more information such as property IDs so that we can deserialize them on the Zipline thread.
  • Loading branch information
JakeWharton authored Oct 1, 2024
1 parent 26d5056 commit 0d795cf
Show file tree
Hide file tree
Showing 11 changed files with 183 additions and 147 deletions.
10 changes: 7 additions & 3 deletions redwood-protocol-host/api/redwood-protocol-host.api
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
public abstract interface class app/cash/redwood/protocol/host/GeneratedProtocolFactory : app/cash/redwood/protocol/host/ProtocolFactory {
public abstract interface class app/cash/redwood/protocol/host/GeneratedHostProtocol : app/cash/redwood/protocol/host/ProtocolFactory {
public abstract fun createModifier (Lapp/cash/redwood/protocol/ModifierElement;)Lapp/cash/redwood/Modifier;
public abstract fun createNode-kyz2zXs (II)Lapp/cash/redwood/protocol/host/ProtocolNode;
public abstract fun widgetChildren-WCEpcRY (I)[I
public abstract fun widget-WCEpcRY (I)Lapp/cash/redwood/protocol/host/WidgetHostProtocol;
}

public final class app/cash/redwood/protocol/host/HostProtocolAdapter : app/cash/redwood/protocol/ChangesSink {
Expand Down Expand Up @@ -69,3 +68,8 @@ public final class app/cash/redwood/protocol/host/VersionKt {
public static final fun getHostRedwoodVersion ()Ljava/lang/String;
}

public abstract interface class app/cash/redwood/protocol/host/WidgetHostProtocol {
public abstract fun createNode-ou3jOuA (I)Lapp/cash/redwood/protocol/host/ProtocolNode;
public abstract fun getChildrenTags ()[I
}

14 changes: 10 additions & 4 deletions redwood-protocol-host/api/redwood-protocol-host.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,16 @@ abstract fun interface app.cash.redwood.protocol.host/UiEventSink { // app.cash.
abstract fun sendEvent(app.cash.redwood.protocol.host/UiEvent) // app.cash.redwood.protocol.host/UiEventSink.sendEvent|sendEvent(app.cash.redwood.protocol.host.UiEvent){}[0]
}

abstract interface <#A: kotlin/Any> app.cash.redwood.protocol.host/GeneratedProtocolFactory : app.cash.redwood.protocol.host/ProtocolFactory<#A> { // app.cash.redwood.protocol.host/GeneratedProtocolFactory|null[0]
abstract fun createModifier(app.cash.redwood.protocol/ModifierElement): app.cash.redwood/Modifier // app.cash.redwood.protocol.host/GeneratedProtocolFactory.createModifier|createModifier(app.cash.redwood.protocol.ModifierElement){}[0]
abstract fun createNode(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/WidgetTag): app.cash.redwood.protocol.host/ProtocolNode<#A>? // app.cash.redwood.protocol.host/GeneratedProtocolFactory.createNode|createNode(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.WidgetTag){}[0]
abstract fun widgetChildren(app.cash.redwood.protocol/WidgetTag): kotlin/IntArray? // app.cash.redwood.protocol.host/GeneratedProtocolFactory.widgetChildren|widgetChildren(app.cash.redwood.protocol.WidgetTag){}[0]
abstract interface <#A: kotlin/Any> app.cash.redwood.protocol.host/GeneratedHostProtocol : app.cash.redwood.protocol.host/ProtocolFactory<#A> { // app.cash.redwood.protocol.host/GeneratedHostProtocol|null[0]
abstract fun createModifier(app.cash.redwood.protocol/ModifierElement): app.cash.redwood/Modifier // app.cash.redwood.protocol.host/GeneratedHostProtocol.createModifier|createModifier(app.cash.redwood.protocol.ModifierElement){}[0]
abstract fun widget(app.cash.redwood.protocol/WidgetTag): app.cash.redwood.protocol.host/WidgetHostProtocol<#A>? // app.cash.redwood.protocol.host/GeneratedHostProtocol.widget|widget(app.cash.redwood.protocol.WidgetTag){}[0]
}

abstract interface <#A: kotlin/Any> app.cash.redwood.protocol.host/WidgetHostProtocol { // app.cash.redwood.protocol.host/WidgetHostProtocol|null[0]
abstract val childrenTags // app.cash.redwood.protocol.host/WidgetHostProtocol.childrenTags|{}childrenTags[0]
abstract fun <get-childrenTags>(): kotlin/IntArray? // app.cash.redwood.protocol.host/WidgetHostProtocol.childrenTags.<get-childrenTags>|<get-childrenTags>(){}[0]

abstract fun createNode(app.cash.redwood.protocol/Id): app.cash.redwood.protocol.host/ProtocolNode<#A> // app.cash.redwood.protocol.host/WidgetHostProtocol.createNode|createNode(app.cash.redwood.protocol.Id){}[0]
}

abstract interface app.cash.redwood.protocol.host/ProtocolMismatchHandler { // app.cash.redwood.protocol.host/ProtocolMismatchHandler|null[0]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public class HostProtocolAdapter<W : Any>(
private val leakDetector: LeakDetector,
) : ChangesSink {
private val factory = when (factory) {
is GeneratedProtocolFactory -> factory
is GeneratedHostProtocol -> factory
}

private val nodes =
Expand All @@ -83,7 +83,8 @@ public class HostProtocolAdapter<W : Any>(
val id = change.id
when (change) {
is Create -> {
val node = factory.createNode(id, change.tag) ?: continue
val widgetProtocol = factory.widget(change.tag) ?: continue
val node = widgetProtocol.createNode(id)
val old = nodes.put(change.id.value, node)
require(old == null) {
"Insert attempted to replace existing widget with ID ${change.id.value}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,16 @@ import app.cash.redwood.protocol.host.HostProtocolAdapter.ReuseNode
*/
@OptIn(RedwoodCodegenApi::class)
internal fun shapesEqual(
factory: GeneratedProtocolFactory<*>,
factory: GeneratedHostProtocol<*>,
a: ReuseNode<*>,
b: ProtocolNode<*>,
): Boolean {
if (!a.eligibleForReuse) return false // This node is ineligible.
if (a.widgetTag == UnknownWidgetTag) return false // No 'Create' for this.
if (b.widgetTag != a.widgetTag) return false // Widget types don't match.

val widgetChildren = factory.widgetChildren(a.widgetTag)
val widgetChildren = factory.widget(a.widgetTag)
?.childrenTags
?: return true // Widget has no children.

return widgetChildren.all { childrenTag ->
Expand All @@ -59,7 +60,7 @@ internal fun shapesEqual(
*/
@OptIn(RedwoodCodegenApi::class)
private fun childrenEqual(
factory: GeneratedProtocolFactory<*>,
factory: GeneratedHostProtocol<*>,
aChildren: List<ReuseNode<*>>,
bChildren: List<ProtocolNode<*>>,
childrenTag: ChildrenTag,
Expand All @@ -81,15 +82,15 @@ private fun childrenEqual(
/** Returns a hash of this node, or 0L if this node isn't eligible for reuse. */
@OptIn(RedwoodCodegenApi::class)
internal fun shapeHash(
factory: GeneratedProtocolFactory<*>,
factory: GeneratedHostProtocol<*>,
node: ReuseNode<*>,
): Long {
if (!node.eligibleForReuse) return 0L // This node is ineligible.
if (node.widgetTag == UnknownWidgetTag) return 0L // No 'Create' for this.

var result = node.widgetTag.value.toLong()

factory.widgetChildren(node.widgetTag)?.forEach { childrenTag ->
factory.widget(node.widgetTag)?.childrenTags?.forEach { childrenTag ->
result = (result * 37L) + childrenTag
var childCount = 0
for (child in node.children) {
Expand All @@ -106,11 +107,11 @@ internal fun shapeHash(
/** Returns the same hash as [shapeHash], but on an already-built [ProtocolNode]. */
@OptIn(RedwoodCodegenApi::class)
internal fun shapeHash(
factory: GeneratedProtocolFactory<*>,
factory: GeneratedHostProtocol<*>,
node: ProtocolNode<*>,
): Long {
var result = node.widgetTag.value.toLong()
factory.widgetChildren(node.widgetTag)?.forEach { childrenTag ->
factory.widget(node.widgetTag)?.childrenTags?.forEach { childrenTag ->
result = (result * 37L) + childrenTag
val children = node.children(ChildrenTag(childrenTag))
?: return@forEach // This acts like a 'continue'.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,20 @@ public sealed interface ProtocolFactory<W : Any> {
}

/**
* [ProtocolFactory] but containing codegen APIs.
* [ProtocolFactory] but containing codegen APIs for a schema.
*
* @suppress
*/
@RedwoodCodegenApi
public interface GeneratedProtocolFactory<W : Any> : ProtocolFactory<W> {
public interface GeneratedHostProtocol<W : Any> : ProtocolFactory<W> {
/**
* Create a new protocol node with [id] of the specified [tag].
* Look up host protocol information for a widget with the given [tag].
*
* Invalid [tag] values can either produce an exception or result in `null` being returned.
* If `null` is returned, the caller should make every effort to ignore this node and
* continue executing.
*/
public fun createNode(id: Id, tag: WidgetTag): ProtocolNode<W>?
public fun widget(tag: WidgetTag): WidgetHostProtocol<W>?

/**
* Create a new modifier from the specified [element].
Expand All @@ -57,12 +57,22 @@ public interface GeneratedProtocolFactory<W : Any> : ProtocolFactory<W> {
* or result in the unit [`Modifier`][Modifier.Companion] being returned.
*/
public fun createModifier(element: ModifierElement): Modifier
}

/**
* Protocol APIs for a widget definition.
*
* @suppress
*/
@RedwoodCodegenApi
public interface WidgetHostProtocol<W : Any> {
/** Create an instance of this widget wrapped as a [ProtocolNode] with the given [id]. */
public fun createNode(id: Id): ProtocolNode<W>

/**
* Look up known children tags for the given widget [tag]. These are stored as a bare [IntArray]
* for efficiency, but are otherwise an array of [ChildrenTag] instances.
*
* @return `null` when widget has no children
* Look up known children tags for this widget. These are stored as a bare [IntArray]
* for efficiency, but are otherwise an array of [ChildrenTag] instances. A value of
* `null` indicates no children.
*/
public fun widgetChildren(tag: WidgetTag): IntArray?
public val childrenTags: IntArray?
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class ProtocolFactoryTest {
)

val t = assertFailsWith<IllegalArgumentException> {
factory.createNode(Id(1), WidgetTag(345432))
factory.widget(WidgetTag(345432))
}
assertThat(t).hasMessage("Unknown widget tag 345432")
}
Expand All @@ -76,8 +76,7 @@ class ProtocolFactoryTest {
mismatchHandler = handler,
)

assertThat(factory.createNode(Id(1), WidgetTag(345432))).isNull()

assertThat(factory.widget(WidgetTag(345432))).isNull()
assertThat(handler.events.single()).isEqualTo("Unknown widget 345432")
}

Expand Down Expand Up @@ -216,7 +215,7 @@ class ProtocolFactoryTest {
RedwoodLazyLayout = RedwoodLazyLayoutTestingWidgetFactory(),
),
)
val button = factory.createNode(Id(1), WidgetTag(4))!!
val button = factory.widget(WidgetTag(4))!!.createNode(Id(1))

val t = assertFailsWith<IllegalArgumentException> {
button.children(ChildrenTag(345432))
Expand All @@ -235,7 +234,7 @@ class ProtocolFactoryTest {
mismatchHandler = handler,
)

val button = factory.createNode(Id(1), WidgetTag(4))!!
val button = factory.widget(WidgetTag(4))!!.createNode(Id(1))
assertThat(button.children(ChildrenTag(345432))).isNull()

assertThat(handler.events.single()).isEqualTo("Unknown children 345432 for 4")
Expand All @@ -255,7 +254,7 @@ class ProtocolFactoryTest {
),
json = json,
)
val textInput = factory.createNode(Id(1), WidgetTag(5))!!
val textInput = factory.widget(WidgetTag(5))!!.createNode(Id(1))

val throwingEventSink = UiEventSink { error(it) }
textInput.apply(PropertyChange(Id(1), PropertyTag(2), JsonPrimitive("PT10S")), throwingEventSink)
Expand All @@ -271,7 +270,7 @@ class ProtocolFactoryTest {
RedwoodLazyLayout = RedwoodLazyLayoutTestingWidgetFactory(),
),
)
val button = factory.createNode(Id(1), WidgetTag(4))!!
val button = factory.widget(WidgetTag(4))!!.createNode(Id(1))

val change = PropertyChange(Id(1), PropertyTag(345432))
val eventSink = UiEventSink { throw UnsupportedOperationException() }
Expand All @@ -291,7 +290,7 @@ class ProtocolFactoryTest {
),
mismatchHandler = handler,
)
val button = factory.createNode(Id(1), WidgetTag(4))!!
val button = factory.widget(WidgetTag(4))!!.createNode(Id(1))

button.apply(PropertyChange(Id(1), PropertyTag(345432))) { throw UnsupportedOperationException() }

Expand All @@ -312,7 +311,7 @@ class ProtocolFactoryTest {
),
json = json,
)
val textInput = factory.createNode(Id(1), WidgetTag(5))!!
val textInput = factory.widget(WidgetTag(5))!!.createNode(Id(1))

val eventSink = RecordingUiEventSink()
textInput.apply(PropertyChange(Id(1), PropertyTag(4), JsonPrimitive(true)), eventSink)
Expand Down
Loading

0 comments on commit 0d795cf

Please sign in to comment.