Android: a hardware keyboard's arrow keys move the cursor (#6199)
CI & Build / Python lint (push) Successful in 2s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
Android / Build, or is the channel already serving this? (push) Successful in 3s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Web typecheck and unit tests (push) Successful in 12s
CI & Build / Python tests (push) Successful in 14s
Android / Core and FFI clippy and tests (push) Successful in 51s
CI & Build / integration (push) Successful in 1m50s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 2m8s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m59s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m38s
Desktop (Tauri) / Update manifest (push) Successful in 3s
Android / Kotlin + Rust (APK) (push) Successful in 8m22s
Android / Build the server image (push) Successful in 1s
CI & Build / Python lint (push) Successful in 2s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
Android / Build, or is the channel already serving this? (push) Successful in 3s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Web typecheck and unit tests (push) Successful in 12s
CI & Build / Python tests (push) Successful in 14s
Android / Core and FFI clippy and tests (push) Successful in 51s
CI & Build / integration (push) Successful in 1m50s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 2m8s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m59s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m38s
Desktop (Tauri) / Update manifest (push) Successful in 3s
Android / Kotlin + Rust (APK) (push) Successful in 8m22s
Android / Build the server image (push) Successful in 1s
Scribe #6199 "Android: a hardware keyboard's arrow keys move focus instead of the cursor". Compose's text field sends the arrow keys of any device reporting D-pad keys to focus search before the field sees them, and a physical keyboard reports exactly that, so left and right jumped to the neighbouring control. Cancelling focus search in those directions hands the key back to the field. Up and down move between a field's lines, and from its first or last line on to the field above or below; single-line checklist rows still step row to row. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,7 +20,10 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
@@ -28,6 +31,7 @@ import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextLayoutResult
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
@@ -241,6 +245,15 @@ private fun BlockField(
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
) {
|
||||
val style = textStyle.copy(color = MaterialTheme.colorScheme.onSurface)
|
||||
// Kept for the arrow keys, which ask which line the caret is on. Read only when
|
||||
// one is pressed, never while composing.
|
||||
var layout by remember { mutableStateOf<TextLayoutResult?>(null) }
|
||||
val line = { offset: Int -> layout?.getLineForOffset(offset) ?: 0 }
|
||||
val arrows =
|
||||
Modifier.arrowKeysMoveCaret(
|
||||
atTop = { line(value.selection.end) == 0 },
|
||||
atBottom = { line(value.selection.end) == (layout?.lineCount ?: 1) - 1 },
|
||||
)
|
||||
Box(modifier = modifier) {
|
||||
if (hint != null && value.text.isEmpty()) {
|
||||
Text(
|
||||
@@ -252,12 +265,13 @@ private fun BlockField(
|
||||
BasicTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = Modifier.fillMaxWidth().then(arrows),
|
||||
enabled = enabled,
|
||||
singleLine = singleLine,
|
||||
textStyle = style,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
onTextLayout = { layout = it },
|
||||
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ import androidx.compose.material3.TextFieldColors
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
@@ -54,7 +56,7 @@ fun PlainTextField(
|
||||
TextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
modifier = modifier.fillMaxWidth().arrowKeysMoveCaret(atTop = { singleLine }, atBottom = { singleLine }),
|
||||
enabled = enabled,
|
||||
placeholder = hint?.let { { Text(stringResource(it)) } },
|
||||
singleLine = singleLine,
|
||||
@@ -86,3 +88,29 @@ private fun plainFieldColors(): TextFieldColors =
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
disabledIndicatorColor = Color.Transparent,
|
||||
)
|
||||
|
||||
/**
|
||||
* Arrow keys on a hardware keyboard move the caret, not the focus.
|
||||
*
|
||||
* Compose's text field gives the arrow keys of any device that reports D-pad keys
|
||||
* to focus search before the field sees them, and a physical keyboard with arrow
|
||||
* keys reports exactly that — so without this, ← and → jump to whatever sits beside
|
||||
* the field instead of moving through the text. Cancelling the search in a
|
||||
* direction hands the key back: the search reports that nothing moved, and the
|
||||
* field takes the key as editing.
|
||||
*
|
||||
* ← and → never leave the field. ↑ and ↓ move between its lines, and from the first
|
||||
* or last line — [atTop], [atBottom] — on to the field above or below, the way a
|
||||
* caret leaves one paragraph for the next. Both are asked at the moment of the key
|
||||
* press, so they can read where the caret is now.
|
||||
*/
|
||||
fun Modifier.arrowKeysMoveCaret(
|
||||
atTop: () -> Boolean,
|
||||
atBottom: () -> Boolean,
|
||||
): Modifier =
|
||||
focusProperties {
|
||||
left = FocusRequester.Cancel
|
||||
right = FocusRequester.Cancel
|
||||
if (!atTop()) up = FocusRequester.Cancel
|
||||
if (!atBottom()) down = FocusRequester.Cancel
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user