1use std::collections::HashMap;
16
17use fret_runtime::Model;
18use fret_runtime::ModelId;
19use fret_ui::action::{OnCloseAutoFocus, OnOpenAutoFocus};
20use fret_ui::element::{AnyElement, Elements, LayoutStyle};
21use fret_ui::elements::GlobalElementId;
22use fret_ui::{ElementContext, UiHost};
23
24use crate::declarative::ModelWatchExt;
25use crate::primitives::dialog as dialog_prim;
26use crate::primitives::dialog::DialogOptions;
27use crate::primitives::trigger_a11y;
28use crate::{IntoUiElement, OverlayPresence, OverlayRequest, collect_children};
29
30pub fn alert_dialog_root_name(id: GlobalElementId) -> String {
32 dialog_prim::dialog_root_name(id)
33}
34
35pub fn alert_dialog_use_open_model<H: UiHost>(
41 cx: &mut ElementContext<'_, H>,
42 controlled_open: Option<Model<bool>>,
43 default_open: impl FnOnce() -> bool,
44) -> crate::primitives::controllable_state::ControllableModel<bool> {
45 crate::primitives::open_state::open_use_model(cx, controlled_open, default_open)
46}
47
48#[derive(Debug, Clone, Default)]
55pub struct AlertDialogRoot {
56 open: Option<Model<bool>>,
57 default_open: bool,
58 options: AlertDialogOptions,
59}
60
61impl AlertDialogRoot {
62 pub fn new() -> Self {
63 Self::default()
64 }
65
66 pub fn open(mut self, open: Option<Model<bool>>) -> Self {
68 self.open = open;
69 self
70 }
71
72 pub fn default_open(mut self, default_open: bool) -> Self {
74 self.default_open = default_open;
75 self
76 }
77
78 pub fn initial_focus(mut self, initial_focus: AlertDialogInitialFocus) -> Self {
79 self.options = self.options.initial_focus(initial_focus);
80 self
81 }
82
83 pub fn options(&self) -> AlertDialogOptions {
84 self.options.clone()
85 }
86
87 pub fn use_open_model<H: UiHost>(
89 &self,
90 cx: &mut ElementContext<'_, H>,
91 ) -> crate::primitives::controllable_state::ControllableModel<bool> {
92 alert_dialog_use_open_model(cx, self.open.clone(), || self.default_open)
93 }
94
95 pub fn open_model<H: UiHost>(&self, cx: &mut ElementContext<'_, H>) -> Model<bool> {
96 self.use_open_model(cx).model()
97 }
98
99 pub fn open_id<H: UiHost>(&self, cx: &mut ElementContext<'_, H>) -> ModelId {
100 self.open_model(cx).id()
101 }
102
103 pub fn is_open<H: UiHost>(&self, cx: &mut ElementContext<'_, H>) -> bool {
104 let open_model = self.open_model(cx);
105 cx.watch_model(&open_model)
106 .layout()
107 .copied()
108 .unwrap_or(false)
109 }
110
111 pub fn dialog_options<H: UiHost>(&self, cx: &mut ElementContext<'_, H>) -> DialogOptions {
112 let open_id = self.open_id(cx);
113 dialog_options_for_alert_dialog(cx, open_id, self.options.clone())
114 }
115
116 pub fn modal_request<H: UiHost, I, T>(
117 &self,
118 cx: &mut ElementContext<'_, H>,
119 id: GlobalElementId,
120 trigger: GlobalElementId,
121 presence: OverlayPresence,
122 children: I,
123 ) -> OverlayRequest
124 where
125 I: IntoIterator<Item = T>,
126 T: IntoUiElement<H>,
127 {
128 let open = self.open_model(cx);
129 let options = self.dialog_options(cx);
130
131 dialog_prim::modal_dialog_request_with_options(
132 id,
133 trigger,
134 open,
135 presence,
136 options,
137 collect_children(cx, children),
138 )
139 }
140}
141
142#[derive(Default)]
143struct AlertDialogCancelRegistry {
144 by_open: HashMap<ModelId, GlobalElementId>,
145}
146
147pub fn register_cancel_for_open_model<H: UiHost>(
152 cx: &mut ElementContext<'_, H>,
153 open_id: ModelId,
154 element: GlobalElementId,
155) {
156 cx.app
157 .with_global_mut_untracked(AlertDialogCancelRegistry::default, |reg, _app| {
158 reg.by_open.entry(open_id).or_insert(element);
159 });
160}
161
162pub fn clear_cancel_for_open_model<H: UiHost>(cx: &mut ElementContext<'_, H>, open_id: ModelId) {
164 cx.app
165 .with_global_mut_untracked(AlertDialogCancelRegistry::default, |reg, _app| {
166 reg.by_open.remove(&open_id);
167 });
168}
169
170pub fn cancel_element_for_open_model<H: UiHost>(
173 cx: &mut ElementContext<'_, H>,
174 open_id: ModelId,
175) -> Option<GlobalElementId> {
176 cx.app
177 .with_global_mut_untracked(AlertDialogCancelRegistry::default, |reg, _app| {
178 reg.by_open.get(&open_id).copied()
179 })
180}
181
182#[derive(Debug, Clone, Copy, Default)]
183pub enum AlertDialogInitialFocus {
184 None,
185 #[default]
186 PreferCancel,
187 Element(GlobalElementId),
188}
189
190#[derive(Clone, Default)]
191pub struct AlertDialogOptions {
192 pub initial_focus: AlertDialogInitialFocus,
193 pub on_open_auto_focus: Option<OnOpenAutoFocus>,
194 pub on_close_auto_focus: Option<OnCloseAutoFocus>,
195}
196
197impl std::fmt::Debug for AlertDialogOptions {
198 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199 f.debug_struct("AlertDialogOptions")
200 .field("initial_focus", &self.initial_focus)
201 .field("on_open_auto_focus", &self.on_open_auto_focus.is_some())
202 .field("on_close_auto_focus", &self.on_close_auto_focus.is_some())
203 .finish()
204 }
205}
206
207impl AlertDialogOptions {
208 pub fn initial_focus(mut self, initial_focus: AlertDialogInitialFocus) -> Self {
209 self.initial_focus = initial_focus;
210 self
211 }
212
213 pub fn on_open_auto_focus(mut self, hook: Option<OnOpenAutoFocus>) -> Self {
214 self.on_open_auto_focus = hook;
215 self
216 }
217
218 pub fn on_close_auto_focus(mut self, hook: Option<OnCloseAutoFocus>) -> Self {
219 self.on_close_auto_focus = hook;
220 self
221 }
222}
223
224pub fn dialog_options_for_alert_dialog<H: UiHost>(
226 cx: &mut ElementContext<'_, H>,
227 open_id: ModelId,
228 options: AlertDialogOptions,
229) -> DialogOptions {
230 let initial_focus = match options.initial_focus {
231 AlertDialogInitialFocus::None => None,
232 AlertDialogInitialFocus::Element(id) => Some(id),
233 AlertDialogInitialFocus::PreferCancel => cancel_element_for_open_model(cx, open_id),
234 };
235
236 DialogOptions::default()
237 .dismiss_on_overlay_press(false)
238 .initial_focus(initial_focus)
239 .on_open_auto_focus(options.on_open_auto_focus.clone())
240 .on_close_auto_focus(options.on_close_auto_focus.clone())
241}
242
243pub fn alert_dialog_modal_barrier_layout() -> LayoutStyle {
247 dialog_prim::modal_barrier_layout()
248}
249
250pub fn alert_dialog_modal_barrier<H: UiHost, I, T>(
252 cx: &mut ElementContext<'_, H>,
253 open: Model<bool>,
254 children: I,
255) -> AnyElement
256where
257 I: IntoIterator<Item = T>,
258 T: IntoUiElement<H>,
259{
260 dialog_prim::modal_barrier(cx, open, false, children)
261}
262
263pub fn alert_dialog_modal_layer_elements<H: UiHost, I, T>(
266 cx: &mut ElementContext<'_, H>,
267 open: Model<bool>,
268 barrier_children: I,
269 content: AnyElement,
270) -> Elements
271where
272 I: IntoIterator<Item = T>,
273 T: IntoUiElement<H>,
274{
275 Elements::from([
276 alert_dialog_modal_barrier(cx, open, barrier_children),
277 content,
278 ])
279}
280
281pub fn apply_alert_dialog_trigger_a11y(
285 trigger: AnyElement,
286 expanded: bool,
287 content_element: Option<GlobalElementId>,
288) -> AnyElement {
289 trigger_a11y::apply_trigger_controls_expanded(trigger, Some(expanded), content_element)
290}
291
292pub fn alert_dialog_modal_request_with_options(
294 id: GlobalElementId,
295 trigger: GlobalElementId,
296 open: Model<bool>,
297 presence: OverlayPresence,
298 options: DialogOptions,
299 children: impl IntoIterator<Item = AnyElement>,
300) -> OverlayRequest {
301 dialog_prim::modal_dialog_request_with_options(id, trigger, open, presence, options, children)
302}
303
304pub fn request_alert_dialog<H: UiHost>(cx: &mut ElementContext<'_, H>, request: OverlayRequest) {
306 dialog_prim::request_modal_dialog(cx, request);
307}
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312
313 use fret_app::App;
314 use fret_core::{AppWindowId, Point, Px, Rect, Size};
315
316 fn bounds() -> Rect {
317 Rect::new(
318 Point::new(Px(0.0), Px(0.0)),
319 Size::new(Px(200.0), Px(120.0)),
320 )
321 }
322
323 #[test]
324 fn alert_dialog_root_open_model_uses_controlled_model() {
325 let window = AppWindowId::default();
326 let mut app = App::new();
327 let b = bounds();
328
329 let controlled = app.models_mut().insert(true);
330
331 fret_ui::elements::with_element_cx(&mut app, window, b, "test", |cx| {
332 let root = AlertDialogRoot::new()
333 .open(Some(controlled.clone()))
334 .default_open(false);
335 assert_eq!(root.open_model(cx), controlled);
336 });
337 }
338
339 #[test]
340 fn alert_dialog_root_options_builder_updates_options() {
341 let root = AlertDialogRoot::new().initial_focus(AlertDialogInitialFocus::None);
342 let options = root.options();
343 assert!(matches!(
344 options.initial_focus,
345 AlertDialogInitialFocus::None
346 ));
347 }
348
349 #[test]
350 fn registry_prefers_first_cancel_and_can_be_cleared() {
351 let window = AppWindowId::default();
352 let mut app = App::new();
353 let b = bounds();
354
355 let open = app.models_mut().insert(false);
356 let open_id = open.id();
357 fret_ui::elements::with_element_cx(&mut app, window, b, "test", |cx| {
358 register_cancel_for_open_model(cx, open_id, GlobalElementId(0xaaa));
359 register_cancel_for_open_model(cx, open_id, GlobalElementId(0xbbb));
360 assert_eq!(
361 cancel_element_for_open_model(cx, open_id),
362 Some(GlobalElementId(0xaaa))
363 );
364 clear_cancel_for_open_model(cx, open_id);
365 assert_eq!(cancel_element_for_open_model(cx, open_id), None);
366 });
367 }
368
369 #[test]
370 fn dialog_options_for_alert_dialog_prefers_cancel() {
371 let window = AppWindowId::default();
372 let mut app = App::new();
373 let b = bounds();
374
375 let open = app.models_mut().insert(false);
376 let open_id = open.id();
377
378 fret_ui::elements::with_element_cx(&mut app, window, b, "test", |cx| {
379 register_cancel_for_open_model(cx, open_id, GlobalElementId(0xaaa));
380
381 let opts = dialog_options_for_alert_dialog(cx, open_id, AlertDialogOptions::default());
382 assert!(!opts.dismiss_on_overlay_press);
383 assert_eq!(opts.initial_focus, Some(GlobalElementId(0xaaa)));
384 });
385 }
386}