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

+15
View File
@@ -5,6 +5,21 @@ they can be judged and reversed later. Detail lives in RUST.md (and IRIS.md
for iris API changes); this file is only the summary. Newest first. Items
marked **DEFERRED** are ones the agent chose not to decide alone.
## 2026-09-08 (later still: the list's overscroll clamp, in frame)
Finishes the item the previous entry deferred. IRIS.md has the account.
- **`List` lays out a second time within the frame** when its walk lands
off the end of the content, instead of writing the correction to the
anchor and asking for another frame. The extra walk is paid only on an
overscrolled frame, and it is mostly O(1) moves.
- **`Painter::draw_again` is removed**, `List` having been its only
caller -- so the framework no longer offers a way to ask for a
corrective frame at all.
- **`List::place`'s top-known and bottom-known cases are one path**
(`Placement::edges`), which is the "write the logic once" rule applied
to two symmetric directions rather than a behaviour change.
## 2026-09-08 (later: a scroll area measures and places in one frame)
From Iris's phone report about the composer's padding while typing
+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
+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
+1 -1
View File
@@ -654,7 +654,7 @@ a change landed the way it did.
`fonts.xml` monospace declaration against fontique's actually-scanned
families, Android-only, verified `mono=Some("Droid Sans Mono")` on this
checkout's emulator.
- [x] Scroll clamped at both ends (e922b73, `List::clamp_to_content`)
- [x] Scroll clamped at both ends (e922b73, `List`'s overscroll clamp)
and Compose's velocity estimator (docs/IRIS_TODO.md, 2026-09-07
later). Ticked 2026-09-08 against those entries, which were already
`[x]` while this box was not. Two things this box's own wording had