Skip to content

Commit 501b59e

Browse files
authored
Two-axis canvas scrolling with axis-aware routing (#190)
* Make canvas scroll state and routing two-axis - Widen ScrollState to eight per-axis fields (ScrollAxisState carries the physics), route each wheel axis independently to the nearest ancestor scrollable on that axis, and step kinetic motion per axis. - Add the scroll axis declaration (attr 86 axis, attr 87 value-x, Widget.scroll_axes/value_x) through schema, markup validation, both engines, layout, clamping, reconcile, semantics, and the two-bar scrollbar renderer. - Break the one-axis on-scroll record consciously: the reflect vocabulary, contract class, and both engines now teach the per-axis migration by field name, and the scroll-driver ABI/journal carry offset_x. * Carry both scroll axes through hosts, docs, and the TS SDK - Widen the macOS native scroll driver ABI to offset_x with a horizontal overlay scroller; GTK and Windows wheel handlers already forward delta_x. - Update the TS SDK ScrollState to the eight per-axis fields and regenerate the markup vocabulary and scroll reference for axis/value-x. - State the ScrollState break and its one-sentence migration in the changelog fragment. * Pin two-axis scrolling with routing, driver, replay, and markup tests - Cover the nested independent-axis routing case (dy to the vertical child, dx to the horizontal ancestor), the horizontal wheel/keymap/scrollbar paths, and per-axis driver sync with pinned ranges on ungranted axes. - Ride a diagonal wheel through the reference record/replay session so a journaled two-axis scroll replays to identical per-axis offsets. - Migrate the example apps to the two-axis ScrollState fields. * Give the soundboard detail page a horizontal collection shelf - Rail every other album under the track list on an axis="horizontal" scroll region: the grid's quiet-hover cover tiles, one press from record to record. - Echo the shelf's offset_x into value_x (the controlled-scroll shape on the sideways axis) and reset the rail when an album opens. - Teach the widget-wheel automation verb an optional delta-x token so drivers can scroll both axes like a real trackpad gesture. * Harden two-axis scrolling per external review - Keep the vertical scrollbar's thickness formula byte-identical (per-axis derivation), exclude anchored/clip-scope/concealed-disclosure subtrees from horizontal extents, and stop region-anchored surfaces riding scrolled content. - Route macOS native wheel gestures by dominant-axis capability with an engine-wire fallback, and rewrite a queued driver report when a programmatic offset lands. - Pick scroll semantics by live range on both-axes regions, page every granted axis on assistive steps, carry fling velocity per axis only while its offset survived, refine the axis/virtualized validation to the horizontal grant, and migrate bench_render. * Split residual wheel axes on macOS and arm driver chrome per grant - Forward the axis a locked native driver cannot travel to the wire, so one diagonal gesture scrolls a vertical list natively while its delta_x reaches the horizontal ancestor through per-axis routing. - Carry the axis grants on the driver spec: elasticity and scroller chrome arm only on granted axes, so a horizontal-only region can never bounce vertically or report an offset the runtime would fight. - Accept the optional delta-x token in the automation CLI, and warn at the builder seam when a DYNAMIC value pairs a horizontal grant with virtualization or value_x with a vertical-only region (both engines share the seam, so the diagnostic stays engine-uniform where validation cannot see the resolved value). * Route every macOS wheel through the axis splitter and split set_offset per axis - Stop the driver view claiming wheel events at hit test: the surface's dominant-axis selection, gesture lock, and residual-axis split now own all wheel routing, so a diagonal gesture over a nested vertical list cannot swallow the horizontal component its ancestor owns. - Make the driver set-offset flag per axis end to end, so a programmatic vertical write can never push a stale horizontal offset over native motion whose coalesced report is still in flight. - Teach the a11y focus-reachability audit the axis grants (offscreen-right tiles on a horizontal shelf are reachable, below-viewport content there is not) and bump the model-contract format to 2 - a format-1 artifact classified the retired one-axis scroll record as a scroll_state payload, which would be a false pass today. * Pin the axis-aware focus-reachability audit - A horizontal shelf's offscreen-right tile is reachable by design while a below-viewport button inside it stays a finding. * Match native wheel routing to the engine walk and step the live axis - Route each gesture axis to the deepest native driver that can consume its delta right now (direction-aware, so a saturated inner region hands an outward swipe to its ancestor), falling back to the outermost elastic region — rubber-band regions bounce even with short content — and reconcile the driver array in layout pre-order so keyed reorders keep the walk honest. - Forward a residual axis only when the locked driver can neither move nor bounce on it. - Assistive increment/decrement on a both-axes region whose only live axis is horizontal now step sideways instead of paging a zero-range vertical axis; pinned through the accessibility action path. * Resolve wheel owners per axis with measured residuals and axis-keyed reach latches - Replace the single-winner gesture lock with per-axis owner resolution at the gesture's anchor point, re-evaluated per event against live scroller offsets: saturation hands an axis to its ancestor (elastic-take never outranks a consumer), cross-owner diagonals whose native recipient would eat the other axis ride the wire whole, and the residual is MEASURED (delta minus what the scroller actually absorbed) so a nearly saturated region can never double-spend. - Flush the coalesced driver report before any residual or cross-owner wire emission (offsets first, one clock), and route wheels over a visible overlay scroller through the same splitter instead of the knob. - Key reach-end/reach-start hysteresis latches by (id, axis) so a region whose primary axis changes re-arms honestly. * Flush driver offsets before the no-owner wire hand-off too - Every wire emission now rides the offsets-first clock, including wheel events no native region owns. * Size the collection shelf to actually overflow at desktop widths - 168-point tiles keep seven sibling covers wider than the regular shell's content row, so the rail scrolls where it ships. * Close the native-seam races and honesty gaps from the cycle-2 review - Restrict wheel-owner resolution to the hit region's ancestor chain (parent driver ids through the ABI), anchor wire hand-offs at the gesture point, carry sub-half-point residuals across events, and wire-bind any region the engine scrolled this gesture so a later absolute native report can never erase relative wire motion. - Skip subtrees anchored directly to the region in every VERTICAL extent walker (a stationary surface counted against a moving offset grew the range without bound), decide the assistive step axis from the semantics metrics (concealed-disclosure and anchored exclusions included), and page exactly one axis on widget-level semantic steps. - Flag content stranded before a horizontal region's origin in both audits (offsets clamp at zero), double the reach-latch capacity for the (id, axis) key space, and fire reach signals only when the latch stores. * Treat each discrete wheel as its own gesture - Legacy (phase-less) events reset the wire bindings and residual carries per event, so a binding can never demote a region to the wire forever. * Scope phase-less wheel gesture state to bursts, not events - A quiet gap (250ms, well past the input queue's one-frame coalescing) is the gesture boundary for discrete streams: residual carries keep accumulating sub-half-point deltas and wire bindings outlive any in-flight relative hand-off, while a fresh burst still resets both so no binding lives forever. * Honor overlays, phases, and revoked axes on the native wheel seam - Push OCCLUDER rects (anchored floating surfaces at their frames, modal catchers as the whole view) with per-driver masks through the driver sync, so the host's geometric wheel routing declines exactly the points the engine's hit test would give to an overlay's branch; drivers inside the overlay stay exempt. Pinned through the null platform. - Forward zero-delta phase events (begins, the terminal Ended/Cancelled) to the gesture's last native recipient so the scroller's overscroll bookkeeping always terminates, and scope residual carries to the hit region so fractional motion never leaks onto whatever chain the pointer wanders to next. - Pin REVOKED axes home on native-driven regions too (the range clamp stays the OS scroller's), so an axis flip behaves identically on every host instead of resurrecting a stale echoed offset on re-grant. * Make native wheel routing engine-exact: one owner or the wire - One owner takes the WHOLE event natively and clamps at the edge exactly like the engine's consume-and-drop rule; split owners, dead axes, and wire-scrolled owners ride the wire whole — the residual measurement, fractional carries, and partial-clamp forwarding are gone, and consumption uses the engine's exact bounds. - Zero-delta phase events forward only the gesture bookkeeping to the last native recipient; nonzero no-owner deltas go wire-only, so an overlay opening mid-momentum can no longer keep moving the obscured region. - Fix the occluder model on every reported edge: view-local modal rects, tooltip passthrough, scrim=false previews, hidden/concealed surfaces, render transforms, self- and paint-order exemptions for anchored scroll regions, and a fail-safe whole-view occluder when surfaces outnumber the budget; both-axes semantic steps read child-frame range so the stepped axis matches the exposed semantics. * Carry the two-axis scroll state through corewire's ABI seam - dispatch_scroll_state now carries the eight per-axis scalars in declaration order; the retired one-axis quartet rides the generic record entry - pin the new routing and the quartet's retirement in emit tests, and match the stub core's export to the widened symbol
1 parent 7563a4e commit 501b59e

