peachsim 0.1.659 → 0.1.660

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/cli/cli-update.ts +53 -21
  2. package/cli/commands/compat.ts +6 -2
  3. package/cli/commands/control.ts +12 -3
  4. package/cli/commands/inspect.ts +16 -14
  5. package/cli/commands/upgrade.ts +4 -2
  6. package/cli/current-sim.ts +4 -0
  7. package/dist-lib/agent-daemon-client.cjs +1 -1
  8. package/dist-lib/agent-events.cjs +1 -1
  9. package/dist-lib/agent-identity.cjs +1 -1
  10. package/dist-lib/agent-sessions.cjs +1 -1
  11. package/dist-lib/attached-projects.cjs +1 -1
  12. package/dist-lib/auth/shared-session.cjs +1 -1
  13. package/dist-lib/backend-origin.cjs +1 -1
  14. package/dist-lib/beta.cjs +1 -1
  15. package/dist-lib/beta.mjs +1 -1
  16. package/dist-lib/bridge-constants.cjs +1 -1
  17. package/dist-lib/bridge-contract-input.cjs +1 -1
  18. package/dist-lib/bridge-contract-input.mjs +1 -1
  19. package/dist-lib/bridge-contract.cjs +1 -1
  20. package/dist-lib/bridge-contract.mjs +1 -1
  21. package/dist-lib/capture-contract.cjs +1 -1
  22. package/dist-lib/capture-contract.mjs +1 -1
  23. package/dist-lib/cli-constants.cjs +1 -1
  24. package/dist-lib/cloud-contract.cjs +1 -1
  25. package/dist-lib/cloud-contract.mjs +1 -1
  26. package/dist-lib/cloud-test-args.cjs +1 -1
  27. package/dist-lib/cloud-test-args.mjs +1 -1
  28. package/dist-lib/cloud.cjs +1 -1
  29. package/dist-lib/cloud.mjs +1 -1
  30. package/dist-lib/config.cjs +1 -1
  31. package/dist-lib/detox/index.cjs +1 -1
  32. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  33. package/dist-lib/home-paths.cjs +1 -1
  34. package/dist-lib/host/bridge-host.cjs +1 -1
  35. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  36. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  37. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  38. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  39. package/dist-lib/host/websocket-proxy.cjs +1 -1
  40. package/dist-lib/index.cjs +5 -1
  41. package/dist-lib/jump-to-source-babel.cjs +1 -1
  42. package/dist-lib/jump-to-source-native.cjs +1 -1
  43. package/dist-lib/kotlin-host.cjs +5 -1
  44. package/dist-lib/menu.cjs +1 -1
  45. package/dist-lib/menu.mjs +1 -1
  46. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  47. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  48. package/dist-lib/metro-production-bundle.cjs +1 -1
  49. package/dist-lib/metro-production-bundle.mjs +1 -1
  50. package/dist-lib/metro.cjs +1 -1
  51. package/dist-lib/native-app-identity.cjs +1 -1
  52. package/dist-lib/native-app-identity.mjs +1 -1
  53. package/dist-lib/plist.cjs +1 -1
  54. package/dist-lib/plist.mjs +1 -1
  55. package/dist-lib/profiles.cjs +1 -1
  56. package/dist-lib/public-brand.cjs +1 -1
  57. package/dist-lib/react-native-host-modules.cjs +1 -1
  58. package/dist-lib/react-native-host-modules.mjs +1 -1
  59. package/dist-lib/render-mode.cjs +1 -1
  60. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  61. package/dist-lib/sdk.cjs +1 -1
  62. package/dist-lib/sdk.mjs +1 -1
  63. package/dist-lib/skills.cjs +8 -4
  64. package/dist-lib/swift-compile.cjs +1 -1
  65. package/dist-lib/swift-compile.mjs +1 -1
  66. package/dist-lib/swift-package.cjs +1 -1
  67. package/dist-lib/swift-package.mjs +1 -1
  68. package/dist-lib/swift-submit.cjs +1 -1
  69. package/dist-lib/swift-submit.mjs +1 -1
  70. package/dist-lib/swift.cjs +1 -1
  71. package/dist-lib/vite-swift.cjs +1 -1
  72. package/dist-lib/vite.cjs +1 -1
  73. package/kotlin/AppSources/android/app/Activity.kt +48 -3
  74. package/kotlin/AppSources/android/app/Application.kt +4 -0
  75. package/kotlin/AppSources/android/content/res/Configuration.kt +4 -0
  76. package/kotlin/AppSources/android/content/res/Resources.kt +3 -1
  77. package/kotlin/AppSources/androidx/activity/ComponentActivity.kt +36 -3
  78. package/kotlin/AppSources/dev/peach/app/PeachApp.kt +207 -73
  79. package/kotlin/ComposeSources/PeachComposeHost.kt +15 -3
  80. package/kotlin/fork/README.md +1 -0
  81. package/kotlin/fork/compose-worker-globals.patch +26 -0
  82. package/kotlin/fork/fork.json +1 -1
  83. package/kotlin/toolchain.json +3 -3
  84. package/package.json +1 -1
  85. package/src/kotlin-app.ts +7 -0
  86. package/src/kotlin-host.ts +4 -0
