Land a closed card centred on the tap that closed it

Two things were wrong with the hold, and each hid the other.

It held a *share* of the row's height: the point the finger was on stayed, in
proportion, which is the same miss in miniature as holding an edge. Tap away
from the middle of a long card and the heading landed most of a card's height
from the finger, and off it. A shut card is a heading, and the only place it
belongs is centred under the hand that shut it, wherever down the card the tap
was.

And the correction was worked out from the change in height, which needs the
list to behave the way the arithmetic assumed. It does not: which item it holds
still across a resize depends on what it has composed -- a row taller than the
screen is anchored on itself -- and a scroll it cannot honour in full is
honoured in part with nothing said. Measured rather than predicted now: each
measure pass asks for the error it can see, and the pass that answers is where
the rest becomes askable. Three passes is the worst seen, including the one
where a card that reached past the bottom of the screen has shut, left the
viewport entirely, and has to be asked back to the bottom edge before there is
anything to measure at all.

The pass has to be the *measure* one. A scroll asked for during placement is
never picked up by another measure and does nothing whatever -- which is what
the first version of this did, and why a close moved nothing -- so the list
took an `afterMeasure` hook and the correction lives there.

Checked with ktfmtFormat, compileDebugKotlin, lintDebug and testDebugUnitTest,
and on the emulator against the sandbox, against a 2,785px card in a
conversation with room on both sides: closed at 1450 it lands 1387..1513, at
1700 it lands 1637..1763, at 1950 it lands 1887..2013 -- centred on the tap to
the pixel each time. Opening by the heading still holds the heading still, and
a group closed from its footer bar lands on the bar. Where the conversation
runs out -- a card at the very start with nothing above it to scroll -- it
lands as close as the list can put it, which is what it could always do.
This commit is contained in:
iris-ai committed 2026-09-16 02:38:48 -04:00
1 parent b86a5dc37a
commit 1b38579b97
3 files changed
+179 -113

No files matched your search

+28 -22
View File
@@ -1189,28 +1189,34 @@ dev-updater (Kotlin 2.4.x, CMP 1.11.x, JDK 21).
question *was*, and "Deny" alone does not say Allow was the alternative.
One rule in two places (`AskedQuestion` and `PermissionAsk`). An answer
typed into **Other** matches no option, so that one is still written out.
- **Opening a row keeps still the end nearest the tap, and closing one
keeps still the point touched** (2026-09-16). A row about to open is
small, so both its edges are within a heading's height of the finger and
the wanted one is the edge pressed: touch the upper half and the top
edge holds, so it opens downwards; touch the lower half and the bottom
edge holds, as the list does by default. A row about to close leaves
almost nothing of itself, and the only place the closed card can
sensibly appear is under the finger that closed it — the case that
settles it is a card taller than the screen, where holding either edge
throws the card a screen's length from the hand. One number carries
both: the share of the row's height above the touch, spent as it is on a
close and rounded to the nearer edge on an open. Which half, rather than
which control, so everything behaves alike whether or not it has a
control at each end. The transcript is laid out from the bottom, so a
bottom edge is anchored for free and anything else has to be arranged:
`Modifier.holdTouchedPoint` asks the list to shift during the *layout*
phase, before anything is drawn. From an effect instead, the wrong
position is drawn once first, which reads as a flick. The scroll offset
that asks for it **goes negative on a close** — that is the list being
asked for the rows below what it has composed and clamping it at zero,
which is what the growing-only version did, leaves every close
uncorrected.
- **Opening a row keeps still the end nearest the tap; closing one lands
the closed row centred on the tap itself** (2026-09-16). A row about to
open is small, so both its edges are within a heading's height of the
finger and the wanted one is the edge pressed: touch the upper half and
the top edge holds, so it opens downwards; touch the lower half and the
bottom edge holds, as the list does by default. A row about to close
leaves a heading where a screenful of card was, and the place that
heading belongs is under the finger that shut it — a card taller than the
screen settles it, since both of its edges can be a screen's length from
the hand, and holding a *share* of the card's height was the same miss in
miniature: tap away from the middle of a long card and the heading landed
a card's height off. Where there is not enough conversation on the far
side to scroll, the row lands as close as the list can put it.
**The correction is measured, never predicted, and it is asked for from
inside the list's own measure** (`settleHold`, called from
`TranscriptList`'s `afterMeasure`). Both halves of that were paid for:
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, and how much of a scroll it can honour, depend on
what it has composed, and a row taller than the screen is anchored on
*itself*. And a scroll asked for during the *placement* phase is never
picked up by another measure, so it does nothing at all; from a coroutine
it arrives a frame late, which is the flick this mechanism exists to
avoid. So each pass asks for the error it can see, and the pass that
answers is where the rest becomes askable — three passes is the worst
case seen, including the one where the shut card has left the viewport
entirely and has to be asked back to the bottom edge before it can be
measured at all.
- **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