Input and responses
Pointer capture, keyboard focus, accumulated input, and event consumption.
Response
PanZoom reserves matching
wheel zoom synchronously from the last published viewport. Its ordered queue keeps
each event's anchor/modifiers; raw InputState::scroll_delta is not an ownership API.
Background pan uses shared primary capture; child editing/dragging wins, and ordinary
wheel keeps Carousel/ScrollArea routing. No camera keyboard shortcuts are installed.
Every Ui::add and component convenience method returns a Response for that UI pass.
| Field or method | Meaning |
|---|---|
id: Id | Final scoped widget identity |
rect: Rect | Allocated rectangle in the current Ui's layout coordinates; content coordinates inside PanZoom |
hovered: bool | Pointer lies in the rectangle and clip, with no blocking front window or other capture |
pressed: bool | This interactive widget owns pointer capture or keyboard activation |
has_focus: bool | This enabled interactive widget has keyboard focus |
focus_visible: bool | Show the keyboard focus indication; mouse focus alone keeps it hidden |
enabled: bool | This control is interactive, including its parent group's enabled state |
state() -> WidgetState | Disabled, Pressed, Hovered, or Idle, in that priority order |
clicked() -> bool | Pointer release inside, or Enter/Space release when focused; a press that became a drag is not a click |
double_clicked() -> bool | The second click of a double click (same widget, within 500 ms and 4 px); clicked() is also true on both |
secondary_clicked() -> bool | The secondary button was pressed and released over this widget |
drag_started() -> bool | The pointer moved more than 4 px while pressed on a widget that senses drags |
dragged() -> bool | A drag is in progress, including the pass that starts it and the pass that ends it |
drag_delta() -> Vec2 | Pointer movement in logical pixels since the previous pass; the first delta starts at the press position, so a drag's deltas add up to its displacement |
drag_stopped() -> bool | The drag ended by release, focus loss or a vanished widget |
gained_focus() -> bool | Keyboard focus arrived by Tab, a press or Context::request_focus |
lost_focus() -> bool | Focus left the widget, including between redraws |
changed() -> bool | A bound value changed during this pass |
submitted() -> bool | TextEdit received Enter outside IME composition |
menu_selected() -> Option<Id> | The context menu attached with Widget::context_menu chose this entry |
mark_changed() | For custom widgets: report a value change |
Every event is reported by exactly one pass per user input: the pass after the input
sees it, the following pass does not, and Ui::add schedules that one follow-up pass.
Response is Copy and holds no borrow of Ui. Events are consistent with each
other: a double click is two clicked() passes with double_clicked() on the second; a
press that turned into a drag never produces clicked(); changed() of a checkbox
arrives in the pass of its clicked(). Context menus open on the same
secondary_clicked() signal, and a drag-and-drop source reports the same drag_started,
drag_delta and drag_stopped as any dragged control.
Which controls report which events: clicks and double clicks on buttons, checkboxes,
switches, combo boxes, text edits and drag values; drag events on sliders, drag values,
text edits and drag-and-drop sources; secondary clicks and focus events on everything
interactive. Ui::interact gives custom widgets all of them through
Sense. Passive controls can be hovered, but cannot be
pressed, focused, clicked, or changed. A disabled control may still report hover; it does
not activate or change its bound value.
Buttons report clicks without a value change. Checkboxes report both on a toggle.
Sliders use changed() and do not report activation through clicked().
ColorPicker reports row activation through clicked() and color edits through
changed(). Its RGB/HEX fields accept committed text from native events or
Context::on_text_event. See ColorPicker for editing keys.
Files from the system
Context::hovered_files, take_dropped_files and DropTarget::accepts_files carry files dragged in
from the file manager; see File dialogs and file drop.
Pointer
Primary-button press captures the hit region from the preceding UI pass. Releasing over that same button or checkbox activates it. Releasing elsewhere cancels activation. Sliders continue accepting captured pointer motion outside their rectangle and clamp to the range endpoints. Focus loss cancels capture and held input.
Window clicks raise the panel to the front. The top visible window blocks controls in windows below it, including when its content has no interactive control at the pointer. Only the left mouse button activates built-in controls.
Events are processed immediately against retained hit regions, so a press/release pair arriving before the next redraw is not lost. Pending activations are stored by ID; multiple activations of the same button between UI passes are not a click counter.
Keyboard
| Key | Action |
|---|---|
| Tab / Shift+Tab | Next / previous enabled focusable control, wrapping at the ends |
| Enter / Space | Press and release a focused button or checkbox |
| Right / Up | Increase focused slider by one step |
| Left / Down | Decrease focused slider by one step |
| PageUp / PageDown | Increase / decrease slider by ten steps |
| Home / End | Select slider minimum / maximum |
Tab and button activation ignore key repeats. Slider adjustment accepts repeated key-press events. Focus traversal follows retained hit-region order after window layer ordering; fully clipped controls are skipped. A focus group is one region in that order, its Tab stop. Clicking a passive panel area clears widget focus.
Who gets a key
Context::on_input decides the owner of a key the moment the event arrives and reports it in
EventResponse::consumed; no UI pass runs in between. The first of these that wants the key
takes it:
- Held keys. The autorepeat and release of a key a widget claimed or a group took stay with that owner, whatever has focus now.
- A drag in progress. 3. Escape on a pending chord, and the repeat or release of a key an action took. 4. The leaf of the open popups (Escape closes it, one level per press; Tab closes it and goes on in what remains). 5. Escape for the top modal. 6. Menu navigation of an open menu or a focused combo box, and Escape for middle-button autoscroll. 7. Dock navigation, the focused split boundary and tree, copying selected text.
- Keys the focused widget claimed with
Ui::keys: explicit ownership. - The keymap: actions and chords. A focused text field keeps printable keys and its editing shortcuts.
- The focused control's own keys: drag value, text field, slider.
- Arrows, Home and End of a focus group around the focused control.
- Tab traversal, Enter as the modal's default action, Enter and Space on a focused button.
A claim sits above the keymap on purpose: a widget that names Ctrl+S in KeyInterest owns it
while it has focus, and the Save action does not run. A chord already in progress, a KeyBox
that is listening, a popup and a modal come first. A key that nothing in the list wants is
returned unconsumed and goes on to the host.
Keys for a widget
Ui::keys(&response, KeyInterest) is how a widget written outside the crate gets the keys it
needs, in order, with the modifiers they had, without reading InputState:
use zaxis::winit::keyboard::KeyCode;
use zaxis::{KeyInterest, Sense, Ui, Vec2};
fn knob(ui: &mut Ui<'_>, value: &mut f32) {
let rect = ui.allocate_space(Vec2::splat(48.0));
let response = ui.interact(rect, "knob", Sense::CLICK | Sense::DRAG | Sense::FOCUS);
for key in ui.keys(&response, KeyInterest::arrows().repeats()) {
match key.code {
KeyCode::ArrowUp | KeyCode::ArrowRight => *value += 0.05,
_ => *value -= 0.05,
}
}
}- Declaring means owning. Call it in every pass; a declaration lasts until the pass after it and is published with the hit regions. While the widget has focus, each matching press is consumed on arrival, so the widget owns the key even if it ignores the event, and Actions, containers and the host never see it. Stop declaring and the key is given back at once. A key that no declaration names takes the usual path.
- Before the first pass, and after the widget is gone, nothing is claimed.
- What counts as a match.
KeyInterest::keys(&[..]),arrows(),navigation(),activation(). Modifiers match exactly (with_mods;Mods::PRIMARYis Command or Control);any_mods()matches all. Shift+Arrow is not anarrows()claim, and neither is AltGr, which some platforms report as Ctrl+Alt. Letters match the printed Latin letter, as shortcuts do, soCtrl+Zworks on a Russian or AZERTY layout;by_position()matches the key's place instead. - Who may claim. Only the focused widget, and only when its region is published, takes focus, is not clipped away, is in the layer that may take keys (the leaf popup, else the top modal, else any) and is not a control that keeps its keys itself: a text field, slider, drag value, combo box, tree or split handle ignores claims and the claim is reported as a usage diagnostic. A widget under an open popup or modal gets nothing.
- Events. Each press is its own
KeyEventin arrival order: three equal presses between two passes are three events. An event carriescode,layout,physical,logical,state,repeat,textand the modifiers held when it arrived. It is delivered once, to the widget that owned the key when it arrived, even if focus moved before the pass, and lapses at the end of the first pass that follows.Ui::take_keysdrains,Ui::claim_keysdeclares, andUi::keysis both. - Autorepeat is owned and consumed, but delivered only with
.repeats(); Actions and activation never fire on a repeat. The release of a claimed key is always owned and consumed, delivered with.releases(), also after focus moved or the widget vanished. - Enter and Space. A claim on them replaces the click a focused region raises; without a claim the click is unchanged. A claimed printable key types no text.
InputState. A claimed key updateskeys_down,keys_pressedandkeys_releasedlike any other consumed key, so a held key stays accurate and an old reader ofkeys_pressedkeeps working. Do not read both for one key.- Limits. A widget has at most 64 claims. The queue holds 256 events per widget and 1024 in
all. Beyond that new presses and repeats are dropped (
InputStats::key_events_dropped, one diagnostic per overflow), the dropped press's repeats and release are still consumed and never queued, and releases of delivered presses are always kept: a key is never half delivered, and no key stays held. A lost window focus, a modal opening or closing and a popup closing drop every queued event and every held key's owner.
Focus groups
FocusGroup::new(id_source).horizontal().wrap(false).label("Tools").show(ui, |ui| ..) makes
the controls built in the closure one Tab stop and gives them arrow-key navigation:
use zaxis::{AccessRole, FocusGroup, Ui};
fn toolbar(ui: &mut Ui<'_>) {
FocusGroup::new("tools").label("Tools").role(AccessRole::Toolbar).show(ui, |ui| {
ui.horizontal(|ui| {
ui.button("Cut");
ui.button("Copy");
ui.button("Paste");
});
});
}- One stop. Tab and Shift+Tab enter and leave the group. Entering lands on the member that
has focus, else the one named with
Ui::focus_entry, else the member focus was last on, else the first. Controls outside groups keep their Tab order. - Moving. Along the axis (
horizontal,vertical,FocusAxis::Both) the arrows move focus to the previous or next member; Home and End jump to the first and last.wrap(true)continues at the other end; by default the key stops at an end, and stays owned by the group. Focus moves; nothing is selected and nothing is called. Modified arrows are not navigation. A held arrow keeps stepping. - Who is a member. Every region that takes focus and is built in the closure, in its layer:
Button,Ui::interactwithSense::FOCUS, other widgets. A region that is disabled, hidden, fully clipped away or gone is not a member, so it is skipped and cannot be the stop. Content in another layer (a popup, a window) does not join. A click or a request from assistive technology focuses any member and makes it the stop. - Memory. The group remembers its last member. When it goes away the stop passes to the member now at its position, or the last. An empty group has no stop and no state, and a group that is not built is forgotten.
- Controls with keys of their own. A text field, slider or other control that uses arrows is a
member: its neighbours move focus onto it, it keeps its keys while focused (so the group's
arrows do not move out of it), and Tab leaves the group. A custom widget does the same with
Ui::keys: its claim wins over the group's key. - Nested groups. A group inside a group is one member of the outer one. The innermost group
whose axis uses the key moves focus; at its end an arrow goes on to the outer group, which
treats the whole inner group as one member, so a key never moves two levels. Home and End stay
with the innermost group.
FocusGroup::slot(id_source)(FocusAxis::None) is for a part that navigates itself, such as a tab strip or a list: the outer arrows move onto it and around it, and while focus is inside no group acts on the arrows, so a key is never handled twice. - Events. The group's
Responsereportshas_focus,focus_visibleand one-shotgained_focus/lost_focus.FocusGroupOutput::navigated()names the member the arrows moved focus to since the last pass, for a control that selects as focus moves (RadioGroupandSegmentedControldo), andstop()the region Tab lands on. InputState. The press that moved focus is held but not left inkeys_pressed: the control that now has focus must not act on a key that took it there.- Published routing. Navigation reads the groups the last pass published, so a second key that
arrives before the next redraw is addressed to the member the first one reached.
The group's own axis and
wraptake effect from the pass that built them.
RadioGroup and SegmentedControl are built on this: each option stays focusable by click and by
assistive technology, Tab lands on the selected option, and arrows, Home and End are the group's.
What stays with RadioGroup is the rule of a grid layout: Up and Down move along a column, which
a flat order cannot say, through Ui::keys.
InputState
Read accumulated input during the UI callback through context.input() or
ui.context().input(). Positions and scroll distances use logical pixels.
| Field | Lifetime / units |
|---|---|
pointer: Option<Vec2> | Last logical pointer position; None after cursor leave or focus loss |
primary_down: bool | Held state across passes |
primary_pressed, primary_released | Transitions accumulated since the preceding pass |
scroll_delta: Vec2 | Accumulated logical delta; pixel events divided by scale, line events multiplied by style font size |
keys_down: HashSet<KeyCode> | Held physical keys across passes |
keys_pressed, keys_released | Accumulated physical-key transitions |
modifiers: ModifiersState | Current modifier state |
text: String | Text from pressed keyboard events and IME commits |
focused: bool | Native window focus state |
Context::run clears transition sets, scroll delta, and text after the callback.
Persistent pointer, held-key, and modifier state survive ordinary passes. Reading
input after run cannot recover the cleared transitions. Text accumulation does not
replace TextEdit, which owns editing and IME composition state.
Input without a window
Context::on_input(InputEvent) is the one entry point for input. on_window_event converts a
winit event with InputEvent::from_window_event and calls it, so the runner and a host that has
no winit window (an overlay inside another process, a test, an engine with its own events) behave
identically. Positions and sizes are physical pixels of the viewport; the context divides by its
scale factor.
InputEvent | Meaning |
|---|---|
PointerMoved { x, y }, PointerLeft | Pointer position, in physical pixels |
Button { button, state } | MouseButton and ElementState |
Wheel(WheelDelta::Lines(v) | Pixels(v)) | Notches (multiplied by the line height) or pixels (divided by the scale) |
Key(KeyInput { physical, logical, state, repeat, text }) | physical is the scan-code key, logical the meaning with the layout (a numpad key with NumLock off is an arrow), text what the press types |
Modifiers(ModifiersState) | Send it when the modifiers change, before the key that needs them |
Ime(ImeEvent::Preedit | Commit | Disabled) | Input method composition |
Focus(bool) | Losing focus releases held keys and buttons, closes popups and cancels drags |
Resized { width, height }, ScaleFactor(f64) | Viewport size, and DPI with the size kept |
FileHovered, FileDropped, FileHoverCancelled | Files dragged in |
The key vocabulary is winit's (KeyCode, Key, MouseButton, ElementState,
ModifiersState): plain data enums that need no window. After a pass, cursor_icon() and
ime_cursor_area() say which cursor to show and where the input method's window belongs.
EventResponse
Custom hosts call context.on_window_event(&event) and receive:
| Field | Host action |
|---|---|
repaint: bool | Request a native redraw |
consumed: bool | The UI handled the event; use this to gate other host input handlers |
consumed is final when on_input returns, and a key is never taken later. A claimed key, an
arrow of a focus group and the stroke of an action are decided right there from what the last
finished pass published; Widget::ui only applies events the dispatcher already addressed to
it. A host (an overlay, an embedded renderer, the browser key policy) can therefore keep the
key from the application beneath at once. Before the first pass, or for a widget that stopped
declaring, nothing is claimed.
Consumed and repaint are independent. Pointer motion outside all panels can request a redraw without being consumed. Text and IME commits may accumulate without being consumed. For event routing, viewport updates, and deadlines, use Custom host.
Keys and commands
Keyboard input is routed in a fixed order, and shortcuts are a registry of their own: see Actions and keymap for where the keymap sits in that order, chords, contexts, layouts (letters by the Latin letter, physical key on a non-Latin layout) and key capture.