@@ -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,6 +48,9 @@ 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
56
  // the system bars the device delivers to the window's content, in pixels: all
@@ -71,6 +77,41 @@ private class ActivityOwners(activity: ComponentActivity) : PlatformArchitecture
71
77
  }
72
78
  }
73
79
 
80
+ // the device and the application one started app process keeps, and what
81
+ // ActivityThread's record holds for its one activity: whether it is paused or
82
+ // stopped, and the state it saved when it stopped, until it resumes
83
+ private class AppProcess(
84
+ val spec: PeachAppSpec,
85
+ val application: Application,
86
+ val density: Float, val fontScale: Float, val locale: String, val gpu: Boolean, val stencilBits: Int,
87
+ val fonts: FontFamily,
88
+ val statusBar: Int, val navigationBar: Int,
89
+ val invalidate: (Boolean) -> Unit,
90
+ ) {
91
+ lateinit var activity: Activity
92
+ lateinit var mount: PeachComposeMount
93
+ // the window the device presents, once its scene is in place
94
+ var window: Window? = null
95
+ var mountId = 0
96
+ // the held view's place among its window's views; a relaunched window's
97
+ // first view takes the place the last window's view held, as it takes
98
+ // that view's saved state on the device
99
+ var view = 0
100
+ var paused = false
101
+ var stopped = false
102
+ var state: Bundle? = null
103
+ // the night mode last reported to the activity: ActivityRecord compares
104
+ // the configuration with it before a relaunch or onConfigurationChanged
105
+ var reportedNight = false
106
+ }
107
+
108
+ private var process: AppProcess? = null
109
+
110
+ private fun nightConfiguration(night: Boolean) = Configuration().apply {
111
+ uiMode = Configuration.UI_MODE_TYPE_NORMAL or
112
+ if (night) Configuration.UI_MODE_NIGHT_YES else Configuration.UI_MODE_NIGHT_NO
113
+ }
114
+
74
115
  // a cold start in ActivityThread's order: the Application and its onCreate,
75
116
  // then the launcher activity attached to its window and launcher intent,
76
117
  // onCreate, onStart, onPostCreate, onResume and onPostResume, and only then the
