Callbacks

DOM showed how to attach a callback to a node. The other side of the wire — what a callback receives, can read, can change, and returns — is what this page documents.

The callback signature

Every callback the framework invokes has the same C-compatible signature:

extern "C" fn(data: RefAny, info: CallbackInfo) -> Update
  • data is the RefAny you passed to with_callback. Downcast it to your concrete type to read or mutate application state.
  • info is a borrowed view into the framework's frame state — the hit node, the current input state, the layout result, and a handful of dispatch helpers.
  • Update tells the framework what to do next: nothing, re-run layout() for this window, or re-run for every window.
use azul::prelude::*;

struct Counter { value: i64 }

extern "C" fn on_click(mut data: RefAny, _info: CallbackInfo) -> Update {
    let mut c = match data.downcast_mut::<Counter>() {
        Some(c) => c,
        None => return Update::DoNothing,
    };
    c.value += 1;
    Update::RefreshDom
}

The extern "C" is mandatory. Callbacks are C function pointers, which is what makes the FFI bindings (Python, JavaScript, C#, …) work the same way.

The Update return

Update has three values:

Variant Meaning
DoNothing No re-layout, no re-render. Use this when the callback only mutates internal state that doesn't show up on screen yet.
RefreshDom Re-run layout() for the window the event came from. The framework reconciles old vs new tree.
RefreshDomAllWindows Re-run layout() for every open window. Use sparingly — pick this when the change touches global state that every window's layout reads.

If a single event fans out to multiple callbacks (e.g. propagation from child to parent), the framework takes the strongest Update across all of them.

Reading application state

RefAny::downcast_ref::<T>() and downcast_mut::<T>() recover the typed payload. The downcast checks the type id, so passing the wrong type returns None rather than reinterpreting memory.

extern "C" fn save(data: RefAny, _info: CallbackInfo) -> Update {
    let model = match data.downcast_ref::<AppModel>() {
        Some(m) => m,
        None => return Update::DoNothing,  // wrong RefAny type
    };
    write_to_disk(&*model);
    Update::DoNothing
}

For a callback that needs to mutate, use downcast_mut. The mutable borrow lasts the callback body.

Identifying the node that fired

let hit: DomNodeId = info.get_hit_node();           // DomId + NodeId
let rect = info.get_node_rect(hit);                 // optional
let css = info.override_node_css_properties(hit, …); // example mutation

A DomNodeId is (DomId, NodeId). Most apps have a single root DOM (DomId::ROOT_ID). Sub-DOMs come from IFrame nodes and virtual views.

For a generic callback that fires from many call sites — say a „submit“ callback that's attached to several forms — the dataset is the cleanest way to identify which instance fired:

let me = match info.get_dataset(info.get_hit_node()) {
    Some(d) => d,
    None => return Update::DoNothing,
};
let row = match me.downcast_ref::<TableRow>() {
    Some(r) => r,
    None => return Update::DoNothing,
};

get_node_id_of_root_dataset(search_key) walks up from the hit node to find the nearest ancestor whose dataset matches search_key. Useful for „click anywhere on this card“ patterns where the card root holds the dataset and the actual click landed on a label inside.

Reading input state

The framework hands you the input state at event time. Rules of thumb:

  • info.get_current_keyboard_state() — modifier keys, currently pressed scancodes, the chars the platform reports for the most recent key event.
  • info.get_current_mouse_state() — button state, scroll delta, whether the cursor is captured.
  • info.get_previous_keyboard_state() / get_previous_mouse_state() — the snapshot from the previous frame. Useful for transition detection (pressed_now && !pressed_last_frame == "just pressed").

For position queries:

info.get_cursor_position_screen()        // LogicalPosition relative to screen
info.get_cursor_relative_to_viewport()   // LogicalPosition in window coords
info.get_cursor_relative_to_node()       // (node_id, LogicalPosition) for the hit node

Mutating the DOM without rebuilding

You don't have to return RefreshDom for small changes. The framework exposes targeted mutations on info that go through a faster path:

  • info.change_node_text(node_id, text) — replaces the text content of a node.
  • info.change_node_image(node_id, image_ref, ...) — swaps an image.
  • info.set_css_property(node_id, prop) / override_node_css_properties(...) — set or override a CSS declaration without re-running the cascade for the whole tree.
  • info.change_node_image_mask(node_id, mask) — update a clip mask.

These produce a CallbackChange queued on the info; the framework applies them between the callback returning and the next paint. The restyle and damage-rect machinery covered in Reconciliation keeps the work proportional to what changed.

For structural edits (insert a child, delete a node), use insert_child_node and delete_node. Larger changes — anything beyond a handful of nodes — are usually clearer expressed as a fresh Dom from layout() plus Update::RefreshDom.

Focus, scroll, cursor

info.set_focus(FocusTarget::Node(node_id));         // focus a specific node
info.set_focus(FocusTarget::Path(/* ... */));       // by selector path
info.scroll_to(node_id, position, alignment);
info.scroll_node_into_view(node_id);
info.is_node_focused(node_id);
info.set_cursor_visibility(false);
info.start_cursor_blink_timer();

A focus change adjusts which node receives keyboard input on the next frame. The reconciler migrates the focus across a RefreshDom for nodes that match.

For text inputs and contenteditable surfaces, the cursor and selection helpers (add_cursor, add_selection_range, get_primary_selection, …) are documented separately in Text Selection.

Stopping propagation

Events bubble from the hit node to the root by default. Two opt-outs:

  • info.stop_propagation() — finish the current node's callbacks, then stop. Other callbacks attached to this node still run.
  • info.stop_immediate_propagation() — stop right now. No further callbacks at this node, no parents.

info.prevent_default() is the third opt-out: it tells the framework not to apply the built-in handling for the event (e.g. don't insert a character on KeyDown after your callback handled it). Browsers use the same name for the same idea.

Event filtering — EventFilter::Hover(...) vs Focus(...) vs Window(...), propagation order, NotEvent — is in Events and Input.

Async work: timers and threads

The callback runs on the UI thread. Any work it does blocks the next frame. For anything slow, schedule it.

let timer = Timer::new(/* interval */ 100.ms, refany.clone(), tick);
info.add_timer(TimerId::unique(), timer);

add_timer registers a recurring callback the framework drives on the main loop. The timer callback returns a TimerCallbackReturn { update, terminate } that controls both whether to re-run layout and whether the timer fires again.

For background work, add_thread spawns a worker thread tied to a RefAny. The thread sends messages back to a merge_callback on the main thread — the framework already understands cross-thread message delivery, so you don't need a manual mutex. See Background Tasks.

Window control

info.create_window(WindowCreateOptions::new(layout_fn));
info.close_window();                          // close the current window
info.modify_window_state(new_state);          // resize, retitle, fullscreen, ...
info.begin_interactive_move();                // start an OS-level drag
info.queue_window_state_sequence(states);     // animate state changes

Routing across pages is done with switch_route(pattern, params), which updates the active route and re-runs layout. Read the current route with get_route_pattern / get_route_param.

Image and font caches

info.add_image_to_cache("logo".into(), image_ref);
info.remove_image_from_cache("logo".into());
info.reload_system_fonts();

Cached images are addressable by name from any layout pass. Reloading system fonts is the right thing to do after a font config change (rare, but desktop environments do change font defaults at runtime).

Layout queries

CallbackInfo exposes the post-layout geometry of every node — the same data the renderer reads. Useful for hit-testing your own widgets or implementing „click on the row but only outside the buttons“:

info.get_node_size(node_id);            // LogicalSize
info.get_node_position(node_id);        // LogicalPosition (in viewport coords)
info.get_node_rect(node_id);            // LogicalRect = position + size
info.get_node_hit_test_bounds(node_id); // includes overflow padding
info.get_hit_node_rect();               // rect of the node that fired

For deeper tree walks, get_parent_node, get_first_child_node, get_all_children_nodes, and get_children_count give you the same hierarchy the framework uses internally.

Working with sub-DOMs

info.trigger_virtual_view_rerender(dom_id, node_id) re-runs the virtual-view callback for one specific sub-DOM. Use it when the virtual view's source data changed but the parent layout hasn't.

info.update_image_callback(dom_id, node_id) triggers a re-render of an ImageCallback-backed node — the GPU canvas pattern documented in SVG and Canvas.

A complete example

A „delete row“ button that lives inside a row's dataset, finds its row, removes it from the model, and refreshes:

use azul::prelude::*;

struct App { rows: Vec<String> }

#[repr(C)]
struct RowMarker { index: usize }

extern "C" fn on_delete(mut data: RefAny, mut info: CallbackInfo) -> Update {
    let mut app = match data.downcast_mut::<App>() {
        Some(a) => a,
        None => return Update::DoNothing,
    };

    let row_node = match info.get_node_id_of_root_dataset(
        RefAny::new(RowMarker { index: 0 }) // type-id only, value is ignored
    ) {
        Some(n) => n,
        None => return Update::DoNothing,
    };
    let marker = match info.get_dataset(row_node)
        .and_then(|d| d.downcast_ref::<RowMarker>().map(|r| r.index))
    {
        Some(i) => i,
        None => return Update::DoNothing,
    };

    if marker < app.rows.len() {
        app.rows.remove(marker);
    }

    Update::RefreshDom
}

The pattern — dataset on the row, generic callback that walks up to find its row marker, mutate the model, return RefreshDom — works for nearly every „this widget acts on its container“ interaction.

Coming Up Next

Back to guide index