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:
1 parent
a00376994e
commit
76fcbdccb9
7 files changed
+307
-190
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user