|
1 | 1 | package com.pureswift.swiftui |
2 | 2 |
|
3 | 3 | import androidx.compose.foundation.background |
| 4 | +import androidx.compose.foundation.clickable |
4 | 5 | import androidx.compose.foundation.rememberScrollState |
5 | 6 | import androidx.compose.foundation.horizontalScroll |
6 | 7 | import androidx.compose.foundation.verticalScroll |
@@ -50,6 +51,7 @@ import androidx.compose.material3.TopAppBar |
50 | 51 | import androidx.compose.material.icons.Icons |
51 | 52 | import androidx.compose.material.icons.automirrored.filled.ArrowBack |
52 | 53 | import androidx.compose.runtime.Composable |
| 54 | +import androidx.compose.runtime.DisposableEffect |
53 | 55 | import androidx.compose.runtime.LaunchedEffect |
54 | 56 | import androidx.compose.runtime.getValue |
55 | 57 | import androidx.compose.runtime.key |
@@ -83,13 +85,15 @@ import androidx.compose.foundation.shape.RoundedCornerShape |
83 | 85 | @Composable |
84 | 86 | fun Render(node: ViewNode) { |
85 | 87 | key(node.id) { |
| 88 | + RenderEffects(node) |
86 | 89 | when (node.type) { |
87 | 90 | "Text" -> RenderText(node) |
88 | 91 |
|
89 | 92 | "Button" -> { |
90 | 93 | val onTap = node.long("onTap") |
91 | 94 | Button( |
92 | 95 | onClick = { onTap?.let { SwiftBridge.sink.invokeVoid(it) } }, |
| 96 | + enabled = !node.isDisabled(), |
93 | 97 | modifier = node.composeModifiers(), |
94 | 98 | ) { |
95 | 99 | RenderChildren(node) |
@@ -334,6 +338,40 @@ private fun zStackAlignment(node: ViewNode): Alignment { |
334 | 338 | } |
335 | 339 | } |
336 | 340 |
|
| 341 | +// Lifecycle modifiers (onAppear/onDisappear/task/onChange) install Compose |
| 342 | +// effects rather than folding into the Modifier chain. Keyed within the node's |
| 343 | +// `key(id)` scope, so onAppear fires once on entry and onDisappear on exit, |
| 344 | +// surviving recomposition. |
| 345 | +@Composable |
| 346 | +private fun RenderEffects(node: ViewNode) { |
| 347 | + val onAppear = node.modifiers.firstOrNull { it.kind == "onAppear" }?.args?.long("action") |
| 348 | + val onDisappear = node.modifiers.firstOrNull { it.kind == "onDisappear" }?.args?.long("action") |
| 349 | + val task = node.modifiers.firstOrNull { it.kind == "task" }?.args?.long("action") |
| 350 | + val onChange = node.modifiers.firstOrNull { it.kind == "onChange" } |
| 351 | + |
| 352 | + if (onAppear != null || onDisappear != null) { |
| 353 | + DisposableEffect(Unit) { |
| 354 | + onAppear?.let { SwiftBridge.sink.invokeVoid(it) } |
| 355 | + onDispose { onDisappear?.let { SwiftBridge.sink.invokeVoid(it) } } |
| 356 | + } |
| 357 | + } |
| 358 | + if (task != null) { |
| 359 | + LaunchedEffect(Unit) { SwiftBridge.sink.invokeVoid(task) } |
| 360 | + } |
| 361 | + if (onChange != null) { |
| 362 | + val token = onChange.args.string("token") |
| 363 | + val action = onChange.args.long("action") |
| 364 | + // skip the initial composition; fire when the token changes thereafter |
| 365 | + var primed by remember(node.id) { mutableStateOf(false) } |
| 366 | + LaunchedEffect(token) { |
| 367 | + if (primed) action?.let { SwiftBridge.sink.invokeVoid(it) } else primed = true |
| 368 | + } |
| 369 | + } |
| 370 | +} |
| 371 | + |
| 372 | +private fun ViewNode.isDisabled(): Boolean = |
| 373 | + modifiers.any { it.kind == "disabled" && (it.args["value"] as? kotlinx.serialization.json.JsonPrimitive)?.content == "true" } |
| 374 | + |
337 | 375 | // Text-styling modifiers describe attributes of the Text composable itself, |
338 | 376 | // not the layout Modifier chain, so they are read off the node here and passed |
339 | 377 | // as `Text(...)` parameters. Later (innermost) entries win for scalar |
@@ -465,6 +503,18 @@ internal fun ViewNode.composeModifiers(): Modifier { |
465 | 503 |
|
466 | 504 | "opacity" -> modifier.alpha((entry.args.double("opacity") ?: 1.0).toFloat()) |
467 | 505 |
|
| 506 | + "onTapGesture" -> { |
| 507 | + val id = entry.args.long("action") |
| 508 | + if (id != null) modifier.clickable { SwiftBridge.sink.invokeVoid(id) } else modifier |
| 509 | + } |
| 510 | + |
| 511 | + // Dim disabled content; controls also drop interactivity via their |
| 512 | + // own `enabled` parameter (e.g. Button). |
| 513 | + "disabled" -> { |
| 514 | + val off = (entry.args["value"] as? kotlinx.serialization.json.JsonPrimitive)?.content == "true" |
| 515 | + if (off) modifier.alpha(0.38f) else modifier |
| 516 | + } |
| 517 | + |
468 | 518 | else -> modifier // unknown modifier: ignore, never crash rendering |
469 | 519 | } |
470 | 520 | } |
|
0 commit comments