Skip to main content

egui_charts/ui_kit/modal/
modal_handler.rs

1//! Modal Handler
2//!
3//! Manages modal open/close state with optional payload.
4
5use egui::{Context, Id, Ui};
6
7use crate::theme::ThemeExt;
8use crate::ui_kit::dialog::dialog_frame;
9
10/// Manages modal open/close state
11pub struct ModalHandler<T = ()> {
12    id: Id,
13    open: bool,
14    payload: Option<T>,
15}
16
17impl<T> Default for ModalHandler<T> {
18    fn default() -> Self {
19        Self {
20            id: Id::NULL,
21            open: false,
22            payload: None,
23        }
24    }
25}
26
27impl<T> ModalHandler<T> {
28    /// Create a new modal handler with the given ID
29    pub fn new(id: impl std::hash::Hash + std::fmt::Debug) -> Self {
30        Self {
31            id: Id::unique(id),
32            open: false,
33            payload: None,
34        }
35    }
36
37    /// Get the modal ID
38    pub fn id(&self) -> Id {
39        self.id
40    }
41
42    /// Open the modal without a payload
43    pub fn open(&mut self) {
44        self.open = true;
45    }
46
47    /// Open the modal with a payload
48    pub fn open_with(&mut self, payload: T) {
49        self.open = true;
50        self.payload = Some(payload);
51    }
52
53    /// Close the modal and clear the payload
54    pub fn close(&mut self) {
55        self.open = false;
56        self.payload = None;
57    }
58
59    /// Check if the modal is open
60    pub fn is_open(&self) -> bool {
61        self.open
62    }
63
64    /// Get a reference to the payload
65    pub fn payload(&self) -> Option<&T> {
66        self.payload.as_ref()
67    }
68
69    /// Get a mutable reference to the payload
70    pub fn payload_mut(&mut self) -> Option<&mut T> {
71        self.payload.as_mut()
72    }
73
74    /// Take the payload out of the handler
75    pub fn take_payload(&mut self) -> Option<T> {
76        self.payload.take()
77    }
78
79    /// Show the modal if open
80    ///
81    /// The content is drawn in the dialog frame, centred above a dimmed backdrop. Escape or a
82    /// click on the backdrop closes the modal.
83    ///
84    /// Returns the result of the content function if the modal was shown.
85    pub fn show<R>(
86        &mut self,
87        ctx: &Context,
88        add_contents: impl FnOnce(&mut Ui, Option<&T>) -> R,
89    ) -> Option<R> {
90        self.show_mut(ctx, |ui, payload| add_contents(ui, payload.map(|p| &*p)))
91    }
92
93    /// Show the modal with mutable payload access
94    pub fn show_mut<R>(
95        &mut self,
96        ctx: &Context,
97        add_contents: impl FnOnce(&mut Ui, Option<&mut T>) -> R,
98    ) -> Option<R> {
99        if !self.open {
100            return None;
101        }
102
103        let response = egui::Modal::new(self.id)
104            .frame(dialog_frame(ctx))
105            .backdrop_color(ctx.active_theme().ui.modal_overlay)
106            .show(ctx, |ui| add_contents(ui, self.payload.as_mut()));
107        if response.should_close() {
108            self.close();
109        }
110        Some(response.inner)
111    }
112}
113
114impl ModalHandler<()> {
115    /// Show the modal if open (simplified for no payload)
116    pub fn show_simple<R>(
117        &mut self,
118        ctx: &Context,
119        add_contents: impl FnOnce(&mut Ui) -> R,
120    ) -> Option<R> {
121        self.show(ctx, |ui, _| add_contents(ui))
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn modal_backdrops_follow_the_active_theme() {
131        use crate::theme::{Theme, apply_to_egui};
132        use crate::ui_kit::{command_palette::CommandPalette, modal::Modal};
133        use egui::{Color32, Pos2, RawInput, Rect, Shape, Vec2};
134        let screen = Rect::from_min_size(Pos2::ZERO, Vec2::new(1440.0, 900.0));
135        for (theme, alpha) in [
136            (Theme::dark(), 180),
137            (Theme::light(), 96),
138            (Theme::high_contrast(), 0),
139        ] {
140            for surface in 0..3 {
141                let ctx = Context::default();
142                apply_to_egui(&ctx, &theme);
143                ctx.all_styles_mut(|style| style.animation_time = 0.0);
144                let mut handler = ModalHandler::new(("backdrop", surface));
145                handler.open();
146                let mut palette = CommandPalette::new(Vec::new());
147                for frame in 0..3 {
148                    let output = ctx.run_ui(
149                        RawInput {
150                            screen_rect: Some(screen),
151                            ..Default::default()
152                        },
153                        |ui| match surface {
154                            0 => {
155                                Modal::new("backdrop", "Modal")
156                                    .show(ui.ctx(), &mut true, |ui| ui.label("Content"));
157                            }
158                            1 => {
159                                handler.show_simple(ui.ctx(), |ui| ui.label("Content"));
160                            }
161                            _ => {
162                                palette.show(ui.ctx(), &mut true);
163                            }
164                        },
165                    );
166                    let fills: Vec<_> = output
167                        .shapes
168                        .iter()
169                        .filter_map(|shape| match &shape.shape {
170                            Shape::Rect(rect)
171                                if rect.rect == screen
172                                    && rect.fill.a() > 0
173                                    && (alpha == 0 || rect.fill.a() < 255) =>
174                            {
175                                Some(rect.fill)
176                            }
177                            _ => None,
178                        })
179                        .collect();
180                    output.drop_without_applying_deltas();
181                    // The command palette leaves the chart undimmed, like the settings dialogs.
182                    let expected = if alpha == 0 || surface == 2 {
183                        vec![]
184                    } else {
185                        vec![Color32::from_black_alpha(alpha)]
186                    };
187                    if frame == 2 {
188                        assert_eq!(fills, expected, "dark: {}, surface: {surface}", theme.dark);
189                    }
190                }
191            }
192        }
193    }
194
195    #[test]
196    fn test_modal_handler_lifecycle() {
197        let mut handler: ModalHandler<String> = ModalHandler::new("test_modal");
198
199        assert!(!handler.is_open());
200        assert!(handler.payload().is_none());
201
202        handler.open_with("test_payload".to_string());
203        assert!(handler.is_open());
204        assert_eq!(handler.payload(), Some(&"test_payload".to_string()));
205
206        handler.close();
207        assert!(!handler.is_open());
208        assert!(handler.payload().is_none());
209    }
210}