peachsim 0.1.648 → 0.1.650

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/README.md +10 -9
  2. package/cli/command-registry.ts +2 -1
  3. package/cli/commands/guest-host.ts +67 -0
  4. package/cli/commands/kotlin.ts +231 -0
  5. package/cli/commands/project-report.ts +1 -1
  6. package/cli/commands/report-issue.ts +55 -32
  7. package/cli/commands/setup.ts +10 -14
  8. package/cli/commands/support.ts +399 -0
  9. package/cli/commands/swift.ts +6 -52
  10. package/cli/commands/telemetry.ts +10 -13
  11. package/cli/main.ts +22 -10
  12. package/cli/outbound-endpoints.ts +23 -2
  13. package/cli/privacy.ts +23 -25
  14. package/cli/runtime-notes.ts +3 -2
  15. package/cli/telemetry.ts +109 -14
  16. package/dist-lib/agent-daemon-client.cjs +1 -1
  17. package/dist-lib/agent-events.cjs +1 -1
  18. package/dist-lib/agent-identity.cjs +1 -1
  19. package/dist-lib/agent-sessions.cjs +1 -1
  20. package/dist-lib/attached-projects.cjs +1 -1
  21. package/dist-lib/auth/shared-session.cjs +1 -1
  22. package/dist-lib/backend-origin.cjs +1 -1
  23. package/dist-lib/beta.cjs +1 -1
  24. package/dist-lib/beta.mjs +1 -1
  25. package/dist-lib/bridge-constants.cjs +1 -1
  26. package/dist-lib/bridge-contract-input.cjs +1 -1
  27. package/dist-lib/bridge-contract-input.mjs +1 -1
  28. package/dist-lib/bridge-contract.cjs +1 -1
  29. package/dist-lib/bridge-contract.mjs +1 -1
  30. package/dist-lib/capture-contract.cjs +1 -1
  31. package/dist-lib/capture-contract.mjs +1 -1
  32. package/dist-lib/cli-constants.cjs +1 -1
  33. package/dist-lib/cloud-contract.cjs +1 -1
  34. package/dist-lib/cloud-contract.mjs +1 -1
  35. package/dist-lib/cloud-test-args.cjs +1 -1
  36. package/dist-lib/cloud-test-args.mjs +1 -1
  37. package/dist-lib/cloud.cjs +1 -1
  38. package/dist-lib/cloud.mjs +1 -1
  39. package/dist-lib/config.cjs +1 -1
  40. package/dist-lib/detox/index.cjs +1 -1
  41. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  42. package/dist-lib/home-paths.cjs +3 -3
  43. package/dist-lib/host/bridge-host.cjs +1 -1
  44. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  45. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  46. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  47. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  48. package/dist-lib/host/websocket-proxy.cjs +1 -1
  49. package/dist-lib/index.cjs +1 -1
  50. package/dist-lib/jump-to-source-babel.cjs +1 -1
  51. package/dist-lib/jump-to-source-native.cjs +1 -1
  52. package/dist-lib/kotlin-host.cjs +102 -21
  53. package/dist-lib/menu.cjs +1 -1
  54. package/dist-lib/menu.mjs +1 -1
  55. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  56. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  57. package/dist-lib/metro-production-bundle.cjs +1 -1
  58. package/dist-lib/metro-production-bundle.mjs +1 -1
  59. package/dist-lib/metro.cjs +1 -1
  60. package/dist-lib/native-app-identity.cjs +1 -1
  61. package/dist-lib/native-app-identity.mjs +1 -1
  62. package/dist-lib/profiles.cjs +1 -1
  63. package/dist-lib/public-brand.cjs +1 -1
  64. package/dist-lib/react-native-host-modules.cjs +1 -1
  65. package/dist-lib/react-native-host-modules.mjs +1 -1
  66. package/dist-lib/render-mode.cjs +1 -1
  67. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  68. package/dist-lib/sdk.cjs +1 -1
  69. package/dist-lib/sdk.mjs +1 -1
  70. package/dist-lib/skills.cjs +28 -39
  71. package/dist-lib/swift-compile.cjs +6 -3
  72. package/dist-lib/swift-compile.mjs +6 -3
  73. package/dist-lib/swift-package.cjs +1 -1
  74. package/dist-lib/swift-package.mjs +1 -1
  75. package/dist-lib/swift-submit.cjs +1 -1
  76. package/dist-lib/swift-submit.mjs +1 -1
  77. package/dist-lib/swift.cjs +1 -1
  78. package/dist-lib/vite-swift.cjs +6 -3
  79. package/dist-lib/vite.cjs +1 -1
  80. package/kotlin/AppSources/android/app/Activity.kt +85 -0
  81. package/kotlin/AppSources/android/app/Application.kt +7 -0
  82. package/kotlin/AppSources/android/content/Context.kt +5 -0
  83. package/kotlin/AppSources/android/content/Intent.kt +16 -0
  84. package/kotlin/AppSources/android/os/Bundle.kt +5 -0
  85. package/kotlin/AppSources/android/util/SuperNotCalledException.kt +3 -0
  86. package/kotlin/AppSources/android/view/View.kt +72 -0
  87. package/kotlin/AppSources/android/view/Window.kt +44 -0
  88. package/kotlin/AppSources/android/widget/FrameLayout.kt +6 -0
  89. package/kotlin/AppSources/androidx/activity/ComponentActivity.kt +57 -0
  90. package/kotlin/AppSources/androidx/activity/compose/ComponentActivity.kt +29 -0
  91. package/kotlin/AppSources/androidx/compose/ui/platform/ComposeView.kt +67 -0
  92. package/kotlin/AppSources/dev/peach/app/PeachApp.kt +136 -0
  93. package/kotlin/AppSources/dev/peach/app/PeachWindowHost.kt +13 -0
  94. package/kotlin/ComposeSources/PeachComposeHost.kt +316 -0
  95. package/kotlin/Sources/dev/onejs/one/OneNativeSourceViewDispatch.kt +13 -0
  96. package/kotlin/Sources/org/json/JSON.kt +6 -0
  97. package/kotlin/fixtures/counter/Counter.d.kt.ts +16 -0
  98. package/kotlin/fixtures/counter/Counter.kt +18 -0
  99. package/kotlin/fixtures/counter/CounterApp.native.tsx +34 -0
  100. package/kotlin/fixtures/counter/CounterApp.tsx +3 -0
  101. package/kotlin/fixtures/counter/CounterScreen.d.kt.ts +7 -0
  102. package/kotlin/fixtures/counter/CounterScreen.kt +18 -3
  103. package/kotlin/fixtures/counter/app/_layout.tsx +4 -0
  104. package/kotlin/fixtures/counter/app/index+spa.tsx +1 -0
  105. package/kotlin/fixtures/counter/app/routes.d.ts +18 -0
  106. package/kotlin/fixtures/counter/package.json +7 -0
  107. package/kotlin/fixtures/counter/tsconfig.json +19 -0
  108. package/kotlin/fixtures/counter/vite.config.ts +10 -0
  109. package/kotlin/fixtures/headless-counter/Headless.kt +5 -0
  110. package/kotlin/fixtures/kotlin-app/app/build.gradle.kts +30 -0
  111. package/kotlin/fixtures/kotlin-app/app/src/main/AndroidManifest.xml +13 -0
  112. package/kotlin/fixtures/kotlin-app/app/src/main/java/dev/contrast/kotlinapp/MainActivity.kt +60 -0
  113. package/kotlin/fixtures/kotlin-app/app/src/main/java/dev/contrast/kotlinapp/ProbeApplication.kt +18 -0
  114. package/kotlin/fixtures/kotlin-app/build.gradle.kts +5 -0
  115. package/kotlin/fixtures/kotlin-app/gradle.properties +4 -0
  116. package/kotlin/fixtures/kotlin-app/settings.gradle.kts +4 -0
  117. package/kotlin/fork/NOTICE +7 -5
  118. package/kotlin/fork/README.md +41 -49
  119. package/kotlin/fork/build.sh +3 -2
  120. package/kotlin/fork/compose-worker-globals.patch +22 -11
  121. package/kotlin/fork/fork.json +2 -2
  122. package/kotlin/fork/publish.ts +30 -15
  123. package/kotlin/fork/skiko-worker-clock.patch +38 -0
  124. package/kotlin/toolchain.json +9 -9
  125. package/package.json +1 -1
  126. package/skills/contrast/SKILL.md +2 -1
  127. package/skills/peach-setup/SKILL.md +2 -2
  128. package/src/{swift-guest.ts → guest-bundle.ts} +22 -7
  129. package/src/home-paths.ts +5 -3
  130. package/src/kotlin-app.ts +411 -0
  131. package/src/kotlin-host.ts +148 -25
  132. package/src/swift-compile.ts +7 -2
