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

Message Dialog

Group: Overlays

Code

pub struct MessageDialogDemo {
    dialog: Option<MessageDialogController>,
    status: String,
}

impl MessageDialogDemo {
    pub fn new() -> Self {
        Self {
            dialog: None,
            status: "press Enter to open the dialog".into(),
        }
    }

    fn controller_dialog() -> MessageDialogController {
        MessageDialogController::new(MessageDialogOptions {
            title: "Unsaved Changes".to_string(),
            body: "You have unsaved changes.\nDo you want to save before closing?".to_string(),
            buttons: vec![
                MessageDialogButton {
                    id: WidgetId::new("gallery:message-dialog:save"),
                    label: "Save".into(),
                    is_default: true,
                    is_cancel: false,
                },
                MessageDialogButton {
                    id: WidgetId::new("gallery:message-dialog:discard"),
                    label: "Discard".into(),
                    is_default: false,
                    is_cancel: true,
                },
            ],
            severity: Some(DialogSeverity::Warning),
        })
    }

    /// Same descriptor shape as [`Self::controller_dialog`], as a raw
    /// [`Dialog`] — [`native_dialog_options`] maps this to
    /// [`MessageDialogOptions`] for the native-service variant. Has no
    /// `table`/`input`, so the mapping always succeeds.
    fn native_dialog() -> Dialog {
        Dialog {
            id: WidgetId::new("gallery:message-dialog:native"),
            title: StyledText::plain("Discard unsaved changes?"),
            body: vec![StyledText::plain(
                "main.rs has unsaved changes. This cannot be undone.",
            )],
            buttons: vec![
                DialogButton {
                    id: WidgetId::new("gallery:message-dialog:keep"),
                    label: "Keep Editing".to_string(),
                    is_default: true,
                    is_cancel: true,
                    tint: None,
                },
                DialogButton {
                    id: WidgetId::new("gallery:message-dialog:discard-native"),
                    label: "Discard".to_string(),
                    is_default: false,
                    is_cancel: false,
                    tint: Some(Color::rgb(200, 60, 60)),
                },
            ],
            severity: Some(DialogSeverity::Warning),
            vertical_buttons: false,
            table: None,
            input: None,
        }
    }

    fn status_bar(&self, variant: usize) -> StatusBar {
        let variant_label = if variant == 0 {
            "Direct controller"
        } else {
            "Native service"
        };
        StatusBar {
            id: WidgetId::new("gallery:message-dialog:status"),
            left_segments: vec![StatusBarSegment {
                text: format!(" [{variant_label}] {} ", self.status),
                fg: Color::rgb(220, 220, 220),
                bg: Color::rgb(40, 60, 100),
                bold: false,
                action_id: None,
            }],
            right_segments: vec![StatusBarSegment {
                text: " Enter=open ".into(),
                fg: Color::rgb(180, 180, 180),
                bg: Color::rgb(40, 60, 100),
                bold: false,
                action_id: None,
            }],
        }
    }
}

impl Default for MessageDialogDemo {
    fn default() -> Self {
        Self::new()
    }
}

impl Demo for MessageDialogDemo {
    fn name(&self) -> &'static str {
        "Message Dialog"
    }

    fn group(&self) -> &'static str {
        "Overlays"
    }

    fn variants(&self) -> &'static [&'static str] {
        &["Direct controller", "Native service"]
    }

    fn render(&self, variant: usize, backend: &mut dyn Backend, area: Rect) {
        let lh = backend.line_height();
        let bar_rect = Rect::new(area.x, area.y, area.width, lh);
        let _ = backend.draw_status_bar_interactive(
            bar_rect,
            &self.status_bar(variant),
            &InteractionState::new(),
        );

        if variant == 0 {
            if let Some(dialog) = &self.dialog {
                dialog.render(backend);
            }
        }
        // The native-service variant (`variant == 1`) never has a
        // lingering `self.dialog` to render: `handle` below resolves it
        // synchronously inside the same call that opens it (either via a
        // real native alert on GTK, or the in-canvas nested loop on TUI),
        // so there is nothing left open by the time `render` runs again.
    }

    fn handle(
        &mut self,
        variant: usize,
        event: &UiEvent,
        backend: &mut dyn Backend,
        _area: Rect,
    ) -> Reaction {
        if variant == 0 {
            if let Some(ref mut dialog) = self.dialog {
                match dialog.handle(event, backend) {
                    MessageDialogEvent::Resolved(id) => {
                        self.status = format!("Resolved: {}", id.as_str());
                        self.dialog = None;
                        return Reaction::Redraw;
                    }
                    MessageDialogEvent::Consumed => return Reaction::Redraw,
                    MessageDialogEvent::Ignored => {}
                }
            } else if let UiEvent::KeyPressed {
                key: Key::Named(quadraui::NamedKey::Enter),
                ..
            } = event
            {
                self.dialog = Some(Self::controller_dialog());
                self.status = "Save changes? — choose Save or Discard".into();
                return Reaction::Redraw;
            }
            return Reaction::Continue;
        }

        // Native-service variant.
        if let UiEvent::KeyPressed {
            key: Key::Named(quadraui::NamedKey::Enter),
            ..
        } = event
        {
            let dialog = Self::native_dialog();
            self.status = match native_dialog_options(&dialog) {
                Some(opts) => match backend.services().show_message_dialog(opts) {
                    Some(id) if id == dialog.buttons[0].id => "Kept editing".to_string(),
                    Some(id) if id == dialog.buttons[1].id => "Discarded".to_string(),
                    Some(other) => format!("Unexpected button: {other:?}"),
                    None => "Cancelled (or unsupported on this backend)".to_string(),
                },
                None => "This dialog has no native mapping".to_string(),
            };
            return Reaction::Redraw;
        }
        Reaction::Continue
    }

    fn source(&self) -> &'static str {
        extract_region(SOURCE)
    }

    fn data(&self, variant: usize) -> serde_json::Value {
        serde_json::json!({
            "variant": if variant == 0 { "direct_controller" } else { "native_service" },
            "status": self.status,
            "open": self.dialog.is_some(),
        })
    }

    fn caps_note(&self, variant: usize, caps: &BackendCaps) -> Option<String> {
        if variant == 1 && !caps.native_dialogs {
            Some(
                "This backend has no native alert facility (BackendCaps::native_dialogs is \
                 false) — show_message_dialog drives the same MessageDialogController in-canvas \
                 instead, through a nested draw-and-read loop, and still returns a real chosen \
                 button."
                    .into(),
            )
        } else {
            None
        }
    }
}

Backends

VariantTUIGTKmacOSWindows
Direct controller
Native service