Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Events and outcomes

Feeding an event to a widget returns an Outcome. It has three values and they mean different things to your app:

extern crate tuile;
use tuile::prelude::*;
fn main() {
let _ = Outcome::Ignored;    // the widget did nothing; offer the event to someone else
let _ = Outcome::Consumed;   // handled; a redraw is enough
let _ = Outcome::Changed;    // the value the widget owns changed
}

Consumed covers hover moves, scrolling, a cursor blink, an arrow key that moved a list cursor without picking anything. Changed is the one your application logic reacts to: a toggle flipped, text was edited, a row was selected, a button fired.

The Interactive trait

Every stateful widget implements Interactive:

extern crate tuile;
extern crate ratatui_core;
use tuile::prelude::*;
fn demo(state: &mut CheckboxState, key: KeyEvent, m: MouseEvent, ev: &Event) {
state.handle_key(key);     // key events only
state.handle_mouse(m);     // mouse events only
state.handle(ev);          // either: dispatches by variant, filters key releases
}
fn main() {}

handle is the one to reach for in an app. It matches on the event, forwards key presses to handle_key and mouse events to handle_mouse, and returns Ignored for anything else (resize, focus, paste). Key releases are dropped, so a widget cannot fire twice on one press under terminals that report releases.

Widgets that animate on interaction do not take the frame instant here. They stamp the event with Instant::now() and compare it against the .now(..) handed to the next render. ButtonState::pressed_at works that way: the press is recorded in handle_mouse, and the flash only appears if the builder was given a now to measure against.

Combining outcomes

Outcome implements BitOr, taking the strongest result:

extern crate tuile;
extern crate ratatui_core;
use tuile::prelude::*;
fn main() {
assert_eq!(Outcome::Ignored | Outcome::Consumed, Outcome::Consumed);
assert_eq!(Outcome::Consumed | Outcome::Changed, Outcome::Changed);
}

That is what makes broadcasting a mouse event to a screen of widgets one expression:

extern crate tuile;
extern crate ratatui_core;
use tuile::prelude::*;
struct Page { name: InputState, email: InputState, save: ButtonState }
impl Page {
fn on_mouse(&mut self, ev: &Event) -> Outcome {
    self.name.handle(ev) | self.email.handle(ev) | self.save.handle(ev)
}
}
fn main() {}

There is also |=, and the helpers is_consumed() (true for Consumed and Changed) and is_changed() (true only for Changed).

Dispatch: keys go to one widget, mouse goes to all

Keyboard events have no coordinates, so only the focused widget may act on them. Mouse events carry coordinates, and every state knows the rectangle it drew into, so the cheapest correct dispatch is to offer the event to everyone and let each widget test its own rectangle.

extern crate tuile;
extern crate ratatui_core;
use tuile::prelude::*;
#[derive(Clone, Copy, PartialEq)] enum Id { Name, Save }
struct Page { name: InputState, save: ButtonState, focus: Focus<Id> }
impl Page {
fn event(&mut self, ev: &Event) -> Outcome {
    match ev {
        Event::Key(k) => {
            if self.focus.handle_key(*k).is_consumed() {
                return Outcome::Consumed;          // Tab or Shift-Tab
            }
            match self.focus.current() {
                Some(Id::Name) => self.name.handle(ev),
                Some(Id::Save) => self.save.handle(ev),
                None => Outcome::Ignored,
            }
        }
        Event::Mouse(_) => self.name.handle(ev) | self.save.handle(ev),
        _ => Outcome::Ignored,
    }
}
}
fn main() {}

The cost of broadcasting is one rectangle test per widget per event, which is not measurable at terminal scale.

Order matters for overlapping widgets

When two widgets overlap, the one drawn last is on top, so it must see the event first. Broadcasting in draw order gives the wrong answer for a dropdown covering a table: both would consume the click.

Handle overlays before the widgets beneath them, and stop when one consumes:

extern crate tuile;
extern crate ratatui_core;
use tuile::prelude::*;
struct Page { menu: ContextMenuState, table: DataTableState }
impl Page {
fn event(&mut self, ev: &Event) -> Outcome {
    let out = self.menu.handle(ev);
    if out.is_consumed() {
        return out;                     // the menu is on top; the table never sees this
    }
    self.table.handle(ev)
}
}
fn main() {}

The overlays chapter covers the drawing half of the same problem.

Reading what changed

Changed says something moved, not what. Read the state:

extern crate tuile;
extern crate ratatui_core;
use tuile::prelude::*;
fn demo(dark: &mut SwitchState, ev: &Event) {
if dark.handle(ev).is_changed() {
    theme::set_by_name(if dark.on { "textual-dark" } else { "textual-light" });
}
}
fn main() {}

Widgets that fire an event rather than hold a value use a take_* method that returns the pending action once and clears it: take_activated() on menus and lists, take_closed() on a closable tab bar, take_action() on an action toast. Calling take_* is what marks the event as handled, so a widget whose result you never take will keep reporting it.

Events you will not see

The runtime filters key releases before App::event. Everything else crossterm produces is forwarded: resize, focus gained and lost, paste, and mouse moves when capture is on. Widgets ignore the variants they do not care about, so passing them through costs nothing.