peachsim 0.1.666 → 0.1.668
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/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 +1 -1
- package/dist-lib/bridge-contract-input.mjs +1 -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 +1 -1
- 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 +9 -3
- 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 +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +1 -1
- 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 +4 -1
- 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 +1 -1
- package/dist-lib/native-app-identity.cjs +1 -1
- package/dist-lib/native-app-identity.mjs +1 -1
- package/dist-lib/plist.cjs +1 -1
- package/dist-lib/plist.mjs +1 -1
- 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 +1 -1
- package/dist-lib/sdk.cjs +1 -1
- package/dist-lib/sdk.mjs +1 -1
- package/dist-lib/skills.cjs +22 -19
- 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 +1 -1
- package/kotlin/AppSources/dev/peach/app/PeachApp.kt +15 -3
- package/kotlin/ComposeSources/PeachComposeHost.kt +200 -0
- package/kotlin/Sources/org/json/JSON.kt +13 -0
- package/kotlin/fixtures/kotlin-app-variants/Anim.kt +62 -0
- package/kotlin/fixtures/kotlin-app-variants/Controls.kt +62 -0
- package/kotlin/fixtures/kotlin-app-variants/Drag.kt +56 -0
- package/kotlin/fixtures/kotlin-app-variants/Draw.kt +74 -0
- package/kotlin/fixtures/kotlin-app-variants/Form.kt +47 -0
- package/kotlin/fixtures/kotlin-app-variants/Ime.kt +51 -0
- package/kotlin/fixtures/kotlin-app-variants/Note.kt +40 -0
- package/package.json +1 -1
- package/src/expo-swift-host.ts +6 -2
- package/src/kotlin-host.ts +3 -0
|
@@ -68,8 +68,9 @@ internal fun peachApplication(): Application = runningApp().application
|
|
|
68
68
|
// them on an edge-to-edge window, none once the decor fits system windows.
|
|
69
69
|
// insets lists the status bars, navigation bars, display cutout, system
|
|
70
70
|
// gestures, mandatory system gestures and tappable element, each left, top,
|
|
71
|
-
// right and bottom.
|
|
72
|
-
|
|
71
|
+
// right and bottom. the IME's inset is the keyboard's height above the
|
|
72
|
+
// screen's bottom edge, which moves with the keyboard frame by frame.
|
|
73
|
+
private class DeviceInsets(private val window: Window, insets: String, imeBottom: () -> Int) : PlatformWindowInsets {
|
|
73
74
|
private val pixels = insets.split(',').map { it.toInt() }.also { require(it.size == 24) }
|
|
74
75
|
|
|
75
76
|
// read when compose lays out, so a later change to the window applies
|
|
@@ -84,6 +85,7 @@ private class DeviceInsets(private val window: Window, insets: String) : Platfor
|
|
|
84
85
|
override val systemGestures = type(3)
|
|
85
86
|
override val mandatorySystemGestures = type(4)
|
|
86
87
|
override val tappableElement = type(5)
|
|
88
|
+
override val ime = PlatformInsets({ 0 }, { 0 }, { 0 }, { if (window.fitsSystemWindows) 0 else imeBottom() })
|
|
87
89
|
override val systemBars = PlatformInsets(
|
|
88
90
|
{ maxOf(statusBars.left, navigationBars.left) },
|
|
89
91
|
{ maxOf(statusBars.top, navigationBars.top) },
|
|
@@ -127,6 +129,8 @@ private class AppProcess(
|
|
|
127
129
|
// the night mode last reported to the activity: ActivityRecord compares
|
|
128
130
|
// the configuration with it before a relaunch or onConfigurationChanged
|
|
129
131
|
var reportedNight = false
|
|
132
|
+
// the software keyboard's height above the screen's bottom edge, in pixels
|
|
133
|
+
var ime by mutableIntStateOf(0)
|
|
130
134
|
}
|
|
131
135
|
|
|
132
136
|
private var process: AppProcess? = null
|
|
@@ -228,7 +232,7 @@ private fun launchActivity(app: AppProcess, lastNonConfigurationInstance: Any?):
|
|
|
228
232
|
app.density, app.fontScale, app.locale, app.gpu, app.stencilBits, app.fonts, app.invalidate,
|
|
229
233
|
night = isNight(activity.resources.configuration),
|
|
230
234
|
owners = componentActivity?.let(::ActivityOwners),
|
|
231
|
-
insets = DeviceInsets(window, app.insets),
|
|
235
|
+
insets = DeviceInsets(window, app.insets) { app.ime },
|
|
232
236
|
clockRunning = { componentActivity == null || componentActivity.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) },
|
|
233
237
|
)
|
|
234
238
|
mount.setContent {
|
|
@@ -322,6 +326,14 @@ private fun reportConfiguration(app: AppProcess): Boolean {
|
|
|
322
326
|
private fun isNight(configuration: Configuration) =
|
|
323
327
|
configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
|
|
324
328
|
|
|
329
|
+
// the software keyboard moved: its inset reaches the window's content
|
|
330
|
+
internal fun peachAppIme(bottom: Int) {
|
|
331
|
+
val app = runningApp()
|
|
332
|
+
if (app.ime == bottom) return
|
|
333
|
+
app.ime = bottom
|
|
334
|
+
app.invalidate(true)
|
|
335
|
+
}
|
|
336
|
+
|
|
325
337
|
// the device's night mode changed. the process's configuration changes in
|
|
326
338
|
// place and the Application hears it first; a visible activity then takes it.
|
|
327
339
|
// a stopped activity takes it once visible again, if it still differs.
|
|
@@ -9,9 +9,13 @@ import androidx.compose.runtime.setValue
|
|
|
9
9
|
import androidx.compose.runtime.snapshots.Snapshot
|
|
10
10
|
import androidx.compose.ui.LocalSystemTheme
|
|
11
11
|
import androidx.compose.ui.geometry.Offset
|
|
12
|
+
import androidx.compose.ui.geometry.Rect
|
|
12
13
|
import androidx.compose.ui.graphics.asComposeCanvas
|
|
13
14
|
import androidx.compose.ui.input.InputMode
|
|
14
15
|
import androidx.compose.ui.input.InputModeManager
|
|
16
|
+
import androidx.compose.ui.input.key.Key
|
|
17
|
+
import androidx.compose.ui.input.key.KeyEvent
|
|
18
|
+
import androidx.compose.ui.input.key.KeyEventType
|
|
15
19
|
import androidx.compose.ui.input.pointer.PointerEventType
|
|
16
20
|
import androidx.compose.ui.input.pointer.PointerType
|
|
17
21
|
import androidx.compose.ui.platform.FrameRecomposer
|
|
@@ -19,6 +23,7 @@ import androidx.compose.ui.platform.LocalFontFamilyResolver
|
|
|
19
23
|
import androidx.compose.ui.platform.PlatformArchitectureComponentsOwner
|
|
20
24
|
import androidx.compose.ui.platform.PlatformWindowInsets
|
|
21
25
|
import androidx.compose.ui.platform.PlatformContext
|
|
26
|
+
import androidx.compose.ui.platform.PlatformTextInputMethodRequest
|
|
22
27
|
import androidx.compose.ui.platform.WindowInfo
|
|
23
28
|
import androidx.compose.ui.platform.registerSkikoComposeImplementation
|
|
24
29
|
import androidx.compose.ui.scene.CanvasLayersComposeScene
|
|
@@ -34,6 +39,19 @@ import androidx.compose.ui.text.font.FontWeight
|
|
|
34
39
|
import androidx.compose.ui.text.font.PlatformTypefacesLoader
|
|
35
40
|
import androidx.compose.ui.text.font.createPlatformFontFamilyResolver
|
|
36
41
|
import androidx.compose.ui.text.font.platformTypefacesLoader
|
|
42
|
+
import androidx.compose.ui.text.input.BackspaceCommand
|
|
43
|
+
import androidx.compose.ui.text.input.CommitTextCommand
|
|
44
|
+
import androidx.compose.ui.text.input.EditCommand
|
|
45
|
+
import androidx.compose.ui.text.input.FinishComposingTextCommand
|
|
46
|
+
import androidx.compose.ui.text.input.ImeAction
|
|
47
|
+
import androidx.compose.ui.text.input.ImeOptions
|
|
48
|
+
import androidx.compose.ui.text.input.KeyboardCapitalization
|
|
49
|
+
import androidx.compose.ui.text.input.KeyboardType
|
|
50
|
+
import androidx.compose.ui.text.input.PlatformTextInputService
|
|
51
|
+
import androidx.compose.ui.text.input.SetComposingRegionCommand
|
|
52
|
+
import androidx.compose.ui.text.input.SetComposingTextCommand
|
|
53
|
+
import androidx.compose.ui.text.input.SetSelectionCommand
|
|
54
|
+
import androidx.compose.ui.text.input.TextFieldValue
|
|
37
55
|
import androidx.compose.ui.text.intl.LocaleList
|
|
38
56
|
import androidx.compose.ui.text.intl.setPlatformLocaleProvider
|
|
39
57
|
import androidx.compose.ui.text.platform.Font
|
|
@@ -45,6 +63,7 @@ import androidx.compose.ui.unit.LayoutDirection
|
|
|
45
63
|
import dev.onejs.one.OneNativeSourceViewDispatch
|
|
46
64
|
import kotlin.io.encoding.Base64
|
|
47
65
|
import kotlinx.coroutines.Dispatchers
|
|
66
|
+
import kotlinx.coroutines.awaitCancellation
|
|
48
67
|
import org.jetbrains.skiko.SystemTheme
|
|
49
68
|
import org.jetbrains.skia.BackendRenderTarget
|
|
50
69
|
import org.jetbrains.skia.Bitmap
|
|
@@ -94,6 +113,129 @@ internal class PeachComposePlatform(
|
|
|
94
113
|
override fun onSemanticsOwnerRemoved(semanticsOwner: SemanticsOwner) { owners.remove(semanticsOwner) }
|
|
95
114
|
override fun onSemanticsChange(semanticsOwner: SemanticsOwner) = Unit
|
|
96
115
|
override fun onLayoutChange(semanticsOwner: SemanticsOwner, semanticsNodeId: Int) = Unit
|
|
116
|
+
|
|
117
|
+
// the input method session the focused text field holds, android's
|
|
118
|
+
// 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
|
|
122
|
+
var onInputChange: () -> Unit = {}
|
|
123
|
+
private var nextInput = 1
|
|
124
|
+
|
|
125
|
+
override suspend fun startInputMethod(request: PlatformTextInputMethodRequest): Nothing {
|
|
126
|
+
val session = PeachInputSession(nextInput++, request)
|
|
127
|
+
input = session
|
|
128
|
+
onInputChange()
|
|
129
|
+
try { awaitCancellation() }
|
|
130
|
+
finally {
|
|
131
|
+
if (input === session) input = null
|
|
132
|
+
onInputChange()
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// compose's keyboard controller: show and hide move the keyboard and keep
|
|
137
|
+
// the session, as Done does on android. a session opens with the keyboard
|
|
138
|
+
// up, as android's legacy text input service shows it on startInput
|
|
139
|
+
@Suppress("DEPRECATION")
|
|
140
|
+
override val textInputService: PlatformTextInputService = object : PlatformTextInputService {
|
|
141
|
+
override fun startInput(value: TextFieldValue, imeOptions: ImeOptions,
|
|
142
|
+
onEditCommand: (List<EditCommand>) -> Unit, onImeActionPerformed: (ImeAction) -> Unit) = Unit
|
|
143
|
+
override fun stopInput() = Unit
|
|
144
|
+
override fun showSoftwareKeyboard() { input?.request(true); onInputChange() }
|
|
145
|
+
override fun hideSoftwareKeyboard() { input?.request(false); onInputChange() }
|
|
146
|
+
override fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) = Unit
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
internal class PeachInputSession(val token: Int, val request: PlatformTextInputMethodRequest) {
|
|
151
|
+
var keyboard = true
|
|
152
|
+
private set
|
|
153
|
+
// each show or hide compose asks for, so a repeat still reaches the keyboard
|
|
154
|
+
private var keyboardRequest = 0
|
|
155
|
+
private val options get() = request.imeOptions
|
|
156
|
+
|
|
157
|
+
// EditorInfo.android.kt: a single line field's default action is Done; a
|
|
158
|
+
// multi line field's default has no enter action, so enter types a newline
|
|
159
|
+
private val action: ImeAction? get() = when (options.imeAction) {
|
|
160
|
+
ImeAction.Default -> if (options.singleLine) ImeAction.Done else null
|
|
161
|
+
ImeAction.None -> null
|
|
162
|
+
else -> options.imeAction
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
fun request(visible: Boolean) {
|
|
166
|
+
keyboard = visible
|
|
167
|
+
keyboardRequest++
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private fun rect(rect: Rect?) = rect?.let { JSONArray().put(it.left).put(it.top).put(it.right).put(it.bottom) }
|
|
171
|
+
|
|
172
|
+
fun state(): JSONObject {
|
|
173
|
+
val value = request.value()
|
|
174
|
+
return JSONObject().put("token", token).put("keyboard", keyboard).put("keyboardRequest", keyboardRequest)
|
|
175
|
+
.put("value", value.text).put("selectionStart", value.selection.start).put("selectionEnd", value.selection.end)
|
|
176
|
+
.put("singleLine", options.singleLine).put("enterInsertsNewline", action == null && !options.singleLine)
|
|
177
|
+
.put("keyboardType", when (options.keyboardType) {
|
|
178
|
+
KeyboardType.Number -> "number"
|
|
179
|
+
KeyboardType.Phone -> "phone"
|
|
180
|
+
KeyboardType.Uri -> "uri"
|
|
181
|
+
KeyboardType.Email -> "email"
|
|
182
|
+
KeyboardType.Password -> "password"
|
|
183
|
+
KeyboardType.NumberPassword -> "number-password"
|
|
184
|
+
KeyboardType.Decimal -> "decimal"
|
|
185
|
+
KeyboardType.Ascii -> "ascii"
|
|
186
|
+
else -> "text"
|
|
187
|
+
})
|
|
188
|
+
.put("imeAction", when (action) {
|
|
189
|
+
ImeAction.Done -> "done"
|
|
190
|
+
ImeAction.Go -> "go"
|
|
191
|
+
ImeAction.Search -> "search"
|
|
192
|
+
ImeAction.Send -> "send"
|
|
193
|
+
ImeAction.Next -> "next"
|
|
194
|
+
ImeAction.Previous -> "previous"
|
|
195
|
+
else -> "default"
|
|
196
|
+
})
|
|
197
|
+
.put("capitalization", when (options.capitalization) {
|
|
198
|
+
KeyboardCapitalization.Characters -> "characters"
|
|
199
|
+
KeyboardCapitalization.Words -> "words"
|
|
200
|
+
KeyboardCapitalization.Sentences -> "sentences"
|
|
201
|
+
else -> "none"
|
|
202
|
+
})
|
|
203
|
+
.put("autoCorrect", options.autoCorrect)
|
|
204
|
+
.put("field", rect(request.textFieldRectInRoot()))
|
|
205
|
+
.put("caret", rect(request.focusedRectInRoot()))
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// one keyboard edit, applied as one batch the way RecordingInputConnection
|
|
209
|
+
// ends a batch edit. typed text commits as an IME's commitText does; a
|
|
210
|
+
// composition replaces its own region, else the edit's range
|
|
211
|
+
fun edit(command: JSONObject): Boolean {
|
|
212
|
+
val value = request.value()
|
|
213
|
+
val start = command.optInt("start")
|
|
214
|
+
val end = command.optInt("end")
|
|
215
|
+
val composing = value.composition?.let { it.start == start && it.end == end } == true
|
|
216
|
+
val commands: List<EditCommand> = when (command.getString("kind")) {
|
|
217
|
+
"text" -> listOf(CommitTextCommand(command.getString("text"), 1))
|
|
218
|
+
"backspace" -> listOf(BackspaceCommand())
|
|
219
|
+
"enter" -> {
|
|
220
|
+
val performed = action
|
|
221
|
+
if (performed != null) {
|
|
222
|
+
request.onImeAction?.invoke(performed)
|
|
223
|
+
return true
|
|
224
|
+
}
|
|
225
|
+
if (options.singleLine) return false
|
|
226
|
+
listOf(CommitTextCommand("\n", 1))
|
|
227
|
+
}
|
|
228
|
+
"compositionStart" -> if (start == end) return true else listOf(SetComposingRegionCommand(start, end))
|
|
229
|
+
"replace" -> {
|
|
230
|
+
val text = command.getString("text")
|
|
231
|
+
val place = if (composing) emptyList() else listOf(FinishComposingTextCommand(), SetSelectionCommand(start, end))
|
|
232
|
+
place + if (command.getString("composition") == "update") SetComposingTextCommand(text, 1) else CommitTextCommand(text, 1)
|
|
233
|
+
}
|
|
234
|
+
else -> error("unknown input edit ${command.getString("kind")}")
|
|
235
|
+
}
|
|
236
|
+
request.onEditCommand(commands)
|
|
237
|
+
return true
|
|
238
|
+
}
|
|
97
239
|
}
|
|
98
240
|
|
|
99
241
|
// android's default and sans-serif families use the same device font bytes as
|
|
@@ -150,12 +292,16 @@ internal class PeachComposeMount(
|
|
|
150
292
|
recomposer, platform.density, LayoutDirection.Ltr, null, platform,
|
|
151
293
|
invalidateLayout = { needsMeasure = true; invalidate(true) }, invalidateDraw = { invalidate(false) },
|
|
152
294
|
)
|
|
295
|
+
// the session's state as last reported; a change reports it again
|
|
296
|
+
private var reportedInput: String? = null
|
|
153
297
|
private var surface: Surface? = null
|
|
154
298
|
private var context: DirectContext? = null
|
|
155
299
|
private var target: BackendRenderTarget? = null
|
|
156
300
|
private var pixels: Bitmap? = null
|
|
157
301
|
private var size = IntSize.Zero
|
|
158
302
|
|
|
303
|
+
init { platform.onInputChange = { invalidate(false) } }
|
|
304
|
+
|
|
159
305
|
fun setContent(content: @Composable () -> Unit) {
|
|
160
306
|
try {
|
|
161
307
|
scene.setContent {
|
|
@@ -221,6 +367,11 @@ internal class PeachComposeMount(
|
|
|
221
367
|
for ((name, args) in frameState) events.add(JSONObject().put("name", name).put("args", args()))
|
|
222
368
|
frameState.clear()
|
|
223
369
|
reply.put("events", JSONArray(events))
|
|
370
|
+
val input = platform.input?.state()?.toString()
|
|
371
|
+
if (input != reportedInput) {
|
|
372
|
+
reportedInput = input
|
|
373
|
+
reply.put("input", input?.let { JSONObject(it) } ?: JSONObject.NULL)
|
|
374
|
+
}
|
|
224
375
|
events.clear()
|
|
225
376
|
if (gpu) checkNotNull(surface).flushAndSubmit()
|
|
226
377
|
else {
|
|
@@ -245,6 +396,37 @@ internal class PeachComposeMount(
|
|
|
245
396
|
invalidate(false)
|
|
246
397
|
}
|
|
247
398
|
|
|
399
|
+
private fun session(token: Int) = platform.input?.takeIf { it.token == token }
|
|
400
|
+
|
|
401
|
+
fun inputEdit(token: Int, command: String): Boolean {
|
|
402
|
+
val session = session(token) ?: return false
|
|
403
|
+
return session.edit(JSONObject(command)).also { invalidate(false) }
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// a hardware key, pressed and released, through compose's key input as an
|
|
407
|
+
// android view dispatches it; true when compose consumed the press
|
|
408
|
+
fun inputKey(token: Int, name: String, ctrl: Boolean, shift: Boolean, alt: Boolean): Boolean {
|
|
409
|
+
if (session(token) == null) return false
|
|
410
|
+
val key = hardwareKeys[name] ?: name.singleOrNull()?.lowercaseChar()?.let { letterKeys["$it"] } ?: return false
|
|
411
|
+
val codePoint = name.singleOrNull()?.code ?: 0
|
|
412
|
+
fun send(type: KeyEventType) = scene.sendKeyEvent(KeyEvent(key, type, codePoint,
|
|
413
|
+
isCtrlPressed = ctrl, isAltPressed = alt, isShiftPressed = shift))
|
|
414
|
+
val consumed = send(KeyEventType.KeyDown)
|
|
415
|
+
send(KeyEventType.KeyUp)
|
|
416
|
+
invalidate(false)
|
|
417
|
+
return consumed
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
fun inputState(token: Int): String = session(token)?.state()?.toString() ?: ""
|
|
421
|
+
|
|
422
|
+
// the host gave focus to another input: the view loses focus, as
|
|
423
|
+
// View.clearFocus does, and compose closes the session itself
|
|
424
|
+
fun inputBlur(token: Int) {
|
|
425
|
+
if (session(token) == null) return
|
|
426
|
+
scene.focusManager.releaseFocus()
|
|
427
|
+
invalidate(false)
|
|
428
|
+
}
|
|
429
|
+
|
|
248
430
|
fun semantics(): String {
|
|
249
431
|
val nodes = JSONArray()
|
|
250
432
|
fun collect(node: SemanticsNode) {
|
|
@@ -274,6 +456,19 @@ internal class PeachComposeMount(
|
|
|
274
456
|
}
|
|
275
457
|
}
|
|
276
458
|
|
|
459
|
+
// the keys a hardware keyboard sends, by their DOM names
|
|
460
|
+
private val hardwareKeys = mapOf(
|
|
461
|
+
"ArrowLeft" to Key.DirectionLeft, "ArrowRight" to Key.DirectionRight,
|
|
462
|
+
"ArrowUp" to Key.DirectionUp, "ArrowDown" to Key.DirectionDown,
|
|
463
|
+
"Home" to Key.MoveHome, "End" to Key.MoveEnd, "PageUp" to Key.PageUp, "PageDown" to Key.PageDown,
|
|
464
|
+
"Tab" to Key.Tab, "Delete" to Key.Delete, "Backspace" to Key.Backspace,
|
|
465
|
+
"Enter" to Key.Enter, "Escape" to Key.Escape,
|
|
466
|
+
)
|
|
467
|
+
private val letterKeys = listOf(
|
|
468
|
+
Key.A, Key.B, Key.C, Key.D, Key.E, Key.F, Key.G, Key.H, Key.I, Key.J, Key.K, Key.L, Key.M,
|
|
469
|
+
Key.N, Key.O, Key.P, Key.Q, Key.R, Key.S, Key.T, Key.U, Key.V, Key.W, Key.X, Key.Y, Key.Z,
|
|
470
|
+
).mapIndexed { index, key -> "${'a' + index}" to key }.toMap()
|
|
471
|
+
|
|
277
472
|
internal val mounts = mutableMapOf<Int, PeachComposeMount>()
|
|
278
473
|
private var nextMount = 1
|
|
279
474
|
private var deviceLocale: String? = null
|
|
@@ -330,6 +525,11 @@ fun peachViewMount(source: String, view: String, hash: String, props: String,
|
|
|
330
525
|
}
|
|
331
526
|
@JsExport fun peachViewFrame(id: Int, millis: Double): String = checkNotNull(mounts[id]).frame((millis * 1_000_000).toLong())
|
|
332
527
|
@JsExport fun peachViewPointer(id: Int, phase: String, x: Float, y: Float, millis: Double) = checkNotNull(mounts[id]).pointer(phase, x, y, millis.toLong())
|
|
528
|
+
@JsExport fun peachViewInputEdit(id: Int, token: Int, command: String): Boolean = checkNotNull(mounts[id]).inputEdit(token, command)
|
|
529
|
+
@JsExport fun peachViewInputKey(id: Int, token: Int, key: String, ctrl: Boolean, shift: Boolean, alt: Boolean): Boolean =
|
|
530
|
+
checkNotNull(mounts[id]).inputKey(token, key, ctrl, shift, alt)
|
|
531
|
+
@JsExport fun peachViewInputState(id: Int, token: Int): String = checkNotNull(mounts[id]).inputState(token)
|
|
532
|
+
@JsExport fun peachViewInputBlur(id: Int, token: Int) = checkNotNull(mounts[id]).inputBlur(token)
|
|
333
533
|
@JsExport fun peachViewSemantics(id: Int): String = checkNotNull(mounts[id]).semantics()
|
|
334
534
|
@JsExport fun peachViewDispose(id: Int) { mounts.remove(id)?.close() }
|
|
335
535
|
|
|
@@ -207,6 +207,19 @@ class JSONObject() {
|
|
|
207
207
|
return this
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
// android's coercions: any value reads as its string; a number or a
|
|
211
|
+
// numeric string reads as an int, anything else as the fallback
|
|
212
|
+
fun getString(name: String): String = when (val value = get(name)) {
|
|
213
|
+
is String -> value
|
|
214
|
+
else -> value.toString()
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
fun optInt(name: String, fallback: Int = 0): Int = when (val value = opt(name)) {
|
|
218
|
+
is Number -> value.toInt()
|
|
219
|
+
is String -> value.toDoubleOrNull()?.toInt() ?: fallback
|
|
220
|
+
else -> fallback
|
|
221
|
+
}
|
|
222
|
+
|
|
210
223
|
fun optBoolean(name: String): Boolean = when (val value = opt(name)) {
|
|
211
224
|
is Boolean -> value
|
|
212
225
|
is String -> value.equals("true", ignoreCase = true)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
package dev.contrast.kotlinapp
|
|
2
|
+
|
|
3
|
+
import androidx.compose.animation.AnimatedVisibility
|
|
4
|
+
import androidx.compose.animation.animateColorAsState
|
|
5
|
+
import androidx.compose.animation.core.animateDpAsState
|
|
6
|
+
import androidx.compose.foundation.background
|
|
7
|
+
import androidx.compose.foundation.layout.Arrangement
|
|
8
|
+
import androidx.compose.foundation.layout.Box
|
|
9
|
+
import androidx.compose.foundation.layout.Column
|
|
10
|
+
import androidx.compose.foundation.layout.fillMaxSize
|
|
11
|
+
import androidx.compose.foundation.layout.fillMaxWidth
|
|
12
|
+
import androidx.compose.foundation.layout.height
|
|
13
|
+
import androidx.compose.foundation.layout.padding
|
|
14
|
+
import androidx.compose.foundation.layout.safeDrawingPadding
|
|
15
|
+
import androidx.compose.foundation.layout.width
|
|
16
|
+
import androidx.compose.material3.Button
|
|
17
|
+
import androidx.compose.material3.Text
|
|
18
|
+
import androidx.compose.runtime.Composable
|
|
19
|
+
import androidx.compose.runtime.LaunchedEffect
|
|
20
|
+
import androidx.compose.runtime.withFrameNanos
|
|
21
|
+
import androidx.compose.runtime.getValue
|
|
22
|
+
import androidx.compose.runtime.mutableStateOf
|
|
23
|
+
import androidx.compose.runtime.remember
|
|
24
|
+
import androidx.compose.runtime.setValue
|
|
25
|
+
import androidx.compose.ui.Modifier
|
|
26
|
+
import androidx.compose.ui.graphics.Color
|
|
27
|
+
import androidx.compose.ui.unit.dp
|
|
28
|
+
import kotlin.math.roundToInt
|
|
29
|
+
|
|
30
|
+
// a tap flips one state that AnimatedVisibility, animateDpAsState and
|
|
31
|
+
// animateColorAsState each animate on the frame clock; the label reads the
|
|
32
|
+
// animated width, so it settles only when the animation has finished. took
|
|
33
|
+
// is the frame clock time and frame count from the flip to the width's arrival
|
|
34
|
+
@Composable
|
|
35
|
+
fun Anim() {
|
|
36
|
+
var on by remember { mutableStateOf(false) }
|
|
37
|
+
var flips by remember { mutableStateOf(0) }
|
|
38
|
+
var took by remember { mutableStateOf("none") }
|
|
39
|
+
val target = if (on) 240.dp else 80.dp
|
|
40
|
+
val width by animateDpAsState(target, label = "width")
|
|
41
|
+
LaunchedEffect(flips) {
|
|
42
|
+
if (flips == 0) return@LaunchedEffect
|
|
43
|
+
val start = withFrameNanos { it }
|
|
44
|
+
var count = 0
|
|
45
|
+
var end = start
|
|
46
|
+
while (width != target) end = withFrameNanos { count += 1; it }
|
|
47
|
+
took = "${(end - start) / 1_000_000}ms $count frames"
|
|
48
|
+
}
|
|
49
|
+
val color by animateColorAsState(if (on) Color(0xFF3F51B5) else Color(0xFFE91E63), label = "color")
|
|
50
|
+
Column(
|
|
51
|
+
Modifier.fillMaxSize().background(Color(0xFFE8EEF8)).safeDrawingPadding().padding(16.dp),
|
|
52
|
+
verticalArrangement = Arrangement.spacedBy(12.dp),
|
|
53
|
+
) {
|
|
54
|
+
Text("Anim: $on width ${if (width == target) "${target.value.roundToInt()}" else "moving"}")
|
|
55
|
+
Button(onClick = { on = !on; flips += 1 }) { Text("Animate") }
|
|
56
|
+
Text("Took: $took")
|
|
57
|
+
Box(Modifier.width(width).height(48.dp).background(color))
|
|
58
|
+
AnimatedVisibility(on) {
|
|
59
|
+
Text("Panel", Modifier.fillMaxWidth().background(Color(0xFF009688)).padding(24.dp), color = Color.White)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
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.Row
|
|
7
|
+
import androidx.compose.foundation.layout.fillMaxSize
|
|
8
|
+
import androidx.compose.foundation.layout.padding
|
|
9
|
+
import androidx.compose.foundation.layout.safeDrawingPadding
|
|
10
|
+
import androidx.compose.material3.AssistChip
|
|
11
|
+
import androidx.compose.material3.Badge
|
|
12
|
+
import androidx.compose.material3.BadgedBox
|
|
13
|
+
import androidx.compose.material3.DatePicker
|
|
14
|
+
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
15
|
+
import androidx.compose.material3.ExtendedFloatingActionButton
|
|
16
|
+
import androidx.compose.material3.SegmentedButton
|
|
17
|
+
import androidx.compose.material3.SegmentedButtonDefaults
|
|
18
|
+
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
|
19
|
+
import androidx.compose.material3.SuggestionChip
|
|
20
|
+
import androidx.compose.material3.Text
|
|
21
|
+
import androidx.compose.material3.rememberDatePickerState
|
|
22
|
+
import androidx.compose.runtime.Composable
|
|
23
|
+
import androidx.compose.runtime.getValue
|
|
24
|
+
import androidx.compose.runtime.mutableStateOf
|
|
25
|
+
import androidx.compose.runtime.remember
|
|
26
|
+
import androidx.compose.runtime.setValue
|
|
27
|
+
import androidx.compose.ui.Modifier
|
|
28
|
+
import androidx.compose.ui.graphics.Color
|
|
29
|
+
import androidx.compose.ui.unit.dp
|
|
30
|
+
|
|
31
|
+
// segmented buttons a tap selects, chips, a badge, an extended FAB and a date
|
|
32
|
+
// picker showing a fixed past month, so no today marker depends on the clock
|
|
33
|
+
@OptIn(ExperimentalMaterial3Api::class)
|
|
34
|
+
@Composable
|
|
35
|
+
fun Controls() {
|
|
36
|
+
val options = listOf("Day", "Week", "Month")
|
|
37
|
+
var picked by remember { mutableStateOf(0) }
|
|
38
|
+
// march 2020, utc
|
|
39
|
+
val dates = rememberDatePickerState(initialSelectedDateMillis = 1584230400000L, initialDisplayedMonthMillis = 1583020800000L)
|
|
40
|
+
Column(
|
|
41
|
+
Modifier.fillMaxSize().background(Color(0xFFE8EEF8)).safeDrawingPadding().padding(16.dp),
|
|
42
|
+
verticalArrangement = Arrangement.spacedBy(12.dp),
|
|
43
|
+
) {
|
|
44
|
+
Text("Range: ${options[picked]}")
|
|
45
|
+
SingleChoiceSegmentedButtonRow {
|
|
46
|
+
options.forEachIndexed { index, label ->
|
|
47
|
+
SegmentedButton(
|
|
48
|
+
selected = picked == index,
|
|
49
|
+
onClick = { picked = index },
|
|
50
|
+
shape = SegmentedButtonDefaults.itemShape(index, options.size),
|
|
51
|
+
) { Text(label) }
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
55
|
+
AssistChip(onClick = {}, label = { Text("Assist") })
|
|
56
|
+
SuggestionChip(onClick = {}, label = { Text("Suggest") })
|
|
57
|
+
BadgedBox(badge = { Badge { Text("8") } }, Modifier.padding(top = 12.dp, start = 8.dp)) { Text("Inbox") }
|
|
58
|
+
}
|
|
59
|
+
ExtendedFloatingActionButton(onClick = {}) { Text("Compose") }
|
|
60
|
+
DatePicker(dates, showModeToggle = false)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
package dev.contrast.kotlinapp
|
|
2
|
+
|
|
3
|
+
import androidx.compose.foundation.background
|
|
4
|
+
import androidx.compose.foundation.gestures.detectDragGestures
|
|
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.fillMaxSize
|
|
9
|
+
import androidx.compose.foundation.layout.fillMaxWidth
|
|
10
|
+
import androidx.compose.foundation.layout.height
|
|
11
|
+
import androidx.compose.foundation.layout.offset
|
|
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.shape.CircleShape
|
|
16
|
+
import androidx.compose.material3.Slider
|
|
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.mutableStateOf
|
|
22
|
+
import androidx.compose.runtime.remember
|
|
23
|
+
import androidx.compose.runtime.setValue
|
|
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.pointer.pointerInput
|
|
28
|
+
import androidx.compose.ui.platform.LocalDensity
|
|
29
|
+
import androidx.compose.ui.platform.testTag
|
|
30
|
+
import androidx.compose.ui.unit.IntOffset
|
|
31
|
+
import androidx.compose.ui.unit.dp
|
|
32
|
+
import kotlin.math.roundToInt
|
|
33
|
+
|
|
34
|
+
// a puck detectDragGestures moves, reporting the drag it received after the
|
|
35
|
+
// touch slop in dp, and a slider a drag sets
|
|
36
|
+
@Composable
|
|
37
|
+
fun Drag() {
|
|
38
|
+
var puck by remember { mutableStateOf(Offset.Zero) }
|
|
39
|
+
var drags by remember { mutableStateOf(0) }
|
|
40
|
+
var slider by remember { mutableFloatStateOf(0f) }
|
|
41
|
+
val density = LocalDensity.current.density
|
|
42
|
+
Column(
|
|
43
|
+
Modifier.fillMaxSize().background(Color(0xFFE8EEF8)).safeDrawingPadding().padding(16.dp),
|
|
44
|
+
verticalArrangement = Arrangement.spacedBy(12.dp),
|
|
45
|
+
) {
|
|
46
|
+
Text("Drag: ${(puck.x / density).roundToInt()},${(puck.y / density).roundToInt()} ends $drags slider ${(slider * 100).roundToInt()}")
|
|
47
|
+
Box(
|
|
48
|
+
Modifier.fillMaxWidth().height(300.dp).background(Color(0xFFCFD8DC)).testTag("pad").pointerInput(Unit) {
|
|
49
|
+
detectDragGestures(onDragEnd = { drags += 1 }) { change, amount -> change.consume(); puck += amount }
|
|
50
|
+
},
|
|
51
|
+
) {
|
|
52
|
+
Box(Modifier.offset { IntOffset(puck.x.roundToInt(), puck.y.roundToInt()) }.size(48.dp).background(Color(0xFF3F51B5), CircleShape))
|
|
53
|
+
}
|
|
54
|
+
Slider(slider, { slider = it }, Modifier.testTag("slider"))
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
package dev.contrast.kotlinapp
|
|
2
|
+
|
|
3
|
+
import androidx.compose.foundation.Canvas
|
|
4
|
+
import androidx.compose.foundation.background
|
|
5
|
+
import androidx.compose.foundation.gestures.detectTapGestures
|
|
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.Row
|
|
10
|
+
import androidx.compose.foundation.layout.fillMaxSize
|
|
11
|
+
import androidx.compose.foundation.layout.fillMaxWidth
|
|
12
|
+
import androidx.compose.foundation.layout.height
|
|
13
|
+
import androidx.compose.foundation.layout.padding
|
|
14
|
+
import androidx.compose.foundation.layout.safeDrawingPadding
|
|
15
|
+
import androidx.compose.foundation.layout.size
|
|
16
|
+
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
17
|
+
import androidx.compose.material3.Text
|
|
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.draw.clip
|
|
25
|
+
import androidx.compose.ui.draw.rotate
|
|
26
|
+
import androidx.compose.ui.geometry.CornerRadius
|
|
27
|
+
import androidx.compose.ui.geometry.Offset
|
|
28
|
+
import androidx.compose.ui.geometry.Size
|
|
29
|
+
import androidx.compose.ui.graphics.Brush
|
|
30
|
+
import androidx.compose.ui.graphics.Color
|
|
31
|
+
import androidx.compose.ui.graphics.Path
|
|
32
|
+
import androidx.compose.ui.graphics.StrokeCap
|
|
33
|
+
import androidx.compose.ui.graphics.drawscope.Stroke
|
|
34
|
+
import androidx.compose.ui.graphics.graphicsLayer
|
|
35
|
+
import androidx.compose.ui.input.pointer.pointerInput
|
|
36
|
+
import androidx.compose.ui.unit.dp
|
|
37
|
+
import kotlin.math.roundToInt
|
|
38
|
+
|
|
39
|
+
// a canvas of paths, arcs, gradients and strokes, transformed layers, and a
|
|
40
|
+
// pointer input pad that reports where a tap lands in its own dp
|
|
41
|
+
@Composable
|
|
42
|
+
fun Draw() {
|
|
43
|
+
var tap by remember { mutableStateOf("none") }
|
|
44
|
+
Column(
|
|
45
|
+
Modifier.fillMaxSize().background(Color(0xFFE8EEF8)).safeDrawingPadding().padding(16.dp),
|
|
46
|
+
verticalArrangement = Arrangement.spacedBy(12.dp),
|
|
47
|
+
) {
|
|
48
|
+
Text("Tap: $tap")
|
|
49
|
+
Canvas(Modifier.fillMaxWidth().height(200.dp)) {
|
|
50
|
+
drawRect(Brush.linearGradient(listOf(Color(0xFF3F51B5), Color(0xFFE91E63)), Offset.Zero, Offset(size.width, size.height)))
|
|
51
|
+
drawCircle(Brush.radialGradient(listOf(Color.White, Color(0x00FFFFFF)), Offset(size.width * 0.25f, size.height * 0.5f), 120f), 120f, Offset(size.width * 0.25f, size.height * 0.5f))
|
|
52
|
+
drawArc(Color(0xFFFFC107), -90f, 250f, false, Offset(size.width * 0.55f, 40f), Size(240f, 240f), style = Stroke(24f, cap = StrokeCap.Round))
|
|
53
|
+
val path = Path().apply {
|
|
54
|
+
moveTo(size.width * 0.8f, 40f)
|
|
55
|
+
cubicTo(size.width, 80f, size.width * 0.7f, size.height - 80f, size.width * 0.95f, size.height - 30f)
|
|
56
|
+
lineTo(size.width * 0.75f, size.height - 30f)
|
|
57
|
+
close()
|
|
58
|
+
}
|
|
59
|
+
drawPath(path, Color(0xCC009688))
|
|
60
|
+
drawRoundRect(Color.White, Offset(30f, size.height - 90f), Size(200f, 60f), CornerRadius(30f), style = Stroke(6f))
|
|
61
|
+
drawLine(Color.Black, Offset(0f, size.height), Offset(size.width, 0f), 3f)
|
|
62
|
+
}
|
|
63
|
+
Row(horizontalArrangement = Arrangement.spacedBy(24.dp)) {
|
|
64
|
+
Box(Modifier.padding(12.dp).size(72.dp).rotate(30f).background(Color(0xFF673AB7)))
|
|
65
|
+
Box(Modifier.size(96.dp).graphicsLayer { alpha = 0.5f; scaleX = 0.8f; scaleY = 1.2f }.background(Color(0xFFFF5722)))
|
|
66
|
+
Box(Modifier.size(96.dp).graphicsLayer { rotationX = 40f; cameraDistance = 8 * density }.clip(RoundedCornerShape(16.dp)).background(Color(0xFF4CAF50)))
|
|
67
|
+
}
|
|
68
|
+
Box(
|
|
69
|
+
Modifier.fillMaxWidth().height(160.dp).background(Color(0xFFCFD8DC)).pointerInput(Unit) {
|
|
70
|
+
detectTapGestures { tap = "${(it.x / density).roundToInt()},${(it.y / density).roundToInt()}" }
|
|
71
|
+
},
|
|
72
|
+
) { Text("Pad", Modifier.padding(8.dp)) }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -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.fillMaxWidth
|
|
8
|
+
import androidx.compose.foundation.layout.padding
|
|
9
|
+
import androidx.compose.foundation.layout.safeDrawingPadding
|
|
10
|
+
import androidx.compose.foundation.text.KeyboardActions
|
|
11
|
+
import androidx.compose.foundation.text.KeyboardOptions
|
|
12
|
+
import androidx.compose.material3.OutlinedTextField
|
|
13
|
+
import androidx.compose.material3.Text
|
|
14
|
+
import androidx.compose.runtime.Composable
|
|
15
|
+
import androidx.compose.runtime.getValue
|
|
16
|
+
import androidx.compose.runtime.mutableStateOf
|
|
17
|
+
import androidx.compose.runtime.remember
|
|
18
|
+
import androidx.compose.runtime.setValue
|
|
19
|
+
import androidx.compose.ui.Modifier
|
|
20
|
+
import androidx.compose.ui.focus.onFocusChanged
|
|
21
|
+
import androidx.compose.ui.graphics.Color
|
|
22
|
+
import androidx.compose.ui.text.input.ImeAction
|
|
23
|
+
import androidx.compose.ui.unit.dp
|
|
24
|
+
|
|
25
|
+
// a single line field a tap focuses, typing fills and Done finishes, keeping
|
|
26
|
+
// its focus as android does
|
|
27
|
+
@Composable
|
|
28
|
+
fun Form() {
|
|
29
|
+
var name by remember { mutableStateOf("") }
|
|
30
|
+
var focused by remember { mutableStateOf(false) }
|
|
31
|
+
var done 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("Name: ${name.ifEmpty { "empty" }} focus $focused done $done")
|
|
37
|
+
OutlinedTextField(
|
|
38
|
+
value = name,
|
|
39
|
+
onValueChange = { name = it },
|
|
40
|
+
label = { Text("Name") },
|
|
41
|
+
singleLine = true,
|
|
42
|
+
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
|
43
|
+
keyboardActions = KeyboardActions(onDone = { done += 1; defaultKeyboardAction(ImeAction.Done) }),
|
|
44
|
+
modifier = Modifier.fillMaxWidth().onFocusChanged { focused = it.isFocused },
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
}
|