78 files changed

Lines changed: 3366 additions & 540 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎changelog.d/horizontal-scroll.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
feature: **Horizontal and two-axis canvas scrolling**: scroll views declare `axis="vertical|horizontal|both"` (builder `axis:`), horizontal offsets ride `value-x` with the same source-wins reconcile as `value`, the engine draws a bottom-edge scrollbar, keyboard scrolling gains Left/Right/Home/End on horizontal-capable regions, and macOS native scroll drivers carry both axes with OS momentum and rubber-band.
2+
- **Independent per-axis wheel routing**: each axis of a wheel/trackpad gesture travels to the nearest ancestor scrollable on that axis, so a horizontal timeline holding a vertical list splits a diagonal gesture — `delta_y` scrolls the list, `delta_x` reaches the timeline.
3+
- **BREAKING — `ScrollState` is two-axis now**: the one-axis `{offset, velocity, viewport_extent, content_extent}` record (TS: `offset`/`velocity`/`viewportExtent`/`contentExtent`) was replaced by per-axis fields `offset_x`/`offset_y`, `velocity_x`/`velocity_y`, `viewport_extent_x`/`viewport_extent_y`, `content_extent_x`/`content_extent_y` (TS: `offsetX`…`contentExtentY`); migrate a vertical region by reading the `_y` fields where it read the old ones — an `on-scroll` arm still declaring the old shape fails the build with a teaching that names the new fields.

