AGPL-3.0-only, a source link in the app, and everything on dev since #5 #6

Merged
bvandeusen merged 214 commits from dev into main 2026-10-21 21:40:00 -04:00
2 changed files with 44 additions and 2 deletions
Showing only changes of commit 854cb4b403 - Show all commits
@@ -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
}