Open a call from its foot upward, as a row already does

A closed call inside a group opened downward wherever it was pressed: the
anchor asked for the group's top in every case, which is right for a tap on the
call's heading and wrong for one at its foot, where what the reader wants held
is the edge under their finger. It is the rule every other row has had since
the anchoring went in, applied one level down.

An open in the call's lower half now asks for no scroll at all, which is the
same answer a row gets and for the same reason: the list holds the group's
bottom edge, a Column keeps the calls below the one growing at their distance
from it, so the growth comes off the call's top. The closing behaviour is
untouched -- the arithmetic is the same shift, written as the one term it
cancels down to, since where the call sits in the group and where the group
sits in the viewport drop out of it.

Checked with ktfmtFormat, compileDebugKotlin, lintDebug and testDebugUnitTest,
and on the emulator against a sandbox group of six calls, with enough
conversation behind it for the list to actually scroll -- a transcript shorter
than the viewport pins to the bottom and gives every anchor the same answer,
which is how this was missed. A call closed at 1321..1447: opened from 1430 it
leaves the call below it at 1485 to the pixel and takes the growth off its top;
opened from 1360 it leaves the calls above it where they are and moves the one
below down by the full 327; closed again from 1400 it lands at 1338..1464,
centred on the tap within a pixel.
This commit is contained in:
iris-ai committed 2026-09-16 03:40:34 -04:00
1 parent ee5bef3686
commit 84f978f16d
2 files changed
+31 -11

No files matched your search

+6 -1
View File
@@ -1210,7 +1210,12 @@ dev-updater (Kotlin 2.4.x, CMP 1.11.x, JDK 21).
call is drawn and how tall that card is (`onToolToggle`), which is the call is drawn and how tall that card is (`onToolToggle`), which is the
part only it knows, and the calls above the one toggled do not move, so part only it knows, and the calls above the one toggled do not move, so
shifting the group by the difference puts the call where the finger shifting the group by the difference puts the call where the finger
wants it. wants it. A call opened in its lower half asks for nothing at all, which
is the same answer a row gets: the list holds the group's bottom edge, a
Column holds the calls below the one growing at their distance from it,
and so the growth comes off the call's top. Asking for the group's top
in every case — which it did for a day — was an open pressed at the
card's foot driving it downward instead.
**The scroll is asked for in the gesture, not from the layout that **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 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 request made at the tap is consumed by the same measure pass that first
@@ -610,7 +610,7 @@ fun SessionScreen(
/** /**
* The same, for a call inside an open group: the call lands centred on the finger that shut it, * The same, for a call inside an open group: the call lands centred on the finger that shut it,
* and holds its own top edge when it is opened. * and when it is opened it holds whichever of its own edges the finger is nearer.
* *
* The group is what the list knows -- a call is drawn inside a row rather than being one -- so * The group is what the list knows -- a call is drawn inside a row rather than being one -- so
* the group is what the scroll is asked for, and [top] and [height], which only the group can * the group is what the scroll is asked for, and [top] and [height], which only the group can
@@ -618,10 +618,13 @@ fun SessionScreen(
* one toggled do not move, so the call's own top stays [top] below the group's, and shifting * one toggled do not move, so the call's own top stays [top] below the group's, and shifting
* the group by the difference puts the call where the finger wants it. * the group by the difference puts the call where the finger wants it.
* *
* Holding the group's top edge is what an open wants and is what a close used to get as well. * The group's top edge is therefore the call's top edge, which is what an open pressed near the
* It is right for the open -- the call's heading is the edge under the finger -- and wrong for * call's heading wants. An open pressed in its lower half wants the call's *bottom* held
* the close by however far down the open call the reader pressed, which for a card of output is * instead, and that is what asking for no scroll at all gives: the list holds the group's
* most of the screen: the card appeared to shrink to its own top, a long way from the hand. * bottom edge, a Column keeps the calls below the one growing at their distance from it, and so
* the growth comes off the top exactly as it does for a row. Asking for the group's top in
* every case -- which it did for a day -- drove a call opened from its foot downward off the
* screen.
*/ */
fun toggleCallAnchored( fun toggleCallAnchored(
row: TranscriptRow, row: TranscriptRow,
@@ -635,12 +638,24 @@ fun SessionScreen(
if (lastTouch.key == row.key && item != null && item.index + 1 < info.totalItemsCount) { if (lastTouch.key == row.key && item != null && item.index + 1 < info.totalItemsCount) {
val opening = id !in expandedTools val opening = id !in expandedTools
val closed = if (opening) null else closedCallHeights[id] val closed = if (opening) null else closedCallHeights[id]
val touch = item.offset + lastTouch.height - lastTouch.y // How far down its own top edge the finger landed is the whole of what the call's
// Where the call's top edge is now, and where it has to be for the closed call to sit // place contributes: where it sits in the group and where the group sits in the
// centred on the finger; the group's top moves by the difference between them. // viewport cancel, since both edges move together.
val callTop = item.offset + lastTouch.height - top val into = lastTouch.y - top
val shift = if (closed == null) 0 else touch + closed / 2 - callTop fun holdGroupTop(shift: Int) =
listState.requestScrollToItem(item.index + 1, -(item.offset + item.size + shift)) listState.requestScrollToItem(item.index + 1, -(item.offset + item.size + shift))
when {
// The call the reader is shutting, centred on the finger that shut it: its top
// goes down by how far the finger is into it, less half of what it is becoming.
closed != null -> holdGroupTop(closed / 2 - into)
// An open in the call's lower half wants its bottom edge held, and that is what
// the list and the group do on their own.
opening && into >= height / 2 -> {}
// What is left holds the call's top, which is the group's top moved by nothing: an
// open in the top half, and a close of a call opened before this screen was, which
// has no remembered height to land on.
else -> holdGroupTop(0)
}
// Its height as it stands is the height it goes back to when it is shut again. // Its height as it stands is the height it goes back to when it is shut again.
if (opening) closedCallHeights[id] = height if (opening) closedCallHeights[id] = height
} }