Skip to main content

Module scrollable

Module scrollable 

Source
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 over ScrollHandlingOptions::smooth_duration or 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 a KineticScroller, 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§

ScrollHandlingOptions
Everything handle_scroll_event needs to know that is not state.
ScrollableAxes
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.
ScrollableBehavior
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.