@@ -50,6 +50,33 @@ import androidx.compose.material3.TextButton
5050import androidx.compose.material3.TopAppBar
5151import androidx.compose.material.icons.Icons
5252import androidx.compose.material.icons.automirrored.filled.ArrowBack
53+ import androidx.compose.material.icons.filled.Add
54+ import androidx.compose.material.icons.filled.Check
55+ import androidx.compose.material.icons.filled.Close
56+ import androidx.compose.material.icons.filled.DateRange
57+ import androidx.compose.material.icons.filled.Delete
58+ import androidx.compose.material.icons.filled.Email
59+ import androidx.compose.material.icons.filled.Face
60+ import androidx.compose.material.icons.filled.Favorite
61+ import androidx.compose.material.icons.filled.Home
62+ import androidx.compose.material.icons.filled.Info
63+ import androidx.compose.material.icons.filled.LocationOn
64+ import androidx.compose.material.icons.filled.Lock
65+ import androidx.compose.material.icons.filled.Menu
66+ import androidx.compose.material.icons.filled.MoreVert
67+ import androidx.compose.material.icons.filled.Notifications
68+ import androidx.compose.material.icons.filled.Person
69+ import androidx.compose.material.icons.filled.Phone
70+ import androidx.compose.material.icons.filled.PlayArrow
71+ import androidx.compose.material.icons.filled.Refresh
72+ import androidx.compose.material.icons.filled.Search
73+ import androidx.compose.material.icons.filled.Settings
74+ import androidx.compose.material.icons.filled.Share
75+ import androidx.compose.material.icons.filled.ShoppingCart
76+ import androidx.compose.material.icons.filled.Star
77+ import androidx.compose.material.icons.filled.ThumbUp
78+ import androidx.compose.material.icons.filled.Warning
79+ import androidx.compose.material3.LocalContentColor
5380import androidx.compose.runtime.Composable
5481import androidx.compose.runtime.DisposableEffect
5582import androidx.compose.runtime.LaunchedEffect
@@ -65,7 +92,11 @@ import androidx.compose.ui.draw.alpha
6592import androidx.compose.ui.draw.clip
6693import androidx.compose.ui.draw.rotate
6794import androidx.compose.ui.draw.scale
95+ import androidx.compose.ui.graphics.Brush
6896import androidx.compose.ui.graphics.Color
97+ import androidx.compose.ui.graphics.RectangleShape
98+ import androidx.compose.ui.graphics.vector.ImageVector
99+ import androidx.compose.foundation.shape.CircleShape
69100import androidx.compose.ui.text.font.FontStyle
70101import androidx.compose.ui.text.font.FontWeight
71102import androidx.compose.ui.text.style.TextAlign
@@ -163,7 +194,11 @@ fun Render(node: ViewNode) {
163194 .background(Color ((node.long(" color" ) ? : 0 ).toInt()))
164195 )
165196
166- " Image" -> Text (" [${node.string(" name" ) ? : " image" } ]" , modifier = node.composeModifiers())
197+ " Image" -> RenderImage (node)
198+
199+ " Shape" -> RenderShape (node)
200+
201+ " LinearGradient" -> RenderGradient (node)
167202
168203 " ProgressView" -> {
169204 val value = node.double(" value" )
@@ -372,6 +407,94 @@ private fun RenderEffects(node: ViewNode) {
372407private fun ViewNode.isDisabled (): Boolean =
373408 modifiers.any { it.kind == " disabled" && (it.args[" value" ] as ? kotlinx.serialization.json.JsonPrimitive )?.content == " true" }
374409
410+ // A shape fills its (frame-derived) size with its fill color; without a frame
411+ // it collapses to zero, matching this backend's no-layout-engine limitation.
412+ @Composable
413+ private fun RenderShape (node : ViewNode ) {
414+ val fill = node.long(" fill" )?.let { Color (it.toInt()) } ? : Color (0xFF000000 )
415+ val shape = when (node.string(" shape" )) {
416+ " circle" -> CircleShape
417+ " capsule" -> RoundedCornerShape (percent = 50 )
418+ " roundedRectangle" -> RoundedCornerShape ((node.double(" cornerRadius" ) ? : 0.0 ).dp)
419+ else -> RectangleShape
420+ }
421+ Box (modifier = node.composeModifiers().background(fill, shape))
422+ }
423+
424+ // Horizontal/vertical when the endpoints share an axis, else a diagonal linear
425+ // gradient across the frame.
426+ @Composable
427+ private fun RenderGradient (node : ViewNode ) {
428+ val colors = node.colorList(" colors" ).ifEmpty { listOf (Color .Transparent , Color .Transparent ) }
429+ val startX = node.double(" startX" ) ? : 0.0
430+ val startY = node.double(" startY" ) ? : 0.5
431+ val endX = node.double(" endX" ) ? : 1.0
432+ val endY = node.double(" endY" ) ? : 0.5
433+ val brush = when {
434+ startY == endY -> Brush .horizontalGradient(colors)
435+ startX == endX -> Brush .verticalGradient(colors)
436+ else -> Brush .linearGradient(colors)
437+ }
438+ // A gradient fills the available width by default (SwiftUI semantics); an
439+ // explicit frame width in the chain still constrains it. fillMaxWidth is
440+ // first so a cornerRadius clip in the chain sees the full width.
441+ Box (modifier = Modifier .fillMaxWidth().then(node.composeModifiers()).background(brush))
442+ }
443+
444+ @Composable
445+ private fun RenderImage (node : ViewNode ) {
446+ val icon = node.string(" systemName" )?.let { materialIcon(it) }
447+ if (icon != null ) {
448+ val tint = node.modifiers.firstOrNull { it.kind == " foregroundColor" }
449+ ?.args?.long(" color" )?.let { Color (it.toInt()) }
450+ Icon (
451+ imageVector = icon,
452+ contentDescription = node.string(" systemName" ),
453+ tint = tint ? : LocalContentColor .current,
454+ modifier = node.composeModifiers(),
455+ )
456+ } else {
457+ Text (" [${node.string(" name" ) ? : " image" } ]" , modifier = node.composeModifiers())
458+ }
459+ }
460+
461+ private fun ViewNode.colorList (key : String ): List <Color > {
462+ val arr = props[key] as ? kotlinx.serialization.json.JsonArray ? : return emptyList()
463+ return arr.mapNotNull { (it as ? kotlinx.serialization.json.JsonPrimitive )?.content?.toLongOrNull()?.let { c -> Color (c.toInt()) } }
464+ }
465+
466+ // A curated SF Symbol → Material icon map; unknown names fall back to the
467+ // placeholder text so gaps stay visible.
468+ private fun materialIcon (name : String ): ImageVector ? = when (name) {
469+ " star" , " star.fill" -> Icons .Filled .Star
470+ " heart" , " heart.fill" -> Icons .Filled .Favorite
471+ " trash" , " trash.fill" -> Icons .Filled .Delete
472+ " person" , " person.fill" -> Icons .Filled .Person
473+ " gear" , " gearshape" , " gearshape.fill" -> Icons .Filled .Settings
474+ " house" , " house.fill" -> Icons .Filled .Home
475+ " magnifyingglass" -> Icons .Filled .Search
476+ " plus" -> Icons .Filled .Add
477+ " checkmark" -> Icons .Filled .Check
478+ " xmark" -> Icons .Filled .Close
479+ " bell" , " bell.fill" -> Icons .Filled .Notifications
480+ " envelope" , " envelope.fill" -> Icons .Filled .Email
481+ " phone" , " phone.fill" -> Icons .Filled .Phone
482+ " lock" , " lock.fill" -> Icons .Filled .Lock
483+ " cart" , " cart.fill" -> Icons .Filled .ShoppingCart
484+ " hand.thumbsup" , " hand.thumbsup.fill" -> Icons .Filled .ThumbUp
485+ " info" , " info.circle" -> Icons .Filled .Info
486+ " exclamationmark.triangle" -> Icons .Filled .Warning
487+ " square.and.arrow.up" -> Icons .Filled .Share
488+ " line.3.horizontal" -> Icons .Filled .Menu
489+ " ellipsis" -> Icons .Filled .MoreVert
490+ " play" , " play.fill" -> Icons .Filled .PlayArrow
491+ " arrow.clockwise" -> Icons .Filled .Refresh
492+ " calendar" -> Icons .Filled .DateRange
493+ " face.smiling" -> Icons .Filled .Face
494+ " location" , " location.fill" -> Icons .Filled .LocationOn
495+ else -> null
496+ }
497+
375498// Text-styling modifiers describe attributes of the Text composable itself,
376499// not the layout Modifier chain, so they are read off the node here and passed
377500// as `Text(...)` parameters. Later (innermost) entries win for scalar
0 commit comments