From 4f0607877c453a8077d2babf0000d85ff04493d0 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 25 Jul 2026 10:39:47 -0400 Subject: [PATCH 1/2] Register pure-Compose custom views in the desktop rig --- .../swiftui/desktop/DemoComposables.kt | 107 ++++++++++++++++++ .../com/pureswift/swiftui/desktop/Main.kt | 1 + 2 files changed, 108 insertions(+) create mode 100644 Demo/desktop/src/jvmMain/kotlin/com/pureswift/swiftui/desktop/DemoComposables.kt diff --git a/Demo/desktop/src/jvmMain/kotlin/com/pureswift/swiftui/desktop/DemoComposables.kt b/Demo/desktop/src/jvmMain/kotlin/com/pureswift/swiftui/desktop/DemoComposables.kt new file mode 100644 index 0000000..389a4c3 --- /dev/null +++ b/Demo/desktop/src/jvmMain/kotlin/com/pureswift/swiftui/desktop/DemoComposables.kt @@ -0,0 +1,107 @@ +package com.pureswift.swiftui.desktop + +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathEffect +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.drawscope.clipRect +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.pureswift.swiftui.ComposableRegistry +import kotlin.math.roundToInt + +/// Registers the desktop rig's custom composables into the interpreter's +/// registry — the desktop counterpart to the Android demo's `registerDemo +/// Composables`. Called once before the first render. It shows that the +/// composable registry is a cross-platform extension point: `DashedBorder` is +/// the identical pure-Compose function used on Android, and `RatingBar` is a +/// pure Compose-Multiplatform reimplementation of what the Android demo bridges +/// to `android.widget.RatingBar`. +fun registerDemoComposables() { + + // A pure Compose-Multiplatform star rating — no native widget. Tapping or + // dragging across the stars reports a half-step rating back to Swift; the + // `rating` prop drives the fill so two-way binding round-trips. + ComposableRegistry.register("RatingBar") { props, _ -> + val rating = props.float("rating") ?: 0f + val max = props.int("max") ?: 5 + val onChanged = props.doubleAction("onRatingChanged") + + // Maps a horizontal position within the row to the nearest half-star. + fun report(x: Float, width: Int) { + if (width <= 0 || onChanged == null) return + val fraction = (x / width).coerceIn(0f, 1f) + val stepped = (fraction * max * 2).roundToInt() / 2.0 + onChanged.invoke(stepped.coerceIn(0.0, max.toDouble())) + } + + Box( + modifier = Modifier + .pointerInput(max) { detectTapGestures { report(it.x, size.width) } } + .pointerInput(max) { + detectDragGestures { change, _ -> report(change.position.x, size.width) } + }, + ) { + androidx.compose.foundation.layout.Row { + for (index in 0 until max) { + val fill = (rating - index).coerceIn(0f, 1f) + Star(fill) + } + } + } + } + + // The same pure Compose function the Android demo registers: a dashed border + // drawn around whatever SwiftUI child content lands in the slot. + ComposableRegistry.register("DashedBorder") { props, children -> + val color = props.color("color") ?: Color(0xFF6750A4.toInt()) + Box( + modifier = Modifier + .drawBehind { + drawRoundRect( + color = color, + style = Stroke( + width = 5f, + pathEffect = PathEffect.dashPathEffect(floatArrayOf(24f, 14f)), + ), + cornerRadius = CornerRadius(20f, 20f), + ) + } + .padding(18.dp), + ) { + children() + } + } +} + +/// One star, filled left-to-right by `fill` (0…1) so halves render exactly. +/// A clipped filled glyph is layered over an empty one — no icon dependency. +@Composable +private fun Star(fill: Float) { + val style = TextStyle(fontSize = 34.sp, textAlign = TextAlign.Center) + Box(modifier = Modifier.size(40.dp), contentAlignment = Alignment.Center) { + Text("☆", style = style, color = Color(0xFFB0B0B0)) // ☆ + Text( + "★", // ★ + style = style, + color = Color(0xFFFFC107), + modifier = Modifier.drawWithContent { + clipRect(right = size.width * fill) { this@drawWithContent.drawContent() } + }, + ) + } +} diff --git a/Demo/desktop/src/jvmMain/kotlin/com/pureswift/swiftui/desktop/Main.kt b/Demo/desktop/src/jvmMain/kotlin/com/pureswift/swiftui/desktop/Main.kt index be81ef4..535c7a4 100644 --- a/Demo/desktop/src/jvmMain/kotlin/com/pureswift/swiftui/desktop/Main.kt +++ b/Demo/desktop/src/jvmMain/kotlin/com/pureswift/swiftui/desktop/Main.kt @@ -87,6 +87,7 @@ private fun LiveContent() { val store = remember { TreeStore().also { com.pureswift.swiftui.SwiftBridge.sink = com.pureswift.swiftui.JextractCallbackSink() + registerDemoComposables() SwiftRuntime().start(it) } } From c049d0f16759b3b3b9175485aa89a868650aa1a8 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 25 Jul 2026 10:39:47 -0400 Subject: [PATCH 2/2] Include the Custom Views screen in the desktop rig --- Demo/App.swiftpm/Sources/Catalog.swift | 5 ++--- Package.swift | 11 ++++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Demo/App.swiftpm/Sources/Catalog.swift b/Demo/App.swiftpm/Sources/Catalog.swift index abe5e06..6dc1a77 100644 --- a/Demo/App.swiftpm/Sources/Catalog.swift +++ b/Demo/App.swiftpm/Sources/Catalog.swift @@ -69,10 +69,9 @@ struct CatalogEntry: Identifiable { CatalogEntry(id: "form", title: "Form", screen: AnyCatalogScreen(FormPlayground())), CatalogEntry(id: "modifier", title: "Modifiers", screen: AnyCatalogScreen(ModifierPlayground())), ] - // Android-only: native-view interop through the composable registry - #if !DESKTOP_RIG + // Custom views through the composable registry — cross-platform: each + // host (Android app, desktop rig) registers its own factories. entries.append(CatalogEntry(id: "representable", title: "Custom Views", screen: AnyCatalogScreen(RepresentablePlayground()))) - #endif entries += [ CatalogEntry(id: "appearance", title: "Appearance", screen: AnyCatalogScreen(AppearancePlayground())), CatalogEntry(id: "animation", title: "Animation", screen: AnyCatalogScreen(AnimationPlayground())), diff --git a/Package.swift b/Package.swift index 7e2b959..cf5fc15 100644 --- a/Package.swift +++ b/Package.swift @@ -119,15 +119,16 @@ let package = Package( ], // `Playgrounds` symlinks the Android demo's shared sources; the rig // reuses them verbatim on desktop. Excluded: the app entry point (it - // needs the Android host or Apple's App/Scene), and the three - // Android-only screens — Map (schematic), Video (Media3), and Custom - // Views (the native-view composable registry) — which have no desktop - // rendering. Their catalog entries are `#if canImport(AndroidSwiftUI)`. + // needs the Android host or Apple's App/Scene) and the two Android- + // only screens — Map (schematic) and Video (Media3) — which have no + // desktop rendering; their catalog entries are gated on `DESKTOP_RIG`. + // The Custom Views screen (the composable registry) IS included: the + // registry is a cross-platform extension point, and the desktop rig + // registers pure-Compose factories for it (see DemoComposables.kt). exclude: [ "Playgrounds/App.swift", "Playgrounds/MapPlaygrounds.swift", "Playgrounds/VideoPlaygrounds.swift", - "Playgrounds/RepresentablePlaygrounds.swift", ], swiftSettings: [ .swiftLanguageMode(.v5),