1use crate::overlay::OverlayDismissCallback;
5use crate::widget_id::WidgetId;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9pub enum ModalPresentation {
10 #[default]
12 Auto,
13 InTree,
15 NativeWindow,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
21pub enum ModalCloseBehavior {
22 ClickOutside,
24 EscapeKey,
26 #[default]
28 EscapeOrClickOutside,
29 Manual,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Default)]
38pub enum ModalPlacement {
39 #[default]
41 Centered,
42 Top {
49 offset: f32,
51 },
52}
53
54pub type ModalBuilder = Box<dyn FnOnce(&mut crate::widget_tree::WidgetTree) -> WidgetId>;
56
57pub enum ModalContent {
59 ExistingWidget(WidgetId),
61 Deferred(ModalBuilder),
63}
64
65impl std::fmt::Debug for ModalContent {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 match self {
68 Self::ExistingWidget(id) => f.debug_tuple("ExistingWidget").field(id).finish(),
69 Self::Deferred(_) => f.write_str("Deferred(..)"),
70 }
71 }
72}
73
74pub struct ModalRequest {
76 pub content: ModalContent,
77 pub presentation: ModalPresentation,
78 pub close_behavior: ModalCloseBehavior,
79 pub title: Option<String>,
80 pub size: Option<(u32, u32)>,
81 pub placement: ModalPlacement,
84 pub focus_target: Option<WidgetId>,
93 pub on_dismiss: Option<OverlayDismissCallback>,
98}
99
100impl ModalRequest {
101 pub fn in_tree(content_id: WidgetId) -> Self {
103 Self {
104 content: ModalContent::ExistingWidget(content_id),
105 presentation: ModalPresentation::Auto,
106 close_behavior: ModalCloseBehavior::default(),
107 title: None,
108 size: None,
109 placement: ModalPlacement::Centered,
110 focus_target: None,
111 on_dismiss: None,
112 }
113 }
114
115 pub fn deferred(
117 builder: impl FnOnce(&mut crate::widget_tree::WidgetTree) -> WidgetId + 'static,
118 ) -> Self {
119 Self {
120 content: ModalContent::Deferred(Box::new(builder)),
121 presentation: ModalPresentation::Auto,
122 close_behavior: ModalCloseBehavior::default(),
123 title: None,
124 size: None,
125 placement: ModalPlacement::Centered,
126 focus_target: None,
127 on_dismiss: None,
128 }
129 }
130
131 pub fn presentation(mut self, presentation: ModalPresentation) -> Self {
132 self.presentation = presentation;
133 self
134 }
135
136 pub fn close_behavior(mut self, close_behavior: ModalCloseBehavior) -> Self {
137 self.close_behavior = close_behavior;
138 self
139 }
140
141 pub fn title(mut self, title: impl Into<String>) -> Self {
142 self.title = Some(title.into());
143 self
144 }
145
146 pub fn size(mut self, width: u32, height: u32) -> Self {
147 self.size = Some((width, height));
148 self
149 }
150
151 pub fn placement(mut self, placement: ModalPlacement) -> Self {
153 self.placement = placement;
154 self
155 }
156
157 pub fn on_dismiss(mut self, callback: OverlayDismissCallback) -> Self {
161 self.on_dismiss = Some(callback);
162 self
163 }
164
165 pub fn focus_target(mut self, id: WidgetId) -> Self {
171 self.focus_target = Some(id);
172 self
173 }
174}
175
176impl std::fmt::Debug for ModalRequest {
177 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178 f.debug_struct("ModalRequest")
179 .field("content", &self.content)
180 .field("presentation", &self.presentation)
181 .field("close_behavior", &self.close_behavior)
182 .field("title", &self.title)
183 .field("size", &self.size)
184 .field("placement", &self.placement)
185 .field("focus_target", &self.focus_target)
186 .field(
187 "on_dismiss",
188 &self.on_dismiss.as_ref().map(|_| "<callback>"),
189 )
190 .finish()
191 }
192}
193
194#[derive(Debug)]
196pub struct QueuedModalRequest {
197 pub source_widget: WidgetId,
198 pub request: ModalRequest,
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204 use crate::test_widgets::FillWidget;
205
206 #[test]
207 fn in_tree_request_defaults_to_auto() {
208 let mut tree = crate::WidgetTree::new();
209 let content_id = tree.add(FillWidget::new());
210 let request = ModalRequest::in_tree(content_id);
211 assert_eq!(request.presentation, ModalPresentation::Auto);
212 assert_eq!(
213 request.close_behavior,
214 ModalCloseBehavior::EscapeOrClickOutside
215 );
216 match request.content {
217 ModalContent::ExistingWidget(id) => assert_eq!(id, content_id),
218 ModalContent::Deferred(_) => panic!("expected ExistingWidget content"),
219 }
220 }
221
222 #[test]
223 fn deferred_request_can_override_metadata() {
224 let request =
225 ModalRequest::deferred(|tree| tree.add(crate::test_widgets::FillWidget::new()))
226 .presentation(ModalPresentation::NativeWindow)
227 .close_behavior(ModalCloseBehavior::Manual)
228 .title("Preferences")
229 .size(640, 480);
230
231 assert_eq!(request.presentation, ModalPresentation::NativeWindow);
232 assert_eq!(request.close_behavior, ModalCloseBehavior::Manual);
233 assert_eq!(request.title.as_deref(), Some("Preferences"));
234 assert_eq!(request.size, Some((640, 480)));
235 assert!(matches!(request.content, ModalContent::Deferred(_)));
236 }
237
238 #[test]
239 fn focus_target_defaults_to_none() {
240 let mut tree = crate::WidgetTree::new();
241 let content_id = tree.add(FillWidget::new());
242 let in_tree = ModalRequest::in_tree(content_id);
243 assert!(in_tree.focus_target.is_none());
244
245 let deferred =
246 ModalRequest::deferred(|tree| tree.add(crate::test_widgets::FillWidget::new()));
247 assert!(deferred.focus_target.is_none());
248 }
249
250 #[test]
251 fn focus_target_builder_sets_field() {
252 let mut tree = crate::WidgetTree::new();
253 let content_id = tree.add(FillWidget::new());
254 let target_id = tree.add(FillWidget::new());
255 let request = ModalRequest::in_tree(content_id).focus_target(target_id);
256 assert_eq!(request.focus_target, Some(target_id));
257 }
258}