1use std::rc::Rc;
12use std::time::{Duration, Instant};
13
14use teksilo_canvas::{Point, Rect, Size, Vec2};
15use teksilo_tokens::Corner;
16
17use crate::environment::LayoutDirection;
18use crate::signal::Signal;
19use crate::widget_id::WidgetId;
20
21pub type OverlayDismissCallback = Rc<dyn Fn()>;
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
36pub struct OverlayId(u64);
37
38impl OverlayId {
39 pub(crate) fn new(id: u64) -> Self {
40 Self(id)
41 }
42}
43
44#[derive(Debug, Clone)]
46pub enum OverlayPlacement {
47 Below,
49 Above,
51 TrailingEdge,
53 AtPointer(Point),
55 NearAnchor { offset: Vec2 },
57 Centered,
59 BottomCenter,
61 BelowPreferred,
64 ViewportCorner { corner: Corner, margin: Vec2 },
71 FullViewport,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
89pub enum TooltipPlacement {
90 #[default]
92 Below,
93 Side,
95}
96
97#[derive(Debug, Clone)]
99pub enum DismissBehavior {
100 ClickOutside,
102 EscapeKey,
104 EscapeOrClickOutside,
106 PointerLeave { delay: Duration },
108 Manual,
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub enum OverlayLayer {
115 InTree,
117 NativePopup,
119 Auto,
121}
122
123pub struct OverlayRequest {
125 pub content_id: WidgetId,
127 pub anchor: WidgetId,
129 pub placement: OverlayPlacement,
131 pub dismiss: DismissBehavior,
133 pub layer: OverlayLayer,
135 pub parent_overlay: Option<OverlayId>,
137 pub on_dismiss: Option<OverlayDismissCallback>,
142 pub fade_duration: Option<Duration>,
151}
152
153impl OverlayRequest {
154 pub fn with_fade(mut self, duration: Duration) -> Self {
164 self.fade_duration = Some(duration);
165 self
166 }
167}
168
169#[derive(Clone)]
183pub(crate) struct OverlayFadeState {
184 pub opacity: Signal<f32>,
189 pub duration: Duration,
193 pub dismissing_started_real: Option<Instant>,
198 pub dismissing_started_sim: Option<Instant>,
202}
203
204pub(crate) struct ActiveOverlay {
206 pub id: OverlayId,
207 pub content_id: WidgetId,
208 pub anchor: WidgetId,
209 pub placement: OverlayPlacement,
210 pub dismiss: DismissBehavior,
211 pub layer: OverlayLayer,
212 pub parent_overlay: Option<OverlayId>,
213 pub bounds: Rect,
215 pub focus_restore: Option<WidgetId>,
218 pub pointer_leave_started_real: Option<std::time::Instant>,
220 pub pointer_leave_started_sim: Option<std::time::Instant>,
222 pub auto_dismiss_after: Option<Duration>,
224 pub paused_remaining: Option<Duration>,
231 pub shown_at_real: std::time::Instant,
233 pub shown_at_sim: std::time::Instant,
235 pub on_dismiss: Option<OverlayDismissCallback>,
239 pub fade: Option<OverlayFadeState>,
248}
249
250impl ActiveOverlay {
251 pub(crate) fn is_dismissing(&self) -> bool {
260 self.fade
261 .as_ref()
262 .is_some_and(|fade| fade.dismissing_started_real.is_some())
263 }
264}
265
266impl std::fmt::Debug for ActiveOverlay {
270 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
271 f.debug_struct("ActiveOverlay")
272 .field("id", &self.id)
273 .field("content_id", &self.content_id)
274 .field("anchor", &self.anchor)
275 .field("placement", &self.placement)
276 .field("dismiss", &self.dismiss)
277 .field("layer", &self.layer)
278 .field("parent_overlay", &self.parent_overlay)
279 .field("bounds", &self.bounds)
280 .field("focus_restore", &self.focus_restore)
281 .field(
282 "pointer_leave_started_real",
283 &self.pointer_leave_started_real,
284 )
285 .field("pointer_leave_started_sim", &self.pointer_leave_started_sim)
286 .field("auto_dismiss_after", &self.auto_dismiss_after)
287 .field("shown_at_real", &self.shown_at_real)
288 .field("shown_at_sim", &self.shown_at_sim)
289 .field(
290 "on_dismiss",
291 &self.on_dismiss.as_ref().map(|_| "<callback>"),
292 )
293 .field("fading", &self.fade.is_some())
294 .finish()
295 }
296}
297
298pub(crate) const MAX_OVERLAY_NESTING_DEPTH: usize = 12;
305
306fn leading_aligned_x(anchor: Rect, actual_width: f32, vw: f32, rtl: bool) -> f32 {
321 let leading = if rtl {
322 anchor.x + anchor.width - actual_width
323 } else {
324 anchor.x
325 };
326 leading.min(vw - actual_width).max(0.0)
327}
328
329pub struct OverlayManager {
330 pub(crate) stack: Vec<ActiveOverlay>,
331 next_id: u64,
332 sim_clock: Instant,
339 version: Signal<u64>,
345}
346
347impl OverlayManager {
348 pub fn new() -> Self {
349 Self {
350 stack: Vec::new(),
351 next_id: 1,
352 sim_clock: Instant::now(),
353 version: Signal::new(0),
354 }
355 }
356
357 pub fn version(&self) -> &Signal<u64> {
361 &self.version
362 }
363
364 fn bump_version(&self) {
366 self.version.set(self.version.get().wrapping_add(1));
367 }
368
369 pub(crate) fn set_sim_clock(&mut self, now_sim: Instant) {
374 self.sim_clock = now_sim;
375 }
376
377 pub fn show(&mut self, request: OverlayRequest) -> OverlayId {
379 self.show_with_auto_dismiss(request, None)
380 }
381
382 pub fn show_for(&mut self, request: OverlayRequest, duration: Duration) -> OverlayId {
384 self.show_with_auto_dismiss(request, Some(duration))
385 }
386
387 fn show_with_auto_dismiss(
388 &mut self,
389 request: OverlayRequest,
390 auto_dismiss_after: Option<Duration>,
391 ) -> OverlayId {
392 let id = OverlayId::new(self.next_id);
393 self.next_id += 1;
394
395 if self.ancestor_depth(request.parent_overlay) >= MAX_OVERLAY_NESTING_DEPTH {
403 return id;
404 }
405
406 let now = std::time::Instant::now();
407
408 let overlay = ActiveOverlay {
409 id,
410 content_id: request.content_id,
411 anchor: request.anchor,
412 placement: request.placement,
413 dismiss: request.dismiss,
414 layer: request.layer,
415 parent_overlay: request.parent_overlay,
416 bounds: Rect::ZERO,
417 focus_restore: None,
418 pointer_leave_started_real: None,
419 pointer_leave_started_sim: None,
420 auto_dismiss_after,
421 paused_remaining: None,
422 shown_at_real: now,
423 shown_at_sim: now,
424 on_dismiss: request.on_dismiss,
425 fade: None,
426 };
427 self.stack.push(overlay);
428 self.bump_version();
429 id
430 }
431
432 pub(crate) fn attach_fade(&mut self, id: OverlayId, opacity: Signal<f32>, duration: Duration) {
439 if let Some(overlay) = self.stack.iter_mut().find(|o| o.id == id) {
440 overlay.fade = Some(OverlayFadeState {
441 opacity,
442 duration,
443 dismissing_started_real: None,
444 dismissing_started_sim: None,
445 });
446 }
447 }
448
449 pub fn fade_duration(&self, id: OverlayId) -> Option<Duration> {
454 self.stack
455 .iter()
456 .find(|o| o.id == id)
457 .and_then(|o| o.fade.as_ref().map(|f| f.duration))
458 }
459
460 pub fn next_auto_dismiss_deadline(&self) -> Option<std::time::Instant> {
461 self.stack
462 .iter()
463 .filter_map(|overlay| {
464 overlay
465 .auto_dismiss_after
466 .map(|delay| overlay.shown_at_real + delay)
467 })
468 .min()
469 }
470
471 pub fn next_pointer_leave_deadline(&self) -> Option<std::time::Instant> {
482 self.stack
483 .iter()
484 .filter_map(|overlay| {
485 let DismissBehavior::PointerLeave { delay } = overlay.dismiss else {
486 return None;
487 };
488 Some(overlay.pointer_leave_started_real? + delay)
489 })
490 .min()
491 }
492
493 pub fn pause_auto_dismiss(&mut self, id: OverlayId) {
509 if let Some(overlay) = self.stack.iter_mut().find(|o| o.id == id)
510 && overlay.paused_remaining.is_none()
511 && let Some(delay) = overlay.auto_dismiss_after.take()
512 {
513 let elapsed = overlay.shown_at_real.elapsed();
514 overlay.paused_remaining = Some(delay.saturating_sub(elapsed));
515 }
516 }
517
518 pub fn resume_auto_dismiss(&mut self, id: OverlayId) {
525 if let Some(overlay) = self.stack.iter_mut().find(|o| o.id == id)
526 && let Some(remaining) = overlay.paused_remaining.take()
527 {
528 overlay.auto_dismiss_after = Some(remaining);
529 let now = std::time::Instant::now();
530 overlay.shown_at_real = now;
531 overlay.shown_at_sim = self.sim_clock;
532 }
533 }
534
535 pub fn is_auto_dismiss_paused(&self, id: OverlayId) -> bool {
539 self.stack
540 .iter()
541 .find(|o| o.id == id)
542 .is_some_and(|o| o.paused_remaining.is_some())
543 }
544
545 pub(crate) fn set_shown_at_sim(&mut self, id: OverlayId, shown_at_sim: std::time::Instant) {
546 if let Some(overlay) = self.stack.iter_mut().find(|overlay| overlay.id == id) {
547 overlay.shown_at_sim = shown_at_sim;
548 }
549 }
550
551 fn ancestor_depth(&self, parent: Option<OverlayId>) -> usize {
557 let mut depth = 0;
558 let mut current = parent;
559 while let Some(p) = current {
560 depth += 1;
561 if depth > self.stack.len() {
562 break;
565 }
566 current = self
567 .stack
568 .iter()
569 .find(|overlay| overlay.id == p)
570 .and_then(|overlay| overlay.parent_overlay);
571 }
572 depth
573 }
574
575 pub(crate) fn is_descendant_of(&self, child: OverlayId, ancestor: OverlayId) -> bool {
576 let mut current = self
577 .stack
578 .iter()
579 .find(|overlay| overlay.id == child)
580 .and_then(|overlay| overlay.parent_overlay);
581
582 while let Some(parent) = current {
583 if parent == ancestor {
584 return true;
585 }
586 current = self
587 .stack
588 .iter()
589 .find(|overlay| overlay.id == parent)
590 .and_then(|overlay| overlay.parent_overlay);
591 }
592
593 false
594 }
595
596 pub(crate) fn overlay(&self, id: OverlayId) -> Option<&ActiveOverlay> {
597 self.stack.iter().find(|overlay| overlay.id == id)
598 }
599
600 pub fn bounds_for(&self, id: OverlayId) -> Option<Rect> {
611 self.overlay(id).map(|o| o.bounds)
612 }
613
614 pub(crate) fn topmost_centered(&self) -> Option<&ActiveOverlay> {
615 self.stack
616 .iter()
617 .rev()
618 .find(|overlay| matches!(overlay.placement, OverlayPlacement::Centered))
619 }
620
621 pub fn dismiss_with_focus_restore(
624 &mut self,
625 id: OverlayId,
626 ) -> (Vec<WidgetId>, Option<WidgetId>) {
627 let focus_restore = self
628 .stack
629 .iter()
630 .find(|overlay| overlay.id == id)
631 .and_then(|overlay| overlay.focus_restore);
632 let dismissed = self.dismiss(id);
633 (dismissed, focus_restore)
634 }
635
636 pub fn dismiss_descendants_of(
639 &mut self,
640 parent: OverlayId,
641 preserve: Option<OverlayId>,
642 ) -> (Vec<WidgetId>, Option<WidgetId>) {
643 let mut to_dismiss = Vec::new();
644
645 for overlay in &self.stack {
646 if !self.is_descendant_of(overlay.id, parent) {
647 continue;
648 }
649 if preserve
650 .is_some_and(|keep| overlay.id == keep || self.is_descendant_of(overlay.id, keep))
651 {
652 continue;
653 }
654 to_dismiss.push(overlay.id);
655 }
656
657 if to_dismiss.is_empty() {
658 return (Vec::new(), None);
659 }
660
661 let focus_restore = self
662 .stack
663 .iter()
664 .rev()
665 .find(|overlay| to_dismiss.contains(&overlay.id))
666 .and_then(|overlay| overlay.focus_restore);
667
668 let dismissed_content: Vec<WidgetId> = self
669 .stack
670 .iter()
671 .filter(|overlay| to_dismiss.contains(&overlay.id))
672 .map(|overlay| overlay.content_id)
673 .collect();
674 let callbacks: Vec<OverlayDismissCallback> = self
675 .stack
676 .iter()
677 .filter(|overlay| to_dismiss.contains(&overlay.id))
678 .filter_map(|overlay| overlay.on_dismiss.clone())
679 .collect();
680 self.stack
681 .retain(|overlay| !to_dismiss.contains(&overlay.id));
682 for cb in callbacks {
683 cb();
684 }
685
686 (dismissed_content, focus_restore)
687 }
688
689 pub fn update_placement(&mut self, id: OverlayId, placement: OverlayPlacement) {
691 if let Some(overlay) = self.stack.iter_mut().find(|o| o.id == id) {
692 overlay.placement = placement;
693 }
694 }
695
696 pub fn set_parent_overlay(&mut self, id: OverlayId, parent: Option<OverlayId>) {
702 if let Some(overlay) = self.stack.iter_mut().find(|o| o.id == id) {
703 overlay.parent_overlay = parent;
704 }
705 }
706
707 pub fn dismiss(&mut self, id: OverlayId) -> Vec<WidgetId> {
721 let sim_now = self.sim_clock;
727 if let Some(overlay) = self.stack.iter_mut().find(|o| o.id == id)
728 && let Some(fade) = &mut overlay.fade
729 && fade.dismissing_started_real.is_none()
730 {
731 let _ = fade
737 .opacity
738 .try_animate_with_options(crate::animation::AnimationRequest {
739 target: 0.0,
740 duration: fade.duration,
741 easing: teksilo_tokens::Easing::EaseOut,
742 frame_interval: None,
743 looping: false,
744 epsilon: 0.0,
745 max_duration: None,
746 });
747 let now_real = Instant::now();
748 fade.dismissing_started_real = Some(now_real);
749 fade.dismissing_started_sim = Some(sim_now);
750 return Vec::new();
751 }
752 self.dismiss_immediate(id)
753 }
754
755 pub(crate) fn dismiss_immediate(&mut self, id: OverlayId) -> Vec<WidgetId> {
762 let mut to_dismiss = vec![id];
764 let mut i = 0;
765 while i < to_dismiss.len() {
766 let parent = to_dismiss[i];
767 for overlay in &self.stack {
768 if overlay.parent_overlay == Some(parent) && !to_dismiss.contains(&overlay.id) {
769 to_dismiss.push(overlay.id);
770 }
771 }
772 i += 1;
773 }
774 let dismissed_content: Vec<WidgetId> = self
775 .stack
776 .iter()
777 .filter(|o| to_dismiss.contains(&o.id))
778 .map(|o| o.content_id)
779 .collect();
780 let callbacks: Vec<OverlayDismissCallback> = self
785 .stack
786 .iter()
787 .filter(|o| to_dismiss.contains(&o.id))
788 .filter_map(|o| o.on_dismiss.clone())
789 .collect();
790 self.stack.retain(|o| !to_dismiss.contains(&o.id));
791 if !to_dismiss.is_empty() {
792 self.bump_version();
793 }
794 for cb in callbacks {
795 cb();
796 }
797 dismissed_content
798 }
799
800 pub fn process_pending_fade_dismissals(
809 &mut self,
810 now: Instant,
811 ) -> Vec<(OverlayId, Vec<WidgetId>, Option<WidgetId>)> {
812 self.process_pending_fade_dismissals_with(|fade| {
813 let started = fade.dismissing_started_real?;
814 Some(now.saturating_duration_since(started) >= fade.duration)
815 })
816 }
817
818 pub fn process_pending_fade_dismissals_sim(
822 &mut self,
823 now_sim: Instant,
824 ) -> Vec<(OverlayId, Vec<WidgetId>, Option<WidgetId>)> {
825 self.process_pending_fade_dismissals_with(|fade| {
826 let started = fade.dismissing_started_sim?;
827 Some(now_sim.saturating_duration_since(started) >= fade.duration)
828 })
829 }
830
831 fn process_pending_fade_dismissals_with(
832 &mut self,
833 mut elapsed_done: impl FnMut(&OverlayFadeState) -> Option<bool>,
834 ) -> Vec<(OverlayId, Vec<WidgetId>, Option<WidgetId>)> {
835 let ready: Vec<(OverlayId, Option<WidgetId>)> = self
836 .stack
837 .iter()
838 .filter_map(|o| {
839 let fade = o.fade.as_ref()?;
840 if elapsed_done(fade)? {
841 Some((o.id, o.focus_restore))
842 } else {
843 None
844 }
845 })
846 .collect();
847 ready
848 .into_iter()
849 .map(|(id, focus_restore)| {
850 let dismissed = self.dismiss_immediate(id);
851 (id, dismissed, focus_restore)
852 })
853 .collect()
854 }
855
856 pub fn next_fade_dismiss_deadline(&self) -> Option<Instant> {
860 self.stack
861 .iter()
862 .filter_map(|o| {
863 let fade = o.fade.as_ref()?;
864 let started = fade.dismissing_started_real?;
865 Some(started + fade.duration)
866 })
867 .min()
868 }
869
870 pub fn dismiss_top(&mut self) -> Option<(OverlayId, Vec<WidgetId>, Option<WidgetId>)> {
873 if let Some(overlay) = self.stack.last() {
874 let id = overlay.id;
875 let focus_restore = overlay.focus_restore;
876 let content_ids = self.dismiss(id);
877 Some((id, content_ids, focus_restore))
878 } else {
879 None
880 }
881 }
882
883 pub fn try_dismiss_top_on_escape(
901 &mut self,
902 ) -> Option<(OverlayId, Vec<WidgetId>, Option<WidgetId>)> {
903 let target = self
904 .stack
905 .iter()
906 .rev()
907 .filter(|o| !o.is_dismissing())
912 .find_map(|o| match o.dismiss {
913 DismissBehavior::EscapeKey
914 | DismissBehavior::EscapeOrClickOutside
915 | DismissBehavior::PointerLeave { .. } => Some(Some(o.id)),
916 DismissBehavior::Manual => Some(None),
918 DismissBehavior::ClickOutside => None,
919 })??;
920 let focus_restore = self
921 .stack
922 .iter()
923 .find(|o| o.id == target)
924 .and_then(|o| o.focus_restore);
925 let content_ids = self.dismiss(target);
926 Some((target, content_ids, focus_restore))
927 }
928
929 pub fn set_top_focus_restore(&mut self, focus_restore: WidgetId) {
931 if let Some(overlay) = self.stack.last_mut() {
932 overlay.focus_restore = Some(focus_restore);
933 }
934 }
935
936 pub fn dismiss_all(&mut self) -> Vec<WidgetId> {
946 let content_ids: Vec<WidgetId> = self.stack.iter().map(|o| o.content_id).collect();
947 if content_ids.is_empty() {
948 return content_ids;
949 }
950 let callbacks: Vec<OverlayDismissCallback> = self
956 .stack
957 .iter()
958 .filter_map(|o| o.on_dismiss.clone())
959 .collect();
960 self.stack.clear();
961 self.bump_version();
962 for cb in callbacks {
963 cb();
964 }
965 content_ids
966 }
967
968 pub fn dismiss_except(&mut self, keep: &std::collections::HashSet<WidgetId>) -> Vec<WidgetId> {
973 let dismissed: Vec<WidgetId> = self
974 .stack
975 .iter()
976 .filter(|o| !keep.contains(&o.content_id))
977 .map(|o| o.content_id)
978 .collect();
979 if dismissed.is_empty() {
980 return dismissed;
981 }
982 let callbacks: Vec<OverlayDismissCallback> = self
985 .stack
986 .iter()
987 .filter(|o| !keep.contains(&o.content_id))
988 .filter_map(|o| o.on_dismiss.clone())
989 .collect();
990 self.stack.retain(|o| keep.contains(&o.content_id));
991 self.bump_version();
992 for cb in callbacks {
993 cb();
994 }
995 dismissed
996 }
997
998 pub fn is_empty(&self) -> bool {
1000 self.stack.is_empty()
1001 }
1002
1003 pub fn len(&self) -> usize {
1005 self.stack.len()
1006 }
1007
1008 pub fn active_content_ids(&self) -> Vec<WidgetId> {
1010 self.stack.iter().map(|o| o.content_id).collect()
1011 }
1012
1013 pub fn active_ids(&self) -> Vec<OverlayId> {
1021 self.stack
1022 .iter()
1023 .filter(|o| {
1024 o.fade
1025 .as_ref()
1026 .is_none_or(|f| f.dismissing_started_real.is_none())
1027 })
1028 .map(|o| o.id)
1029 .collect()
1030 }
1031
1032 pub fn anchor_for(&self, id: OverlayId) -> Option<WidgetId> {
1034 self.stack.iter().find(|o| o.id == id).map(|o| o.anchor)
1035 }
1036
1037 pub fn interactive_rects(&self) -> Vec<Rect> {
1053 self.stack
1054 .iter()
1055 .filter(|o| {
1056 o.fade
1057 .as_ref()
1058 .is_none_or(|f| f.dismissing_started_real.is_none())
1059 })
1060 .map(|o| o.bounds)
1061 .filter(|r| r.width > 0.0 && r.height > 0.0)
1062 .collect()
1063 }
1064
1065 #[allow(dead_code)] pub(crate) fn topmost(&self) -> Option<&ActiveOverlay> {
1068 self.stack.last()
1069 }
1070
1071 pub fn hit_test(&self, point: Point) -> Option<OverlayId> {
1082 for overlay in self.stack.iter().rev() {
1083 let fading_out = overlay
1084 .fade
1085 .as_ref()
1086 .is_some_and(|f| f.dismissing_started_real.is_some());
1087 if !fading_out && overlay.bounds.contains(point) {
1088 return Some(overlay.id);
1089 }
1090 }
1091 None
1092 }
1093
1094 pub fn handle_click_outside(
1116 &mut self,
1117 point: Point,
1118 ) -> (Vec<WidgetId>, Option<WidgetId>, Vec<WidgetId>) {
1119 if self.stack.is_empty() {
1120 return (Vec::new(), None, Vec::new());
1121 }
1122
1123 let hit_index = self.stack.iter().enumerate().rev().find_map(|(i, o)| {
1139 let fading_out = o
1140 .fade
1141 .as_ref()
1142 .is_some_and(|f| f.dismissing_started_real.is_some());
1143 (!fading_out && o.bounds.contains(point)).then_some(i)
1144 });
1145
1146 let mut to_dismiss: Vec<OverlayId> = Vec::new();
1155 let mut toggle_anchors: Vec<WidgetId> = Vec::new();
1156 for (i, o) in self.stack.iter().enumerate() {
1157 if hit_index.is_some_and(|k| i <= k) {
1160 continue;
1161 }
1162 match o.dismiss {
1163 DismissBehavior::ClickOutside | DismissBehavior::EscapeOrClickOutside => {
1164 to_dismiss.push(o.id);
1165 toggle_anchors.push(o.anchor);
1166 }
1167 DismissBehavior::PointerLeave { .. } => to_dismiss.push(o.id),
1168 DismissBehavior::EscapeKey | DismissBehavior::Manual => {}
1169 }
1170 }
1171
1172 if to_dismiss.is_empty() {
1173 return (Vec::new(), None, Vec::new());
1174 }
1175
1176 let focus_restore = self
1177 .stack
1178 .iter()
1179 .find(|o| to_dismiss.contains(&o.id))
1180 .and_then(|o| o.focus_restore);
1181
1182 let mut all_dismissed = Vec::new();
1183 for id in to_dismiss {
1184 all_dismissed.extend(self.dismiss(id));
1185 }
1186 (all_dismissed, focus_restore, toggle_anchors)
1187 }
1188
1189 pub fn position_overlays(
1200 &mut self,
1201 anchor_bounds_fn: impl Fn(WidgetId) -> Option<Rect>,
1202 viewport: (f32, f32),
1203 layout_direction: LayoutDirection,
1204 ) {
1205 let (vw, vh) = viewport;
1206 let rtl = matches!(layout_direction, LayoutDirection::RightToLeft);
1207 for overlay in &mut self.stack {
1208 let anchor = match anchor_bounds_fn(overlay.anchor) {
1209 Some(a) => a,
1210 None => {
1211 if matches!(
1217 overlay.placement,
1218 OverlayPlacement::Centered
1219 | OverlayPlacement::FullViewport
1220 | OverlayPlacement::BottomCenter
1221 | OverlayPlacement::ViewportCorner { .. }
1222 | OverlayPlacement::AtPointer(_)
1223 ) {
1224 Rect::ZERO
1225 } else {
1226 continue;
1227 }
1228 }
1229 };
1230 let content_size = overlay.bounds.size(); overlay.bounds = match &overlay.placement {
1233 OverlayPlacement::Below => {
1234 let actual_width = content_size.width.max(anchor.width);
1235 let x = leading_aligned_x(anchor, actual_width, vw, rtl);
1236 Rect::new(
1237 x,
1238 anchor.y + anchor.height + 4.0,
1239 actual_width,
1240 content_size.height,
1241 )
1242 }
1243 OverlayPlacement::Above => {
1244 let actual_width = content_size.width.max(anchor.width);
1245 let x = leading_aligned_x(anchor, actual_width, vw, rtl);
1246 Rect::new(
1247 x,
1248 anchor.y - content_size.height - 4.0,
1249 actual_width,
1250 content_size.height,
1251 )
1252 }
1253 OverlayPlacement::TrailingEdge => {
1254 let x = if rtl {
1256 let x_left = anchor.x - content_size.width - 2.0;
1257 if x_left >= 0.0 {
1258 x_left
1259 } else {
1260 anchor.x + anchor.width + 2.0
1262 }
1263 } else {
1264 let x_right = anchor.x + anchor.width + 2.0;
1265 if x_right + content_size.width <= vw {
1266 x_right
1267 } else {
1268 anchor.x - content_size.width - 2.0
1270 }
1271 };
1272 let y = anchor.y.min(vh - content_size.height).max(0.0);
1273 Rect::new(x, y, content_size.width, content_size.height)
1274 }
1275 OverlayPlacement::AtPointer(point) => {
1276 let x = point.x.min(vw - content_size.width).max(0.0);
1278 let y = if point.y + content_size.height <= vh {
1279 point.y
1280 } else {
1281 (point.y - content_size.height).max(0.0)
1283 };
1284 Rect::new(x, y, content_size.width, content_size.height)
1285 }
1286 OverlayPlacement::NearAnchor { offset } => {
1287 let below_y = anchor.y + anchor.height + offset.y + 4.0;
1295 let fits_below = below_y + content_size.height <= vh;
1296 let y = if fits_below {
1297 below_y
1298 } else {
1299 let above_y = anchor.y - content_size.height - offset.y - 4.0;
1301 above_y.max(0.0)
1302 };
1303 let unclamped_x = if rtl {
1310 anchor.x + anchor.width - content_size.width - offset.x
1311 } else {
1312 anchor.x + offset.x
1313 };
1314 let x = unclamped_x.min(vw - content_size.width).max(0.0);
1315 Rect::new(x, y, content_size.width, content_size.height)
1316 }
1317 OverlayPlacement::Centered => Rect::new(
1318 ((vw - content_size.width) / 2.0).max(0.0),
1319 ((vh - content_size.height) / 2.0).max(0.0),
1320 content_size.width.min(vw),
1321 content_size.height.min(vh),
1322 ),
1323 OverlayPlacement::BottomCenter => Rect::new(
1324 ((vw - content_size.width) / 2.0).max(0.0),
1325 (vh - content_size.height - 24.0).max(0.0),
1326 content_size.width.min(vw),
1327 content_size.height.min(vh),
1328 ),
1329 OverlayPlacement::BelowPreferred => {
1330 let below_y = anchor.y + anchor.height + 4.0;
1331 let fits_below = below_y + content_size.height <= vh;
1332 let y = if fits_below {
1333 below_y
1334 } else {
1335 anchor.y - content_size.height - 4.0
1336 };
1337 let actual_width = content_size.width.max(anchor.width);
1338 let x = if rtl {
1340 (anchor.x + anchor.width - actual_width)
1341 .min(vw - actual_width)
1342 .max(0.0)
1343 } else {
1344 anchor.x.min(vw - actual_width).max(0.0)
1345 };
1346 Rect::new(x, y, actual_width, content_size.height)
1347 }
1348 OverlayPlacement::ViewportCorner { corner, margin } => {
1349 let (x, y) = corner.resolve(
1350 (content_size.width, content_size.height),
1351 (vw, vh),
1352 (margin.x, margin.y),
1353 rtl,
1354 );
1355 Rect::new(
1356 x,
1357 y,
1358 content_size.width.min(vw),
1359 content_size.height.min(vh),
1360 )
1361 }
1362 OverlayPlacement::FullViewport => Rect::new(0.0, 0.0, vw, vh),
1363 };
1364 }
1365 }
1366
1367 pub fn set_content_bounds(&mut self, id: OverlayId, size: Size) {
1369 if let Some(overlay) = self.stack.iter_mut().find(|o| o.id == id) {
1370 overlay.bounds = Rect::new(overlay.bounds.x, overlay.bounds.y, size.width, size.height);
1371 }
1372 }
1373
1374 pub fn find_by_content(&self, content_id: WidgetId) -> Option<OverlayId> {
1376 self.stack
1377 .iter()
1378 .find(|o| o.content_id == content_id)
1379 .map(|o| o.id)
1380 }
1381
1382 pub fn bounds_for_content(&self, content_id: WidgetId) -> Option<Rect> {
1387 self.stack
1388 .iter()
1389 .find(|o| o.content_id == content_id)
1390 .map(|o| o.bounds)
1391 }
1392
1393 pub fn set_dismiss(&mut self, id: OverlayId, behavior: DismissBehavior) {
1401 if let Some(overlay) = self.stack.iter_mut().find(|o| o.id == id) {
1402 overlay.dismiss = behavior;
1403 overlay.pointer_leave_started_real = None;
1404 overlay.pointer_leave_started_sim = None;
1405 }
1406 }
1407}
1408
1409impl Default for OverlayManager {
1410 fn default() -> Self {
1411 Self::new()
1412 }
1413}
1414
1415impl std::fmt::Debug for OverlayManager {
1416 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1417 f.debug_struct("OverlayManager")
1418 .field("active_count", &self.stack.len())
1419 .finish()
1420 }
1421}
1422
1423#[cfg(test)]
1424mod tests {
1425 use super::*;
1426 use slotmap::KeyData;
1427
1428 fn fake_id(n: u64) -> WidgetId {
1429 KeyData::from_ffi(n).into()
1430 }
1431
1432 #[test]
1441 fn a_wide_overlay_near_the_trailing_edge_is_clamped_into_the_viewport() {
1442 let vw = 1200.0;
1443 let anchor = Rect::new(1035.0, 760.0, 90.0, 28.0);
1446 let x = leading_aligned_x(anchor, 380.0, vw, false);
1447 assert!(
1448 x + 380.0 <= vw + 0.01,
1449 "overlay must not extend past the viewport: x={x}"
1450 );
1451 assert!(x >= 0.0, "and must not start off the leading edge: x={x}");
1452
1453 let inside = Rect::new(100.0, 760.0, 90.0, 28.0);
1456 assert_eq!(leading_aligned_x(inside, 380.0, vw, false), 100.0);
1457
1458 let x_rtl = leading_aligned_x(inside, 380.0, vw, true);
1460 assert!(x_rtl >= 0.0 && x_rtl + 380.0 <= vw + 0.01);
1461 }
1462
1463 #[test]
1467 fn an_overlay_wider_than_the_viewport_keeps_its_leading_edge_visible() {
1468 let x = leading_aligned_x(Rect::new(40.0, 10.0, 60.0, 20.0), 900.0, 500.0, false);
1469 assert_eq!(x, 0.0);
1470 }
1471
1472 #[test]
1473 fn dismiss_all_fires_on_dismiss_callbacks() {
1474 use std::cell::Cell;
1482 use std::rc::Rc;
1483 let mut mgr = OverlayManager::new();
1484 let fired_a = Rc::new(Cell::new(0_u32));
1485 let fired_b = Rc::new(Cell::new(0_u32));
1486 let cb_a: OverlayDismissCallback = {
1487 let f = fired_a.clone();
1488 Rc::new(move || f.set(f.get() + 1))
1489 };
1490 let cb_b: OverlayDismissCallback = {
1491 let f = fired_b.clone();
1492 Rc::new(move || f.set(f.get() + 1))
1493 };
1494 mgr.show(OverlayRequest {
1495 content_id: fake_id(10),
1496 anchor: fake_id(1),
1497 placement: OverlayPlacement::Below,
1498 dismiss: DismissBehavior::ClickOutside,
1499 layer: OverlayLayer::InTree,
1500 parent_overlay: None,
1501 on_dismiss: Some(cb_a),
1502 fade_duration: None,
1503 });
1504 mgr.show(OverlayRequest {
1505 content_id: fake_id(11),
1506 anchor: fake_id(2),
1507 placement: OverlayPlacement::Below,
1508 dismiss: DismissBehavior::ClickOutside,
1509 layer: OverlayLayer::InTree,
1510 parent_overlay: None,
1511 on_dismiss: Some(cb_b),
1512 fade_duration: None,
1513 });
1514 let dismissed = mgr.dismiss_all();
1515 assert_eq!(dismissed.len(), 2);
1516 assert!(mgr.is_empty());
1517 assert_eq!(
1518 fired_a.get(),
1519 1,
1520 "first overlay's on_dismiss must fire exactly once",
1521 );
1522 assert_eq!(
1523 fired_b.get(),
1524 1,
1525 "second overlay's on_dismiss must fire exactly once",
1526 );
1527 }
1528
1529 #[test]
1530 fn show_and_dismiss() {
1531 let mut mgr = OverlayManager::new();
1532 let id = mgr.show(OverlayRequest {
1533 content_id: fake_id(10),
1534 anchor: fake_id(1),
1535 placement: OverlayPlacement::Below,
1536 dismiss: DismissBehavior::ClickOutside,
1537 layer: OverlayLayer::InTree,
1538 parent_overlay: None,
1539 on_dismiss: None,
1540 fade_duration: None,
1541 });
1542 assert_eq!(mgr.len(), 1);
1543
1544 mgr.dismiss(id);
1545 assert!(mgr.is_empty());
1546 }
1547
1548 #[test]
1549 fn cascade_dismissal() {
1550 let mut mgr = OverlayManager::new();
1551 let parent = mgr.show(OverlayRequest {
1552 content_id: fake_id(10),
1553 anchor: fake_id(1),
1554 placement: OverlayPlacement::Below,
1555 dismiss: DismissBehavior::ClickOutside,
1556 layer: OverlayLayer::InTree,
1557 parent_overlay: None,
1558 on_dismiss: None,
1559 fade_duration: None,
1560 });
1561 let _child = mgr.show(OverlayRequest {
1562 content_id: fake_id(11),
1563 anchor: fake_id(10),
1564 placement: OverlayPlacement::TrailingEdge,
1565 dismiss: DismissBehavior::ClickOutside,
1566 layer: OverlayLayer::InTree,
1567 parent_overlay: Some(parent),
1568 on_dismiss: None,
1569 fade_duration: None,
1570 });
1571 assert_eq!(mgr.len(), 2);
1572
1573 mgr.dismiss(parent);
1575 assert!(mgr.is_empty());
1576 }
1577
1578 #[test]
1579 fn cascade_depth_is_bounded() {
1580 let mut mgr = OverlayManager::new();
1586 let mut parent = mgr.show(OverlayRequest {
1587 content_id: fake_id(100),
1588 anchor: fake_id(1),
1589 placement: OverlayPlacement::Below,
1590 dismiss: DismissBehavior::Manual,
1591 layer: OverlayLayer::InTree,
1592 parent_overlay: None,
1593 on_dismiss: None,
1594 fade_duration: None,
1595 });
1596 for i in 1..MAX_OVERLAY_NESTING_DEPTH {
1599 parent = mgr.show(OverlayRequest {
1600 content_id: fake_id(100 + i as u64),
1601 anchor: fake_id(1),
1602 placement: OverlayPlacement::Below,
1603 dismiss: DismissBehavior::Manual,
1604 layer: OverlayLayer::InTree,
1605 parent_overlay: Some(parent),
1606 on_dismiss: None,
1607 fade_duration: None,
1608 });
1609 }
1610 assert_eq!(
1611 mgr.len(),
1612 MAX_OVERLAY_NESTING_DEPTH,
1613 "chain should fill exactly to the cap"
1614 );
1615
1616 let dropped = mgr.show(OverlayRequest {
1618 content_id: fake_id(999),
1619 anchor: fake_id(1),
1620 placement: OverlayPlacement::Below,
1621 dismiss: DismissBehavior::Manual,
1622 layer: OverlayLayer::InTree,
1623 parent_overlay: Some(parent),
1624 on_dismiss: None,
1625 fade_duration: None,
1626 });
1627 assert_eq!(
1628 mgr.len(),
1629 MAX_OVERLAY_NESTING_DEPTH,
1630 "over-cap overlay must not be pushed"
1631 );
1632 assert!(
1633 mgr.stack.iter().all(|o| o.id != dropped),
1634 "the dropped overlay id must not appear in the stack"
1635 );
1636 }
1637
1638 #[test]
1639 fn dismiss_top() {
1640 let mut mgr = OverlayManager::new();
1641 let _a = mgr.show(OverlayRequest {
1642 content_id: fake_id(10),
1643 anchor: fake_id(1),
1644 placement: OverlayPlacement::Below,
1645 dismiss: DismissBehavior::Manual,
1646 layer: OverlayLayer::InTree,
1647 parent_overlay: None,
1648 on_dismiss: None,
1649 fade_duration: None,
1650 });
1651 let b = mgr.show(OverlayRequest {
1652 content_id: fake_id(11),
1653 anchor: fake_id(2),
1654 placement: OverlayPlacement::Below,
1655 dismiss: DismissBehavior::Manual,
1656 layer: OverlayLayer::InTree,
1657 parent_overlay: None,
1658 on_dismiss: None,
1659 fade_duration: None,
1660 });
1661
1662 let dismissed = mgr.dismiss_top();
1663 assert_eq!(dismissed.map(|(id, _, _)| id), Some(b));
1664 assert_eq!(mgr.len(), 1);
1665 }
1666
1667 #[test]
1668 fn click_outside_dismisses() {
1669 let mut mgr = OverlayManager::new();
1670 mgr.show(OverlayRequest {
1671 content_id: fake_id(10),
1672 anchor: fake_id(1),
1673 placement: OverlayPlacement::Below,
1674 dismiss: DismissBehavior::ClickOutside,
1675 layer: OverlayLayer::InTree,
1676 parent_overlay: None,
1677 on_dismiss: None,
1678 fade_duration: None,
1679 });
1680
1681 let id = mgr.active_ids()[0];
1683 mgr.set_content_bounds(id, Size::new(100.0, 50.0));
1684
1685 let (dismissed, _, _) = mgr.handle_click_outside(Point::new(50.0, 25.0));
1687 assert!(dismissed.is_empty());
1688 assert_eq!(mgr.len(), 1);
1689
1690 let (dismissed, _, _) = mgr.handle_click_outside(Point::new(500.0, 500.0));
1692 assert!(!dismissed.is_empty());
1693 assert!(mgr.is_empty());
1694 }
1695
1696 #[test]
1697 fn click_outside_returns_focus_restore() {
1698 let mut mgr = OverlayManager::new();
1699 let trigger = fake_id(99);
1700 mgr.show(OverlayRequest {
1701 content_id: fake_id(10),
1702 anchor: fake_id(1),
1703 placement: OverlayPlacement::Below,
1704 dismiss: DismissBehavior::ClickOutside,
1705 layer: OverlayLayer::InTree,
1706 parent_overlay: None,
1707 on_dismiss: None,
1708 fade_duration: None,
1709 });
1710 let id = mgr.active_ids()[0];
1711 mgr.set_content_bounds(id, Size::new(100.0, 50.0));
1712 mgr.set_top_focus_restore(trigger);
1713
1714 let (dismissed, focus_restore, _) = mgr.handle_click_outside(Point::new(500.0, 500.0));
1715 assert_eq!(dismissed.len(), 1);
1716 assert_eq!(focus_restore, Some(trigger));
1717 }
1718
1719 #[test]
1720 fn click_outside_focus_restore_picks_bottommost() {
1721 let mut mgr = OverlayManager::new();
1727 let pre_overlay_focus = fake_id(99);
1728 let inside_a = fake_id(50);
1729 let a = mgr.show(OverlayRequest {
1730 content_id: fake_id(10),
1731 anchor: fake_id(1),
1732 placement: OverlayPlacement::Below,
1733 dismiss: DismissBehavior::ClickOutside,
1734 layer: OverlayLayer::InTree,
1735 parent_overlay: None,
1736 on_dismiss: None,
1737 fade_duration: None,
1738 });
1739 mgr.set_content_bounds(a, Size::new(100.0, 50.0));
1740 mgr.set_top_focus_restore(pre_overlay_focus);
1741 let b = mgr.show(OverlayRequest {
1742 content_id: fake_id(11),
1743 anchor: fake_id(2),
1744 placement: OverlayPlacement::Below,
1745 dismiss: DismissBehavior::ClickOutside,
1746 layer: OverlayLayer::InTree,
1747 parent_overlay: None,
1748 on_dismiss: None,
1749 fade_duration: None,
1750 });
1751 mgr.set_content_bounds(b, Size::new(100.0, 50.0));
1752 mgr.set_top_focus_restore(inside_a);
1753
1754 let (_, focus_restore, _) = mgr.handle_click_outside(Point::new(500.0, 500.0));
1755 assert_eq!(focus_restore, Some(pre_overlay_focus));
1756 }
1757
1758 #[test]
1759 fn manual_dismiss_ignores_click_outside() {
1760 let mut mgr = OverlayManager::new();
1761 mgr.show(OverlayRequest {
1762 content_id: fake_id(10),
1763 anchor: fake_id(1),
1764 placement: OverlayPlacement::Below,
1765 dismiss: DismissBehavior::Manual,
1766 layer: OverlayLayer::InTree,
1767 parent_overlay: None,
1768 on_dismiss: None,
1769 fade_duration: None,
1770 });
1771
1772 let (dismissed, _, _) = mgr.handle_click_outside(Point::new(500.0, 500.0));
1773 assert!(dismissed.is_empty());
1774 assert_eq!(mgr.len(), 1);
1775 }
1776
1777 #[test]
1778 fn escape_dismisses_escape_or_click_outside() {
1779 let mut mgr = OverlayManager::new();
1780 let id = mgr.show(OverlayRequest {
1781 content_id: fake_id(10),
1782 anchor: fake_id(1),
1783 placement: OverlayPlacement::Below,
1784 dismiss: DismissBehavior::EscapeOrClickOutside,
1785 layer: OverlayLayer::InTree,
1786 parent_overlay: None,
1787 on_dismiss: None,
1788 fade_duration: None,
1789 });
1790
1791 let dismissed = mgr.try_dismiss_top_on_escape();
1792 assert_eq!(dismissed.map(|(oid, _, _)| oid), Some(id));
1793 assert!(mgr.is_empty());
1794 }
1795
1796 #[test]
1797 fn escape_dismisses_escape_key_only() {
1798 let mut mgr = OverlayManager::new();
1799 let id = mgr.show(OverlayRequest {
1800 content_id: fake_id(10),
1801 anchor: fake_id(1),
1802 placement: OverlayPlacement::Below,
1803 dismiss: DismissBehavior::EscapeKey,
1804 layer: OverlayLayer::InTree,
1805 parent_overlay: None,
1806 on_dismiss: None,
1807 fade_duration: None,
1808 });
1809
1810 let dismissed = mgr.try_dismiss_top_on_escape();
1812 assert_eq!(dismissed.map(|(oid, _, _)| oid), Some(id));
1813 assert!(mgr.is_empty());
1814 }
1815
1816 #[test]
1817 fn escape_does_not_dismiss_click_outside_only() {
1818 let mut mgr = OverlayManager::new();
1819 mgr.show(OverlayRequest {
1820 content_id: fake_id(10),
1821 anchor: fake_id(1),
1822 placement: OverlayPlacement::Below,
1823 dismiss: DismissBehavior::ClickOutside,
1824 layer: OverlayLayer::InTree,
1825 parent_overlay: None,
1826 on_dismiss: None,
1827 fade_duration: None,
1828 });
1829
1830 assert!(mgr.try_dismiss_top_on_escape().is_none());
1831 assert_eq!(mgr.len(), 1);
1832 }
1833
1834 #[test]
1835 fn escape_does_not_dismiss_manual() {
1836 let mut mgr = OverlayManager::new();
1837 mgr.show(OverlayRequest {
1838 content_id: fake_id(10),
1839 anchor: fake_id(1),
1840 placement: OverlayPlacement::Below,
1841 dismiss: DismissBehavior::Manual,
1842 layer: OverlayLayer::InTree,
1843 parent_overlay: None,
1844 on_dismiss: None,
1845 fade_duration: None,
1846 });
1847
1848 assert!(mgr.try_dismiss_top_on_escape().is_none());
1849 assert_eq!(mgr.len(), 1);
1850 }
1851
1852 #[test]
1853 fn click_outside_dismisses_escape_or_click_outside() {
1854 let mut mgr = OverlayManager::new();
1855 mgr.show(OverlayRequest {
1856 content_id: fake_id(10),
1857 anchor: fake_id(1),
1858 placement: OverlayPlacement::Below,
1859 dismiss: DismissBehavior::EscapeOrClickOutside,
1860 layer: OverlayLayer::InTree,
1861 parent_overlay: None,
1862 on_dismiss: None,
1863 fade_duration: None,
1864 });
1865
1866 let id = mgr.active_ids()[0];
1867 mgr.set_content_bounds(id, Size::new(100.0, 50.0));
1868
1869 let (dismissed, _, _) = mgr.handle_click_outside(Point::new(500.0, 500.0));
1870 assert!(!dismissed.is_empty());
1871 assert!(mgr.is_empty());
1872 }
1873
1874 #[test]
1875 fn click_outside_reports_click_opened_anchors_only() {
1876 let mut mgr = OverlayManager::new();
1881 let click_anchor = fake_id(1);
1882 let hover_anchor = fake_id(2);
1883
1884 let click_overlay = mgr.show(OverlayRequest {
1885 content_id: fake_id(10),
1886 anchor: click_anchor,
1887 placement: OverlayPlacement::Below,
1888 dismiss: DismissBehavior::EscapeOrClickOutside,
1889 layer: OverlayLayer::InTree,
1890 parent_overlay: None,
1891 on_dismiss: None,
1892 fade_duration: None,
1893 });
1894 mgr.set_content_bounds(click_overlay, Size::new(100.0, 50.0));
1895
1896 let hover_overlay = mgr.show(OverlayRequest {
1897 content_id: fake_id(11),
1898 anchor: hover_anchor,
1899 placement: OverlayPlacement::Below,
1900 dismiss: DismissBehavior::PointerLeave {
1901 delay: std::time::Duration::from_millis(150),
1902 },
1903 layer: OverlayLayer::InTree,
1904 parent_overlay: None,
1905 on_dismiss: None,
1906 fade_duration: None,
1907 });
1908 mgr.set_content_bounds(hover_overlay, Size::new(100.0, 50.0));
1909
1910 let (dismissed, _focus, toggle_anchors) =
1911 mgr.handle_click_outside(Point::new(500.0, 500.0));
1912
1913 assert_eq!(dismissed.len(), 2);
1915 assert!(mgr.is_empty());
1916 assert_eq!(toggle_anchors, vec![click_anchor]);
1918 }
1919
1920 #[test]
1921 fn click_outside_does_not_dismiss_escape_key_only() {
1922 let mut mgr = OverlayManager::new();
1923 mgr.show(OverlayRequest {
1924 content_id: fake_id(10),
1925 anchor: fake_id(1),
1926 placement: OverlayPlacement::Below,
1927 dismiss: DismissBehavior::EscapeKey,
1928 layer: OverlayLayer::InTree,
1929 parent_overlay: None,
1930 on_dismiss: None,
1931 fade_duration: None,
1932 });
1933
1934 let (dismissed, _, _) = mgr.handle_click_outside(Point::new(500.0, 500.0));
1935 assert!(dismissed.is_empty());
1936 assert_eq!(mgr.len(), 1);
1937 }
1938
1939 #[test]
1940 fn click_outside_is_layered_over_a_modal() {
1941 fn set_bounds(mgr: &mut OverlayManager, id: OverlayId, r: Rect) {
1947 mgr.stack.iter_mut().find(|o| o.id == id).unwrap().bounds = r;
1948 }
1949 fn build() -> (OverlayManager, OverlayId, OverlayId) {
1953 let mut mgr = OverlayManager::new();
1954 let modal = mgr.show(OverlayRequest {
1955 content_id: fake_id(10),
1956 anchor: fake_id(1),
1957 placement: OverlayPlacement::Centered,
1958 dismiss: DismissBehavior::EscapeOrClickOutside,
1959 layer: OverlayLayer::InTree,
1960 parent_overlay: None,
1961 on_dismiss: None,
1962 fade_duration: None,
1963 });
1964 set_bounds(&mut mgr, modal, Rect::new(300.0, 60.0, 600.0, 680.0));
1965 let tooltip = mgr.show(OverlayRequest {
1966 content_id: fake_id(11),
1967 anchor: fake_id(2),
1968 placement: OverlayPlacement::Below,
1969 dismiss: DismissBehavior::EscapeOrClickOutside,
1971 layer: OverlayLayer::InTree,
1972 parent_overlay: None,
1973 on_dismiss: None,
1974 fade_duration: None,
1975 });
1976 set_bounds(&mut mgr, tooltip, Rect::new(400.0, 620.0, 200.0, 140.0));
1977 (mgr, modal, tooltip)
1978 }
1979
1980 let (mut mgr, modal, _tooltip) = build();
1983 let (dismissed, _, _) = mgr.handle_click_outside(Point::new(350.0, 100.0));
1984 assert!(dismissed.contains(&fake_id(11)), "tooltip should dismiss");
1985 assert!(
1986 mgr.active_ids().contains(&modal),
1987 "modal must survive a click inside itself"
1988 );
1989
1990 let (mut mgr, modal, tooltip) = build();
1993 let (dismissed, _, _) = mgr.handle_click_outside(Point::new(450.0, 750.0));
1994 assert!(
1995 dismissed.is_empty(),
1996 "clicking the tooltip dismisses nothing"
1997 );
1998 assert!(mgr.active_ids().contains(&modal));
1999 assert!(mgr.active_ids().contains(&tooltip));
2000
2001 let (mut mgr, _modal, _tooltip) = build();
2004 let (dismissed, _, _) = mgr.handle_click_outside(Point::new(10.0, 10.0));
2005 assert!(dismissed.contains(&fake_id(10)));
2006 assert!(dismissed.contains(&fake_id(11)));
2007 assert!(mgr.is_empty());
2008 }
2009
2010 #[test]
2011 fn active_content_ids() {
2012 let mut mgr = OverlayManager::new();
2013 mgr.show(OverlayRequest {
2014 content_id: fake_id(10),
2015 anchor: fake_id(1),
2016 placement: OverlayPlacement::Below,
2017 dismiss: DismissBehavior::Manual,
2018 layer: OverlayLayer::InTree,
2019 parent_overlay: None,
2020 on_dismiss: None,
2021 fade_duration: None,
2022 });
2023 mgr.show(OverlayRequest {
2024 content_id: fake_id(20),
2025 anchor: fake_id(2),
2026 placement: OverlayPlacement::Below,
2027 dismiss: DismissBehavior::Manual,
2028 layer: OverlayLayer::InTree,
2029 parent_overlay: None,
2030 on_dismiss: None,
2031 fade_duration: None,
2032 });
2033
2034 let ids = mgr.active_content_ids();
2035 assert_eq!(ids.len(), 2);
2036 assert_eq!(ids[0], fake_id(10));
2037 assert_eq!(ids[1], fake_id(20));
2038 }
2039
2040 #[test]
2041 fn hit_test_topmost_first() {
2042 let mut mgr = OverlayManager::new();
2043 let a = mgr.show(OverlayRequest {
2044 content_id: fake_id(10),
2045 anchor: fake_id(1),
2046 placement: OverlayPlacement::Below,
2047 dismiss: DismissBehavior::Manual,
2048 layer: OverlayLayer::InTree,
2049 parent_overlay: None,
2050 on_dismiss: None,
2051 fade_duration: None,
2052 });
2053 let b = mgr.show(OverlayRequest {
2054 content_id: fake_id(11),
2055 anchor: fake_id(2),
2056 placement: OverlayPlacement::Below,
2057 dismiss: DismissBehavior::Manual,
2058 layer: OverlayLayer::InTree,
2059 parent_overlay: None,
2060 on_dismiss: None,
2061 fade_duration: None,
2062 });
2063
2064 mgr.set_content_bounds(a, Size::new(100.0, 50.0));
2066 mgr.set_content_bounds(b, Size::new(100.0, 50.0));
2067
2068 assert_eq!(mgr.hit_test(Point::new(50.0, 25.0)), Some(b));
2070 }
2071
2072 #[test]
2073 fn hit_test_skips_a_fading_out_overlay() {
2074 let mut mgr = OverlayManager::new();
2081 let id = mgr.show(OverlayRequest {
2082 content_id: fake_id(10),
2083 anchor: fake_id(1),
2084 placement: OverlayPlacement::Below,
2085 dismiss: DismissBehavior::ClickOutside,
2086 layer: OverlayLayer::InTree,
2087 parent_overlay: None,
2088 on_dismiss: None,
2089 fade_duration: Some(Duration::from_millis(150)),
2090 });
2091 mgr.set_content_bounds(id, Size::new(100.0, 50.0));
2092 let point = Point::new(50.0, 25.0);
2093
2094 assert_eq!(mgr.hit_test(point), Some(id));
2096 assert!(mgr.active_ids().contains(&id));
2097
2098 mgr.attach_fade(id, Signal::new(1.0), Duration::from_millis(150));
2100
2101 let dismissed = mgr.dismiss(id);
2104 assert!(dismissed.is_empty(), "fade-out defers removal");
2105 assert_eq!(mgr.stack.len(), 1, "overlay lingers during the fade");
2106
2107 assert_eq!(
2109 mgr.hit_test(point),
2110 None,
2111 "fading overlay no longer eats clicks"
2112 );
2113 assert!(!mgr.active_ids().contains(&id));
2114 }
2115
2116 #[test]
2117 fn centered_placement_uses_viewport_center() {
2118 let mut mgr = OverlayManager::new();
2119 let id = mgr.show(OverlayRequest {
2120 content_id: fake_id(10),
2121 anchor: fake_id(1),
2122 placement: OverlayPlacement::Centered,
2123 dismiss: DismissBehavior::Manual,
2124 layer: OverlayLayer::InTree,
2125 parent_overlay: None,
2126 on_dismiss: None,
2127 fade_duration: None,
2128 });
2129
2130 mgr.set_content_bounds(id, Size::new(240.0, 120.0));
2131 mgr.position_overlays(
2132 |_| Some(Rect::new(0.0, 0.0, 10.0, 10.0)),
2133 (800.0, 600.0),
2134 LayoutDirection::LeftToRight,
2135 );
2136
2137 let bounds = mgr
2138 .stack
2139 .iter()
2140 .find(|overlay| overlay.id == id)
2141 .unwrap()
2142 .bounds;
2143 assert!((bounds.x - 280.0).abs() < 0.01);
2144 assert!((bounds.y - 240.0).abs() < 0.01);
2145 }
2146
2147 #[test]
2148 fn bottom_center_placement_uses_viewport_bottom_margin() {
2149 let mut mgr = OverlayManager::new();
2150 let id = mgr.show(OverlayRequest {
2151 content_id: fake_id(10),
2152 anchor: fake_id(1),
2153 placement: OverlayPlacement::BottomCenter,
2154 dismiss: DismissBehavior::Manual,
2155 layer: OverlayLayer::InTree,
2156 parent_overlay: None,
2157 on_dismiss: None,
2158 fade_duration: None,
2159 });
2160
2161 mgr.set_content_bounds(id, Size::new(240.0, 64.0));
2162 mgr.position_overlays(
2163 |_| Some(Rect::new(0.0, 0.0, 10.0, 10.0)),
2164 (800.0, 600.0),
2165 LayoutDirection::LeftToRight,
2166 );
2167
2168 let bounds = mgr
2169 .stack
2170 .iter()
2171 .find(|overlay| overlay.id == id)
2172 .unwrap()
2173 .bounds;
2174 assert!((bounds.x - 280.0).abs() < 0.01);
2175 assert!((bounds.y - 512.0).abs() < 0.01);
2176 }
2177
2178 fn show_corner_overlay(
2181 mgr: &mut OverlayManager,
2182 corner: Corner,
2183 margin: Vec2,
2184 size: Size,
2185 ) -> OverlayId {
2186 let id = mgr.show(OverlayRequest {
2187 content_id: fake_id(10),
2188 anchor: fake_id(1),
2189 placement: OverlayPlacement::ViewportCorner { corner, margin },
2190 dismiss: DismissBehavior::Manual,
2191 layer: OverlayLayer::InTree,
2192 parent_overlay: None,
2193 on_dismiss: None,
2194 fade_duration: None,
2195 });
2196 mgr.set_content_bounds(id, size);
2197 id
2198 }
2199
2200 fn overlay_bounds(mgr: &OverlayManager, id: OverlayId) -> Rect {
2201 mgr.stack.iter().find(|o| o.id == id).unwrap().bounds
2202 }
2203
2204 #[test]
2205 fn viewport_corner_top_leading_ltr() {
2206 let mut mgr = OverlayManager::new();
2207 let id = show_corner_overlay(
2208 &mut mgr,
2209 Corner::TopLeading,
2210 Vec2::new(24.0, 24.0),
2211 Size::new(380.0, 100.0),
2212 );
2213 mgr.position_overlays(
2214 |_| Some(Rect::ZERO),
2215 (800.0, 600.0),
2216 LayoutDirection::LeftToRight,
2217 );
2218 let b = overlay_bounds(&mgr, id);
2219 assert!((b.x - 24.0).abs() < 0.01, "x = {}", b.x);
2220 assert!((b.y - 24.0).abs() < 0.01, "y = {}", b.y);
2221 }
2222
2223 #[test]
2224 fn viewport_corner_top_trailing_ltr() {
2225 let mut mgr = OverlayManager::new();
2226 let id = show_corner_overlay(
2227 &mut mgr,
2228 Corner::TopTrailing,
2229 Vec2::new(24.0, 24.0),
2230 Size::new(380.0, 100.0),
2231 );
2232 mgr.position_overlays(
2233 |_| Some(Rect::ZERO),
2234 (800.0, 600.0),
2235 LayoutDirection::LeftToRight,
2236 );
2237 let b = overlay_bounds(&mgr, id);
2238 assert!((b.x - 396.0).abs() < 0.01, "x = {}", b.x);
2240 assert!((b.y - 24.0).abs() < 0.01);
2241 }
2242
2243 #[test]
2244 fn viewport_corner_bottom_leading_ltr() {
2245 let mut mgr = OverlayManager::new();
2246 let id = show_corner_overlay(
2247 &mut mgr,
2248 Corner::BottomLeading,
2249 Vec2::new(24.0, 24.0),
2250 Size::new(380.0, 100.0),
2251 );
2252 mgr.position_overlays(
2253 |_| Some(Rect::ZERO),
2254 (800.0, 600.0),
2255 LayoutDirection::LeftToRight,
2256 );
2257 let b = overlay_bounds(&mgr, id);
2258 assert!((b.x - 24.0).abs() < 0.01);
2260 assert!((b.y - 476.0).abs() < 0.01, "y = {}", b.y);
2261 }
2262
2263 #[test]
2264 fn viewport_corner_bottom_trailing_ltr() {
2265 let mut mgr = OverlayManager::new();
2266 let id = show_corner_overlay(
2267 &mut mgr,
2268 Corner::BottomTrailing,
2269 Vec2::new(24.0, 24.0),
2270 Size::new(380.0, 100.0),
2271 );
2272 mgr.position_overlays(
2273 |_| Some(Rect::ZERO),
2274 (800.0, 600.0),
2275 LayoutDirection::LeftToRight,
2276 );
2277 let b = overlay_bounds(&mgr, id);
2278 assert!((b.x - 396.0).abs() < 0.01);
2279 assert!((b.y - 476.0).abs() < 0.01);
2280 }
2281
2282 #[test]
2283 fn viewport_corner_top_trailing_rtl_flips_to_left() {
2284 let mut mgr = OverlayManager::new();
2285 let id = show_corner_overlay(
2286 &mut mgr,
2287 Corner::TopTrailing,
2288 Vec2::new(24.0, 24.0),
2289 Size::new(380.0, 100.0),
2290 );
2291 mgr.position_overlays(
2292 |_| Some(Rect::ZERO),
2293 (800.0, 600.0),
2294 LayoutDirection::RightToLeft,
2295 );
2296 let b = overlay_bounds(&mgr, id);
2297 assert!((b.x - 24.0).abs() < 0.01, "x = {}", b.x);
2299 assert!((b.y - 24.0).abs() < 0.01);
2300 }
2301
2302 #[test]
2303 fn viewport_corner_bottom_leading_rtl_flips_to_right() {
2304 let mut mgr = OverlayManager::new();
2305 let id = show_corner_overlay(
2306 &mut mgr,
2307 Corner::BottomLeading,
2308 Vec2::new(24.0, 24.0),
2309 Size::new(380.0, 100.0),
2310 );
2311 mgr.position_overlays(
2312 |_| Some(Rect::ZERO),
2313 (800.0, 600.0),
2314 LayoutDirection::RightToLeft,
2315 );
2316 let b = overlay_bounds(&mgr, id);
2317 assert!((b.x - 396.0).abs() < 0.01, "x = {}", b.x);
2318 assert!((b.y - 476.0).abs() < 0.01);
2319 }
2320
2321 #[test]
2322 fn viewport_corner_ignores_anchor_bounds() {
2323 let mut mgr = OverlayManager::new();
2324 let id = show_corner_overlay(
2325 &mut mgr,
2326 Corner::BottomTrailing,
2327 Vec2::new(0.0, 0.0),
2328 Size::new(100.0, 100.0),
2329 );
2330 mgr.position_overlays(
2332 |_| Some(Rect::new(123.0, 456.0, 7.0, 8.0)),
2333 (800.0, 600.0),
2334 LayoutDirection::LeftToRight,
2335 );
2336 let b = overlay_bounds(&mgr, id);
2337 assert_eq!((b.x, b.y), (700.0, 500.0));
2338 }
2339
2340 #[test]
2341 fn near_anchor_horizontal_is_direction_aware() {
2342 let anchor = Rect::new(600.0, 100.0, 100.0, 20.0);
2348 let resolved_x = |dir: LayoutDirection| {
2349 let mut mgr = OverlayManager::new();
2350 let id = mgr.show(OverlayRequest {
2351 content_id: fake_id(10),
2352 anchor: fake_id(1),
2353 placement: OverlayPlacement::NearAnchor {
2354 offset: Vec2::new(0.0, 8.0),
2355 },
2356 dismiss: DismissBehavior::Manual,
2357 layer: OverlayLayer::InTree,
2358 parent_overlay: None,
2359 on_dismiss: None,
2360 fade_duration: None,
2361 });
2362 mgr.set_content_bounds(id, Size::new(200.0, 50.0));
2363 mgr.position_overlays(|_| Some(anchor), (800.0, 600.0), dir);
2364 overlay_bounds(&mgr, id).x
2365 };
2366 assert!(
2368 (resolved_x(LayoutDirection::LeftToRight) - 600.0).abs() < 0.01,
2369 "LTR x = {}",
2370 resolved_x(LayoutDirection::LeftToRight)
2371 );
2372 assert!(
2374 (resolved_x(LayoutDirection::RightToLeft) - 500.0).abs() < 0.01,
2375 "RTL x = {}",
2376 resolved_x(LayoutDirection::RightToLeft)
2377 );
2378 }
2379
2380 #[test]
2383 fn pause_auto_dismiss_removes_overlay_from_deadline_set() {
2384 let mut mgr = OverlayManager::new();
2385 let id = mgr.show_for(
2386 OverlayRequest {
2387 content_id: fake_id(10),
2388 anchor: fake_id(1),
2389 placement: OverlayPlacement::Centered,
2390 dismiss: DismissBehavior::Manual,
2391 layer: OverlayLayer::InTree,
2392 parent_overlay: None,
2393 on_dismiss: None,
2394 fade_duration: None,
2395 },
2396 Duration::from_secs(10),
2397 );
2398 assert!(mgr.next_auto_dismiss_deadline().is_some());
2399 assert!(!mgr.is_auto_dismiss_paused(id));
2400
2401 mgr.pause_auto_dismiss(id);
2402 assert!(mgr.is_auto_dismiss_paused(id));
2403 assert!(
2404 mgr.next_auto_dismiss_deadline().is_none(),
2405 "paused overlay must drop out of the deadline-min query"
2406 );
2407
2408 mgr.resume_auto_dismiss(id);
2409 assert!(!mgr.is_auto_dismiss_paused(id));
2410 assert!(mgr.next_auto_dismiss_deadline().is_some());
2411 }
2412
2413 #[test]
2414 fn pause_then_resume_restores_remaining_time() {
2415 let mut mgr = OverlayManager::new();
2416 let id = mgr.show_for(
2417 OverlayRequest {
2418 content_id: fake_id(11),
2419 anchor: fake_id(1),
2420 placement: OverlayPlacement::Centered,
2421 dismiss: DismissBehavior::Manual,
2422 layer: OverlayLayer::InTree,
2423 parent_overlay: None,
2424 on_dismiss: None,
2425 fade_duration: None,
2426 },
2427 Duration::from_secs(10),
2428 );
2429
2430 mgr.pause_auto_dismiss(id);
2431 let overlay = mgr.stack.iter().find(|o| o.id == id).unwrap();
2434 let remaining = overlay.paused_remaining.unwrap();
2435 assert!(
2436 remaining >= Duration::from_secs(9),
2437 "remaining should be near the original 10s, got {remaining:?}"
2438 );
2439 assert!(remaining <= Duration::from_secs(10));
2440
2441 mgr.resume_auto_dismiss(id);
2442 let overlay = mgr.stack.iter().find(|o| o.id == id).unwrap();
2443 assert_eq!(overlay.auto_dismiss_after, Some(remaining));
2447 assert!(overlay.paused_remaining.is_none());
2448 }
2449
2450 #[test]
2451 fn pause_is_idempotent() {
2452 let mut mgr = OverlayManager::new();
2453 let id = mgr.show_for(
2454 OverlayRequest {
2455 content_id: fake_id(12),
2456 anchor: fake_id(1),
2457 placement: OverlayPlacement::Centered,
2458 dismiss: DismissBehavior::Manual,
2459 layer: OverlayLayer::InTree,
2460 parent_overlay: None,
2461 on_dismiss: None,
2462 fade_duration: None,
2463 },
2464 Duration::from_secs(10),
2465 );
2466 mgr.pause_auto_dismiss(id);
2467 let first_remaining = mgr.stack[0].paused_remaining;
2468 mgr.pause_auto_dismiss(id); let second_remaining = mgr.stack[0].paused_remaining;
2470 assert_eq!(
2471 first_remaining, second_remaining,
2472 "double-pause must preserve the original stashed remaining"
2473 );
2474 }
2475
2476 #[test]
2477 fn resume_on_unpaused_is_noop() {
2478 let mut mgr = OverlayManager::new();
2479 let id = mgr.show_for(
2480 OverlayRequest {
2481 content_id: fake_id(13),
2482 anchor: fake_id(1),
2483 placement: OverlayPlacement::Centered,
2484 dismiss: DismissBehavior::Manual,
2485 layer: OverlayLayer::InTree,
2486 parent_overlay: None,
2487 on_dismiss: None,
2488 fade_duration: None,
2489 },
2490 Duration::from_secs(10),
2491 );
2492 let before = mgr.stack[0].auto_dismiss_after;
2493 mgr.resume_auto_dismiss(id); let after = mgr.stack[0].auto_dismiss_after;
2495 assert_eq!(before, after);
2496 }
2497
2498 #[test]
2499 fn pause_on_persistent_overlay_is_noop() {
2500 let mut mgr = OverlayManager::new();
2501 let id = mgr.show(OverlayRequest {
2502 content_id: fake_id(14),
2503 anchor: fake_id(1),
2504 placement: OverlayPlacement::Centered,
2505 dismiss: DismissBehavior::Manual,
2506 layer: OverlayLayer::InTree,
2507 parent_overlay: None,
2508 on_dismiss: None,
2509 fade_duration: None,
2510 });
2511 mgr.pause_auto_dismiss(id);
2513 assert!(!mgr.is_auto_dismiss_paused(id));
2514 assert!(mgr.stack[0].paused_remaining.is_none());
2515 }
2516
2517 #[test]
2518 fn pause_on_unknown_id_is_noop() {
2519 let mut mgr = OverlayManager::new();
2520 mgr.pause_auto_dismiss(OverlayId::new(9999)); mgr.resume_auto_dismiss(OverlayId::new(9999));
2522 }
2523
2524 #[test]
2525 fn viewport_corner_zero_margin_snaps_to_edge() {
2526 let mut mgr = OverlayManager::new();
2527 let id = show_corner_overlay(
2528 &mut mgr,
2529 Corner::TopLeading,
2530 Vec2::ZERO,
2531 Size::new(50.0, 50.0),
2532 );
2533 mgr.position_overlays(
2534 |_| Some(Rect::ZERO),
2535 (800.0, 600.0),
2536 LayoutDirection::LeftToRight,
2537 );
2538 let b = overlay_bounds(&mgr, id);
2539 assert_eq!((b.x, b.y), (0.0, 0.0));
2540 }
2541}