Skip to content

Commit 4312ee2

Browse files
committed
Render shapes, gradients, and mapped SF Symbols
1 parent dde4ed5 commit 4312ee2

1 file changed

Lines changed: 124 additions & 1 deletion

File tree

  • Demo/swiftui/src/commonMain/kotlin/com/pureswift/swiftui

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

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,33 @@ import androidx.compose.material3.TextButton
5050
import androidx.compose.material3.TopAppBar
5151
import androidx.compose.material.icons.Icons
5252
import 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
5380
import androidx.compose.runtime.Composable
5481
import androidx.compose.runtime.DisposableEffect
5582
import androidx.compose.runtime.LaunchedEffect
@@ -65,7 +92,11 @@ import androidx.compose.ui.draw.alpha
6592
import androidx.compose.ui.draw.clip
6693
import androidx.compose.ui.draw.rotate
6794
import androidx.compose.ui.draw.scale
95+
import androidx.compose.ui.graphics.Brush
6896
import 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
69100
import androidx.compose.ui.text.font.FontStyle
70101
import androidx.compose.ui.text.font.FontWeight
71102
import 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) {
372407
private 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

Comments
 (0)