iris: scrolling belongs to Scroll, and a LazySpan only lays out
Steps 2 and 3 of the plan in docs/IRIS_TODO.md, together because
deleting the fling before `Scroll` could drive it would leave the app
unable to scroll at all. IRIS.md has the account and the measurements.
`LazySpan` loses its `Flinger`, its `density`, its
`Arc<dyn RequestRedraw>` -- which had no business existing in a
single-threaded frame loop -- its `tick`, and the whole
`fling`/`cancel_fling`/`tick_fling`/`is_scrolling`/`fling_velocity`
surface. `Scroll` was the only other `Flinger` user, so there is now one
implementation of the physics rather than two, and a transcript is
`list.scrollable_to_end()` like anything else.
Three new `Widget` methods carry the handoff:
fn scrolls_itself(&self) -> bool { false }
fn apply_scroll(&mut self, delta: &mut f32) {}
fn scroll_offset(&self) -> f32 { 0.0 }
`Scroll` asks the first, and a child that says yes is handed deltas
instead of being slid about as a lump -- which a lazy layout cannot be,
since which rows exist at all is a function of where it is scrolled to,
and it has no content length to be clamped against. `scrolls_itself` is
`&self` deliberately: `Widgets::get_dyn_mut` marks a widget dirty, so
asking through `apply_scroll` would dirty every ordinary child on every
tick and cost exactly the O(1) move the scheme exists for.
`Scroll::draw` is measure, apply, place -- the idiom it already used for
its own content length. The measuring draw is free in the common case
(unchanged region, nothing dirty, `draw_inner` returns immediately and
the child's stored walls are still correct) and really walks exactly
when the content changed. Nothing is marked by hand: reaching the child
to hand it the delta is what dirties it, which is why `draw_again` could
stay deleted.
`scroll_offset` was not in the plan and is needed. A lazy span usually
cannot say where its content ends until it has walked there, so it takes
a delta in full whenever the wall is not already in view and the walk
gives part of it back; the remainder is exact only when the wall was
already visible, and `Scroll` adding remainders up would over-count by
every overshoot and never correct. It reads the child's accumulated
movement after the placing draw instead, so `amt` equals what is on
screen. `amt_counts_only_what_the_child_could_take` is the test.
One convention for a scroll delta, the finger's. `Scroll::scroll(+)`
moved toward the start while `LazySpan::scroll(+)` moved toward the end,
with the latter's doc claiming to mirror the former -- so every call site
had to know which it was talking to. `LazySpan::scroll` is private now
and the single negation is inside its `apply_scroll`; call sites that
passed `-dy`/`-v` pass them through, and `phone_screen.rs`'s recorded
velocity flips sign with its magnitude unchanged.
`a_negative_delta_moves_toward_the_end` pins the sign across the whole
handoff, since nothing else can catch a list scrolling backwards.
The transcript builds its `Scroll` by hand rather than through
`.scrollable_to_end()`: that helper registers a finger drag, and
`Selection` is already the arbiter for those frames -- two `DragGesture`s
seeing one gesture is what its own doc rules out. Caught by
`a_long_press_and_drag_selects_text`, which failed when both were live.
Deferred, in DECISIONS.md and IRIS_TODO.md: the *pin* is still each
widget's own. Applying one happens when a row is appended, between
frames with no painter in hand, so moving it to `Scroll` needs a fourth
`Widget` method or a parameter on `apply_scroll`; nothing external edits
a pin today.
Verified: cargo fmt --check, clippy --workspace --all-targets clean,
cargo test --workspace green (21 suites), the arm64 release APK builds,
and the phone-shaped headless window replaying flick-120hz.touch scrolls
back through the transcript in the direction it did before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8e5928cc6a
commit
b7474f61b0
19 files changed
+1016
-611
No files matched your search
+39
-1
@@ -52,7 +52,45 @@ order and what "done" looks like. Tick and date them in place.
|
||||
honest for every current use and must be written at the field so
|
||||
nobody builds a scrollbar on it.
|
||||
|
||||
Step 1 is done. Steps 2 and 3 are not.
|
||||
Done 2026-09-08, in two commits (the rename, then steps 2 and 3
|
||||
together -- deleting the fling before `Scroll` could drive it would
|
||||
have left the app unable to scroll at all).
|
||||
|
||||
**Two things the plan did not anticipate, both settled in the code:**
|
||||
|
||||
- **`apply_scroll`'s remainder is not enough on its own, so `Widget`
|
||||
gained a third method, `scroll_offset`.** A lazy span usually cannot
|
||||
say where its content ends until it has walked there, so it takes a
|
||||
delta in full whenever the wall is not already in view, and the walk
|
||||
that follows gives part of it back. The remainder is therefore right
|
||||
only when the wall was already visible, and `Scroll` adding
|
||||
remainders up would over-count by every overshoot and never correct.
|
||||
`scroll_offset` is the child's accumulated movement, read `&self`
|
||||
after the placing draw, and `Scroll::amt` is set from it -- so `amt`
|
||||
equals what is on screen rather than what was asked for. There is a
|
||||
test, `amt_counts_only_what_the_child_could_take`.
|
||||
- **There were two opposite scroll-delta conventions**, and the
|
||||
handoff made keeping both impossible. `Scroll::scroll(+)` moved
|
||||
toward the *start* while `LazySpan::scroll(+)` moved toward the
|
||||
*end*, and `LazySpan::scroll`'s own doc claimed to mirror `Scroll`'s.
|
||||
There is one now -- the finger's, which is `Scroll`'s -- and
|
||||
`LazySpan::scroll` is private, with the single negation inside
|
||||
`apply_scroll`. Call sites that used to pass `-dy`/`-v` pass them
|
||||
through, and the fixture recordings' expected velocity flipped sign
|
||||
with its magnitude unchanged.
|
||||
|
||||
**Still open, and the one thing to decide:** the *pin* ("stay at the
|
||||
end as rows are appended") is still each widget's own -- `Scroll` has
|
||||
`snap_end` for an ordinary child, `LazySpan` has one for itself, and
|
||||
the constructor argument sets each. Iris asked for `amt` and "other
|
||||
controls (iirc only at end for now)" to live in `Scroll` so a caller
|
||||
always edits the `Scroll`; that half is done for `amt` and not for the
|
||||
pin, because a pin has to be *applied* when a row is appended --
|
||||
between frames, with no painter in hand -- so moving it needs either a
|
||||
fourth `Widget` method or a parameter on `apply_scroll`. Nothing
|
||||
external edits a pin today (the transcript sets it once at
|
||||
construction and calls `jump_to_end` on the span for the rest), so
|
||||
this is a design question rather than a missing capability.
|
||||
|
||||
|
||||
- [x] **`List::clamp_to_content` still corrects on the next frame
|
||||
|
||||
Reference in new issue
Block a user