Programmatic scrolling

The scroll piece stays gesture-first: the native widget owns the viewport, physics, and indicators (DESIGN §7.6). This document covers driving it from code and from dayscript.

Authoring

let jump: Signal<Option<ScrollTarget>> = Signal::new(None);

scroll(column(rows)).scroll_target(jump);

button("Bottom").action(move || jump.set(Some(ScrollTarget::Bottom)));
button("Item 100").action(move || jump.set(Some(ScrollTarget::Id("row-100".into()))));

.scroll_target(sig) takes a Signal<Option<ScrollTarget>>: each Some(target) written to it scrolls there (animated), then the signal resets to None, so the same target can be sent twice in a row. ScrollTarget is:

targetmeaning
Top / Bottomthe vertical extremes
Leading / Trailingthe horizontal extremes (start/end in layout direction)
Offset(Point)pin the viewport origin to a content-space point (clamped to range)
Id(String)reveal the element with that dayscript id inside its nearest enclosing scroll

Lower-level, day_core::scroll_to(node, target) drives any scroll node directly, and TreeOps::scroll_reveal(node, animated) scrolls an element’s nearest scroll ancestor so the element is visible, the same call keyboard avoidance uses (docs/focus.md). Reveals are minimal: content already in view doesn’t move.

The showcase’s List page is the live reference (Day-Showcase/src/pages/list.rs): its scroll buttons drive the recycling list’s row rail (.scroll_to_row/.scroll_to_end, docs/list.md); scroll_target on a plain scroll piece works the same way with a ScrollTarget.

dayscript

- scroll_to: { id: page-scroll, edge: bottom }     # top | bottom | leading | trailing
- scroll_to: { id: page-scroll, x: 0, y: 300 }     # pin the viewport origin
- scroll_to: { id: row-100 }                       # reveal an element in its nearest scroll

The step is unanimated so the next step sees the settled position. assert_visible remains a presence check (realized + nonzero frame; DESIGN Appendix C); it does not test whether an element is inside the viewport, so pair scroll_to with screenshots when the test is about what’s on screen.

How a target becomes a scroll

ScrollLayout reports the content size per scroll node (cached in the tree), so day-core can compose each target into a content-space rect: edges become 1×1 rects at the extremes, Offset becomes a viewport-sized rect (minimal-reveal on a viewport-sized rect pins the origin exactly), and reveal-element accumulates native-ancestor origins from the element up to its scroll. Every backend then applies the same “minimal scroll to make the rect visible” rule:

backendnative call
AppKitNSView.scrollRectToVisible on the document view
UIKitUIScrollView.scrollRectToVisible(_:animated:)
Androidoffset math + ScrollView.smoothScrollTo / scrollTo (per axis class)
GTKadjustment clamp + set_value (no animation; GTK adjusts immediately)
Qtscroll-bar clamp + setValue (no animation)
XAMLScrollViewer.ChangeView (shim day_xaml_scroll_to)
ArkUINODE_SCROLL_OFFSET get/compute/set (300 ms animation when animated)
mockrecords the computed offset (MockWidget::scroll_offset), the unit-test probe

Nested scrolls reveal in the nearest enclosing scroll only; driving an outer scroll takes a second target aimed at it.