peachsim 0.1.659 → 0.1.661
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 -15
- package/cli/cli-update.ts +53 -21
- package/cli/commands/compat.ts +6 -2
- package/cli/commands/control.ts +12 -3
- package/cli/commands/inspect.ts +16 -14
- package/cli/commands/upgrade.ts +4 -2
- package/cli/current-sim.ts +4 -0
- package/cli/drivers/linux-browser-libs.lock.json +1088 -0
- package/cli/drivers/linux-browser-libs.ts +182 -0
- package/cli/drivers/playwright-provisioning.ts +20 -98
- package/cli/drivers/playwright-sim-host.ts +9 -0
- package/cli/drivers/playwright.ts +2 -1
- 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 +18 -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 +45 -41
- 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 +5 -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 +8 -3
- 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/plist.cjs +1 -1
- package/dist-lib/plist.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 +1339 -180
- 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 +1 -1
- package/dist-lib/swift-submit.mjs +1 -1
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +1 -1
- package/dist-lib/vite.cjs +1 -1
- package/kotlin/AppSources/android/app/Activity.kt +48 -3
- package/kotlin/AppSources/android/app/Application.kt +4 -0
- package/kotlin/AppSources/android/content/res/Configuration.kt +4 -0
- package/kotlin/AppSources/android/content/res/Resources.kt +3 -1
- package/kotlin/AppSources/android/view/Window.kt +15 -9
- package/kotlin/AppSources/androidx/activity/ComponentActivity.kt +36 -3
- package/kotlin/AppSources/dev/peach/app/PeachApp.kt +233 -86
- package/kotlin/ComposeSources/PeachComposeHost.kt +15 -3
- package/kotlin/fork/README.md +1 -0
- package/kotlin/fork/compose-worker-globals.patch +26 -0
- package/kotlin/fork/fork.json +1 -1
- package/kotlin/toolchain.json +3 -3
- package/package.json +2 -1
- package/src/android-theme.ts +14 -0
- package/src/backend-origin.ts +25 -0
- package/src/host/bridge-host.ts +24 -37
- package/src/kotlin-app.ts +29 -4
- package/src/kotlin-host.ts +7 -2
|
@@ -24,13 +24,16 @@ import androidx.compose.ui.platform.PlatformInsets
|
|
|
24
24
|
import androidx.compose.ui.platform.PlatformWindowInsets
|
|
25
25
|
import androidx.compose.runtime.getValue
|
|
26
26
|
import androidx.compose.runtime.key
|
|
27
|
+
import androidx.compose.runtime.mutableIntStateOf
|
|
27
28
|
import androidx.compose.runtime.mutableStateOf
|
|
28
29
|
import androidx.compose.runtime.setValue
|
|
30
|
+
import androidx.compose.ui.text.font.FontFamily
|
|
29
31
|
import androidx.lifecycle.Lifecycle
|
|
30
32
|
import androidx.navigationevent.NavigationEventDispatcher
|
|
31
33
|
import androidx.navigationevent.NavigationEventDispatcherOwner
|
|
32
34
|
import PeachComposeMount
|
|
33
35
|
import addMount
|
|
36
|
+
import mounts
|
|
34
37
|
import preparePeachDevice
|
|
35
38
|
import robotoFamily
|
|
36
39
|
import org.json.JSONObject
|
|
@@ -45,21 +48,37 @@ internal class PeachAppSpec(
|
|
|
45
48
|
val window: WindowTheme,
|
|
46
49
|
val createApplication: () -> Application,
|
|
47
50
|
val createActivity: () -> Activity,
|
|
51
|
+
// the activity's configChanges includes uiMode: it takes a night mode
|
|
52
|
+
// change in onConfigurationChanged rather than relaunching
|
|
53
|
+
val handlesUiMode: Boolean,
|
|
48
54
|
)
|
|
49
55
|
|
|
50
|
-
// the
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
)
|
|
56
|
+
// the insets the device delivers to the window's content, in pixels: all of
|
|
57
|
+
// them on an edge-to-edge window, none once the decor fits system windows.
|
|
58
|
+
// insets lists the status bars, navigation bars, display cutout, system
|
|
59
|
+
// gestures, mandatory system gestures and tappable element, each left, top,
|
|
60
|
+
// right and bottom.
|
|
61
|
+
private class DeviceInsets(private val window: Window, insets: String) : PlatformWindowInsets {
|
|
62
|
+
private val pixels = insets.split(',').map { it.toInt() }.also { require(it.size == 24) }
|
|
63
|
+
|
|
57
64
|
// read when compose lays out, so a later change to the window applies
|
|
58
|
-
private fun
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
private fun type(index: Int): PlatformInsets {
|
|
66
|
+
fun side(offset: Int) = { if (window.fitsSystemWindows) 0 else pixels[index * 4 + offset] }
|
|
67
|
+
return PlatformInsets(side(0), side(1), side(2), side(3))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
override val statusBars = type(0)
|
|
71
|
+
override val navigationBars = type(1)
|
|
72
|
+
override val displayCutout = type(2)
|
|
73
|
+
override val systemGestures = type(3)
|
|
74
|
+
override val mandatorySystemGestures = type(4)
|
|
75
|
+
override val tappableElement = type(5)
|
|
76
|
+
override val systemBars = PlatformInsets(
|
|
77
|
+
{ maxOf(statusBars.left, navigationBars.left) },
|
|
78
|
+
{ maxOf(statusBars.top, navigationBars.top) },
|
|
79
|
+
{ maxOf(statusBars.right, navigationBars.right) },
|
|
80
|
+
{ maxOf(statusBars.bottom, navigationBars.bottom) },
|
|
81
|
+
)
|
|
63
82
|
}
|
|
64
83
|
|
|
65
84
|
private class ActivityOwners(activity: ComponentActivity) : PlatformArchitectureComponentsOwner {
|
|
@@ -71,6 +90,41 @@ private class ActivityOwners(activity: ComponentActivity) : PlatformArchitecture
|
|
|
71
90
|
}
|
|
72
91
|
}
|
|
73
92
|
|
|
93
|
+
// the device and the application one started app process keeps, and what
|
|
94
|
+
// ActivityThread's record holds for its one activity: whether it is paused or
|
|
95
|
+
// stopped, and the state it saved when it stopped, until it resumes
|
|
96
|
+
private class AppProcess(
|
|
97
|
+
val spec: PeachAppSpec,
|
|
98
|
+
val application: Application,
|
|
99
|
+
val density: Float, val fontScale: Float, val locale: String, val gpu: Boolean, val stencilBits: Int,
|
|
100
|
+
val fonts: FontFamily,
|
|
101
|
+
val insets: String,
|
|
102
|
+
val invalidate: (Boolean) -> Unit,
|
|
103
|
+
) {
|
|
104
|
+
lateinit var activity: Activity
|
|
105
|
+
lateinit var mount: PeachComposeMount
|
|
106
|
+
// the window the device presents, once its scene is in place
|
|
107
|
+
var window: Window? = null
|
|
108
|
+
var mountId = 0
|
|
109
|
+
// the held view's place among its window's views; a relaunched window's
|
|
110
|
+
// first view takes the place the last window's view held, as it takes
|
|
111
|
+
// that view's saved state on the device
|
|
112
|
+
var view = 0
|
|
113
|
+
var paused = false
|
|
114
|
+
var stopped = false
|
|
115
|
+
var state: Bundle? = null
|
|
116
|
+
// the night mode last reported to the activity: ActivityRecord compares
|
|
117
|
+
// the configuration with it before a relaunch or onConfigurationChanged
|
|
118
|
+
var reportedNight = false
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private var process: AppProcess? = null
|
|
122
|
+
|
|
123
|
+
private fun nightConfiguration(night: Boolean) = Configuration().apply {
|
|
124
|
+
uiMode = Configuration.UI_MODE_TYPE_NORMAL or
|
|
125
|
+
if (night) Configuration.UI_MODE_NIGHT_YES else Configuration.UI_MODE_NIGHT_NO
|
|
126
|
+
}
|
|
127
|
+
|
|
74
128
|
// a cold start in ActivityThread's order: the Application and its onCreate,
|
|
75
129
|
// then the launcher activity attached to its window and launcher intent,
|
|
76
130
|
// onCreate, onStart, onPostCreate, onResume and onPostResume, and only then the
|
|
@@ -79,71 +133,23 @@ internal fun peachStartApp(
|
|
|
79
133
|
spec: PeachAppSpec,
|
|
80
134
|
density: Float, fontScale: Float, locale: String, backend: String, stencilBits: Int,
|
|
81
135
|
regular: String, medium: String, bold: String,
|
|
82
|
-
|
|
136
|
+
insets: String, night: Boolean,
|
|
83
137
|
invalidate: (Boolean) -> Unit,
|
|
84
138
|
): String {
|
|
85
139
|
try {
|
|
86
140
|
require(backend == "graphite" || backend == "raster")
|
|
87
141
|
preparePeachDevice(locale)
|
|
88
|
-
processResources = Resources(
|
|
89
|
-
uiMode = Configuration.UI_MODE_TYPE_NORMAL or
|
|
90
|
-
if (night) Configuration.UI_MODE_NIGHT_YES else Configuration.UI_MODE_NIGHT_NO
|
|
91
|
-
})
|
|
92
|
-
val fonts = robotoFamily(regular, medium, bold)
|
|
142
|
+
processResources = Resources(nightConfiguration(night))
|
|
93
143
|
val application = spec.createApplication()
|
|
94
144
|
application.onCreate()
|
|
95
|
-
val
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
lateinit var mount: PeachComposeMount
|
|
99
|
-
var mountId = 0
|
|
100
|
-
// the window's one scene composes the ComposeView it holds; a new
|
|
101
|
-
// view starts a new composition, as on the device
|
|
102
|
-
var held by mutableStateOf<ComposeView?>(null)
|
|
103
|
-
val host = object : PeachWindowHost {
|
|
104
|
-
override fun compose(view: ComposeView): AutoCloseable {
|
|
105
|
-
held = view
|
|
106
|
-
return AutoCloseable { if (held === view) held = null }
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
override fun windowChanged() {
|
|
110
|
-
if (mountId != 0)
|
|
111
|
-
mount.emitAtFrame("window") { window.describe().toString() }
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
window = Window(activity, spec.window, host)
|
|
115
|
-
activity.attach(
|
|
116
|
-
application,
|
|
117
|
-
spec.component,
|
|
118
|
-
Intent(Intent.ACTION_MAIN, setOf(Intent.CATEGORY_LAUNCHER), spec.launcher),
|
|
119
|
-
window,
|
|
145
|
+
val app = AppProcess(
|
|
146
|
+
spec, application, density, fontScale, locale, backend == "graphite", stencilBits,
|
|
147
|
+
robotoFamily(regular, medium, bold), insets, invalidate,
|
|
120
148
|
)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
activity.performPostCreate(null)
|
|
124
|
-
activity.performResume()
|
|
125
|
-
running = activity
|
|
126
|
-
mount = PeachComposeMount(
|
|
127
|
-
density, fontScale, locale, backend == "graphite", stencilBits, fonts, invalidate,
|
|
128
|
-
owners = componentActivity?.let(::ActivityOwners),
|
|
129
|
-
insets = SystemBarInsets(window, statusBar, navigationBar),
|
|
130
|
-
clockRunning = { componentActivity == null || componentActivity.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) },
|
|
131
|
-
)
|
|
132
|
-
mount.setContent {
|
|
133
|
-
val content = @Composable { held?.let { view -> key(view) { view.Content() } }; Unit }
|
|
134
|
-
// the content view's tree resolves the activity as its back dispatcher owner
|
|
135
|
-
if (componentActivity != null)
|
|
136
|
-
CompositionLocalProvider(LocalOnBackPressedDispatcherOwner provides componentActivity, content = content)
|
|
137
|
-
else content()
|
|
138
|
-
}
|
|
139
|
-
mountId = addMount(mount)
|
|
140
|
-
runningMount = mount
|
|
141
|
-
window.addToWindowManager()
|
|
142
|
-
val view = checkNotNull(held) {
|
|
143
|
-
"${spec.component.flattenToShortString()} set no ComposeView as its content; Peach composes only ComposeView content"
|
|
144
|
-
}
|
|
149
|
+
process = app
|
|
150
|
+
val view = launchActivity(app, null)
|
|
145
151
|
val params = view.layoutParams
|
|
146
|
-
return window.describe().put("ok", true).put("id", mountId)
|
|
152
|
+
return app.activity.window.describe().put("ok", true).put("id", app.mountId)
|
|
147
153
|
.put("wrapWidth", params?.width == ViewGroup.LayoutParams.WRAP_CONTENT)
|
|
148
154
|
.put("wrapHeight", params?.height == ViewGroup.LayoutParams.WRAP_CONTENT)
|
|
149
155
|
.toString()
|
|
@@ -153,9 +159,89 @@ internal fun peachStartApp(
|
|
|
153
159
|
}
|
|
154
160
|
}
|
|
155
161
|
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
162
|
+
// ActivityThread's handleLaunchActivity: a new instance of the launcher
|
|
163
|
+
// activity created from the state its last instance saved, if any, with what
|
|
164
|
+
// that instance retained, then started, restored, resumed and its window
|
|
165
|
+
// added. the window's scene takes the process's mount id, so the device keeps
|
|
166
|
+
// presenting it.
|
|
167
|
+
private fun launchActivity(app: AppProcess, lastNonConfigurationInstance: Any?): ComposeView {
|
|
168
|
+
val spec = app.spec
|
|
169
|
+
val activity = spec.createActivity()
|
|
170
|
+
val componentActivity = activity as? ComponentActivity
|
|
171
|
+
val state = app.state
|
|
172
|
+
lateinit var window: Window
|
|
173
|
+
lateinit var mount: PeachComposeMount
|
|
174
|
+
// the window's one scene composes the ComposeView it holds; a new
|
|
175
|
+
// view starts a new composition, as on the device. the key is the view's
|
|
176
|
+
// place, not the view, so a relaunched window's view lands on the keys its
|
|
177
|
+
// saved state was written under
|
|
178
|
+
var held by mutableStateOf<ComposeView?>(null)
|
|
179
|
+
var place by mutableIntStateOf((app.view - 1).coerceAtLeast(0))
|
|
180
|
+
val host = object : PeachWindowHost {
|
|
181
|
+
override fun compose(view: ComposeView): AutoCloseable {
|
|
182
|
+
held = view
|
|
183
|
+
place += 1
|
|
184
|
+
app.view = place
|
|
185
|
+
return AutoCloseable { if (held === view) held = null }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// a change before the window is presented reaches the device with
|
|
189
|
+
// the window itself
|
|
190
|
+
override fun windowChanged() {
|
|
191
|
+
if (app.window === window)
|
|
192
|
+
mount.emitAtFrame("window") { window.describe().toString() }
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
window = Window(activity, spec.window, host)
|
|
196
|
+
activity.attach(
|
|
197
|
+
app.application,
|
|
198
|
+
spec.component,
|
|
199
|
+
Intent(Intent.ACTION_MAIN, setOf(Intent.CATEGORY_LAUNCHER), spec.launcher),
|
|
200
|
+
window,
|
|
201
|
+
lastNonConfigurationInstance,
|
|
202
|
+
)
|
|
203
|
+
activity.performCreate(state)
|
|
204
|
+
activity.performStart()
|
|
205
|
+
if (state != null) activity.performRestoreInstanceState(state)
|
|
206
|
+
activity.performPostCreate(state)
|
|
207
|
+
activity.performResume()
|
|
208
|
+
app.activity = activity
|
|
209
|
+
app.paused = false
|
|
210
|
+
app.stopped = false
|
|
211
|
+
app.state = null
|
|
212
|
+
app.reportedNight = isNight(activity.resources.configuration)
|
|
213
|
+
mount = PeachComposeMount(
|
|
214
|
+
app.density, app.fontScale, app.locale, app.gpu, app.stencilBits, app.fonts, app.invalidate,
|
|
215
|
+
night = isNight(activity.resources.configuration),
|
|
216
|
+
owners = componentActivity?.let(::ActivityOwners),
|
|
217
|
+
insets = DeviceInsets(window, app.insets),
|
|
218
|
+
clockRunning = { componentActivity == null || componentActivity.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) },
|
|
219
|
+
)
|
|
220
|
+
mount.setContent {
|
|
221
|
+
val content = @Composable { held?.let { view -> key(place) { view.Content() } }; Unit }
|
|
222
|
+
// the content view's tree resolves the activity as its back dispatcher owner
|
|
223
|
+
if (componentActivity != null)
|
|
224
|
+
CompositionLocalProvider(LocalOnBackPressedDispatcherOwner provides componentActivity, content = content)
|
|
225
|
+
else content()
|
|
226
|
+
}
|
|
227
|
+
if (app.mountId == 0) app.mountId = addMount(mount)
|
|
228
|
+
else {
|
|
229
|
+
// the relaunched window takes the device's bounds and reports its
|
|
230
|
+
// window, which the new instance may have set differently
|
|
231
|
+
mount.constraints = app.mount.constraints
|
|
232
|
+
mounts[app.mountId] = mount
|
|
233
|
+
mount.emitAtFrame("window") { window.describe().toString() }
|
|
234
|
+
app.invalidate(true)
|
|
235
|
+
}
|
|
236
|
+
app.mount = mount
|
|
237
|
+
app.window = window
|
|
238
|
+
window.addToWindowManager()
|
|
239
|
+
return checkNotNull(held) {
|
|
240
|
+
"${spec.component.flattenToShortString()} set no ComposeView as its content; Peach composes only ComposeView content"
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private fun runningApp() = checkNotNull(process) { "the app is not running" }
|
|
159
245
|
|
|
160
246
|
// the device's back: an activity with predictive back gets it through its
|
|
161
247
|
// window's back dispatch, where ComponentActivity's dispatcher takes it only
|
|
@@ -163,7 +249,7 @@ private var runningMount: PeachComposeMount? = null
|
|
|
163
249
|
// rest; without predictive back the key reaches onBackPressed. false when the
|
|
164
250
|
// task moved to the back, which the device then shows as home.
|
|
165
251
|
internal fun peachAppBack(predictive: Boolean): Boolean {
|
|
166
|
-
val activity =
|
|
252
|
+
val activity = runningApp().activity
|
|
167
253
|
activity.movedTaskToBack = false
|
|
168
254
|
val dispatcher = (activity as? ComponentActivity)?.onBackPressedDispatcher
|
|
169
255
|
if (!predictive) activity.onBackPressed()
|
|
@@ -172,20 +258,81 @@ internal fun peachAppBack(predictive: Boolean): Boolean {
|
|
|
172
258
|
return !activity.movedTaskToBack
|
|
173
259
|
}
|
|
174
260
|
|
|
175
|
-
//
|
|
176
|
-
//
|
|
261
|
+
// ActivityThread's pause and stop; from API 28 an activity that stops saves
|
|
262
|
+
// its state after onStop, kept for a relaunch until it resumes
|
|
263
|
+
private fun pauseAndStop(app: AppProcess) {
|
|
264
|
+
if (!app.paused) {
|
|
265
|
+
app.activity.performPause()
|
|
266
|
+
app.paused = true
|
|
267
|
+
}
|
|
268
|
+
if (!app.stopped) {
|
|
269
|
+
app.activity.performStop()
|
|
270
|
+
app.stopped = true
|
|
271
|
+
app.state = Bundle().also(app.activity::performSaveInstanceState)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// the task left the foreground (onPause, onStop, onSaveInstanceState) or came
|
|
276
|
+
// back to it (onRestart, onStart, onResume, onPostResume). a configuration
|
|
277
|
+
// that changed while it was stopped reaches it first: a relaunch in place of
|
|
278
|
+
// the restart, or onConfigurationChanged.
|
|
177
279
|
internal fun peachAppTask(foreground: Boolean) {
|
|
178
|
-
val
|
|
179
|
-
if (foreground)
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
280
|
+
val app = runningApp()
|
|
281
|
+
if (!foreground) return pauseAndStop(app)
|
|
282
|
+
if (reportConfiguration(app)) return
|
|
283
|
+
app.activity.performRestart()
|
|
284
|
+
app.activity.performResume()
|
|
285
|
+
app.paused = false
|
|
286
|
+
app.stopped = false
|
|
287
|
+
app.state = null
|
|
288
|
+
// the frame clock runs again once the activity is started
|
|
289
|
+
app.mount.invalidate(false)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// gives a visible activity the process configuration when it differs from the
|
|
293
|
+
// one last reported to it: a relaunch, or onConfigurationChanged under
|
|
294
|
+
// configChanges uiMode. true when it relaunched.
|
|
295
|
+
private fun reportConfiguration(app: AppProcess): Boolean {
|
|
296
|
+
val configuration = processResources.configuration
|
|
297
|
+
if (isNight(configuration) == app.reportedNight) return false
|
|
298
|
+
if (!app.spec.handlesUiMode) {
|
|
299
|
+
relaunch(app)
|
|
300
|
+
return true
|
|
301
|
+
}
|
|
302
|
+
app.reportedNight = isNight(configuration)
|
|
303
|
+
app.activity.performConfigurationChanged(Configuration().apply { setTo(configuration) })
|
|
304
|
+
app.mount.night = app.reportedNight
|
|
305
|
+
return false
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private fun isNight(configuration: Configuration) =
|
|
309
|
+
configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
|
|
310
|
+
|
|
311
|
+
// the device's night mode changed. the process's configuration changes in
|
|
312
|
+
// place and the Application hears it first; a visible activity then takes it.
|
|
313
|
+
// a stopped activity takes it once visible again, if it still differs.
|
|
314
|
+
internal fun peachAppNight(night: Boolean) {
|
|
315
|
+
val app = runningApp()
|
|
316
|
+
val configuration = processResources.configuration
|
|
317
|
+
if (isNight(configuration) == night) return
|
|
318
|
+
configuration.setTo(nightConfiguration(night))
|
|
319
|
+
app.application.onConfigurationChanged(Configuration().apply { setTo(configuration) })
|
|
320
|
+
if (!app.stopped) reportConfiguration(app)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ActivityThread's handleRelaunchActivityInner: pause and stop with the state
|
|
324
|
+
// saved, collect what the next instance keeps, destroy, then launch anew
|
|
325
|
+
private fun relaunch(app: AppProcess) {
|
|
326
|
+
val old = app.activity
|
|
327
|
+
val oldMount = app.mount
|
|
328
|
+
old.isChangingConfigurations = true
|
|
329
|
+
pauseAndStop(app)
|
|
330
|
+
val retained = old.retainNonConfigurationInstances()
|
|
331
|
+
old.performDestroy()
|
|
332
|
+
old.window.removeFromWindowManager()
|
|
333
|
+
try {
|
|
334
|
+
launchActivity(app, retained)
|
|
335
|
+
} finally {
|
|
336
|
+
oldMount.close()
|
|
190
337
|
}
|
|
191
338
|
}
|
|
@@ -7,6 +7,7 @@ import androidx.compose.runtime.mutableStateOf
|
|
|
7
7
|
import androidx.compose.runtime.remember
|
|
8
8
|
import androidx.compose.runtime.setValue
|
|
9
9
|
import androidx.compose.runtime.snapshots.Snapshot
|
|
10
|
+
import androidx.compose.ui.LocalSystemTheme
|
|
10
11
|
import androidx.compose.ui.geometry.Offset
|
|
11
12
|
import androidx.compose.ui.graphics.asComposeCanvas
|
|
12
13
|
import androidx.compose.ui.input.InputMode
|
|
@@ -44,6 +45,7 @@ import androidx.compose.ui.unit.LayoutDirection
|
|
|
44
45
|
import dev.onejs.one.OneNativeSourceViewDispatch
|
|
45
46
|
import kotlin.io.encoding.Base64
|
|
46
47
|
import kotlinx.coroutines.Dispatchers
|
|
48
|
+
import org.jetbrains.skiko.SystemTheme
|
|
47
49
|
import org.jetbrains.skia.BackendRenderTarget
|
|
48
50
|
import org.jetbrains.skia.Bitmap
|
|
49
51
|
import org.jetbrains.skia.Color
|
|
@@ -123,11 +125,14 @@ internal class PeachComposeMount(
|
|
|
123
125
|
private val stencilBits: Int,
|
|
124
126
|
private val fonts: FontFamily,
|
|
125
127
|
val invalidate: (Boolean) -> Unit,
|
|
128
|
+
night: Boolean,
|
|
126
129
|
owners: PlatformArchitectureComponentsOwner? = null,
|
|
127
130
|
insets: PlatformWindowInsets? = null,
|
|
128
131
|
private val clockRunning: () -> Boolean = { true },
|
|
129
132
|
) : AutoCloseable {
|
|
130
133
|
var props by mutableStateOf("")
|
|
134
|
+
// the configuration's night mode, which isSystemInDarkTheme reads
|
|
135
|
+
var night by mutableStateOf(night)
|
|
131
136
|
var constraints = Constraints()
|
|
132
137
|
set(value) { if (field != value) { field = value; needsMeasure = true } }
|
|
133
138
|
private val events = mutableListOf<JSONObject>()
|
|
@@ -150,7 +155,11 @@ internal class PeachComposeMount(
|
|
|
150
155
|
|
|
151
156
|
fun setContent(content: @Composable () -> Unit) {
|
|
152
157
|
try {
|
|
153
|
-
scene.setContent {
|
|
158
|
+
scene.setContent {
|
|
159
|
+
CompositionLocalProvider(LocalSystemTheme provides if (night) SystemTheme.DARK else SystemTheme.LIGHT) {
|
|
160
|
+
PeachAndroidFonts(fonts, content)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
154
163
|
} catch (failure: Throwable) {
|
|
155
164
|
close()
|
|
156
165
|
throw failure
|
|
@@ -289,7 +298,7 @@ internal fun robotoFamily(regular: String, medium: String, bold: String) = FontF
|
|
|
289
298
|
|
|
290
299
|
@JsExport
|
|
291
300
|
fun peachViewMount(source: String, view: String, hash: String, props: String,
|
|
292
|
-
density: Float, fontScale: Float, locale: String, backend: String, stencilBits: Int,
|
|
301
|
+
density: Float, fontScale: Float, locale: String, night: Boolean, backend: String, stencilBits: Int,
|
|
293
302
|
regular: String, medium: String, bold: String, invalidate: (Boolean) -> Unit): String {
|
|
294
303
|
try {
|
|
295
304
|
val dispatch = peachViewDispatcher(source)
|
|
@@ -297,7 +306,7 @@ fun peachViewMount(source: String, view: String, hash: String, props: String,
|
|
|
297
306
|
.put("error", "composable source $source changed; rebuild the app").toString()
|
|
298
307
|
require(backend == "graphite" || backend == "raster")
|
|
299
308
|
preparePeachDevice(locale)
|
|
300
|
-
val mount = PeachComposeMount(density, fontScale, locale, backend == "graphite", stencilBits, robotoFamily(regular, medium, bold), invalidate)
|
|
309
|
+
val mount = PeachComposeMount(density, fontScale, locale, backend == "graphite", stencilBits, robotoFamily(regular, medium, bold), invalidate, night)
|
|
301
310
|
mount.props = props
|
|
302
311
|
mount.setContent { dispatch.Content(view, mount.props, mount::emit) }
|
|
303
312
|
return JSONObject().put("ok", true).put("id", addMount(mount)).toString()
|
|
@@ -307,6 +316,9 @@ fun peachViewMount(source: String, view: String, hash: String, props: String,
|
|
|
307
316
|
}
|
|
308
317
|
|
|
309
318
|
@JsExport fun peachViewUpdate(id: Int, props: String) { checkNotNull(mounts[id]).props = props }
|
|
319
|
+
// a host activity that handles uiMode itself, as React Native's does, keeps
|
|
320
|
+
// its views and hands them the new configuration
|
|
321
|
+
@JsExport fun peachViewNight(id: Int, night: Boolean) { checkNotNull(mounts[id]).night = night }
|
|
310
322
|
@JsExport fun peachViewConstraints(id: Int, minWidth: Int, maxWidth: Int, minHeight: Int, maxHeight: Int) {
|
|
311
323
|
checkNotNull(mounts[id]).also {
|
|
312
324
|
it.constraints = Constraints(minWidth, maxWidth, minHeight, maxHeight)
|
package/kotlin/fork/README.md
CHANGED
|
@@ -10,6 +10,7 @@ builds until upstream supplies equivalent worker and native host hooks.
|
|
|
10
10
|
| --- | --- | --- |
|
|
11
11
|
| Compose `ui` | `Actuals.wasm.kt` | clock reads `globalThis.performance.now()` |
|
|
12
12
|
| Compose `ui` | `PlatformClipboard.web.kt` | worker-safe secure-context and clipboard reads |
|
|
13
|
+
| Compose `ui` | `SystemTheme.kt` | `LocalSystemTheme` visible to the host, which provides the device's night mode |
|
|
13
14
|
| Compose `ui-text` | `PlatformLocale.web.kt` | explicit `setPlatformLocaleProvider`; its default keeps upstream navigator behavior |
|
|
14
15
|
| Skiko | `OsArch.web.kt` | explicit `setHostOsProvider`; its default lazily retains upstream OS detection |
|
|
15
16
|
| Skiko | `Actuals.web.kt` | nanosecond clock reads `globalThis.performance.now()` |
|
|
@@ -105,3 +105,29 @@ index 7aa3e26..47e20a8 100644
|
|
|
105
105
|
|
|
106
106
|
/**
|
|
107
107
|
* https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API
|
|
108
|
+
diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/SystemTheme.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/SystemTheme.kt
|
|
109
|
+
index a14f182..9c4862d 100644
|
|
110
|
+
--- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/SystemTheme.kt
|
|
111
|
+
+++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/SystemTheme.kt
|
|
112
|
+
@@ -12,6 +12,11 @@
|
|
113
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
114
|
+
* See the License for the specific language governing permissions and
|
|
115
|
+
* limitations under the License.
|
|
116
|
+
+ *
|
|
117
|
+
+ * modified for Peach: the system theme composition local is visible to a
|
|
118
|
+
+ * native host, which provides the device's night mode to the scenes it owns,
|
|
119
|
+
+ * as later upstream releases expose it. see the fork record for the patch and
|
|
120
|
+
+ * upstream revision.
|
|
121
|
+
*/
|
|
122
|
+
|
|
123
|
+
package androidx.compose.ui
|
|
124
|
+
@@ -27,7 +32,8 @@ enum class SystemTheme {
|
|
125
|
+
Dark, Light, Unknown
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
-internal val LocalSystemTheme = staticCompositionLocalOf { currentSystemTheme }
|
|
129
|
+
+@InternalComposeUiApi
|
|
130
|
+
+val LocalSystemTheme = staticCompositionLocalOf { currentSystemTheme }
|
|
131
|
+
|
|
132
|
+
@InternalComposeUiApi
|
|
133
|
+
@Composable
|
package/kotlin/fork/fork.json
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"tag": "v1.13.0-alpha01",
|
|
19
19
|
"commit": "a9e47e8aa04c95cfe572ab4297c770f3cbfb234e",
|
|
20
20
|
"patch": "compose-worker-globals.patch",
|
|
21
|
-
"patchSha256": "
|
|
21
|
+
"patchSha256": "5c45b67b3028637e6eafa351db004f23ad50c6f00ac2b5e7738fd026ac93cef0"
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
"name": "skiko",
|
package/kotlin/toolchain.json
CHANGED
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
],
|
|
19
19
|
"composeLibraries": [
|
|
20
20
|
{
|
|
21
|
-
"name": "ui-wasm-js-1.13.0-alpha01-peach-
|
|
22
|
-
"url": "https://r2.rnxsim.com/peach-kotlin-fork/
|
|
23
|
-
"sha256": "
|
|
21
|
+
"name": "ui-wasm-js-1.13.0-alpha01-peach-8caddb83ba2f.klib",
|
|
22
|
+
"url": "https://r2.rnxsim.com/peach-kotlin-fork/8caddb83ba2f8dddd4c869eca936f0039d99c39c986f8af162e59eb27dbb92ed/ui-wasm-js-1.13.0-alpha01.klib",
|
|
23
|
+
"sha256": "8caddb83ba2f8dddd4c869eca936f0039d99c39c986f8af162e59eb27dbb92ed"
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
"name": "foundation-wasm-js-1.13.0-alpha01.klib",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peachsim",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.661",
|
|
4
4
|
"description": "Vite and Metro plugins, testing drivers, and SDK exports for peach.",
|
|
5
5
|
"author": "Tamagui LLC",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -303,6 +303,7 @@
|
|
|
303
303
|
"build:cli-binary": "bun scripts/build-cli-binary.ts",
|
|
304
304
|
"smoke:cli-binary": "bun scripts/smoke-cli-binary.ts",
|
|
305
305
|
"publish:cli-binary": "bun scripts/publish-cli-binaries.ts",
|
|
306
|
+
"publish:linux-browser-libs": "bun scripts/linux-browser-libs.ts publish",
|
|
306
307
|
"pack:smoke": "bun scripts/pack-smoke.ts"
|
|
307
308
|
},
|
|
308
309
|
"dependencies": {
|
package/src/android-theme.ts
CHANGED
|
@@ -165,3 +165,17 @@ export function resolveThemeAttribute(
|
|
|
165
165
|
// unset: the reader's own default applies, as PhoneWindow's getBoolean/getColor do
|
|
166
166
|
return undefined
|
|
167
167
|
}
|
|
168
|
+
|
|
169
|
+
// a framework color resource as context.getColor reads it, following
|
|
170
|
+
// references to other framework colors
|
|
171
|
+
export function frameworkColor(name: string, sources: ThemeSources): number {
|
|
172
|
+
let value = sources.android.colors[name]
|
|
173
|
+
while (value !== undefined && !value.startsWith('#')) {
|
|
174
|
+
const ref = /^@(?:android:)?color\/(.+)$/.exec(value)
|
|
175
|
+
if (!ref?.[1])
|
|
176
|
+
throw new Error(`@android:color/${name} has the unsupported value ${value}`)
|
|
177
|
+
value = sources.android.colors[ref[1]]
|
|
178
|
+
}
|
|
179
|
+
if (value === undefined) throw new Error(`@android:color/${name} is not defined`)
|
|
180
|
+
return color(value)
|
|
181
|
+
}
|
package/src/backend-origin.ts
CHANGED
|
@@ -46,6 +46,31 @@ export function buildBrowserAssetProxyUrl(targetUrl: string): string {
|
|
|
46
46
|
return `${proxyPath}?url=${encodeURIComponent(targetUrl)}`
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
export function resolveBrowserAssetProxyUrl(
|
|
50
|
+
targetUrl: string,
|
|
51
|
+
currentOrigin: string,
|
|
52
|
+
): string {
|
|
53
|
+
const proxyUrl = isBrowserAssetProxyUrl(targetUrl)
|
|
54
|
+
? targetUrl
|
|
55
|
+
: buildBrowserAssetProxyUrl(targetUrl)
|
|
56
|
+
// loopback assets belong to the local bridge; external bytes to the backend.
|
|
57
|
+
if (proxyUrl.startsWith('/__bundle-proxy?')) return proxyUrl
|
|
58
|
+
return new URL(proxyUrl, resolveContrastBackendOrigin(currentOrigin)).toString()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function isBrowserAssetProxyUrl(value: string): boolean {
|
|
62
|
+
try {
|
|
63
|
+
const url = new URL(value, DEFAULT_CONTRAST_BACKEND_ORIGIN)
|
|
64
|
+
return (
|
|
65
|
+
isContrastBackendOrigin(url.origin) &&
|
|
66
|
+
url.pathname === '/__fetch-proxy' &&
|
|
67
|
+
url.searchParams.has('url')
|
|
68
|
+
)
|
|
69
|
+
} catch {
|
|
70
|
+
return false
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
49
74
|
export function isContrastBackendHost(hostname: string): boolean {
|
|
50
75
|
const normalized = normalizeHostname(hostname)
|
|
51
76
|
return (
|