Scrolling

Introduction

WIP. Scrolling is functional. Drag-and-drop has its data and effect types in place but the DragEnter / DragOver / DragLeave / Drop events aren't yet generated by the event loop. The page covers both subjects because they share the input pipeline.

Scrolling and drag are the two ways a pointer interacts with the layout rather than with a single element. Both are gestures: the framework tracks the mouse across multiple events, decides which gesture is in progress, and emits high-level events you handle the same way as any other event filter from events.

Making a node scrollable

CSS does the work. Set overflow to scroll, auto, or hidden on a node whose content exceeds its box, and the framework gives it scrollbars and wires the wheel/trackpad/touchpad events to it.

use azul::prelude::*;
let scroll_box = Dom::create_div()
    .with_css("width: 400px; height: 200px; overflow-y: scroll;");
  • visible (default). Children draw outside the box. No clip, no scrollbar, no Scroll events.
  • hidden. Children clip to the box. No scrollbar. No scrolling.
  • scroll. Children clip. Scrollbar always visible, even if not needed. Wheel scrolls.
  • auto. Children clip. Scrollbar visible only when content overflows. Wheel scrolls.

overflow-x and overflow-y set the axes independently. The shorthand overflow: scroll sets both.

The container's scroll-clip size is its inner box (border-box minus borders, padding, and scrollbar track). The content size is the total extent of its children. Scrolling shifts which slice of the content sits inside the clip.

What you don't get

  • No viewport-level scrolling. The window itself doesn't scroll. Make <body> or a descendant the scroll container.
  • No automatic scroll-into-view on focus. Call info.scroll_to(...) from a focus callback if you want this.

Reading scroll events

HoverEventFilter::Scroll fires for wheel/trackpad/touch scroll over a scrollable node. ScrollStart and ScrollEnd bracket a contiguous gesture so you can debounce work.

use azul::prelude::*;

extern "C" fn on_scroll(_: RefAny, _: CallbackInfo) -> Update {
    Update::DoNothing
}

fn build(data: RefAny) -> Dom {
    let mut node = Dom::create_div();
    node.add_callback(
        EventFilter::Hover(HoverEventFilter::Scroll),
        data,
        on_scroll,
    );
    node
}

Inside the callback, query the current scroll state through CallbackInfo:

impl CallbackInfo {
    pub fn get_scroll_offset(&self) -> Option<LogicalPosition>;
    pub fn get_scroll_offset_for_node(&self, dom_id: DomId, node_id: NodeId)
        -> Option<LogicalPosition>;
    pub fn get_scroll_state(&self, /* ... */) -> Option<ScrollState>;
    pub fn get_scroll_delta(&self) -> LogicalPosition;
    pub fn had_scroll_activity(&self) -> bool;
}

get_scroll_offset() returns the offset for the hit node, which is convenient inside a Scroll callback. ScrollState::scroll_position is the live position.

Scrolling programmatically

CallbackInfo::scroll_to queues a scroll that the runtime applies after the callback returns:

impl CallbackInfo {
    pub fn scroll_to(&mut self, /* ... */);
}

The position is in the scroll container's content space, so (0, 0) scrolls to the top-left of the content.

Hit-test order

Front-to-back. The topmost element under the cursor is hit first; deeper nodes are tried only if the topmost doesn't claim the event. A scroll routes to the topmost scrollable ancestor of the hit node, not to whichever scroll container the cursor happens to sit inside. This matches browser behaviour: a button's cursor: pointer overrides the container's cursor: text, and a <select> swallows wheel events that would otherwise scroll the page.

Smooth and momentum scrolling

The framework animates between scroll positions when scroll_to is called. Trackpad and wheel deltas are accumulated frame-to-frame; momentum drives the inertia phase after a fling. You don't manage the timer.

CSS -azul-overflow-scrolling: touch enables momentum on a node. overscroll-behavior: contain prevents scroll chaining to the parent.

Drag and drop

The drag system handles selection drags, scrollbar thumb drags, window moves, window resizes, OS file drops, and DOM-node drags. The framework handles the first four for you. Node and file drops are the variants you write callbacks for.

Marking a node draggable

Set the draggable attribute on the DOM node. The gesture manager sees it during hit-test and starts a drag when the user begins one.

use azul::prelude::*;
let card = Dom::create_div()
    .with_attribute(AttributeType::Draggable(true))
    .with_css("padding: 12px; background: #fef;");

