Skip to content

Commit e12fb09

Browse files
authored
Merge pull request #76 from PureSwift/feature/desktop-custom-views
Support custom compose views in the desktop rig
2 parents 579da62 + c049d0f commit e12fb09

4 files changed

Lines changed: 116 additions & 8 deletions

File tree

‎Demo/App.swiftpm/Sources/Catalog.swift‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,9 @@ struct CatalogEntry: Identifiable {
6969
CatalogEntry(id: "form", title: "Form", screen: AnyCatalogScreen(FormPlayground())),
7070
CatalogEntry(id: "modifier", title: "Modifiers", screen: AnyCatalogScreen(ModifierPlayground())),
7171
]
72-
// Android-only: native-view interop through the composable registry
73-
#if !DESKTOP_RIG
72+
// Custom views through the composable registry — cross-platform: each
73+
// host (Android app, desktop rig) registers its own factories.
7474
entries.append(CatalogEntry(id: "representable", title: "Custom Views", screen: AnyCatalogScreen(RepresentablePlayground())))
75-
#endif
7675
entries += [
7776
CatalogEntry(id: "appearance", title: "Appearance", screen: AnyCatalogScreen(AppearancePlayground())),
7877
CatalogEntry(id: "animation", title: "Animation", screen: AnyCatalogScreen(AnimationPlayground())),
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package com.pureswift.swiftui.desktop
2+
3+
import androidx.compose.foundation.gestures.detectDragGestures
4+
import androidx.compose.foundation.gestures.detectTapGestures
5+
import androidx.compose.foundation.layout.Box
6+
import androidx.compose.foundation.layout.padding
7+
import androidx.compose.foundation.layout.size
8+
import androidx.compose.material3.Text
9+
import androidx.compose.runtime.Composable
10+
import androidx.compose.ui.Alignment
11+
import androidx.compose.ui.Modifier
12+
import androidx.compose.ui.draw.drawBehind
13+
import androidx.compose.ui.draw.drawWithContent
14+
import androidx.compose.ui.geometry.CornerRadius
15+
import androidx.compose.ui.graphics.Color
16+
import androidx.compose.ui.graphics.PathEffect
17+
import androidx.compose.ui.graphics.drawscope.Stroke
18+
import androidx.compose.ui.graphics.drawscope.clipRect
19+
import androidx.compose.ui.input.pointer.pointerInput
20+
import androidx.compose.ui.text.TextStyle
21+
import androidx.compose.ui.text.style.TextAlign
22+
import androidx.compose.ui.unit.dp
23+
import androidx.compose.ui.unit.sp
24+
import com.pureswift.swiftui.ComposableRegistry
25+
import kotlin.math.roundToInt
26+
27+
/// Registers the desktop rig's custom composables into the interpreter's
28+
/// registry — the desktop counterpart to the Android demo's `registerDemo
29+
/// Composables`. Called once before the first render. It shows that the
30+
/// composable registry is a cross-platform extension point: `DashedBorder` is
31+
/// the identical pure-Compose function used on Android, and `RatingBar` is a
32+
/// pure Compose-Multiplatform reimplementation of what the Android demo bridges
33+
/// to `android.widget.RatingBar`.
34+
fun registerDemoComposables() {
35+
36+
// A pure Compose-Multiplatform star rating — no native widget. Tapping or
37+
// dragging across the stars reports a half-step rating back to Swift; the
38+
// `rating` prop drives the fill so two-way binding round-trips.
39+
ComposableRegistry.register("RatingBar") { props, _ ->
40+
val rating = props.float("rating") ?: 0f
41+
val max = props.int("max") ?: 5
42+
val onChanged = props.doubleAction("onRatingChanged")
43+
44+
// Maps a horizontal position within the row to the nearest half-star.
45+
fun report(x: Float, width: Int) {
46+
if (width <= 0 || onChanged == null) return
47+
val fraction = (x / width).coerceIn(0f, 1f)
48+
val stepped = (fraction * max * 2).roundToInt() / 2.0
49+
onChanged.invoke(stepped.coerceIn(0.0, max.toDouble()))
50+
}
51+
52+
Box(
53+
modifier = Modifier
54+
.pointerInput(max) { detectTapGestures { report(it.x, size.width) } }
55+
.pointerInput(max) {
56+
detectDragGestures { change, _ -> report(change.position.x, size.width) }
57+
},
58+
) {
59+
androidx.compose.foundation.layout.Row {
60+
for (index in 0 until max) {
61+
val fill = (rating - index).coerceIn(0f, 1f)
62+
Star(fill)
63+
}
64+
}
65+
}
66+
}
67+
68+
// The same pure Compose function the Android demo registers: a dashed border
69+
// drawn around whatever SwiftUI child content lands in the slot.
70+
ComposableRegistry.register("DashedBorder") { props, children ->
71+
val color = props.color("color") ?: Color(0xFF6750A4.toInt())
72+
Box(
73+
modifier = Modifier
74+
.drawBehind {
75+
drawRoundRect(
76+
color = color,
77+
style = Stroke(
78+
width = 5f,
79+
pathEffect = PathEffect.dashPathEffect(floatArrayOf(24f, 14f)),
80+
),
81+
cornerRadius = CornerRadius(20f, 20f),
82+
)
83+
}
84+
.padding(18.dp),
85+
) {
86+
children()
87+
}
88+
}
89+
}
90+
91+
/// One star, filled left-to-right by `fill` (0…1) so halves render exactly.
92+
/// A clipped filled glyph is layered over an empty one — no icon dependency.
93+
@Composable
94+
private fun Star(fill: Float) {
95+
val style = TextStyle(fontSize = 34.sp, textAlign = TextAlign.Center)
96+
Box(modifier = Modifier.size(40.dp), contentAlignment = Alignment.Center) {
97+
Text("☆", style = style, color = Color(0xFFB0B0B0)) // ☆
98+
Text(
99+
"★", // ★
100+
style = style,
101+
color = Color(0xFFFFC107),
102+
modifier = Modifier.drawWithContent {
103+
clipRect(right = size.width * fill) { this@drawWithContent.drawContent() }
104+
},
105+
)
106+
}
107+
}

‎Demo/desktop/src/jvmMain/kotlin/com/pureswift/swiftui/desktop/Main.kt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ private fun LiveContent() {
8787
val store = remember {
8888
TreeStore().also {
8989
com.pureswift.swiftui.SwiftBridge.sink = com.pureswift.swiftui.JextractCallbackSink()
90+
registerDemoComposables()
9091
SwiftRuntime().start(it)
9192
}
9293
}

‎Package.swift‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -119,15 +119,16 @@ let package = Package(
119119
],
120120
// `Playgrounds` symlinks the Android demo's shared sources; the rig
121121
// reuses them verbatim on desktop. Excluded: the app entry point (it
122-
// needs the Android host or Apple's App/Scene), and the three
123-
// Android-only screens — Map (schematic), Video (Media3), and Custom
124-
// Views (the native-view composable registry) — which have no desktop
125-
// rendering. Their catalog entries are `#if canImport(AndroidSwiftUI)`.
122+
// needs the Android host or Apple's App/Scene) and the two Android-
123+
// only screens — Map (schematic) and Video (Media3) — which have no
124+
// desktop rendering; their catalog entries are gated on `DESKTOP_RIG`.
125+
// The Custom Views screen (the composable registry) IS included: the
126+
// registry is a cross-platform extension point, and the desktop rig
127+
// registers pure-Compose factories for it (see DemoComposables.kt).
126128
exclude: [
127129
"Playgrounds/App.swift",
128130
"Playgrounds/MapPlaygrounds.swift",
129131
"Playgrounds/VideoPlaygrounds.swift",
130-
"Playgrounds/RepresentablePlaygrounds.swift",
131132
],
132133
swiftSettings: [
133134
.swiftLanguageMode(.v5),

0 commit comments

Comments
 (0)