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
datais theRefAnyyou passed towith_callback. Downcast it to your concrete type to read or mutate application state.infois 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.Updatetells the framework what to do next: nothing, re-runlayout()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
- Events and Input — Event filters, propagation, NotEvent
- Background Tasks — Timers, threads, and merge callbacks
- Datasets — Per-node state and the navigation patterns that read it
- Reconciliation — How RefreshDom maps old to new