@@ -0,0 +1,316 @@
1
+ @file:OptIn(androidx.compose.ui.ExperimentalComposeUiApi::class, androidx.compose.ui.InternalComposeUiApi::class, androidx.compose.ui.text.ExperimentalTextApi::class, kotlin.js.ExperimentalJsExport::class, kotlin.js.ExperimentalWasmJsInterop::class)
2
+
3
+ import androidx.compose.runtime.Composable
4
+ import androidx.compose.runtime.CompositionLocalProvider
5
+ import androidx.compose.runtime.getValue
6
+ import androidx.compose.runtime.mutableStateOf
7
+ import androidx.compose.runtime.remember
8
+ import androidx.compose.runtime.setValue
9
+ import androidx.compose.runtime.snapshots.Snapshot
10
+ import androidx.compose.ui.geometry.Offset
11
+ import androidx.compose.ui.graphics.asComposeCanvas
12
+ import androidx.compose.ui.input.InputMode
13
+ import androidx.compose.ui.input.InputModeManager
14
+ import androidx.compose.ui.input.pointer.PointerEventType
15
+ import androidx.compose.ui.input.pointer.PointerType
16
+ import androidx.compose.ui.platform.FrameRecomposer
17
+ import androidx.compose.ui.platform.LocalFontFamilyResolver
18
+ import androidx.compose.ui.platform.PlatformArchitectureComponentsOwner
19
+ import androidx.compose.ui.platform.PlatformWindowInsets
20
+ import androidx.compose.ui.platform.PlatformContext
21
+ import androidx.compose.ui.platform.WindowInfo
22
+ import androidx.compose.ui.platform.registerSkikoComposeImplementation
23
+ import androidx.compose.ui.scene.CanvasLayersComposeScene
24
+ import androidx.compose.ui.scene.hasInvalidations
25
+ import androidx.compose.ui.semantics.SemanticsNode
26
+ import androidx.compose.ui.semantics.SemanticsOwner
27
+ import androidx.compose.ui.semantics.SemanticsProperties
28
+ import androidx.compose.ui.semantics.getOrNull
29
+ import androidx.compose.ui.text.font.FontFamily
30
+ import androidx.compose.ui.text.font.FontStyle
31
+ import androidx.compose.ui.text.font.FontSynthesis
32
+ import androidx.compose.ui.text.font.FontWeight
33
+ import androidx.compose.ui.text.font.PlatformTypefacesLoader
34
+ import androidx.compose.ui.text.font.createPlatformFontFamilyResolver
35
+ import androidx.compose.ui.text.font.platformTypefacesLoader
36
+ import androidx.compose.ui.text.intl.LocaleList
37
+ import androidx.compose.ui.text.intl.setPlatformLocaleProvider
38
+ import androidx.compose.ui.text.platform.Font
39
+ import androidx.compose.ui.unit.Constraints
40
+ import androidx.compose.ui.unit.Density
41
+ import androidx.compose.ui.unit.DpSize
42
+ import androidx.compose.ui.unit.IntSize
43
+ import androidx.compose.ui.unit.LayoutDirection
44
+ import dev.onejs.one.OneNativeSourceViewDispatch
45
+ import kotlin.io.encoding.Base64
46
+ import kotlinx.coroutines.Dispatchers
47
+ import org.jetbrains.skia.BackendRenderTarget
48
+ import org.jetbrains.skia.Bitmap
49
+ import org.jetbrains.skia.Color
50
+ import org.jetbrains.skia.ColorAlphaType
51
+ import org.jetbrains.skia.ColorSpace
52
+ import org.jetbrains.skia.ColorType
53
+ import org.jetbrains.skia.DirectContext
54
+ import org.jetbrains.skia.ImageInfo
55
+ import org.jetbrains.skia.Surface
56
+ import org.jetbrains.skia.SurfaceColorFormat
57
+ import org.jetbrains.skia.SurfaceOrigin
58
+ import org.jetbrains.skia.SurfaceProps
59
+ import org.jetbrains.skiko.OS
60
+ import org.jetbrains.skiko.setHostOsProvider
61
+ import org.json.JSONArray
62
+ import org.json.JSONObject
63
+
64
+ // owners and window insets come from the hosting window: an activity's
65
+ // ComposeView passes the activity and the insets its window delivers; an
66
+ // imported composable keeps the empty defaults.
67
+ internal class PeachComposePlatform(
68
+ val density: Density,
69
+ override val localeList: LocaleList,
70
+ var size: IntSize,
71
+ owners: PlatformArchitectureComponentsOwner?,
72
+ insets: PlatformWindowInsets?,
73
+ base: PlatformContext = PlatformContext.Empty(),
74
+ ) : PlatformContext by base, PlatformContext.SemanticsOwnerListener {
75
+ override val architectureComponentsOwner: PlatformArchitectureComponentsOwner = owners ?: base.architectureComponentsOwner
76
+ override val windowInsets: PlatformWindowInsets = insets ?: base.windowInsets
77
+ val owners = mutableSetOf<SemanticsOwner>()
78
+ override val windowInfo: WindowInfo = object : WindowInfo {
79
+ override val isWindowFocused: Boolean get() = true
80
+ override val containerSize: IntSize get() = size
81
+ override val containerDpSize: DpSize get() = with(density) { DpSize(size.width.toDp(), size.height.toDp()) }
82
+ }
83
+ override val inputModeManager: InputModeManager = object : InputModeManager {
84
+ override var inputMode: InputMode by mutableStateOf(InputMode.Touch)
85
+ override fun requestInputMode(inputMode: InputMode): Boolean {
86
+ this.inputMode = inputMode
87
+ return true
88
+ }
89
+ }
90
+ override val semanticsOwnerListener: PlatformContext.SemanticsOwnerListener get() = this
91
+ override fun onSemanticsOwnerAppended(semanticsOwner: SemanticsOwner) { owners.add(semanticsOwner) }
92
+ override fun onSemanticsOwnerRemoved(semanticsOwner: SemanticsOwner) { owners.remove(semanticsOwner) }
93
+ override fun onSemanticsChange(semanticsOwner: SemanticsOwner) = Unit
94
+ override fun onLayoutChange(semanticsOwner: SemanticsOwner, semanticsNodeId: Int) = Unit
95
+ }
96
+
97
+ // android's default and sans-serif families use the same device font bytes as
98
+ // peach's react native renderer. compose owns shaping and all other families.
99
+ @Composable
100
+ internal fun PeachAndroidFonts(roboto: FontFamily, content: @Composable () -> Unit) {
101
+ val parent = LocalFontFamilyResolver.current
102
+ val resolver = remember(parent, roboto) {
103
+ val backend = checkNotNull(parent.platformTypefacesLoader())
104
+ createPlatformFontFamilyResolver(object : PlatformTypefacesLoader by backend {
105
+ override val cacheKey: Any get() = this
106
+ override fun loadPlatformTypes(fontFamily: FontFamily, fontWeight: FontWeight, fontStyle: FontStyle): Any =
107
+ if (fontFamily == FontFamily.Default || fontFamily == FontFamily.SansSerif)
108
+ parent.resolve(roboto, fontWeight, fontStyle, FontSynthesis.All).value
109
+ else backend.loadPlatformTypes(fontFamily, fontWeight, fontStyle)
110
+ })
111
+ }
112
+ CompositionLocalProvider(LocalFontFamilyResolver provides resolver, content = content)
113
+ }
114
+
115
+ // one compose scene presented by the tenant canvas. the frame clock runs only
116
+ // while clockRunning answers true, as Android's window recomposer pauses its
117
+ // clock below STARTED.
118
+ internal class PeachComposeMount(
119
+ density: Float,
120
+ fontScale: Float,
121
+ locale: String,
122
+ private val gpu: Boolean,
123
+ private val stencilBits: Int,
124
+ private val fonts: FontFamily,
125
+ val invalidate: (Boolean) -> Unit,
126
+ owners: PlatformArchitectureComponentsOwner? = null,
127
+ insets: PlatformWindowInsets? = null,
128
+ private val clockRunning: () -> Boolean = { true },
129
+ ) : AutoCloseable {
130
+ var props by mutableStateOf("")
131
+ var constraints = Constraints()
132
+ set(value) { if (field != value) { field = value; needsMeasure = true } }
133
+ private val events = mutableListOf<JSONObject>()
134
+ private val platform = PeachComposePlatform(Density(density, fontScale), LocaleList(locale), IntSize.Zero, owners, insets)
135
+ private val recomposer = FrameRecomposer(Dispatchers.Main) { invalidate(false) }
136
+ private var needsMeasure = true
137
+ private var measuredSize = IntSize.Zero
138
+ private val scene = CanvasLayersComposeScene(
139
+ recomposer, platform.density, LayoutDirection.Ltr, null, platform,
140
+ invalidateLayout = { needsMeasure = true; invalidate(true) }, invalidateDraw = { invalidate(false) },
141
+ )
142
+ private var surface: Surface? = null
143
+ private var context: DirectContext? = null
144
+ private var target: BackendRenderTarget? = null
145
+ private var pixels: Bitmap? = null
146
+ private var size = IntSize.Zero
147
+
148
+ fun setContent(content: @Composable () -> Unit) {
149
+ try {
150
+ scene.setContent { PeachAndroidFonts(fonts, content) }
151
+ } catch (failure: Throwable) {
152
+ close()
153
+ throw failure
154
+ }
155
+ }
156
+
157
+ fun emit(name: String, args: Any?) {
158
+ events.add(JSONObject().put("name", name).put("args", args))
159
+ invalidate(false)
160
+ }
161
+
162
+ fun measure(): IntSize {
163
+ val measured = scene.measureContent(constraints)
164
+ scene.size = measured
165
+ platform.size = measured
166
+ measuredSize = measured
167
+ needsMeasure = false
168
+ return measured
169
+ }
170
+
171
+ fun frame(nanos: Long): String {
172
+ Snapshot.sendApplyNotifications()
173
+ if (clockRunning()) recomposer.performFrame(nanos)
174
+ val measured = if (needsMeasure) measure() else measuredSize
175
+ // a zero-size composition still runs its effects and reports its size;
176
+ // its backing surface uses one pixel until layout gives it real bounds.
177
+ val backing = IntSize(measured.width.coerceAtLeast(1), measured.height.coerceAtLeast(1))
178
+ if (backing != size) {
179
+ releaseSurface()
180
+ size = backing
181
+ platform.size = measured
182
+ scene.size = measured
183
+ if (gpu) {
184
+ context = DirectContext.makeGL()
185
+ target = BackendRenderTarget.makeGL(size.width, size.height, 1, stencilBits, 0, 0x8058)
186
+ surface = checkNotNull(Surface.makeFromBackendRenderTarget(
187
+ checkNotNull(context), checkNotNull(target), SurfaceOrigin.BOTTOM_LEFT,
188
+ SurfaceColorFormat.RGBA_8888, ColorSpace.sRGB, SurfaceProps(),
189
+ )) { "Skiko GPU surface creation failed" }
190
+ } else {
191
+ surface = Surface.makeRasterN32Premul(size.width, size.height)
192
+ pixels = Bitmap().also { check(it.allocPixels(ImageInfo(size.width, size.height, ColorType.RGBA_8888, ColorAlphaType.PREMUL, ColorSpace.sRGB))) }
193
+ }
194
+ }
195
+ scene.measureAndLayout()
196
+ val canvas = checkNotNull(surface).canvas
197
+ canvas.clear(Color.TRANSPARENT)
198
+ scene.draw(canvas.asComposeCanvas())
199
+ val reply = JSONObject().put("width", measured.width).put("height", measured.height)
200
+ .put("pending", recomposer.hasPendingWork() || scene.hasInvalidations())
201
+ .put("events", JSONArray(events))
202
+ events.clear()
203
+ if (gpu) checkNotNull(surface).flushAndSubmit()
204
+ else {
205
+ check(checkNotNull(surface).readPixels(checkNotNull(pixels), 0, 0))
206
+ val map = checkNotNull(checkNotNull(pixels).peekPixels())
207
+ try { reply.put("pointer", map.addr).put("length", size.width * size.height * 4) }
208
+ finally { map.close() }
209
+ }
210
+ return reply.toString()
211
+ }
212
+
213
+ fun pointer(phase: String, x: Float, y: Float, millis: Long) {
214
+ if (phase == "cancel") scene.cancelPointerInput()
215
+ else scene.sendPointerEvent(
216
+ when (phase) {
217
+ "press" -> PointerEventType.Press
218
+ "release" -> PointerEventType.Release
219
+ "move" -> PointerEventType.Move
220
+ else -> error("unknown pointer phase $phase")
221
+ }, Offset(x, y), timeMillis = millis, type = PointerType.Touch,
222
+ )
223
+ invalidate(false)
224
+ }
225
+
226
+ fun semantics(): String {
227
+ val nodes = JSONArray()
228
+ fun collect(node: SemanticsNode) {
229
+ val bounds = node.boundsInRoot
230
+ nodes.put(JSONObject().put("text", node.config.getOrNull(SemanticsProperties.Text)?.joinToString { it.text })
231
+ .put("tag", node.config.getOrNull(SemanticsProperties.TestTag))
232
+ .put("left", bounds.left).put("top", bounds.top).put("right", bounds.right).put("bottom", bounds.bottom))
233
+ node.children.forEach { collect(it) }
234
+ }
235
+ platform.owners.forEach { collect(it.unmergedRootSemanticsNode) }
236
+ return nodes.toString()
237
+ }
238
+
239
+ private fun releaseSurface() {
240
+ pixels?.close(); pixels = null
241
+ surface?.close(); surface = null
242
+ target?.close(); target = null
243
+ context?.close(); context = null
244
+ }
245
+
246
+ override fun close() {
247
+ try { scene.close() }
248
+ finally {
249
+ try { recomposer.close() }
250
+ finally { releaseSurface() }
251
+ }
252
+ }
253
+ }
254
+
255
+ internal val mounts = mutableMapOf<Int, PeachComposeMount>()
256
+ private var nextMount = 1
257
+ private var deviceLocale: String? = null
258
+
259
+ internal fun addMount(mount: PeachComposeMount): Int = nextMount++.also { mounts[it] = mount }
260
+
261
+ // the process-wide device setup every scene shares: skiko's platform, the
262
+ // host OS and the device locale, fixed for the life of the process.
263
+ internal fun preparePeachDevice(locale: String) {
264
+ registerSkikoComposeImplementation()
265
+ if (deviceLocale == null) {
266
+ setHostOsProvider { OS.Android }
267
+ val locales = LocaleList(locale)
268
+ setPlatformLocaleProvider { locales }
269
+ deviceLocale = locale
270
+ }
271
+ require(deviceLocale == locale) { "device locale changes require a new Kotlin process" }
272
+ }
273
+
274
+ internal fun robotoFamily(regular: String, medium: String, bold: String) = FontFamily(
275
+ Font("Roboto-400", Base64.decode(regular), FontWeight.Normal),
276
+ Font("Roboto-500", Base64.decode(medium), FontWeight.Medium),
277
+ Font("Roboto-700", Base64.decode(bold), FontWeight.Bold),
278
+ )
279
+
280
+ @JsExport
281
+ fun peachViewMount(source: String, view: String, hash: String, props: String,
282
+ density: Float, fontScale: Float, locale: String, backend: String, stencilBits: Int,
283
+ regular: String, medium: String, bold: String, invalidate: (Boolean) -> Unit): String {
284
+ try {
285
+ val dispatch = peachViewDispatcher(source)
286
+ if (dispatch.contractHash != hash) return JSONObject().put("ok", false).put("code", "E_NATIVE_SOURCE_REBUILD")
287
+ .put("error", "composable source $source changed; rebuild the app").toString()
288
+ require(backend == "graphite" || backend == "raster")
289
+ preparePeachDevice(locale)
290
+ val mount = PeachComposeMount(density, fontScale, locale, backend == "graphite", stencilBits, robotoFamily(regular, medium, bold), invalidate)
291
+ mount.props = props
292
+ mount.setContent { dispatch.Content(view, mount.props, mount::emit) }
293
+ return JSONObject().put("ok", true).put("id", addMount(mount)).toString()
294
+ } catch (error: Throwable) {
295
+ return JSONObject().put("ok", false).put("code", "E_NATIVE_SOURCE").put("error", "$source.$view: ${error.message}").toString()
296
+ }
297
+ }
298
+
299
+ @JsExport fun peachViewUpdate(id: Int, props: String) { checkNotNull(mounts[id]).props = props }
300
+ @JsExport fun peachViewConstraints(id: Int, minWidth: Int, maxWidth: Int, minHeight: Int, maxHeight: Int) {
301
+ checkNotNull(mounts[id]).also {
302
+ it.constraints = Constraints(minWidth, maxWidth, minHeight, maxHeight)
303
+ it.invalidate(false)
304
+ }
305
+ }
306
+ @JsExport fun peachViewFrame(id: Int, millis: Double): String = checkNotNull(mounts[id]).frame((millis * 1_000_000).toLong())
307
+ @JsExport fun peachViewPointer(id: Int, phase: String, x: Float, y: Float, millis: Double) = checkNotNull(mounts[id]).pointer(phase, x, y, millis.toLong())
308
+ @JsExport fun peachViewSemantics(id: Int): String = checkNotNull(mounts[id]).semantics()
309
+ @JsExport fun peachViewDispose(id: Int) { mounts.remove(id)?.close() }
310
+
311
+ @JsExport fun peachViewMeasure(id: Int, minWidth: Int, maxWidth: Int, minHeight: Int, maxHeight: Int): String {
312
+ val mount = checkNotNull(mounts[id])
313
+ mount.constraints = Constraints(minWidth, maxWidth, minHeight, maxHeight)
314
+ val size = mount.measure()
315
+ return JSONObject().put("width", size.width).put("height", size.height).toString()
316
+ }
@@ -0,0 +1,13 @@
1
+ package dev.onejs.one
2
+
3
+ import androidx.compose.runtime.Composable
4
+
5
+ // generated per .kt source that declares public @Composable functions: renders
6
+ // one of them from the host's json props. a callback reports its arguments
7
+ // through emit as a json array.
8
+ interface OneNativeSourceViewDispatch {
9
+ val contractHash: String
10
+
11
+ @Composable
12
+ fun Content(view: String, propsJson: String, emit: (String, String) -> Unit)
13
+ }
@@ -207,6 +207,12 @@ class JSONObject() {
207
207
  return this
208
208
  }
