Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Demo/App.swiftpm/Sources/Catalog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ struct CatalogEntry: Identifiable {

static let all: [CatalogEntry] = [
CatalogEntry(id: "text", title: "Text", screen: AnyCatalogScreen(TextPlayground())),
CatalogEntry(id: "style", title: "Styling", screen: AnyCatalogScreen(StylePlayground())),
CatalogEntry(id: "button", title: "Button", screen: AnyCatalogScreen(ButtonPlayground())),
CatalogEntry(id: "toggle", title: "Toggle", screen: AnyCatalogScreen(TogglePlayground())),
CatalogEntry(id: "slider", title: "Slider", screen: AnyCatalogScreen(SliderPlayground())),
Expand Down
53 changes: 53 additions & 0 deletions Demo/App.swiftpm/Sources/StylePlaygrounds.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#if canImport(AndroidSwiftUI)
import AndroidSwiftUI
#else
import SwiftUI
#endif

struct StylePlayground: View {
@State private var taps = 0
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 0) {
Example("Inherited font") {
VStack(alignment: .leading, spacing: 6) {
Text("All three")
Text("inherit the")
Text("title font")
}
.font(.title)
}
Example("Inherited color") {
VStack(alignment: .leading, spacing: 6) {
Text("Both lines are")
Text("blue from the container")
}
.foregroundColor(.blue)
}
Example("Child overrides parent") {
VStack(alignment: .leading, spacing: 6) {
Text("Title by inheritance")
Text("Caption overrides").font(.caption)
}
.font(.title)
}
Example("Font and color together") {
VStack(alignment: .leading, spacing: 6) {
Text("Headline weight")
Text("and purple color")
}
.font(.headline)
.foregroundColor(.purple)
}
Example("Inherited disabled") {
VStack(alignment: .leading, spacing: 8) {
Text("Both buttons are disabled (taps: \(taps))")
Button("First") { taps += 1 }
Button("Second") { taps += 1 }
}
.disabled(true)
}
}
}
}
}
50 changes: 45 additions & 5 deletions Demo/swiftui/src/commonMain/kotlin/com/pureswift/swiftui/Render.kt
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,15 @@ internal data class AnimSpec(val curve: String, val durationMs: Int)

internal val LocalAnimationSpec = compositionLocalOf<AnimSpec?> { null }

// Inherited style environment (SwiftUI's `.font`/`.foregroundColor`/`.disabled`
// set values for a whole subtree). A node folds its own such modifiers into
// these before rendering its children; leaves consume them as defaults that
// their own modifiers still override.
internal val LocalInheritedFontSize = compositionLocalOf { TextUnit.Unspecified }
internal val LocalInheritedFontWeight = compositionLocalOf<FontWeight?> { null }
internal val LocalInheritedColor = compositionLocalOf { Color.Unspecified }
internal val LocalInheritedDisabled = compositionLocalOf { false }

/// Interprets a Swift-evaluated node tree into Material 3 composables.
///
/// One `when` per node type; unknown types render a diagnostic so schema
Expand All @@ -152,7 +161,36 @@ fun Render(node: ViewNode) {
val spec = node.string("animationCurve")?.let {
AnimSpec(it, (node.double("animationDurationMs") ?: 350.0).toInt())
} ?: LocalAnimationSpec.current
CompositionLocalProvider(LocalAnimationSpec provides spec) { RenderResolved(node) }

// Fold this node's own style modifiers into the inherited environment so
// its subtree sees them.
var fontSize = LocalInheritedFontSize.current
var fontWeight = LocalInheritedFontWeight.current
var color = LocalInheritedColor.current
var disabled = LocalInheritedDisabled.current
for (m in node.modifiers) {
when (m.kind) {
"font" -> {
m.args.string("style")?.let {
fontSize = fontSizeForStyle(it).sp
fontWeight = defaultWeightForStyle(it)
}
m.args.double("size")?.let { fontSize = it.sp }
m.args.string("weight")?.let { fontWeight = fontWeightFor(it) }
}
"fontWeight" -> m.args.string("weight")?.let { fontWeight = fontWeightFor(it) }
"foregroundColor" -> m.args.long("color")?.let { color = Color(it.toInt()) }
"disabled" -> if ((m.args["value"] as? kotlinx.serialization.json.JsonPrimitive)?.content == "true") disabled = true
}
}

CompositionLocalProvider(
LocalAnimationSpec provides spec,
LocalInheritedFontSize provides fontSize,
LocalInheritedFontWeight provides fontWeight,
LocalInheritedColor provides color,
LocalInheritedDisabled provides disabled,
) { RenderResolved(node) }
}

@Composable
Expand All @@ -165,8 +203,9 @@ private fun RenderResolved(node: ViewNode) {
"Button" -> {
val onTap = node.long("onTap")
Button(
// own disabled and any inherited `.disabled` both apply
onClick = { onTap?.let { SwiftBridge.sink.invokeVoid(it) } },
enabled = !node.isDisabled(),
enabled = !node.isDisabled() && !LocalInheritedDisabled.current,
modifier = node.composeModifiers(),
) {
RenderChildren(node)
Expand Down Expand Up @@ -744,9 +783,10 @@ private fun materialIcon(name: String): ImageVector? = when (name) {
// attributes. Layout modifiers in the same chain still apply via composeModifiers().
@Composable
private fun RenderText(node: ViewNode) {
var color = Color.Unspecified
var fontSize: TextUnit = TextUnit.Unspecified
var weight: FontWeight? = null
// Start from the inherited environment; the node's own modifiers override.
var color = LocalInheritedColor.current
var fontSize: TextUnit = LocalInheritedFontSize.current
var weight: FontWeight? = LocalInheritedFontWeight.current
var fontStyle: FontStyle? = null
var maxLines = Int.MAX_VALUE
var textAlign: TextAlign? = null
Expand Down
Loading