peachsim 0.1.657 → 0.1.658

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 (105) hide show
  1. package/README.md +21 -2
  2. package/cli/app-config.ts +25 -9
  3. package/cli/bridge-flow-runner.ts +10 -23
  4. package/cli/commands/compat.ts +234 -10
  5. package/cli/commands/do-chain.ts +5 -1
  6. package/cli/commands/inspect/actions.ts +33 -76
  7. package/cli/commands/inspect/resolve-target.ts +67 -2
  8. package/cli/commands/inspect.ts +16 -1
  9. package/cli/drivers/playwright-provisioning.ts +95 -0
  10. package/cli/ws-bridge.ts +7 -7
  11. package/dist-lib/agent-daemon-client.cjs +1 -1
  12. package/dist-lib/agent-events.cjs +1 -1
  13. package/dist-lib/agent-identity.cjs +1 -1
  14. package/dist-lib/agent-sessions.cjs +1 -1
  15. package/dist-lib/attached-projects.cjs +1 -1
  16. package/dist-lib/auth/shared-session.cjs +1 -1
  17. package/dist-lib/backend-origin.cjs +1 -1
  18. package/dist-lib/beta.cjs +1 -1
  19. package/dist-lib/beta.mjs +1 -1
  20. package/dist-lib/bridge-constants.cjs +1 -1
  21. package/dist-lib/bridge-contract-input.cjs +9 -1
  22. package/dist-lib/bridge-contract-input.mjs +9 -1
  23. package/dist-lib/bridge-contract.cjs +1 -1
  24. package/dist-lib/bridge-contract.mjs +1 -1
  25. package/dist-lib/capture-contract.cjs +1 -1
  26. package/dist-lib/capture-contract.mjs +1 -1
  27. package/dist-lib/cli-constants.cjs +1 -1
  28. package/dist-lib/cloud-contract.cjs +1 -1
  29. package/dist-lib/cloud-contract.mjs +1 -1
  30. package/dist-lib/cloud-test-args.cjs +1 -1
  31. package/dist-lib/cloud-test-args.mjs +1 -1
  32. package/dist-lib/cloud.cjs +1 -1
  33. package/dist-lib/cloud.mjs +1 -1
  34. package/dist-lib/config.cjs +3 -2
  35. package/dist-lib/detox/index.cjs +1 -1
  36. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  37. package/dist-lib/home-paths.cjs +1 -1
  38. package/dist-lib/host/bridge-host.cjs +78 -35
  39. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  40. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  41. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  42. package/dist-lib/host/replacement-module-handler.cjs +51 -8
  43. package/dist-lib/host/websocket-proxy.cjs +1 -1
  44. package/dist-lib/index.cjs +317 -70
  45. package/dist-lib/jump-to-source-babel.cjs +1 -1
  46. package/dist-lib/jump-to-source-native.cjs +1 -1
  47. package/dist-lib/kotlin-host.cjs +8 -4
  48. package/dist-lib/menu.cjs +1 -1
  49. package/dist-lib/menu.mjs +1 -1
  50. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  51. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  52. package/dist-lib/metro-production-bundle.cjs +1 -1
  53. package/dist-lib/metro-production-bundle.mjs +1 -1
  54. package/dist-lib/metro.cjs +300 -29
  55. package/dist-lib/native-app-identity.cjs +58 -25
  56. package/dist-lib/native-app-identity.mjs +57 -25
  57. package/dist-lib/plist.cjs +64 -0
  58. package/dist-lib/plist.mjs +29 -0
  59. package/dist-lib/profiles.cjs +1 -1
  60. package/dist-lib/public-brand.cjs +1 -1
  61. package/dist-lib/react-native-host-modules.cjs +1 -1
  62. package/dist-lib/react-native-host-modules.mjs +1 -1
  63. package/dist-lib/render-mode.cjs +1 -1
  64. package/dist-lib/scripts/dev-server-scanner.cjs +2 -2
  65. package/dist-lib/sdk.cjs +36 -49
  66. package/dist-lib/sdk.mjs +36 -49
  67. package/dist-lib/skills.cjs +250 -131
  68. package/dist-lib/swift-compile.cjs +1 -1
  69. package/dist-lib/swift-compile.mjs +1 -1
  70. package/dist-lib/swift-package.cjs +1 -1
  71. package/dist-lib/swift-package.mjs +1 -1
  72. package/dist-lib/swift-submit.cjs +1 -1
  73. package/dist-lib/swift-submit.mjs +1 -1
  74. package/dist-lib/swift.cjs +1 -1
  75. package/dist-lib/vite-swift.cjs +1 -1
  76. package/dist-lib/vite.cjs +51 -8
  77. package/kotlin/AppSources/android/app/UiModeManager.kt +9 -0
  78. package/kotlin/AppSources/android/content/Context.kt +9 -2
  79. package/kotlin/AppSources/android/content/res/Configuration.kt +16 -0
  80. package/kotlin/AppSources/android/content/res/Resources.kt +7 -0
  81. package/kotlin/AppSources/android/graphics/Color.kt +13 -0
  82. package/kotlin/AppSources/android/view/View.kt +2 -0
  83. package/kotlin/AppSources/android/view/Window.kt +99 -8
  84. package/kotlin/AppSources/android/view/WindowInsetsController.kt +12 -0
  85. package/kotlin/AppSources/android/view/WindowManager.kt +16 -0
  86. package/kotlin/AppSources/androidx/activity/EdgeToEdge.kt +71 -0
  87. package/kotlin/AppSources/androidx/core/view/WindowCompat.kt +32 -0
  88. package/kotlin/AppSources/dev/peach/app/PeachApp.kt +16 -11
  89. package/kotlin/AppSources/dev/peach/app/PeachWindowHost.kt +2 -1
  90. package/kotlin/ComposeSources/PeachComposeHost.kt +11 -1
  91. package/package.json +8 -1
  92. package/skills/peach-setup/SKILL.md +27 -0
  93. package/src/android-theme.ts +167 -0
  94. package/src/android-xml.ts +70 -0
  95. package/src/bridge-contract-input.ts +10 -1
  96. package/src/bridge-contract.ts +7 -1
  97. package/src/config.ts +8 -0
  98. package/src/generated/android-framework-themes.json +6303 -0
  99. package/src/host/replacement-module-handler.ts +33 -0
  100. package/src/index.ts +1 -0
  101. package/src/kotlin-app.ts +75 -61
  102. package/src/kotlin-host.ts +7 -3
  103. package/src/metro-plugin.ts +46 -3
  104. package/src/native-app-identity.ts +75 -26
  105. package/src/sim-client.ts +2 -4
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.657 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.657 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // src/swift-compile.ts
4
4
  import { execFile as execFile2 } from "node:child_process";
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.657 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.657 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // src/swift-package.ts
4
4
  function isSwiftManifestFile(file) {
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.657 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.657 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // src/swift-submit.ts
4
4
  var SWIFT_COMPILE_PATH = "/api/swift-compile";
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.657 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.657 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
package/dist-lib/vite.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.657 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -184,6 +184,7 @@ function isDaemonLockfileFresh(lock, now = Date.now()) {
184
184
  }
185
185
 
186
186
  // src/host/replacement-module-handler.ts
187
+ var import_node_fs3 = require("node:fs");
187
188
  var import_promises = __toESM(require("node:fs/promises"), 1);
188
189
  var import_node_path3 = __toESM(require("node:path"), 1);
189
190
 
@@ -313,12 +314,15 @@ function transformsFor(filePath) {
313
314
  );
314
315
  }