@@ -85,65 +126,17 @@ internal fun peachStartApp(
85
126
  try {
86
127
  require(backend == "graphite" || backend == "raster")
87
128
  preparePeachDevice(locale)
88
- processResources = Resources(Configuration().apply {
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)
129
+ processResources = Resources(nightConfiguration(night))
93
130
  val application = spec.createApplication()
94
131
  application.onCreate()
95
- val activity = spec.createActivity()
96
- val componentActivity = activity as? ComponentActivity
97
- lateinit var window: Window
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,
132
+ val app = AppProcess(
133
+ spec, application, density, fontScale, locale, backend == "graphite", stencilBits,
134
+ robotoFamily(regular, medium, bold), statusBar, navigationBar, invalidate,
120
135
  )
121
- activity.performCreate(null)
122
- activity.performStart()
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
- }
136
+ process = app
137
+ val view = launchActivity(app, null)
145
138
  val params = view.layoutParams
146
- return window.describe().put("ok", true).put("id", mountId)
139
+ return app.activity.window.describe().put("ok", true).put("id", app.mountId)
147
140
  .put("wrapWidth", params?.width == ViewGroup.LayoutParams.WRAP_CONTENT)
148
141
  .put("wrapHeight", params?.height == ViewGroup.LayoutParams.WRAP_CONTENT)
149
142
  .toString()
@@ -153,9 +146,89 @@ internal fun peachStartApp(
153
146
  }
154
147
  }
155
148
 
156
- // the activity the app runs, once started, and the scene its window composes
157
- private var running: Activity? = null
158
- private var runningMount: PeachComposeMount? = null
149
+ // ActivityThread's handleLaunchActivity: a new instance of the launcher
150
+ // activity created from the state its last instance saved, if any, with what
151
+ // that instance retained, then started, restored, resumed and its window
152
+ // added. the window's scene takes the process's mount id, so the device keeps
153
+ // presenting it.
154
+ private fun launchActivity(app: AppProcess, lastNonConfigurationInstance: Any?): ComposeView {
155
+ val spec = app.spec
156
+ val activity = spec.createActivity()
157
+ val componentActivity = activity as? ComponentActivity
158
+ val state = app.state
159
+ lateinit var window: Window
160
+ lateinit var mount: PeachComposeMount
161
+ // the window's one scene composes the ComposeView it holds; a new
162
+ // view starts a new composition, as on the device. the key is the view's
163
+ // place, not the view, so a relaunched window's view lands on the keys its
164
+ // saved state was written under
165
+ var held by mutableStateOf<ComposeView?>(null)
166
+ var place by mutableIntStateOf((app.view - 1).coerceAtLeast(0))
167
+ val host = object : PeachWindowHost {
168
+ override fun compose(view: ComposeView): AutoCloseable {
169
+ held = view
170
+ place += 1
171
+ app.view = place
172
+ return AutoCloseable { if (held === view) held = null }
173
+ }
174
+
175
+ // a change before the window is presented reaches the device with
176
+ // the window itself
177
+ override fun windowChanged() {
178
+ if (app.window === window)
179
+ mount.emitAtFrame("window") { window.describe().toString() }
180
+ }
181
+ }
182
+ window = Window(activity, spec.window, host)
183
+ activity.attach(
184
+ app.application,
185
+ spec.component,
186
+ Intent(Intent.ACTION_MAIN, setOf(Intent.CATEGORY_LAUNCHER), spec.launcher),
187
+ window,
188
+ lastNonConfigurationInstance,
189
+ )
190
+ activity.performCreate(state)
191
+ activity.performStart()
192
+ if (state != null) activity.performRestoreInstanceState(state)
193
+ activity.performPostCreate(state)
194
+ activity.performResume()
195
+ app.activity = activity
196
+ app.paused = false
197
+ app.stopped = false
198
+ app.state = null
199
+ app.reportedNight = isNight(activity.resources.configuration)
200
+ mount = PeachComposeMount(
201
+ app.density, app.fontScale, app.locale, app.gpu, app.stencilBits, app.fonts, app.invalidate,
202
+ night = isNight(activity.resources.configuration),
203
+ owners = componentActivity?.let(::ActivityOwners),
204
+ insets = SystemBarInsets(window, app.statusBar, app.navigationBar),
205
+ clockRunning = { componentActivity == null || componentActivity.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) },
206
+ )
207
+ mount.setContent {
208
+ val content = @Composable { held?.let { view -> key(place) { view.Content() } }; Unit }
209
+ // the content view's tree resolves the activity as its back dispatcher owner
210
+ if (componentActivity != null)
211
+ CompositionLocalProvider(LocalOnBackPressedDispatcherOwner provides componentActivity, content = content)
212
+ else content()
213
+ }
214
+ if (app.mountId == 0) app.mountId = addMount(mount)
215
+ else {
216
+ // the relaunched window takes the device's bounds and reports its
217
+ // window, which the new instance may have set differently
218
+ mount.constraints = app.mount.constraints
219
+ mounts[app.mountId] = mount
220
+ mount.emitAtFrame("window") { window.describe().toString() }
221
+ app.invalidate(true)
222
+ }
223
+ app.mount = mount
224
+ app.window = window
225
+ window.addToWindowManager()
226
+ return checkNotNull(held) {
227
+ "${spec.component.flattenToShortString()} set no ComposeView as its content; Peach composes only ComposeView content"
228
+ }
229
+ }
230
+
231
+ private fun runningApp() = checkNotNull(process) { "the app is not running" }
159
232
 
160
233
  // the device's back: an activity with predictive back gets it through its
161
234
  // window's back dispatch, where ComponentActivity's dispatcher takes it only
