1use std::time::Duration;
2
3use fret_core::{AppWindowId, Rect};
4use fret_runtime::Model;
5use fret_ui::action::{
6 OnCloseAutoFocus, OnDismissRequest, OnDismissiblePointerMove, OnOpenAutoFocus,
7};
8use fret_ui::element::AnyElement;
9use fret_ui::elements::GlobalElementId;
10use fret_ui::{ElementContext, UiHost, UiTree};
11
12use crate::headless::presence::PresenceOutput;
13use crate::headless::transition::TransitionOutput;
14use crate::primitives::presence;
15use crate::window_overlays;
16
17#[derive(Debug, Clone, Copy, PartialEq)]
22pub struct OverlayPresence {
23 pub present: bool,
24 pub interactive: bool,
25}
26
27impl OverlayPresence {
28 pub fn hidden() -> Self {
29 Self {
30 present: false,
31 interactive: false,
32 }
33 }
34
35 pub fn instant(open: bool) -> Self {
36 Self {
37 present: open,
38 interactive: open,
39 }
40 }
41
42 pub fn from_fade(open: bool, presence: PresenceOutput) -> Self {
43 Self {
44 present: presence.present,
45 interactive: open,
46 }
47 }
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum OverlayKind {
52 NonModalDismissible,
53 Modal,
54 Tooltip,
55 Hover,
56 ToastLayer,
57}
58
59#[derive(Debug, Clone)]
60pub struct ToastLayerSpec {
61 pub store: Model<window_overlays::ToastStore>,
62 pub position: window_overlays::ToastPosition,
63 pub style: window_overlays::ToastLayerStyle,
64 pub toaster_id: Option<std::sync::Arc<str>>,
65 pub visible_toasts: usize,
66 pub expand: bool,
67 pub rich_colors: bool,
68 pub invert: bool,
69 pub container_aria_label: Option<std::sync::Arc<str>>,
70 pub custom_aria_label: Option<std::sync::Arc<str>>,
71 pub offset: Option<window_overlays::ToastOffset>,
72 pub mobile_offset: Option<window_overlays::ToastOffset>,
73 pub margin: Option<fret_core::Px>,
74 pub gap: Option<fret_core::Px>,
75 pub toast_min_width: Option<fret_core::Px>,
76 pub toast_max_width: Option<fret_core::Px>,
77}
78
79pub struct OverlayRequest {
80 pub kind: OverlayKind,
81 pub id: GlobalElementId,
82 pub root_name: Option<String>,
83 pub trigger: Option<GlobalElementId>,
84 pub dismissable_branches: Vec<GlobalElementId>,
88 pub consume_outside_pointer_events: bool,
91 pub disable_outside_pointer_events: bool,
94 pub close_on_window_focus_lost: bool,
96 pub close_on_window_resize: bool,
98 pub open: Option<Model<bool>>,
99 pub on_open_auto_focus: Option<OnOpenAutoFocus>,
100 pub on_close_auto_focus: Option<OnCloseAutoFocus>,
101 pub dismissible_on_dismiss_request: Option<OnDismissRequest>,
102 pub dismissible_on_pointer_move: Option<OnDismissiblePointerMove>,
103 pub presence: OverlayPresence,
104 pub initial_focus: Option<GlobalElementId>,
105 pub children: Vec<AnyElement>,
106 pub toast_layer: Option<ToastLayerSpec>,
107}
108
109impl std::fmt::Debug for OverlayRequest {
110 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 f.debug_struct("OverlayRequest")
112 .field("kind", &self.kind)
113 .field("id", &self.id)
114 .field("root_name", &self.root_name)
115 .field("trigger", &self.trigger)
116 .field("dismissable_branches_len", &self.dismissable_branches.len())
117 .field(
118 "consume_outside_pointer_events",
119 &self.consume_outside_pointer_events,
120 )
121 .field(
122 "disable_outside_pointer_events",
123 &self.disable_outside_pointer_events,
124 )
125 .field("open", &self.open)
126 .field("on_open_auto_focus", &self.on_open_auto_focus.is_some())
127 .field("on_close_auto_focus", &self.on_close_auto_focus.is_some())
128 .field(
129 "dismissible_on_dismiss_request",
130 &self.dismissible_on_dismiss_request.is_some(),
131 )
132 .field(
133 "dismissible_on_pointer_move",
134 &self.dismissible_on_pointer_move.is_some(),
135 )
136 .field("presence", &self.presence)
137 .field("initial_focus", &self.initial_focus)
138 .field("children_len", &self.children.len())
139 .field("toast_layer", &self.toast_layer)
140 .finish()
141 }
142}
143
144impl OverlayRequest {
145 pub fn dismissible_popover(
146 id: GlobalElementId,
147 trigger: GlobalElementId,
148 open: Model<bool>,
149 presence: OverlayPresence,
150 children: Vec<AnyElement>,
151 ) -> Self {
152 Self {
153 kind: OverlayKind::NonModalDismissible,
154 id,
155 root_name: None,
156 trigger: Some(trigger),
157 dismissable_branches: Vec::new(),
158 consume_outside_pointer_events: false,
159 disable_outside_pointer_events: false,
160 close_on_window_focus_lost: false,
161 close_on_window_resize: false,
162 open: Some(open),
163 on_open_auto_focus: None,
164 on_close_auto_focus: None,
165 dismissible_on_dismiss_request: None,
166 dismissible_on_pointer_move: None,
167 presence,
168 initial_focus: None,
169 children,
170 toast_layer: None,
171 }
172 }
173
174 pub fn dismissible_menu(
179 id: GlobalElementId,
180 trigger: GlobalElementId,
181 open: Model<bool>,
182 presence: OverlayPresence,
183 children: Vec<AnyElement>,
184 ) -> Self {
185 let mut req = Self::dismissible_popover(id, trigger, open, presence, children);
186 req.consume_outside_pointer_events = true;
187 req.disable_outside_pointer_events = true;
188 req
189 }
190
191 pub fn modal(
192 id: GlobalElementId,
193 trigger: Option<GlobalElementId>,
194 open: Model<bool>,
195 presence: OverlayPresence,
196 children: Vec<AnyElement>,
197 ) -> Self {
198 Self {
199 kind: OverlayKind::Modal,
200 id,
201 root_name: None,
202 trigger,
203 dismissable_branches: Vec::new(),
204 consume_outside_pointer_events: false,
205 disable_outside_pointer_events: false,
206 close_on_window_focus_lost: false,
207 close_on_window_resize: false,
208 open: Some(open),
209 on_open_auto_focus: None,
210 on_close_auto_focus: None,
211 dismissible_on_dismiss_request: None,
212 dismissible_on_pointer_move: None,
213 presence,
214 initial_focus: None,
215 children,
216 toast_layer: None,
217 }
218 }
219
220 pub fn tooltip(
221 id: GlobalElementId,
222 open: Model<bool>,
223 presence: OverlayPresence,
224 children: Vec<AnyElement>,
225 ) -> Self {
226 Self {
227 kind: OverlayKind::Tooltip,
228 id,
229 root_name: None,
230 trigger: None,
231 dismissable_branches: Vec::new(),
232 consume_outside_pointer_events: false,
233 disable_outside_pointer_events: false,
234 close_on_window_focus_lost: false,
235 close_on_window_resize: false,
236 open: Some(open),
237 on_open_auto_focus: None,
238 on_close_auto_focus: None,
239 dismissible_on_dismiss_request: None,
240 dismissible_on_pointer_move: None,
241 presence,
242 initial_focus: None,
243 children,
244 toast_layer: None,
245 }
246 }
247
248 pub fn hover(
249 id: GlobalElementId,
250 trigger: GlobalElementId,
251 open: Model<bool>,
252 presence: OverlayPresence,
253 children: impl IntoIterator<Item = AnyElement>,
254 ) -> Self {
255 Self {
256 kind: OverlayKind::Hover,
257 id,
258 root_name: None,
259 trigger: Some(trigger),
260 dismissable_branches: Vec::new(),
261 consume_outside_pointer_events: false,
262 disable_outside_pointer_events: false,
263 close_on_window_focus_lost: false,
264 close_on_window_resize: false,
265 open: Some(open),
266 on_open_auto_focus: None,
267 on_close_auto_focus: None,
268 dismissible_on_dismiss_request: None,
269 dismissible_on_pointer_move: None,
270 presence,
271 initial_focus: None,
272 children: children.into_iter().collect(),
273 toast_layer: None,
274 }
275 }
276
277 pub fn toast_layer(id: GlobalElementId, store: Model<window_overlays::ToastStore>) -> Self {
278 Self {
279 kind: OverlayKind::ToastLayer,
280 id,
281 root_name: None,
282 trigger: None,
283 dismissable_branches: Vec::new(),
284 consume_outside_pointer_events: false,
285 disable_outside_pointer_events: false,
286 close_on_window_focus_lost: false,
287 close_on_window_resize: false,
288 open: None,
289 on_open_auto_focus: None,
290 on_close_auto_focus: None,
291 dismissible_on_dismiss_request: None,
292 dismissible_on_pointer_move: None,
293 presence: OverlayPresence::hidden(),
294 initial_focus: None,
295 children: Vec::new(),
296 toast_layer: Some(ToastLayerSpec {
297 store,
298 position: window_overlays::ToastPosition::default(),
299 style: window_overlays::ToastLayerStyle::default(),
300 toaster_id: None,
301 visible_toasts: window_overlays::DEFAULT_VISIBLE_TOASTS,
302 expand: false,
303 rich_colors: false,
304 invert: false,
305 container_aria_label: None,
306 custom_aria_label: None,
307 offset: None,
308 mobile_offset: None,
309 margin: None,
310 gap: None,
311 toast_min_width: None,
312 toast_max_width: None,
313 }),
314 }
315 }
316
317 pub fn close_on_window_focus_lost(mut self, close: bool) -> Self {
318 self.close_on_window_focus_lost = close;
319 self
320 }
321
322 pub fn close_on_window_resize(mut self, close: bool) -> Self {
323 self.close_on_window_resize = close;
324 self
325 }
326
327 pub fn toast_position(mut self, position: window_overlays::ToastPosition) -> Self {
328 let spec = self
329 .toast_layer
330 .as_mut()
331 .expect("toast_position requires a ToastLayer request");
332 spec.position = position;
333 self
334 }
335
336 pub fn toast_toaster_id(mut self, id: impl Into<std::sync::Arc<str>>) -> Self {
337 let spec = self
338 .toast_layer
339 .as_mut()
340 .expect("toast_toaster_id requires a ToastLayer request");
341 spec.toaster_id = Some(id.into());
342 self
343 }
344
345 pub fn toast_visible_toasts(mut self, visible_toasts: usize) -> Self {
346 let spec = self
347 .toast_layer
348 .as_mut()
349 .expect("toast_visible_toasts requires a ToastLayer request");
350 spec.visible_toasts = visible_toasts.max(1);
351 self
352 }
353
354 pub fn toast_expand_by_default(mut self, expand: bool) -> Self {
355 let spec = self
356 .toast_layer
357 .as_mut()
358 .expect("toast_expand_by_default requires a ToastLayer request");
359 spec.expand = expand;
360 self
361 }
362
363 pub fn toast_rich_colors(mut self, rich_colors: bool) -> Self {
364 let spec = self
365 .toast_layer
366 .as_mut()
367 .expect("toast_rich_colors requires a ToastLayer request");
368 spec.rich_colors = rich_colors;
369 self
370 }
371
372 pub fn toast_invert(mut self, invert: bool) -> Self {
373 let spec = self
374 .toast_layer
375 .as_mut()
376 .expect("toast_invert requires a ToastLayer request");
377 spec.invert = invert;
378 self
379 }
380
381 pub fn toast_container_aria_label(mut self, label: impl Into<std::sync::Arc<str>>) -> Self {
382 let spec = self
383 .toast_layer
384 .as_mut()
385 .expect("toast_container_aria_label requires a ToastLayer request");
386 spec.container_aria_label = Some(label.into());
387 self
388 }
389
390 pub fn toast_container_aria_label_opt(mut self, label: Option<std::sync::Arc<str>>) -> Self {
391 let spec = self
392 .toast_layer
393 .as_mut()
394 .expect("toast_container_aria_label_opt requires a ToastLayer request");
395 spec.container_aria_label = label;
396 self
397 }
398
399 pub fn toast_custom_aria_label_opt(mut self, label: Option<std::sync::Arc<str>>) -> Self {
400 let spec = self
401 .toast_layer
402 .as_mut()
403 .expect("toast_custom_aria_label_opt requires a ToastLayer request");
404 spec.custom_aria_label = label;
405 self
406 }
407
408 pub fn toast_offset(mut self, offset: window_overlays::ToastOffset) -> Self {
409 let spec = self
410 .toast_layer
411 .as_mut()
412 .expect("toast_offset requires a ToastLayer request");
413 spec.offset = Some(offset);
414 self
415 }
416
417 pub fn toast_offset_opt(mut self, offset: Option<window_overlays::ToastOffset>) -> Self {
418 let spec = self
419 .toast_layer
420 .as_mut()
421 .expect("toast_offset_opt requires a ToastLayer request");
422 spec.offset = offset;
423 self
424 }
425
426 pub fn toast_mobile_offset(mut self, offset: window_overlays::ToastOffset) -> Self {
427 let spec = self
428 .toast_layer
429 .as_mut()
430 .expect("toast_mobile_offset requires a ToastLayer request");
431 spec.mobile_offset = Some(offset);
432 self
433 }
434
435 pub fn toast_mobile_offset_opt(mut self, offset: Option<window_overlays::ToastOffset>) -> Self {
436 let spec = self
437 .toast_layer
438 .as_mut()
439 .expect("toast_mobile_offset_opt requires a ToastLayer request");
440 spec.mobile_offset = offset;
441 self
442 }
443
444 pub fn toast_style(mut self, style: window_overlays::ToastLayerStyle) -> Self {
445 let spec = self
446 .toast_layer
447 .as_mut()
448 .expect("toast_style requires a ToastLayer request");
449 spec.style = style;
450 self
451 }
452
453 pub fn toast_margin(mut self, margin: fret_core::Px) -> Self {
454 let spec = self
455 .toast_layer
456 .as_mut()
457 .expect("toast_margin requires a ToastLayer request");
458 spec.margin = Some(margin);
459 self
460 }
461
462 pub fn toast_gap(mut self, gap: fret_core::Px) -> Self {
463 let spec = self
464 .toast_layer
465 .as_mut()
466 .expect("toast_gap requires a ToastLayer request");
467 spec.gap = Some(gap);
468 self
469 }
470
471 pub fn toast_min_width(mut self, width: fret_core::Px) -> Self {
472 let spec = self
473 .toast_layer
474 .as_mut()
475 .expect("toast_min_width requires a ToastLayer request");
476 spec.toast_min_width = Some(width);
477 self
478 }
479
480 pub fn toast_max_width(mut self, width: fret_core::Px) -> Self {
481 let spec = self
482 .toast_layer
483 .as_mut()
484 .expect("toast_max_width requires a ToastLayer request");
485 spec.toast_max_width = Some(width);
486 self
487 }
488
489 pub fn dismissable_branches(
490 mut self,
491 branches: impl IntoIterator<Item = GlobalElementId>,
492 ) -> Self {
493 self.dismissable_branches = branches.into_iter().collect();
494 self
495 }
496
497 pub fn add_dismissable_branch(mut self, branch: GlobalElementId) -> Self {
498 if !self.dismissable_branches.contains(&branch) {
499 self.dismissable_branches.push(branch);
500 }
501 self
502 }
503
504 pub fn extend_dismissable_branches(
505 mut self,
506 branches: impl IntoIterator<Item = GlobalElementId>,
507 ) -> Self {
508 for branch in branches {
509 if !self.dismissable_branches.contains(&branch) {
510 self.dismissable_branches.push(branch);
511 }
512 }
513 self
514 }
515
516 pub fn consume_outside_pointer_events(mut self, consume: bool) -> Self {
517 self.consume_outside_pointer_events = consume;
518 self
519 }
520
521 pub fn disable_outside_pointer_events(mut self, disable: bool) -> Self {
522 self.disable_outside_pointer_events = disable;
523 self
524 }
525}
526
527pub struct OverlayController;
529
530#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
536pub struct OverlayArbitrationSnapshot {
537 pub has_any_overlays: bool,
539 pub modal_barrier_active: bool,
541 pub pointer_occlusion: fret_ui::tree::PointerOcclusion,
546 pub pointer_capture_active: bool,
548}
549
550#[derive(Debug, Clone, Copy, PartialEq, Eq)]
551pub enum OverlayStackEntryKind {
552 Base,
553 Popover,
554 Modal,
555 Tooltip,
556 Hover,
557 ToastLayer,
558 Unknown,
559}
560
561#[derive(Debug, Clone, Copy, PartialEq, Eq)]
562pub struct WindowOverlayStackEntry {
563 pub kind: OverlayStackEntryKind,
564 pub id: Option<GlobalElementId>,
565 pub open: bool,
566 pub visible: bool,
567 pub blocks_underlay_input: bool,
568 pub hit_testable: bool,
569 pub pointer_occlusion: fret_ui::tree::PointerOcclusion,
570}
571
572#[derive(Debug, Clone, PartialEq, Eq)]
573pub struct WindowOverlayStackSnapshot {
574 pub arbitration: OverlayArbitrationSnapshot,
575 pub stack: Vec<WindowOverlayStackEntry>,
576 pub topmost_overlay: Option<GlobalElementId>,
577 pub topmost_popover: Option<GlobalElementId>,
578 pub topmost_modal: Option<GlobalElementId>,
579 pub topmost_pointer_occluding_overlay: Option<GlobalElementId>,
580}
581
582impl OverlayController {
583 pub fn begin_frame<H: UiHost>(app: &mut H, window: AppWindowId) {
584 window_overlays::begin_frame(app, window);
585 }
586
587 pub fn last_known_window_bounds<H: UiHost>(app: &mut H, window: AppWindowId) -> Option<Rect> {
588 window_overlays::last_known_window_bounds_for_window(app, window)
589 }
590
591 pub fn popover_root_name(id: GlobalElementId) -> String {
592 window_overlays::popover_root_name(id)
593 }
594
595 pub fn modal_root_name(id: GlobalElementId) -> String {
596 window_overlays::modal_root_name(id)
597 }
598
599 pub fn tooltip_root_name(id: GlobalElementId) -> String {
600 window_overlays::tooltip_root_name(id)
601 }
602
603 pub fn hover_overlay_root_name(id: GlobalElementId) -> String {
604 window_overlays::hover_overlay_root_name(id)
605 }
606
607 pub fn toast_layer_root_name(id: GlobalElementId) -> String {
608 window_overlays::toast_layer_root_name(id)
609 }
610
611 pub fn request<H: UiHost>(cx: &mut ElementContext<'_, H>, request: OverlayRequest) {
612 Self::request_for_window_with_owner(cx.app, cx.window, request, Some(cx.root_id()));
613 }
614
615 pub fn request_for_window<H: UiHost>(
616 app: &mut H,
617 window: AppWindowId,
618 request: OverlayRequest,
619 ) {
620 Self::request_for_window_with_owner(app, window, request, None);
621 }
622
623 fn request_for_window_with_owner<H: UiHost>(
624 app: &mut H,
625 window: AppWindowId,
626 request: OverlayRequest,
627 owner: Option<GlobalElementId>,
628 ) {
629 match request.kind {
630 OverlayKind::NonModalDismissible => {
631 let open = request
632 .open
633 .expect("NonModalDismissible requires open model");
634 let trigger = request
635 .trigger
636 .expect("NonModalDismissible requires trigger");
637 let root_name = request
638 .root_name
639 .unwrap_or_else(|| window_overlays::popover_root_name(request.id));
640 let req = window_overlays::DismissiblePopoverRequest {
641 id: request.id,
642 root_name,
643 trigger,
644 dismissable_branches: request.dismissable_branches,
645 consume_outside_pointer_events: request.consume_outside_pointer_events,
646 disable_outside_pointer_events: request.disable_outside_pointer_events,
647 close_on_window_focus_lost: request.close_on_window_focus_lost,
648 close_on_window_resize: request.close_on_window_resize,
649 open,
650 present: request.presence.present,
651 initial_focus: request.initial_focus,
652 on_open_auto_focus: request.on_open_auto_focus,
653 on_close_auto_focus: request.on_close_auto_focus,
654 on_dismiss_request: request.dismissible_on_dismiss_request,
655 on_pointer_move: request.dismissible_on_pointer_move,
656 children: request.children,
657 };
658 window_overlays::request_dismissible_popover_for_window_owned(
659 app, window, req, owner,
660 );
661 }
662 OverlayKind::Modal => {
663 let open = request.open.expect("Modal requires open model");
664 let root_name = request
665 .root_name
666 .unwrap_or_else(|| window_overlays::modal_root_name(request.id));
667 let req = window_overlays::ModalRequest {
668 id: request.id,
669 root_name,
670 trigger: request.trigger,
671 close_on_window_focus_lost: request.close_on_window_focus_lost,
672 close_on_window_resize: request.close_on_window_resize,
673 open,
674 present: request.presence.present,
675 initial_focus: request.initial_focus,
676 on_open_auto_focus: request.on_open_auto_focus,
677 on_close_auto_focus: request.on_close_auto_focus,
678 on_dismiss_request: request.dismissible_on_dismiss_request,
679 children: request.children,
680 };
681 window_overlays::request_modal_for_window_owned(app, window, req, owner);
682 }
683 OverlayKind::Tooltip => {
684 let open = request.open.expect("Tooltip requires open model");
685 if !request.presence.present {
686 return;
687 }
688 let root_name = request
689 .root_name
690 .unwrap_or_else(|| window_overlays::tooltip_root_name(request.id));
691 let req = window_overlays::TooltipRequest {
692 id: request.id,
693 root_name,
694 interactive: request.presence.interactive,
695 trigger: request.trigger,
696 open,
697 present: request.presence.present,
698 on_dismiss_request: request.dismissible_on_dismiss_request,
699 on_pointer_move: request.dismissible_on_pointer_move,
700 children: request.children,
701 };
702 window_overlays::request_tooltip_for_window_owned(app, window, req, owner);
703 }
704 OverlayKind::Hover => {
705 let open = request.open.expect("Hover requires open model");
706 if !request.presence.present {
707 return;
708 }
709 let trigger = request.trigger.expect("Hover requires trigger");
710 let root_name = request
711 .root_name
712 .unwrap_or_else(|| window_overlays::hover_overlay_root_name(request.id));
713 let req = window_overlays::HoverOverlayRequest {
714 id: request.id,
715 root_name,
716 interactive: request.presence.interactive,
717 trigger,
718 open,
719 present: request.presence.present,
720 on_pointer_move: request.dismissible_on_pointer_move,
721 children: request.children,
722 };
723 window_overlays::request_hover_overlay_for_window_owned(app, window, req, owner);
724 }
725 OverlayKind::ToastLayer => {
726 let spec = request
727 .toast_layer
728 .expect("ToastLayer requires toast_layer spec");
729 let root_name = request
730 .root_name
731 .unwrap_or_else(|| window_overlays::toast_layer_root_name(request.id));
732
733 let mut toast_req = window_overlays::ToastLayerRequest::new(request.id, spec.store)
734 .position(spec.position)
735 .style(spec.style)
736 .toaster_id_opt(spec.toaster_id)
737 .visible_toasts(spec.visible_toasts)
738 .expand_by_default(spec.expand)
739 .rich_colors(spec.rich_colors)
740 .invert(spec.invert)
741 .container_aria_label_opt(spec.container_aria_label)
742 .custom_aria_label_opt(spec.custom_aria_label)
743 .root_name(root_name);
744 if let Some(offset) = spec.offset {
745 toast_req = toast_req.offset(offset);
746 }
747 if let Some(offset) = spec.mobile_offset {
748 toast_req = toast_req.mobile_offset(offset);
749 }
750 if let Some(margin) = spec.margin {
751 toast_req = toast_req.margin(margin);
752 }
753 if let Some(gap) = spec.gap {
754 toast_req = toast_req.gap(gap);
755 }
756 if let Some(width) = spec.toast_min_width {
757 toast_req = toast_req.toast_min_width(width);
758 }
759 if let Some(width) = spec.toast_max_width {
760 toast_req = toast_req.toast_max_width(width);
761 }
762 window_overlays::request_toast_layer_for_window_owned(
763 app, window, toast_req, owner,
764 );
765 }
766 }
767 }
768
769 pub fn render<H: UiHost + 'static>(
770 ui: &mut UiTree<H>,
771 app: &mut H,
772 services: &mut dyn fret_core::UiServices,
773 window: AppWindowId,
774 bounds: Rect,
775 ) {
776 window_overlays::render(ui, app, services, window, bounds);
777 }
778
779 pub fn arbitration_snapshot<H: UiHost>(ui: &UiTree<H>) -> OverlayArbitrationSnapshot {
786 use fret_ui::tree::PointerOcclusion;
787
788 let runtime = ui.input_arbitration_snapshot();
789 let base_root = ui.base_root();
790 let layers = ui.debug_layers_in_paint_order();
791
792 let modal_barrier_active = runtime.modal_barrier_root.is_some();
793 OverlayArbitrationSnapshot {
794 has_any_overlays: layers
795 .iter()
796 .any(|l| l.visible && base_root.is_none_or(|base| l.root != base)),
797 modal_barrier_active,
798 pointer_capture_active: runtime.pointer_capture_active,
799 pointer_occlusion: if modal_barrier_active {
800 PointerOcclusion::None
801 } else {
802 runtime.pointer_occlusion
803 },
804 }
805 }
806
807 pub fn stack_snapshot_for_window<H: UiHost>(
816 ui: &UiTree<H>,
817 app: &mut H,
818 window: AppWindowId,
819 ) -> WindowOverlayStackSnapshot {
820 use fret_ui::tree::PointerOcclusion;
821 use std::collections::HashMap;
822
823 let arbitration = Self::arbitration_snapshot(ui);
824 let base_root = ui.base_root();
825 let layers = ui.debug_layers_in_paint_order();
826
827 let mut by_layer = HashMap::new();
828 for entry in window_overlays::overlay_layer_entries_for_window(app, window) {
829 let kind = match entry.kind {
830 window_overlays::WindowOverlayLayerKind::Popover => OverlayStackEntryKind::Popover,
831 window_overlays::WindowOverlayLayerKind::Modal => OverlayStackEntryKind::Modal,
832 window_overlays::WindowOverlayLayerKind::Hover => OverlayStackEntryKind::Hover,
833 window_overlays::WindowOverlayLayerKind::Tooltip => OverlayStackEntryKind::Tooltip,
834 window_overlays::WindowOverlayLayerKind::ToastLayer => {
835 OverlayStackEntryKind::ToastLayer
836 }
837 };
838 by_layer.insert(entry.layer, (kind, entry.id, entry.open));
839 }
840
841 let mut stack: Vec<WindowOverlayStackEntry> = Vec::with_capacity(layers.len());
842 for layer in layers {
843 let (kind, id, open) = if base_root == Some(layer.root) {
844 (OverlayStackEntryKind::Base, None, false)
845 } else if let Some((kind, id, open)) = by_layer.get(&layer.id).copied() {
846 (kind, Some(id), open)
847 } else {
848 (OverlayStackEntryKind::Unknown, None, false)
849 };
850 stack.push(WindowOverlayStackEntry {
851 kind,
852 id,
853 open,
854 visible: layer.visible,
855 blocks_underlay_input: layer.blocks_underlay_input,
856 hit_testable: layer.hit_testable,
857 pointer_occlusion: layer.pointer_occlusion,
858 });
859 }
860
861 let topmost_overlay = stack
862 .iter()
863 .rev()
864 .find_map(|e| (e.visible && e.id.is_some()).then_some(e.id))
865 .flatten();
866 let topmost_popover = stack
867 .iter()
868 .rev()
869 .find_map(|e| {
870 (e.visible && e.open && e.kind == OverlayStackEntryKind::Popover).then_some(e.id)
871 })
872 .flatten();
873 let topmost_modal = stack
874 .iter()
875 .rev()
876 .find_map(|e| (e.visible && e.kind == OverlayStackEntryKind::Modal).then_some(e.id))
877 .flatten();
878 let topmost_pointer_occluding_overlay = stack
879 .iter()
880 .rev()
881 .find_map(|e| {
882 (e.visible && e.pointer_occlusion != PointerOcclusion::None).then_some(e.id)
883 })
884 .flatten();
885
886 WindowOverlayStackSnapshot {
887 arbitration,
888 stack,
889 topmost_overlay,
890 topmost_popover,
891 topmost_modal,
892 topmost_pointer_occluding_overlay,
893 }
894 }
895
896 pub fn fade_presence<H: UiHost>(
897 cx: &mut ElementContext<'_, H>,
898 open: bool,
899 fade_ticks: u64,
900 ) -> PresenceOutput {
901 presence::fade_presence(cx, open, fade_ticks)
902 }
903
904 pub fn fade_presence_with_durations<H: UiHost>(
905 cx: &mut ElementContext<'_, H>,
906 open: bool,
907 open_ticks: u64,
908 close_ticks: u64,
909 ) -> PresenceOutput {
910 presence::fade_presence_with_durations(cx, open, open_ticks, close_ticks)
911 }
912
913 #[track_caller]
918 pub fn transition<H: UiHost>(
919 cx: &mut ElementContext<'_, H>,
920 open: bool,
921 ticks: u64,
922 ) -> TransitionOutput {
923 crate::declarative::transition::drive_transition(cx, open, ticks)
924 }
925
926 #[track_caller]
928 pub fn transition_with_durations<H: UiHost>(
929 cx: &mut ElementContext<'_, H>,
930 open: bool,
931 open_ticks: u64,
932 close_ticks: u64,
933 ) -> TransitionOutput {
934 crate::declarative::transition::drive_transition_with_durations(
935 cx,
936 open,
937 open_ticks,
938 close_ticks,
939 )
940 }
941
942 #[track_caller]
944 pub fn transition_with_durations_duration<H: UiHost>(
945 cx: &mut ElementContext<'_, H>,
946 open: bool,
947 open_duration: Duration,
948 close_duration: Duration,
949 ) -> TransitionOutput {
950 crate::declarative::transition::drive_transition_with_durations_duration(
951 cx,
952 open,
953 open_duration,
954 close_duration,
955 )
956 }
957
958 #[track_caller]
963 pub fn transition_with_durations_and_easing<H: UiHost>(
964 cx: &mut ElementContext<'_, H>,
965 open: bool,
966 open_ticks: u64,
967 close_ticks: u64,
968 ease: fn(f32) -> f32,
969 ) -> TransitionOutput {
970 crate::declarative::transition::drive_transition_with_durations_and_easing(
971 cx,
972 open,
973 open_ticks,
974 close_ticks,
975 ease,
976 )
977 }
978
979 #[track_caller]
981 pub fn transition_with_durations_and_easing_duration<H: UiHost>(
982 cx: &mut ElementContext<'_, H>,
983 open: bool,
984 open_duration: Duration,
985 close_duration: Duration,
986 ease: fn(f32) -> f32,
987 ) -> TransitionOutput {
988 crate::declarative::transition::drive_transition_with_durations_and_easing_duration(
989 cx,
990 open,
991 open_duration,
992 close_duration,
993 ease,
994 )
995 }
996
997 #[track_caller]
998 pub fn transition_with_durations_and_cubic_bezier<H: UiHost>(
999 cx: &mut ElementContext<'_, H>,
1000 open: bool,
1001 open_ticks: u64,
1002 close_ticks: u64,
1003 bezier: fret_ui::theme::CubicBezier,
1004 ) -> TransitionOutput {
1005 crate::declarative::transition::drive_transition_with_durations_and_cubic_bezier(
1006 cx,
1007 open,
1008 open_ticks,
1009 close_ticks,
1010 bezier,
1011 )
1012 }
1013
1014 #[track_caller]
1015 pub fn transition_with_durations_and_cubic_bezier_duration<H: UiHost>(
1016 cx: &mut ElementContext<'_, H>,
1017 open: bool,
1018 open_duration: Duration,
1019 close_duration: Duration,
1020 bezier: fret_ui::theme::CubicBezier,
1021 ) -> TransitionOutput {
1022 crate::declarative::transition::drive_transition_with_durations_and_cubic_bezier_duration(
1023 cx,
1024 open,
1025 open_duration,
1026 close_duration,
1027 bezier,
1028 )
1029 }
1030
1031 pub fn toast_store<H: UiHost>(app: &mut H) -> Model<window_overlays::ToastStore> {
1032 window_overlays::toast_store(app)
1033 }
1034
1035 pub fn toast_action(
1036 host: &mut dyn fret_ui::action::UiActionHost,
1037 store: Model<window_overlays::ToastStore>,
1038 window: AppWindowId,
1039 request: window_overlays::ToastRequest,
1040 ) -> window_overlays::ToastId {
1041 window_overlays::toast_action(host, store, window, request)
1042 }
1043
1044 pub fn dismiss_toast_action(
1045 host: &mut dyn fret_ui::action::UiActionHost,
1046 store: Model<window_overlays::ToastStore>,
1047 window: AppWindowId,
1048 id: window_overlays::ToastId,
1049 ) -> bool {
1050 window_overlays::dismiss_toast_action(host, store, window, id)
1051 }
1052
1053 pub fn dismiss_all_toasts_action(
1054 host: &mut dyn fret_ui::action::UiActionHost,
1055 store: Model<window_overlays::ToastStore>,
1056 window: AppWindowId,
1057 ) -> usize {
1058 window_overlays::dismiss_all_toasts_action(host, store, window)
1059 }
1060}
1061
1062#[cfg(test)]
1063mod tests {
1064 use super::*;
1065 use fret_app::App;
1066 use fret_core::{
1067 Event, KeyCode, Modifiers, Point, Px, Rect, TextBlobId, TextConstraints, TextInput,
1068 TextMetrics, TextService,
1069 };
1070 use fret_core::{PathCommand, SvgId, SvgService};
1071 use fret_core::{PathConstraints, PathId, PathMetrics, PathService, PathStyle};
1072 use fret_runtime::CommandId;
1073 use fret_runtime::Effect;
1074 use fret_runtime::{FrameId, TickId};
1075 use fret_ui::element::{LayoutStyle, Length, PointerRegionProps, PressableProps};
1076 use std::sync::Arc;
1077
1078 #[derive(Default)]
1079 struct FakeServices;
1080
1081 impl TextService for FakeServices {
1082 fn prepare(
1083 &mut self,
1084 _input: &TextInput,
1085 _constraints: TextConstraints,
1086 ) -> (TextBlobId, TextMetrics) {
1087 (
1088 TextBlobId::default(),
1089 TextMetrics {
1090 size: fret_core::Size::new(Px(0.0), Px(0.0)),
1091 baseline: Px(0.0),
1092 },
1093 )
1094 }
1095
1096 fn release(&mut self, _blob: TextBlobId) {}
1097 }
1098
1099 impl PathService for FakeServices {
1100 fn prepare(
1101 &mut self,
1102 _commands: &[PathCommand],
1103 _style: PathStyle,
1104 _constraints: PathConstraints,
1105 ) -> (PathId, PathMetrics) {
1106 (PathId::default(), PathMetrics::default())
1107 }
1108
1109 fn release(&mut self, _path: PathId) {}
1110 }
1111
1112 #[test]
1113 fn arbitration_snapshot_reports_modal_and_pointer_occlusion() {
1114 let window = AppWindowId::default();
1115 let mut app = App::new();
1116 let mut ui: UiTree<App> = UiTree::new();
1117 ui.set_window(window);
1118
1119 let mut services = FakeServices;
1120 let bounds = Rect::new(
1121 Point::new(Px(0.0), Px(0.0)),
1122 fret_core::Size::new(Px(300.0), Px(200.0)),
1123 );
1124
1125 OverlayController::begin_frame(&mut app, window);
1126 let base = fret_ui::declarative::render_root(
1127 &mut ui,
1128 &mut app,
1129 &mut services,
1130 window,
1131 bounds,
1132 "base",
1133 |_| Vec::new(),
1134 );
1135 ui.set_root(base);
1136 ui.layout_all(&mut app, &mut services, bounds, 1.0);
1137
1138 let snap = OverlayController::arbitration_snapshot(&ui);
1139 assert_eq!(
1140 snap,
1141 OverlayArbitrationSnapshot {
1142 has_any_overlays: false,
1143 modal_barrier_active: false,
1144 pointer_occlusion: fret_ui::tree::PointerOcclusion::None,
1145 pointer_capture_active: false,
1146 }
1147 );
1148
1149 OverlayController::begin_frame(&mut app, window);
1151 let overlay = fret_ui::declarative::render_root(
1152 &mut ui,
1153 &mut app,
1154 &mut services,
1155 window,
1156 bounds,
1157 "overlay",
1158 |_| Vec::new(),
1159 );
1160 let overlay_layer = ui.push_overlay_root(overlay, false);
1161 ui.set_layer_pointer_occlusion(
1162 overlay_layer,
1163 fret_ui::tree::PointerOcclusion::BlockMouseExceptScroll,
1164 );
1165 ui.layout_all(&mut app, &mut services, bounds, 1.0);
1166
1167 let snap = OverlayController::arbitration_snapshot(&ui);
1168 assert!(snap.has_any_overlays);
1169 assert!(!snap.modal_barrier_active);
1170 assert_eq!(
1171 snap.pointer_occlusion,
1172 fret_ui::tree::PointerOcclusion::BlockMouseExceptScroll
1173 );
1174
1175 OverlayController::begin_frame(&mut app, window);
1177 let modal = fret_ui::declarative::render_root(
1178 &mut ui,
1179 &mut app,
1180 &mut services,
1181 window,
1182 bounds,
1183 "modal",
1184 |_| Vec::new(),
1185 );
1186 let _modal_layer = ui.push_overlay_root(modal, true);
1187 ui.layout_all(&mut app, &mut services, bounds, 1.0);
1188
1189 let snap = OverlayController::arbitration_snapshot(&ui);
1190 assert!(snap.has_any_overlays);
1191 assert!(snap.modal_barrier_active);
1192 assert_eq!(
1193 snap.pointer_occlusion,
1194 fret_ui::tree::PointerOcclusion::None
1195 );
1196 }
1197
1198 #[test]
1199 fn arbitration_snapshot_reports_pointer_capture_active() {
1200 let window = AppWindowId::default();
1201 let mut app = App::new();
1202 let mut ui: UiTree<App> = UiTree::new();
1203 ui.set_window(window);
1204
1205 let mut services = FakeServices;
1206 let bounds = Rect::new(
1207 Point::new(Px(0.0), Px(0.0)),
1208 fret_core::Size::new(Px(300.0), Px(200.0)),
1209 );
1210
1211 OverlayController::begin_frame(&mut app, window);
1212 let base = fret_ui::declarative::render_root(
1213 &mut ui,
1214 &mut app,
1215 &mut services,
1216 window,
1217 bounds,
1218 "base",
1219 |cx| {
1220 vec![cx.pointer_region(
1221 PointerRegionProps {
1222 layout: {
1223 let mut layout = LayoutStyle::default();
1224 layout.size.width = Length::Fill;
1225 layout.size.height = Length::Fill;
1226 layout
1227 },
1228 enabled: true,
1229 ..Default::default()
1230 },
1231 |cx| {
1232 cx.pointer_region_on_pointer_down(Arc::new(move |host, _cx, _down| {
1233 host.capture_pointer();
1234 true
1235 }));
1236 Vec::new()
1237 },
1238 )]
1239 },
1240 );
1241 ui.set_root(base);
1242 ui.layout_all(&mut app, &mut services, bounds, 1.0);
1243
1244 ui.dispatch_event(
1245 &mut app,
1246 &mut services,
1247 &Event::Pointer(fret_core::PointerEvent::Down {
1248 position: Point::new(Px(10.0), Px(10.0)),
1249 button: fret_core::MouseButton::Left,
1250 modifiers: fret_core::Modifiers::default(),
1251 click_count: 1,
1252 pointer_id: fret_core::PointerId(0),
1253 pointer_type: fret_core::PointerType::Mouse,
1254 }),
1255 );
1256
1257 let snap = OverlayController::arbitration_snapshot(&ui);
1258 assert!(!snap.has_any_overlays);
1259 assert!(!snap.modal_barrier_active);
1260 assert_eq!(
1261 snap.pointer_occlusion,
1262 fret_ui::tree::PointerOcclusion::None
1263 );
1264 assert!(snap.pointer_capture_active);
1265 }
1266
1267 #[test]
1268 fn stack_snapshot_reports_topmost_popover_and_modal_in_paint_order() {
1269 let window = AppWindowId::default();
1270 let mut app = App::new();
1271 let mut ui: UiTree<App> = UiTree::new();
1272 ui.set_window(window);
1273
1274 let popover_open = app.models_mut().insert(true);
1275 let modal_open = app.models_mut().insert(true);
1276
1277 let mut services = FakeServices;
1278 let bounds = Rect::new(
1279 Point::new(Px(0.0), Px(0.0)),
1280 fret_core::Size::new(Px(300.0), Px(200.0)),
1281 );
1282
1283 let mut trigger_id: Option<GlobalElementId> = None;
1284
1285 OverlayController::begin_frame(&mut app, window);
1287 let base = fret_ui::declarative::render_root(
1288 &mut ui,
1289 &mut app,
1290 &mut services,
1291 window,
1292 bounds,
1293 "base",
1294 |cx| {
1295 vec![cx.pressable_with_id(
1296 PressableProps {
1297 layout: {
1298 let mut layout = LayoutStyle::default();
1299 layout.size.width = Length::Px(Px(80.0));
1300 layout.size.height = Length::Px(Px(32.0));
1301 layout
1302 },
1303 ..Default::default()
1304 },
1305 |_cx, _st, id| {
1306 trigger_id = Some(id);
1307 Vec::new()
1308 },
1309 )]
1310 },
1311 );
1312 ui.set_root(base);
1313 OverlayController::render(&mut ui, &mut app, &mut services, window, bounds);
1314 ui.layout_all(&mut app, &mut services, bounds, 1.0);
1315
1316 let trigger_id = trigger_id.expect("trigger id");
1317
1318 OverlayController::begin_frame(&mut app, window);
1320 let popover_id = GlobalElementId(0xabc);
1321 OverlayController::request_for_window(
1322 &mut app,
1323 window,
1324 OverlayRequest::dismissible_menu(
1325 popover_id,
1326 trigger_id,
1327 popover_open.clone(),
1328 OverlayPresence::instant(true),
1329 Vec::new(),
1330 ),
1331 );
1332 OverlayController::render(&mut ui, &mut app, &mut services, window, bounds);
1333 ui.layout_all(&mut app, &mut services, bounds, 1.0);
1334
1335 let snap = OverlayController::stack_snapshot_for_window(&ui, &mut app, window);
1336 assert_eq!(snap.topmost_popover, Some(popover_id));
1337 assert_eq!(snap.topmost_modal, None);
1338 assert_eq!(snap.topmost_overlay, Some(popover_id));
1339 assert_eq!(
1340 snap.stack.last().map(|e| (e.kind, e.id)),
1341 Some((OverlayStackEntryKind::Popover, Some(popover_id)))
1342 );
1343
1344 OverlayController::begin_frame(&mut app, window);
1346 let modal_id = GlobalElementId(0xdef);
1347 OverlayController::request_for_window(
1348 &mut app,
1349 window,
1350 OverlayRequest::dismissible_menu(
1351 popover_id,
1352 trigger_id,
1353 popover_open.clone(),
1354 OverlayPresence::instant(true),
1355 Vec::new(),
1356 ),
1357 );
1358 OverlayController::request_for_window(
1359 &mut app,
1360 window,
1361 OverlayRequest::modal(
1362 modal_id,
1363 Some(trigger_id),
1364 modal_open.clone(),
1365 OverlayPresence::instant(true),
1366 Vec::new(),
1367 ),
1368 );
1369 OverlayController::render(&mut ui, &mut app, &mut services, window, bounds);
1370 ui.layout_all(&mut app, &mut services, bounds, 1.0);
1371
1372 let snap = OverlayController::stack_snapshot_for_window(&ui, &mut app, window);
1373 assert_eq!(snap.topmost_modal, Some(modal_id));
1374 assert_eq!(snap.topmost_overlay, Some(modal_id));
1375 assert!(
1376 snap.stack
1377 .iter()
1378 .any(|e| e.kind == OverlayStackEntryKind::Popover && e.id == Some(popover_id)),
1379 "expected popover layer to still be identifiable in the stack snapshot even if it closes on modal open"
1380 );
1381 assert_eq!(
1382 snap.stack.last().map(|e| (e.kind, e.id)),
1383 Some((OverlayStackEntryKind::Modal, Some(modal_id)))
1384 );
1385 }
1386
1387 impl SvgService for FakeServices {
1388 fn register_svg(&mut self, _bytes: &[u8]) -> SvgId {
1389 SvgId::default()
1390 }
1391
1392 fn unregister_svg(&mut self, _svg: SvgId) -> bool {
1393 true
1394 }
1395 }
1396
1397 impl fret_core::MaterialService for FakeServices {
1398 fn register_material(
1399 &mut self,
1400 _desc: fret_core::MaterialDescriptor,
1401 ) -> Result<fret_core::MaterialId, fret_core::MaterialRegistrationError> {
1402 Err(fret_core::MaterialRegistrationError::Unsupported)
1403 }
1404
1405 fn unregister_material(&mut self, _id: fret_core::MaterialId) -> bool {
1406 true
1407 }
1408 }
1409
1410 fn dispatch_keydown_and_apply_commands(
1411 ui: &mut UiTree<App>,
1412 app: &mut App,
1413 services: &mut dyn fret_core::UiServices,
1414 key: KeyCode,
1415 modifiers: Modifiers,
1416 ) {
1417 ui.dispatch_event(
1418 app,
1419 services,
1420 &Event::KeyDown {
1421 key,
1422 modifiers,
1423 repeat: false,
1424 },
1425 );
1426
1427 for effect in app.flush_effects() {
1428 let Effect::Command { command, .. } = effect else {
1429 continue;
1430 };
1431 let _ = ui.dispatch_command(app, services, &command);
1432 }
1433 }
1434
1435 #[test]
1436 fn toast_layer_request_enables_timer_events_when_toasts_exist() {
1437 let window = AppWindowId::default();
1438 let mut app = App::new();
1439 let mut ui: UiTree<App> = UiTree::new();
1440 ui.set_window(window);
1441
1442 let mut services = FakeServices;
1443 let bounds = Rect::new(
1444 Point::new(Px(0.0), Px(0.0)),
1445 fret_core::Size::new(Px(300.0), Px(200.0)),
1446 );
1447
1448 OverlayController::begin_frame(&mut app, window);
1450 let base = fret_ui::declarative::render_root(
1451 &mut ui,
1452 &mut app,
1453 &mut services,
1454 window,
1455 bounds,
1456 "base",
1457 |_| Vec::new(),
1458 );
1459 ui.set_root(base);
1460
1461 let store = OverlayController::toast_store(&mut app);
1463 let _ = OverlayController::toast_action(
1464 &mut fret_ui::action::UiActionHostAdapter { app: &mut app },
1465 store.clone(),
1466 window,
1467 window_overlays::ToastRequest::new("Hello"),
1468 );
1469
1470 OverlayController::begin_frame(&mut app, window);
1472 OverlayController::request_for_window(
1473 &mut app,
1474 window,
1475 OverlayRequest::toast_layer(GlobalElementId(0xbeef), store)
1476 .toast_position(window_overlays::ToastPosition::BottomRight),
1477 );
1478 OverlayController::render(&mut ui, &mut app, &mut services, window, bounds);
1479 ui.layout_all(&mut app, &mut services, bounds, 1.0);
1480
1481 let layers = ui.debug_layers_in_paint_order();
1482 assert!(
1483 layers.iter().any(|l| l.wants_timer_events),
1484 "expected at least one layer to request timer events when toasts exist"
1485 );
1486 }
1487
1488 #[test]
1489 fn modal_focus_traversal_is_scoped_to_modal_layer() {
1490 let window = AppWindowId::default();
1491 let mut app = App::new();
1492 let mut ui: UiTree<App> = UiTree::new();
1493 ui.set_window(window);
1494
1495 let mut services = FakeServices;
1496 let bounds = Rect::new(
1497 Point::new(Px(0.0), Px(0.0)),
1498 fret_core::Size::new(Px(300.0), Px(200.0)),
1499 );
1500
1501 let mut underlay_a: Option<GlobalElementId> = None;
1502 let mut underlay_b: Option<GlobalElementId> = None;
1503
1504 OverlayController::begin_frame(&mut app, window);
1506 let base = fret_ui::declarative::render_root(
1507 &mut ui,
1508 &mut app,
1509 &mut services,
1510 window,
1511 bounds,
1512 "base",
1513 |cx| {
1514 vec![
1515 cx.pressable_with_id(
1516 PressableProps {
1517 layout: {
1518 let mut layout = LayoutStyle::default();
1519 layout.size.width = Length::Px(Px(80.0));
1520 layout.size.height = Length::Px(Px(32.0));
1521 layout
1522 },
1523 focusable: true,
1524 ..Default::default()
1525 },
1526 |_cx, _st, id| {
1527 underlay_a = Some(id);
1528 Vec::new()
1529 },
1530 ),
1531 cx.pressable_with_id(
1532 PressableProps {
1533 layout: {
1534 let mut layout = LayoutStyle::default();
1535 layout.size.width = Length::Px(Px(80.0));
1536 layout.size.height = Length::Px(Px(32.0));
1537 layout
1538 },
1539 focusable: true,
1540 ..Default::default()
1541 },
1542 |_cx, _st, id| {
1543 underlay_b = Some(id);
1544 Vec::new()
1545 },
1546 ),
1547 ]
1548 },
1549 );
1550 ui.set_root(base);
1551 ui.layout_all(&mut app, &mut services, bounds, 1.0);
1552
1553 let underlay_a = underlay_a.expect("underlay a id");
1554 let underlay_b = underlay_b.expect("underlay b id");
1555 let underlay_a_node =
1556 fret_ui::elements::node_for_element(&mut app, window, underlay_a).expect("underlay a");
1557 let underlay_b_node =
1558 fret_ui::elements::node_for_element(&mut app, window, underlay_b).expect("underlay b");
1559
1560 let open = app.models_mut().insert(true);
1562 let mut modal_a: Option<GlobalElementId> = None;
1563 let mut modal_b: Option<GlobalElementId> = None;
1564
1565 OverlayController::begin_frame(&mut app, window);
1566 let modal_children =
1567 fret_ui::elements::with_element_cx(&mut app, window, bounds, "modal-child", |cx| {
1568 vec![
1569 cx.pressable_with_id(
1570 PressableProps {
1571 layout: {
1572 let mut layout = LayoutStyle::default();
1573 layout.size.width = Length::Px(Px(80.0));
1574 layout.size.height = Length::Px(Px(32.0));
1575 layout
1576 },
1577 focusable: true,
1578 ..Default::default()
1579 },
1580 |_cx, _st, id| {
1581 modal_a = Some(id);
1582 Vec::new()
1583 },
1584 ),
1585 cx.pressable_with_id(
1586 PressableProps {
1587 layout: {
1588 let mut layout = LayoutStyle::default();
1589 layout.size.width = Length::Px(Px(80.0));
1590 layout.size.height = Length::Px(Px(32.0));
1591 layout
1592 },
1593 focusable: true,
1594 ..Default::default()
1595 },
1596 |_cx, _st, id| {
1597 modal_b = Some(id);
1598 Vec::new()
1599 },
1600 ),
1601 ]
1602 });
1603
1604 let modal_a = modal_a.expect("modal a id");
1605 let modal_b = modal_b.expect("modal b id");
1606
1607 let mut req = OverlayRequest::modal(
1608 GlobalElementId(0x1234),
1609 None,
1610 open,
1611 OverlayPresence::instant(true),
1612 modal_children,
1613 );
1614 req.initial_focus = Some(modal_a);
1615 OverlayController::request_for_window(&mut app, window, req);
1616
1617 OverlayController::render(&mut ui, &mut app, &mut services, window, bounds);
1618 ui.layout_all(&mut app, &mut services, bounds, 1.0);
1619
1620 let modal_a_node =
1621 fret_ui::elements::node_for_element(&mut app, window, modal_a).expect("modal a");
1622 let modal_b_node =
1623 fret_ui::elements::node_for_element(&mut app, window, modal_b).expect("modal b");
1624
1625 assert_eq!(ui.focus(), Some(modal_a_node));
1626
1627 let _ = ui.dispatch_command(&mut app, &mut services, &CommandId::from("focus.next"));
1629 assert_eq!(ui.focus(), Some(modal_b_node));
1630 assert_ne!(ui.focus(), Some(underlay_a_node));
1631 assert_ne!(ui.focus(), Some(underlay_b_node));
1632
1633 let _ = ui.dispatch_command(&mut app, &mut services, &CommandId::from("focus.next"));
1634 assert_eq!(ui.focus(), Some(modal_a_node));
1635 assert_ne!(ui.focus(), Some(underlay_a_node));
1636 assert_ne!(ui.focus(), Some(underlay_b_node));
1637 }
1638
1639 #[test]
1640 fn modal_tab_keydown_cycles_focus_within_modal() {
1641 let window = AppWindowId::default();
1642 let mut app = App::new();
1643 let mut ui: UiTree<App> = UiTree::new();
1644 ui.set_window(window);
1645
1646 let mut services = FakeServices;
1647 let bounds = Rect::new(
1648 Point::new(Px(0.0), Px(0.0)),
1649 fret_core::Size::new(Px(300.0), Px(200.0)),
1650 );
1651
1652 let open = app.models_mut().insert(true);
1653 let mut modal_a: Option<GlobalElementId> = None;
1654 let mut modal_b: Option<GlobalElementId> = None;
1655
1656 OverlayController::begin_frame(&mut app, window);
1658 let base = fret_ui::declarative::render_root(
1659 &mut ui,
1660 &mut app,
1661 &mut services,
1662 window,
1663 bounds,
1664 "base",
1665 |_| Vec::new(),
1666 );
1667 ui.set_root(base);
1668
1669 OverlayController::begin_frame(&mut app, window);
1671 let modal_children =
1672 fret_ui::elements::with_element_cx(&mut app, window, bounds, "modal-child", |cx| {
1673 vec![
1674 cx.pressable_with_id(
1675 PressableProps {
1676 layout: {
1677 let mut layout = LayoutStyle::default();
1678 layout.size.width = Length::Px(Px(80.0));
1679 layout.size.height = Length::Px(Px(32.0));
1680 layout
1681 },
1682 focusable: true,
1683 ..Default::default()
1684 },
1685 |_cx, _st, id| {
1686 modal_a = Some(id);
1687 Vec::new()
1688 },
1689 ),
1690 cx.pressable_with_id(
1691 PressableProps {
1692 layout: {
1693 let mut layout = LayoutStyle::default();
1694 layout.size.width = Length::Px(Px(80.0));
1695 layout.size.height = Length::Px(Px(32.0));
1696 layout
1697 },
1698 focusable: true,
1699 ..Default::default()
1700 },
1701 |_cx, _st, id| {
1702 modal_b = Some(id);
1703 Vec::new()
1704 },
1705 ),
1706 ]
1707 });
1708
1709 let modal_a = modal_a.expect("modal a id");
1710 let modal_b = modal_b.expect("modal b id");
1711
1712 let mut req = OverlayRequest::modal(
1713 GlobalElementId(0x1234),
1714 None,
1715 open,
1716 OverlayPresence::instant(true),
1717 modal_children,
1718 );
1719 req.initial_focus = Some(modal_a);
1720 OverlayController::request_for_window(&mut app, window, req);
1721
1722 OverlayController::render(&mut ui, &mut app, &mut services, window, bounds);
1723 ui.layout_all(&mut app, &mut services, bounds, 1.0);
1724
1725 let modal_a_node =
1726 fret_ui::elements::node_for_element(&mut app, window, modal_a).expect("modal a");
1727 let modal_b_node =
1728 fret_ui::elements::node_for_element(&mut app, window, modal_b).expect("modal b");
1729
1730 assert_eq!(ui.focus(), Some(modal_a_node));
1731
1732 dispatch_keydown_and_apply_commands(
1734 &mut ui,
1735 &mut app,
1736 &mut services,
1737 KeyCode::Tab,
1738 Modifiers::default(),
1739 );
1740 assert_eq!(ui.focus(), Some(modal_b_node));
1741
1742 dispatch_keydown_and_apply_commands(
1744 &mut ui,
1745 &mut app,
1746 &mut services,
1747 KeyCode::Tab,
1748 Modifiers::default(),
1749 );
1750 assert_eq!(ui.focus(), Some(modal_a_node));
1751
1752 let mods = Modifiers {
1754 shift: true,
1755 ..Default::default()
1756 };
1757 dispatch_keydown_and_apply_commands(&mut ui, &mut app, &mut services, KeyCode::Tab, mods);
1758 assert_eq!(ui.focus(), Some(modal_b_node));
1759 }
1760
1761 #[test]
1762 fn transition_wrapper_keeps_independent_state_per_call_site() {
1763 let window = AppWindowId::default();
1764 let mut app = App::new();
1765
1766 app.set_tick_id(TickId(1));
1767 app.set_frame_id(FrameId(1));
1768
1769 let (a, b) = fret_ui::elements::with_element_cx(
1770 &mut app,
1771 window,
1772 Rect::new(
1773 Point::new(Px(0.0), Px(0.0)),
1774 fret_core::Size::new(Px(200.0), Px(120.0)),
1775 ),
1776 "overlay-transition-wrapper-independence",
1777 |cx| {
1778 let a = OverlayController::transition_with_durations(cx, true, 6, 6);
1779 let b = OverlayController::transition_with_durations(cx, false, 6, 6);
1780 (a, b)
1781 },
1782 );
1783
1784 assert!(a.present);
1785 assert!(a.animating);
1786 assert!(a.progress > 0.0 && a.progress < 1.0);
1787
1788 assert!(!b.present);
1789 assert!(!b.animating);
1790 assert_eq!(b.progress, 0.0);
1791 }
1792}