peachsim 0.1.669 → 0.1.671

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 (116) hide show
  1. package/cli/commands/control.ts +8 -5
  2. package/cli/commands/inspect/core.ts +7 -5
  3. package/cli/commands/inspect/wait-selector.ts +2 -11
  4. package/cli/commands/inspect.ts +6 -59
  5. package/cli/commands/kotlin.ts +6 -4
  6. package/cli/commands/skills.ts +28 -7
  7. package/cli/commands/upgrade.ts +18 -3
  8. package/cli/main.ts +21 -0
  9. package/cli/output-redaction.ts +182 -0
  10. package/cli/parse-args.ts +1 -0
  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 +1 -1
  22. package/dist-lib/bridge-contract-input.mjs +1 -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 +1 -1
  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 +30 -22
  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 +1 -1
  43. package/dist-lib/host/websocket-proxy.cjs +1 -1
  44. package/dist-lib/index.cjs +260 -17
  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 +2 -1
  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 +259 -16
  55. package/dist-lib/native-app-identity.cjs +1 -1
  56. package/dist-lib/native-app-identity.mjs +1 -1
  57. package/dist-lib/native-firebase-metadata.cjs +115 -0
  58. package/dist-lib/native-firebase-metadata.mjs +86 -0
  59. package/dist-lib/plist.cjs +1 -1
  60. package/dist-lib/plist.mjs +1 -1
  61. package/dist-lib/profiles.cjs +1 -1
  62. package/dist-lib/public-brand.cjs +1 -1
  63. package/dist-lib/react-native-host-modules.cjs +1 -1
  64. package/dist-lib/react-native-host-modules.mjs +1 -1
  65. package/dist-lib/render-mode.cjs +1 -1
  66. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  67. package/dist-lib/sdk.cjs +2 -2
  68. package/dist-lib/sdk.mjs +2 -2
  69. package/dist-lib/skills.cjs +480 -206
  70. package/dist-lib/swift-compile.cjs +1 -1
  71. package/dist-lib/swift-compile.mjs +1 -1
  72. package/dist-lib/swift-package.cjs +1 -1
  73. package/dist-lib/swift-package.mjs +1 -1
  74. package/dist-lib/swift-submit.cjs +1 -1
  75. package/dist-lib/swift-submit.mjs +1 -1
  76. package/dist-lib/swift.cjs +1 -1
  77. package/dist-lib/vite-swift.cjs +21 -13
  78. package/dist-lib/vite.cjs +1 -1
  79. package/kotlin/AppSources/androidx/activity/ActivityFlags.kt +9 -0
  80. package/kotlin/AppSources/androidx/activity/BackEventCompat.kt +25 -4
  81. package/kotlin/AppSources/androidx/activity/ComponentActivity.kt +15 -4
  82. package/kotlin/AppSources/androidx/activity/EdgeToEdge.kt +12 -8
  83. package/kotlin/AppSources/androidx/activity/ExperimentalActivityApi.kt +6 -0
  84. package/kotlin/AppSources/androidx/activity/OnBackPressedCallback.kt +50 -9
  85. package/kotlin/AppSources/androidx/activity/OnBackPressedDispatcher.kt +67 -89
  86. package/kotlin/AppSources/androidx/activity/compose/BackHandler.kt +55 -24
  87. package/kotlin/AppSources/androidx/activity/compose/internal/BackHandlerCompat.kt +38 -0
  88. package/kotlin/AppSources/androidx/activity/compose/internal/BackHandlerDispatcherCompat.kt +27 -0
  89. package/kotlin/AppSources/dev/peach/app/PeachApp.kt +5 -5
  90. package/kotlin/ComposeSources/PeachComposeHost.kt +31 -7
  91. package/kotlin/fixtures/kotlin-app/app/build.gradle.kts +1 -1
  92. package/kotlin/fixtures/kotlin-app-variants/Browse.kt +73 -0
  93. package/kotlin/fixtures/kotlin-app-variants/Focus.kt +62 -0
  94. package/kotlin/fixtures/kotlin-app-variants/Lists.kt +57 -0
  95. package/kotlin/fixtures/kotlin-app-variants/Nav.kt +47 -0
  96. package/kotlin/fixtures/kotlin-app-variants/Nested.kt +64 -0
  97. package/kotlin/fixtures/kotlin-app-variants/Pan.kt +38 -0
  98. package/kotlin/fixtures/kotlin-app-variants/Pickers.kt +44 -0
  99. package/kotlin/fixtures/kotlin-app-variants/Press.kt +43 -0
  100. package/kotlin/fixtures/kotlin-app-variants/Transition.kt +69 -0
  101. package/kotlin/fixtures/kotlin-app-variants/ViewConfig.kt +22 -0
  102. package/kotlin/toolchain.json +30 -2
  103. package/package.json +8 -1
  104. package/skills/contrast/SKILL.md +2 -1
  105. package/skills/peach-debug/SKILL.md +2 -2
  106. package/skills/peach-test/SKILL.md +3 -3
  107. package/src/expo-swift-host.ts +13 -4
  108. package/src/host/bridge-host.ts +3 -3
  109. package/src/kotlin-app.ts +35 -16
  110. package/src/kotlin-host.ts +1 -0
  111. package/src/metro-plugin.ts +24 -0
  112. package/src/native-firebase-config.ts +202 -21
  113. package/src/native-firebase-metadata.ts +114 -0
  114. package/src/swift-host.ts +30 -15
  115. package/src/vite-plugin.ts +46 -2
  116. package/kotlin/AppSources/androidx/activity/Cancellable.kt +0 -5