@@ -163,7 +236,7 @@ private var runningMount: PeachComposeMount? = null
163
236
  // rest; without predictive back the key reaches onBackPressed. false when the
164
237
  // task moved to the back, which the device then shows as home.
165
238
  internal fun peachAppBack(predictive: Boolean): Boolean {
166
- val activity = checkNotNull(running) { "the app is not running" }
239
+ val activity = runningApp().activity
167
240
  activity.movedTaskToBack = false
168
241
  val dispatcher = (activity as? ComponentActivity)?.onBackPressedDispatcher
169
242
  if (!predictive) activity.onBackPressed()
@@ -172,20 +245,81 @@ internal fun peachAppBack(predictive: Boolean): Boolean {
172
245
  return !activity.movedTaskToBack
173
246
  }
174
247
 
175
- // the task left the foreground (onPause, onStop, onSaveInstanceState) or came back to it
176
- // (onRestart, onStart, onResume, onPostResume), as ActivityThread drives it
248
+ // ActivityThread's pause and stop; from API 28 an activity that stops saves
249
+ // its state after onStop, kept for a relaunch until it resumes
250
+ private fun pauseAndStop(app: AppProcess) {
251
+ if (!app.paused) {
252
+ app.activity.performPause()
253
+ app.paused = true
254
+ }
255
+ if (!app.stopped) {
256
+ app.activity.performStop()
257
+ app.stopped = true
258
+ app.state = Bundle().also(app.activity::performSaveInstanceState)
259
+ }
260
+ }
261
+
262
+ // the task left the foreground (onPause, onStop, onSaveInstanceState) or came
263
+ // back to it (onRestart, onStart, onResume, onPostResume). a configuration
264
+ // that changed while it was stopped reaches it first: a relaunch in place of
265
+ // the restart, or onConfigurationChanged.
177
266
  internal fun peachAppTask(foreground: Boolean) {
178
- val activity = checkNotNull(running) { "the app is not running" }
179
- if (foreground) {
180
- activity.performRestart()
181
- activity.performResume()
182
- // the frame clock runs again once the activity is started
183
- checkNotNull(runningMount).invalidate(false)
184
- } else {
185
- activity.performPause()
186
- activity.performStop()
187
- // from API 28 an activity that stops saves its state after onStop;
188
- // ActivityThread keeps it for a relaunch, which no path here takes yet
189
- activity.performSaveInstanceState(Bundle())
267
+ val app = runningApp()
268
+ if (!foreground) return pauseAndStop(app)
269
+ if (reportConfiguration(app)) return
270
+ app.activity.performRestart()
271
+ app.activity.performResume()
272
+ app.paused = false
273
+ app.stopped = false
274
+ app.state = null
275
+ // the frame clock runs again once the activity is started
276
+ app.mount.invalidate(false)
277
+ }
278
+
279
+ // gives a visible activity the process configuration when it differs from the
280
+ // one last reported to it: a relaunch, or onConfigurationChanged under
281
+ // configChanges uiMode. true when it relaunched.
282
+ private fun reportConfiguration(app: AppProcess): Boolean {
283
+ val configuration = processResources.configuration
284
+ if (isNight(configuration) == app.reportedNight) return false
285
+ if (!app.spec.handlesUiMode) {
286
+ relaunch(app)
287
+ return true
288
+ }
289
+ app.reportedNight = isNight(configuration)
290
+ app.activity.performConfigurationChanged(Configuration().apply { setTo(configuration) })
291
+ app.mount.night = app.reportedNight
292
+ return false
293
+ }
294
+
295
+ private fun isNight(configuration: Configuration) =
296
+ configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
297
+
298
+ // the device's night mode changed. the process's configuration changes in
299
+ // place and the Application hears it first; a visible activity then takes it.
300
+ // a stopped activity takes it once visible again, if it still differs.
301
+ internal fun peachAppNight(night: Boolean) {
302
+ val app = runningApp()
303
+ val configuration = processResources.configuration
304
+ if (isNight(configuration) == night) return
305
+ configuration.setTo(nightConfiguration(night))
306
+ app.application.onConfigurationChanged(Configuration().apply { setTo(configuration) })
307
+ if (!app.stopped) reportConfiguration(app)
308
+ }
309
+
310
+ // ActivityThread's handleRelaunchActivityInner: pause and stop with the state
311
+ // saved, collect what the next instance keeps, destroy, then launch anew
312
+ private fun relaunch(app: AppProcess) {
313
+ val old = app.activity
314
+ val oldMount = app.mount
315
+ old.isChangingConfigurations = true
316
+ pauseAndStop(app)
317
+ val retained = old.retainNonConfigurationInstances()
318
+ old.performDestroy()
319
+ old.window.removeFromWindowManager()
320
+ try {
321
+ launchActivity(app, retained)
322
+ } finally {
323
+ oldMount.close()
190
324
  }
191
325
  }
@@ -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 { PeachAndroidFonts(fonts, content) }
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)
@@ -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
@@ -18,7 +18,7 @@
18
18
  "tag": "v1.13.0-alpha01",
19
19
  "commit": "a9e47e8aa04c95cfe572ab4297c770f3cbfb234e",
20
20
  "patch": "compose-worker-globals.patch",
21
- "patchSha256": "51fa45bda1e0aae7db2eab1b056def406352958257e1d3b899c5c3d8aa5edddb"
21
+ "patchSha256": "5c45b67b3028637e6eafa351db004f23ad50c6f00ac2b5e7738fd026ac93cef0"
22
22
  },
