iris: a List gives back its overscroll in the frame that found it

The last place in iris that corrected itself on a later frame, and the
item docs/IRIS_TODO.md carried from the Scroll change. Iris's rule:
"nothing in the framework should ever self heal because it should not be
drawn incorrectly in the first place. If you need 2 draws to get
something into the correct position then that should happen within the
same frame."

`clamp_to_content` measured the gap past the end of the content from the
edges the walk had just placed, wrote it to the anchor and asked for
another frame -- so one frame was drawn with the list past its own end,
and a fling that had already stopped was not going to ask for the frame
that fixed it. Now the walk outward from the anchor is `List::lay_out`,
`overscroll_gap` is a pure measurement of the same gap (no painter, no
redraw handle), and `draw` moves the anchor and walks a second time
inside the same frame.

One further pass always settles it: the gap comes from the edges the
first walk placed, so moving the anchor by it puts that edge exactly on
the viewport's, and the opposite end can only open a new gap when the
content is shorter than the viewport, which `overscroll_gap` declines to
touch. The second walk is paid only on an overscrolled frame and re-offers
every row the same cached-height box at a new offset, which `draw_inner`
dispatches as an O(1) move.

`Painter::draw_again` had no other caller and is removed with it, so the
framework no longer offers a way to ask for a corrective frame at all.

Simplification in the same change: a placement is one pinned edge plus a
height, so `Placement::edges(height)` gives the box and `place`'s
top-known and bottom-known cases stop being two copies of the same
arithmetic -- four match arms down to two.

Four tests draw no settling frame on purpose and fail without the change:
`fling_toward_the_start_stops_at_the_first_row` and the new
`scrolling_past_the_start_is_given_back_in_the_same_frame` (list.rs), and
`scrolling_past_the_first_row_settles_on_it` /
`scrolling_past_the_last_row_settles_on_it` (layer 1, top_edge.rs).

Verified: cargo fmt --check, clippy --workspace --all-targets clean,
cargo test --workspace and ./run-tests.sh green, the phone-shaped
headless window replaying flick-120hz.touch draws the transcript
correctly, and the arm64 release APK builds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Opus 5 committed 2026-09-08 17:26:51 -04:00
1 parent a00376994e
commit 76fcbdccb9
7 files changed
+307 -190

No files matched your search

+23 -12
View File
@@ -7,7 +7,7 @@ order and what "done" looks like. Tick and date them in place.
## Fix
- [ ] **`List::clamp_to_content` still corrects on the next frame
- [x] **`List::clamp_to_content` still corrects on the next frame
(2026-09-08).** Iris's rule, stated while the composer's caret was
being fixed: "nothing in the framework should ever self heal because
it should not be drawn incorrectly in the first place. If you need 2
@@ -15,15 +15,26 @@ order and what "done" looks like. Tick and date them in place.
happen within the same frame. Layout should never be frame dependent,
it should be a pure function of the state." `Scroll::draw` was brought
to that rule the same day (it measures its content and places it
again in the one frame, IRIS.md's entry). `List::clamp_to_content` is
the one place left that has not been: it discovers a fling has run
past the content's end, calls `scroll(gap)` and `Painter::draw_again`,
and asks its own `RequestRedraw` handle for a frame -- so one frame is
drawn with the content past its end and the next one snaps it back.
The fix is the same shape as `Scroll`'s: re-place inside the draw that
found the gap. Not done in the same change because `List::place` is a
larger piece of machinery than `Scroll::draw` and this deserves its
own before/after on the phone.
again in the one frame, IRIS.md's entry). Done for `List` later the
same day: the walk outward from the anchor is now `List::lay_out`, and
`draw` runs it, asks `overscroll_gap` (a pure measurement, no painter
and no redraw handle) whether the layout landed off the end of the
content, and on a gap moves the anchor and runs the walk a second time
**inside the same frame**. `Painter::draw_again` had no other caller
and is gone with it, so there is now no "ask for a corrective frame"
mechanism in the framework at all. One further pass always settles it:
the gap is measured from the edges the walk actually placed, so moving
the anchor by it puts that edge exactly on the viewport's, and the
opposite end cannot open a new gap without the content being shorter
than the viewport, which `overscroll_gap` declines to touch. The extra
walk is paid only on an overscrolled frame and re-offers every row the
same box at a new offset, which `draw_inner` dispatches as an O(1)
move. Three tests draw no settling frame on purpose and fail without
the change: `fling_toward_the_start_stops_at_the_first_row`,
`scrolling_past_the_start_is_given_back_in_the_same_frame` (both in
`list.rs`) and `scrolling_past_the_first_row_settles_on_it` /
`scrolling_past_the_last_row_settles_on_it` (layer 1,
`transcript-fixture/tests/top_edge.rs`).
- [x] **`request_device` asked for compute-shader limits it never uses
(2026-09-05).** `Limits::default()` (both `iris/src/android/render.rs`
@@ -1124,7 +1135,7 @@ do not duplicate it there.
**past its own first row** (`fling_toward_the_start_stops_at_the_
first_row` was leaving it 1398px below a 600px viewport, a blank
screen, and that test's own assertion could not see it).
`clamp_to_content` gives the gap back. Both ends:
the overscroll clamp gives the gap back. Both ends:
`scrolling_past_the_first_row_settles_on_it`,
`scrolling_past_the_last_row_settles_on_it`. This is also the first
item of the later report below.
@@ -1140,7 +1151,7 @@ do not duplicate it there.
## From the phone, 2026-09-07, later (build from 4274b8b, ai-app-bench b47eb73)
- [x] **"You shouldn't be able to scroll below the bottom (or above
top)."** Done in e922b73, as `List::clamp_to_content` rather than as a
top)."** Done in e922b73, as a clamp in `List::draw` rather than as a
clamp inside the scroll setter: nothing at the moment of a `scroll`
call knows where the content ends (that is what walking the rows finds
out), so the correction is measured from the ends the layout walk