Skip to main content

egui_components/
dialog.rs

1//! `Dialog` and `AlertDialog` — modal overlays built on [`egui::Modal`].
2//!
3//! Both are controlled by an `&mut bool` you own. `Dialog` is a general
4//! container with a title + body closure; `AlertDialog` is a focused
5//! confirm/cancel prompt that reports which button was pressed.
6//!
7//! ```ignore
8//! if sc::Dialog::new("Edit profile").show(ctx, &mut self.open, |ui| {
9//!     ui.add(sc::Input::new(&mut self.name));
10//! }).is_some() { /* rendered this frame */ }
11//!
12//! match sc::AlertDialog::new("Delete file?")
13//!     .description("This cannot be undone.")
14//!     .danger()
15//!     .show(ctx, &mut self.confirm_open)
16//! {
17//!     Some(sc::AlertChoice::Confirm) => { /* delete */ }
18//!     Some(sc::AlertChoice::Cancel) | None => {}
19//! }
20//! ```
21
22use egui::{Frame, Id, Margin, Sense};
23use egui_components_theme::Theme;
24
25use crate::button::Button;
26use crate::common::{Size, Variant};
27use crate::icon::{paint_icon, IconKind};
28use crate::label::Label;
29
30/// A general modal dialog.
31pub struct Dialog {
32    title: String,
33    width: f32,
34    closable: bool,
35}
36
37impl Dialog {
38    pub fn new(title: impl Into<String>) -> Self {
39        Self {
40            title: title.into(),
41            width: 420.0,
42            closable: true,
43        }
44    }
45    pub fn width(mut self, w: f32) -> Self {
46        self.width = w;
47        self
48    }
49    /// Hide the header close (×) button (Esc / backdrop still close it).
50    pub fn no_close_button(mut self) -> Self {
51        self.closable = false;
52        self
53    }
54
55    /// Show the dialog while `*open`. Returns `Some(R)` (the body's value) on
56    /// the frames it is visible; sets `*open = false` when dismissed.
57    pub fn show<R>(
58        self,
59        ctx: &egui::Context,
60        open: &mut bool,
61        body: impl FnOnce(&mut egui::Ui) -> R,
62    ) -> Option<R> {
63        if !*open {
64            return None;
65        }
66        let theme = Theme::get(ctx);
67        let c = theme.colors;
68        let width = self.width;
69
70        let frame = Frame::new()
71            .fill(c.popover_background)
72            .stroke(theme.border_stroke())
73            .corner_radius(theme.corner_lg())
74            .inner_margin(Margin::same(20))
75            .shadow(egui::epaint::Shadow {
76                offset: [0, 8],
77                blur: 32,
78                spread: 0,
79                color: c.overlay,
80            });
81
82        let modal = egui::Modal::new(Id::new(("sc-dialog", self.title.as_str())))
83            .frame(frame)
84            .backdrop_color(c.overlay);
85
86        let title = self.title;
87        let closable = self.closable;
88        let res = modal.show(ctx, |ui| {
89            ui.set_width(width);
90            let mut close_requested = false;
91            ui.horizontal(|ui| {
92                ui.add(Label::new(title.clone()).strong().size(Size::Large));
93                if closable {
94                    ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
95                        let (rect, resp) =
96                            ui.allocate_exact_size(egui::vec2(20.0, 20.0), Sense::click());
97                        let col = if resp.hovered() {
98                            c.foreground
99                        } else {
100                            c.muted_foreground
101                        };
102                        paint_icon(ui.painter(), IconKind::Close, rect, col, 1.6);
103                        if resp.hovered() {
104                            ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
105                        }
106                        if resp.clicked() {
107                            close_requested = true;
108                        }
109                    });
110                }
111            });
112            ui.add_space(12.0);
113            let inner = body(ui);
114            (inner, close_requested)
115        });
116
117        let should_close = res.should_close();
118        let (inner, close_requested) = res.inner;
119        if should_close || close_requested {
120            *open = false;
121        }
122        Some(inner)
123    }
124}
125
126/// Which button an [`AlertDialog`] reported.
127#[derive(Clone, Copy, Debug, PartialEq, Eq)]
128pub enum AlertChoice {
129    Confirm,
130    Cancel,
131}
132
133/// A confirm / cancel prompt.
134pub struct AlertDialog {
135    title: String,
136    description: Option<String>,
137    confirm_label: String,
138    cancel_label: String,
139    confirm_variant: Variant,
140    width: f32,
141}
142
143impl AlertDialog {
144    pub fn new(title: impl Into<String>) -> Self {
145        Self {
146            title: title.into(),
147            description: None,
148            confirm_label: "Continue".to_string(),
149            cancel_label: "Cancel".to_string(),
150            confirm_variant: Variant::Primary,
151            width: 380.0,
152        }
153    }
154    pub fn description(mut self, d: impl Into<String>) -> Self {
155        self.description = Some(d.into());
156        self
157    }
158    pub fn confirm_label(mut self, s: impl Into<String>) -> Self {
159        self.confirm_label = s.into();
160        self
161    }
162    pub fn cancel_label(mut self, s: impl Into<String>) -> Self {
163        self.cancel_label = s.into();
164        self
165    }
166    /// Style the confirm button as destructive.
167    pub fn danger(mut self) -> Self {
168        self.confirm_variant = Variant::Danger;
169        self
170    }
171    pub fn width(mut self, w: f32) -> Self {
172        self.width = w;
173        self
174    }
175
176    /// Show while `*open`. Returns the chosen action on the frame a button is
177    /// pressed (or the dialog is dismissed), and sets `*open = false` then.
178    pub fn show(self, ctx: &egui::Context, open: &mut bool) -> Option<AlertChoice> {
179        if !*open {
180            return None;
181        }
182        let theme = Theme::get(ctx);
183        let c = theme.colors;
184        let width = self.width;
185
186        let frame = Frame::new()
187            .fill(c.popover_background)
188            .stroke(theme.border_stroke())
189            .corner_radius(theme.corner_lg())
190            .inner_margin(Margin::same(20))
191            .shadow(egui::epaint::Shadow {
192                offset: [0, 8],
193                blur: 32,
194                spread: 0,
195                color: c.overlay,
196            });
197
198        let modal = egui::Modal::new(Id::new(("sc-alert", self.title.as_str())))
199            .frame(frame)
200            .backdrop_color(c.overlay);
201
202        let AlertDialog {
203            title,
204            description,
205            confirm_label,
206            cancel_label,
207            confirm_variant,
208            ..
209        } = self;
210
211        let res = modal.show(ctx, |ui| {
212            ui.set_width(width);
213            ui.add(Label::new(title.clone()).strong().size(Size::Large));
214            if let Some(desc) = &description {
215                ui.add_space(6.0);
216                ui.add(Label::new(desc.clone()).muted());
217            }
218            ui.add_space(18.0);
219            let mut choice = None;
220            ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
221                if ui
222                    .add(Button::new(confirm_label.clone()).variant(confirm_variant))
223                    .clicked()
224                {
225                    choice = Some(AlertChoice::Confirm);
226                }
227                ui.add_space(8.0);
228                if ui.add(Button::secondary(cancel_label.clone())).clicked() {
229                    choice = Some(AlertChoice::Cancel);
230                }
231            });
232            choice
233        });
234
235        let choice = res.inner;
236        if let Some(ch) = choice {
237            *open = false;
238            return Some(ch);
239        }
240        if res.should_close() {
241            *open = false;
242            return Some(AlertChoice::Cancel);
243        }
244        None
245    }
246}