Skip to content

Input layers

Input events are not broadcast to every component; they are dispatched by the central InputRuntime. During each frame’s update phase, components register input layers and handlers again. When a terminal event arrives, the runtime sends it only to currently active layers.

To see a complete running example first, start with Input isolation.

Global handlers are not cut off by layers; ordinary handlers dispatch by layer first, then same-layer priority.

Dispatch has two phases:

PhaseCandidate handlersOrdering
GlobalEventScope::Globalpriority desc, order asc
LayerHandlers in active input layersz-order desc, priority desc, order asc

z-order beats priority. A Normal handler in an upper layer runs before a High handler in a lower layer; otherwise a component behind a modal could still steal the event.

InputLayer is the handle for the current frame’s layer:

let layer = hooks.use_input_layer(open, true);

When open=false, a handle is returned but the layer is not pushed, and handlers bound to it are silently skipped. When blocks_lower=true, this layer cuts off lower layers.

EventScope decides which layer a handler belongs to:

EventScope::Current
EventScope::Layer(layer)
EventScope::Global

Current resolves to the nearest current layer in context. Components inside a Modal subtree usually only need Current; parent components that want to control modal keys should bind explicitly with Layer(layer).

EventResult decides whether the event continues propagating:

EventResult::Consumed
EventResult::Ignored

Return Consumed after handling an event. Return Ignored when this handler did not handle it, so later same-layer handlers or lower layers can try.

By default, a handler receives all keyboard and mouse events in its layer. For cases where mouse input should be handled only inside a component’s area, use use_event_handler_with_options:

hooks.use_event_handler_with_options(
    EventScope::Current,
    EventPriority::Normal,
    EventOptions { hit_test: true },
    move |event| {
        // Mouse events outside this component's previous-frame area do not call this closure.
        EventResult::Ignored
    },
);

hit_test affects only mouse events. Keyboard events have no coordinates and are still delivered by layer, priority, and registration order. The hit area comes from the owning component’s previous-frame drawn Rect, so it is appropriate for local mouse wheel, click, and drag interactions. Do not use it to express modal isolation; modal isolation should still use InputLayer.

The standard modal isolation pattern:

let layer = hooks.use_input_layer(open, true);

hooks.use_event_handler(
    EventScope::Layer(layer),
    EventPriority::High,
    move |event| {
        // handle modal keys
        EventResult::Consumed
    },
);

element!(
    Modal(open: open, layer: Some(layer)) {
        // modal content
    }
)

Modal(layer: Some(layer)) must receive the same layer used by the handler. If you omit it, Modal opens a new layer and cuts off the parent’s layer, so the parent handler no longer receives events.

InputLayer handles are rebuilt every frame and should only be passed to handlers or child components in that same frame. Do not store one in State; on the next frame, that ID is no longer in the active layer stack.

This is also why the input system does not leak: after a modal closes, the next frame no longer registers its layer or handlers, so dispatch naturally cannot find them.

Prefer built-in components for ordinary interactions:

NeedComponent
Search inputSearchInput
Confirmation dialogConfirmModal
Alert dialogAlertModal
Shortcut helpShortcutInfoModal

These components already wire their input layers internally. Application code only handles open, callbacks, and displayed content. Write use_input_layer, use_event_handler(EventScope::Layer(...)), and Modal(layer: Some(...)) by hand only for specialized modal interactions.