zgx

Actions and key bindings

Components register keyboard bindings and action handlers without owning the native event loop. Bindings are resolved along the focused component’s ancestor path before editor mutation and ordinary key handling. A modal focus scope bounds that path. on_action receives target/bubble events; call prevent_default() to mark the action handled, and stop_propagation() to stop ancestor handlers.

use zgx::{
    actions::{Action, ContextPredicate, KeyBinding, KeyContext, Keymap, Keystroke},
    compose::prelude::*,
    input::{Key, Modifiers},
};
struct Save;
let bindings = Keymap::new().bind(
    KeyBinding::new(
        [Keystroke::new(Key::Function(5), Modifiers::default())],
        Action::new(Save),
    ).unwrap().when_predicate(
        ContextPredicate::parse("Editor && mode == insert && !Terminal").unwrap()
    )
);
let view = column().keymap(bindings)
    .on_action(|_: &Save, cx| {
        // Save the application's document here.
        cx.prevent_default();
        cx.stop_propagation();
    })
    .child(button().child("Document").keymap(
        Keymap::new().key_context(KeyContext::new().flag("Editor").attribute("mode", "insert"))
    ));

Keymap::context("Editor") adds a flag, and KeyBinding::when("Editor") requires that flag. Parsed predicates additionally support ==, !=, !, &&, ||, parentheses and ancestor > relationships. && requires its positive operands to match the same context node; Workspace > Editor explicitly names an ancestor relationship. Negation excludes a match anywhere in the considered path. Configuration parsing rejects malformed expressions, inputs over 4096 bytes and nesting beyond 64 expressions.

A key sequence has one through four strokes. Closer scopes and later bindings have precedence. A later longer sequence can defer an earlier exact binding; a newer exact binding overrides older extensions. Ambiguous input has a single one-second interaction deadline. Completing a sequence dispatches candidates in priority order until a handler prevents the default. Action::new(NoAction) explicitly suppresses a binding. Unhandled candidates fall back to ordinary key handling.

Timeout or mismatch first tries a deferred exact command, then replays the original key and native text events if unhandled. A mismatch subsequently tries the new stroke independently. Replayed input skips keymap resolution, respects editor/default prevention and stops if focus changes or ownership disappears. The desktop host preserves the actual native text payload while waiting, avoiding reconstructed characters or duplicate insertion. Custom hosts should dispatch native Text events when has_pending_keys() is true even if the prefix key was prevented, and schedule next_key_deadline() / advance_key_sequence(); Ui’s existing interaction scheduling already integrates these methods.

Focus changes, native blur, pointer press, composition, registration changes, disabling, subtree disposal and host interaction cancellation clear pending input. Idle views have no key deadline or polling. F1–F35 and Insert survive the desktop mapping alongside existing character/navigation/editing keys. On a layout of other letters (Russian, Greek, Hebrew, Arabic), a key pressed with Ctrl or Cmd (and not Alt, which with Ctrl is Windows’ AltGr) whose character is outside ASCII arrives as the US letter on that key, as browsers and GTK take shortcuts: Ctrl and the C key copy whatever the layout types there, and a binding of "c" matches it. Typing is untouched. The Rust configuration API is independently designed; this is not a GPUI keymap JSON parser or a promise of identical precedence for every mixed-context rule set.

InputDispatcher::dispatch_action(&scene, Action::new(value)) lets native menu handlers invoke the same typed route. Its DispatchResult::default_prevented indicates whether a listener marked the action handled. With no focused node, it targets the active focus scope or scene root; applications can attach a root listener or retain their native-menu fallback.

Keymaps retain action values and unregister with their owning subtree. Ordinary component wrappers merge their keymaps with their inner component. Callback execution and captured-value destruction occur outside keymap registry borrows.

Run cargo run -p zgx-desktop --example actions for the editor/F5/chord example. It also accepts native file drops. InputEvent::FileHover, FileDrop and FileHoverCancelled route to the region under the host’s last logical pointer position. Paths remain PathBuf; the native host sends one event per path, matching winit’s event interface. This is not a batched ExternalPaths payload, and it does not implement initiating an operating-system file drag. Native backends that do not report a current pointer position during external drags can limit precise region targeting; synthetic routing tests are not native cross-desktop validation.

Validation: crates/zgx/tests/actions.rs covers scoped commands before editor mutation, bubbling and native dispatch, chord cancellation, reentrant unmount, file payload routing and context predicates. Unit tests cover expiry, malformed configuration and captured-action destructor reentry. The native X11 harness scripts/actions_smoke.py verifies F5, function-key and character chords, timer wakeup and unmatched prefix replay against the actual edited model.

Edit on GitHub