egui/containers/
modal.rs

1use emath::{Align2, Vec2};
2
3use crate::{
4    Area, Color32, Context, Frame, Id, InnerResponse, Order, Response, Sense, Ui, UiBuilder, UiKind,
5};
6
7/// A modal dialog.
8///
9/// Similar to a [`crate::Window`] but centered and with a backdrop that
10/// blocks input to the rest of the UI.
11///
12/// You can show multiple modals on top of each other. The topmost modal will always be
13/// the most recently shown one.
14/// If multiple modals are newly shown in the same frame, the order of the modals not undefined
15/// (either first or second could be top).
16pub struct Modal {
17    pub area: Area,
18    pub backdrop_color: Color32,
19    pub frame: Option<Frame>,
20}
21
22impl Modal {
23    /// Create a new Modal.
24    ///
25    /// The id is passed to the area.
26    pub fn new(id: Id) -> Self {
27        Self {
28            area: Self::default_area(id),
29            backdrop_color: Color32::from_black_alpha(100),
30            frame: None,
31        }
32    }
33
34    /// Returns an area customized for a modal.
35    ///
36    /// Makes these changes to the default area:
37    /// - sense: hover
38    /// - anchor: center
39    /// - order: foreground
40    pub fn default_area(id: Id) -> Area {
41        Area::new(id)
42            .kind(UiKind::Modal)
43            .sense(Sense::hover())
44            .anchor(Align2::CENTER_CENTER, Vec2::ZERO)
45            .order(Order::Foreground)
46            .interactable(true)
47    }
48
49    /// Set the frame of the modal.
50    ///
51    /// Default is [`Frame::popup`].
52    #[inline]
53    pub fn frame(mut self, frame: Frame) -> Self {
54        self.frame = Some(frame);
55        self
56    }
57
58    /// Set the backdrop color of the modal.
59    ///
60    /// Default is `Color32::from_black_alpha(100)`.
61    #[inline]
62    pub fn backdrop_color(mut self, color: Color32) -> Self {
63        self.backdrop_color = color;
64        self
65    }
66
67    /// Set the area of the modal.
68    ///
69    /// Default is [`Modal::default_area`].
70    #[inline]
71    pub fn area(mut self, area: Area) -> Self {
72        self.area = area;
73        self
74    }
75
76    /// Show the modal.
77    pub fn show<T>(self, ctx: &Context, content: impl FnOnce(&mut Ui) -> T) -> ModalResponse<T> {
78        let Self {
79            area,
80            backdrop_color,
81            frame,
82        } = self;
83
84        let is_top_modal = ctx.memory_mut(|mem| {
85            mem.set_modal_layer(area.layer());
86            mem.top_modal_layer() == Some(area.layer())
87        });
88        let any_popup_open = crate::Popup::is_any_open(ctx);
89        let InnerResponse {
90            inner: (inner, backdrop_response),
91            response,
92        } = area.show(ctx, |ui| {
93            let bg_rect = ui.ctx().screen_rect();
94            let bg_sense = Sense::CLICK | Sense::DRAG;
95            let mut backdrop = ui.new_child(UiBuilder::new().sense(bg_sense).max_rect(bg_rect));
96            backdrop.set_min_size(bg_rect.size());
97            ui.painter().rect_filled(bg_rect, 0.0, backdrop_color);
98            let backdrop_response = backdrop.response();
99
100            let frame = frame.unwrap_or_else(|| Frame::popup(ui.style()));
101
102            // We need the extra scope with the sense since frame can't have a sense and since we
103            // need to prevent the clicks from passing through to the backdrop.
104            let inner = ui
105                .scope_builder(UiBuilder::new().sense(Sense::CLICK | Sense::DRAG), |ui| {
106                    frame.show(ui, content).inner
107                })
108                .inner;
109
110            (inner, backdrop_response)
111        });
112
113        ModalResponse {
114            response,
115            backdrop_response,
116            inner,
117            is_top_modal,
118            any_popup_open,
119        }
120    }
121}
122
123/// The response of a modal dialog.
124pub struct ModalResponse<T> {
125    /// The response of the modal contents
126    pub response: Response,
127
128    /// The response of the modal backdrop.
129    ///
130    /// A click on this means the user clicked outside the modal,
131    /// in which case you might want to close the modal.
132    pub backdrop_response: Response,
133
134    /// The inner response from the content closure
135    pub inner: T,
136
137    /// Is this the topmost modal?
138    pub is_top_modal: bool,
139
140    /// Is there any popup open?
141    /// We need to check this before the modal contents are shown, so we can know if any popup
142    /// was open when checking if the escape key was clicked.
143    pub any_popup_open: bool,
144}
145
146impl<T> ModalResponse<T> {
147    /// Should the modal be closed?
148    /// Returns true if:
149    ///  - the backdrop was clicked
150    ///  - this is the topmost modal, no popup is open and the escape key was pressed
151    pub fn should_close(&self) -> bool {
152        let ctx = &self.response.ctx;
153
154        // this is a closure so that `Esc` is consumed only if the modal is topmost
155        let escape_clicked =
156            || ctx.input_mut(|i| i.consume_key(crate::Modifiers::NONE, crate::Key::Escape));
157
158        let ui_close_called = self.response.should_close();
159
160        self.backdrop_response.clicked()
161            || ui_close_called
162            || (self.is_top_modal && !self.any_popup_open && escape_clicked())
163    }
164}