Expand description
One scroll handler for every scrollable surface.
The scrollable widgets in this crate each hand-rolled the same on_scroll
body: convert a ScrollDelta to pixels, clamp each axis, animate or set,
and answer Ignored at a hard boundary so the event chains to an ancestor.
They agreed on the arithmetic and disagreed on everything around it — which
of them honoured Contain, which animated, which read a line height. This
module is that body written once, plus the two things none of them had: a
finger’s pan, and the rubber band that pan needs at the edge.
ScrollArea is the reference adopter and the one to
copy. The data views (ListView,
TreeView, GridView,
TableView,
TreeTableView) and the three text surfaces
(RichTextEditor, CodeEditor
and LogView) install it too. A widget that handles a
wheel without owning a scroll offset — SpinBox steps a number, TabBar
remaps a notch sideways — is not a scrollable and does not appear here.
§The two paths
handle_scroll_event branches on
EventContext::scroll_source,
not on the phase:
- Everything except
ScrollSource::TouchPan— a wheel notch, a trackpad stream, a programmatic scroll — takes the path this crate has always taken: clamp against the animation target rather than the rendered offset (so a mid-tween boundary chains correctly), then either tween overScrollHandlingOptions::smooth_durationor set outright, per axis and only where the clamp moved. ScrollSource::TouchPan— a pan synthesised from a direct pointer by the router, and the coast that follows it — goes through aKineticScroller, which is what supplies the rubber band. A pan never tweens: a finger is already the animation.
Splitting on the source and not the phase is what makes the migration safe.
A legacy WidgetEvent::Scroll reports ScrollSource::Wheel, so every
existing call site and every existing test keeps the path it had. The
distinction is load-bearing rather than cosmetic, and is pinned by
a_trackpad_stream_takes_the_wheel_path_and_not_the_kinetic_one
(tests/scrollables_touch.rs): a trackpad stream carries the same
Began/Changed/Ended phases a synthesised pan does, so a phase test
would route a pointing device into the kinetic path and start tracking
velocity for a contact that will never lift.
§Who owns what
The scroller belongs to the widget, not to this module: it is the widget
that knows its viewport (which is the only thing the rubber-band curve reads
beyond the range) and the widget whose layout pass is where that number
becomes available. So the surface owns an Rc<RefCell<KineticScroller>>,
calls set_viewport from its own layout,
and hands the handle to handle_scroll_event on every event. The range is
read from the ScrollableAxes signals each time, so it is never stale.
The coast is not owned here at all. A release hands its velocity to the
tree’s FlingDriver, which re-dispatches it as
ScrollPhase::Fling deltas along the same claimant chain the pan walked —
that is what makes a flick that runs out of an inner list scroll the outer
one. A fling delta therefore arrives here as an ordinary positive-or-negative
offset change and is applied with a hard clamp: the driver’s simulation
is unbounded and stopping it at the edge is this surface’s job, not the
band’s.
§Adoption
let axes = ScrollableAxes::new(scroll_x, scroll_y, max_x, max_y);
let behavior = ScrollableBehavior::new(axes)
.with_scroller(self.scroller.clone())
.axes(PanAxes::BOTH)
.smooth(self.smooth_scrolling)
.line_height(self.line_height)
.reduced_motion(ctx.prefers_reduced_motion());
let handlers = behavior.install(HandlerSet::new());install attaches both halves: the on_scroll handler and the
PanClaim that makes the node a pan claimant in the first place. A
surface that installs the handler without the claim is a surface a finger
cannot scroll, which is the bug this module exists to stop shipping.
Reference: docs/kinetic-scrolling.md.
Structs§
- Scroll
Handling Options - Everything
handle_scroll_eventneeds to know that is not state. - Scrollable
Axes - The reactive state one scrollable surface scrolls: where it is on each axis, how far it can go, and how far past the end it is currently being held.
- Scrollable
Behavior - The whole of what a widget must do to become scrollable, as one value it
installs onto its
HandlerSet.
Constants§
- SMOOTH_
SCROLL_ DURATION - The 150 ms ease-out every smooth-scrolling surface in this crate uses for a wheel notch. Unchanged by the touch programme — a wheel still feels the way it always did.
Functions§
- handle_
scroll_ event - Apply one scroll event to
axes, and answer the boundary question. - shift_
wheel_ remap - Rewrite a Shift+wheel notch into a horizontal one, or decline.