Ask for the scroll in the gesture, not from the layout

A correction made from the layout is a frame late whatever phase it is made in:
from placement it is never picked up by another measure and does nothing at all,
and from measure it lands on the next frame with the uncorrected one drawn
first. That is the flick when a card is opened, and it is why a close could
finish somewhere other than where it was aimed -- the two are the same fault.

Asked for at the tap instead, the request is consumed by the same measure pass
that first lays the row out at its new size, so the resize is drawn once, in its
right place. What makes that possible is that none of the three positions needs
to know the new height. A top edge holds by placing the item *above* the row
where it already is -- that item's bottom edge is the row's top edge whatever
becomes of the row, and it does not have to be composed for the list to place
it. A close places the row itself against the height it had at the moment it was
opened, which is the height it is going back to.

So the measure-phase hold, its modifier and the list's `afterMeasure` hook are
all gone, and this is 75 lines shorter than the version that could not do it.

Checked with ktfmtFormat, compileDebugKotlin, lintDebug and testDebugUnitTest,
and on the emulator against a real imported conversation as well as the sandbox:
a group closed by its heading lands centred on the tap (1467..1593 for a tap at
1530), a card closed at 1500 and at 1800 lands centred on each, opening by a
heading holds the heading to the pixel, and opening or closing a real call
inside a group of two leaves that group's heading exactly where it was.
This commit is contained in:
iris-ai committed 2026-09-16 03:08:17 -04:00
1 parent 581e07624f
commit 914985b8b2
3 files changed
+58 -133

No files matched your search

+16 -16
View File
@@ -1208,22 +1208,22 @@ dev-updater (Kotlin 2.4.x, CMP 1.11.x, JDK 21).
included — exactly where it is. Centring the group instead threw a group
of six the length of the screen. Where there is not enough conversation
on the far side to scroll, the row lands as close as the list can put it.
**The position is stated, not converged on, and it is asked for from
inside the list's own measure** (`settleHold`, called from
`TranscriptList`'s `afterMeasure`). Three things were paid for to get
there. A scroll asked for during the *placement* phase is never picked up
by another measure, so it does nothing whatever; from a coroutine it
arrives a frame late, which is the flick this mechanism exists to avoid.
Working the scroll out from the change in height needs the list to behave
the way the arithmetic assumed, and it does not — which item it holds
still across a resize depends on what it has composed, and a row taller
than the screen is anchored on *itself*. And correcting by the error a
pass can see, then asking again, is a *frame* per pass with the ones in
between drawn: opening a card visibly stepped. What is exact and takes
one pass is `requestScrollToItem` against the row itself, which places it
wherever it has ended up and whether or not it is still on screen — a
negative offset included, which is the list being asked for the rows
below a card that has just given the screen its whole height back.
**The scroll is asked for in the gesture, not from the layout that
discovers the new height**, and that is what makes it invisible: a
request made at the tap is consumed by the same measure pass that first
lays the row out at its new size, so the resize is drawn once, in its
right place. Everything else was tried and is a frame late — a request
from the placement phase is never picked up by another measure and does
nothing at all; one from the measure phase lands on the *next* frame,
with the uncorrected one drawn first; and correcting by the error each
pass can see is that again, once per pass. None of the three positions
needs the new height, which is what lets them be asked for before it
exists: a top edge holds by placing the item *above* the row where it
already is, since that item's bottom edge is the row's top edge whatever
becomes of the row, and it does not have to be composed for the list to
place it. A close places the row itself, against the height it had at the
moment it was opened — what the reader is shutting is the card they
opened, so that is the height it is going back to (`closedHeights`).
- **The full-screen image lives on the screen, not in the row that drew
the thumbnail** (`SessionImageViewer`). A `Read` whose result is an image
is a row of one call until the next call arrives and makes it a group — a