@@ -0,0 +1,38 @@
1
+ package androidx.activity.compose.internal
2
+
3
+ import androidx.activity.BackEventCompat
4
+ import androidx.activity.OnBackPressedCallback
5
+ import androidx.navigationevent.NavigationEvent
6
+ import androidx.navigationevent.NavigationEventHandler
7
+ import androidx.navigationevent.NavigationEventInfo
8
+
9
+ // activity-compose 1.13.0's BackHandlerCompat: one back handler as both a
10
+ // navigation event handler and a back callback, for whichever dispatcher the
11
+ // composition provides
12
+ internal abstract class BackHandlerCompat(val info: NavigationEventInfo) {
13
+ open fun onBackStarted(event: BackEventCompat) {}
14
+ open fun onBackProgressed(event: BackEventCompat) {}
15
+ abstract fun onBackCompleted()
16
+ open fun onBackCancelled() {}
17
+
18
+ open var isBackEnabled: Boolean
19
+ get() = onBackPressedCallback.isEnabled && navigationEventHandler.isBackEnabled
20
+ set(value) {
21
+ onBackPressedCallback.isEnabled = value
22
+ navigationEventHandler.isBackEnabled = value
23
+ }
24
+
25
+ val onBackPressedCallback = object : OnBackPressedCallback(enabled = false) {
26
+ override fun handleOnBackStarted(backEvent: BackEventCompat) = this@BackHandlerCompat.onBackStarted(backEvent)
27
+ override fun handleOnBackProgressed(backEvent: BackEventCompat) = this@BackHandlerCompat.onBackProgressed(backEvent)
28
+ override fun handleOnBackPressed() = this@BackHandlerCompat.onBackCompleted()
29
+ override fun handleOnBackCancelled() = this@BackHandlerCompat.onBackCancelled()
30
+ }
31
+
32
+ val navigationEventHandler = object : NavigationEventHandler<NavigationEventInfo>(info, isBackEnabled = false) {
33
+ override fun onBackStarted(event: NavigationEvent) = this@BackHandlerCompat.onBackStarted(BackEventCompat(event))
34
+ override fun onBackProgressed(event: NavigationEvent) = this@BackHandlerCompat.onBackProgressed(BackEventCompat(event))
35
+ override fun onBackCompleted() = this@BackHandlerCompat.onBackCompleted()
36
+ override fun onBackCancelled() = this@BackHandlerCompat.onBackCancelled()
37
+ }
38
+ }
@@ -0,0 +1,27 @@
1
+ package androidx.activity.compose.internal
2
+
3
+ import androidx.activity.OnBackPressedDispatcher
4
+ import androidx.navigationevent.NavigationEventDispatcher
5
+
6
+ // activity-compose 1.13.0's BackHandlerDispatcherCompat: the navigation event
7
+ // dispatcher when there is one, else the back dispatcher
8
+ internal class BackHandlerDispatcherCompat(
9
+ private val navigationEventDispatcher: NavigationEventDispatcher?,
10
+ private val onBackPressedDispatcher: OnBackPressedDispatcher?,
11
+ ) {
12
+ init {
13
+ requireNotNull(navigationEventDispatcher ?: onBackPressedDispatcher) {
14
+ "At least one dispatcher (NavigationEventDispatcher or OnBackPressedDispatcher) must be non-null."
15
+ }
16
+ }
17
+
18
+ fun addHandler(handler: BackHandlerCompat) {
19
+ if (navigationEventDispatcher != null) navigationEventDispatcher.addHandler(handler.navigationEventHandler)
20
+ else checkNotNull(onBackPressedDispatcher).addCallback(handler.onBackPressedCallback)
21
+ }
22
+
23
+ fun removeHandler(handler: BackHandlerCompat) {
24
+ if (navigationEventDispatcher != null) handler.navigationEventHandler.remove()
25
+ else handler.onBackPressedCallback.remove()
26
+ }
27
+ }
@@ -30,8 +30,6 @@ import androidx.compose.runtime.mutableStateOf
30
30
  import androidx.compose.runtime.setValue
31
31
  import androidx.compose.ui.text.font.FontFamily
32
32
  import androidx.lifecycle.Lifecycle
33
- import androidx.navigationevent.NavigationEventDispatcher
34
- import androidx.navigationevent.NavigationEventDispatcherOwner
35
33
  import PeachComposeMount
36
34
  import addMount
37
35
  import mounts
