# Event Dispatch Deft's TUI uses a DOM-style **target + bubble** model. Events identify a target widget via focus (keyboard) or hit-testing (mouse), then bubble up to the panel for handling. This page covers how to wire up an app that participates cleanly in dispatch. ## The Dispatch Phases For each input event: 1. **Target phase** — the framework identifies the widget that should receive the event: - **Keyboard events** → the panel's focused widget (an `:id` stored on its `LeafData`). - **Mouse events** → the topmost widget whose bounds contain the pointer (hit-tested via the `Hittable` protocol). 2. **Bubble phase** — if the target didn't consume the event (or there's no target), the event bubbles to the panel's typed protocol methods (`on-key`, `hit`, `on-scroll`). The framework runs the target phase **before** the panel's bubble handlers, so focused widgets get first claim on keyboard input without the panel needing to forward manually. ## Keyboard Routing A focused text-input consumes keystrokes (so single-key app shortcuts like `s`/`c` don't collide with typing) while still letting the app handle leftover keys (escape, global shortcuts). ``` User presses 's' │ ▼ ┌────────────────────┐ │ Framework │ │ reads focus slot │ └────────────────────┘ │ ▼ ┌────────────────────┐ consumes? ┌────────────────────┐ │ Focused text-input │ ───────────────► │ Panel's on-key │ │ (Keyable.on-key) │ no │ (bubble handler) │ └────────────────────┘ └────────────────────┘ ``` ## Two Wiring Styles ### Legacy: `on-event` bubble handler with pre-pass The panel calls `[tui/dispatch-tree-event $tree $event $bounds]` **first**, then handles only what the tree didn't consume:
 defimpl MyApp Panel {
    on-event {|event|
        # Pre-pass: let the widget tree consume first
        if ![tui/dispatch-tree-event $widget-state $event $bounds] {
            # Then handle leftover events at the panel level
            [match $event~key {
                :escape => [do-cancel]
                _       => nil
            }]
        }
    }
} 
### Preferred: fully-anchored (framework target phase) Register the widget tree once on `on-init` and let the framework run the target phase. The panel implements typed protocols as pure bubble handlers — no pre-pass, no `tui/dispatch-tree-event` call:
 defimpl MyApp AppLifecycle {
    on-init {||
        [tui/set-widget-tree $widget-state]     # opt into framework dispatch
        # ... rest of init ...
    }
}

defimpl MyApp Keyable {
    on-key {|event|
        @doc "Fires ONLY when no focused widget consumed the key."
        [match $event~key {
            :escape => [do-cancel]
            _       => nil                      # bubble further
        }]
        # Modifier chords read the boolean fields:
        if ($event~ctrl and ($event~key == :s)) { [do-save] }
    }
} 
The framework maps each event to the right protocol method (`on-key` for key-down **and** text events, `hit` for mouse events, `on-scroll` for wheel) via the same routing widgets use. The panel is anchored identically to widgets. New apps should use this style — `on-event` is the legacy fallback. ## A Widget Must Consume Or Bubble A widget's `on-key` (or `hit`) must return `nil` for events it doesn't act on, so they bubble. Returning a non-nil value consumes the event:
 defimpl MyInput Keyable {
    on-key {|event bounds|
        [match $event~key {
            :backspace => [handle-backspace $self]
            :enter     => [handle-submit $self]
            _          => nil                   # bubble everything else
        }]
    }
} 
This is what lets a focused text-input consume typing while still letting `escape` reach the panel's `on-key`. See `packages/ui/src/components/grid.dft` for a production example of this pattern. ## Focus Stdlib functions The panel owns a single widget focus slot. Read and set it with:
 [tui/focus-widget "id"]                       # set focus
