peachsim 0.1.647 → 0.1.649
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.
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract-input.cjs +1 -1
- package/dist-lib/bridge-contract-input.mjs +1 -1
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +1 -1
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/cloud-contract.cjs +1 -1
- package/dist-lib/cloud-contract.mjs +1 -1
- package/dist-lib/cloud-test-args.cjs +1 -1
- package/dist-lib/cloud-test-args.mjs +1 -1
- package/dist-lib/cloud.cjs +1 -1
- package/dist-lib/cloud.mjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +1 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +1 -1
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/replacement-module-handler.cjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +1 -1
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +1 -1
- package/dist-lib/kotlin-host.cjs +21 -5
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro-fingerprint-registry.cjs +1 -1
- package/dist-lib/metro-fingerprint-registry.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +1 -1
- package/dist-lib/metro-production-bundle.mjs +1 -1
- package/dist-lib/metro.cjs +1 -1
- package/dist-lib/native-app-identity.cjs +1 -1
- package/dist-lib/native-app-identity.mjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +1 -1
- package/dist-lib/sdk.mjs +1 -1
- package/dist-lib/skills.cjs +14 -14
- package/dist-lib/swift-compile.cjs +1 -1
- package/dist-lib/swift-compile.mjs +1 -1
- package/dist-lib/swift-package.cjs +1 -1
- package/dist-lib/swift-package.mjs +1 -1
- package/dist-lib/swift-submit.cjs +2 -2
- package/dist-lib/swift-submit.mjs +2 -2
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +1067 -1064
- package/dist-lib/vite.cjs +1 -1
- package/kotlin/ComposeSources/PeachComposeHost.kt +295 -0
- package/kotlin/Sources/dev/onejs/one/OneNativeSourceViewDispatch.kt +13 -0
- package/kotlin/Sources/org/json/JSON.kt +6 -0
- package/kotlin/fixtures/counter/Counter.d.kt.ts +16 -0
- package/kotlin/fixtures/counter/Counter.kt +18 -0
- package/kotlin/fixtures/counter/CounterApp.native.tsx +34 -0
- package/kotlin/fixtures/counter/CounterApp.tsx +3 -0
- package/kotlin/fixtures/counter/CounterScreen.d.kt.ts +7 -0
- package/kotlin/fixtures/counter/CounterScreen.kt +18 -3
- package/kotlin/fixtures/counter/app/_layout.tsx +4 -0
- package/kotlin/fixtures/counter/app/index+spa.tsx +1 -0
- package/kotlin/fixtures/counter/app/routes.d.ts +18 -0
- package/kotlin/fixtures/counter/package.json +7 -0
- package/kotlin/fixtures/counter/tsconfig.json +19 -0
- package/kotlin/fixtures/counter/vite.config.ts +10 -0
- package/kotlin/fixtures/headless-counter/Headless.kt +5 -0
- package/kotlin/fixtures/kotlin-app/app/build.gradle.kts +30 -0
- package/kotlin/fixtures/kotlin-app/app/src/main/AndroidManifest.xml +13 -0
- package/kotlin/fixtures/kotlin-app/app/src/main/java/dev/contrast/kotlinapp/MainActivity.kt +60 -0
- package/kotlin/fixtures/kotlin-app/app/src/main/java/dev/contrast/kotlinapp/ProbeApplication.kt +18 -0
- package/kotlin/fixtures/kotlin-app/build.gradle.kts +5 -0
- package/kotlin/fixtures/kotlin-app/gradle.properties +4 -0
- package/kotlin/fixtures/kotlin-app/settings.gradle.kts +4 -0
- package/kotlin/fork/NOTICE +7 -5
- package/kotlin/fork/README.md +41 -49
- package/kotlin/fork/build.sh +3 -2
- package/kotlin/fork/compose-worker-globals.patch +22 -11
- package/kotlin/fork/fork.json +2 -2
- package/kotlin/fork/publish.ts +30 -15
- package/kotlin/fork/skiko-worker-clock.patch +38 -0
- package/kotlin/toolchain.json +9 -9
- package/package.json +1 -1
- package/src/kotlin-host.ts +20 -4
- package/src/swift-submit.ts +3 -1
- package/src/vite-plugin-swift.ts +10 -6
package/dist-lib/vite.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.649 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -0,0 +1,295 @@
|
|
|
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.PlatformContext
|
|
19
|
+
import androidx.compose.ui.platform.WindowInfo
|
|
20
|
+
import androidx.compose.ui.platform.registerSkikoComposeImplementation
|
|
21
|
+
import androidx.compose.ui.scene.CanvasLayersComposeScene
|
|
22
|
+
import androidx.compose.ui.scene.hasInvalidations
|
|
23
|
+
import androidx.compose.ui.semantics.SemanticsNode
|
|
24
|
+
import androidx.compose.ui.semantics.SemanticsOwner
|
|
25
|
+
import androidx.compose.ui.semantics.SemanticsProperties
|
|
26
|
+
import androidx.compose.ui.semantics.getOrNull
|
|
27
|
+
import androidx.compose.ui.text.font.FontFamily
|
|
28
|
+
import androidx.compose.ui.text.font.FontStyle
|
|
29
|
+
import androidx.compose.ui.text.font.FontSynthesis
|
|
30
|
+
import androidx.compose.ui.text.font.FontWeight
|
|
31
|
+
import androidx.compose.ui.text.font.PlatformTypefacesLoader
|
|
32
|
+
import androidx.compose.ui.text.font.createPlatformFontFamilyResolver
|
|
33
|
+
import androidx.compose.ui.text.font.platformTypefacesLoader
|
|
34
|
+
import androidx.compose.ui.text.intl.LocaleList
|
|
35
|
+
import androidx.compose.ui.text.intl.setPlatformLocaleProvider
|
|
36
|
+
import androidx.compose.ui.text.platform.Font
|
|
37
|
+
import androidx.compose.ui.unit.Constraints
|
|
38
|
+
import androidx.compose.ui.unit.Density
|
|
39
|
+
import androidx.compose.ui.unit.DpSize
|
|
40
|
+
import androidx.compose.ui.unit.IntSize
|
|
41
|
+
import androidx.compose.ui.unit.LayoutDirection
|
|
42
|
+
import dev.onejs.one.OneNativeSourceViewDispatch
|
|
43
|
+
import kotlin.io.encoding.Base64
|
|
44
|
+
import kotlinx.coroutines.Dispatchers
|
|
45
|
+
import org.jetbrains.skia.BackendRenderTarget
|
|
46
|
+
import org.jetbrains.skia.Bitmap
|
|
47
|
+
import org.jetbrains.skia.Color
|
|
48
|
+
import org.jetbrains.skia.ColorAlphaType
|
|
49
|
+
import org.jetbrains.skia.ColorSpace
|
|
50
|
+
import org.jetbrains.skia.ColorType
|
|
51
|
+
import org.jetbrains.skia.DirectContext
|
|
52
|
+
import org.jetbrains.skia.ImageInfo
|
|
53
|
+
import org.jetbrains.skia.Surface
|
|
54
|
+
import org.jetbrains.skia.SurfaceColorFormat
|
|
55
|
+
import org.jetbrains.skia.SurfaceOrigin
|
|
56
|
+
import org.jetbrains.skia.SurfaceProps
|
|
57
|
+
import org.jetbrains.skiko.OS
|
|
58
|
+
import org.jetbrains.skiko.setHostOsProvider
|
|
59
|
+
import org.json.JSONArray
|
|
60
|
+
import org.json.JSONObject
|
|
61
|
+
|
|
62
|
+
private class PeachComposePlatform(
|
|
63
|
+
val density: Density,
|
|
64
|
+
override val localeList: LocaleList,
|
|
65
|
+
var size: IntSize,
|
|
66
|
+
) : PlatformContext by PlatformContext.Empty(), PlatformContext.SemanticsOwnerListener {
|
|
67
|
+
val owners = mutableSetOf<SemanticsOwner>()
|
|
68
|
+
override val windowInfo: WindowInfo = object : WindowInfo {
|
|
69
|
+
override val isWindowFocused: Boolean get() = true
|
|
70
|
+
override val containerSize: IntSize get() = size
|
|
71
|
+
override val containerDpSize: DpSize get() = with(density) { DpSize(size.width.toDp(), size.height.toDp()) }
|
|
72
|
+
}
|
|
73
|
+
override val inputModeManager: InputModeManager = object : InputModeManager {
|
|
74
|
+
override var inputMode: InputMode by mutableStateOf(InputMode.Touch)
|
|
75
|
+
override fun requestInputMode(inputMode: InputMode): Boolean {
|
|
76
|
+
this.inputMode = inputMode
|
|
77
|
+
return true
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
override val semanticsOwnerListener: PlatformContext.SemanticsOwnerListener get() = this
|
|
81
|
+
override fun onSemanticsOwnerAppended(semanticsOwner: SemanticsOwner) { owners.add(semanticsOwner) }
|
|
82
|
+
override fun onSemanticsOwnerRemoved(semanticsOwner: SemanticsOwner) { owners.remove(semanticsOwner) }
|
|
83
|
+
override fun onSemanticsChange(semanticsOwner: SemanticsOwner) = Unit
|
|
84
|
+
override fun onLayoutChange(semanticsOwner: SemanticsOwner, semanticsNodeId: Int) = Unit
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// android's default and sans-serif families use the same device font bytes as
|
|
88
|
+
// peach's react native renderer. compose owns shaping and all other families.
|
|
89
|
+
@Composable
|
|
90
|
+
private fun PeachAndroidFonts(roboto: FontFamily, content: @Composable () -> Unit) {
|
|
91
|
+
val parent = LocalFontFamilyResolver.current
|
|
92
|
+
val resolver = remember(parent, roboto) {
|
|
93
|
+
val backend = checkNotNull(parent.platformTypefacesLoader())
|
|
94
|
+
createPlatformFontFamilyResolver(object : PlatformTypefacesLoader by backend {
|
|
95
|
+
override val cacheKey: Any get() = this
|
|
96
|
+
override fun loadPlatformTypes(fontFamily: FontFamily, fontWeight: FontWeight, fontStyle: FontStyle): Any =
|
|
97
|
+
if (fontFamily == FontFamily.Default || fontFamily == FontFamily.SansSerif)
|
|
98
|
+
parent.resolve(roboto, fontWeight, fontStyle, FontSynthesis.All).value
|
|
99
|
+
else backend.loadPlatformTypes(fontFamily, fontWeight, fontStyle)
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
CompositionLocalProvider(LocalFontFamilyResolver provides resolver, content = content)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private class PeachComposeMount(
|
|
106
|
+
dispatch: OneNativeSourceViewDispatch,
|
|
107
|
+
view: String,
|
|
108
|
+
propsJson: String,
|
|
109
|
+
density: Float,
|
|
110
|
+
fontScale: Float,
|
|
111
|
+
locale: String,
|
|
112
|
+
private val gpu: Boolean,
|
|
113
|
+
private val stencilBits: Int,
|
|
114
|
+
fonts: FontFamily,
|
|
115
|
+
val invalidate: (Boolean) -> Unit,
|
|
116
|
+
) : AutoCloseable {
|
|
117
|
+
var props by mutableStateOf(propsJson)
|
|
118
|
+
var constraints = Constraints()
|
|
119
|
+
set(value) { if (field != value) { field = value; needsMeasure = true } }
|
|
120
|
+
private val events = mutableListOf<JSONObject>()
|
|
121
|
+
private val platform = PeachComposePlatform(Density(density, fontScale), LocaleList(locale), IntSize.Zero)
|
|
122
|
+
private val recomposer = FrameRecomposer(Dispatchers.Main) { invalidate(false) }
|
|
123
|
+
private var needsMeasure = true
|
|
124
|
+
private var measuredSize = IntSize.Zero
|
|
125
|
+
private val scene = CanvasLayersComposeScene(
|
|
126
|
+
recomposer, platform.density, LayoutDirection.Ltr, null, platform,
|
|
127
|
+
invalidateLayout = { needsMeasure = true; invalidate(true) }, invalidateDraw = { invalidate(false) },
|
|
128
|
+
)
|
|
129
|
+
private var surface: Surface? = null
|
|
130
|
+
private var context: DirectContext? = null
|
|
131
|
+
private var target: BackendRenderTarget? = null
|
|
132
|
+
private var pixels: Bitmap? = null
|
|
133
|
+
private var size = IntSize.Zero
|
|
134
|
+
|
|
135
|
+
init {
|
|
136
|
+
try {
|
|
137
|
+
scene.setContent {
|
|
138
|
+
PeachAndroidFonts(fonts) {
|
|
139
|
+
dispatch.Content(view, props) { name, args ->
|
|
140
|
+
events.add(JSONObject().put("name", name).put("args", args))
|
|
141
|
+
invalidate(false)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} catch (failure: Throwable) {
|
|
146
|
+
close()
|
|
147
|
+
throw failure
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
fun measure(): IntSize {
|
|
152
|
+
val measured = scene.measureContent(constraints)
|
|
153
|
+
scene.size = measured
|
|
154
|
+
platform.size = measured
|
|
155
|
+
measuredSize = measured
|
|
156
|
+
needsMeasure = false
|
|
157
|
+
return measured
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
fun frame(nanos: Long): String {
|
|
161
|
+
Snapshot.sendApplyNotifications()
|
|
162
|
+
recomposer.performFrame(nanos)
|
|
163
|
+
val measured = if (needsMeasure) measure() else measuredSize
|
|
164
|
+
// a zero-size composition still runs its effects and reports its size;
|
|
165
|
+
// its backing surface uses one pixel until layout gives it real bounds.
|
|
166
|
+
val backing = IntSize(measured.width.coerceAtLeast(1), measured.height.coerceAtLeast(1))
|
|
167
|
+
if (backing != size) {
|
|
168
|
+
releaseSurface()
|
|
169
|
+
size = backing
|
|
170
|
+
platform.size = measured
|
|
171
|
+
scene.size = measured
|
|
172
|
+
if (gpu) {
|
|
173
|
+
context = DirectContext.makeGL()
|
|
174
|
+
target = BackendRenderTarget.makeGL(size.width, size.height, 1, stencilBits, 0, 0x8058)
|
|
175
|
+
surface = checkNotNull(Surface.makeFromBackendRenderTarget(
|
|
176
|
+
checkNotNull(context), checkNotNull(target), SurfaceOrigin.BOTTOM_LEFT,
|
|
177
|
+
SurfaceColorFormat.RGBA_8888, ColorSpace.sRGB, SurfaceProps(),
|
|
178
|
+
)) { "Skiko GPU surface creation failed" }
|
|
179
|
+
} else {
|
|
180
|
+
surface = Surface.makeRasterN32Premul(size.width, size.height)
|
|
181
|
+
pixels = Bitmap().also { check(it.allocPixels(ImageInfo(size.width, size.height, ColorType.RGBA_8888, ColorAlphaType.PREMUL, ColorSpace.sRGB))) }
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
scene.measureAndLayout()
|
|
185
|
+
val canvas = checkNotNull(surface).canvas
|
|
186
|
+
canvas.clear(Color.TRANSPARENT)
|
|
187
|
+
scene.draw(canvas.asComposeCanvas())
|
|
188
|
+
val reply = JSONObject().put("width", measured.width).put("height", measured.height)
|
|
189
|
+
.put("pending", recomposer.hasPendingWork() || scene.hasInvalidations())
|
|
190
|
+
.put("events", JSONArray(events))
|
|
191
|
+
events.clear()
|
|
192
|
+
if (gpu) checkNotNull(surface).flushAndSubmit()
|
|
193
|
+
else {
|
|
194
|
+
check(checkNotNull(surface).readPixels(checkNotNull(pixels), 0, 0))
|
|
195
|
+
val map = checkNotNull(checkNotNull(pixels).peekPixels())
|
|
196
|
+
try { reply.put("pointer", map.addr).put("length", size.width * size.height * 4) }
|
|
197
|
+
finally { map.close() }
|
|
198
|
+
}
|
|
199
|
+
return reply.toString()
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
fun pointer(phase: String, x: Float, y: Float, millis: Long) {
|
|
203
|
+
if (phase == "cancel") scene.cancelPointerInput()
|
|
204
|
+
else scene.sendPointerEvent(
|
|
205
|
+
when (phase) {
|
|
206
|
+
"press" -> PointerEventType.Press
|
|
207
|
+
"release" -> PointerEventType.Release
|
|
208
|
+
"move" -> PointerEventType.Move
|
|
209
|
+
else -> error("unknown pointer phase $phase")
|
|
210
|
+
}, Offset(x, y), timeMillis = millis, type = PointerType.Touch,
|
|
211
|
+
)
|
|
212
|
+
invalidate(false)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
fun semantics(): String {
|
|
216
|
+
val nodes = JSONArray()
|
|
217
|
+
fun collect(node: SemanticsNode) {
|
|
218
|
+
val bounds = node.boundsInRoot
|
|
219
|
+
nodes.put(JSONObject().put("text", node.config.getOrNull(SemanticsProperties.Text)?.joinToString { it.text })
|
|
220
|
+
.put("tag", node.config.getOrNull(SemanticsProperties.TestTag))
|
|
221
|
+
.put("left", bounds.left).put("top", bounds.top).put("right", bounds.right).put("bottom", bounds.bottom))
|
|
222
|
+
node.children.forEach { collect(it) }
|
|
223
|
+
}
|
|
224
|
+
platform.owners.forEach { collect(it.unmergedRootSemanticsNode) }
|
|
225
|
+
return nodes.toString()
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private fun releaseSurface() {
|
|
229
|
+
pixels?.close(); pixels = null
|
|
230
|
+
surface?.close(); surface = null
|
|
231
|
+
target?.close(); target = null
|
|
232
|
+
context?.close(); context = null
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
override fun close() {
|
|
236
|
+
try { scene.close() }
|
|
237
|
+
finally {
|
|
238
|
+
try { recomposer.close() }
|
|
239
|
+
finally { releaseSurface() }
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private val mounts = mutableMapOf<Int, PeachComposeMount>()
|
|
245
|
+
private var nextMount = 1
|
|
246
|
+
private var deviceLocale: String? = null
|
|
247
|
+
|
|
248
|
+
@JsExport
|
|
249
|
+
fun peachViewMount(source: String, view: String, hash: String, props: String,
|
|
250
|
+
density: Float, fontScale: Float, locale: String, backend: String, stencilBits: Int,
|
|
251
|
+
regular: String, medium: String, bold: String, invalidate: (Boolean) -> Unit): String {
|
|
252
|
+
try {
|
|
253
|
+
registerSkikoComposeImplementation()
|
|
254
|
+
val dispatch = peachViewDispatcher(source)
|
|
255
|
+
if (dispatch.contractHash != hash) return JSONObject().put("ok", false).put("code", "E_NATIVE_SOURCE_REBUILD")
|
|
256
|
+
.put("error", "composable source $source changed; rebuild the app").toString()
|
|
257
|
+
require(backend == "graphite" || backend == "raster")
|
|
258
|
+
if (deviceLocale == null) {
|
|
259
|
+
setHostOsProvider { OS.Android }
|
|
260
|
+
val locales = LocaleList(locale)
|
|
261
|
+
setPlatformLocaleProvider { locales }
|
|
262
|
+
deviceLocale = locale
|
|
263
|
+
}
|
|
264
|
+
require(deviceLocale == locale) { "device locale changes require a new Kotlin process" }
|
|
265
|
+
val fonts = FontFamily(
|
|
266
|
+
Font("Roboto-400", Base64.decode(regular), FontWeight.Normal),
|
|
267
|
+
Font("Roboto-500", Base64.decode(medium), FontWeight.Medium),
|
|
268
|
+
Font("Roboto-700", Base64.decode(bold), FontWeight.Bold),
|
|
269
|
+
)
|
|
270
|
+
val id = nextMount++
|
|
271
|
+
mounts[id] = PeachComposeMount(dispatch, view, props, density, fontScale, locale, backend == "graphite", stencilBits, fonts, invalidate)
|
|
272
|
+
return JSONObject().put("ok", true).put("id", id).toString()
|
|
273
|
+
} catch (error: Throwable) {
|
|
274
|
+
return JSONObject().put("ok", false).put("code", "E_NATIVE_SOURCE").put("error", "$source.$view: ${error.message}").toString()
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
@JsExport fun peachViewUpdate(id: Int, props: String) { checkNotNull(mounts[id]).props = props }
|
|
279
|
+
@JsExport fun peachViewConstraints(id: Int, minWidth: Int, maxWidth: Int, minHeight: Int, maxHeight: Int) {
|
|
280
|
+
checkNotNull(mounts[id]).also {
|
|
281
|
+
it.constraints = Constraints(minWidth, maxWidth, minHeight, maxHeight)
|
|
282
|
+
it.invalidate(false)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
@JsExport fun peachViewFrame(id: Int, millis: Double): String = checkNotNull(mounts[id]).frame((millis * 1_000_000).toLong())
|
|
286
|
+
@JsExport fun peachViewPointer(id: Int, phase: String, x: Float, y: Float, millis: Double) = checkNotNull(mounts[id]).pointer(phase, x, y, millis.toLong())
|
|
287
|
+
@JsExport fun peachViewSemantics(id: Int): String = checkNotNull(mounts[id]).semantics()
|
|
288
|
+
@JsExport fun peachViewDispose(id: Int) { mounts.remove(id)?.close() }
|
|
289
|
+
|
|
290
|
+
@JsExport fun peachViewMeasure(id: Int, minWidth: Int, maxWidth: Int, minHeight: Int, maxHeight: Int): String {
|
|
291
|
+
val mount = checkNotNull(mounts[id])
|
|
292
|
+
mount.constraints = Constraints(minWidth, maxWidth, minHeight, maxHeight)
|
|
293
|
+
val size = mount.measure()
|
|
294
|
+
return JSONObject().put("width", size.width).put("height", size.height).toString()
|
|
295
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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 @@
|
|
|
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,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>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
package dev.contrast.kotlinapp
|
|
2
|
+
|
|
3
|
+
import android.os.Bundle
|
|
4
|
+
import androidx.activity.ComponentActivity
|
|
5
|
+
import androidx.activity.compose.setContent
|
|
6
|
+
import androidx.compose.foundation.background
|
|
7
|
+
import androidx.compose.foundation.layout.Arrangement
|
|
8
|
+
import androidx.compose.foundation.layout.Column
|
|
9
|
+
import androidx.compose.foundation.layout.fillMaxSize
|
|
10
|
+
import androidx.compose.foundation.layout.padding
|
|
11
|
+
import androidx.compose.foundation.layout.safeDrawingPadding
|
|
12
|
+
import androidx.compose.material3.Button
|
|
13
|
+
import androidx.compose.material3.Text
|
|
14
|
+
import androidx.compose.runtime.Composable
|
|
15
|
+
import androidx.compose.runtime.LaunchedEffect
|
|
16
|
+
import androidx.compose.runtime.SideEffect
|
|
17
|
+
import androidx.compose.runtime.getValue
|
|
18
|
+
import androidx.compose.runtime.mutableIntStateOf
|
|
19
|
+
import androidx.compose.runtime.remember
|
|
20
|
+
import androidx.compose.runtime.setValue
|
|
21
|
+
import androidx.compose.ui.Modifier
|
|
22
|
+
import androidx.compose.ui.graphics.Color
|
|
23
|
+
import androidx.compose.ui.platform.testTag
|
|
24
|
+
import androidx.compose.ui.unit.dp
|
|
25
|
+
|
|
26
|
+
class MainActivity : ComponentActivity() {
|
|
27
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
28
|
+
super.onCreate(savedInstanceState)
|
|
29
|
+
Probe.log += "activity.onCreate ${intent.action}"
|
|
30
|
+
setContent { ProbeScreen(content = "first", replace = { replaceContent() }) }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// a second setContent on the same activity: android reuses its ComposeView
|
|
34
|
+
private fun replaceContent() {
|
|
35
|
+
Probe.log += "activity.setContent"
|
|
36
|
+
setContent { ProbeScreen(content = "second", replace = { replaceContent() }) }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
override fun onStart() { super.onStart(); Probe.log += "activity.onStart" }
|
|
40
|
+
override fun onPostCreate(savedInstanceState: Bundle?) { super.onPostCreate(savedInstanceState); Probe.log += "activity.onPostCreate" }
|
|
41
|
+
override fun onResume() { super.onResume(); Probe.log += "activity.onResume" }
|
|
42
|
+
override fun onPostResume() { super.onPostResume(); Probe.log += "activity.onPostResume" }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@Composable
|
|
46
|
+
fun ProbeScreen(content: String, replace: () -> Unit) {
|
|
47
|
+
var count by remember { mutableIntStateOf(0) }
|
|
48
|
+
SideEffect { if ("compose" !in Probe.log) Probe.log += "compose" }
|
|
49
|
+
LaunchedEffect(Unit) { Probe.log += "effect" }
|
|
50
|
+
Column(
|
|
51
|
+
Modifier.fillMaxSize().background(Color(0xFFE8EEF8)).safeDrawingPadding().padding(16.dp),
|
|
52
|
+
verticalArrangement = Arrangement.spacedBy(12.dp),
|
|
53
|
+
) {
|
|
54
|
+
Text("Content: $content", Modifier.testTag("content"))
|
|
55
|
+
Text("Greeting: ${Probe.greeting}", Modifier.testTag("greeting"))
|
|
56
|
+
Button(onClick = { count += 1 }, Modifier.testTag("count")) { Text("Count: $count") }
|
|
57
|
+
Button(onClick = replace, Modifier.testTag("replace")) { Text("Replace content") }
|
|
58
|
+
Text(Probe.log.joinToString("\n"), Modifier.testTag("log"))
|
|
59
|
+
}
|
|
60
|
+
}
|