peachsim 0.1.649 → 0.1.650

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +10 -9
  2. package/cli/command-registry.ts +2 -1
  3. package/cli/commands/guest-host.ts +67 -0
  4. package/cli/commands/kotlin.ts +231 -0
  5. package/cli/commands/project-report.ts +1 -1
  6. package/cli/commands/report-issue.ts +55 -32
  7. package/cli/commands/setup.ts +10 -14
  8. package/cli/commands/support.ts +399 -0
  9. package/cli/commands/swift.ts +6 -52
  10. package/cli/commands/telemetry.ts +10 -13
  11. package/cli/main.ts +22 -10
  12. package/cli/outbound-endpoints.ts +23 -2
  13. package/cli/privacy.ts +23 -25
  14. package/cli/runtime-notes.ts +3 -2
  15. package/cli/telemetry.ts +109 -14
  16. package/dist-lib/agent-daemon-client.cjs +1 -1
  17. package/dist-lib/agent-events.cjs +1 -1
  18. package/dist-lib/agent-identity.cjs +1 -1
  19. package/dist-lib/agent-sessions.cjs +1 -1
  20. package/dist-lib/attached-projects.cjs +1 -1
  21. package/dist-lib/auth/shared-session.cjs +1 -1
  22. package/dist-lib/backend-origin.cjs +1 -1
  23. package/dist-lib/beta.cjs +1 -1
  24. package/dist-lib/beta.mjs +1 -1
  25. package/dist-lib/bridge-constants.cjs +1 -1
  26. package/dist-lib/bridge-contract-input.cjs +1 -1
  27. package/dist-lib/bridge-contract-input.mjs +1 -1
  28. package/dist-lib/bridge-contract.cjs +1 -1
  29. package/dist-lib/bridge-contract.mjs +1 -1
  30. package/dist-lib/capture-contract.cjs +1 -1
  31. package/dist-lib/capture-contract.mjs +1 -1
  32. package/dist-lib/cli-constants.cjs +1 -1
  33. package/dist-lib/cloud-contract.cjs +1 -1
  34. package/dist-lib/cloud-contract.mjs +1 -1
  35. package/dist-lib/cloud-test-args.cjs +1 -1
  36. package/dist-lib/cloud-test-args.mjs +1 -1
  37. package/dist-lib/cloud.cjs +1 -1
  38. package/dist-lib/cloud.mjs +1 -1
  39. package/dist-lib/config.cjs +1 -1
  40. package/dist-lib/detox/index.cjs +1 -1
  41. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  42. package/dist-lib/home-paths.cjs +3 -3
  43. package/dist-lib/host/bridge-host.cjs +1 -1
  44. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  45. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  46. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  47. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  48. package/dist-lib/host/websocket-proxy.cjs +1 -1
  49. package/dist-lib/index.cjs +1 -1
  50. package/dist-lib/jump-to-source-babel.cjs +1 -1
  51. package/dist-lib/jump-to-source-native.cjs +1 -1
  52. package/dist-lib/kotlin-host.cjs +93 -28
  53. package/dist-lib/menu.cjs +1 -1
  54. package/dist-lib/menu.mjs +1 -1
  55. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  56. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  57. package/dist-lib/metro-production-bundle.cjs +1 -1
  58. package/dist-lib/metro-production-bundle.mjs +1 -1
  59. package/dist-lib/metro.cjs +1 -1
  60. package/dist-lib/native-app-identity.cjs +1 -1
  61. package/dist-lib/native-app-identity.mjs +1 -1
  62. package/dist-lib/profiles.cjs +1 -1
  63. package/dist-lib/public-brand.cjs +1 -1
  64. package/dist-lib/react-native-host-modules.cjs +1 -1
  65. package/dist-lib/react-native-host-modules.mjs +1 -1
  66. package/dist-lib/render-mode.cjs +1 -1
  67. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  68. package/dist-lib/sdk.cjs +1 -1
  69. package/dist-lib/sdk.mjs +1 -1
  70. package/dist-lib/skills.cjs +15 -26
  71. package/dist-lib/swift-compile.cjs +6 -3
  72. package/dist-lib/swift-compile.mjs +6 -3
  73. package/dist-lib/swift-package.cjs +1 -1
  74. package/dist-lib/swift-package.mjs +1 -1
  75. package/dist-lib/swift-submit.cjs +1 -1
  76. package/dist-lib/swift-submit.mjs +1 -1
  77. package/dist-lib/swift.cjs +1 -1
  78. package/dist-lib/vite-swift.cjs +6 -3
  79. package/dist-lib/vite.cjs +1 -1
  80. package/kotlin/AppSources/android/app/Activity.kt +85 -0
  81. package/kotlin/AppSources/android/app/Application.kt +7 -0
  82. package/kotlin/AppSources/android/content/Context.kt +5 -0
  83. package/kotlin/AppSources/android/content/Intent.kt +16 -0
  84. package/kotlin/AppSources/android/os/Bundle.kt +5 -0
  85. package/kotlin/AppSources/android/util/SuperNotCalledException.kt +3 -0
  86. package/kotlin/AppSources/android/view/View.kt +72 -0
  87. package/kotlin/AppSources/android/view/Window.kt +44 -0
  88. package/kotlin/AppSources/android/widget/FrameLayout.kt +6 -0
  89. package/kotlin/AppSources/androidx/activity/ComponentActivity.kt +57 -0
  90. package/kotlin/AppSources/androidx/activity/compose/ComponentActivity.kt +29 -0
  91. package/kotlin/AppSources/androidx/compose/ui/platform/ComposeView.kt +67 -0
  92. package/kotlin/AppSources/dev/peach/app/PeachApp.kt +136 -0
  93. package/kotlin/AppSources/dev/peach/app/PeachWindowHost.kt +13 -0
  94. package/kotlin/ComposeSources/PeachComposeHost.kt +58 -37
  95. package/package.json +1 -1
  96. package/skills/contrast/SKILL.md +2 -1
  97. package/skills/peach-setup/SKILL.md +2 -2
  98. package/src/{swift-guest.ts → guest-bundle.ts} +22 -7
  99. package/src/home-paths.ts +5 -3
  100. package/src/kotlin-app.ts +411 -0
  101. package/src/kotlin-host.ts +139 -32
  102. package/src/swift-compile.ts +7 -2
