Skip to content

Commit

Permalink
Create a widget protocol descriptor type
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 committed Sep 30, 2024
1 parent 7934954 commit 48c6985
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 105 deletions.
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 48c6985

Please sign in to comment.