egui/containers/
modal.rs

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