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 {
90                click: true,
91                drag: true,
92                focusable: false,
93            };
94            let mut backdrop = ui.new_child(UiBuilder::new().sense(bg_sense).max_rect(bg_rect));
95            backdrop.set_min_size(bg_rect.size());
96            ui.painter().rect_filled(bg_rect, 0.0, backdrop_color);
97            let backdrop_response = backdrop.response();
98
99            let frame = frame.unwrap_or_else(|| Frame::popup(ui.style()));
100
101            // We need the extra scope with the sense since frame can't have a sense and since we
102            // need to prevent the clicks from passing through to the backdrop.
103            let inner = ui
104                .scope_builder(
105                    UiBuilder::new().sense(Sense {
106                        click: true,
107                        drag: true,
108                        focusable: false,
109                    }),
110                    |ui| frame.show(ui, content).inner,
111                )
112                .inner;
113
114            (inner, backdrop_response)
115        });
116
117        ModalResponse {
118            response,
119            backdrop_response,
120            inner,
121            is_top_modal,
122            any_popup_open,
123        }
124    }
125}
126
127/// The response of a modal dialog.
128pub struct ModalResponse<T> {
129    /// The response of the modal contents
130    pub response: Response,
131
132    /// The response of the modal backdrop.
133    ///
134    /// A click on this means the user clicked outside the modal,
135    /// in which case you might want to close the modal.
136    pub backdrop_response: Response,
137
138    /// The inner response from the content closure
139    pub inner: T,
140
141    /// Is this the topmost modal?
142    pub is_top_modal: bool,
143
144    /// Is there any popup open?
145    /// We need to check this before the modal contents are shown, so we can know if any popup
146    /// was open when checking if the escape key was clicked.
147    pub any_popup_open: bool,
148}
149
150impl<T> ModalResponse<T> {
151    /// Should the modal be closed?
152    /// Returns true if:
153    ///  - the backdrop was clicked
154    ///  - this is the topmost modal, no popup is open and the escape key was pressed
155    pub fn should_close(&self) -> bool {
156        let ctx = &self.response.ctx;
157
158        // this is a closure so that `Esc` is consumed only if the modal is topmost
159        let escape_clicked =
160            || ctx.input_mut(|i| i.consume_key(crate::Modifiers::NONE, crate::Key::Escape));
161
162        self.backdrop_response.clicked()
163            || (self.is_top_modal && !self.any_popup_open && escape_clicked())
164    }
165}