209
209
 
210
+ fun optBoolean(name: String): Boolean = when (val value = opt(name)) {
211
+ is Boolean -> value
212
+ is String -> value.equals("true", ignoreCase = true)
213
+ else -> false
214
+ }
215
+
210
216
  fun keys(): Iterator<String> = fields.keys.iterator()
211
217
 
212
218
  internal fun writeTo(out: StringBuilder) {
@@ -0,0 +1,16 @@
1
+ // generated by one typegen; edits will be replaced
2
+ export declare const Counter: {
3
+ read(): Promise<number>
4
+ increment(): Promise<number>
5
+ reset(): Promise<number>
6
+ delayedRead(ms: number): Promise<number>
7
+ }
8
+
9
+ export declare const CounterEffects: {
10
+ recordLocale(current: string, composition: string): Promise<void>
11
+ locales(): Promise<string[]>
12
+ enter(): Promise<void>
13
+ dispose(): Promise<void>
14
+ cancel(): Promise<void>
15
+ read(): Promise<number[]>
16
+ }
@@ -24,3 +24,21 @@ object Counter : OneModule {
24
24
  return count
25
25
  }
26
26
  }
27
+
28
+ // lifecycle counters let the mount probe observe effect cleanup after removal.
29
+ object CounterEffects : OneModule {
30
+ private var entered = 0
31
+ private var disposed = 0
32
+ private var cancelled = 0
33
+ private var compositionLocale = ""
34
+ private var currentLocale = ""
35
+ internal fun recordLocale(current: String, composition: String) {
36
+ currentLocale = current
37
+ compositionLocale = composition
38
+ }
39
+ fun locales(): List<String> = listOf(currentLocale, compositionLocale)
40
+ fun enter() { entered += 1 }
41
+ fun dispose() { disposed += 1 }
42
+ fun cancel() { cancelled += 1 }
43
+ fun read(): List<Int> = listOf(entered, disposed, cancelled)
44
+ }
@@ -0,0 +1,34 @@
1
+ import { useEffect, useState } from 'react'
2
+ import { Pressable, Text, View } from 'react-native'
3
+ import { CounterEffects } from './Counter.kt'
4
+ import { CounterScreen } from './CounterScreen.kt'
5
+
6
+ export default function Index() {
7
+ const [title, setTitle] = useState('Kotlin counter')
8
+ const [mounted, setMounted] = useState(true)
9
+ const [callback, setCallback] = useState(0)
10
+ const [effects, setEffects] = useState<number[]>([])
11
+ const [locales, setLocales] = useState<string[]>([])
12
+ useEffect(() => {
13
+ if (callback > 0) void CounterEffects.locales().then(setLocales)
14
+ }, [callback])
15
+ useEffect(() => {
16
+ if (!mounted) void CounterEffects.read().then(setEffects)
17
+ }, [mounted])
18
+ return (
19
+ <View style={{ flex: 1 }}>
20
+ {mounted && <CounterScreen title={title} onTap={setCallback} />}
21
+ <View style={{ position: 'absolute', bottom: 40, left: 24 }}>
22
+ <Text testID={`callback-${callback}`}>Callback: {callback}</Text>
23
+ <Text testID={`effects-${effects.join('-')}`}>Effects: {effects.join(',')}</Text>
24
+ <Text testID={`locales-${locales.join('-')}`}>Locales: {locales.join(',')}</Text>
25
+ <Pressable testID="edit-title" onPress={() => setTitle('Edited title')}>
26
+ <Text>Edit title</Text>
27
+ </Pressable>
28
+ <Pressable testID="unmount" onPress={() => setMounted(false)}>
29
+ <Text>Unmount</Text>
30
+ </Pressable>
31
+ </View>
32
+ </View>
33
+ )
34
+ }
@@ -0,0 +1,3 @@
1
+ export default function CounterApp() {
2
+ throw new Error('the imported Kotlin counter fixture requires an Android device')
3
+ }
@@ -0,0 +1,7 @@
1
+ // generated by one typegen; edits will be replaced
2
+ import type { ReactElement } from 'react'
3
+
4
+ export declare function CounterScreen(props: {
5
+ title?: string | null
6
+ onTap?: ((value0: number) => void) | null
7
+ }): ReactElement
@@ -8,6 +8,12 @@ import androidx.compose.material3.Button
8
8
  import androidx.compose.material3.MaterialTheme
