Hold a row's top edge during layout, so nothing is drawn out of place

The correction ran in a coroutine, so it landed a frame or more after the
layout it was correcting: the wrong position was drawn once and then fixed,
which reads as a flick and gets worse the faster the screen refreshes. That
is a race with the display rather than a bug that can be tuned out, so the
fix is not a shorter delay but a different phase.

It now happens in the layout phase. `Modifier.holdTopEdge` learns the row's
new height from the measurement that produced it and asks the list to shift
by exactly that much, before anything is drawn. `requestScrollToItem` is
the form that may be asked for during layout; `dispatchRawDelta` is not --
it calls forceRemeasure and dies with "performMeasureAndLayout called
during measure layout", which cost one crash to establish.

The arming flag and the per-row height are deliberately not snapshot state.
Both are written from layout, where a snapshot write that composition reads
would schedule another recomposition -- another frame, which is the thing
being removed.

This also drops the machinery the previous attempt needed: no waiting on a
size change, no timeout, no marking the rows above to find one that could
still report the move. A row measures itself, so a row that shrinks out of
the viewport is no longer a special case.

Verified with ui-trace sampling at ~1kHz, where a single bad frame would
show as ten to twenty samples: expanding and collapsing from a heading are
each one step from old position to held position with nothing in between,
collapsing from the foot bar holds the four rows below it, a drag 120ms
after a tap is left alone, and scrolling back stays put for six seconds.
ktfmt, lint and 85 tests clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Opus 5 committed 2026-08-30 12:15:44 -04:00
1 parent cab21442d6
commit 5eba6ec529
2 files changed
+173 -172

No files matched your search

+8 -8
View File
@@ -628,14 +628,14 @@ dev-updater (Kotlin 2.4.x, CMP 1.11.x, JDK 21). Screens:
a group's heading or the bar at its foot — is the end that stays put,
and the row grows away from it. The transcript is laid out from the
bottom, so a row's bottom edge is anchored for free and the top one
has to be arranged. `toggleAnchored` waits on the row's *size*, which
a scroll cannot change — a version that waited on the top edge instead
mistook the reader's own drag for the row resizing and undid it — and
then reads how far things moved off any row from the pressed one
upwards, since a group taller than the screen disappears under the
bottom edge as it shrinks and cannot report its own move (2026-08-30,
asked for after groups opened upwards and sent their own heading off
the top of the screen).
has to be arranged. The correction lives in the *layout* phase
(`Modifier.holdTopEdge`): the measurement that discovers the row's new
height asks the list to shift by that much, via
`requestScrollToItem`, before anything is drawn. Doing it from an
effect instead means the wrong position is drawn once first, which
reads as a flick and gets worse the faster the screen refreshes
(2026-08-30, asked for after groups opened upwards and sent their own
heading off the top of the screen).
- Input bar: text, attach (camera/gallery/file), send — **always enabled**;
mid-run sends become steering messages.
- Top bar: model chip (tap to change), stop button while running, token