315
316
  }
317
+ function isLocalSpecifier(specifier) {
318
+ return specifier.startsWith("./") || specifier.startsWith("../");
319
+ }
316
320
  function isContrastKitSpecifier(specifier) {
317
321
  return specifier === "contrast-kit" || specifier.startsWith("contrast-kit/");
318
322
  }
319
323
  function unsupportedSpecifier(specifier) {
320
324
  return new Error(
321
- `replacement modules may only import react, react-native, contrast-kit, and the packages rnx stubs for this app; '${specifier}' is none of those. vendor the code into the replacement file, or add the package to peach.config so the simulator bridges it.`
325
+ `replacement modules may only import react, react-native, contrast-kit, the packages rnx stubs for this app, and other project files by relative path; '${specifier}' is none of those. vendor the code into a project file, or add the package to peach.config so the simulator bridges it.`
322
326
  );
323
327
  }
324
328
  function bridgeDeclaration(name, specifier) {
@@ -343,11 +347,9 @@ function compileReplacementModule(input) {
343
347
  const transformed = (0, import_dist.transform)(input.source, {
344
348
  transforms: transformsFor(input.filePath),
345
349
  filePath: input.filePath,
346
- // classic runtime, matching what the app's own Metro build does for a file
347
- // that has no automatic-runtime pragma: the file imports React itself.
348
- jsxRuntime: "classic",
349
- jsxPragma: "React.createElement",
350
- jsxFragmentPragma: "React.Fragment",
350
+ // the automatic runtime, as @react-native/babel-preset and babel-preset-expo
351
+ // compile the app's own files: jsx needs no React import.
352
+ jsxRuntime: "automatic",
351
353
  production: true,
352
354
  sourceMapOptions: { compiledFilename: input.filePath }
353
355
  });
@@ -374,6 +376,13 @@ function compileReplacementModule(input) {
374
376
  return name;
375
377
  };
376
378
  const edits = [];
379
+ const pointAtLocalModule = (specifierToken, specifier) => {
380
+ edits.push({
381
+ start: specifierToken.start,
382
+ end: specifierToken.end,
383
+ text: JSON.stringify(input.localModuleUrl(specifier))
384
+ });
385
+ };
377
386
  for (let index = 0; index < tokens.length; index++) {
378
387
  const token = tokens[index];
379
388
  if (token.type === EXPORT_KEYWORD) {
@@ -393,6 +402,10 @@ function compileReplacementModule(input) {
393
402
  }
394
403
  if (fromAt > 0 && tokens[fromAt] && read(tokens[fromAt]) === "from" && tokens[fromAt + 1]?.type === import_types.TokenType.string) {
395
404
  const specifier2 = readSpecifier(tokens[fromAt + 1]);
405
+ if (isLocalSpecifier(specifier2)) {
406
+ pointAtLocalModule(tokens[fromAt + 1], specifier2);
407
+ continue;
408
+ }
396
409
  throw new Error(
397
410
  `a replacement module cannot re-export from another module, and this one re-exports from '${specifier2}'. import what you need and export the binding.`
398
411
  );
@@ -445,6 +458,10 @@ function compileReplacementModule(input) {
445
458
  const specifierToken = tokens[cursor];
446
459
  if (!specifierToken) throw new Error("replacement module import is unterminated");
447
460
  const specifier = readSpecifier(specifierToken);
461
+ if (isLocalSpecifier(specifier)) {
462
+ pointAtLocalModule(specifierToken, specifier);
463
+ continue;
464
+ }
448
465
  if (specifier !== "react" && specifier !== "react-native" && !JSX_RUNTIME_SPECIFIERS.has(specifier) && !isContrastKitSpecifier(specifier) && !input.runtimePackages.has(specifier)) {
449
466
  throw unsupportedSpecifier(specifier);
450
467
  }
@@ -501,6 +518,25 @@ function sendJavaScript(req, res, source) {
501
518
  });
502
519
  res.end(req.method === "HEAD" ? void 0 : source);
503
520
  }
521
+ function resolveLocalImport(fromFile, specifier) {
522
+ const base = import_node_path3.default.resolve(import_node_path3.default.dirname(fromFile), specifier);
523
+ const candidates = [
524
+ ...REPLACEMENT_MODULE_EXTENSIONS.some(
525
+ (ext) => ext === import_node_path3.default.extname(base).toLowerCase()
526
+ ) ? [base] : [],
527
+ ...REPLACEMENT_MODULE_EXTENSIONS.map((ext) => base + ext),
528
+ ...REPLACEMENT_MODULE_EXTENSIONS.map((ext) => import_node_path3.default.join(base, `index${ext}`))
529
+ ];
530
+ const found = candidates.find(
531
+ (candidate) => (0, import_node_fs3.statSync)(candidate, { throwIfNoEntry: false })?.isFile()
532
+ );
533
+ if (!found) {
534
+ throw new Error(
535
+ `'${specifier}' imported by ${fromFile} matches no ${REPLACEMENT_MODULE_EXTENSIONS.join(", ")} file`
536
+ );
537
+ }
538
+ return found;
539
+ }
504
540
  async function handleReplacementModuleRequest(req, res, options = {}) {
505
541
  const rawUrl = req.url || "";
506
542
  let url;
@@ -582,7 +618,14 @@ async function handleReplacementModuleRequest(req, res, options = {}) {
582
618
  const compiled = compileReplacementModule({
583
619
  filePath: resolvedTargetPath,
584
620
  source: await import_promises.default.readFile(resolvedTargetPath, "utf8"),
585
- runtimePackages
621
+ runtimePackages,
622
+ // the imported file's url is this request's with only its path changed,
623
+ // so the token, cache key and runtime packages carry over.
624
+ localModuleUrl: (specifier) => {
625
+ const local = new URL(url.href);
626
+ local.searchParams.set("path", resolveLocalImport(targetPath, specifier));
627
+ return `${local.pathname}${local.search}`;
628
+ }
586
629
  });
587
630
  const map = Buffer.from(JSON.stringify(compiled.sourceMap), "utf8").toString("base64");
588
631
  sendJavaScript(
@@ -0,0 +1,9 @@
1
+ package android.app
2
+
3
+ class UiModeManager private constructor() {
4
+ companion object {
5
+ const val MODE_NIGHT_AUTO = 0
6
+ const val MODE_NIGHT_NO = 1
7
+ const val MODE_NIGHT_YES = 2
8
+ }
9
+ }
@@ -1,5 +1,12 @@
1
1
  package android.content
2
2
 
3
- abstract class Context
3
+ import android.content.res.Resources
4
+ import android.content.res.processResources
4
5
 
5
- open class ContextWrapper : Context()
6
+ abstract class Context {
7
+ abstract val resources: Resources
8
+ }
9
+
10
+ open class ContextWrapper : Context() {
11
+ override val resources: Resources get() = processResources
12
+ }
@@ -0,0 +1,16 @@
1
+ package android.content.res
2
+
3
+ // the device configuration an app reads through its resources: a phone's
4
+ // normal ui mode type, night or not as the device's dark theme is set
5
+ class Configuration {
6
+ var uiMode: Int = UI_MODE_TYPE_NORMAL or UI_MODE_NIGHT_NO
7
+
8
+ companion object {
9
+ const val UI_MODE_TYPE_MASK = 0x0f
10
+ const val UI_MODE_TYPE_NORMAL = 0x01
11
+ const val UI_MODE_NIGHT_MASK = 0x30
12
+ const val UI_MODE_NIGHT_UNDEFINED = 0x00
13
+ const val UI_MODE_NIGHT_NO = 0x10
14
+ const val UI_MODE_NIGHT_YES = 0x20
15
+ }
16
+ }
@@ -0,0 +1,7 @@
1
+ package android.content.res
2
+
3
+ class Resources internal constructor(val configuration: Configuration)
4
+
5
+ // the app process's resources, set once the device configuration is known at
6
+ // cold start; every context in the process reads the same configuration
7
+ internal lateinit var processResources: Resources
@@ -0,0 +1,13 @@
1
+ package android.graphics
2
+
3
+ // the packed ARGB ints android.graphics.Color names and builds
4
+ object Color {
5
+ const val BLACK = -0x1000000
6
+ const val WHITE = -0x1
7
+ const val TRANSPARENT = 0
8
+
9
+ fun argb(alpha: Int, red: Int, green: Int, blue: Int): Int =
10
+ (alpha shl 24) or (red shl 16) or (green shl 8) or blue
11
+
12
+ fun alpha(color: Int): Int = color ushr 24
13
+ }
@@ -1,8 +1,10 @@
1
1
  package android.view
2
2
 
3
3
  import android.content.Context
4
+ import android.content.res.Resources
4
5
 
5
6
  open class View(val context: Context) {
7
+ val resources: Resources get() = context.resources
6
8
  var parent: ViewGroup? = null
7
9
  internal set
8
10
  var layoutParams: ViewGroup.LayoutParams? = null
@@ -1,33 +1,124 @@
1
1
  package android.view
2
2
 
3
3
  import android.content.Context
4
+ import android.graphics.Color
4
5
  import android.widget.FrameLayout
5
6
  import androidx.compose.runtime.getValue
6
7
  import androidx.compose.runtime.mutableStateOf
7
8
  import androidx.compose.runtime.setValue
8
9
  import dev.peach.app.PeachWindowHost
10
+ import org.json.JSONObject
11
+
12
+ // what PhoneWindow.generateLayout read from the activity's theme
13
+ internal class WindowTheme(
14
+ val edgeToEdgeEnforced: Boolean,
15
+ val statusBarColor: Int,
16
+ val statusBarContrastEnforced: Boolean,
17
+ val navigationBarContrastEnforced: Boolean,
18
+ val lightStatusBar: Boolean,
19
+ val lightNavigationBar: Boolean,
20
+ )
21
+
22
+ // DecorView's light contrast scrim behind a transparent bar
23
+ private const val SCRIM_LIGHT = -0x19000001
9
24
 
10
25
  // PhoneWindow's content parent (android.R.id.content) inside its decor. the
11
26
  // window is added, and its views attached, after the activity resumes.
12
27
  class Window internal constructor(
13
28
  context: Context,
14
- private val edgeToEdgeEnforced: Boolean,
29
+ theme: WindowTheme,
15
30
  internal val host: PeachWindowHost,
16
31
  ) {
32
+ private val edgeToEdgeEnforced = theme.edgeToEdgeEnforced
17
33
  internal val contentParent = FrameLayout(context)
18
34
  val decorView: View get() = contentParent
35
+ val attributes = WindowManager.LayoutParams()
19
36
 
20
- // PhoneWindow fits system windows unless told otherwise, and applies that
21
- // only while edge to edge is not enforced for the app's target SDK
22
- private var decorFitsSystemWindows by mutableStateOf(true)
23
- internal val fitsSystemWindows: Boolean get() = decorFitsSystemWindows && !edgeToEdgeEnforced
37
+ // PhoneWindow fits system windows until told otherwise; an enforced edge
38
+ // to edge window never does, and ignores the request
39
+ private var fits by mutableStateOf(!edgeToEdgeEnforced)
40
+ internal val fitsSystemWindows: Boolean get() = fits
24
41
 
25
42
  fun setDecorFitsSystemWindows(decorFitsSystemWindows: Boolean) {
26
- val before = fitsSystemWindows
27
- this.decorFitsSystemWindows = decorFitsSystemWindows
28
- if (fitsSystemWindows != before) host.windowChanged()
43
+ if (edgeToEdgeEnforced || fits == decorFitsSystemWindows) return
44
+ fits = decorFitsSystemWindows
45
+ host.windowChanged()
29
46
  }
30
47
 
48
+ fun decorFitsSystemWindows(): Boolean = fits
49
+
50
+ // the theme's statusBarColor, transparent while edge to edge is enforced,
51
+ // where setting it does nothing
52
+ private var statusBar = if (edgeToEdgeEnforced) Color.TRANSPARENT else theme.statusBarColor
53
+
54
+ var statusBarColor: Int
55
+ get() = statusBar
56
+ set(color) {
57
+ if (edgeToEdgeEnforced || color == statusBar) return
58
+ statusBar = color
59
+ host.windowChanged()
60
+ }
61
+
62
+ fun getStatusBarColor(): Int = statusBarColor
63
+ fun setStatusBarColor(color: Int) { statusBarColor = color }
64
+
65
+ // the theme's navigation bar color resolves through device configuration
66
+ // peach kotlin does not model, so it reads only once the app sets it; an
67
+ // enforced edge to edge window reports transparent
68
+ private var navigationBar: Int? = null
69
+
70
+ var navigationBarColor: Int
71
+ get() = if (edgeToEdgeEnforced) Color.TRANSPARENT
72
+ else checkNotNull(navigationBar) { "the theme's navigation bar color is not modeled; read it after setting it" }
73
+ set(color) { navigationBar = color }
74
+
75
+ fun getNavigationBarColor(): Int = navigationBarColor
76
+ fun setNavigationBarColor(color: Int) { navigationBarColor = color }
77
+
78
+ var isStatusBarContrastEnforced: Boolean = theme.statusBarContrastEnforced
79
+ set(value) {
80
+ if (field == value) return
81
+ field = value
82
+ host.windowChanged()
83
+ }
84
+
85
+ var isNavigationBarContrastEnforced: Boolean = theme.navigationBarContrastEnforced
86
+
87
+ fun setStatusBarContrastEnforced(enforced: Boolean) { isStatusBarContrastEnforced = enforced }
88
+ fun setNavigationBarContrastEnforced(enforced: Boolean) { isNavigationBarContrastEnforced = enforced }
89
+
90
+ val insetsController: WindowInsetsController = object : WindowInsetsController {
91
+ private var appearance =
92
+ (if (theme.lightStatusBar) WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS else 0) or
93
+ (if (theme.lightNavigationBar) WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS else 0)
94
+
95
+ override val systemBarsAppearance: Int get() = appearance
96
+
97
+ override fun setSystemBarsAppearance(appearance: Int, mask: Int) {
98
+ val next = (this.appearance and mask.inv()) or (appearance and mask)
99
+ if (next == this.appearance) return
100
+ this.appearance = next
101
+ host.windowChanged()
102
+ }
103
+ }
104
+
105
+ // DecorView.calculateBarColor for the status bar of a window that draws
106
+ // its bar backgrounds: a transparent color under enforced contrast shows
107
+ // the scrim for the bar's appearance
108
+ private fun drawnStatusBarColor(): Int {
109
+ if (!isStatusBarContrastEnforced || Color.alpha(statusBar) != 0) return statusBar
110
+ check(insetsController.systemBarsAppearance and WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS != 0) {
111
+ "a dark status bar's contrast scrim is the device's dynamic system_bar_background_semi_transparent, which peach kotlin does not model"
112
+ }
113
+ return SCRIM_LIGHT
114
+ }
115
+
116
+ // what the decor draws for this window: its inset policy and its status
117
+ // bar color as unsigned ARGB
118
+ internal fun describe(): JSONObject = JSONObject()
119
+ .put("decorFitsSystemWindows", fits)
120
+ .put("statusBarColor", drawnStatusBarColor().toLong() and 0xffffffffL)
121
+
31
122
  // Activity.setContentView(View) fills the content parent
32
123
  internal fun setContentView(view: View, params: ViewGroup.LayoutParams) {
33
124
  contentParent.removeAllViews()
@@ -0,0 +1,12 @@
1
+ package android.view
2
+
3
+ // the window's requested system bar appearance: light bars ask for dark icons
4
+ interface WindowInsetsController {
5
+ fun setSystemBarsAppearance(appearance: Int, mask: Int)
6
+ val systemBarsAppearance: Int
7
+
8
+ companion object {
9
+ const val APPEARANCE_LIGHT_STATUS_BARS = 1 shl 3
10
+ const val APPEARANCE_LIGHT_NAVIGATION_BARS = 1 shl 4
11
+ }
12
+ }
@@ -0,0 +1,16 @@
1
+ package android.view
2
+
3
+ interface WindowManager {
4
+ // the window attributes edge to edge code sets; the cutout mode is held as
5
+ // the app sets it
6
+ class LayoutParams {
7
+ var layoutInDisplayCutoutMode: Int = LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
8
+
9
+ companion object {
10
+ const val LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT = 0
11
+ const val LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES = 1
12
+ const val LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER = 2
13
+ const val LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS = 3
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,71 @@
1
+ package androidx.activity
2
+
3
+ import android.app.UiModeManager
4
+ import android.content.res.Configuration
5
+ import android.content.res.Resources
6
+ import android.graphics.Color
7
+ import android.view.WindowManager
8
+ import androidx.core.view.WindowCompat
9
+ import androidx.core.view.WindowInsetsControllerCompat
10
+
11
+ // androidx.activity 1.9.3's EdgeToEdge as it runs on Peach's API 37 device,
12
+ // where it takes EdgeToEdgeApi30: the decor stops fitting system windows, the
13
+ // bars take each style's scrim (transparent for auto styles), status bar
14
+ // contrast is off, navigation bar contrast follows an auto style, the bars'
15
+ // appearance follows dark mode and the window lays out into every cutout.
16
+
17
+ // DecorView's light scrim and the platform's dark scrim
18
+ internal val DefaultLightScrim = Color.argb(0xe6, 0xFF, 0xFF, 0xFF)
19
+ internal val DefaultDarkScrim = Color.argb(0x80, 0x1b, 0x1b, 0x1b)
20
+
21
+ fun ComponentActivity.enableEdgeToEdge(
22
+ statusBarStyle: SystemBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT),
23
+ navigationBarStyle: SystemBarStyle = SystemBarStyle.auto(DefaultLightScrim, DefaultDarkScrim),
24
+ ) {
25
+ val view = window.decorView
26
+ val statusBarIsDark = statusBarStyle.detectDarkMode(view.resources)
27
+ val navigationBarIsDark = navigationBarStyle.detectDarkMode(view.resources)
28
+ WindowCompat.setDecorFitsSystemWindows(window, false)
29
+ window.statusBarColor = statusBarStyle.getScrimWithEnforcedContrast(statusBarIsDark)
30
+ window.navigationBarColor = navigationBarStyle.getScrimWithEnforcedContrast(navigationBarIsDark)
31
+ window.isStatusBarContrastEnforced = false
32
+ window.isNavigationBarContrastEnforced = navigationBarStyle.nightMode == UiModeManager.MODE_NIGHT_AUTO
33
+ WindowInsetsControllerCompat(window, view).run {
34
+ isAppearanceLightStatusBars = !statusBarIsDark
35
+ isAppearanceLightNavigationBars = !navigationBarIsDark
36
+ }
37
+ window.attributes.layoutInDisplayCutoutMode =
38
+ WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
39
+ }
40
+
41
+ class SystemBarStyle private constructor(
42
+ private val lightScrim: Int,
43
+ internal val darkScrim: Int,
44
+ internal val nightMode: Int,
45
+ internal val detectDarkMode: (Resources) -> Boolean,
46
+ ) {
47
+ companion object {
48
+ fun auto(
49
+ lightScrim: Int,
50
+ darkScrim: Int,
51
+ detectDarkMode: (Resources) -> Boolean = { resources ->
52
+ (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) ==
53
+ Configuration.UI_MODE_NIGHT_YES
54
+ },
55
+ ): SystemBarStyle = SystemBarStyle(lightScrim, darkScrim, UiModeManager.MODE_NIGHT_AUTO, detectDarkMode)
56
+
57
+ fun dark(scrim: Int): SystemBarStyle =
58
+ SystemBarStyle(scrim, scrim, UiModeManager.MODE_NIGHT_YES) { true }
59
+
60
+ fun light(scrim: Int, darkScrim: Int): SystemBarStyle =
61
+ SystemBarStyle(scrim, darkScrim, UiModeManager.MODE_NIGHT_NO) { false }
62
+ }
63
+
64
+ internal fun getScrim(isDark: Boolean) = if (isDark) darkScrim else lightScrim
65
+
66
+ internal fun getScrimWithEnforcedContrast(isDark: Boolean): Int = when {
67
+ nightMode == UiModeManager.MODE_NIGHT_AUTO -> Color.TRANSPARENT
68
+ isDark -> darkScrim
69
+ else -> lightScrim
70
+ }
71
+ }
@@ -0,0 +1,32 @@
1
+ package androidx.core.view
2
+
3
+ import android.view.View
4
+ import android.view.Window
5
+ import android.view.WindowInsetsController
6
+
7
+ // androidx.core's window helpers as they run from API 30, on the platform
8
+ // window and its insets controller
9
+ object WindowCompat {
10
+ fun setDecorFitsSystemWindows(window: Window, decorFitsSystemWindows: Boolean) {
11
+ window.setDecorFitsSystemWindows(decorFitsSystemWindows)
12
+ }
13
+
14
+ fun getInsetsController(window: Window, view: View): WindowInsetsControllerCompat =
15
+ WindowInsetsControllerCompat(window, view)
16
+ }
17
+
18
+ class WindowInsetsControllerCompat(window: Window, @Suppress("UNUSED_PARAMETER") view: View) {
19
+ private val controller = window.insetsController
20
+
21
+ private fun appearance(flag: Int) = controller.systemBarsAppearance and flag != 0
22
+ private fun setAppearance(flag: Int, light: Boolean) =
23
+ controller.setSystemBarsAppearance(if (light) flag else 0, flag)
24
+
25
+ var isAppearanceLightStatusBars: Boolean
26
+ get() = appearance(WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS)
27
+ set(light) = setAppearance(WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS, light)
28
+
29
+ var isAppearanceLightNavigationBars: Boolean
30
+ get() = appearance(WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS)
31
+ set(light) = setAppearance(WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS, light)
32
+ }
@@ -6,8 +6,12 @@ import android.app.Activity
6
6
  import android.app.Application
7
7
  import android.content.ComponentName
8
8
  import android.content.Intent
9
+ import android.content.res.Configuration
10
+ import android.content.res.Resources
11
+ import android.content.res.processResources
9
12
  import android.view.ViewGroup
10
13
  import android.view.Window
14
+ import android.view.WindowTheme
11
15
  import androidx.activity.ComponentActivity
12
16
  import androidx.compose.ui.platform.ComposeView
13
17
  import androidx.compose.ui.platform.PlatformArchitectureComponentsOwner
@@ -26,15 +30,14 @@ import preparePeachDevice
26
30
  import robotoFamily
27
31
  import org.json.JSONObject
28
32
 
29
- // what the manifest and Gradle files resolved: the launcher component the
30
- // intent names (an alias stays the alias), the activity class it runs, the
31
- // app's target SDK and theme opt-out, and constructors for the manifest's
33
+ // what the manifest, Gradle files and theme resolved: the launcher component
34
+ // the intent names (an alias stays the alias), the activity class it runs, what
35
+ // its window read from the theme, and constructors for the manifest's
32
36
  // Application and that class (wasm has no reflection).
33
37
  internal class PeachAppSpec(
34
38
  val launcher: ComponentName,
35
39
  val component: ComponentName,
36
- val targetSdk: Int,
37
- val optOutEdgeToEdge: Boolean,
40
+ val window: WindowTheme,
38
41
  val createApplication: () -> Application,
39
42
  val createActivity: () -> Activity,
40
43
  )
@@ -71,12 +74,16 @@ internal fun peachStartApp(
71
74
  spec: PeachAppSpec,
72
75
  density: Float, fontScale: Float, locale: String, backend: String, stencilBits: Int,
73
76
  regular: String, medium: String, bold: String,
74
- statusBar: Int, navigationBar: Int,
77
+ statusBar: Int, navigationBar: Int, night: Boolean,
75
78
  invalidate: (Boolean) -> Unit,
76
79
  ): String {
77
80
  try {
78
81
  require(backend == "graphite" || backend == "raster")
79
82
  preparePeachDevice(locale)
83
+ processResources = Resources(Configuration().apply {
84
+ uiMode = Configuration.UI_MODE_TYPE_NORMAL or
85
+ if (night) Configuration.UI_MODE_NIGHT_YES else Configuration.UI_MODE_NIGHT_NO
86
+ })
80
87
  val fonts = robotoFamily(regular, medium, bold)
81
88
  val application = spec.createApplication()
82
89
  application.onCreate()
@@ -96,11 +103,10 @@ internal fun peachStartApp(
96
103
 
97
104
  override fun windowChanged() {
98
105
  if (mountId != 0)
99
- mount.emit("window", JSONObject().put("decorFitsSystemWindows", window.fitsSystemWindows).toString())
106
+ mount.emitAtFrame("window") { window.describe().toString() }
100
107
  }
101
108
  }
102
- val enforced = spec.targetSdk > 35 || (spec.targetSdk == 35 && !spec.optOutEdgeToEdge)
103
- window = Window(activity, enforced, host)
109
+ window = Window(activity, spec.window, host)
104
110
  activity.attach(
105
111
  application,
106
112
  spec.component,
@@ -124,8 +130,7 @@ internal fun peachStartApp(
124
130
  "${spec.component.flattenToShortString()} set no ComposeView as its content; Peach composes only ComposeView content"
125
131
  }
126
132
  val params = view.layoutParams
127
- return JSONObject().put("ok", true).put("id", mountId)
128
- .put("decorFitsSystemWindows", window.fitsSystemWindows)
133
+ return window.describe().put("ok", true).put("id", mountId)
129
134
  .put("wrapWidth", params?.width == ViewGroup.LayoutParams.WRAP_CONTENT)
130
135
  .put("wrapHeight", params?.height == ViewGroup.LayoutParams.WRAP_CONTENT)
131
136
  .toString()
@@ -8,6 +8,7 @@ import androidx.compose.ui.platform.ComposeView
8
8
  internal interface PeachWindowHost {
9
9
  fun compose(view: ComposeView): AutoCloseable
10
10
 
11
- // the window's inset policy changed after it was added
11
+ // the inset policy or status bar the window asks the device for changed
12
+ // after it was added
12
13
  fun windowChanged()
13
14
  }
@@ -131,6 +131,9 @@ internal class PeachComposeMount(
131
131
  var constraints = Constraints()
132
132
  set(value) { if (field != value) { field = value; needsMeasure = true } }
133
133
  private val events = mutableListOf<JSONObject>()
134
+ // state reported once a frame, read when the frame draws: like the decor,
135
+ // the device sees the state a frame ends with, not each step to it
136
+ private val frameState = linkedMapOf<String, () -> Any?>()
134
137
  private val platform = PeachComposePlatform(Density(density, fontScale), LocaleList(locale), IntSize.Zero, owners, insets)
135
138
  private val recomposer = FrameRecomposer(Dispatchers.Main) { invalidate(false) }
136
139
  private var needsMeasure = true
@@ -159,6 +162,11 @@ internal class PeachComposeMount(
159
162
  invalidate(false)
160
163
  }
161
164
 
165
+ fun emitAtFrame(name: String, args: () -> Any?) {
166
+ frameState[name] = args
167
+ invalidate(false)
168
+ }
169
+
162
170
  fun measure(): IntSize {
163
171
  val measured = scene.measureContent(constraints)
164
172
  scene.size = measured
@@ -198,7 +206,9 @@ internal class PeachComposeMount(
198
206
  scene.draw(canvas.asComposeCanvas())
199
207
  val reply = JSONObject().put("width", measured.width).put("height", measured.height)
200
208
  .put("pending", recomposer.hasPendingWork() || scene.hasInvalidations())
201
- .put("events", JSONArray(events))
209
+ for ((name, args) in frameState) events.add(JSONObject().put("name", name).put("args", args()))
210
+ frameState.clear()
211
+ reply.put("events", JSONArray(events))
202
212
  events.clear()
203
213
  if (gpu) checkNotNull(surface).flushAndSubmit()
204
214
  else {