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

Dialogs

A built-in modal dialog, behind the dialog feature. It's the one piece of rendering the crate ships, because a yes/no confirmation is the same everywhere and not worth rewriting per app. Everything here is the minimal_dialog example.

tui-pages = { version = "0.7", features = ["dialog"] }

How it fits

The dialog rides on the runtime's modal payload slot, M. You set M to DialogData<YourPurpose>, where YourPurpose is your own "which dialog is this" type:

#![allow(unused)]
fn main() {
// O = () (no named overlays), M = DialogData<Purpose>
pub type App = TuiApp<View, Action, AppState, Handler, (), DialogData<Purpose>>;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Purpose {
    ConfirmDelete,
}
}

The focus manager stores the dialog content and tracks which button is active. Your handler opens the dialog; a one-line helper drives it; you act on the result.

Opening a dialog

Build a DialogData and turn it into a focus effect with show_intent():

#![allow(unused)]
fn main() {
Action::Select => match ctx.focus {
    Some(FocusTarget::Button(0)) => {
        let dialog = DialogData::new(
            "Delete an item?",                          // title
            format!("Delete \"{first}\"?"),             // message (may be multi-line)
            ["Delete", "Cancel"],                       // buttons, left to right
            Purpose::ConfirmDelete,                     // your purpose payload
        );
        ActionOutcome::effect(TuiEffect::Focus(dialog.show_intent()))
    }
    _ => ActionOutcome::none(),
}
}

There's also DialogData::loading(title, message) for a buttonless "please wait" modal — swap it for a real DialogData::new (via another show_intent) when the work finishes.

Driving it in the event loop

dialog::handle_key does the conventional bindings for you and closes the dialog when answered. It returns a DialogKey<D>no Result, no ?:

#![allow(unused)]
fn main() {
match dialog::handle_key(&mut tui.focus, key) {
    // No dialog open — pass the key through to normal handling.
    DialogKey::Ignored => {
        if tui.handle_key(key, state)?.quit_requested {
            return Ok(());
        }
    }
    // Dialog absorbed the key (e.g. moved between buttons). Nothing to do.
    DialogKey::Consumed => {}
    // Dialog finished — act on the answer.
    DialogKey::Resolved(result) => apply_dialog(result, state),
}
}

The conventional bindings it applies:

KeyEffect
Tab / next button
Shift+Tab / previous button
Enterchoose the active button
Escdismiss

(Want different bindings? Skip the helper and drive it yourself with dialog::current_dialog, dialog::active_button, dialog::selection, and FocusIntent. The helper is just the common path.)

The result

#![allow(unused)]
fn main() {
pub enum DialogResult<D> {
    Selected { purpose: Option<D>, index: usize }, // which button (0-based)
    Dismissed,                                      // Esc, or a loading dialog
}

fn apply_dialog(result: DialogResult<Purpose>, state: &mut AppState) {
    match result {
        DialogResult::Selected { purpose: Some(Purpose::ConfirmDelete), index: 0 } => {
            state.items.remove(0); // "Delete" chosen
        }
        DialogResult::Selected { .. } => {} // "Cancel" or some other button
        DialogResult::Dismissed       => {} // Esc
    }
}
}

Rendering it

Draw your normal UI, then draw the dialog on top if one is open. The renderer needs the data and the active button index, both read from the focus manager:

#![allow(unused)]
fn main() {
use tui_pages::{render_dialog, DialogTheme};

// ...draw the rest of the screen first...

if let Some(data) = dialog::current_dialog(&tui.focus) {
    let active = dialog::active_button(&tui.focus).unwrap_or(0);
    render_dialog(frame, frame.area(), data, active, &DialogTheme::default());
}
}

render_dialog centers the modal in the area you pass and draws it. To restyle it, build a DialogTheme (all fields are ratatui::style::Color):

#![allow(unused)]
fn main() {
pub struct DialogTheme {
    pub background: Color,
    pub border: Color,
    pub border_active: Color,
    pub title: Color,
    pub text: Color,
    pub button: Color,
    pub button_active: Color,
}
}