Skip to content

Commit a8a0c0c

Browse files
committed
Interpret List and grid nodes
List renders a LazyColumn fetching each visible row through itemNode, wrapped in a PullToRefreshBox when refreshable; grids render LazyVerticalGrid/ LazyHorizontalGrid with fixed or adaptive tracks.
1 parent dcd95f2 commit a8a0c0c

3 files changed

Lines changed: 78 additions & 0 deletions

File tree

Demo/swiftui/src/commonMain/kotlin/com/pureswift/swiftui/Render.kt

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ import androidx.compose.animation.slideInHorizontally
2121
import androidx.compose.animation.slideOutHorizontally
2222
import androidx.compose.foundation.layout.Column as LayoutColumn
2323
import androidx.compose.foundation.layout.fillMaxSize
24+
import androidx.compose.foundation.layout.height
25+
import androidx.compose.foundation.lazy.LazyColumn
26+
import androidx.compose.foundation.lazy.grid.GridCells
27+
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
28+
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
2429
import androidx.compose.material3.AlertDialog
2530
import androidx.compose.material3.Button
2631
import androidx.compose.material3.CircularProgressIndicator
@@ -33,6 +38,7 @@ import androidx.compose.material3.Icon
3338
import androidx.compose.material3.IconButton
3439
import androidx.compose.material3.ModalBottomSheet
3540
import androidx.compose.material3.NavigationBar
41+
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
3642
import androidx.compose.material3.NavigationBarItem
3743
import androidx.compose.material3.OutlinedTextField
3844
import androidx.compose.material3.Scaffold
@@ -187,6 +193,12 @@ fun Render(node: ViewNode) {
187193

188194
"TabView" -> RenderTabView(node)
189195

196+
"List" -> RenderList(node)
197+
198+
"LazyVGrid" -> RenderGrid(node, vertical = true)
199+
200+
"LazyHGrid" -> RenderGrid(node, vertical = false)
201+
190202
"EmptyView" -> Unit
191203

192204
"Composable" -> ComposableRegistry.Render(node)
@@ -509,3 +521,63 @@ private fun ViewNode.stringArray(key: String): List<String> {
509521
val arr = props[key] as? kotlinx.serialization.json.JsonArray ?: return emptyList()
510522
return arr.mapNotNull { (it as? kotlinx.serialization.json.JsonPrimitive)?.content }
511523
}
524+
525+
526+
@OptIn(ExperimentalMaterial3Api::class)
527+
@Composable
528+
private fun RenderList(node: ViewNode) {
529+
val provider = node.long("itemProvider") ?: return
530+
val count = node.count ?: 0
531+
val keys = node.stringArray("keys")
532+
val onRefresh = node.long("onRefresh")
533+
val list: @Composable () -> Unit = {
534+
LazyColumn(modifier = Modifier.fillMaxSize()) {
535+
items(count = count, key = { keys.getOrNull(it) ?: it }) { index ->
536+
// one JNI call per newly-visible row; re-fetched when the tree
537+
// (hence the provider id) changes
538+
val row = remember(provider, index) { SwiftBridge.sink.itemNode(provider, index) }
539+
row?.let { Render(it) }
540+
}
541+
}
542+
}
543+
if (onRefresh != null) {
544+
var refreshing by remember { mutableStateOf(false) }
545+
PullToRefreshBox(
546+
isRefreshing = refreshing,
547+
onRefresh = {
548+
refreshing = true
549+
SwiftBridge.sink.invokeVoid(onRefresh)
550+
},
551+
) { list() }
552+
// clear the indicator once the tree updates (new content arrived)
553+
LaunchedEffect(node.count) { refreshing = false }
554+
} else {
555+
list()
556+
}
557+
}
558+
559+
@Composable
560+
private fun RenderGrid(node: ViewNode, vertical: Boolean) {
561+
val spacing = (node.double("spacing") ?: 8.0).dp
562+
val cells = node.double("adaptiveMin")?.let { GridCells.Adaptive(it.dp) }
563+
?: GridCells.Fixed(node.long("trackCount")?.toInt() ?: 1)
564+
if (vertical) {
565+
LazyVerticalGrid(
566+
columns = cells,
567+
verticalArrangement = Arrangement.spacedBy(spacing),
568+
horizontalArrangement = Arrangement.spacedBy(spacing),
569+
modifier = node.composeModifiers(),
570+
) {
571+
items(node.children.size) { Render(node.children[it]) }
572+
}
573+
} else {
574+
LazyHorizontalGrid(
575+
rows = cells,
576+
verticalArrangement = Arrangement.spacedBy(spacing),
577+
horizontalArrangement = Arrangement.spacedBy(spacing),
578+
modifier = node.composeModifiers(),
579+
) {
580+
items(node.children.size) { Render(node.children[it]) }
581+
}
582+
}
583+
}

Demo/swiftui/src/commonMain/kotlin/com/pureswift/swiftui/TreeStore.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ interface CallbackSink {
3333
fun invokeDouble(id: Long, value: Double)
3434
fun invokeInt(id: Long, value: Int)
3535
fun invokeString(id: Long, value: String)
36+
/// Resolves a lazy row on demand; the logging default has no rows.
37+
fun itemNode(id: Long, index: Int): ViewNode? = null
3638
}
3739

3840
/// Global event sink used by the interpreter. Defaults to a logger.
@@ -46,5 +48,6 @@ object SwiftBridge {
4648
override fun invokeDouble(id: Long, value: Double) = println("SwiftBridge.invokeDouble($id, $value)")
4749
override fun invokeInt(id: Long, value: Int) = println("SwiftBridge.invokeInt($id, $value)")
4850
override fun invokeString(id: Long, value: String) = println("SwiftBridge.invokeString($id, $value)")
51+
override fun itemNode(id: Long, index: Int): ViewNode? = null
4952
}
5053
}

Demo/swiftui/src/jvmShared/kotlin/com/pureswift/swiftui/SwiftCallbackSink.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,7 @@ class SwiftCallbackSink : CallbackSink {
1616
external override fun invokeInt(id: Long, value: Int)
1717

1818
external override fun invokeString(id: Long, value: String)
19+
20+
// A lazy row query: returns the materialized row subtree, or null.
21+
external override fun itemNode(id: Long, index: Int): ViewNode?
1922
}

0 commit comments

Comments
 (0)