android: put the way out of a note back within reach
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m5s
Android / Kotlin + Rust (APK) (push) Canceled after 4m27s
Desktop (Tauri) / Tauri desktop (Linux) (push) Canceled after 4m27s
Desktop (Tauri) / Update manifest (push) Canceled after 0s

Moving the toolbar to the top took the back arrow with it, and left the only
exit from a full-screen editor in the top-left corner — the furthest point on
the display from a right-handed thumb, reached over the whole note to get to.
Reported on the first device pass, and correctly.

So the footer carries a Done as well as the timestamp. With the keyboard up it
sits directly above the thumb, which is where a hand already is for every other
part of writing a note.

The top-left arrow stays. Two affordances for one action is usually clutter,
but this is the case that earns it: the arrow is what habit, the system back
gesture and TalkBack all expect of a full-screen surface, and removing it would
strand the reflex to strike a duplicate costing one icon slot.

A word rather than a checkmark, on the same argument the overflow menu makes: a
tick in a notes app is a checklist item to anyone who has used one, and "Done"
cannot be misread, including aloud.

EditorSavedLine is now EditorFooter, since it is no longer only a line.
This commit is contained in:
2026-08-23 23:46:03 -04:00
parent bca9e16bd0
commit 8257e1035c
3 changed files with 52 additions and 21 deletions
@@ -157,48 +157,70 @@ fun EditorTopBar(
} }
/** /**
* When the note was last written, in the bottom corner. * The footer: when the note was last written, and the way out.
* *
* There is no save button. That is right — a note is saved continuously, so a * **Where the note stands.** There is no save button, and there should not be — a
* button offering to do what already happened is a lie with a tap attached — but it * note is saved continuously, so a button offering to do what already happened is a
* leaves nothing on screen saying the work is safe, and "closing this saves it" is * lie with a tap attached. But that left nothing on screen saying the work is safe,
* not a thing anyone should have to be told twice. * and "closing this keeps it" is not a thing anyone should have to be told twice. So
* the state says it, as a fact rather than an instruction: Not saved yet → Saving… →
* Edited just now is the whole lifecycle, and someone who watches it once never has
* to wonder again.
* *
* So the state says it instead, and it says it as a fact rather than an * **The way out.** Down here because of where hands are. Moving the toolbar to the
* instruction. Not saved yet → Saving… → Edited just now is the whole lifecycle, * top took the back arrow with it, which left the only exit from a full-screen
* visible in the corner, and someone who watches it once never has to wonder again. * editor in the top-left corner — the furthest point on the display from a
* right-handed thumb, and reached over the whole note to get to. The operator hit
* that on the first device pass and was right to. So the exit lives in the bottom
* corner, which with the keyboard up sits directly above it.
*
* The top-left arrow stays as well. Two affordances for one action is usually
* clutter, but this is the case that earns it: the arrow is what habit, the system
* back gesture and TalkBack all expect of a full-screen surface, and removing it
* would strand the reflex to strike a duplicate that costs one icon slot.
*
* A WORD rather than a checkmark, like the overflow menu below and for the same
* reason. A tick in a notes app is a checklist item to anyone who has used one, and
* "Done" cannot be misread — including aloud.
* *
* [DateUtils] rather than a hand-rolled formatter: it is localised, it already * [DateUtils] rather than a hand-rolled formatter: it is localised, it already
* knows the difference between minutes, hours and yesterday, and getting plurals * knows the difference between minutes, hours and yesterday, and getting plurals
* right in every language is not this app's problem to solve twice. * right in every language is not this app's problem to solve twice.
*/ */
@Composable @Composable
fun EditorSavedLine( fun EditorFooter(
updatedAt: String?, updatedAt: String?,
saving: Boolean, saving: Boolean,
onClose: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Row( Row(
modifier = modifier =
modifier modifier
.fillMaxWidth() .fillMaxWidth()
// Rides above the keyboard, like the bar it replaced. The content // Rides above the keyboard, like the bar that used to be here. The
// Column deliberately does not also inset for the IME: Scaffold // content Column deliberately does not also inset for the IME:
// measures this at its lifted height and passes the inset down. // Scaffold measures this row at its lifted height and passes the
// inset down.
.imePadding() .imePadding()
.navigationBarsPadding() .navigationBarsPadding()
.padding(horizontal = 16.dp, vertical = 8.dp), // Asymmetric: the button brings its own padding, the text does not.
.padding(start = 16.dp, end = 4.dp),
horizontalArrangement = Arrangement.End, horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically,
) { ) {
Text( Text(
text = savedLabel(updatedAt, saving), text = savedLabel(updatedAt, saving),
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
TextButton(onClick = onClose) {
Text(stringResource(R.string.editor_done))
}
} }
} }
/** The three things [EditorSavedLine] can be saying, in the order it says them. */ /** The three things the footer can be saying, in the order it says them. */
@Composable @Composable
private fun savedLabel( private fun savedLabel(
updatedAt: String?, updatedAt: String?,
@@ -53,8 +53,9 @@ import kotlinx.coroutines.delay
* opening a note reads as the same object growing to fill the display. * opening a note reads as the same object growing to fill the display.
* *
* No save button, deliberately. Writes are continuous, so a button offering to do * No save button, deliberately. Writes are continuous, so a button offering to do
* what already happened would be a lie with a tap attached; [EditorSavedLine] in the * what already happened would be a lie with a tap attached; [EditorFooter] in the
* bottom corner says the same thing as a fact instead. * bottom corner says the same thing as a fact instead, beside the Done that
* leaves.
*/ */
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -182,16 +183,23 @@ fun NoteEditorScreen(
onAction = onAction, onAction = onAction,
) )
}, },
// Where the action bar used to be. A note is saved as it is written, // Where the action bar used to be, carrying the two things that
// so what belongs at the bottom of the screen is not a control but // belong within reach of a thumb: whether the note is safe, and the
// the answer to "did that land" — see [EditorSavedLine]. // way out. See [EditorFooter] for why the exit is down here and not
bottomBar = { EditorSavedLine(updatedAt = note.updatedAt, saving = saving) }, // only in the top-left corner.
bottomBar = {
EditorFooter(
updatedAt = note.updatedAt,
saving = saving,
onClose = leave,
)
},
) { padding -> ) { padding ->
Column( Column(
modifier = modifier =
Modifier Modifier
.fillMaxSize() .fillMaxSize()
// No imePadding here: EditorSavedLine carries it, so // No imePadding here: EditorFooter carries it, so
// Scaffold measures that row at its keyboard-lifted // Scaffold measures that row at its keyboard-lifted
// height and the inset already reaches this Column // height and the inset already reaches this Column
// through `padding`. Adding it again would inset for the // through `padding`. Adding it again would inset for the
@@ -45,6 +45,7 @@
<string name="editor_unsaved">Not saved yet</string> <string name="editor_unsaved">Not saved yet</string>
<string name="editor_edited">Edited %1$s</string> <string name="editor_edited">Edited %1$s</string>
<string name="editor_just_now">just now</string> <string name="editor_just_now">just now</string>
<string name="editor_done">Done</string>
<string name="editor_pin">Pin</string> <string name="editor_pin">Pin</string>
<string name="editor_unpin">Unpin</string> <string name="editor_unpin">Unpin</string>
<string name="editor_labels">Labels…</string> <string name="editor_labels">Labels…</string>