23
23
  {
24
24
  "name": "skiko",
@@ -18,9 +18,9 @@
18
18
  ],
19
19
  "composeLibraries": [
20
20
  {
21
- "name": "ui-wasm-js-1.13.0-alpha01-peach-0e3c251301e0.klib",
22
- "url": "https://r2.rnxsim.com/peach-kotlin-fork/0e3c251301e0df405e50d0aa7d45b401c1aa26f7ed0a7001c6ce2bc68f5d667b/ui-wasm-js-1.13.0-alpha01.klib",
23
- "sha256": "0e3c251301e0df405e50d0aa7d45b401c1aa26f7ed0a7001c6ce2bc68f5d667b"
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.659",
3
+ "version": "0.1.660",
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",
package/src/kotlin-app.ts CHANGED
@@ -32,6 +32,9 @@ export interface KotlinAppProject {
32
32
  // back reaches the activity through its window's back dispatch rather than
33
33
  // the back key's onBackPressed
34
34
  predictiveBack: boolean
35
+ // the activity's configChanges includes uiMode: a night mode change reaches
36
+ // onConfigurationChanged instead of relaunching it
37
+ handlesUiMode: boolean
35
38
  }
36
39
 
37
40
  export interface ComposeAndroidPins {
@@ -402,6 +405,9 @@ export function readKotlinApp(
402
405
  (themeOwner?.attributes[`${ANDROID_NS}enableOnBackInvokedCallback`] ??
403
406
  application.attributes[`${ANDROID_NS}enableOnBackInvokedCallback`] ??
404
407
  String(targetSdk > 35)) === 'true'
408
+ const handlesUiMode = (themeOwner?.attributes[`${ANDROID_NS}configChanges`] ?? '')
409
+ .split('|')
410
+ .includes('uiMode')
405
411
 
406
412
  const sources: string[] = []
407
413
  for (const set of ['java', 'kotlin']) {
@@ -432,5 +438,6 @@ export function readKotlinApp(
432
438
  applicationClass: applicationName ? className(applicationName) : null,
433
439
  launcher: { name: launcherName, targetClass },
434
440
  predictiveBack,
441
+ handlesUiMode,
435
442
  }
436
443
  }
@@ -349,6 +349,7 @@ export async function kotlinAppArtifact(
349
349
  ' ),',
350
350
  ` { ${app.applicationClass ?? 'android.app.Application'}() },`,
351
351
  ` { ${app.launcher.targetClass}() },`,
352
+ ` ${app.handlesUiMode},`,
352
353
  ' ),',
353
354
  ' density, fontScale, locale, backend, stencilBits, regular, medium, bold, statusBar, navigationBar, night, invalidate,',
354
355
  ')',
@@ -359,6 +360,9 @@ export async function kotlinAppArtifact(
359
360
  '@JsExport',
360
361
  'fun peachAppTask(foreground: Boolean) = dev.peach.app.peachAppTask(foreground)',
361
362
  '',
363
+ '@JsExport',
364
+ 'fun peachAppNight(night: Boolean) = dev.peach.app.peachAppNight(night)',
365
+ '',
362
366
  ].join('\n'),
363
367
  }
364
368
  const shims = [