Checklists in the body, colour from tags, and commit-derived CalVer #4
@@ -1,8 +1,12 @@
|
|||||||
package com.fabledsword.thoughtsync.ui
|
package com.fabledsword.thoughtsync.ui
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
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.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
@@ -11,6 +15,7 @@ import androidx.compose.material3.Checkbox
|
|||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -19,8 +24,10 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
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.res.stringResource
|
||||||
import androidx.compose.ui.text.TextRange
|
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.ImeAction
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
@@ -190,13 +197,12 @@ private fun ProseBlock(
|
|||||||
requester: FocusRequester,
|
requester: FocusRequester,
|
||||||
onChange: (EditorBlock) -> Unit,
|
onChange: (EditorBlock) -> Unit,
|
||||||
) {
|
) {
|
||||||
PlainTextField(
|
BlockField(
|
||||||
value = block.value,
|
value = block.value,
|
||||||
onValueChange = { onChange(block.copy(value = it)) },
|
onValueChange = { onChange(block.copy(value = it)) },
|
||||||
modifier = Modifier.focusRequester(requester),
|
modifier = Modifier.focusRequester(requester),
|
||||||
hint = R.string.editor_body_hint,
|
|
||||||
enabled = !readOnly,
|
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)) },
|
onCheckedChange = { onChange(block.copy(checked = it)) },
|
||||||
enabled = !readOnly,
|
enabled = !readOnly,
|
||||||
)
|
)
|
||||||
PlainTextField(
|
BlockField(
|
||||||
value = block.value,
|
value = block.value,
|
||||||
onValueChange = { onChange(block.copy(value = it)) },
|
onValueChange = { onChange(block.copy(value = it)) },
|
||||||
modifier = Modifier.weight(1f).focusRequester(requester),
|
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.
|
* 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.graphics.Color
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A text field with no box around it.
|
* A text field with no box around it.
|
||||||
*
|
*
|
||||||
* Every writing surface in the app — the capture sheet, the editor's title and
|
* The search box, the label picker, the sync-pairing form: fields that sit on a
|
||||||
* body, each checklist row — sits on a surface that already has its own edges and
|
* surface which already has its own edges and its own colour, where Material's filled
|
||||||
* its own colour. Material's filled field would draw a second, differently
|
* field would draw a second, differently coloured box inside the first. Stripping the
|
||||||
* coloured box inside the first, which makes writing a note look like filling in a
|
* container and the indicator at each site independently is how they drift apart, so
|
||||||
* form. Stripping the container and the indicator in four places independently is
|
* it happens once, here.
|
||||||
* 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
|
* 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
|
* 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
|
* One definition of "no box". Two copies of this is exactly the drift this file
|
||||||
* the SELECTION as well as the text — the editor, which opens an existing note with
|
* exists to prevent.
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
Reference in New Issue
Block a user