‎docs/src/app/automation/page.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,8 @@ The runtime watches the command queue and processes these actions:
130130
<td>Dispatch pointer down/drag/up across a retained canvas widget</td>
131131
</tr>
132132
<tr>
133-
<td><code>widget-wheel &lt;view-label&gt; &lt;widget-id&gt; &lt;delta-y&gt;</code></td>
134-
<td>Dispatch wheel input at a retained canvas widget</td>
133+
<td><code>widget-wheel &lt;view-label&gt; &lt;widget-id&gt; &lt;delta-y&gt; [&lt;delta-x&gt;]</code></td>
134+
<td>Dispatch wheel input at a retained canvas widget (the optional <code>delta-x</code> scrolls the horizontal axis)</td>
135135
</tr>
136136
<tr>
137137
<td><code>widget-key &lt;view-label&gt; &lt;key&gt; [&lt;text&gt;]</code></td>

‎docs/src/app/cli/page.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,8 +229,8 @@ Interact with the automation server of a running automation-enabled app. See [Au
229229
<dd>Invoke a widget's declared context-menu item by index — the snapshot lists each widget's items in order.</dd>
230230
<dt><code>automate widget-drag &lt;view-label&gt; &lt;widget-id&gt; &lt;start-x-ratio&gt; &lt;end-x-ratio&gt; [&lt;start-y-ratio&gt; &lt;end-y-ratio&gt;]</code></dt>
231231
<dd>Dispatch pointer down/drag/up across a retained canvas widget.</dd>
232-
<dt><code>automate widget-wheel &lt;view-label&gt; &lt;widget-id&gt; &lt;delta-y&gt;</code></dt>
233-
<dd>Dispatch wheel input at a retained canvas widget.</dd>
232+
<dt><code>automate widget-wheel &lt;view-label&gt; &lt;widget-id&gt; &lt;delta-y&gt; [&lt;delta-x&gt;]</code></dt>
233+
<dd>Dispatch wheel input at a retained canvas widget; the optional <code>delta-x</code> scrolls the horizontal axis (each axis routes to the nearest region that scrolls it).</dd>
234234
<dt><code>automate widget-key &lt;view-label&gt; &lt;key&gt; [&lt;text&gt;]</code></dt>
235235
<dd>Dispatch key input to the focused retained canvas widget.</dd>
236236
<dt><code>automate widget-pinch &lt;view-label&gt; &lt;scale&gt; [&lt;x&gt; &lt;y&gt;]</code></dt>

‎docs/src/app/components/scroll/page.mdx‎

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import { CodeToggle } from "@/components/code-toggle";
44

55
# Scroll
66

7-
A scroll view: wrap multiple children in a single column inside it. The engine owns wheel, kinetic, and keyboard scrolling and draws the scrollbar while a scroll is in flight; `on-scroll` names a Msg variant with a `canvas.ScrollState` payload — or, in a transpiled TypeScript core, a declared record of the same `offset`/`velocity`/`viewport_extent`/`content_extent` fields, matched by name — that delivers the post-scroll offset and viewport/content extents, so the model can observe position without owning it. Echo the offset into a model field bound as `value` and the model owns the position too: setting the field scrolls the region (the controlled-scroll shape). Scrolling pins at the content edges by default — no rubber-band bounce; kinetic motion stops cleanly at the boundary. `overscroll="rubber_band"` opts one region into bouncing past its edges (both the engine physics and the native macOS scroller honor it), and the `ScrollPhysics.overscroll` design token flips the app-wide default, which per-region values override. `on-reach-end` dispatches a plain Msg when a scroll comes within one viewport of the content end — the infinite-fetch signal, fired once per approach with hysteresis (appending a batch grows the extent and re-arms the next approach). A programmatic jump to the end fires once and never re-arms while the offset stays near the end — re-arming needs a post-scroll observation at least 1.5 viewports from it. Pair with [list](/components/list) for layout-culled rows, or the builder's [virtual list](/components/virtual-list) for dataset-scale windows.
7+
A scroll view: wrap multiple children in a single column inside it. The engine owns wheel, kinetic, and keyboard scrolling and draws the scrollbar while a scroll is in flight; `on-scroll` names a Msg variant with a `canvas.ScrollState` payload — or, in a transpiled TypeScript core, a declared record of the same two-axis fields (`offset_x`/`offset_y`, `velocity_x`/`velocity_y`, `viewport_extent_x`/`viewport_extent_y`, `content_extent_x`/`content_extent_y`), matched by name — that delivers the post-scroll offsets and viewport/content extents on both axes, so the model can observe position without owning it. Echo `offset_y` into a model field bound as `value` and the model owns the position too: setting the field scrolls the region (the controlled-scroll shape).
8+
9+
`axis` declares which axes the region scrolls: `vertical` (the default), `horizontal`, or `both`. A horizontal grant opts the region into wheel/trackpad `delta_x`, a bottom-edge scrollbar, and the horizontal keymap — Left/Right step in lines, and a horizontal-only region takes Home/End/PageUp/PageDown on its one axis too. The horizontal offset rides `value-x`, the sideways counterpart of `value` with the same source-wins reconcile. Nested regions route each wheel axis independently: every axis of a scroll gesture travels to the nearest ancestor that scrolls on that axis, so inside a horizontal timeline holding a vertical list, `delta_y` scrolls the list while `delta_x` reaches the timeline — one diagonal gesture, two regions, no fighting. Virtualized scrolls stay vertical (windowed virtualization prices rows, not columns). Scrolling pins at the content edges by default — no rubber-band bounce; kinetic motion stops cleanly at the boundary. `overscroll="rubber_band"` opts one region into bouncing past its edges (both the engine physics and the native macOS scroller honor it), and the `ScrollPhysics.overscroll` design token flips the app-wide default, which per-region values override. `on-reach-end` dispatches a plain Msg when a scroll comes within one viewport of the content end — the infinite-fetch signal, fired once per approach with hysteresis (appending a batch grows the extent and re-arms the next approach). A programmatic jump to the end fires once and never re-arms while the offset stays near the end — re-arming needs a post-scroll observation at least 1.5 viewports from it. Pair with [list](/components/list) for layout-culled rows, or the builder's [virtual list](/components/virtual-list) for dataset-scale windows.
810

911
<ComponentPreview name="scroll" alt="A scroll region rendered by the engine" caption="a fixed-height scroll region; the engine draws the scrollbar during scrolling" />
1012

@@ -24,7 +26,7 @@ A scroll view: wrap multiple children in a single column inside it. The engine o
2426
</scroll>
2527
```
2628

27-
The arm receives the scroll state and stores what the model wants to remember — echo `offset` into a field bound as the scroll's `value` and the model owns the position too:
29+
The arm receives the scroll state and stores what the model wants to remember — echo `offsetY` into a field bound as the scroll's `value` (and `offsetX` into `value-x` on horizontal-capable regions) and the model owns the position too:
2830

2931
<CodeToggle>
3032

@@ -34,16 +36,28 @@ import { type ScrollState } from "@native-sdk/core/events";
3436
export type Msg = /* ... */ | { readonly kind: "log_scrolled"; readonly scroll: ScrollState };
3537
// in update:
3638
case "log_scrolled":
37-
return { ...model, changelog_offset: msg.scroll.offset };
39+
return { ...model, changelog_offset: msg.scroll.offsetY };
3840
```
3941

4042
```zig
4143
// Msg arm: log_scrolled: canvas.ScrollState
42-
.log_scrolled => |scroll| model.changelog_offset = scroll.offset,
44+
.log_scrolled => |scroll| model.changelog_offset = scroll.offset_y,
4345
```
4446

4547
</CodeToggle>
4648

49+
A horizontal shelf declares its axis and reads the `X` fields:
50+
51+
```html
52+
<scroll axis="horizontal" height="180" on-scroll="shelf_scrolled">
53+
<row gap="8">
54+
<panel width="140"><text>Cover 1</text></panel>
55+
<panel width="140"><text>Cover 2</text></panel>
56+
<panel width="140"><text>Cover 3</text></panel>
57+
</row>
58+
</scroll>
59+
```
60+
4761
## Programmatic construction (Zig)
4862

4963
In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically. `on_scroll` pairs with `Ui.scrollMsg(.tag)`; the delivered offset is the value the runtime already applied, so echoing it back never fights the scroll reconcile.
@@ -62,4 +76,4 @@ ui.scroll(.{ .height = 240, .padding = 8, .on_scroll = Ui.scrollMsg(.log_scrolle
6276

6377
## Attributes
6478

65-
<AttrTable attrs={["on-scroll", "on-reach-end", "overscroll", "height", "width", "padding"]} />
79+
<AttrTable attrs={["on-scroll", "on-reach-end", "axis", "value-x", "overscroll", "height", "width", "padding"]} />

‎docs/src/lib/component-vocab.json‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,14 @@
432432
"name": "overscroll",
433433
"doc": "scroll only: edge behavior of the region. none pins scrolling at the content edges (the shipped default via the ScrollPhysics.overscroll token), rubber_band lets this region bounce past them, default follows the token. Honored by the engine's scroll physics and the native OS scroller alike."
434434
},
435+
{
436+
"name": "axis",
437+
"doc": "scroll only: which axes the region scrolls - vertical (the default), horizontal, or both. Horizontal grants opt the region into wheel/trackpad delta-x, the bottom-edge scrollbar, and the horizontal keymap; in a nested tree each wheel axis routes independently to the nearest ancestor scrolling that axis. Virtualized scrolls stay vertical (a horizontal grant there is a teaching error)."
438+
},
439+
{
440+
"name": "value-x",
441+
"doc": "scroll only, beside axis=\"horizontal\" or axis=\"both\": the horizontal scroll offset - the sideways counterpart of value, following the same source-wins reconcile rule (echo on-scroll's offset_x back to keep user scrolling; move it model-side to scroll programmatically). Without a horizontal axis grant it is a teaching error (it would be silently inert)."
442+
},
435443
{
436444
"name": "resize-duration",
437445
"doc": "split only: layout-tween duration in milliseconds (a plain number or one {binding}). Nonzero makes the bound value a TARGET - a rebuild that moves it lays both panes out at the target ONCE, then the runtime slides the rendered boundary there one presented frame at a time under the panes' clips (content never re-wraps mid-flight), dispatching ONE on-resize echo at settle with the applied fraction. 0 (and absent) snaps, today's behavior. A divider DRAG keeps live per-step reflow and echoes. Reduced-motion appearances snap automatically - apps declare nothing extra."
@@ -504,7 +512,7 @@
504512
},
505513
{
506514
"name": "on-scroll",
507-
"doc": "scroll element only: names a Msg variant with canvas.ScrollState payload; delivers the post-scroll offset/viewport/content extents after wheel, kinetic, keyboard, and accessibility scrolls."
515+
"doc": "scroll element only: names a Msg variant with canvas.ScrollState payload; delivers the post-scroll two-axis state (offset_x/offset_y, velocity_x/velocity_y, viewport_extent_x/viewport_extent_y, content_extent_x/content_extent_y) after wheel, kinetic, keyboard, and accessibility scrolls."
508516
},
509517
{
510518
"name": "on-dismiss",

‎examples/ai-chat-ts/src/core.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,7 @@ export function update(model: Model, msg: Msg): Model | [Model, Cmd<Msg>] {
390390
// The controlled-scroll echo: the applied offset lands in the
391391
// model, so the next rebuild's `value` binding never fights the
392392
// runtime.
393-
return { ...model, chatScrollTop: msg.scroll.offset };
393+
return { ...model, chatScrollTop: msg.scroll.offsetY };
394394
case "endpoint_set":
395395
return { ...model, endpoint: msg.value };
396396
case "model_set":

‎examples/feed/src/tests.zig‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,8 +226,10 @@ const Harness = struct {
226226
try self.harness.runtime.dispatchAutomationCommand(self.app, command);
227227
}
228228

229-
fn scrollState(self: *Harness) canvas.ScrollState {
230-
return self.harness.runtime.views[0].canvasWidgetScrollStateById(timeline_id).?;
229+
/// The timeline's VERTICAL axis state — the feed is a vertical
230+
/// virtual list, so every assertion here reads that axis.
231+
fn scrollState(self: *Harness) canvas.ScrollAxisState {
232+
return self.harness.runtime.views[0].canvasWidgetScrollStateById(timeline_id).?.axis(.vertical);
231233
}
232234

233235
fn root(self: *Harness) canvas.Widget {

‎examples/gpu-components/src/app.zig‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -721,7 +721,7 @@ pub const GpuComponentsApp = struct {
721721
};
722722
}
723723

724-
fn componentVirtualScrollState(self: *@This(), id: canvas.ObjectId, viewport_extent: f32, content_extent: f32) ?canvas.ScrollState {
724+
fn componentVirtualScrollState(self: *@This(), id: canvas.ObjectId, viewport_extent: f32, content_extent: f32) ?canvas.ScrollAxisState {
725725
const offset = self.componentVirtualScrollValue(id) orelse return null;
726726
const velocity = self.componentVirtualScrollVelocity(id) orelse return null;
727727
return .{
@@ -754,7 +754,7 @@ pub const GpuComponentsApp = struct {
754754
}
755755
}
756756

757-
fn setComponentVirtualScrollState(self: *@This(), id: canvas.ObjectId, state: canvas.ScrollState) anyerror!void {
757+
fn setComponentVirtualScrollState(self: *@This(), id: canvas.ObjectId, state: canvas.ScrollAxisState) anyerror!void {
758758
switch (id) {
759759
120 => {
760760
self.virtual_scroll.nav = state.offset;

‎examples/gpu-components/src/scene.zig‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,7 @@ pub fn clampComponentVirtualScrollOffset(raw_next: f32, max_offset: f32, fallbac
455455
return std.math.clamp(@max(0, raw_next), 0, @max(0, max_offset));
456456
}
457457

458-
pub fn componentScrollStatesEqual(a: canvas.ScrollState, b: canvas.ScrollState) bool {
458+
pub fn componentScrollStatesEqual(a: canvas.ScrollAxisState, b: canvas.ScrollAxisState) bool {
459459
return a.offset == b.offset and
460460
a.velocity == b.velocity and
461461
a.viewport_extent == b.viewport_extent and

‎examples/gpu-dashboard/src/main.zig‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ pub fn update(model: *Model, msg: Msg) void {
278278
// runtime already applied, so echoing it back through
279279
// `.value = model.activity_scroll` on the next rebuild never
280280
// fights the scroll reconcile rule.
281-
.activity_scrolled => |scroll_state| model.activity_scroll = scroll_state.offset,
281+
.activity_scrolled => |scroll_state| model.activity_scroll = scroll_state.offset_y,
282282
.submit_forecast => model.setStatus("Forecast amount submitted."),
283283
.submit_search => model.setStatus("Segment search submitted."),
284284
.chrome_changed => |chrome| model.chrome_leading = chrome.insets.left,

0 commit comments

Comments
 (0)