Checklists in the body, colour from tags, and commit-derived CalVer #4
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user