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.
- package/README.md +10 -9
- package/cli/command-registry.ts +2 -1
- package/cli/commands/guest-host.ts +67 -0
- package/cli/commands/kotlin.ts +231 -0
- package/cli/commands/project-report.ts +1 -1
- package/cli/commands/report-issue.ts +55 -32
- package/cli/commands/setup.ts +10 -14
- package/cli/commands/support.ts +399 -0
- package/cli/commands/swift.ts +6 -52
- package/cli/commands/telemetry.ts +10 -13
- package/cli/main.ts +22 -10
- package/cli/outbound-endpoints.ts +23 -2
- package/cli/privacy.ts +23 -25
- package/cli/runtime-notes.ts +3 -2
- package/cli/telemetry.ts +109 -14
- 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 +3 -3
- 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 +102 -21
- 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 +28 -39
- package/dist-lib/swift-compile.cjs +6 -3
- package/dist-lib/swift-compile.mjs +6 -3
- package/dist-lib/swift-package.cjs +1 -1
- package/dist-lib/swift-package.mjs +1 -1
- package/dist-lib/swift-submit.cjs +1 -1
- package/dist-lib/swift-submit.mjs +1 -1
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +6 -3
- package/dist-lib/vite.cjs +1 -1
- package/kotlin/AppSources/android/app/Activity.kt +85 -0
- package/kotlin/AppSources/android/app/Application.kt +7 -0
- package/kotlin/AppSources/android/content/Context.kt +5 -0
- package/kotlin/AppSources/android/content/Intent.kt +16 -0
- package/kotlin/AppSources/android/os/Bundle.kt +5 -0
- package/kotlin/AppSources/android/util/SuperNotCalledException.kt +3 -0
- package/kotlin/AppSources/android/view/View.kt +72 -0
- package/kotlin/AppSources/android/view/Window.kt +44 -0
- package/kotlin/AppSources/android/widget/FrameLayout.kt +6 -0
- package/kotlin/AppSources/androidx/activity/ComponentActivity.kt +57 -0
- package/kotlin/AppSources/androidx/activity/compose/ComponentActivity.kt +29 -0
- package/kotlin/AppSources/androidx/compose/ui/platform/ComposeView.kt +67 -0
- package/kotlin/AppSources/dev/peach/app/PeachApp.kt +136 -0
- package/kotlin/AppSources/dev/peach/app/PeachWindowHost.kt +13 -0
- package/kotlin/ComposeSources/PeachComposeHost.kt +316 -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/skills/contrast/SKILL.md +2 -1
- package/skills/peach-setup/SKILL.md +2 -2
- package/src/{swift-guest.ts → guest-bundle.ts} +22 -7
- package/src/home-paths.ts +5 -3
- package/src/kotlin-app.ts +411 -0
- package/src/kotlin-host.ts +148 -25
- 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
|
+
}
|
|
@@ -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>
|