@@ -55,6 +53,9 @@ internal class PeachAppSpec(
55
53
  // the activity's configChanges includes uiMode: it takes a night mode
56
54
  // change in onConfigurationChanged rather than relaunching
57
55
  val handlesUiMode: Boolean,
56
+ // how the activity's window meets the keyboard, pan, resize or nothing:
57
+ // adjustPan and an unspecified adjust on a compose window pan
58
+ val softInputAdjust: String,
58
59
  // the app's view models, each built with the constructor the device's
59
60
  // default factory finds for it
60
61
  val viewModels: ViewModelProvider.Factory,
@@ -98,9 +99,7 @@ private class ActivityOwners(activity: ComponentActivity) : PlatformArchitecture
98
99
  override val lifecycleOwner = activity
99
100
  override val viewModelStoreOwner = activity
100
101
  override val savedStateRegistryOwner = activity
101
- override val navigationEventDispatcherOwner = object : NavigationEventDispatcherOwner {
102
- override val navigationEventDispatcher = NavigationEventDispatcher()
103
- }
102
+ override val navigationEventDispatcherOwner = activity
104
103
  }
105
104
 
106
105
  // the device and the application one started app process keeps, and what
@@ -167,6 +166,7 @@ internal fun peachStartApp(
167
166
  return app.activity.window.describe().put("ok", true).put("id", app.mountId)
168
167
  .put("wrapWidth", params?.width == ViewGroup.LayoutParams.WRAP_CONTENT)
169
168
  .put("wrapHeight", params?.height == ViewGroup.LayoutParams.WRAP_CONTENT)
169
+ .put("softInputAdjust", spec.softInputAdjust)
170
170
  .toString()
171
171
  } catch (error: Throwable) {
172
172
  return JSONObject().put("ok", false).put("code", "E_KOTLIN_APP")
@@ -24,6 +24,7 @@ import androidx.compose.ui.platform.PlatformArchitectureComponentsOwner
24
24
  import androidx.compose.ui.platform.PlatformWindowInsets
25
25
  import androidx.compose.ui.platform.PlatformContext
26
26
  import androidx.compose.ui.platform.PlatformTextInputMethodRequest
27
+ import androidx.compose.ui.platform.ViewConfiguration
27
28
  import androidx.compose.ui.platform.WindowInfo
28
29
  import androidx.compose.ui.platform.registerSkikoComposeImplementation
29
30
  import androidx.compose.ui.scene.CanvasLayersComposeScene
@@ -63,7 +64,10 @@ import androidx.compose.ui.unit.LayoutDirection
63
64
  import dev.onejs.one.OneNativeSourceViewDispatch
64
65
  import kotlin.io.encoding.Base64
65
66
  import kotlinx.coroutines.Dispatchers
67
+ import kotlinx.coroutines.Job
66
68
  import kotlinx.coroutines.awaitCancellation
69
+ import kotlinx.coroutines.currentCoroutineContext
70
+ import kotlinx.coroutines.job
67
71
  import org.jetbrains.skiko.SystemTheme
68
72
  import org.jetbrains.skia.BackendRenderTarget
69
73
  import org.jetbrains.skia.Bitmap
@@ -85,6 +89,20 @@ import org.json.JSONObject
85
89
  // owners and window insets come from the hosting window: an activity's
86
90
  // ComposeView passes the activity and the insets its window delivers; an
87
91
  // imported composable keeps the empty defaults.
92
+ // the values android.view.ViewConfiguration gives a window: config.xml's dp
93
+ // dimensions resolved as getDimensionPixelSize does, rounding half up
94
+ internal class AndroidViewConfiguration(private val density: Float) : ViewConfiguration {
95
+ private fun pixels(dp: Float) = (dp * density + 0.5f).toInt().toFloat()
96
+ override val longPressTimeoutMillis: Long = 400
97
+ override val doubleTapTimeoutMillis: Long = 300
98
+ override val doubleTapMinTimeMillis: Long = 40
99
+ override val touchSlop: Float = pixels(8f)
100
+ override val handwritingSlop: Float = pixels(2f)
101
+ override val maximumFlingVelocity: Float = pixels(8000f)
102
+ override val minimumFlingVelocity: Float = pixels(50f)
103
+ override val handwritingGestureLineMargin: Float = pixels(16f)
104
+ }
105
+
88
106
  internal class PeachComposePlatform(
89
107
  val density: Density,
90
108
  override val localeList: LocaleList,
@@ -101,6 +119,9 @@ internal class PeachComposePlatform(
101
119
  override val containerSize: IntSize get() = size
102
120
  override val containerDpSize: DpSize get() = with(density) { DpSize(size.width.toDp(), size.height.toDp()) }
103
121
  }
122
+ // android's ViewConfiguration: AOSP's dp defaults as the device's pixels,
123
+ // where the skiko default is a desktop's
124
+ override val viewConfiguration: ViewConfiguration = AndroidViewConfiguration(density.density)
104
125
  override val inputModeManager: InputModeManager = object : InputModeManager {
105
126
  override var inputMode: InputMode by mutableStateOf(InputMode.Touch)
106
127
  override fun requestInputMode(inputMode: InputMode): Boolean {
@@ -116,19 +137,22 @@ internal class PeachComposePlatform(
116
137
 
117
138
  // the input method session the focused text field holds, android's
118
139
  // InputConnection. each session gets a new token, so the host can tell a
119
- // late close from the session that replaced it
120
- var input: PeachInputSession? = null
121
- private set
140
+ // late close from the session that replaced it. a field that detaches
141
+ // cancels its session at once, though the cancelled coroutine finishes
142
+ // later; from the cancel on the session is closed, as compose web only
143
+ // reads a request inside the session's own coroutines
144
+ private var session: PeachInputSession? = null
145
+ val input get() = session?.takeIf { it.job.isActive }
122
146
  var onInputChange: () -> Unit = {}
123
147
  private var nextInput = 1
124
148
 
125
149
  override suspend fun startInputMethod(request: PlatformTextInputMethodRequest): Nothing {
126
- val session = PeachInputSession(nextInput++, request)
127
- input = session
150
+ val opened = PeachInputSession(nextInput++, request, currentCoroutineContext().job)
151
+ session = opened
128
152
  onInputChange()
129
153
  try { awaitCancellation() }
130
154
  finally {
131
- if (input === session) input = null
155
+ if (session === opened) session = null
132
156
  onInputChange()
133
157
  }
134
158
  }
@@ -147,7 +171,7 @@ internal class PeachComposePlatform(
147
171
  }
148
172
  }
149
173
 
150
- internal class PeachInputSession(val token: Int, val request: PlatformTextInputMethodRequest) {
174
+ internal class PeachInputSession(val token: Int, val request: PlatformTextInputMethodRequest, val job: Job) {
151
175
  var keyboard = true
152
176
  private set
153
177
  // each show or hide compose asks for, so a repeat still reaches the keyboard
@@ -23,7 +23,7 @@ android {
23
23
  }
24
24
 
25
25
  dependencies {
26
- implementation("androidx.activity:activity-compose:1.9.3")
26
+ implementation("androidx.activity:activity-compose:1.13.0")
27
27
  implementation("androidx.compose.ui:ui:1.13.0-alpha02")
28
28
  implementation("androidx.compose.foundation:foundation:1.13.0-alpha02")
29
29
  implementation("androidx.compose.material3:material3:1.5.0-alpha27")
@@ -0,0 +1,73 @@
1
+ package dev.contrast.kotlinapp
2
+
3
+ import androidx.compose.foundation.background
4
+ import androidx.compose.foundation.layout.Arrangement
5
+ import androidx.compose.foundation.layout.Box
6
+ import androidx.compose.foundation.layout.Column
7
+ import androidx.compose.foundation.layout.fillMaxSize
8
+ import androidx.compose.foundation.layout.fillMaxWidth
9
+ import androidx.compose.foundation.layout.height
10
+ import androidx.compose.foundation.layout.padding
11
+ import androidx.compose.foundation.layout.safeDrawingPadding
12
+ import androidx.compose.foundation.text.input.rememberTextFieldState
13
+ import androidx.compose.material3.ExpandedFullScreenSearchBar
14
+ import androidx.compose.material3.ExperimentalMaterial3Api
15
+ import androidx.compose.material3.SearchBar
16
+ import androidx.compose.material3.SearchBarDefaults
17
+ import androidx.compose.material3.Text
18
+ import androidx.compose.material3.carousel.HorizontalMultiBrowseCarousel
19
+ import androidx.compose.material3.carousel.rememberCarouselState
20
+ import androidx.compose.material3.rememberSearchBarState
21
+ import androidx.compose.runtime.Composable
22
+ import androidx.compose.runtime.getValue
23
+ import androidx.compose.runtime.mutableStateOf
24
+ import androidx.compose.runtime.remember
25
+ import androidx.compose.runtime.rememberCoroutineScope
26
+ import androidx.compose.runtime.setValue
27
+ import androidx.compose.ui.Alignment
28
+ import androidx.compose.ui.Modifier
29
+ import androidx.compose.ui.graphics.Color
30
+ import androidx.compose.ui.unit.dp
31
+ import kotlinx.coroutines.launch
32
+
33
+ // a search bar that expands over the screen and a multi browse carousel a
34
+ // swipe advances, reported in one line
35
+ @OptIn(ExperimentalMaterial3Api::class)
36
+ @Composable
37
+ fun Browse() {
38
+ val search = rememberSearchBarState()
39
+ val field = rememberTextFieldState()
40
+ val scope = rememberCoroutineScope()
41
+ var searched by remember { mutableStateOf("") }
42
+ val carousel = rememberCarouselState { 8 }
43
+ val colors = listOf(0xFF6750A4, 0xFF7D5260, 0xFF625B71, 0xFF386A20, 0xFF00639B, 0xFF8C4A00, 0xFF984061, 0xFF006A6A)
44
+ Column(
45
+ Modifier.fillMaxSize().background(Color(0xFFE8EEF8)).safeDrawingPadding().padding(16.dp),
46
+ verticalArrangement = Arrangement.spacedBy(12.dp),
47
+ ) {
48
+ Text("Browse: ${carousel.currentItem} searched $searched ${search.currentValue}")
49
+ val input = @Composable {
50
+ SearchBarDefaults.InputField(
51
+ searchBarState = search,
52
+ textFieldState = field,
53
+ onSearch = { searched = it; scope.launch { search.animateToCollapsed() } },
54
+ placeholder = { Text("Find") },
55
+ )
56
+ }
57
+ SearchBar(state = search, inputField = input)
58
+ ExpandedFullScreenSearchBar(state = search, inputField = input) {
59
+ Text("Suggestion", Modifier.padding(16.dp))
60
+ }
61
+ HorizontalMultiBrowseCarousel(
62
+ state = carousel,
63
+ preferredItemWidth = 186.dp,
64
+ modifier = Modifier.fillMaxWidth().height(200.dp),
65
+ itemSpacing = 8.dp,
66
+ ) { index ->
67
+ Box(
68
+ Modifier.fillMaxSize().maskClip(SearchBarDefaults.inputFieldShape).background(Color(colors[index])),
69
+ contentAlignment = Alignment.Center,
70
+ ) { Text("Item $index", color = Color.White) }
71
+ }
72
+ }
73
+ }
@@ -0,0 +1,62 @@
1
+ package dev.contrast.kotlinapp
2
+
3
+ import androidx.compose.foundation.ExperimentalFoundationApi
4
+ import androidx.compose.foundation.background
5
+ import androidx.compose.foundation.layout.Arrangement
6
+ import androidx.compose.foundation.layout.Column
7
+ import androidx.compose.foundation.layout.Row
8
+ import androidx.compose.foundation.layout.WindowInsets
9
+ import androidx.compose.foundation.layout.fillMaxSize
10
+ import androidx.compose.foundation.layout.ime
11
+ import androidx.compose.foundation.layout.padding
12
+ import androidx.compose.foundation.layout.safeDrawingPadding
13
+ import androidx.compose.foundation.text.KeyboardOptions
14
+ import androidx.compose.foundation.text.input.rememberTextFieldState
15
+ import androidx.compose.material3.Button
16
+ import androidx.compose.material3.Text
17
+ import androidx.compose.material3.TextField
18
+ import androidx.compose.runtime.Composable
19
+ import androidx.compose.runtime.getValue
20
+ import androidx.compose.runtime.mutableStateOf
21
+ import androidx.compose.runtime.remember
22
+ import androidx.compose.runtime.setValue
23
+ import androidx.compose.ui.Modifier
24
+ import androidx.compose.ui.focus.FocusRequester
25
+ import androidx.compose.ui.focus.focusRequester
26
+ import androidx.compose.ui.focus.onFocusChanged
27
+ import androidx.compose.ui.graphics.Color
28
+ import androidx.compose.ui.platform.LocalDensity
29
+ import androidx.compose.ui.unit.dp
30
+
31
+ // two fields focused from code, one with showKeyboardOnFocus off, and whether
32
+ // the keyboard came up for each
33
+ @OptIn(ExperimentalFoundationApi::class)
34
+ @Composable
35
+ fun Focus() {
36
+ val alpha = remember { FocusRequester() }
37
+ val beta = remember { FocusRequester() }
38
+ var alphaFocused by remember { mutableStateOf(false) }
39
+ var betaFocused by remember { mutableStateOf(false) }
40
+ Column(
41
+ Modifier.fillMaxSize().background(Color(0xFFE8EEF8)).safeDrawingPadding().padding(16.dp),
42
+ verticalArrangement = Arrangement.spacedBy(12.dp),
43
+ ) {
44
+ val ime = WindowInsets.ime.getBottom(LocalDensity.current) > 0
45
+ Text("Focus: alpha $alphaFocused beta $betaFocused ime $ime")
46
+ Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
47
+ Button(onClick = { alpha.requestFocus() }) { Text("Focus alpha") }
48
+ Button(onClick = { beta.requestFocus() }) { Text("Focus beta") }
49
+ }
50
+ TextField(
51
+ rememberTextFieldState(),
52
+ Modifier.focusRequester(alpha).onFocusChanged { alphaFocused = it.isFocused },
53
+ label = { Text("Alpha") },
54
+ )
55
+ TextField(
56
+ rememberTextFieldState(),
57
+ Modifier.focusRequester(beta).onFocusChanged { betaFocused = it.isFocused },
58
+ label = { Text("Beta") },
59
+ keyboardOptions = KeyboardOptions(showKeyboardOnFocus = false),
60
+ )
61
+ }
62
+ }
@@ -0,0 +1,57 @@
1
+ package dev.contrast.kotlinapp
2
+
3
+ import androidx.compose.foundation.background
4
+ import androidx.compose.foundation.layout.Arrangement
5
+ import androidx.compose.foundation.layout.Box
6
+ import androidx.compose.foundation.layout.Column
7
+ import androidx.compose.foundation.layout.PaddingValues
8
+ import androidx.compose.foundation.layout.aspectRatio
9
+ import androidx.compose.foundation.layout.fillMaxSize
10
+ import androidx.compose.foundation.layout.fillMaxWidth
11
+ import androidx.compose.foundation.layout.height
12
+ import androidx.compose.foundation.layout.padding
13
+ import androidx.compose.foundation.layout.safeDrawingPadding
14
+ import androidx.compose.foundation.layout.size
15
+ import androidx.compose.foundation.lazy.LazyRow
16
+ import androidx.compose.foundation.lazy.grid.GridCells
17
+ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
18
+ import androidx.compose.foundation.lazy.grid.rememberLazyGridState
19
+ import androidx.compose.foundation.lazy.rememberLazyListState
20
+ import androidx.compose.material3.Text
21
+ import androidx.compose.runtime.Composable
22
+ import androidx.compose.ui.Alignment
23
+ import androidx.compose.ui.Modifier
24
+ import androidx.compose.ui.graphics.Color
25
+ import androidx.compose.ui.unit.dp
26
+
27
+ // a lazy row a horizontal drag scrolls and a lazy grid a vertical drag
28
+ // scrolls, each reporting its first visible item and offset in dp
29
+ @Composable
30
+ fun Lists() {
31
+ val row = rememberLazyListState()
32
+ val grid = rememberLazyGridState()
33
+ val colors = listOf(Color(0xFF3F51B5), Color(0xFFE91E63), Color(0xFF009688), Color(0xFFFF9800))
34
+ Column(
35
+ Modifier.fillMaxSize().background(Color(0xFFE8EEF8)).safeDrawingPadding().padding(16.dp),
36
+ verticalArrangement = Arrangement.spacedBy(12.dp),
37
+ ) {
38
+ Text("Row: ${row.firstVisibleItemIndex} ${row.firstVisibleItemScrollOffset} grid ${grid.firstVisibleItemIndex} ${grid.firstVisibleItemScrollOffset}")
39
+ LazyRow(Modifier.fillMaxWidth().height(96.dp), state = row, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
40
+ items(30) { i ->
41
+ Box(Modifier.size(96.dp).background(colors[i % 4]), contentAlignment = Alignment.Center) { Text("R$i", color = Color.White) }
42
+ }
43
+ }
44
+ LazyVerticalGrid(
45
+ GridCells.Fixed(3),
46
+ Modifier.fillMaxWidth().weight(1f),
47
+ state = grid,
48
+ contentPadding = PaddingValues(4.dp),
49
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
50
+ verticalArrangement = Arrangement.spacedBy(8.dp),
51
+ ) {
52
+ items(90) { i ->
53
+ Box(Modifier.aspectRatio(1f).background(colors[(i + 1) % 4]), contentAlignment = Alignment.Center) { Text("G$i", color = Color.White) }
54
+ }
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,47 @@
1
+ package dev.contrast.kotlinapp
2
+
3
+ import androidx.compose.foundation.background
4
+ import androidx.compose.foundation.layout.Arrangement
5
+ import androidx.compose.foundation.layout.Column
6
+ import androidx.compose.foundation.layout.fillMaxSize
7
+ import androidx.compose.foundation.layout.padding
8
+ import androidx.compose.foundation.layout.safeDrawingPadding
9
+ import androidx.compose.material3.Button
10
+ import androidx.compose.material3.Text
11
+ import androidx.compose.runtime.Composable
12
+ import androidx.compose.runtime.getValue
13
+ import androidx.compose.runtime.mutableIntStateOf
14
+ import androidx.compose.runtime.saveable.rememberSaveable
15
+ import androidx.compose.runtime.setValue
16
+ import androidx.compose.ui.Modifier
17
+ import androidx.compose.ui.graphics.Color
18
+ import androidx.compose.ui.unit.dp
19
+ import androidx.navigation.compose.NavHost
20
+ import androidx.navigation.compose.composable
21
+ import androidx.navigation.compose.currentBackStackEntryAsState
22
+ import androidx.navigation.compose.rememberNavController
23
+
24
+ // a NavHost with a home destination holding saveable state and a detail
25
+ // destination the system back pops
26
+ @Composable
27
+ fun Nav() {
28
+ val nav = rememberNavController()
29
+ val entry by nav.currentBackStackEntryAsState()
30
+ Column(
31
+ Modifier.fillMaxSize().background(Color(0xFFE8EEF8)).safeDrawingPadding().padding(16.dp),
32
+ verticalArrangement = Arrangement.spacedBy(12.dp),
33
+ ) {
34
+ Text("Route: ${entry?.destination?.route}")
35
+ NavHost(nav, startDestination = "home") {
36
+ composable("home") {
37
+ var taps by rememberSaveable { mutableIntStateOf(0) }
38
+ Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
39
+ Text("Home taps $taps")
40
+ Button(onClick = { taps++ }) { Text("Tap") }
41
+ Button(onClick = { nav.navigate("detail") }) { Text("Open detail") }
42
+ }
43
+ }
44
+ composable("detail") { Text("Detail screen") }
45
+ }
46
+ }
47
+ }
@@ -0,0 +1,64 @@
1
+ package dev.contrast.kotlinapp
2
+
3
+ import androidx.compose.foundation.background
4
+ import androidx.compose.foundation.horizontalScroll
5
+ import androidx.compose.foundation.layout.Arrangement
6
+ import androidx.compose.foundation.layout.Box
7
+ import androidx.compose.foundation.layout.Column
8
+ import androidx.compose.foundation.layout.Row
9
+ import androidx.compose.foundation.layout.fillMaxSize
10
+ import androidx.compose.foundation.layout.fillMaxWidth
11
+ import androidx.compose.foundation.layout.height
12
+ import androidx.compose.foundation.layout.padding
13
+ import androidx.compose.foundation.layout.safeDrawingPadding
14
+ import androidx.compose.foundation.layout.size
15
+ import androidx.compose.foundation.rememberScrollState
16
+ import androidx.compose.foundation.verticalScroll
17
+ import androidx.compose.material3.Text
18
+ import androidx.compose.runtime.Composable
19
+ import androidx.compose.runtime.getValue
20
+ import androidx.compose.runtime.mutableFloatStateOf
21
+ import androidx.compose.runtime.remember
22
+ import androidx.compose.runtime.setValue
23
+ import androidx.compose.ui.Alignment
24
+ import androidx.compose.ui.Modifier
25
+ import androidx.compose.ui.geometry.Offset
26
+ import androidx.compose.ui.graphics.Color
27
+ import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
28
+ import androidx.compose.ui.input.nestedscroll.NestedScrollSource
29
+ import androidx.compose.ui.input.nestedscroll.nestedScroll
30
+ import androidx.compose.ui.platform.LocalDensity
31
+ import androidx.compose.ui.unit.dp
32
+ import kotlin.math.roundToInt
33
+
34
+ // a header a nested scroll connection collapses before a verticalScroll
35
+ // column moves, and a horizontalScroll row inside that column
36
+ @Composable
37
+ fun Nested() {
38
+ val maxHeader = with(LocalDensity.current) { 160.dp.toPx() }
39
+ var header by remember { mutableFloatStateOf(maxHeader) }
40
+ val column = rememberScrollState()
41
+ val row = rememberScrollState()
42
+ val connection = remember {
43
+ object : NestedScrollConnection {
44
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
45
+ if (available.y >= 0) return Offset.Zero
46
+ val next = (header + available.y).coerceAtLeast(0f)
47
+ val used = next - header
48
+ header = next
49
+ return Offset(0f, used)
50
+ }
51
+ }
52
+ }
53
+ val colors = listOf(Color(0xFF3F51B5), Color(0xFFE91E63), Color(0xFF009688), Color(0xFFFF9800))
54
+ Column(Modifier.fillMaxSize().background(Color(0xFFE8EEF8)).safeDrawingPadding().nestedScroll(connection)) {
55
+ Text("Nested: header ${header.roundToInt()} column ${column.value} row ${row.value}", Modifier.padding(16.dp))
56
+ Box(Modifier.fillMaxWidth().height(with(LocalDensity.current) { header.toDp() }).background(Color(0xFF673AB7)))
57
+ Column(Modifier.fillMaxWidth().weight(1f).verticalScroll(column).padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
58
+ Row(Modifier.horizontalScroll(row), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
59
+ repeat(12) { i -> Box(Modifier.size(96.dp).background(colors[i % 4]), contentAlignment = Alignment.Center) { Text("H$i", color = Color.White) } }
60
+ }
61
+ repeat(20) { i -> Box(Modifier.fillMaxWidth().height(64.dp).background(colors[(i + 2) % 4]), contentAlignment = Alignment.Center) { Text("V$i", color = Color.White) } }
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,38 @@
1
+ package dev.contrast.kotlinapp
2
+
3
+ import androidx.compose.foundation.background
4
+ import androidx.compose.foundation.layout.Column
5
+ import androidx.compose.foundation.layout.Spacer
6
+ import androidx.compose.foundation.layout.fillMaxSize
7
+ import androidx.compose.foundation.layout.fillMaxWidth
8
+ import androidx.compose.foundation.layout.padding
9
+ import androidx.compose.material3.OutlinedTextField
10
+ import androidx.compose.material3.Text
11
+ import androidx.compose.runtime.Composable
12
+ import androidx.compose.runtime.getValue
13
+ import androidx.compose.runtime.mutableStateOf
14
+ import androidx.compose.runtime.remember
15
+ import androidx.compose.runtime.setValue
16
+ import androidx.compose.ui.Modifier
17
+ import androidx.compose.ui.focus.onFocusChanged
18
+ import androidx.compose.ui.graphics.Color
19
+ import androidx.compose.ui.unit.dp
20
+
21
+ // a field at the bottom of a window that fits system windows, which the
22
+ // keyboard would cover: android pans the window to keep it in view
23
+ @Composable
24
+ fun Pan() {
25
+ var text by remember { mutableStateOf("") }
26
+ var focused by remember { mutableStateOf(false) }
27
+ Column(Modifier.fillMaxSize().background(Color(0xFFE8EEF8)).padding(16.dp)) {
28
+ Text("Top line")
29
+ Spacer(Modifier.weight(1f))
30
+ Text("Pan: focused $focused")
31
+ OutlinedTextField(
32
+ value = text,
33
+ onValueChange = { text = it },
34
+ label = { Text("Reply") },
35
+ modifier = Modifier.fillMaxWidth().padding(top = 12.dp).onFocusChanged { focused = it.isFocused },
36
+ )
37
+ }
38
+ }
@@ -0,0 +1,44 @@
1
+ package dev.contrast.kotlinapp
2
+
3
+ import androidx.compose.foundation.background
4
+ import androidx.compose.foundation.layout.Arrangement
5
+ import androidx.compose.foundation.layout.Column
6
+ import androidx.compose.foundation.layout.fillMaxSize
7
+ import androidx.compose.foundation.layout.padding
8
+ import androidx.compose.foundation.layout.safeDrawingPadding
9
+ import androidx.compose.material3.Button
10
+ import androidx.compose.material3.ExperimentalMaterial3Api
11
+ import androidx.compose.material3.PlainTooltip
12
+ import androidx.compose.material3.Text
13
+ import androidx.compose.material3.TimePicker
14
+ import androidx.compose.material3.TooltipBox
15
+ import androidx.compose.material3.TooltipDefaults
16
+ import androidx.compose.material3.rememberTimePickerState
17
+ import androidx.compose.material3.rememberTooltipState
18
+ import androidx.compose.runtime.Composable
19
+ import androidx.compose.ui.Modifier
20
+ import androidx.compose.ui.graphics.Color
21
+ import androidx.compose.ui.unit.dp
22
+
23
+ // a persistent tooltip a long press shows and a 12 hour time picker whose dial
24
+ // a tap sets, both reported in one line
25
+ @OptIn(ExperimentalMaterial3Api::class)
26
+ @Composable
27
+ fun Pickers() {
28
+ val time = rememberTimePickerState(initialHour = 9, initialMinute = 30, is24Hour = false)
29
+ val tip = rememberTooltipState(isPersistent = true)
30
+ Column(
31
+ Modifier.fillMaxSize().background(Color(0xFFE8EEF8)).safeDrawingPadding().padding(16.dp),
32
+ verticalArrangement = Arrangement.spacedBy(12.dp),
33
+ ) {
34
+ Text("Time: ${time.hour}:${time.minute} tip ${tip.isVisible}")
35
+ TooltipBox(
36
+ positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
37
+ tooltip = { PlainTooltip { Text("Saves the draft") } },
38
+ state = tip,
39
+ ) {
40
+ Button(onClick = {}) { Text("Save") }
41
+ }
42
+ TimePicker(time)
43
+ }
44
+ }
@@ -0,0 +1,43 @@
1
+ package dev.contrast.kotlinapp
2
+
3
+ import androidx.compose.foundation.ExperimentalFoundationApi
4
+ import androidx.compose.foundation.background
5
+ import androidx.compose.foundation.combinedClickable
6
+ import androidx.compose.foundation.layout.Arrangement
7
+ import androidx.compose.foundation.layout.Box
8
+ import androidx.compose.foundation.layout.Column
9
+ import androidx.compose.foundation.layout.fillMaxSize
10
+ import androidx.compose.foundation.layout.fillMaxWidth
11
+ import androidx.compose.foundation.layout.height
12
+ import androidx.compose.foundation.layout.padding
13
+ import androidx.compose.foundation.layout.safeDrawingPadding
14
+ import androidx.compose.material3.Text
15
+ import androidx.compose.runtime.Composable
16
+ import androidx.compose.runtime.getValue
17
+ import androidx.compose.runtime.mutableStateOf
18
+ import androidx.compose.runtime.remember
19
+ import androidx.compose.runtime.setValue
20
+ import androidx.compose.ui.Alignment
21
+ import androidx.compose.ui.Modifier
22
+ import androidx.compose.ui.graphics.Color
23
+ import androidx.compose.ui.unit.dp
24
+
25
+ // a target combinedClickable counts taps and long presses on, so a hold just
26
+ // past the long press timeout tells which timeout the window uses
27
+ @OptIn(ExperimentalFoundationApi::class)
28
+ @Composable
29
+ fun Press() {
30
+ var clicks by remember { mutableStateOf(0) }
31
+ var longs by remember { mutableStateOf(0) }
32
+ Column(
33
+ Modifier.fillMaxSize().background(Color(0xFFE8EEF8)).safeDrawingPadding().padding(16.dp),
34
+ verticalArrangement = Arrangement.spacedBy(12.dp),
35
+ ) {
36
+ Text("Press: clicks $clicks long $longs")
37
+ Box(
38
+ Modifier.fillMaxWidth().height(200.dp).background(Color(0xFFCFD8DC))
39
+ .combinedClickable(onLongClick = { longs += 1 }) { clicks += 1 },
40
+ contentAlignment = Alignment.Center,
41
+ ) { Text("Target") }
42
+ }
43
+ }