Drag events

  • HoverEventFilter::DragStart (source node). Drag begins. Set drag data here.
  • HoverEventFilter::Drag (source node). Each cursor move during drag.
  • HoverEventFilter::DragEnd (source node). Drag ends, on drop or cancel.
  • HoverEventFilter::DragEnter (target node). Cursor enters a candidate drop zone.
  • HoverEventFilter::DragOver (target node). Cursor stays over a drop zone, throttled.
  • HoverEventFilter::DragLeave (target node). Cursor leaves a drop zone.
  • HoverEventFilter::Drop (target node). User releases on a drop zone that has accepted the drop.
  • HoverEventFilter::DroppedFile (hit node). OS file drop landed.

Each variant has a corresponding FocusEventFilter::* and WindowEventFilter::*. Use Hover for „this specific node is the drop zone“. Use Window to observe drags anywhere in the window.

Drag data: set on start, read on drop

DragData is a MIME-keyed payload that mirrors the W3C DataTransfer API:

impl CallbackInfo {
    /// W3C dataTransfer.setData(type, data). Call from a DragStart callback.
    pub fn set_drag_data(&mut self, /* ... */);

    /// W3C dataTransfer.types: visible during DragOver.
    pub fn get_drag_types(&self) -> /* ... */;

    /// W3C dataTransfer.getData(type): only readable inside the Drop handler.
    pub fn get_drag_data(&self, /* ... */) -> Option</* ... */>;
}

Multiple MIME types per drag are allowed. Set the same payload as text/plain for foreign drop targets and as your own application/x-myapp-task for the structured drop.

Accepting (or rejecting) a drop

Drop targets opt in. Inside a DragEnter or DragOver callback, inspect get_drag_types(). If the data is for you, call accept_drop() and set the drop effect:

impl CallbackInfo {
    /// Equivalent to event.preventDefault() in a W3C dragover handler.
    pub fn accept_drop(&mut self);

    /// W3C dataTransfer.dropEffect.
    pub fn set_drop_effect(&mut self, effect: DropEffect);
}

A node that doesn't call accept_drop() isn't a drop target. The cursor shows the no-drop indicator and Drop won't fire. This matches the W3C model.

Drop and DragEnd

Drop fires once on the accepted target with the drop position and the data. DragEnd fires once on the source, even if the drop was cancelled or rejected, so the source can finish the move/copy/link operation.

DropEffect and DragEffect

Two related enums:

  • DragEffect. What the source allows. Set on the drag context at DragStart. Variants: Uninitialized, None, Copy, CopyLink, CopyMove, Link, LinkMove, Move, All.
  • DropEffect. What the target chose. Set in DragOver or DragEnter. Variants: None, Copy, Link, Move.

A drop target's DropEffect must be in the source's DragEffect set, otherwise the drop is rejected.

Drag query methods

impl CallbackInfo {
    pub fn is_drag_active(&self) -> bool;
    pub fn is_dragging(&self) -> bool;
    pub fn is_node_drag_active(&self) -> bool;
    pub fn is_file_drag_active(&self) -> bool;
    pub fn get_drag_state(&self) -> Option<DragState>;
    pub fn get_drag_delta(&self) -> DragDelta;
    pub fn get_dragged_node(&self) -> Option<DomNodeId>;
}

DragState carries drag_type, source_node, current_drop_target, and file_path for file drops.

Drag-and-drop status

  • Done. DragData MIME map, DragEffect and DropEffect enums, drag-state query, and accept_drop, set_drop_effect, get_drag_types, get_drag_data on CallbackInfo.
  • Not done. DragEnter, DragOver, DragLeave, Drop event generation by the event loop. The filters exist; the loop doesn't synthesize the events. The Drop handler won't fire today.
  • Not done. Visual drag feedback (transform the source node to follow the cursor, opacity dim).
  • Not done. CSS pseudo-classes :dragging, :drag-over, :drag-over-invalid for styling source and target during a drag.

Code that follows the API described above will start working as the missing event-generation lands without needing a rewrite.

File drops from the OS

OS-level file drops work through the same drag pipeline. Your DroppedFile callback reads the file list:

use azul::prelude::*;

extern "C" fn on_dropped_file(_: RefAny, info: CallbackInfo) -> Update {
    if let Some(_path) = info.get_dropped_file() {
        // ... open the file ...
    }
    Update::RefreshDom
}

info.get_dropped_file() returns the dropped file path. info.get_hovered_file() returns the path while the file is being dragged over the window.

Cross-references

  • events: the event filter system this page builds on.
  • timers: scrolling momentum and drag auto-scroll run on reserved timers.

Coming Up Next

  • Events — Callbacks, event filters, and how state triggers relayout
  • Text Selection — Selection ranges, cursors, and copy/paste
  • Virtual Views — A node that materialises lazily, for infinite lists and embedded sub-DOMs

Back to guide index