diff --git a/android/app/src/main/java/com/fabledsword/thoughtsync/ui/EditorBlock.kt b/android/app/src/main/java/com/fabledsword/thoughtsync/ui/EditorBlock.kt index 052e13b..d48dc02 100644 --- a/android/app/src/main/java/com/fabledsword/thoughtsync/ui/EditorBlock.kt +++ b/android/app/src/main/java/com/fabledsword/thoughtsync/ui/EditorBlock.kt @@ -1,8 +1,12 @@ package com.fabledsword.thoughtsync.ui +import androidx.annotation.StringRes import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons @@ -11,6 +15,7 @@ import androidx.compose.material3.Checkbox import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember @@ -19,8 +24,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextDecoration @@ -190,13 +197,12 @@ private fun ProseBlock( requester: FocusRequester, onChange: (EditorBlock) -> Unit, ) { - PlainTextField( + BlockField( value = block.value, onValueChange = { onChange(block.copy(value = it)) }, modifier = Modifier.focusRequester(requester), - hint = R.string.editor_body_hint, enabled = !readOnly, - textStyle = MaterialTheme.typography.bodyLarge, + hint = R.string.editor_body_hint, ) } @@ -222,7 +228,7 @@ private fun TaskBlock( onCheckedChange = { onChange(block.copy(checked = it)) }, enabled = !readOnly, ) - PlainTextField( + BlockField( value = block.value, onValueChange = { onChange(block.copy(value = it)) }, modifier = Modifier.weight(1f).focusRequester(requester), @@ -248,6 +254,61 @@ private fun TaskBlock( } } +/** + * The field a block is typed into. + * + * `BasicTextField`, not the Material one [PlainTextField] wraps, and the reason is + * density. Material's TextField puts 16dp above and below its text — padding that + * makes a FORM field comfortable to hit, and that on a checklist IS the row height. It + * made six items twice as tall as the six items, which is what the operator saw. + * + * Nothing is lost by dropping down a layer. `PlainTextField` exists to strip a + * container and an indicator; `BasicTextField` never had either, so there is no box + * here to drift back into existence. What it does not supply and this must: + * + * - the text COLOUR. It defaults to `Color.Unspecified`, which draws BLACK — the same + * default that made the editor's toolbar invisible in dark mode. Set, not inherited. + * - the cursor brush, which would otherwise be black for the same reason. + * - the placeholder, which is a plain Text behind the field rather than a slot. + * + * `enabled = false` deliberately does not grey the text out: a trashed note renders + * read-only through this and its words are meant to be READ. + */ +@Composable +private fun BlockField( + value: TextFieldValue, + onValueChange: (TextFieldValue) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + singleLine: Boolean = false, + @StringRes hint: Int? = null, + textStyle: TextStyle = MaterialTheme.typography.bodyLarge, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, +) { + val style = textStyle.copy(color = MaterialTheme.colorScheme.onSurface) + Box(modifier = modifier) { + if (hint != null && value.text.isEmpty()) { + Text( + text = stringResource(hint), + style = style, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + BasicTextField( + value = value, + onValueChange = onValueChange, + modifier = Modifier.fillMaxWidth(), + enabled = enabled, + singleLine = singleLine, + textStyle = style, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), + ) + } +} + /** * What the return key does on a checklist item. * diff --git a/android/app/src/main/java/com/fabledsword/thoughtsync/ui/PlainField.kt b/android/app/src/main/java/com/fabledsword/thoughtsync/ui/PlainField.kt index ec7c19f..b96e9ab 100644 --- a/android/app/src/main/java/com/fabledsword/thoughtsync/ui/PlainField.kt +++ b/android/app/src/main/java/com/fabledsword/thoughtsync/ui/PlainField.kt @@ -16,18 +16,21 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation /** * A text field with no box around it. * - * Every writing surface in the app — the capture sheet, the editor's title and - * body, each checklist row — sits on a surface that already has its own edges and - * its own colour. Material's filled field would draw a second, differently - * coloured box inside the first, which makes writing a note look like filling in a - * form. Stripping the container and the indicator in four places independently is - * how they drift apart, so it happens once, here. + * The search box, the label picker, the sync-pairing form: fields that sit on a + * surface which already has its own edges and its own colour, where Material's filled + * field would draw a second, differently coloured box inside the first. Stripping the + * container and the indicator at each site independently is how they drift apart, so + * it happens once, here. + * + * The note EDITOR no longer comes through this. It dropped to `BasicTextField` + * (see `EditorBlock.kt`) for density: Material's field puts 16dp above and below its + * text, which is right for a form and is the whole row height on a checklist. Nothing + * about "no box" was lost there — BasicTextField never had one. * * The disabled colours are stripped too: a trashed note is shown through this * field read-only, and Material's disabled treatment would grey out text the user @@ -65,47 +68,8 @@ fun PlainTextField( } /** - * The same field over a [TextFieldValue], for the one caller that needs to control - * the SELECTION as well as the text — the editor, which opens an existing note with - * the cursor at the end so continuing to write costs no extra taps. - * - * A `String` field cannot express that: Compose starts its internal selection at - * offset zero, so focusing one drops the cursor before the first character. - */ -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun PlainTextField( - value: TextFieldValue, - onValueChange: (TextFieldValue) -> Unit, - modifier: Modifier = Modifier, - @StringRes hint: Int? = null, - enabled: Boolean = true, - singleLine: Boolean = false, - minLines: Int = 1, - textStyle: TextStyle = LocalTextStyle.current, - keyboardOptions: KeyboardOptions = KeyboardOptions.Default, - keyboardActions: KeyboardActions = KeyboardActions.Default, - visualTransformation: VisualTransformation = VisualTransformation.None, -) { - TextField( - value = value, - onValueChange = onValueChange, - modifier = modifier.fillMaxWidth(), - enabled = enabled, - placeholder = hint?.let { { Text(stringResource(it)) } }, - singleLine = singleLine, - minLines = minLines, - textStyle = textStyle, - keyboardOptions = keyboardOptions, - keyboardActions = keyboardActions, - visualTransformation = visualTransformation, - colors = plainFieldColors(), - ) -} - -/** - * One definition of "no box", shared by both overloads. Two copies of this is - * exactly the drift this file exists to prevent. + * One definition of "no box". Two copies of this is exactly the drift this file + * exists to prevent. */ @OptIn(ExperimentalMaterial3Api::class) @Composable