Events
Overview
Every interactive callback in azul reaches the user the same way: the
platform shell pushes a raw OS event into FullWindowState, the input
interpreter turns it into one or more SyntheticEvents plus
framework-internal SystemChanges, the dispatcher walks the DOM in
capture/target/bubble order to collect matching EventFilters, the user
callbacks run against the matched nodes, and the unprevented default
actions are applied last.
This page covers event types, filter taxonomy, propagation, and default
actions. The cursor-to-node routing — how a viewport pixel becomes a
DomNodeId plus its scroll context, cursor icon, and selection regions —
lives in Hit Testing.
OS event -> FullWindowState diff -> SyntheticEvent
|
v
default_input_interpreter() (events.rs)
|-> SystemChange (apply_system_change -> managers)
`-> user_events
|
v
dispatch_events_propagated() (event.rs)
|-> event_type_to_filters() (events.rs)
`-> propagate_event() (events.rs)
|-> Capture
|-> Target
`-> Bubble
|
v
Callback returns Update
|
v
determine_keyboard_default_action() (default_actions.rs)
default_post_filter() (events.rs)
Pipeline order in process_events
The shell entry point PlatformWindowV2::process_events (in
dll/src/desktop/shell2/common/event.rs) executes the steps below for
every input batch:
- State diff. The shell mutates
current_window_statewith raw input. Diffing it againstprevious_window_stateproducesSyntheticEvents for cursor moves, button transitions, key presses, focus, theme changes, etc. - Manager events. Managers that need temporal context
(
GestureManager,ScrollManager,CursorManager,TextEditManager) implementEventProvider::get_pending_eventsand contribute additionalSyntheticEvents. - Pre-callback filter.
default_input_interpreter(overridable viaInputInterpreterCallbackonLayoutWindow) folds those events into aPreCallbackFilterResult { system_changes, user_events }. System changes are applied immediately (focus, scroll, drag activation, selection updates). - Dispatch.
dispatch_events_propagated(&user_events)runs each event throughpropagate_eventand invokes the plannedCoreCallbacks. Callbacks returnUpdateand may callevent.prevent_default(),stop_propagation(), orstop_immediate_propagation(). - Default actions. If no callback prevented default and any
KeyDownwas in the batch,determine_keyboard_default_actionreturns aDefaultActionResult. Tab/Shift+Tab/Home+Ctrl/End+Ctrl/Escape are converted viadefault_action_to_focus_targetand applied throughSystemChange::SetFocus. Enter/Space on activatable elements synthesise aClickevent and re-enter dispatch. - Post filter.
default_post_filter(overridable viaPostFilterCallback) inspects(prevent_default, pre_changes, old_focus, new_focus)and emits finalSystemChanges. It clears selections on focus change, finalises IME composition state, and scrolls the new focus into view.
The dispatch loop recurses up to a fixed MAX_EVENT_RECURSION_DEPTH so
that Update::RefreshDom returned from a callback rebuilds the DOM,
runs lifecycle reconciliation, and re-enters event delivery for
synthetic Mount/Unmount/Resize events.
SyntheticEvent
pub struct SyntheticEvent {
pub event_type: EventType,
pub source: EventSource, // User | Programmatic | Synthetic | Lifecycle
pub phase: EventPhase, // Capture | Target | Bubble
pub target: DomNodeId,
pub current_target: DomNodeId, // updated as propagation walks the path
pub timestamp: Instant,
pub data: EventData, // Mouse | Keyboard | Scroll | Touch | Clipboard | Lifecycle | Window | None
pub stopped: bool,
pub stopped_immediate: bool,
pub prevented_default: bool,
}
The source field is load-bearing. EventSource::Lifecycle
short-circuits propagation, since propagate_target_phase is the only
phase used. EventSource::Synthetic events generated by the framework
(e.g. activation clicks) re-enter dispatch as if they were user events.
EventSource::Programmatic is set on API-driven scrolls and focus
changes so that scroll callbacks can distinguish.
stop_propagation() halts the current phase boundary. Capture stops
before target, and target stops before bubble. stop_immediate_propagation()
additionally drops remaining handlers on the current node.
prevent_default() only suppresses the post-dispatch default action; it
doesn't stop callback delivery.
EventType and EventData
EventType is the W3C-aligned superset: mouse, keyboard, IME
composition, focus, input/change/submit, scroll, drag, touch, gesture,
clipboard, media, lifecycle, window, application, file. EventData
carries type-specific payloads (MouseEventData, KeyboardEventData,
ScrollEventData, TouchEventData, ClipboardEventData,
LifecycleEventData, WindowEventData, or None). The data variant
must match the event type. dispatch_events_propagated doesn't validate
this; callers (the input interpreter and manager get_pending_events
impls) are responsible.
KeyModifiers { shift, ctrl, alt, meta } is duplicated inside
MouseEventData and KeyboardEventData rather than read from
KeyboardState because gesture managers may produce events with stale
modifier snapshots.
EventFilter taxonomy
Callbacks are registered against one of five filter categories:
- Hover.
Hover(HoverEventFilter)fires on nodes hit by the cursor and uses W3C capture/target/bubble through the DOM path. - Focus.
Focus(FocusEventFilter)fires on the focused node only. There's no propagation, and the node must be in the tab order. - Window.
Window(WindowEventFilter)fires on every node with a matching callback. It's a brute-force fan-out across all DOMs. - Component.
Component(ComponentEventFilter)fires on the reconciler's target node. Variants areAfterMount,BeforeUnmount,Updated, andNodeResized. - Application.
Application(ApplicationEventFilter)behaves like Window. It's reserved for monitor-connect/disconnect-style events.
EventFilter::Not exists in the type but matches_filter_phase returns
false for it. Registering a Not filter today never fires.
From<On> for EventFilter routes the public On enum to the right
category. Two cases are non-obvious:
On::TextInputbecomesFocus(TextInput). Text input is delivered to whatever currently owns focus, not to whichever node was hit.On::VirtualKeyDownandOn::VirtualKeyUpbecomeWindow(VirtualKeyDown/Up). Keyboard events fan out window-wide so layout-driven shortcuts can register on the root.
event_type_to_filters
pub fn event_type_to_filters(event_type: EventType, event_data: &EventData) -> Vec<EventFilter>;
One incoming event can match several filters. EventType::MouseDown
with a MouseEventData { button: Left, .. } returns both the generic
Hover(MouseDown) and the button-specific Hover(LeftMouseDown).
EventType::Click only matches Hover(LeftMouseDown) because W3C says
click is left-button only. Drag events fan out to both Hover(...) and
Window(...) so a global drop handler on the root works.
This function is the single source of truth for the dispatch plan.
propagate_event uses it implicitly by reading the per-node filter
list.
Capture/target/bubble in propagate_event
pub fn propagate_event(
event: &mut SyntheticEvent,
node_hierarchy: &NodeHierarchy,
callbacks: &BTreeMap<NodeId, Vec<EventFilter>>,
) -> PropagationResult;
The path is computed by walking parent pointers from the target to
the root, then reversed:
- Capture phase: ancestors in root-to-target order (excluding the
target itself), with
event.phase = EventPhase::Capture. - Target phase: the target node alone, with
event.phase = EventPhase::Target. - Bubble phase: ancestors in target-to-root order, with
event.phase = EventPhase::Bubble.
Each phase iterates nodes; if event.stopped_immediate is set the loop
returns immediately, if event.stopped is set the next phase is
skipped. current_target is rewritten to the visiting node before each
filter check.
Today matches_filter_phase does not consult current_phase, so a
registered Hover(MouseDown) callback fires once per phase the node
appears in. The phase enum is exposed for future per-phase registration;
do not assume otherwise when reading callback counts.
PropagationResult { callbacks_to_invoke: Vec<(NodeId, EventFilter)>, default_prevented } is the dispatch plan returned to the shell.
default_prevented is the OR of every event.prevented_default flip
during propagation.
Filter matchers
matches_filter_phase dispatches to four predicates:
matches_hover_filter, matches_focus_filter, matches_window_filter,
and matches_component_filter. Each is a long match table mapping
filter variant by EventType to a boolean. Mouse-button filters
(LeftMouseDown etc.) call check_mouse_button(&event.data, expected)
to confirm EventData::Mouse(_).button matches.
Gesture variants (LongPress, SwipeLeft, PinchIn,
RotateClockwise, ...) and the IME composition variants are present in
the enums but the matchers currently fall through _ => false for them
in the hover/focus/window paths. GestureManager handles those events
through its own dispatch, not through propagate_event.
Default actions
pub enum DefaultAction {
FocusNext, FocusPrevious, FocusFirst, FocusLast, ClearFocus,
ActivateFocusedElement { target: DomNodeId },
SubmitForm { form_node: DomNodeId },
CloseModal { modal_node: DomNodeId },
ScrollFocusedContainer { direction: ScrollDirection, amount: ScrollAmount },
SelectAllText,
None,
}
The function that produces them is in the layout crate (it needs the
styled DOM to query is_activatable and is_text_input):
pub fn determine_keyboard_default_action(
keyboard_state: &KeyboardState,
focused_node: Option<DomNodeId>,
layout_results: &BTreeMap<DomId, DomLayoutResult>,
prevented: bool,
) -> DefaultActionResult;
The current key-to-action mapping:
TabproducesFocusNext.Shift+TabproducesFocusPrevious.Ctrl+TabandAlt+TabproduceNoneso the OS handles them.EnterandNumpadEnterproduceActivateFocusedElementwhenis_activatable.SpaceproducesActivateFocusedElementwhen the node is activatable and not a text input.EscapeproducesClearFocuswhen a node is focused.- Arrow keys outside text inputs produce
ScrollFocusedContainer { Line }. PageUpandPageDownproduceScrollFocusedContainer { Page }.HomeandEndproduceScrollFocusedContainer { Document }.Ctrl+HomeandCtrl+EndproduceFocusFirstandFocusLast.
SubmitForm, CloseModal, and SelectAllText exist in the enum but
no key combination produces them yet. The shell handler matches them
and falls through to a placeholder.
default_action_to_focus_target bridges the focus variants to the
FocusTarget consumed by FocusManager::resolve_focus_target.
create_activation_click_event builds the SyntheticEvent { event_type: Click, source: Synthetic, ... } re-fed to dispatch_events_propagated
for Enter/Space activation.
Pre-callback interpreter (SystemChange)
SystemChange is the framework-side counterpart to user callbacks.
Variants cover text selection, IME, drag-and-drop, focus, scroll, and
the auto-scroll timer. They're produced by default_input_interpreter
and consumed by apply_system_change on the shell. Adding a variant
deliberately causes a compile error there, so every change is handled
exhaustively.
pub struct InputInterpreterInfo<'a> {
pub events: &'a [SyntheticEvent],
pub hit_test: Option<&'a FullHitTest>,
pub keyboard_state: &'a KeyboardState,
pub mouse_state: &'a MouseState,
pub state: InputInterpreterState, // focused_node, click_count, drag_start_position, has_selection
}
pub type InputInterpreterCallbackType = extern "C" fn(
RefAny,
*const InputInterpreterInfo<'static>,
) -> PreCallbackFilterResult;
Replace LayoutWindow::input_interpreter_callback to implement vim
modes, game controls, or custom shortcut tables. Native Rust callers
wrap a fn via InputInterpreterCallback::from(fn_ptr) (sets ctx = None); FFI callers use the trampoline pattern with ctx: OptionRefAny
holding the foreign callable.
Three helper enums live alongside:
ArrowDirection::from_key(vk, ctrl)maps(VirtualKeyCode, ctrl)toLeft,Right,Up,Down,LineStart,LineEnd,DocumentStart, orDocumentEnd.KeyboardShortcut::from_key(vk, ctrl, shift)recognisesCtrl+C,Ctrl+X,Ctrl+V,Ctrl+A,Ctrl+Z,Ctrl+Y, andCtrl+Shift+Z.SelectionOp { direction, step, mode, repeat }is the unified cursor/selection/delete operation produced by the interpreter from arrow, backspace, and delete keys.
Post-callback filter
pub type PostFilterCallbackType = extern "C" fn(
RefAny,
bool, // prevent_default
SystemChangeVecSlice, // pre_changes
DomNodeId, // old_focus (0xFFFF = None)
DomNodeId, // new_focus
) -> SystemChangeVec;
Runs after user callbacks return, given the merged prevent_default
flag, the SystemChanges the interpreter produced before dispatch, and
the focus delta. It returns more SystemChanges. Typical examples are
ClearAllSelections, FinalizePendingFocusChanges, and
ScrollSelectionIntoView. The default impl is default_post_filter.
Override LayoutWindow::post_filter_callback to customise.
Lifecycle reconciliation
pub fn detect_lifecycle_events_with_reconciliation(
dom_id: DomId,
old_node_data: &[NodeData],
new_node_data: &[NodeData],
old_hierarchy: &[NodeHierarchyItem],
new_hierarchy: &[NodeHierarchyItem],
old_layout: &OrderedMap<NodeId, LogicalRect>,
new_layout: &OrderedMap<NodeId, LogicalRect>,
timestamp: Instant,
) -> LifecycleEventResult;
After a RefreshDom rebuild, the reconciler emits Mount, Unmount,
Resize, and Update synthetic events tagged
EventSource::Lifecycle. It also returns node_id_mapping: OrderedMap<old NodeId, new NodeId> so the shell can migrate focus,
scroll position, drag context, and selection across the rebuild. The
match strategy starts with the stable reconciliation key
(.with_reconciliation_key()), then content hash, then mount/unmount
fallback. The simpler index-based detect_lifecycle_events exists for
cases where reconciliation isn't required.
Component filters fire only on the lifecycle event's target.
propagate_event is bypassed for them, and matches_component_filter
is the predicate the dispatcher consults.
Callback invocation surface
User callbacks attach to NodeData as CoreCallbackData { event: EventFilter, callback: CoreCallback, refany: RefAny }. CoreCallback
stores the function pointer as a usize plus an optional FFI ctx: OptionRefAny:
pub type CoreCallbackType = usize; // actually: extern "C" fn(RefAny, CallbackInfo) -> Update
pub struct CoreCallback {
pub cb: CoreCallbackType,
pub ctx: OptionRefAny,
}
pub struct CoreCallbackData {
pub event: EventFilter,
pub callback: CoreCallback,
pub refany: RefAny,
}
The usize masks a circular dependency. The real callback signature is
in azul-layout (CallbackType and the CallbackInfo struct), but
azul-core has to store the pointer without depending on layout. The
dispatcher in the shell is the only code that performs the unsafe
transmute back to the function pointer at invoke time. Everything in
azul-core keeps it opaque.
The same pattern recurs for image rendering
(CoreRenderImageCallbackType), input interpreter
(InputInterpreterCallbackType), and post-filter
(PostFilterCallbackType). The „Core“ prefix marks the usize-stored
variant. The layout-side type without the prefix is the real signature.
Update, the callback return value, has three levels:
pub enum Update {
DoNothing,
RefreshDom, // rebuild the DOM for this window
RefreshDomAllWindows,
}
Update::max_self merges results across all callbacks in a batch; the
dispatcher uses the merged value to decide whether to recurse with a
fresh layout pass.
Event source distinctions
The shell sets EventSource deliberately so downstream consumers can
diverge:
EventSource::Useris direct OS input (mouse, keyboard, touch).EventSource::Programmaticis produced byCallbackInfo::scroll_node_into_view,set_focus, etc. Scroll callbacks should treat this as authoritative and not retrigger.EventSource::Syntheticis emitted by the framework on behalf of the user. The clearest example iscreate_activation_click_eventfor Enter/Space activation. Callback filters treat it identically toUser.EventSource::Lifecycleis emitted bydetect_lifecycle_events*after a DOM rebuild. It bypassespropagate_eventand is target-only.
Callback invocation paths
The shell drives four callback paths through LayoutWindow, all
wrapping the same six-step pattern (build CallbackInfo, invoke the
callback, drain the Arc<Mutex<Vec<CallbackChange>>>, apply changes
via apply_callback_changes, merge into CallCallbacksResult,
return):
- Timer path.
run_single_timerfires when aTimerexpires. Driven byinvoke_expired_timersin the shell tick. - Thread path.
run_all_threadsfires when a backgroundThreadposts a message. Driven byinvoke_thread_callbacksafter epoll/select. - Dispatch path.
invoke_single_callbackfires when one filter matched during dispatch. Driven bydispatch_events_propagated. - Menu path.
invoke_menu_callbackfires when a native menu item is clicked. Driven by platform menu handlers on macOS, Windows, and Linux.
Hit-testing and scroll dispatch flow into this pipeline; see Hit Testing for tag generation, the four result-type maps, and cursor-to-node routing. VirtualView callbacks also generate events the interpreter sees; see VirtualView for nested-DOM lifecycle. For the IFrame-specific scroll routing, see IFrame Scroll.
Coming Up Next
- Hit Testing — tag namespaces, result types, cursor and selection routing
- VirtualView — nested DOMs, lazy loading, viewport-driven instantiation
- IFrame Scroll — coordinate translation across nested pipelines
- DOM Internals — how the public
Domtype is built and stored