From 73923dbbc30a6ae4c4fda781b3b7623f83e52a9e Mon Sep 17 00:00:00 2001 From: iris <2+iris@noreply.localhost> Date: Sat, 29 Aug 2026 13:46:13 -0400 Subject: [PATCH] Make jump-to-latest the same chevron, pointing down It was a labelled button beside a tool group that collapses with a drawn chevron -- two controls doing the same kind of thing in two visual languages. Now one `Chevron` composable serves both directions, parameterised rather than copied, since a pair that differs by a minus sign drifts and the drift is a bug in exactly one direction. The comment it replaces argued against an arrow here, on the grounds that the list is laid out upside down. That reasoning was about the code: nobody reading the screen knows the list is reversed, and on screen the newest message is at the bottom, which is where this goes. It draws no text, so the name lives in its content description -- the whole of what a screen reader has, and the answer to "what was that arrow for" later. Verified on the emulator: scrolled up, the chevron appears bottom centre matching the group's; tapped, it returns to the newest message and takes itself away. Co-Authored-By: Claude Opus 5 --- .../main/kotlin/com/example/aiapp/Chevron.kt | 53 +++++++++++++++++++ .../kotlin/com/example/aiapp/SessionScreen.kt | 31 ++++++++--- .../main/kotlin/com/example/aiapp/ToolRows.kt | 20 +------ 3 files changed, 78 insertions(+), 26 deletions(-) create mode 100644 app/androidApp/src/main/kotlin/com/example/aiapp/Chevron.kt diff --git a/app/androidApp/src/main/kotlin/com/example/aiapp/Chevron.kt b/app/androidApp/src/main/kotlin/com/example/aiapp/Chevron.kt new file mode 100644 index 0000000..0d7cdf7 --- /dev/null +++ b/app/androidApp/src/main/kotlin/com/example/aiapp/Chevron.kt @@ -0,0 +1,53 @@ +package com.example.aiapp + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.unit.dp + +/** + * A chevron, pointing up or down. + * + * Drawn rather than set in a font: a chevron from an icon font is one of the glyphs a system font + * may simply not have, and the reader who gets an empty box instead is never the one who wrote it. + * + * One composable for both directions rather than two that differ by a minus sign -- the pair would + * drift, and the drift would be a bug in exactly one direction. + * + * It draws no label of its own, so every caller owes it a `contentDescription`: this is the whole + * of what assistive technology has to go on, and it is also the answer to "what was that arrow + * for" six months from now. + */ +@Composable +fun Chevron( + pointingUp: Boolean, + modifier: Modifier = Modifier, + colour: Color = MaterialTheme.colorScheme.onSurfaceVariant, +) { + Canvas(modifier.width(20.dp).height(10.dp)) { + val inset = 2.dp.toPx() + val point = if (pointingUp) inset else size.height - inset + val ends = if (pointingUp) size.height - inset else inset + val stroke = 2.dp.toPx() + drawLine( + colour, + Offset(inset, ends), + Offset(size.width / 2, point), + strokeWidth = stroke, + cap = StrokeCap.Round, + ) + drawLine( + colour, + Offset(size.width / 2, point), + Offset(size.width - inset, ends), + strokeWidth = stroke, + cap = StrokeCap.Round, + ) + } +} diff --git a/app/androidApp/src/main/kotlin/com/example/aiapp/SessionScreen.kt b/app/androidApp/src/main/kotlin/com/example/aiapp/SessionScreen.kt index 664a144..0bcf7de 100644 --- a/app/androidApp/src/main/kotlin/com/example/aiapp/SessionScreen.kt +++ b/app/androidApp/src/main/kotlin/com/example/aiapp/SessionScreen.kt @@ -28,6 +28,10 @@ import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Surface +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -660,16 +664,29 @@ fun SessionScreen( // Only while the newest message is off-screen. Reading back // through a conversation is a place to be, not a state to be - // rescued from, so this waits to be wanted -- and says where it - // goes rather than drawing an arrow, since an arrow in a list - // that is laid out upside down is the one thing nobody should - // have to reason about. + // rescued from, so this waits to be wanted. + // + // Down, and the same chevron a tool group collapses with: the + // list is built upside down internally, but nobody reading it + // knows that -- on screen the newest message is at the bottom, + // which is where this goes. The name is carried in the + // description, since an arrow alone says nothing to a screen + // reader and nothing to whoever finds this in six months. if (!followTail) { - FilledTonalButton( + Surface( onClick = { scope.launch { listState.animateScrollToItem(0) } }, - modifier = Modifier.align(Alignment.BottomCenter).padding(bottom = 12.dp), + shape = CircleShape, + color = MaterialTheme.colorScheme.surfaceContainerHigh, + modifier = + Modifier.align(Alignment.BottomCenter) + .padding(bottom = 12.dp) + .semantics { contentDescription = "Jump to latest" }, ) { - Text("Jump to latest", style = MaterialTheme.typography.bodySmall) + Chevron( + pointingUp = false, + colour = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), + ) } } } diff --git a/app/androidApp/src/main/kotlin/com/example/aiapp/ToolRows.kt b/app/androidApp/src/main/kotlin/com/example/aiapp/ToolRows.kt index 3e1661f..2eabe12 100644 --- a/app/androidApp/src/main/kotlin/com/example/aiapp/ToolRows.kt +++ b/app/androidApp/src/main/kotlin/com/example/aiapp/ToolRows.kt @@ -132,25 +132,7 @@ private fun CollapseBar(onToggle: () -> Unit) { .padding(vertical = 10.dp), horizontalArrangement = Arrangement.Center, ) { - // Drawn rather than set in a font: a chevron from an icon font is one of the glyphs a - // system font may simply not have, and the reader who gets the empty box is never me. - androidx.compose.foundation.Canvas(Modifier.width(20.dp).height(10.dp)) { - val inset = 2.dp.toPx() - drawLine( - colour, - Offset(inset, size.height - inset), - Offset(size.width / 2, inset), - strokeWidth = 2.dp.toPx(), - cap = StrokeCap.Round, - ) - drawLine( - colour, - Offset(size.width / 2, inset), - Offset(size.width - inset, size.height - inset), - strokeWidth = 2.dp.toPx(), - cap = StrokeCap.Round, - ) - } + Chevron(pointingUp = true, colour = colour) } }