1use crate::{
2 Area, Color32, Context, Frame, Id, InnerResponse, Order, Response, Sense, Ui, UiBuilder, UiKind,
3};
4use emath::{Align2, Vec2};
56/// 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 {
13pub area: Area,
14pub backdrop_color: Color32,
15pub frame: Option<Frame>,
16}
1718impl Modal {
19/// Create a new Modal. The id is passed to the area.
20pub fn new(id: Id) -> Self {
21Self {
22 area: Self::default_area(id),
23 backdrop_color: Color32::from_black_alpha(100),
24 frame: None,
25 }
26 }
2728/// Returns an area customized for a modal.
29 /// Makes these changes to the default area:
30 /// - sense: hover
31 /// - anchor: center
32 /// - order: foreground
33pub 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 }
4142/// Set the frame of the modal.
43 ///
44 /// Default is [`Frame::popup`].
45#[inline]
46pub fn frame(mut self, frame: Frame) -> Self {
47self.frame = Some(frame);
48self
49}
5051/// Set the backdrop color of the modal.
52 ///
53 /// Default is `Color32::from_black_alpha(100)`.
54#[inline]
55pub fn backdrop_color(mut self, color: Color32) -> Self {
56self.backdrop_color = color;
57self
58}
5960/// Set the area of the modal.
61 ///
62 /// Default is [`Modal::default_area`].
63#[inline]
64pub fn area(mut self, area: Area) -> Self {
65self.area = area;
66self
67}
6869/// Show the modal.
70pub fn show<T>(self, ctx: &Context, content: impl FnOnce(&mut Ui) -> T) -> ModalResponse<T> {
71let Self {
72 area,
73 backdrop_color,
74 frame,
75 } = self;
7677let (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 });
84let InnerResponse {
85 inner: (inner, backdrop_response),
86 response,
87 } = area.show(ctx, |ui| {
88let bg_rect = ui.ctx().screen_rect();
89let bg_sense = Sense {
90 click: true,
91 drag: true,
92 focusable: false,
93 };
94let 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);
97let backdrop_response = backdrop.response();
9899let frame = frame.unwrap_or_else(|| Frame::popup(ui.style()));
100101// 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.
103let 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;
113114 (inner, backdrop_response)
115 });
116117 ModalResponse {
118 response,
119 backdrop_response,
120 inner,
121 is_top_modal,
122 any_popup_open,
123 }
124 }
125}
126127/// The response of a modal dialog.
128pub struct ModalResponse<T> {
129/// The response of the modal contents
130pub response: Response,
131132/// 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.
136pub backdrop_response: Response,
137138/// The inner response from the content closure
139pub inner: T,
140141/// Is this the topmost modal?
142pub is_top_modal: bool,
143144/// 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.
147pub any_popup_open: bool,
148}
149150impl<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
155pub fn should_close(&self) -> bool {
156let ctx = &self.response.ctx;
157158// this is a closure so that `Esc` is consumed only if the modal is topmost
159let escape_clicked =
160 || ctx.input_mut(|i| i.consume_key(crate::Modifiers::NONE, crate::Key::Escape));
161162self.backdrop_response.clicked()
163 || (self.is_top_modal && !self.any_popup_open && escape_clicked())
164 }
165}