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.
- package/README.md +21 -2
- package/cli/app-config.ts +25 -9
- package/cli/bridge-flow-runner.ts +10 -23
- package/cli/commands/compat.ts +234 -10
- package/cli/commands/do-chain.ts +5 -1
- package/cli/commands/inspect/actions.ts +33 -76
- package/cli/commands/inspect/resolve-target.ts +67 -2
- package/cli/commands/inspect.ts +16 -1
- package/cli/drivers/playwright-provisioning.ts +95 -0
- package/cli/ws-bridge.ts +7 -7
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract-input.cjs +9 -1
- package/dist-lib/bridge-contract-input.mjs +9 -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 +3 -2
- 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 +78 -35
- 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 +51 -8
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +317 -70
- 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 -4
- 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 +300 -29
- package/dist-lib/native-app-identity.cjs +58 -25
- package/dist-lib/native-app-identity.mjs +57 -25
- package/dist-lib/plist.cjs +64 -0
- package/dist-lib/plist.mjs +29 -0
- 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 +2 -2
- package/dist-lib/sdk.cjs +36 -49
- package/dist-lib/sdk.mjs +36 -49
- package/dist-lib/skills.cjs +250 -131
- 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 +51 -8
- package/kotlin/AppSources/android/app/UiModeManager.kt +9 -0
- package/kotlin/AppSources/android/content/Context.kt +9 -2
- package/kotlin/AppSources/android/content/res/Configuration.kt +16 -0
- package/kotlin/AppSources/android/content/res/Resources.kt +7 -0
- package/kotlin/AppSources/android/graphics/Color.kt +13 -0
- package/kotlin/AppSources/android/view/View.kt +2 -0
- package/kotlin/AppSources/android/view/Window.kt +99 -8
- package/kotlin/AppSources/android/view/WindowInsetsController.kt +12 -0
- package/kotlin/AppSources/android/view/WindowManager.kt +16 -0
- package/kotlin/AppSources/androidx/activity/EdgeToEdge.kt +71 -0
- package/kotlin/AppSources/androidx/core/view/WindowCompat.kt +32 -0
- package/kotlin/AppSources/dev/peach/app/PeachApp.kt +16 -11
- package/kotlin/AppSources/dev/peach/app/PeachWindowHost.kt +2 -1
- package/kotlin/ComposeSources/PeachComposeHost.kt +11 -1
- package/package.json +8 -1
- package/skills/peach-setup/SKILL.md +27 -0
- package/src/android-theme.ts +167 -0
- package/src/android-xml.ts +70 -0
- package/src/bridge-contract-input.ts +10 -1
- package/src/bridge-contract.ts +7 -1
- package/src/config.ts +8 -0
- package/src/generated/android-framework-themes.json +6303 -0
- package/src/host/replacement-module-handler.ts +33 -0
- package/src/index.ts +1 -0
- package/src/kotlin-app.ts +75 -61
- package/src/kotlin-host.ts +7 -3
- package/src/metro-plugin.ts +46 -3
- package/src/native-app-identity.ts +75 -26
- package/src/sim-client.ts +2 -4
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
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.
|
|
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.
|
|
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;
|
package/dist-lib/swift.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
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;
|
package/dist-lib/vite-swift.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
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.
|
|
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,
|
|
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
|
-
//
|
|
347
|
-
//
|
|
348
|
-
jsxRuntime: "
|
|
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(
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
package android.content
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import android.content.res.Resources
|
|
4
|
+
import android.content.res.processResources
|
|
4
5
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
|
21
|
-
//
|
|
22
|
-
private var
|
|
23
|
-
internal val fitsSystemWindows: Boolean get() =
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
30
|
-
// intent names (an alias stays the alias), the activity class it runs,
|
|
31
|
-
//
|
|
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
|
|
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.
|
|
106
|
+
mount.emitAtFrame("window") { window.describe().toString() }
|
|
100
107
|
}
|
|
101
108
|
}
|
|
102
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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 {
|