iris: one ScrollController, a Scrollable trait, and Pin
Iris's three points on docs/SCROLL.md, in the shape she proposed: a controller both scrolling widgets *contain*, rather than a protocol between them. "I don't like adding methods to widget, it seems like we can structure things better instead." `Scroll` becomes `ScrollArea`, because it only scrolls a predefined area. `ScrollController` holds everything that is not a particular widget's layout -- the position, the pending delta, the travel left each way, the pin, the DragGesture and the Flinger -- and `Scrollable` is the trait over it, one required pair of methods with the rest defaulted. `Widget` loses `scrolls_itself`, `apply_scroll` and `scroll_offset`. They existed only so a `Scroll` could drive a `LazySpan` it had no business wrapping; the span owns its own controller now, so the wrapper, the measure/apply/place dance between two widgets and `amt`'s two meanings all go with them. The transcript's tree loses a node: `list` is the layout and the position. `.scrollable(axis, pin)` replaces `scrollable`/`scrollable_on`/ `scrollable_to_end` -- one mechanism whose arguments had been hidden in three names. `LazySpan` has an inherent `scrollable()` that shadows it, since Rust resolves inherent methods before trait ones: the same word at the call site, and the wrapping version cannot reach the one widget that must not be wrapped. `Pin` says which end either way round: `Start`/`End` are content-relative and `Neg`/`Pos` axis-absolute, so a caller can say "the bottom" and mean it whichever way the content runs. They differ only for a reversed span, which is the whole reason both exist. One behaviour changes: a delta is applied by the next draw rather than where it arrives, since the layout is the only thing that knows where the content ends. Nothing on screen differs -- input is followed by a frame -- but `amt` no longer moves between draws, which several tests were reading. This also closes SCROLL.md's open question about the pin living in two places. Verified: cargo test --workspace (all green, including the layer-1 transcript-fixture fling/selection/top-edge tests), clippy --all-targets clean, fmt clean, `cargo ndk` check of android-app, and `run-headless.sh phone --phone --replay flick-120hz.touch`, whose before/after screenshots show the recorded flick carrying the transcript back from turn 270 to turn 258 on the Vulkan adapter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bf8658c404
commit
4fdabc39d0
33 files changed
+1725
-1435
No files matched your search
+62
-1
@@ -12,7 +12,68 @@ 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 (last): `List` is `LazySpan`, and scrolling belongs to `Scroll`
|
||||
## 2026-09-08 (newest): one `ScrollController`, a `Scrollable` trait, and `Pin`
|
||||
|
||||
Your three points on `docs/SCROLL.md`, in one change. The shape is the one
|
||||
you proposed: **a controller both scrolling widgets contain**, rather than
|
||||
a protocol between them.
|
||||
|
||||
**`Scroll` is `ScrollArea`**, because it only scrolls a predefined area --
|
||||
your word for it. **`ScrollController`** (`widget/position/scrollable.rs`)
|
||||
holds everything that is not a particular widget's layout: the position,
|
||||
the pending delta, the travel left each way, the pin, the `DragGesture`
|
||||
and the `Flinger`. **`Scrollable`** is the trait over it -- one required
|
||||
pair of methods handing the controller back, and `scroll`, `fling`, `drag`,
|
||||
`amt`, `is_scrolling`, `cancel_fling`, `tick_fling` and the pin as
|
||||
defaults.
|
||||
|
||||
**The three scrolling methods are off `Widget`.** `scrolls_itself`,
|
||||
`apply_scroll` and `scroll_offset` existed only so a `Scroll` could drive a
|
||||
`LazySpan` it had no business wrapping. A `LazySpan` owns its own
|
||||
controller now, so there is no wrapper, no measure/apply/place dance
|
||||
between two widgets, and no `amt` with two meanings depending on which kind
|
||||
of child it had. The transcript's tree lost a node with it: `list` is the
|
||||
layout *and* the position.
|
||||
|
||||
before Masked(Scroll(LazySpan)) .scrollable_to_end(Axis::Y)
|
||||
after Masked(LazySpan) .scrollable()
|
||||
|
||||
**`.scrollable(axis, pin)`** is the only one now -- `scrollable`,
|
||||
`scrollable_on` and `scrollable_to_end` were one mechanism with the
|
||||
arguments hidden in the names. A `LazySpan` has an **inherent**
|
||||
`scrollable()` that shadows it, since Rust resolves inherent methods
|
||||
first: same word at the call site, and the wrapping version cannot reach a
|
||||
widget that must not be wrapped. The axis and pin are already its own.
|
||||
|
||||
**`Pin` says which end either way round.** `Start`/`End` are
|
||||
content-relative, `Neg`/`Pos` axis-absolute -- your ask, so a caller can
|
||||
say "the bottom" and mean it whichever way the content runs. They coincide
|
||||
for everything except a reversed `LazySpan`, where they are opposites.
|
||||
|
||||
**A delta's sign is now a screen direction**, positive scrolling up or
|
||||
left. It was "positive brings earlier content into view", which points the
|
||||
opposite way for a `Dir::UP` span -- a real defect, latent only because
|
||||
nothing builds one yet, and invisible to the existing test because that
|
||||
test asserts in the same space the bug lives in.
|
||||
|
||||
**Why overscroll exists at all**, since you asked: a lazy span cannot see
|
||||
the wall until it has walked to it, so with rows loaded past an edge it
|
||||
honestly reports infinite travel, takes the whole delta, and the walk
|
||||
finds the content ran out 200px ago. It is given back inside the same
|
||||
frame. The rows past the edge have never been measured, and measuring them
|
||||
is the work virtualisation exists to skip.
|
||||
|
||||
**One behaviour changed**: a delta is applied by the next `draw` rather
|
||||
than the moment it arrives, since the layout is the only thing that knows
|
||||
where the content ends. Nothing on screen differs -- input is followed by
|
||||
a frame -- but `amt` no longer moves between draws, which several tests
|
||||
were reading.
|
||||
|
||||
The pin question `SCROLL.md` had open ("the pin lives in each widget, not
|
||||
in `Scroll`") is closed by this: it is one field on the controller, and a
|
||||
caller edits one place.
|
||||
|
||||
## 2026-09-08 (earlier): `List` is `LazySpan`, and scrolling belongs to `Scroll`
|
||||
|
||||
From the design exchange after the overscroll fix, where you asked
|
||||
whether `List` could just be `Span::scrollable()`. It cannot -- a lazy
|
||||
|
||||
Reference in new issue
Block a user