Skip to content

Dialog

Dialog is a modal overlay: a centered, bordered box with a title, an optional description or custom content area, and an optional footer of interactive children. It wraps a View, so it has its own focus scope.

Focus Open Dialog and press Enter. Inside the dialog, Tab moves between buttons, Enter presses the focused button, Escape closes the overlay in the demo, and Alt+T opens the shared theme picker.

The library does not own open/closed state. Keep dialog state in your app state, usually as an Option.

rust
use ratcn::{Button, Dialog};
use ratcn::view::{EventResult, FocusState};

#[derive(Default)]
struct DialogState {
    focus: FocusState,
}

struct AppState {
    dialog: Option<DialogState>,
    theme: Theme,
}

Render the base UI first, then render the dialog on top while open. Route events to the dialog first while it is open.

rust
root.render(frame, frame.area(), &state);

if state.dialog.is_some() {
    dialog.render(frame, frame.area(), &state);
}

if state.dialog.is_some() {
    if let EventResult::Emit(msg) = dialog.handle_event(&event, &state) {
        update(&mut state, msg);
    }
    return;
}

if let EventResult::Emit(msg) = root.handle_event(&event, &state) {
    update(&mut state, msg);
}

Building A Dialog

rust
let dialog = Dialog::new("confirm")
    .focus(
        |s: &AppState| &s.dialog.as_ref().expect("open").focus,
        Msg::DialogFocusChanged,
    )
    .theme(|s: &AppState| &s.theme)
    .title("Delete item")
    .description("This cannot be undone.")
    .child("delete", Button::new("Delete").destructive().on_press(Msg::Delete))
    .child("cancel", Button::new("Cancel").secondary().on_press(Msg::Cancel))
    .footer(|frame, _state, view| {
        view.render_child(frame, "delete", delete_area);
        view.render_child(frame, "cancel", cancel_area);
    });

Dialog API

  • Dialog::new(id): Creates a dialog focus scope. Defaults to FocusPolicy::Wrap.
  • .title("..."): Sets the title shown in the top border.
  • .description("..."): Renders a wrapped text body and auto-sizes the dialog. Ignored when .content(...) is set.
  • .content(|frame, state, view| ...): Custom main content closure. Uses view.area() for the content strip.
  • .content_height(height): Preferred height for custom content.
  • .child(id, component): Registers an interactive child, usually footer buttons. Declaration order is Tab order.
  • .footer(|frame, state, view| ...): Custom footer closure. Uses view.area() for the footer strip.
  • .focus(read, on_change): Binds the dialog focus state to app state.
  • .theme(read): Reads the active theme from app state.
  • .focus_policy(policy): Overrides Tab wrap behavior.
  • .render(frame, area, state): Paints the dialog overlay.
  • .handle_event(event, state): Routes input to the dialog focus scope.

Events

Dialog delegates event handling to its inner View. While a modal is open, route events to the dialog before the base UI. Tab and Shift+Tab move within the dialog according to its focus policy, and child components emit their own messages. Escape is not built into Dialog; close it from app-level key handling or from a child message such as Cancel.

Description vs Content

Use .description(...) for simple confirmation or alert dialogs. Use .content(...) when the body needs arbitrary Ratatui widgets or focusable children. If both are provided, custom content wins.

Focus Behavior

Dialog uses a nested View, so footer buttons and any custom content children share one focus path. The default focus policy wraps inside the dialog, which is usually what a modal should do.