@@ -21,6 +21,11 @@ import androidx.compose.animation.slideInHorizontally
2121import androidx.compose.animation.slideOutHorizontally
2222import androidx.compose.foundation.layout.Column as LayoutColumn
2323import 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
2429import androidx.compose.material3.AlertDialog
2530import androidx.compose.material3.Button
2631import androidx.compose.material3.CircularProgressIndicator
@@ -33,6 +38,7 @@ import androidx.compose.material3.Icon
3338import androidx.compose.material3.IconButton
3439import androidx.compose.material3.ModalBottomSheet
3540import androidx.compose.material3.NavigationBar
41+ import androidx.compose.material3.pulltorefresh.PullToRefreshBox
3642import androidx.compose.material3.NavigationBarItem
3743import androidx.compose.material3.OutlinedTextField
3844import 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+ }
0 commit comments