|
| 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 | +} |
0 commit comments