Skip to main content

guise/overlay/
confirm.rs

1//! `ConfirmModal` — a yes/no dialog built on [`Modal`](super::Modal).
2//!
3//! Controlled exactly like `Modal`: the parent owns an `opened` flag, renders
4//! the `ConfirmModal` only while it is true, and flips the flag from
5//! `on_confirm` / `on_cancel`. The backdrop and the header `×` also run
6//! `on_cancel`. A message string covers the common case; extra body content
7//! can be added as children (`ParentElement`).
8//!
9//! ```ignore
10//! if self.confirm_open {
11//!     root = root.child(
12//!         ConfirmModal::new()
13//!             .title("Delete file?")
14//!             .message("del.rs will be moved to the Trash.")
15//!             .confirm_label("Delete")
16//!             .danger()
17//!             .on_confirm(cx.listener(|this, _ev, _w, cx| { this.confirm_open = false; cx.notify(); }))
18//!             .on_cancel(cx.listener(|this, _ev, _w, cx| { this.confirm_open = false; cx.notify(); })),
19//!     );
20//! }
21//! ```
22
23use std::rc::Rc;
24
25use gpui::prelude::*;
26use gpui::{div, px, AnyElement, App, ClickEvent, IntoElement, SharedString, Window};
27
28use super::Modal;
29use crate::button::Button;
30use crate::style::Variant;
31use crate::text::Text;
32use crate::theme::{theme, ColorName, Size};
33
34type Handler = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;
35
36/// A confirm/cancel dialog. The Mantine `modals.openConfirmModal`, as a
37/// controlled component.
38#[derive(IntoElement)]
39pub struct ConfirmModal {
40    title: Option<SharedString>,
41    message: Option<SharedString>,
42    children: Vec<AnyElement>,
43    confirm_label: SharedString,
44    cancel_label: SharedString,
45    danger: bool,
46    width: Option<f32>,
47    on_confirm: Option<Handler>,
48    on_cancel: Option<Handler>,
49}
50
51impl ConfirmModal {
52    pub fn new() -> Self {
53        ConfirmModal {
54            title: None,
55            message: None,
56            children: Vec::new(),
57            confirm_label: SharedString::new_static("Confirm"),
58            cancel_label: SharedString::new_static("Cancel"),
59            danger: false,
60            width: None,
61            on_confirm: None,
62            on_cancel: None,
63        }
64    }
65
66    pub fn title(mut self, title: impl Into<SharedString>) -> Self {
67        self.title = Some(title.into());
68        self
69    }
70
71    /// The dimmed body text. For richer content add children instead (or too).
72    pub fn message(mut self, message: impl Into<SharedString>) -> Self {
73        self.message = Some(message.into());
74        self
75    }
76
77    /// Label of the confirming button (default `"Confirm"`).
78    pub fn confirm_label(mut self, label: impl Into<SharedString>) -> Self {
79        self.confirm_label = label.into();
80        self
81    }
82
83    /// Label of the cancelling button (default `"Cancel"`).
84    pub fn cancel_label(mut self, label: impl Into<SharedString>) -> Self {
85        self.cancel_label = label.into();
86        self
87    }
88
89    /// Render the confirm button in red for destructive actions.
90    pub fn danger(mut self) -> Self {
91        self.danger = true;
92        self
93    }
94
95    /// Dialog width in pixels (defaults to `Modal`'s 440).
96    pub fn width(mut self, width: f32) -> Self {
97        self.width = Some(width);
98        self
99    }
100
101    /// Called when the confirm button is clicked. Close the dialog here.
102    pub fn on_confirm(
103        mut self,
104        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
105    ) -> Self {
106        self.on_confirm = Some(Rc::new(handler));
107        self
108    }
109
110    /// Called on cancel — the cancel button, the backdrop, and the header `×`.
111    pub fn on_cancel(
112        mut self,
113        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
114    ) -> Self {
115        self.on_cancel = Some(Rc::new(handler));
116        self
117    }
118}
119
120impl Default for ConfirmModal {
121    fn default() -> Self {
122        ConfirmModal::new()
123    }
124}
125
126impl ParentElement for ConfirmModal {
127    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
128        self.children.extend(elements);
129    }
130}
131
132impl RenderOnce for ConfirmModal {
133    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
134        let t = theme(cx);
135        let gap = t.spacing(Size::Sm);
136
137        let mut modal = Modal::new();
138        if let Some(width) = self.width {
139            modal = modal.width(width);
140        }
141        if let Some(title) = self.title {
142            modal = modal.title(title);
143        }
144        if let Some(cancel) = self.on_cancel.clone() {
145            modal = modal.on_close(move |ev, window, cx| cancel(ev, window, cx));
146        }
147        if let Some(message) = self.message {
148            modal = modal.child(Text::new(message).dimmed().size(Size::Sm));
149        }
150        modal = modal.children(self.children);
151
152        let mut cancel_button =
153            Button::new("guise-confirm-cancel", self.cancel_label).variant(Variant::Default);
154        if let Some(handler) = self.on_cancel {
155            cancel_button = cancel_button.on_click(move |ev, window, cx| handler(ev, window, cx));
156        }
157
158        let mut confirm_button = Button::new("guise-confirm-accept", self.confirm_label);
159        if self.danger {
160            confirm_button = confirm_button.color(ColorName::Red);
161        }
162        if let Some(handler) = self.on_confirm {
163            confirm_button = confirm_button.on_click(move |ev, window, cx| handler(ev, window, cx));
164        }
165
166        modal.child(
167            div()
168                .flex()
169                .justify_end()
170                .gap(px(gap))
171                .child(cancel_button)
172                .child(confirm_button),
173        )
174    }
175}