diff --git a/PLAN.md b/PLAN.md index 3f57a8c..43ae3c9 100644 --- a/PLAN.md +++ b/PLAN.md @@ -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 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 - 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 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 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 ee26e09..54eacab 100644 --- a/app/androidApp/src/main/kotlin/com/example/aiapp/SessionScreen.kt +++ b/app/androidApp/src/main/kotlin/com/example/aiapp/SessionScreen.kt @@ -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, - * 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 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 * 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. - * It is right for the open -- the call's heading is the edge under the finger -- and wrong for - * the close by however far down the open call the reader pressed, which for a card of output is - * most of the screen: the card appeared to shrink to its own top, a long way from the hand. + * The group's top edge is therefore the call's top edge, which is what an open pressed near the + * call's heading wants. An open pressed in its lower half wants the call's *bottom* held + * instead, and that is what asking for no scroll at all gives: the list holds the group's + * 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( row: TranscriptRow, @@ -635,12 +638,24 @@ fun SessionScreen( if (lastTouch.key == row.key && item != null && item.index + 1 < info.totalItemsCount) { val opening = id !in expandedTools val closed = if (opening) null else closedCallHeights[id] - val touch = item.offset + lastTouch.height - lastTouch.y - // Where the call's top edge is now, and where it has to be for the closed call to sit - // centred on the finger; the group's top moves by the difference between them. - val callTop = item.offset + lastTouch.height - top - val shift = if (closed == null) 0 else touch + closed / 2 - callTop - listState.requestScrollToItem(item.index + 1, -(item.offset + item.size + shift)) + // How far down its own top edge the finger landed is the whole of what the call's + // place contributes: where it sits in the group and where the group sits in the + // viewport cancel, since both edges move together. + val into = lastTouch.y - top + fun holdGroupTop(shift: Int) = + 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. if (opening) closedCallHeights[id] = height }