v0.6.1 (build 914)

Enable scroll: true or scroll: {axis: 'y'} on an element to make it a native scroll viewport. Its children are clipped to its bounds. Wheel/touchpad input and touch dragging update child layout bounds, keeping rendering, hover, clicks, and nested layout aligned. No JavaScript movement loop is required.

import * as UI from 'Helix/UserInterface';
const viewport = await UI.createElement(windowId, {
  scroll: {axis: 'y', wheelStep: 40, friction: 8},
  onScroll(event) {
    if (event.phase === 'end') console.log('Stopped at', event.scrollOffset);
  },
  child: [
    {item: {aabb: {min: {x: 0, y: 0}, max: {x: 300, y: 1200}}},
     renderable: {type: 'box', colour: [0.2, 0.3, 0.4, 1]}, contentAlign: 'fill'}
  ]
});
await UI.attachRoot(windowId, viewport);

The viewport keeps its allocated size. Its scroll limits are measured from its immediate children’s layout bounds and margins, after padding. Give a content child an explicit height/width larger than the viewport, or use flow rows with explicit item sizes. Grandchildren do not increase a content wrapper’s declared size automatically. Offsets are non-negative UI canvas units and clamp again when content or viewport sizes change. Native scrolling does not add scrollbars or elastic overscroll.

OptionDefault / meaning
enabledtrue when scroll is configured; scroll: false disables it and resets the offset.
axis'y'; 'x' or 'both' also supported.
wheelStep40 UI units per normalized wheel step; fractional steps are preserved.
friction8 per second; positive exponential velocity decay. Larger values stop sooner.
minVelocity5 UI units/second; inertia ends below this speed.
stopDelayMs100 ms; input inactivity ends a wheel burst or starts eligible inertia when an explicit stop is unavailable.

Touch dragging starts after the input movement threshold and can take over from a child press; that child receives cancellation and does not activate. Releasing a moving touch starts inertia; pausing before release suppresses a fling. Wayland finger-source scrolling uses axis_stop (per axis), with an inactivity fallback, to start inertia. Wheel steps apply immediately and stop after the idle interval. SDL wheel events do not reliably identify finger versus wheel source, so their deltas are applied without adding another inertia simulation. Direct touch dragging has native inertia on both platforms.

Native onScroll phaseMeaning
startA native scroll sequence starts; interactionId remains stable until end/cancel.
changeOffset changed through direct input or inertia.
endInput and inertia have settled.
cancelInterrupted by new input, explicit stop/reposition, configuration/handler change, owner teardown, focus loss, or device cancellation.

Native notifications have nativeScroll: true, scrollOffset, scrollLimit, velocity (UI units/second), and coasting. The existing scroll delta uses logical-window pixels (unit: pixel). Raw onScroll handlers on elements without native scrolling keep their phase-less delta events. Do not apply an extra content translation in a native observer: the engine already moved the children. Callback return values do not change native routing.

setScroll
Replace native scroll configuration. Reconfiguration cancels active interactions. Wrapper: element.setScroll(options).
UI.setScroll(windowId, elementId, boolean | ScrollOptions): Promise<boolean>
scrollTo
Cancel active scrolling and move immediately to a clamped offset. Does not start an input lifecycle. Wrapper: element.scrollTo(offset).
UI.scrollTo(windowId, elementId, {x, y}): Promise<boolean>
stopScroll
Cancel active scrolling and gestures in this subtree while preserving the current offset. Wrapper: element.stopScroll().
UI.stopScroll(windowId, elementId): Promise<boolean>
getScrollOffset
Read the current clamped offset. Wrapper: element.getScrollOffset().
UI.getScrollOffset(windowId, elementId): Promise<{x, y}>

Nested scroll containers choose the nearest surface that can move when input begins. A child’s ordinary onScroll handler still consumes its input. Native scrolling does not distribute unused delta or transfer momentum between containers. Additional touches during an active native drag are consumed until release. Use onTransform on a separate gesture surface for pinch/rotate interactions.

Native integrations use HelixScrollOptions and koya_ui_set_scroll, koya_ui_scroll_to, koya_ui_stop_scroll, and koya_ui_get_scroll_offset from helix_input.h. Native scroll notifications use the existing HELIX_INPUT_SCROLL callback and added HelixInputEvent state fields. Rebuild consumers of this header.

Lifecycle and rendering #

Inertia advances on native input deadlines and requests rendering frames through its final step, even when Koya is otherwise idle. Content limits refresh before inertia movement when layout changes. Hover is re-evaluated as content moves under a stationary cursor. end/cancel closes the interaction once; stopScroll emits cancel, not end. Destroying an owner cancels native scrolling. No physical mouse-out handler is needed.

Wayland pointer frames apply all final scroll values before axis stops, regardless of their wire order. Axis values and stops share the frame timestamp. Zero scroll deltas do not start a lifecycle or interrupt inertia; a later non-zero input sample still interrupts and chooses its scroll surface again.