@@ -0,0 +1,7 @@
1
+ package android.app
2
+
3
+ import android.content.ContextWrapper
4
+
5
+ open class Application : ContextWrapper() {
6
+ open fun onCreate() {}
7
+ }
@@ -0,0 +1,5 @@
1
+ package android.content
2
+
3
+ abstract class Context
4
+
5
+ open class ContextWrapper : Context()
@@ -0,0 +1,16 @@
1
+ package android.content
2
+
3
+ class ComponentName(val packageName: String, val className: String) {
4
+ fun flattenToShortString(): String =
5
+ if (className.startsWith("$packageName.")) "$packageName/${className.removePrefix(packageName)}"
6
+ else "$packageName/$className"
7
+ }
8
+
9
+ class Intent(val action: String?, val categories: Set<String>?, val component: ComponentName?) {
10
+ fun hasCategory(category: String): Boolean = categories?.contains(category) == true
11
+
12
+ companion object {
13
+ const val ACTION_MAIN = "android.intent.action.MAIN"
14
+ const val CATEGORY_LAUNCHER = "android.intent.category.LAUNCHER"
15
+ }
16
+ }
@@ -0,0 +1,5 @@
1
+ package android.os
2
+
3
+ // a cold start has no saved instance state, so the launcher receives null;
4
+ // the type exists for the signatures that name it.
5
+ class Bundle
@@ -0,0 +1,3 @@
1
+ package android.util
2
+
3
+ class SuperNotCalledException(message: String) : RuntimeException(message)
@@ -0,0 +1,72 @@
1
+ package android.view
2
+
3
+ import android.content.Context
4
+
5
+ open class View(val context: Context) {
6
+ var parent: ViewGroup? = null
7
+ internal set
8
+ var layoutParams: ViewGroup.LayoutParams? = null
9
+ internal var attachedWindow: Window? = null
10
+
11
+ val isAttachedToWindow: Boolean get() = attachedWindow != null
12
+
13
+ // ViewRootImpl dispatches attach through the tree once the window is added
14
+ internal open fun dispatchAttachedToWindow(window: Window) {
15
+ attachedWindow = window
16
+ onAttachedToWindow()
17
+ }
18
+
19
+ internal open fun dispatchDetachedFromWindow() {
20
+ onDetachedFromWindow()
21
+ attachedWindow = null
22
+ }
23
+
24
+ protected open fun onAttachedToWindow() {}
25
+ protected open fun onDetachedFromWindow() {}
26
+ }
27
+
28
+ open class ViewGroup(context: Context) : View(context) {
29
+ open class LayoutParams(val width: Int, val height: Int) {
30
+ companion object {
31
+ const val MATCH_PARENT = -1
32
+ const val WRAP_CONTENT = -2
33
+ }
34
+ }
35
+
36
+ private val children = mutableListOf<View>()
37
+
38
+ val childCount: Int get() = children.size
39
+ fun getChildAt(index: Int): View? = children.getOrNull(index)
40
+
41
+ fun addView(child: View, params: LayoutParams) {
42
+ child.layoutParams = params
43
+ addView(child)
44
+ }
45
+
46
+ open fun addView(child: View) {
47
+ check(child.parent == null) {
48
+ "The specified child already has a parent. You must call removeView() on the child's parent first."
49
+ }
50
+ children += child
51
+ child.parent = this
52
+ attachedWindow?.let { child.dispatchAttachedToWindow(it) }
53
+ }
54
+
55
+ fun removeAllViews() {
56
+ for (child in children) {
57
+ if (child.isAttachedToWindow) child.dispatchDetachedFromWindow()
58
+ child.parent = null
59
+ }
60
+ children.clear()
61
+ }
62
+
63
+ override fun dispatchAttachedToWindow(window: Window) {
64
+ super.dispatchAttachedToWindow(window)
65
+ for (child in children.toList()) child.dispatchAttachedToWindow(window)
66
+ }
67
+
68
+ override fun dispatchDetachedFromWindow() {
69
+ for (child in children.toList()) child.dispatchDetachedFromWindow()
70
+ super.dispatchDetachedFromWindow()
71
+ }
72
+ }
@@ -0,0 +1,44 @@
1
+ package android.view
2
+
3
+ import android.content.Context
4
+ import android.widget.FrameLayout
5
+ import androidx.compose.runtime.getValue
6
+ import androidx.compose.runtime.mutableStateOf
7
+ import androidx.compose.runtime.setValue
8
+ import dev.peach.app.PeachWindowHost
9
+
10
+ // PhoneWindow's content parent (android.R.id.content) inside its decor. the
11
+ // window is added, and its views attached, after the activity resumes.
12
+ class Window internal constructor(
13
+ context: Context,
14
+ private val edgeToEdgeEnforced: Boolean,
15
+ internal val host: PeachWindowHost,
16
+ ) {
17
+ internal val contentParent = FrameLayout(context)
18
+ val decorView: View get() = contentParent
19
+
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
24
+
25
+ fun setDecorFitsSystemWindows(decorFitsSystemWindows: Boolean) {
26
+ val before = fitsSystemWindows
27
+ this.decorFitsSystemWindows = decorFitsSystemWindows
28
+ if (fitsSystemWindows != before) host.windowChanged()
29
+ }
30
+
31
+ // Activity.setContentView(View) fills the content parent
32
+ internal fun setContentView(view: View, params: ViewGroup.LayoutParams) {
33
+ contentParent.removeAllViews()
34
+ contentParent.addView(view, params)
35
+ }
36
+
37
+ internal fun addToWindowManager() {
38
+ contentParent.dispatchAttachedToWindow(this)
39
+ }
40
+
41
+ internal fun removeFromWindowManager() {
42
+ if (contentParent.isAttachedToWindow) contentParent.dispatchDetachedFromWindow()
43
+ }
44
+ }
@@ -0,0 +1,6 @@
1
+ package android.widget
2
+
3
+ import android.content.Context
4
+ import android.view.ViewGroup
5
+
6
+ open class FrameLayout(context: Context) : ViewGroup(context)
@@ -0,0 +1,57 @@
1
+ package androidx.activity
2
+
3
+ import android.app.Activity
4
+ import android.os.Bundle
5
+ import androidx.lifecycle.HasDefaultViewModelProviderFactory
6
+ import androidx.lifecycle.Lifecycle
7
+ import androidx.lifecycle.LifecycleEventObserver
8
+ import androidx.lifecycle.LifecycleOwner
9
+ import androidx.lifecycle.LifecycleRegistry
10
+ import androidx.lifecycle.SAVED_STATE_REGISTRY_OWNER_KEY
11
+ import androidx.lifecycle.SavedStateViewModelFactory
12
+ import androidx.lifecycle.VIEW_MODEL_STORE_OWNER_KEY
13
+ import androidx.lifecycle.ViewModelStore
14
+ import androidx.lifecycle.ViewModelStoreOwner
15
+ import androidx.lifecycle.enableSavedStateHandles
16
+ import androidx.lifecycle.viewmodel.CreationExtras
17
+ import androidx.lifecycle.viewmodel.MutableCreationExtras
18
+ import androidx.savedstate.SavedStateRegistry
19
+ import androidx.savedstate.SavedStateRegistryController
20
+ import androidx.savedstate.SavedStateRegistryOwner
21
+
22
+ // androidx.activity's ComponentActivity owners, on the multiplatform
23
+ // lifecycle, viewmodel and savedstate libraries: the registry attaches in the
24
+ // constructor and restores in onCreate before super, the lifecycle moves on
25
+ // the post callbacks, and destroy clears the view model store.
26
+ open class ComponentActivity : Activity(), LifecycleOwner, ViewModelStoreOwner,
27
+ HasDefaultViewModelProviderFactory, SavedStateRegistryOwner {
28
+ private val lifecycleRegistry = LifecycleRegistry(this)
29
+ private val savedStateRegistryController = SavedStateRegistryController.create(this)
30
+
31
+ override val lifecycle: Lifecycle get() = lifecycleRegistry
32
+ override val viewModelStore: ViewModelStore = ViewModelStore()
33
+ override val savedStateRegistry: SavedStateRegistry get() = savedStateRegistryController.savedStateRegistry
34
+ override val defaultViewModelProviderFactory = SavedStateViewModelFactory()
35
+ override val defaultViewModelCreationExtras: CreationExtras
36
+ get() = MutableCreationExtras().also {
37
+ it[SAVED_STATE_REGISTRY_OWNER_KEY] = this
38
+ it[VIEW_MODEL_STORE_OWNER_KEY] = this
39
+ }
40
+
41
+ init {
42
+ lifecycleRegistry.addObserver(LifecycleEventObserver { _, event ->
43
+ if (event == Lifecycle.Event.ON_DESTROY) viewModelStore.clear()
44
+ })
45
+ savedStateRegistryController.performAttach()
46
+ enableSavedStateHandles()
47
+ }
48
+
49
+ override fun onCreate(savedInstanceState: Bundle?) {
50
+ savedStateRegistryController.performRestore(null)
51
+ super.onCreate(savedInstanceState)
52
+ }
53
+
54
+ override fun dispatchActivityPostCreated() = lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
55
+ override fun dispatchActivityPostStarted() = lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
56
+ override fun dispatchActivityPostResumed() = lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
57
+ }
@@ -0,0 +1,29 @@
1
+ package androidx.activity.compose
2
+
3
+ import android.view.ViewGroup
4
+ import androidx.activity.ComponentActivity
5
+ import androidx.compose.runtime.Composable
6
+ import androidx.compose.runtime.CompositionContext
7
+ import androidx.compose.ui.platform.ComposeView
8
+
9
+ private val DefaultActivityContentLayoutParams =
10
+ ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
11
+
12
+ // activity-compose's setContent: reuse the ComposeView already in the content
13
+ // parent, otherwise create one and set it as the content view. the activity
14
+ // itself supplies the view tree owners.
15
+ fun ComponentActivity.setContent(
16
+ parent: CompositionContext? = null,
17
+ content: @Composable () -> Unit,
18
+ ) {
19
+ val existingComposeView =
20
+ (window.decorView as ViewGroup).getChildAt(0) as? ComposeView
21
+ if (existingComposeView != null) with(existingComposeView) {
22
+ setParentCompositionContext(parent)
23
+ setContent(content)
24
+ } else ComposeView(this).apply {
25
+ setParentCompositionContext(parent)
26
+ setContent(content)
27
+ setContentView(this, DefaultActivityContentLayoutParams)
28
+ }
29
+ }
@@ -0,0 +1,67 @@
1
+ package androidx.compose.ui.platform
2
+
3
+ import android.content.Context
4
+ import android.view.ViewGroup
5
+ import androidx.compose.runtime.Composable
6
+ import androidx.compose.runtime.CompositionContext
7
+ import androidx.compose.runtime.mutableStateOf
8
+
9
+ // AbstractComposeView and ComposeView as compose-ui's Android source has them:
10
+ // setContent stores a state-backed lambda, composition starts when the view
11
+ // attaches to a window, a later setContent replaces the lambda inside the same
12
+ // composition, and detaching disposes it (the default view composition
13
+ // strategy).
14
+ class ComposeView(context: Context) : ViewGroup(context) {
15
+ private val content = mutableStateOf<(@Composable () -> Unit)?>(null)
16
+ private var shouldCreateCompositionOnAttachedToWindow = false
17
+ private var parentContext: CompositionContext? = null
18
+ private var composition: AutoCloseable? = null
19
+
20
+ val hasComposition: Boolean get() = composition != null
21
+
22
+ @Composable
23
+ internal fun Content() {
24
+ content.value?.invoke()
25
+ }
26
+
27
+ fun setParentCompositionContext(parent: CompositionContext?) {
28
+ parentContext = parent
29
+ }
30
+
31
+ fun setContent(content: @Composable () -> Unit) {
32
+ shouldCreateCompositionOnAttachedToWindow = true
33
+ this.content.value = content
34
+ if (isAttachedToWindow) createComposition()
35
+ }
36
+
37
+ fun createComposition() {
38
+ check(parentContext != null || isAttachedToWindow) {
39
+ "createComposition requires a previous call to createComposition(ComposeViewContext)," +
40
+ " a parent reference, or the View to be attached to a window. Attach the View or " +
41
+ "call setParentCompositionReference."
42
+ }
43
+ ensureCompositionCreated()
44
+ }
45
+
46
+ fun disposeComposition() {
47
+ composition?.close()
48
+ composition = null
49
+ }
50
+
51
+ private fun ensureCompositionCreated() {
52
+ if (composition != null) return
53
+ check(parentContext == null) { "a ComposeView composing into a parent composition is not supported in Peach yet" }
54
+ val window = checkNotNull(attachedWindow) { "ComposeView is not attached to a window" }
55
+ composition = window.host.compose(this)
56
+ }
57
+
58
+ override fun onAttachedToWindow() {
59
+ super.onAttachedToWindow()
60
+ if (shouldCreateCompositionOnAttachedToWindow) ensureCompositionCreated()
61
+ }
62
+
63
+ override fun onDetachedFromWindow() {
64
+ super.onDetachedFromWindow()
65
+ disposeComposition()
66
+ }
67
+ }
@@ -0,0 +1,136 @@
1
+ @file:OptIn(androidx.compose.ui.InternalComposeUiApi::class)
2
+
3
+ package dev.peach.app
4
+
5
+ import android.app.Activity
6
+ import android.app.Application
7
+ import android.content.ComponentName
8
+ import android.content.Intent
9
+ import android.view.ViewGroup
10
+ import android.view.Window
11
+ import androidx.activity.ComponentActivity
12
+ import androidx.compose.ui.platform.ComposeView
13
+ import androidx.compose.ui.platform.PlatformArchitectureComponentsOwner
14
+ import androidx.compose.ui.platform.PlatformInsets
15
+ import androidx.compose.ui.platform.PlatformWindowInsets
16
+ import androidx.compose.runtime.getValue
17
+ import androidx.compose.runtime.key
18
+ import androidx.compose.runtime.mutableStateOf
19
+ import androidx.compose.runtime.setValue
20
+ import androidx.lifecycle.Lifecycle
21
+ import androidx.navigationevent.NavigationEventDispatcher
22
+ import androidx.navigationevent.NavigationEventDispatcherOwner
23
+ import PeachComposeMount
24
+ import addMount
25
+ import preparePeachDevice
26
+ import robotoFamily
27
+ import org.json.JSONObject
28
+
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
32
+ // Application and that class (wasm has no reflection).
33
+ internal class PeachAppSpec(
34
+ val launcher: ComponentName,
35
+ val component: ComponentName,
36
+ val targetSdk: Int,
37
+ val optOutEdgeToEdge: Boolean,
38
+ val createApplication: () -> Application,
39
+ val createActivity: () -> Activity,
40
+ )
41
+
42
+ // the system bars the device delivers to the window's content, in pixels: all
43
+ // of them on an edge-to-edge window, none once the decor fits system windows.
44
+ private class SystemBarInsets(
45
+ private val window: Window,
46
+ private val status: Int,
47
+ private val navigation: Int,
48
+ ) : PlatformWindowInsets {
49
+ // read when compose lays out, so a later change to the window applies
50
+ private fun delivered(pixels: Int) = if (window.fitsSystemWindows) 0 else pixels
51
+ override val statusBars = PlatformInsets(getTop = { delivered(status) })
52
+ override val navigationBars = PlatformInsets(getBottom = { delivered(navigation) })
53
+ override val systemBars =
54
+ PlatformInsets(getTop = { delivered(status) }, getBottom = { delivered(navigation) })
55
+ }
56
+
57
+ private class ActivityOwners(activity: ComponentActivity) : PlatformArchitectureComponentsOwner {
58
+ override val lifecycleOwner = activity
59
+ override val viewModelStoreOwner = activity
60
+ override val savedStateRegistryOwner = activity
61
+ override val navigationEventDispatcherOwner = object : NavigationEventDispatcherOwner {
62
+ override val navigationEventDispatcher = NavigationEventDispatcher()
63
+ }
64
+ }
65
+
66
+ // a cold start in ActivityThread's order: the Application and its onCreate,
67
+ // then the launcher activity attached to its window and launcher intent,
68
+ // onCreate, onStart, onPostCreate, onResume and onPostResume, and only then the
69
+ // window added, which attaches the content view and composes it.
70
+ internal fun peachStartApp(
71
+ spec: PeachAppSpec,
72
+ density: Float, fontScale: Float, locale: String, backend: String, stencilBits: Int,
73
+ regular: String, medium: String, bold: String,
74
+ statusBar: Int, navigationBar: Int,
75
+ invalidate: (Boolean) -> Unit,
76
+ ): String {
77
+ try {
78
+ require(backend == "graphite" || backend == "raster")
79
+ preparePeachDevice(locale)
80
+ val fonts = robotoFamily(regular, medium, bold)
81
+ val application = spec.createApplication()
82
+ application.onCreate()
83
+ val activity = spec.createActivity()
84
+ val componentActivity = activity as? ComponentActivity
85
+ lateinit var window: Window
86
+ lateinit var mount: PeachComposeMount
87
+ var mountId = 0
88
+ // the window's one scene composes the ComposeView it holds; a new
89
+ // view starts a new composition, as on the device
90
+ var held by mutableStateOf<ComposeView?>(null)
91
+ val host = object : PeachWindowHost {
92
+ override fun compose(view: ComposeView): AutoCloseable {
93
+ held = view
94
+ return AutoCloseable { if (held === view) held = null }
95
+ }
96
+
97
+ override fun windowChanged() {
98
+ if (mountId != 0)
99
+ mount.emit("window", JSONObject().put("decorFitsSystemWindows", window.fitsSystemWindows).toString())
100
+ }
101
+ }
102
+ val enforced = spec.targetSdk > 35 || (spec.targetSdk == 35 && !spec.optOutEdgeToEdge)
103
+ window = Window(activity, enforced, host)
104
+ activity.attach(
105
+ application,
106
+ spec.component,
107
+ Intent(Intent.ACTION_MAIN, setOf(Intent.CATEGORY_LAUNCHER), spec.launcher),
108
+ window,
109
+ )
110
+ activity.performCreate(null)
111
+ activity.performStart()
112
+ activity.performPostCreate(null)
113
+ activity.performResume()
114
+ mount = PeachComposeMount(
115
+ density, fontScale, locale, backend == "graphite", stencilBits, fonts, invalidate,
116
+ owners = componentActivity?.let(::ActivityOwners),
117
+ insets = SystemBarInsets(window, statusBar, navigationBar),
118
+ clockRunning = { componentActivity == null || componentActivity.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) },
119
+ )
120
+ mount.setContent { held?.let { view -> key(view) { view.Content() } } }
121
+ mountId = addMount(mount)
122
+ window.addToWindowManager()
123
+ val view = checkNotNull(held) {
124
+ "${spec.component.flattenToShortString()} set no ComposeView as its content; Peach composes only ComposeView content"
125
+ }
126
+ val params = view.layoutParams
127
+ return JSONObject().put("ok", true).put("id", mountId)
128
+ .put("decorFitsSystemWindows", window.fitsSystemWindows)
129
+ .put("wrapWidth", params?.width == ViewGroup.LayoutParams.WRAP_CONTENT)
130
+ .put("wrapHeight", params?.height == ViewGroup.LayoutParams.WRAP_CONTENT)
131
+ .toString()
132
+ } catch (error: Throwable) {
133
+ return JSONObject().put("ok", false).put("code", "E_KOTLIN_APP")
134
+ .put("error", "${error::class.simpleName}: ${error.message}").toString()
135
+ }
136
+ }
@@ -0,0 +1,13 @@
1
+ package dev.peach.app
2
+
3
+ import androidx.compose.ui.platform.ComposeView
4
+
5
+ // the device side of a window: one Peach scene for the window's life, which
6
+ // composes whichever ComposeView the window holds once that view attaches.
7
+ // the composition ends when the view disposes it.
8
+ internal interface PeachWindowHost {
9
+ fun compose(view: ComposeView): AutoCloseable
10
+
11
+ // the window's inset policy changed after it was added
12
+ fun windowChanged()
13
+ }
@@ -15,6 +15,8 @@ import androidx.compose.ui.input.pointer.PointerEventType
15
15
  import androidx.compose.ui.input.pointer.PointerType