9
9
  import androidx.compose.material3.Text
10
10
  import androidx.compose.runtime.Composable
11
+ import androidx.compose.runtime.DisposableEffect
12
+ import androidx.compose.runtime.LaunchedEffect
13
+ import androidx.compose.runtime.SideEffect
14
+ import androidx.compose.ui.platform.LocalLocaleList
15
+ import androidx.compose.ui.text.intl.Locale
16
+ import kotlinx.coroutines.awaitCancellation
11
17
  import androidx.compose.runtime.getValue
12
18
  import androidx.compose.runtime.mutableStateOf
13
19
  import androidx.compose.runtime.remember
@@ -20,7 +26,16 @@ import androidx.compose.material3.Surface
20
26
  import androidx.compose.material3.lightColorScheme
21
27
 
22
28
  @Composable
23
- fun CounterScreen() {
29
+ fun CounterScreen(title: String? = null, onTap: ((Int) -> Unit)? = null) {
30
+ val locales = LocalLocaleList.current
31
+ SideEffect { CounterEffects.recordLocale(Locale.current.toLanguageTag(), locales.joinToString(",") { it.toLanguageTag() }) }
32
+ DisposableEffect(Unit) {
33
+ CounterEffects.enter()
34
+ onDispose { CounterEffects.dispose() }
35
+ }
36
+ LaunchedEffect(Unit) {
37
+ try { awaitCancellation() } finally { CounterEffects.cancel() }
38
+ }
24
39
  var count by remember { mutableStateOf(Counter.read()) }
25
40
  MaterialTheme(colorScheme = lightColorScheme(primary = Color(0xFF6750A4))) {
26
41
  Surface(modifier = Modifier.fillMaxSize()) {
@@ -28,10 +43,10 @@ fun CounterScreen() {
28
43
  modifier = Modifier.fillMaxSize().padding(24.dp),
29
44
  verticalArrangement = Arrangement.spacedBy(16.dp),
30
45
  ) {
31
- Text("Kotlin counter", style = MaterialTheme.typography.headlineSmall)
46
+ Text(title ?: "Kotlin counter", style = MaterialTheme.typography.headlineSmall)
32
47
  Text("Count: $count", modifier = Modifier.testTag("counter-value"))
33
48
  Button(
34
- onClick = { count = Counter.increment() },
49
+ onClick = { count = Counter.increment(); onTap?.invoke(count) },
35
50
  modifier = Modifier.testTag("counter-increment"),
36
51
  ) { Text("Increment") }
37
52
  }
@@ -0,0 +1,4 @@
1
+ import { Slot } from 'one'
2
+ export default function Layout() {
3
+ return <Slot />
4
+ }
@@ -0,0 +1 @@
1
+ export { default } from '../CounterApp'
@@ -0,0 +1,18 @@
1
+ // deno-lint-ignore-file
2
+ /* eslint-disable */
3
+ // biome-ignore: needed import
4
+ import type { OneRouter } from 'one'
5
+
6
+ declare module 'one' {
7
+ export namespace OneRouter {
8
+ export interface __routes<T extends string = string> extends Record<string, unknown> {
9
+ StaticRoutes:
10
+ | `/`
11
+ | `/_sitemap`
12
+ DynamicRoutes: never
13
+ DynamicRouteTemplate: never
14
+ IsTyped: true
15
+
16
+ }
17
+ }
18
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "peach-kotlin-counter-fixture",
3
+ "private": true,
4
+ "type": "module",
5
+ "dependencies": { "one": "catalog:", "react": "catalog:", "react-native": "catalog:" },
6
+ "devDependencies": { "peachsim": "workspace:*", "vite": "catalog:" }
7
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "paths": {
4
+ "~/*": ["./*"]
5
+ },
6
+ "strict": true,
7
+ "rootDir": ".",
8
+ "module": "Preserve",
9
+ // allows react-native style imports without path extensions, for compat with platform-specific files
10
+ "moduleResolution": "Bundler",
11
+ "preserveSymlinks": true,
12
+ "skipLibCheck": true,
13
+ "jsx": "react-jsx",
14
+ "noImplicitAny": false,
15
+ "types": ["node", "react", "vite/client"],
16
+ "lib": ["dom", "esnext"]
17
+ },
18
+ "exclude": ["node_modules", "**/test", "**/dist", "**/types", "**/__tests__"]
19
+ }
@@ -0,0 +1,10 @@
1
+ import { one } from 'one/vite'
2
+ import { rnxPlugin } from 'peachsim/vite'
3
+ export default {
4
+ root: import.meta.dirname,
5
+ plugins: [
6
+ one({ web: { defaultRenderMode: 'spa' }, native: { bundler: 'vite' } }),
7
+ rnxPlugin(),
8
+ ],
9
+ server: { host: '0.0.0.0' },
10
+ }
@@ -33,6 +33,9 @@ import androidx.compose.ui.text.font.PlatformTypefacesLoader
33
33
  import androidx.compose.ui.text.font.createPlatformFontFamilyResolver
34
34
  import androidx.compose.ui.text.font.platformTypefacesLoader
35
35
  import androidx.compose.ui.text.intl.LocaleList
36
+ import androidx.compose.ui.text.intl.setPlatformLocaleProvider
37
+ import org.jetbrains.skiko.OS
38
+ import org.jetbrains.skiko.setHostOsProvider
36
39
  import androidx.compose.ui.text.platform.Font
37
40
  import androidx.compose.ui.unit.Density
38
41
  import androidx.compose.ui.unit.DpSize
@@ -85,7 +88,9 @@ private class DeviceScene(locale: String, content: @Composable () -> Unit) : Aut
85
88
  private val size = IntSize(1080, 2400)
86
89
  private val density = Density(420f / 160f)
87
90
  init {
91
+ setHostOsProvider { OS.Android }
88
92
  registerSkikoComposeImplementation()
93
+ setPlatformLocaleProvider { LocaleList(locale) }
89
94
  }
90
95
  val context = PeachPlatformContext(size, density, LocaleList(locale))
91
96
  private val recomposer = FrameRecomposer(Dispatchers.Unconfined)
@@ -0,0 +1,30 @@
1
+ plugins {
2
+ id("com.android.application")
3
+ id("org.jetbrains.kotlin.android")
4
+ id("org.jetbrains.kotlin.plugin.compose")
5
+ }
6
+
7
+ android {
8
+ namespace = "dev.contrast.kotlinapp"
9
+ compileSdk = 37
10
+ defaultConfig {
11
+ applicationId = "dev.contrast.kotlinapp"
12
+ minSdk = 24
13
+ targetSdk = 37
14
+ versionCode = 1
15
+ versionName = "1"
16
+ }
17
+ buildFeatures { compose = true }
18
+ compileOptions {
19
+ sourceCompatibility = JavaVersion.VERSION_17
20
+ targetCompatibility = JavaVersion.VERSION_17
21
+ }
22
+ kotlinOptions { jvmTarget = "17" }
23
+ }
24
+
25
+ dependencies {
26
+ implementation("androidx.activity:activity-compose:1.9.3")
27
+ implementation("androidx.compose.ui:ui:1.13.0-alpha02")
28
+ implementation("androidx.compose.foundation:foundation:1.13.0-alpha02")
29
+ implementation("androidx.compose.material3:material3:1.5.0-alpha27")
30
+ }
@@ -0,0 +1,13 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <application
3
+ android:name=".ProbeApplication"
4
+ android:label="Kotlin app"
5
+ android:theme="@android:style/Theme.Material.Light.NoActionBar">
6
+ <activity android:name=".MainActivity" android:exported="true">
7
+ <intent-filter>
8
+ <action android:name="android.intent.action.MAIN" />
9
+ <category android:name="android.intent.category.LAUNCHER" />
10
+ </intent-filter>
11
+ </activity>
12
+ </application>
13
+ </manifest>