Dialogs - tui-pages

## 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:

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,
    }
    }