From 56264a9220ad4b9a3043f25bb7d08ce757c1281d Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 26 Aug 2026 07:03:15 -0400 Subject: [PATCH] android: the checklist rows were carrying a form field's padding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported from the device with a screenshot: six items took up most of a phone screen. The rows measured ~57dp apart, which is Material's TextField content padding almost exactly — 16dp above the text, 16dp below, around a 24dp line. That padding is right for a form field, where it is the difference between a comfortable target and a fiddly one. On a checklist it IS the row height, so every item was paying for a hit area the checkbox beside it already provides. The editor's blocks drop to BasicTextField. Nothing about the "no box" treatment is lost — PlainTextField exists to strip a container and an indicator, and BasicTextField never had either, so there is nothing here to drift back into existence. What it does not supply and BlockField now does: the text colour, which defaults to Color.Unspecified and draws BLACK (the same default that made the toolbar invisible in dark mode), the cursor brush for the same reason, and the placeholder, which becomes a plain Text behind the field. PlainTextField keeps serving the search box, the label picker and the sync-pairing form — fields where Material's padding is what you want. Its TextFieldValue overload went with the change: the editor was its only caller, and every remaining one passes a String. Rows are now bound by the 48dp checkbox rather than by the field. If that is still looser than it should be, the next lever is the touch targets themselves, which trades against how easy the box is to hit — worth looking at on a device before spending it. --- .../fabledsword/thoughtsync/ui/EditorBlock.kt | 69 +++++++++++++++++-- .../fabledsword/thoughtsync/ui/PlainField.kt | 60 ++++------------ 2 files changed, 77 insertions(+), 52 deletions(-) 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