diff --git a/android/app/src/main/java/com/fabledsword/thoughtsync/ui/BlockBody.kt b/android/app/src/main/java/com/fabledsword/thoughtsync/ui/BlockBody.kt index c609eeb..c1b2ed7 100644 --- a/android/app/src/main/java/com/fabledsword/thoughtsync/ui/BlockBody.kt +++ b/android/app/src/main/java/com/fabledsword/thoughtsync/ui/BlockBody.kt @@ -14,9 +14,11 @@ import androidx.compose.material.icons.filled.Close import androidx.compose.material3.Checkbox import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalMinimumInteractiveComponentSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -136,38 +138,54 @@ private fun TaskBlock( onEnter: () -> Unit, onDelete: () -> Unit, ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Checkbox( - checked = block.checked == true, - onCheckedChange = { onChange(block.copy(checked = it)) }, - enabled = !readOnly, - ) - BlockField( - value = block.value, - onValueChange = { onChange(block.copy(value = it)) }, - modifier = Modifier.weight(1f).focusRequester(requester), - enabled = !readOnly, - singleLine = true, - textStyle = - MaterialTheme.typography.bodyLarge.copy( - // Struck through when done, matching the card and the web. - textDecoration = - if (block.checked == true) TextDecoration.LineThrough else null, - ), - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), - keyboardActions = KeyboardActions(onNext = { onEnter() }), - ) - if (!readOnly) { - IconButton(onClick = onDelete) { - Icon( - Icons.Filled.Close, - contentDescription = stringResource(R.string.editor_remove_item), - ) + // Material sizes every interactive component to a 48dp touch target, and on a + // checklist that IS the row height — which is why six items filled a phone screen + // even after the field's own padding came off. + CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides ROW_TOUCH) { + Row(verticalAlignment = Alignment.CenterVertically) { + Checkbox( + checked = block.checked == true, + onCheckedChange = { onChange(block.copy(checked = it)) }, + enabled = !readOnly, + ) + BlockField( + value = block.value, + onValueChange = { onChange(block.copy(value = it)) }, + modifier = Modifier.weight(1f).focusRequester(requester), + enabled = !readOnly, + singleLine = true, + textStyle = + MaterialTheme.typography.bodyLarge.copy( + // Struck through when done, matching the card and the web. + textDecoration = + if (block.checked == true) TextDecoration.LineThrough else null, + ), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + keyboardActions = KeyboardActions(onNext = { onEnter() }), + ) + if (!readOnly) { + IconButton(onClick = onDelete) { + Icon( + Icons.Filled.Close, + contentDescription = stringResource(R.string.editor_remove_item), + ) + } } } } } +/** + * The touch target for a checklist row's controls. + * + * Material's floor is 48dp and this is deliberately under it. That floor is sized for + * a control somebody has to find; a checklist box sits in a predictable column with an + * identical box directly above and below, and the cost of a near miss is ticking the + * neighbouring item — visible, and undone by tapping again. Trading twelve of those + * dp for a list that fits on a screen is what was asked for, twice. + */ +private val ROW_TOUCH = 36.dp + /** * The field a block is typed into. *