[tui/focused-widget-id]                       # -> id string | nil
[tui/cycle-focus :forward $tree]              # Tab-style cycling
[tui/cycle-focus :back    $tree]              # Shift-Tab
[tui/clear-focus]                             # drop focus 
### When to set focus - **`on-init`** — set a default focus if your panel opens with an obvious editing target. - **On entering an edit mode** — when the user starts editing, focus the text input. - **On clicking a Focusable widget** — the framework claims focus automatically; you don't need to do this manually. ### When to clear focus - **Escape from edit mode** — return to browse mode. - **After submitting a form** — release focus back to the panel. ## Tab / Shift-Tab The framework cycles focus among `Focusable` widgets (in `tab-index` order) as the default action when nothing consumes a Tab keypress. Apps that want custom Tab handling consume it earlier. There is no need to implement Tab manually. ## Mouse Events Mouse events hit-test by bounds. Clicking a `Focusable` widget claims focus; clicking a non-focusable one leaves focus unchanged. A widget's `hit {|event bounds|}` method returns an action value (keyword, string, or map) if it handled the event, or `nil` to let the click bubble.
 defimpl MyButton Hittable {
    hit {|event bounds|
        if ($event~type == :mouse-down) {
            :clicked                          # or a map with details
        } {
            nil                                # let it bubble
        }
    }
} 
## Common Patterns ### Default focus on init
 defimpl MyApp AppLifecycle {
    on-init {||
        # ... set up widget tree ...
        [tui/set-widget-tree $widget-state]
        [tui/focus-widget "query"]            # default focus on the search box
    }
} 
### Escape cancels edit mode
 defimpl MyApp Keyable {
    on-key {|event|
        if ($event~key == :escape) {
            [tui/clear-focus]
            [reset-to-browse-mode]
        }
    }
} 
### Avoiding single-key collisions with focused inputs Plain printable characters arrive as **`:text` events** (in the `:text` field) — named keys (arrows, enter, escape, …) arrive as `:key-down` with a `:key` keyword. Handle each in its branch:
 defimpl MyApp Keyable {
    on-key {|event|
        # This only fires when no widget consumed the key.
        # So typing 's' is safe even if a text input is focused —
        # the input would have consumed it first.
        [match $event~type {
            :text => {
                [match $event~text {
                    "s" => [do-stage]
                    "c" => [do-commit]
                    _   => nil
                }]
            }
            :key-down => {
                if ($event~ctrl and ($event~key == :s)) { [do-save] }
            }
            _ => nil
        }]
    }
} 
Chord *strings* (`"ctrl-s"`, `"page_up"`) for keymap lookups are built with `[keymap/chord $event]` — it accepts either event form and normalizes to the canonical chord. ## Event Map Shape Events arrive as maps. The canonical shapes (shared by panels and SDL surfaces — see the `Surface` protocol):
 %{
    :type  :key-down                  # named key pressed
    :key   :page_up                   # keyword from the Key vocabulary
    :ctrl  false                      # mods are booleans: :ctrl :shift :alt
    :shift true
    :alt   false
}

%{
    :type  :text                      # printable character(s) typed
    :text  "s"
}

%{
    :type  :mouse-down                # or :mouse-up
    :x     42                         # panel-local CELLS
    :y     10                         # (pixels on SDL surfaces)
    :ctrl  false :shift false :alt false
}

%{
    :type     :mouse-move             # motion; :dragging true while held
    :x        42
    :y        10
    :dragging false
}

%{
    :type  :wheel                     # mouse scroll
    :dx    0
    :dy    -3                          # notches, negative = up
    :x     42 :y 10                    # position
} 
Surfaces add `:resized` (`:w`/`:h`), `:closed`, `:focus` (`:gained`) and `:key-up`, and surface key events carry `:repeat`. Every surface event carries `:surface` (the surface id). Panels also deliver `:focus`/`:blur` and `:panel-open`/`:panel-close`. ## What's In `$event`? | Field | When | Value | |---|---|---| | `:type` | always | Event type keyword (see above) | | `:key` | `:key-down`/`:key-up` | **Keyword** from the Key vocabulary: `:escape`, `:enter`, `:tab`, `:backspace`, `:up`…`:down`, `:page_up`, `:home`, `:f1`…, `:space`, letters `:a`…`:z` (letters mainly with ctrl/alt) | | `:text` | `:text` events | The typed string (printable characters never arrive as `:key`) | | `:ctrl`, `:shift`, `:alt` | key + mouse events | Booleans | | `:x`, `:y` | mouse events | Pointer position — **cells** on panels, **pixels** on SDL surfaces (`tui/px->cell` / `tui/cell->px` convert) | | `:button` | `:mouse-down`/`:mouse-up` (SDL surfaces) | `:left`, `:middle`, `:right`, `:x1`, `:x2` — not present on panel events | | `:dx`, `:dy` | `:wheel` | Scroll deltas in notches | | `:dragging` | `:mouse-move` | True while a button is held | | `:surface` | surface events | The surface id | ## Quick Reference | Form | Purpose | |---|---| | `[tui/set-widget-tree $atom]` | Register tree for framework dispatch | | `[tui/dispatch-tree-event $tree $event $bounds]` | Legacy manual pre-pass | | `[tui/focus-widget "id"]` | Set widget focus slot | | `[tui/focused-widget-id]` | Read focus slot | | `[tui/cycle-focus :forward $tree]` | Tab cycling | | `[tui/clear-focus]` | Drop focus | | `[keymap/chord $event]` | Canonical chord string (`"ctrl-s"`) for keymap lookups | | `defimpl App Panel { on-event {|event| … } }` | Legacy bubble handler | | `defimpl App Keyable { on-key {|event| … } }` | Modern typed bubble (key-down + text) | | `defimpl App Hittable { hit {|event bounds| … } }` | Mouse dispatch | | `defimpl App Scrollable { on-scroll {|event| … } }` | Wheel dispatch |