Skip to main content

Module scroll_controller

Module scroll_controller 

Source
Expand description

ScrollController: a cloneable, reactive-free handle onto a ScrollView or a ListView — the programmatic-scroll seam.

App code keeps a handle in its state, attaches it with ScrollView::controller or ListView::controller, and drives or reads the surface from anywhere on the UI thread, inside a dispatch or not:

  • Writes are recorded, not applied. ScrollController::jump_to, ScrollController::animate_to and ScrollController::scroll_to_item queue a command the attached widget drains at the start of its next layout or paint (whichever runs first — a ListView also drains at its next rebuild, before it plans the window, so the jump’s frame already materializes the rows it lands on), so the clamp is always taken against the freshly laid-out extent — a jump issued in the same handler that appended content lands on the new bottom, not the old one. A command queued before any surface attaches (or before the attached one has laid out) waits for the first layout. Recording raises frust_core::mark_pending_result_flush so a frame-gated shell runs the frame that applies it even when nothing else changed (the PanZoomController/NavigatorController precedent).
  • Reads are the last published snapshot. ScrollController::offset, ScrollController::max_offset and ScrollController::viewport_extent return what the attached surface last published — after every layout, paint and event pass that changed it — and ScrollController::on_change listeners hear each new ScrollInfo as it is published.
  • One surface per handle. The most recent attach wins: a second surface attaching the same handle takes it over and the first stops draining or publishing; dropping the attached widget detaches it.
  • is_animating tracks the current binding, never a stale one. ScrollController::is_animating is true only while the currently attached binding’s surface has a live controller-driven ScrollController::animate_to tween running — never a snapshot of whatever the handle last happened to report. Both places current-ness changes reconcile it so a detached or superseded binding can never leave it stuck true: ScrollController::bind resets it for the newly attached binding, and dropping the binding that was still current clears it (impl Drop for ScrollBinding) even when the widget itself never got to call ScrollController::is_animating’s setter — covering a tween whose surface is dropped, taken over, or rebound mid-flight. A widget whose tween genuinely survives a rebind to a different handle (the tween belongs to the widget, not the handle) republishes its live state onto the freshly bound handle right after binding, so a real in-flight tween is still reported, just under the new current binding instead of the old one.
  • Item commands are list-only. ScrollController::scroll_to_item names a row by its ChildKey, which only a keyed ListView can resolve. The queue therefore holds a wider crate-internal command type than the offset-only ScrollCommand a ScrollView matches on: the ScrollView drain filters item commands out (ignoring each with a debug-build log) and keeps the offset ones in recording order, so ScrollView never sees — and never has to match on — a variant it cannot act on.
  • The queue is bounded, not an unbounded backlog. Offset commands are grouped into segments by the item commands between them, and within a segment a jump_to supersedes every earlier offset command (it cancels any tween and sets the position outright) while an animate_to supersedes an earlier animate_to but keeps a preceding jump_to, so the tween still starts from the jumped-to position. A segment therefore holds at most one jump and one animate in whatever order they were recorded — a tight burst (a pointer-drag thumb, say) or an alternating jump/animate loop both leave at most two entries — and superseding never crosses a ScrollController::scroll_to_item (an offset/item interleaving stays in exact recording order). Item commands keep their own ordered channel but are capped at [ITEM_COMMAND_CAP] entries; recording one past the cap drops the oldest queued item command (debug-build log) rather than growing further, so the whole queue never exceeds the cap plus two offset entries per segment. One behavioural consequence: a superseded jump_to that would, had it actually applied, transiently cross a ListView near-start/near-end threshold never does — the edge callbacks evaluate only the surviving target, not every value a caller recorded.
  • The pre-attach backlog survives a bind. ScrollController::bind does not clear the queue: a command recorded before any surface attaches (or before the newly attached one has laid out) waits for that surface’s first layout, per the apply contract above. The hazard this trades for: a handle driven in a loop while nothing is attached cannot grow without bound either — superseding and the item cap still apply — but it does accumulate up to that bound (at most one jump and one animate per segment, up to [ITEM_COMMAND_CAP] item commands) rather than draining as it is issued.

See docs/WIDGETS_ARCHITECTURE.md’s Scroll Physics for the surface the handle drives.

Structs§

AnimateTo
The duration/curve ScrollController::animate_to eases a programmatic scroll through. Both fields are required rather than defaulted — Flutter’s ScrollController.animateTo keeps duration/curve required for the same reason: an app names the motion it wants rather than inheriting a guessed one.
ScrollController
A cloneable handle onto a ScrollView or a ListView: drive it with jump_to/ animate_to (and, on a keyed list, scroll_to_item), read where it stands with offset/max_offset/ viewport_extent, and observe it with on_change. Every clone shares one state, so the handle an app keeps in its state and the one attached to the view are the same. See the module docs for the apply/publish contract.
ScrollSubscription
Keeps an ScrollController::on_change listener subscribed; dropping it unsubscribes. Holds the handle weakly, so it never keeps a controller alive.

Enums§

ItemAlignment
Where ScrollController::scroll_to_item lands a row in the viewport.