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

+50
View File
@@ -12,6 +12,56 @@ things still stay out.
An entry gives the date, what changed, why, and a short before/after where
it helps judge the change without the session that made it. Newest first.
## 2026-09-08 (later still): a `List` clamps its overscroll in the same frame, and `draw_again` is gone
The last place in iris that corrected itself on a later frame. `List`'s
walk outward from its anchor could end up off the end of its content --
a fling stops wherever the spline's last step left it, and a `scroll` is
deliberately unclamped because nothing at the moment of the call knows
where the content ends. `clamp_to_content` measured that gap 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 content past its own end,
and on Iris's phone a hard fling to the top left the whole screen blank
until something asked for that frame -- which a fling that has stopped no
longer does.
It is the same shape as `Scroll`'s fix. The walk is now `List::lay_out`,
and `List::draw` runs it, asks `overscroll_gap` whether the layout landed
off the end, and on a gap moves the anchor and runs the walk **again,
inside the same frame**. `overscroll_gap` is a pure measurement -- no
painter, no redraw handle -- and the decision to lay out again is `draw`'s.
Three properties make the second pass cheap and correct:
- **It runs only on a frame that actually overscrolled.** An ordinary
scroll tick still walks once.
- **One further pass always settles it.** The gap is measured from the
edges the first walk placed, so moving the anchor by it puts that edge
exactly on the viewport's; the opposite end can only open a new gap if
the content is shorter than the viewport, which `overscroll_gap`
declines to touch at all (a short list is bottom-anchored on purpose).
- **The second walk is mostly moves.** Every row keeps the box its cached
height gives it and only its offset changes, which is `draw_inner`'s
O(1) `mov` path.
**Public surface: `Painter::draw_again` is removed.** `List` was its only
caller, so with this there is no "ask for a corrective frame" mechanism in
the framework -- which is the point, since reaching for one is the sign a
placement should have been redone inside the draw that discovered the
problem.
`List::place` also lost half its body to the same simplification the rule
suggests: a placement is one pinned edge plus a height, so `Placement::
edges(height)` gives the box and the top-known and bottom-known cases stop
being two copies of the same arithmetic.
Tests that 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` in `list.rs`,
and `scrolling_past_the_first_row_settles_on_it` /
`scrolling_past_the_last_row_settles_on_it` at layer 1
(`transcript-fixture/tests/top_edge.rs`).
## 2026-09-08 (later): a `Scroll` measures and places its content in one frame
Iris's phone: "when typing with the keyboard up and entering enough