16
16
  import androidx.compose.ui.platform.FrameRecomposer
17
17
  import androidx.compose.ui.platform.LocalFontFamilyResolver
18
+ import androidx.compose.ui.platform.PlatformArchitectureComponentsOwner
19
+ import androidx.compose.ui.platform.PlatformWindowInsets
18
20
  import androidx.compose.ui.platform.PlatformContext
19
21
  import androidx.compose.ui.platform.WindowInfo
20
22
  import androidx.compose.ui.platform.registerSkikoComposeImplementation
@@ -59,11 +61,19 @@ import org.jetbrains.skiko.setHostOsProvider
59
61
  import org.json.JSONArray
60
62
  import org.json.JSONObject
61
63
 
62
- private class PeachComposePlatform(
64
+ // owners and window insets come from the hosting window: an activity's
65
+ // ComposeView passes the activity and the insets its window delivers; an
66
+ // imported composable keeps the empty defaults.
67
+ internal class PeachComposePlatform(
63
68
  val density: Density,
64
69
  override val localeList: LocaleList,
65
70
  var size: IntSize,
66
- ) : PlatformContext by PlatformContext.Empty(), PlatformContext.SemanticsOwnerListener {
71
+ owners: PlatformArchitectureComponentsOwner?,
72
+ insets: PlatformWindowInsets?,
73
+ base: PlatformContext = PlatformContext.Empty(),
74
+ ) : PlatformContext by base, PlatformContext.SemanticsOwnerListener {
75
+ override val architectureComponentsOwner: PlatformArchitectureComponentsOwner = owners ?: base.architectureComponentsOwner
76
+ override val windowInsets: PlatformWindowInsets = insets ?: base.windowInsets
67
77
  val owners = mutableSetOf<SemanticsOwner>()
68
78
  override val windowInfo: WindowInfo = object : WindowInfo {
69
79
  override val isWindowFocused: Boolean get() = true
@@ -87,7 +97,7 @@ private class PeachComposePlatform(
87
97
  // android's default and sans-serif families use the same device font bytes as
88
98
  // peach's react native renderer. compose owns shaping and all other families.
89
99
  @Composable
90
- private fun PeachAndroidFonts(roboto: FontFamily, content: @Composable () -> Unit) {
100
+ internal fun PeachAndroidFonts(roboto: FontFamily, content: @Composable () -> Unit) {
91
101
  val parent = LocalFontFamilyResolver.current
92
102
  val resolver = remember(parent, roboto) {
93
103
  val backend = checkNotNull(parent.platformTypefacesLoader())
@@ -102,23 +112,26 @@ private fun PeachAndroidFonts(roboto: FontFamily, content: @Composable () -> Uni
102
112
  CompositionLocalProvider(LocalFontFamilyResolver provides resolver, content = content)
103
113
  }
104
114
 
105
- private class PeachComposeMount(
106
- dispatch: OneNativeSourceViewDispatch,
107
- view: String,
108
- propsJson: String,
115
+ // one compose scene presented by the tenant canvas. the frame clock runs only
116
+ // while clockRunning answers true, as Android's window recomposer pauses its
117
+ // clock below STARTED.
118
+ internal class PeachComposeMount(
109
119
  density: Float,
110
120
  fontScale: Float,
111
121
  locale: String,
112
122
  private val gpu: Boolean,
113
123
  private val stencilBits: Int,
114
- fonts: FontFamily,
124
+ private val fonts: FontFamily,
115
125
  val invalidate: (Boolean) -> Unit,
126
+ owners: PlatformArchitectureComponentsOwner? = null,
127
+ insets: PlatformWindowInsets? = null,
128
+ private val clockRunning: () -> Boolean = { true },
116
129
  ) : AutoCloseable {
117
- var props by mutableStateOf(propsJson)
130
+ var props by mutableStateOf("")
118
131
  var constraints = Constraints()
119
132
  set(value) { if (field != value) { field = value; needsMeasure = true } }
120
133
  private val events = mutableListOf<JSONObject>()
121
- private val platform = PeachComposePlatform(Density(density, fontScale), LocaleList(locale), IntSize.Zero)
134
+ private val platform = PeachComposePlatform(Density(density, fontScale), LocaleList(locale), IntSize.Zero, owners, insets)
122
135
  private val recomposer = FrameRecomposer(Dispatchers.Main) { invalidate(false) }
123
136
  private var needsMeasure = true
124
137
  private var measuredSize = IntSize.Zero
@@ -132,22 +145,20 @@ private class PeachComposeMount(
132
145
  private var pixels: Bitmap? = null
133
146
  private var size = IntSize.Zero
134
147
 
135
- init {
148
+ fun setContent(content: @Composable () -> Unit) {
136
149
  try {
137
- scene.setContent {
138
- PeachAndroidFonts(fonts) {
139
- dispatch.Content(view, props) { name, args ->
140
- events.add(JSONObject().put("name", name).put("args", args))
141
- invalidate(false)
142
- }
143
- }
144
- }
150
+ scene.setContent { PeachAndroidFonts(fonts, content) }
145
151
  } catch (failure: Throwable) {
146
152
  close()
147
153
  throw failure
148
154
  }
149
155
  }
150
156
 
157
+ fun emit(name: String, args: Any?) {
158
+ events.add(JSONObject().put("name", name).put("args", args))
159
+ invalidate(false)
160
+ }
161
+
151
162
  fun measure(): IntSize {
152
163
  val measured = scene.measureContent(constraints)
153
164
  scene.size = measured
@@ -159,7 +170,7 @@ private class PeachComposeMount(
159
170
 
160
171
  fun frame(nanos: Long): String {
161
172
  Snapshot.sendApplyNotifications()
162
- recomposer.performFrame(nanos)
173
+ if (clockRunning()) recomposer.performFrame(nanos)
163
174
  val measured = if (needsMeasure) measure() else measuredSize
164
175
  // a zero-size composition still runs its effects and reports its size;
165
176
  // its backing surface uses one pixel until layout gives it real bounds.
@@ -241,35 +252,45 @@ private class PeachComposeMount(
241
252
  }
242
253
  }
243
254
 
244
- private val mounts = mutableMapOf<Int, PeachComposeMount>()
255
+ internal val mounts = mutableMapOf<Int, PeachComposeMount>()
245
256
  private var nextMount = 1
246
257
  private var deviceLocale: String? = null
247
258
 
259
+ internal fun addMount(mount: PeachComposeMount): Int = nextMount++.also { mounts[it] = mount }
260
+
261
+ // the process-wide device setup every scene shares: skiko's platform, the
262
+ // host OS and the device locale, fixed for the life of the process.
263
+ internal fun preparePeachDevice(locale: String) {
264
+ registerSkikoComposeImplementation()
265
+ if (deviceLocale == null) {
266
+ setHostOsProvider { OS.Android }
267
+ val locales = LocaleList(locale)
268
+ setPlatformLocaleProvider { locales }
269
+ deviceLocale = locale
270
+ }
271
+ require(deviceLocale == locale) { "device locale changes require a new Kotlin process" }
272
+ }
273
+
274
+ internal fun robotoFamily(regular: String, medium: String, bold: String) = FontFamily(
275
+ Font("Roboto-400", Base64.decode(regular), FontWeight.Normal),
276
+ Font("Roboto-500", Base64.decode(medium), FontWeight.Medium),
277
+ Font("Roboto-700", Base64.decode(bold), FontWeight.Bold),
278
+ )
279
+
248
280
  @JsExport
249
281
  fun peachViewMount(source: String, view: String, hash: String, props: String,
250
282
  density: Float, fontScale: Float, locale: String, backend: String, stencilBits: Int,
251
283
  regular: String, medium: String, bold: String, invalidate: (Boolean) -> Unit): String {
252
284
  try {
253
- registerSkikoComposeImplementation()
254
285
  val dispatch = peachViewDispatcher(source)
255
286
  if (dispatch.contractHash != hash) return JSONObject().put("ok", false).put("code", "E_NATIVE_SOURCE_REBUILD")
256
287
  .put("error", "composable source $source changed; rebuild the app").toString()
257
288
  require(backend == "graphite" || backend == "raster")
258
- if (deviceLocale == null) {
259
- setHostOsProvider { OS.Android }
260
- val locales = LocaleList(locale)
261
- setPlatformLocaleProvider { locales }
262
- deviceLocale = locale
263
- }
264
- require(deviceLocale == locale) { "device locale changes require a new Kotlin process" }
265
- val fonts = FontFamily(
266
- Font("Roboto-400", Base64.decode(regular), FontWeight.Normal),
267
- Font("Roboto-500", Base64.decode(medium), FontWeight.Medium),
268
- Font("Roboto-700", Base64.decode(bold), FontWeight.Bold),
269
- )
270
- val id = nextMount++
271
- mounts[id] = PeachComposeMount(dispatch, view, props, density, fontScale, locale, backend == "graphite", stencilBits, fonts, invalidate)
272
- return JSONObject().put("ok", true).put("id", id).toString()
289
+ preparePeachDevice(locale)
290
+ val mount = PeachComposeMount(density, fontScale, locale, backend == "graphite", stencilBits, robotoFamily(regular, medium, bold), invalidate)
291
+ mount.props = props
292
+ mount.setContent { dispatch.Content(view, mount.props, mount::emit) }
293
+ return JSONObject().put("ok", true).put("id", addMount(mount)).toString()
273
294
  } catch (error: Throwable) {
274
295
  return JSONObject().put("ok", false).put("code", "E_NATIVE_SOURCE").put("error", "$source.$view: ${error.message}").toString()
275
296
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peachsim",
3
- "version": "0.1.649",
3
+ "version": "0.1.650",
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",