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:
| Key | Effect |
|---|---|
Tab / → | next button |
Shift+Tab / ← | previous button |
Enter | choose the active button |
Esc | dismiss |
(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, } }