Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

Actions & Keybindings

Kael separates what happens (an action) from how it's triggered (a keybinding or click). Actions are dispatched up the focused element tree, so a keystroke is routed to the nearest handler in the currently focused context — the same model that powers editor-grade keyboard UX.

Defining actions

The actions! macro generates zero-field action types in a namespace:

#![allow(unused)]
fn main() {
use kael::actions;

actions!(editor, [Save, Undo, Redo, Tab, TabPrev]);
}

Each entry becomes a type (Save, Undo, …) implementing the Action trait, with a stable name like editor::Save used for keymaps and dispatch.

Binding keys

Register bindings once at startup with cx.bind_keys. KeyBinding::new takes the keystroke string, the action, and an optional key context that scopes the binding:

use kael::{App, Application, KeyBinding};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    Application::try_new()?.run(|cx: &mut App| {
        cx.bind_keys([
            KeyBinding::new("cmd-s", Save, None),
            KeyBinding::new("cmd-z", Undo, None),
            KeyBinding::new("cmd-shift-z", Redo, None),
            KeyBinding::new("tab", Tab, Some("Editor")),
            KeyBinding::new("shift-tab", TabPrev, Some("Editor")),
        ]);
        // ... open windows ...
    });
    Ok(())
}

Keystroke syntax uses cmd / ctrl / alt / shift modifiers joined with -, and a space separates multi-key sequences (e.g. "cmd-k cmd-s"). Use cmd on macOS and ctrl on Windows/Linux.

Command Registry

Use CommandRegistry when the same app command should be available from a command palette, menu, toolbar, or agent action list:

#![allow(unused)]
fn main() {
use kael::app_runtime::CommandRegistry;

let mut commands = CommandRegistry::new();
commands.register_action_checked("editor.save", "Save", || {
    // persist the active document
})?;

commands.execute("editor.save")?;
}

Prefer register_checked(...) and register_action_checked(...) for generated app chrome. Checked registration rejects empty, padded, overly long, or non-portable command IDs, rejects empty/padded/control-character/overly long names, and catches duplicate IDs before menus or command palettes become ambiguous. Raw register(...) and register_action(...) remain available when an app intentionally wants replacement semantics.

Handling actions

In render, mark the element that owns a focus context with track_focus, then register handlers with on_action(cx.listener(...)). Handlers take &mut self, a reference to the action, the window, and the context:

#![allow(unused)]
fn main() {
use kael::{div, prelude::*, Context, FocusHandle, Render, Window};

struct Editor { focus_handle: FocusHandle }

impl Editor {
    fn on_save(&mut self, _: &Save, _window: &mut Window, cx: &mut Context<Self>) {
        // ... persist ...
        cx.notify();
    }
}

impl Render for Editor {
    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        div()
            .track_focus(&self.focus_handle)
            .on_action(cx.listener(Self::on_save))
            .on_action(cx.listener(Self::on_undo))
            .child("editor surface")
    }
}
}

Focus & tab order

Create focus handles from the context and arrange tab order with tab_index / tab_stop. Move focus from the window:

#![allow(unused)]
fn main() {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
    let items = vec![
        cx.focus_handle().tab_index(1).tab_stop(true),
        cx.focus_handle().tab_index(2).tab_stop(true),
        cx.focus_handle().tab_index(3).tab_stop(true),
    ];
    let focus_handle = cx.focus_handle();
    window.focus(&focus_handle);
    Self { focus_handle, items }
}
}

Window focus methods: window.focus(&handle), window.focus_next() (Tab), and window.focus_prev() (Shift-Tab). Query state with handle.is_focused(window) and style focused elements with .focus(|s| s.border_color(...)).

Dispatch resolution

An element's .key_context("Editor") scopes matching bindings to that part of the tree. When a keystroke matches, Kael starts at the focused element and walks through its ancestors until an on_action handler accepts the action. A closer handler can therefore override application-level behavior without coupling the keymap to a concrete view type.

Use global bindings for commands that are valid throughout the application and context bindings for editor modes, dialogs, lists, and other surfaces where the same keystroke has a local meaning. See the Astryx showcase for a complete focus-navigation composition.