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>,
244}
245
246impl ActiveOverlay {
247 pub(crate) fn is_dismissing(&self) -> bool {
256 self.fade
257 .as_ref()
258 .is_some_and(|fade| fade.dismissing_started_real.is_some())
259 }
260}
261
262impl std::fmt::Debug for ActiveOverlay {
266 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
267 f.debug_struct("ActiveOverlay")
268 .field("id", &self.id)
269 .field("content_id", &self.content_id)
270 .field("anchor", &self.anchor)
271 .field("placement", &self.placement)
272 .field("dismiss", &self.dismiss)
273 .field("layer", &self.layer)
274 .field("parent_overlay", &self.parent_overlay)
275 .field("bounds", &self.bounds)
276 .field("focus_restore", &self.focus_restore)
277 .field(
278 "pointer_leave_started_real",
279 &self.pointer_leave_started_real,
280 )
281 .field("pointer_leave_started_sim", &self.pointer_leave_started_sim)
282 .field("auto_dismiss_after", &self.auto_dismiss_after)
283 .field("shown_at_real", &self.shown_at_real)
284 .field("shown_at_sim", &self.shown_at_sim)
285 .field(
286 "on_dismiss",
287 &self.on_dismiss.as_ref().map(|_| "<callback>"),
288 )
289 .field("fading", &self.fade.is_some())
290 .finish()
291 }
292}
293
294pub(crate) const MAX_OVERLAY_NESTING_DEPTH: usize = 12;
301
302fn leading_aligned_x(anchor: Rect, actual_width: f32, vw: f32, rtl: bool) -> f32 {
317 let leading = if rtl {
318 anchor.x + anchor.width - actual_width
319 } else {
320 anchor.x
321 };
322 leading.min(vw - actual_width).max(0.0)
323}
324
325pub struct OverlayManager {
326 pub(crate) stack: Vec<ActiveOverlay>,
327 next_id: u64,
328 sim_clock: Instant,
335 version: Signal<u64>,
341}
342
343impl OverlayManager {
344 pub fn new() -> Self {
345 Self {
346 stack: Vec::new(),
347 next_id: 1,
348 sim_clock: Instant::now(),
349 version: Signal::new(0),
350 }
351 }
352
353 pub fn version(&self) -> &Signal<u64> {
357 &self.version
358 }
359
360 fn bump_version(&self) {
362 self.version.set(self.version.get().wrapping_add(1));
363 }
364
365 pub(crate) fn set_sim_clock(&mut self, now_sim: Instant) {
370 self.sim_clock = now_sim;
371 }
372
373 pub fn show(&mut self, request: OverlayRequest) -> OverlayId {
375 self.show_with_auto_dismiss(request, None)
376 }
377
378 pub fn show_for(&mut self, request: OverlayRequest, duration: Duration) -> OverlayId {
380 self.show_with_auto_dismiss(request, Some(duration))
381 }
382
383 fn show_with_auto_dismiss(
384 &mut self,
385 request: OverlayRequest,
386 auto_dismiss_after: Option<Duration>,
387 ) -> OverlayId {
388 let id = OverlayId::new(self.next_id);
389 self.next_id += 1;
390
391 if self.ancestor_depth(request.parent_overlay) >= MAX_OVERLAY_NESTING_DEPTH {
399 return id;
400 }
401
402 let now = std::time::Instant::now();
403
404 let overlay = ActiveOverlay {
405 id,
406 content_id: request.content_id,
407 anchor: request.anchor,
408 placement: request.placement,
409 dismiss: request.dismiss,
410 layer: request.layer,
411 parent_overlay: request.parent_overlay,
412 bounds: Rect::ZERO,
413 focus_restore: None,
414 pointer_leave_started_real: None,
415 pointer_leave_started_sim: None,
416 auto_dismiss_after,
417 paused_remaining: None,
418 shown_at_real: now,
419 shown_at_sim: now,
420 on_dismiss: request.on_dismiss,
421 fade: None,
422 };
423 self.stack.push(overlay);
424 self.bump_version();
425 id
426 }
427
428 pub(crate) fn attach_fade(&mut self, id: OverlayId, opacity: Signal<f32>, duration: Duration) {
435 if let Some(overlay) = self.stack.iter_mut().find(|o| o.id == id) {
436 overlay.fade = Some(OverlayFadeState {
437 opacity,
438 duration,
439 dismissing_started_real: None,
440 dismissing_started_sim: None,
441 });
442 }
443 }
444
445 pub fn fade_duration(&self, id: OverlayId) -> Option<Duration> {
450 self.stack
451 .iter()
452 .find(|o| o.id == id)
453 .and_then(|o| o.fade.as_ref().map(|f| f.duration))
454 }
455
456 pub fn next_auto_dismiss_deadline(&self) -> Option<std::time::Instant> {
457 self.stack
458 .iter()
459 .filter_map(|overlay| {
460 overlay
461 .auto_dismiss_after
462 .map(|delay| overlay.shown_at_real + delay)
463 })
464 .min()
465 }
466
467 pub fn next_pointer_leave_deadline(&self) -> Option<std::time::Instant> {
478 self.stack
479 .iter()
480 .filter_map(|overlay| {
481 let DismissBehavior::PointerLeave { delay } = overlay.dismiss else {
482 return None;
483 };
484 Some(overlay.pointer_leave_started_real? + delay)
485 })
486 .min()
487 }
488
489 pub fn pause_auto_dismiss(&mut self, id: OverlayId) {
505 if let Some(overlay) = self.stack.iter_mut().find(|o| o.id == id)
506 && overlay.paused_remaining.is_none()
507 && let Some(delay) = overlay.auto_dismiss_after.take()
508 {
509 let elapsed = overlay.shown_at_real.elapsed();
510 overlay.paused_remaining = Some(delay.saturating_sub(elapsed));
511 }
512 }
513
514 pub fn resume_auto_dismiss(&mut self, id: OverlayId) {
521 if let Some(overlay) = self.stack.iter_mut().find(|o| o.id == id)
522 && let Some(remaining) = overlay.paused_remaining.take()
523 {
524 overlay.auto_dismiss_after = Some(remaining);
525 let now = std::time::Instant::now();
526 overlay.shown_at_real = now;
527 overlay.shown_at_sim = self.sim_clock;
528 }
529 }
530
531 pub fn is_auto_dismiss_paused(&self, id: OverlayId) -> bool {
535 self.stack
536 .iter()
537 .find(|o| o.id == id)
538 .is_some_and(|o| o.paused_remaining.is_some())
539 }
540
541 pub(crate) fn set_shown_at_sim(&mut self, id: OverlayId, shown_at_sim: std::time::Instant) {
542 if let Some(overlay) = self.stack.iter_mut().find(|overlay| overlay.id == id) {
543 overlay.shown_at_sim = shown_at_sim;
544 }
545 }
546
547 fn ancestor_depth(&self, parent: Option<OverlayId>) -> usize {
553 let mut depth = 0;
554 let mut current = parent;
555 while let Some(p) = current {
556 depth += 1;
557 if depth > self.stack.len() {
558 break;
561 }
562 current = self
563 .stack
564 .iter()
565 .find(|overlay| overlay.id == p)
566 .and_then(|overlay| overlay.parent_overlay);
567 }
568 depth
569 }
570
571 pub(crate) fn is_descendant_of(&self, child: OverlayId, ancestor: OverlayId) -> bool {
572 let mut current = self
573 .stack
574 .iter()
575 .find(|overlay| overlay.id == child)
576 .and_then(|overlay| overlay.parent_overlay);
577
578 while let Some(parent) = current {
579 if parent == ancestor {
580 return true;
581 }
582 current = self
583 .stack
584 .iter()
585 .find(|overlay| overlay.id == parent)
586 .and_then(|overlay| overlay.parent_overlay);
587 }
588
589 false
590 }
591
592 pub(crate) fn overlay(&self, id: OverlayId) -> Option<&ActiveOverlay> {
593 self.stack.iter().find(|overlay| overlay.id == id)
594 }
595
596 pub fn bounds_for(&self, id: OverlayId) -> Option<Rect> {
607 self.overlay(id).map(|o| o.bounds)
608 }
609
610 pub(crate) fn topmost_centered(&self) -> Option<&ActiveOverlay> {
611 self.stack
612 .iter()
613 .rev()
614 .find(|overlay| matches!(overlay.placement, OverlayPlacement::Centered))
615 }
616
617 pub fn dismiss_with_focus_restore(
620 &mut self,
621 id: OverlayId,
622 ) -> (Vec<WidgetId>, Option<WidgetId>) {
623 let focus_restore = self
624 .stack
625 .iter()
626 .find(|overlay| overlay.id == id)
627 .and_then(|overlay| overlay.focus_restore);
628 let dismissed = self.dismiss(id);
629 (dismissed, focus_restore)
630 }
631
632 pub fn dismiss_descendants_of(
635 &mut self,
636 parent: OverlayId,
637 preserve: Option<OverlayId>,
638 ) -> (Vec<WidgetId>, Option<WidgetId>) {
639 let mut to_dismiss = Vec::new();
640
641 for overlay in &self.stack {
642 if !self.is_descendant_of(overlay.id, parent) {
643 continue;
644 }
645 if preserve
646 .is_some_and(|keep| overlay.id == keep || self.is_descendant_of(overlay.id, keep))
647 {
648 continue;
649 }
650 to_dismiss.push(overlay.id);
651 }
652
653 if to_dismiss.is_empty() {
654 return (Vec::new(), None);
655 }
656
657 let focus_restore = self
658 .stack
659 .iter()
660 .rev()
661 .find(|overlay| to_dismiss.contains(&overlay.id))
662 .and_then(|overlay| overlay.focus_restore);
663
664 let dismissed_content: Vec<WidgetId> = self
665 .stack
666 .iter()
667 .filter(|overlay| to_dismiss.contains(&overlay.id))
668 .map(|overlay| overlay.content_id)
669 .collect();
670 let callbacks: Vec<OverlayDismissCallback> = self
671 .stack
672 .iter()
673 .filter(|overlay| to_dismiss.contains(&overlay.id))
674 .filter_map(|overlay| overlay.on_dismiss.clone())
675 .collect();
676 self.stack
677 .retain(|overlay| !to_dismiss.contains(&overlay.id));
678 for cb in callbacks {
679 cb();
680 }
681
682 (dismissed_content, focus_restore)
683 }
684
685 pub fn update_placement(&mut self, id: OverlayId, placement: OverlayPlacement) {
687 if let Some(overlay) = self.stack.iter_mut().find(|o| o.id == id) {
688 overlay.placement = placement;
689 }
690 }
691
692 pub fn set_parent_overlay(&mut self, id: OverlayId, parent: Option<OverlayId>) {
698 if let Some(overlay) = self.stack.iter_mut().find(|o| o.id == id) {
699 overlay.parent_overlay = parent;
700 }
701 }
702
703 pub fn dismiss(&mut self, id: OverlayId) -> Vec<WidgetId> {
717 let sim_now = self.sim_clock;
723 if let Some(overlay) = self.stack.iter_mut().find(|o| o.id == id)
724 && let Some(fade) = &mut overlay.fade
725 && fade.dismissing_started_real.is_none()
726 {
727 let _ = fade
733 .opacity
734 .try_animate_with_options(crate::animation::AnimationRequest {
735 target: 0.0,
736 duration: fade.duration,
737 easing: teksilo_tokens::Easing::EaseOut,
738 frame_interval: None,
739 looping: false,
740 epsilon: 0.0,
741 max_duration: None,
742 });
743 let now_real = Instant::now();
744 fade.dismissing_started_real = Some(now_real);
745 fade.dismissing_started_sim = Some(sim_now);
746 return Vec::new();
747 }
748 self.dismiss_immediate(id)
749 }
750
751 pub(crate) fn dismiss_immediate(&mut self, id: OverlayId) -> Vec<WidgetId> {
758 let mut to_dismiss = vec![id];
760 let mut i = 0;
761 while i < to_dismiss.len() {
762 let parent = to_dismiss[i];
763 for overlay in &self.stack {
764 if overlay.parent_overlay == Some(parent) && !to_dismiss.contains(&overlay.id) {
765 to_dismiss.push(overlay.id);
766 }
767 }
768 i += 1;
769 }
770 let dismissed_content: Vec<WidgetId> = self
771 .stack
772 .iter()
773 .filter(|o| to_dismiss.contains(&o.id))
774 .map(|o| o.content_id)
775 .collect();
776 let callbacks: Vec<OverlayDismissCallback> = self
781 .stack
782 .iter()
783 .filter(|o| to_dismiss.contains(&o.id))
784 .filter_map(|o| o.on_dismiss.clone())
785 .collect();
786 self.stack.retain(|o| !to_dismiss.contains(&o.id));
787 if !to_dismiss.is_empty() {
788 self.bump_version();
789 }
790 for cb in callbacks {
791 cb();
792 }
793 dismissed_content
794 }
795
796 pub fn process_pending_fade_dismissals(
805 &mut self,
806 now: Instant,
807 ) -> Vec<(OverlayId, Vec<WidgetId>, Option<WidgetId>)> {
808 self.process_pending_fade_dismissals_with(|fade| {
809 let started = fade.dismissing_started_real?;
810 Some(now.saturating_duration_since(started) >= fade.duration)
811 })
812 }
813
814 pub fn process_pending_fade_dismissals_sim(
818 &mut self,
819 now_sim: Instant,
820 ) -> Vec<(OverlayId, Vec<WidgetId>, Option<WidgetId>)> {
821 self.process_pending_fade_dismissals_with(|fade| {
822 let started = fade.dismissing_started_sim?;
823 Some(now_sim.saturating_duration_since(started) >= fade.duration)
824 })
825 }
826
827 fn process_pending_fade_dismissals_with(
828 &mut self,
829 mut elapsed_done: impl FnMut(&OverlayFadeState) -> Option<bool>,
830 ) -> Vec<(OverlayId, Vec<WidgetId>, Option<WidgetId>)> {
831 let ready: Vec<(OverlayId, Option<WidgetId>)> = self
832 .stack
833 .iter()
834 .filter_map(|o| {
835 let fade = o.fade.as_ref()?;
836 if elapsed_done(fade)? {
837 Some((o.id, o.focus_restore))
838 } else {
839 None
840 }
841 })
842 .collect();
843 ready
844 .into_iter()
845 .map(|(id, focus_restore)| {
846 let dismissed = self.dismiss_immediate(id);
847 (id, dismissed, focus_restore)
848 })
849 .collect()
850 }
851
852 pub fn next_fade_dismiss_deadline(&self) -> Option<Instant> {
856 self.stack
857 .iter()
858 .filter_map(|o| {
859 let fade = o.fade.as_ref()?;
860 let started = fade.dismissing_started_real?;
861 Some(started + fade.duration)
862 })
863 .min()
864 }
865
866 pub fn dismiss_top(&mut self) -> Option<(OverlayId, Vec<WidgetId>, Option<WidgetId>)> {
869 if let Some(overlay) = self.stack.last() {
870 let id = overlay.id;
871 let focus_restore = overlay.focus_restore;
872 let content_ids = self.dismiss(id);
873 Some((id, content_ids, focus_restore))
874 } else {
875 None
876 }
877 }
878
879 pub fn try_dismiss_top_on_escape(
897 &mut self,
898 ) -> Option<(OverlayId, Vec<WidgetId>, Option<WidgetId>)> {
899 let target = self
900 .stack
901 .iter()
902 .rev()
903 .filter(|o| !o.is_dismissing())
908 .find_map(|o| match o.dismiss {
909 DismissBehavior::EscapeKey
910 | DismissBehavior::EscapeOrClickOutside
911 | DismissBehavior::PointerLeave { .. } => Some(Some(o.id)),
912 DismissBehavior::Manual => Some(None),
914 DismissBehavior::ClickOutside => None,
915 })??;
916 let focus_restore = self
917 .stack
918 .iter()
919 .find(|o| o.id == target)
920 .and_then(|o| o.focus_restore);
921 let content_ids = self.dismiss(target);
922 Some((target, content_ids, focus_restore))
923 }
924
925 pub fn set_top_focus_restore(&mut self, focus_restore: WidgetId) {
927 if let Some(overlay) = self.stack.last_mut() {
928 overlay.focus_restore = Some(focus_restore);
929 }
930 }
931
932 pub fn dismiss_all(&mut self) -> Vec<WidgetId> {
942 let content_ids: Vec<WidgetId> = self.stack.iter().map(|o| o.content_id).collect();
943 if content_ids.is_empty() {
944 return content_ids;
945 }
946 let callbacks: Vec<OverlayDismissCallback> = self
952 .stack
953 .iter()
954 .filter_map(|o| o.on_dismiss.clone())
955 .collect();
956 self.stack.clear();
957 self.bump_version();
958 for cb in callbacks {
959 cb();
960 }
961 content_ids
962 }
963
964 pub fn dismiss_except(&mut self, keep: &std::collections::HashSet<WidgetId>) -> Vec<WidgetId> {
969 let dismissed: Vec<WidgetId> = self
970 .stack
971 .iter()
972 .filter(|o| !keep.contains(&o.content_id))
973 .map(|o| o.content_id)
974 .collect();
975 if dismissed.is_empty() {
976 return dismissed;
977 }
978 let callbacks: Vec<OverlayDismissCallback> = self
981 .stack
982 .iter()
983 .filter(|o| !keep.contains(&o.content_id))
984 .filter_map(|o| o.on_dismiss.clone())
985 .collect();
986 self.stack.retain(|o| keep.contains(&o.content_id));
987 self.bump_version();
988 for cb in callbacks {
989 cb();
990 }
991 dismissed
992 }
993
994 pub fn is_empty(&self) -> bool {
996 self.stack.is_empty()
997 }
998
999 pub fn len(&self) -> usize {
1001 self.stack.len()
1002 }
1003
1004 pub fn active_content_ids(&self) -> Vec<WidgetId> {
1006 self.stack.iter().map(|o| o.content_id).collect()
1007 }
1008
1009 pub fn active_ids(&self) -> Vec<OverlayId> {
1017 self.stack
1018 .iter()
1019 .filter(|o| {
1020 o.fade
1021 .as_ref()
1022 .is_none_or(|f| f.dismissing_started_real.is_none())
1023 })
1024 .map(|o| o.id)
1025 .collect()
1026 }
1027
1028 pub fn anchor_for(&self, id: OverlayId) -> Option<WidgetId> {
1030 self.stack.iter().find(|o| o.id == id).map(|o| o.anchor)
1031 }
1032
1033 pub fn interactive_rects(&self) -> Vec<Rect> {
1049 self.stack
1050 .iter()
1051 .filter(|o| {
1052 o.fade
1053 .as_ref()
1054 .is_none_or(|f| f.dismissing_started_real.is_none())
1055 })
1056 .map(|o| o.bounds)
1057 .filter(|r| r.width > 0.0 && r.height > 0.0)
1058 .collect()
1059 }
1060
1061 #[allow(dead_code)] pub(crate) fn topmost(&self) -> Option<&ActiveOverlay> {
1064 self.stack.last()
1065 }
1066
1067 pub fn hit_test(&self, point: Point) -> Option<OverlayId> {
1078 for overlay in self.stack.iter().rev() {
1079 let fading_out = overlay
1080 .fade
1081 .as_ref()
1082 .is_some_and(|f| f.dismissing_started_real.is_some());
1083 if !fading_out && overlay.bounds.contains(point) {
1084 return Some(overlay.id);
1085 }
1086 }
1087 None
1088 }
1089
1090 pub fn handle_click_outside(
1112 &mut self,
1113 point: Point,
1114 ) -> (Vec<WidgetId>, Option<WidgetId>, Vec<WidgetId>) {
1115 if self.stack.is_empty() {
1116 return (Vec::new(), None, Vec::new());
1117 }
1118
1119 let hit_index = self.stack.iter().enumerate().rev().find_map(|(i, o)| {
1135 let fading_out = o
1136 .fade
1137 .as_ref()
1138 .is_some_and(|f| f.dismissing_started_real.is_some());
1139 (!fading_out && o.bounds.contains(point)).then_some(i)
1140 });
1141
1142 let mut to_dismiss: Vec<OverlayId> = Vec::new();
1151 let mut toggle_anchors: Vec<WidgetId> = Vec::new();
1152 for (i, o) in self.stack.iter().enumerate() {
1153 if hit_index.is_some_and(|k| i <= k) {
1156 continue;
1157 }
1158 match o.dismiss {
1159 DismissBehavior::ClickOutside | DismissBehavior::EscapeOrClickOutside => {
1160 to_dismiss.push(o.id);
1161 toggle_anchors.push(o.anchor);
1162 }
1163 DismissBehavior::PointerLeave { .. } => to_dismiss.push(o.id),
1164 DismissBehavior::EscapeKey | DismissBehavior::Manual => {}
1165 }
1166 }
1167
1168 if to_dismiss.is_empty() {
1169 return (Vec::new(), None, Vec::new());
1170 }
1171
1172 let focus_restore = self
1173 .stack
1174 .iter()
1175 .find(|o| to_dismiss.contains(&o.id))
1176 .and_then(|o| o.focus_restore);
1177
1178 let mut all_dismissed = Vec::new();
1179 for id in to_dismiss {
1180 all_dismissed.extend(self.dismiss(id));
1181 }
1182 (all_dismissed, focus_restore, toggle_anchors)
1183 }
1184
1185 pub fn position_overlays(
1196 &mut self,
1197 anchor_bounds_fn: impl Fn(WidgetId) -> Option<Rect>,
1198 viewport: (f32, f32),
1199 layout_direction: LayoutDirection,
1200 ) {
1201 let (vw, vh) = viewport;
1202 let rtl = matches!(layout_direction, LayoutDirection::RightToLeft);
1203 for overlay in &mut self.stack {
1204 let anchor = match anchor_bounds_fn(overlay.anchor) {
1205 Some(a) => a,
1206 None => {
1207 if matches!(
1213 overlay.placement,
1214 OverlayPlacement::Centered
1215 | OverlayPlacement::FullViewport
1216 | OverlayPlacement::BottomCenter
1217 | OverlayPlacement::ViewportCorner { .. }
1218 | OverlayPlacement::AtPointer(_)
1219 ) {
1220 Rect::ZERO
1221 } else {
1222 continue;
1223 }
1224 }
1225 };
1226 let content_size = overlay.bounds.size(); overlay.bounds = match &overlay.placement {
1229 OverlayPlacement::Below => {
1230 let actual_width = content_size.width.max(anchor.width);
1231 let x = leading_aligned_x(anchor, actual_width, vw, rtl);
1232 Rect::new(
1233 x,
1234 anchor.y + anchor.height + 4.0,
1235 actual_width,
1236 content_size.height,
1237 )
1238 }
1239 OverlayPlacement::Above => {
1240 let actual_width = content_size.width.max(anchor.width);
1241 let x = leading_aligned_x(anchor, actual_width, vw, rtl);
1242 Rect::new(
1243 x,
1244 anchor.y - content_size.height - 4.0,
1245 actual_width,
1246 content_size.height,
1247 )
1248 }
1249 OverlayPlacement::TrailingEdge => {
1250 let x = if rtl {
1252 let x_left = anchor.x - content_size.width - 2.0;
1253 if x_left >= 0.0 {
1254 x_left
1255 } else {
1256 anchor.x + anchor.width + 2.0
1258 }
1259 } else {
1260 let x_right = anchor.x + anchor.width + 2.0;
1261 if x_right + content_size.width <= vw {
1262 x_right
1263 } else {
1264 anchor.x - content_size.width - 2.0
1266 }
1267 };
1268 let y = anchor.y.min(vh - content_size.height).max(0.0);
1269 Rect::new(x, y, content_size.width, content_size.height)
1270 }
1271 OverlayPlacement::AtPointer(point) => {
1272 let x = point.x.min(vw - content_size.width).max(0.0);
1274 let y = if point.y + content_size.height <= vh {
1275 point.y
1276 } else {
1277 (point.y - content_size.height).max(0.0)
1279 };
1280 Rect::new(x, y, content_size.width, content_size.height)
1281 }
1282 OverlayPlacement::NearAnchor { offset } => {
1283 let below_y = anchor.y + anchor.height + offset.y + 4.0;
1291 let fits_below = below_y + content_size.height <= vh;
1292 let y = if fits_below {
1293 below_y
1294 } else {
1295 let above_y = anchor.y - content_size.height - offset.y - 4.0;
1297 above_y.max(0.0)
1298 };
1299 let unclamped_x = if rtl {
1306 anchor.x + anchor.width - content_size.width - offset.x
1307 } else {
1308 anchor.x + offset.x
1309 };
1310 let x = unclamped_x.min(vw - content_size.width).max(0.0);
1311 Rect::new(x, y, content_size.width, content_size.height)
1312 }
1313 OverlayPlacement::Centered => Rect::new(
1314 ((vw - content_size.width) / 2.0).max(0.0),
1315 ((vh - content_size.height) / 2.0).max(0.0),
1316 content_size.width.min(vw),
1317 content_size.height.min(vh),
1318 ),
1319 OverlayPlacement::BottomCenter => Rect::new(
1320 ((vw - content_size.width) / 2.0).max(0.0),
1321 (vh - content_size.height - 24.0).max(0.0),
1322 content_size.width.min(vw),
1323 content_size.height.min(vh),
1324 ),
1325 OverlayPlacement::BelowPreferred => {
1326 let below_y = anchor.y + anchor.height + 4.0;
1327 let fits_below = below_y + content_size.height <= vh;
1328 let y = if fits_below {
1329 below_y
1330 } else {
1331 anchor.y - content_size.height - 4.0
1332 };
1333 let actual_width = content_size.width.max(anchor.width);
1334 let x = if rtl {
1336 (anchor.x + anchor.width - actual_width)
1337 .min(vw - actual_width)
1338 .max(0.0)
1339 } else {
1340 anchor.x.min(vw - actual_width).max(0.0)
1341 };
1342 Rect::new(x, y, actual_width, content_size.height)
1343 }
1344 OverlayPlacement::ViewportCorner { corner, margin } => {
1345 let (x, y) = corner.resolve(
1346 (content_size.width, content_size.height),
1347 (vw, vh),
1348 (margin.x, margin.y),
1349 rtl,
1350 );
1351 Rect::new(
1352 x,
1353 y,
1354 content_size.width.min(vw),
1355 content_size.height.min(vh),
1356 )
1357 }
1358 OverlayPlacement::FullViewport => Rect::new(0.0, 0.0, vw, vh),
1359 };
1360 }
1361 }
1362
1363 pub fn set_content_bounds(&mut self, id: OverlayId, size: Size) {
1365 if let Some(overlay) = self.stack.iter_mut().find(|o| o.id == id) {
1366 overlay.bounds = Rect::new(overlay.bounds.x, overlay.bounds.y, size.width, size.height);
1367 }
1368 }
1369
1370 pub fn find_by_content(&self, content_id: WidgetId) -> Option<OverlayId> {
1372 self.stack
1373 .iter()
1374 .find(|o| o.content_id == content_id)
1375 .map(|o| o.id)
1376 }
1377
1378 pub fn bounds_for_content(&self, content_id: WidgetId) -> Option<Rect> {
1383 self.stack
1384 .iter()
1385 .find(|o| o.content_id == content_id)
1386 .map(|o| o.bounds)
1387 }
1388
1389 pub fn set_dismiss(&mut self, id: OverlayId, behavior: DismissBehavior) {
1397 if let Some(overlay) = self.stack.iter_mut().find(|o| o.id == id) {
1398 overlay.dismiss = behavior;
1399 overlay.pointer_leave_started_real = None;
1400 overlay.pointer_leave_started_sim = None;
1401 }
1402 }
1403}
1404
1405impl Default for OverlayManager {
1406 fn default() -> Self {
1407 Self::new()
1408 }
1409}
1410
1411impl std::fmt::Debug for OverlayManager {
1412 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1413 f.debug_struct("OverlayManager")
1414 .field("active_count", &self.stack.len())
1415 .finish()
1416 }
1417}
1418
1419#[cfg(test)]
1420mod tests {
1421 use super::*;
1422 use slotmap::KeyData;
1423
1424 fn fake_id(n: u64) -> WidgetId {
1425 KeyData::from_ffi(n).into()
1426 }
1427
1428 #[test]
1437 fn a_wide_overlay_near_the_trailing_edge_is_clamped_into_the_viewport() {
1438 let vw = 1200.0;
1439 let anchor = Rect::new(1035.0, 760.0, 90.0, 28.0);
1442 let x = leading_aligned_x(anchor, 380.0, vw, false);
1443 assert!(
1444 x + 380.0 <= vw + 0.01,
1445 "overlay must not extend past the viewport: x={x}"
1446 );
1447 assert!(x >= 0.0, "and must not start off the leading edge: x={x}");
1448
1449 let inside = Rect::new(100.0, 760.0, 90.0, 28.0);
1452 assert_eq!(leading_aligned_x(inside, 380.0, vw, false), 100.0);
1453
1454 let x_rtl = leading_aligned_x(inside, 380.0, vw, true);
1456 assert!(x_rtl >= 0.0 && x_rtl + 380.0 <= vw + 0.01);
1457 }
1458
1459 #[test]
1463 fn an_overlay_wider_than_the_viewport_keeps_its_leading_edge_visible() {
1464 let x = leading_aligned_x(Rect::new(40.0, 10.0, 60.0, 20.0), 900.0, 500.0, false);
1465 assert_eq!(x, 0.0);
1466 }
1467
1468 #[test]
1469 fn dismiss_all_fires_on_dismiss_callbacks() {
1470 use std::cell::Cell;
1478 use std::rc::Rc;
1479 let mut mgr = OverlayManager::new();
1480 let fired_a = Rc::new(Cell::new(0_u32));
1481 let fired_b = Rc::new(Cell::new(0_u32));
1482 let cb_a: OverlayDismissCallback = {
1483 let f = fired_a.clone();
1484 Rc::new(move || f.set(f.get() + 1))
1485 };
1486 let cb_b: OverlayDismissCallback = {
1487 let f = fired_b.clone();
1488 Rc::new(move || f.set(f.get() + 1))
1489 };
1490 mgr.show(OverlayRequest {
1491 content_id: fake_id(10),
1492 anchor: fake_id(1),
1493 placement: OverlayPlacement::Below,
1494 dismiss: DismissBehavior::ClickOutside,
1495 layer: OverlayLayer::InTree,
1496 parent_overlay: None,
1497 on_dismiss: Some(cb_a),
1498 fade_duration: None,
1499 });
1500 mgr.show(OverlayRequest {
1501 content_id: fake_id(11),
1502 anchor: fake_id(2),
1503 placement: OverlayPlacement::Below,
1504 dismiss: DismissBehavior::ClickOutside,
1505 layer: OverlayLayer::InTree,
1506 parent_overlay: None,
1507 on_dismiss: Some(cb_b),
1508 fade_duration: None,
1509 });
1510 let dismissed = mgr.dismiss_all();
1511 assert_eq!(dismissed.len(), 2);
1512 assert!(mgr.is_empty());
1513 assert_eq!(
1514 fired_a.get(),
1515 1,
1516 "first overlay's on_dismiss must fire exactly once",
1517 );
1518 assert_eq!(
1519 fired_b.get(),
1520 1,
1521 "second overlay's on_dismiss must fire exactly once",
1522 );
1523 }
1524
1525 #[test]
1526 fn show_and_dismiss() {
1527 let mut mgr = OverlayManager::new();
1528 let id = mgr.show(OverlayRequest {
1529 content_id: fake_id(10),
1530 anchor: fake_id(1),
1531 placement: OverlayPlacement::Below,
1532 dismiss: DismissBehavior::ClickOutside,
1533 layer: OverlayLayer::InTree,
1534 parent_overlay: None,
1535 on_dismiss: None,
1536 fade_duration: None,
1537 });
1538 assert_eq!(mgr.len(), 1);
1539
1540 mgr.dismiss(id);
1541 assert!(mgr.is_empty());
1542 }
1543
1544 #[test]
1545 fn cascade_dismissal() {
1546 let mut mgr = OverlayManager::new();
1547 let parent = mgr.show(OverlayRequest {
1548 content_id: fake_id(10),
1549 anchor: fake_id(1),
1550 placement: OverlayPlacement::Below,
1551 dismiss: DismissBehavior::ClickOutside,
1552 layer: OverlayLayer::InTree,
1553 parent_overlay: None,
1554 on_dismiss: None,
1555 fade_duration: None,
1556 });
1557 let _child = mgr.show(OverlayRequest {
1558 content_id: fake_id(11),
1559 anchor: fake_id(10),
1560 placement: OverlayPlacement::TrailingEdge,
1561 dismiss: DismissBehavior::ClickOutside,
1562 layer: OverlayLayer::InTree,
1563 parent_overlay: Some(parent),
1564 on_dismiss: None,
1565 fade_duration: None,
1566 });
1567 assert_eq!(mgr.len(), 2);
1568
1569 mgr.dismiss(parent);
1571 assert!(mgr.is_empty());
1572 }
1573
1574 #[test]
1575 fn cascade_depth_is_bounded() {
1576 let mut mgr = OverlayManager::new();
1582 let mut parent = mgr.show(OverlayRequest {
1583 content_id: fake_id(100),
1584 anchor: fake_id(1),
1585 placement: OverlayPlacement::Below,
1586 dismiss: DismissBehavior::Manual,
1587 layer: OverlayLayer::InTree,
1588 parent_overlay: None,
1589 on_dismiss: None,
1590 fade_duration: None,
1591 });
1592 for i in 1..MAX_OVERLAY_NESTING_DEPTH {
1595 parent = mgr.show(OverlayRequest {
1596 content_id: fake_id(100 + i as u64),
1597 anchor: fake_id(1),
1598 placement: OverlayPlacement::Below,
1599 dismiss: DismissBehavior::Manual,
1600 layer: OverlayLayer::InTree,
1601 parent_overlay: Some(parent),
1602 on_dismiss: None,
1603 fade_duration: None,
1604 });
1605 }
1606 assert_eq!(
1607 mgr.len(),
1608 MAX_OVERLAY_NESTING_DEPTH,
1609 "chain should fill exactly to the cap"
1610 );
1611
1612 let dropped = mgr.show(OverlayRequest {
1614 content_id: fake_id(999),
1615 anchor: fake_id(1),
1616 placement: OverlayPlacement::Below,
1617 dismiss: DismissBehavior::Manual,
1618 layer: OverlayLayer::InTree,
1619 parent_overlay: Some(parent),
1620 on_dismiss: None,
1621 fade_duration: None,
1622 });
1623 assert_eq!(
1624 mgr.len(),
1625 MAX_OVERLAY_NESTING_DEPTH,
1626 "over-cap overlay must not be pushed"
1627 );
1628 assert!(
1629 mgr.stack.iter().all(|o| o.id != dropped),
1630 "the dropped overlay id must not appear in the stack"
1631 );
1632 }
1633
1634 #[test]
1635 fn dismiss_top() {
1636 let mut mgr = OverlayManager::new();
1637 let _a = mgr.show(OverlayRequest {
1638 content_id: fake_id(10),
1639 anchor: fake_id(1),
1640 placement: OverlayPlacement::Below,
1641 dismiss: DismissBehavior::Manual,
1642 layer: OverlayLayer::InTree,
1643 parent_overlay: None,
1644 on_dismiss: None,
1645 fade_duration: None,
1646 });
1647 let b = mgr.show(OverlayRequest {
1648 content_id: fake_id(11),
1649 anchor: fake_id(2),
1650 placement: OverlayPlacement::Below,
1651 dismiss: DismissBehavior::Manual,
1652 layer: OverlayLayer::InTree,
1653 parent_overlay: None,
1654 on_dismiss: None,
1655 fade_duration: None,
1656 });
1657
1658 let dismissed = mgr.dismiss_top();
1659 assert_eq!(dismissed.map(|(id, _, _)| id), Some(b));
1660 assert_eq!(mgr.len(), 1);
1661 }
1662
1663 #[test]
1664 fn click_outside_dismisses() {
1665 let mut mgr = OverlayManager::new();
1666 mgr.show(OverlayRequest {
1667 content_id: fake_id(10),
1668 anchor: fake_id(1),
1669 placement: OverlayPlacement::Below,
1670 dismiss: DismissBehavior::ClickOutside,
1671 layer: OverlayLayer::InTree,
1672 parent_overlay: None,
1673 on_dismiss: None,
1674 fade_duration: None,
1675 });
1676
1677 let id = mgr.active_ids()[0];
1679 mgr.set_content_bounds(id, Size::new(100.0, 50.0));
1680
1681 let (dismissed, _, _) = mgr.handle_click_outside(Point::new(50.0, 25.0));
1683 assert!(dismissed.is_empty());
1684 assert_eq!(mgr.len(), 1);
1685
1686 let (dismissed, _, _) = mgr.handle_click_outside(Point::new(500.0, 500.0));
1688 assert!(!dismissed.is_empty());
1689 assert!(mgr.is_empty());
1690 }
1691
1692 #[test]
1693 fn click_outside_returns_focus_restore() {
1694 let mut mgr = OverlayManager::new();
1695 let trigger = fake_id(99);
1696 mgr.show(OverlayRequest {
1697 content_id: fake_id(10),
1698 anchor: fake_id(1),
1699 placement: OverlayPlacement::Below,
1700 dismiss: DismissBehavior::ClickOutside,
1701 layer: OverlayLayer::InTree,
1702 parent_overlay: None,
1703 on_dismiss: None,
1704 fade_duration: None,
1705 });
1706 let id = mgr.active_ids()[0];
1707 mgr.set_content_bounds(id, Size::new(100.0, 50.0));
1708 mgr.set_top_focus_restore(trigger);
1709
1710 let (dismissed, focus_restore, _) = mgr.handle_click_outside(Point::new(500.0, 500.0));
1711 assert_eq!(dismissed.len(), 1);
1712 assert_eq!(focus_restore, Some(trigger));
1713 }
1714
1715 #[test]
1716 fn click_outside_focus_restore_picks_bottommost() {
1717 let mut mgr = OverlayManager::new();
1723 let pre_overlay_focus = fake_id(99);
1724 let inside_a = fake_id(50);
1725 let a = mgr.show(OverlayRequest {
1726 content_id: fake_id(10),
1727 anchor: fake_id(1),
1728 placement: OverlayPlacement::Below,
1729 dismiss: DismissBehavior::ClickOutside,
1730 layer: OverlayLayer::InTree,
1731 parent_overlay: None,
1732 on_dismiss: None,
1733 fade_duration: None,
1734 });
1735 mgr.set_content_bounds(a, Size::new(100.0, 50.0));
1736 mgr.set_top_focus_restore(pre_overlay_focus);
1737 let b = mgr.show(OverlayRequest {
1738 content_id: fake_id(11),
1739 anchor: fake_id(2),
1740 placement: OverlayPlacement::Below,
1741 dismiss: DismissBehavior::ClickOutside,
1742 layer: OverlayLayer::InTree,
1743 parent_overlay: None,
1744 on_dismiss: None,
1745 fade_duration: None,
1746 });
1747 mgr.set_content_bounds(b, Size::new(100.0, 50.0));
1748 mgr.set_top_focus_restore(inside_a);
1749
1750 let (_, focus_restore, _) = mgr.handle_click_outside(Point::new(500.0, 500.0));
1751 assert_eq!(focus_restore, Some(pre_overlay_focus));
1752 }
1753
1754 #[test]
1755 fn manual_dismiss_ignores_click_outside() {
1756 let mut mgr = OverlayManager::new();
1757 mgr.show(OverlayRequest {
1758 content_id: fake_id(10),
1759 anchor: fake_id(1),
1760 placement: OverlayPlacement::Below,
1761 dismiss: DismissBehavior::Manual,
1762 layer: OverlayLayer::InTree,
1763 parent_overlay: None,
1764 on_dismiss: None,
1765 fade_duration: None,
1766 });
1767
1768 let (dismissed, _, _) = mgr.handle_click_outside(Point::new(500.0, 500.0));
1769 assert!(dismissed.is_empty());
1770 assert_eq!(mgr.len(), 1);
1771 }
1772
1773 #[test]
1774 fn escape_dismisses_escape_or_click_outside() {
1775 let mut mgr = OverlayManager::new();
1776 let id = mgr.show(OverlayRequest {
1777 content_id: fake_id(10),
1778 anchor: fake_id(1),
1779 placement: OverlayPlacement::Below,
1780 dismiss: DismissBehavior::EscapeOrClickOutside,
1781 layer: OverlayLayer::InTree,
1782 parent_overlay: None,
1783 on_dismiss: None,
1784 fade_duration: None,
1785 });
1786
1787 let dismissed = mgr.try_dismiss_top_on_escape();
1788 assert_eq!(dismissed.map(|(oid, _, _)| oid), Some(id));
1789 assert!(mgr.is_empty());
1790 }
1791
1792 #[test]
1793 fn escape_dismisses_escape_key_only() {
1794 let mut mgr = OverlayManager::new();
1795 let id = mgr.show(OverlayRequest {
1796 content_id: fake_id(10),
1797 anchor: fake_id(1),
1798 placement: OverlayPlacement::Below,
1799 dismiss: DismissBehavior::EscapeKey,
1800 layer: OverlayLayer::InTree,
1801 parent_overlay: None,
1802 on_dismiss: None,
1803 fade_duration: None,
1804 });
1805
1806 let dismissed = mgr.try_dismiss_top_on_escape();
1808 assert_eq!(dismissed.map(|(oid, _, _)| oid), Some(id));
1809 assert!(mgr.is_empty());
1810 }
1811
1812 #[test]
1813 fn escape_does_not_dismiss_click_outside_only() {
1814 let mut mgr = OverlayManager::new();
1815 mgr.show(OverlayRequest {
1816 content_id: fake_id(10),
1817 anchor: fake_id(1),
1818 placement: OverlayPlacement::Below,
1819 dismiss: DismissBehavior::ClickOutside,
1820 layer: OverlayLayer::InTree,
1821 parent_overlay: None,
1822 on_dismiss: None,
1823 fade_duration: None,
1824 });
1825
1826 assert!(mgr.try_dismiss_top_on_escape().is_none());
1827 assert_eq!(mgr.len(), 1);
1828 }
1829
1830 #[test]
1831 fn escape_does_not_dismiss_manual() {
1832 let mut mgr = OverlayManager::new();
1833 mgr.show(OverlayRequest {
1834 content_id: fake_id(10),
1835 anchor: fake_id(1),
1836 placement: OverlayPlacement::Below,
1837 dismiss: DismissBehavior::Manual,
1838 layer: OverlayLayer::InTree,
1839 parent_overlay: None,
1840 on_dismiss: None,
1841 fade_duration: None,
1842 });
1843
1844 assert!(mgr.try_dismiss_top_on_escape().is_none());
1845 assert_eq!(mgr.len(), 1);
1846 }
1847
1848 #[test]
1849 fn click_outside_dismisses_escape_or_click_outside() {
1850 let mut mgr = OverlayManager::new();
1851 mgr.show(OverlayRequest {
1852 content_id: fake_id(10),
1853 anchor: fake_id(1),
1854 placement: OverlayPlacement::Below,
1855 dismiss: DismissBehavior::EscapeOrClickOutside,
1856 layer: OverlayLayer::InTree,
1857 parent_overlay: None,
1858 on_dismiss: None,
1859 fade_duration: None,
1860 });
1861
1862 let id = mgr.active_ids()[0];
1863 mgr.set_content_bounds(id, Size::new(100.0, 50.0));
1864
1865 let (dismissed, _, _) = mgr.handle_click_outside(Point::new(500.0, 500.0));
1866 assert!(!dismissed.is_empty());
1867 assert!(mgr.is_empty());
1868 }
1869
1870 #[test]
1871 fn click_outside_reports_click_opened_anchors_only() {
1872 let mut mgr = OverlayManager::new();
1877 let click_anchor = fake_id(1);
1878 let hover_anchor = fake_id(2);
1879
1880 let click_overlay = mgr.show(OverlayRequest {
1881 content_id: fake_id(10),
1882 anchor: click_anchor,
1883 placement: OverlayPlacement::Below,
1884 dismiss: DismissBehavior::EscapeOrClickOutside,
1885 layer: OverlayLayer::InTree,
1886 parent_overlay: None,
1887 on_dismiss: None,
1888 fade_duration: None,
1889 });
1890 mgr.set_content_bounds(click_overlay, Size::new(100.0, 50.0));
1891
1892 let hover_overlay = mgr.show(OverlayRequest {
1893 content_id: fake_id(11),
1894 anchor: hover_anchor,
1895 placement: OverlayPlacement::Below,
1896 dismiss: DismissBehavior::PointerLeave {
1897 delay: std::time::Duration::from_millis(150),
1898 },
1899 layer: OverlayLayer::InTree,
1900 parent_overlay: None,
1901 on_dismiss: None,
1902 fade_duration: None,
1903 });
1904 mgr.set_content_bounds(hover_overlay, Size::new(100.0, 50.0));
1905
1906 let (dismissed, _focus, toggle_anchors) =
1907 mgr.handle_click_outside(Point::new(500.0, 500.0));
1908
1909 assert_eq!(dismissed.len(), 2);
1911 assert!(mgr.is_empty());
1912 assert_eq!(toggle_anchors, vec![click_anchor]);
1914 }
1915
1916 #[test]
1917 fn click_outside_does_not_dismiss_escape_key_only() {
1918 let mut mgr = OverlayManager::new();
1919 mgr.show(OverlayRequest {
1920 content_id: fake_id(10),
1921 anchor: fake_id(1),
1922 placement: OverlayPlacement::Below,
1923 dismiss: DismissBehavior::EscapeKey,
1924 layer: OverlayLayer::InTree,
1925 parent_overlay: None,
1926 on_dismiss: None,
1927 fade_duration: None,
1928 });
1929
1930 let (dismissed, _, _) = mgr.handle_click_outside(Point::new(500.0, 500.0));
1931 assert!(dismissed.is_empty());
1932 assert_eq!(mgr.len(), 1);
1933 }
1934
1935 #[test]
1936 fn click_outside_is_layered_over_a_modal() {
1937 fn set_bounds(mgr: &mut OverlayManager, id: OverlayId, r: Rect) {
1943 mgr.stack.iter_mut().find(|o| o.id == id).unwrap().bounds = r;
1944 }
1945 fn build() -> (OverlayManager, OverlayId, OverlayId) {
1949 let mut mgr = OverlayManager::new();
1950 let modal = mgr.show(OverlayRequest {
1951 content_id: fake_id(10),
1952 anchor: fake_id(1),
1953 placement: OverlayPlacement::Centered,
1954 dismiss: DismissBehavior::EscapeOrClickOutside,
1955 layer: OverlayLayer::InTree,
1956 parent_overlay: None,
1957 on_dismiss: None,
1958 fade_duration: None,
1959 });
1960 set_bounds(&mut mgr, modal, Rect::new(300.0, 60.0, 600.0, 680.0));
1961 let tooltip = mgr.show(OverlayRequest {
1962 content_id: fake_id(11),
1963 anchor: fake_id(2),
1964 placement: OverlayPlacement::Below,
1965 dismiss: DismissBehavior::EscapeOrClickOutside,
1967 layer: OverlayLayer::InTree,
1968 parent_overlay: None,
1969 on_dismiss: None,
1970 fade_duration: None,
1971 });
1972 set_bounds(&mut mgr, tooltip, Rect::new(400.0, 620.0, 200.0, 140.0));
1973 (mgr, modal, tooltip)
1974 }
1975
1976 let (mut mgr, modal, _tooltip) = build();
1979 let (dismissed, _, _) = mgr.handle_click_outside(Point::new(350.0, 100.0));
1980 assert!(dismissed.contains(&fake_id(11)), "tooltip should dismiss");
1981 assert!(
1982 mgr.active_ids().contains(&modal),
1983 "modal must survive a click inside itself"
1984 );
1985
1986 let (mut mgr, modal, tooltip) = build();
1989 let (dismissed, _, _) = mgr.handle_click_outside(Point::new(450.0, 750.0));
1990 assert!(
1991 dismissed.is_empty(),
1992 "clicking the tooltip dismisses nothing"
1993 );
1994 assert!(mgr.active_ids().contains(&modal));
1995 assert!(mgr.active_ids().contains(&tooltip));
1996
1997 let (mut mgr, _modal, _tooltip) = build();
2000 let (dismissed, _, _) = mgr.handle_click_outside(Point::new(10.0, 10.0));
2001 assert!(dismissed.contains(&fake_id(10)));
2002 assert!(dismissed.contains(&fake_id(11)));
2003 assert!(mgr.is_empty());
2004 }
2005
2006 #[test]
2007 fn active_content_ids() {
2008 let mut mgr = OverlayManager::new();
2009 mgr.show(OverlayRequest {
2010 content_id: fake_id(10),
2011 anchor: fake_id(1),
2012 placement: OverlayPlacement::Below,
2013 dismiss: DismissBehavior::Manual,
2014 layer: OverlayLayer::InTree,
2015 parent_overlay: None,
2016 on_dismiss: None,
2017 fade_duration: None,
2018 });
2019 mgr.show(OverlayRequest {
2020 content_id: fake_id(20),
2021 anchor: fake_id(2),
2022 placement: OverlayPlacement::Below,
2023 dismiss: DismissBehavior::Manual,
2024 layer: OverlayLayer::InTree,
2025 parent_overlay: None,
2026 on_dismiss: None,
2027 fade_duration: None,
2028 });
2029
2030 let ids = mgr.active_content_ids();
2031 assert_eq!(ids.len(), 2);
2032 assert_eq!(ids[0], fake_id(10));
2033 assert_eq!(ids[1], fake_id(20));
2034 }
2035
2036 #[test]
2037 fn hit_test_topmost_first() {
2038 let mut mgr = OverlayManager::new();
2039 let a = mgr.show(OverlayRequest {
2040 content_id: fake_id(10),
2041 anchor: fake_id(1),
2042 placement: OverlayPlacement::Below,
2043 dismiss: DismissBehavior::Manual,
2044 layer: OverlayLayer::InTree,
2045 parent_overlay: None,
2046 on_dismiss: None,
2047 fade_duration: None,
2048 });
2049 let b = mgr.show(OverlayRequest {
2050 content_id: fake_id(11),
2051 anchor: fake_id(2),
2052 placement: OverlayPlacement::Below,
2053 dismiss: DismissBehavior::Manual,
2054 layer: OverlayLayer::InTree,
2055 parent_overlay: None,
2056 on_dismiss: None,
2057 fade_duration: None,
2058 });
2059
2060 mgr.set_content_bounds(a, Size::new(100.0, 50.0));
2062 mgr.set_content_bounds(b, Size::new(100.0, 50.0));
2063
2064 assert_eq!(mgr.hit_test(Point::new(50.0, 25.0)), Some(b));
2066 }
2067
2068 #[test]
2069 fn hit_test_skips_a_fading_out_overlay() {
2070 let mut mgr = OverlayManager::new();
2077 let id = mgr.show(OverlayRequest {
2078 content_id: fake_id(10),
2079 anchor: fake_id(1),
2080 placement: OverlayPlacement::Below,
2081 dismiss: DismissBehavior::ClickOutside,
2082 layer: OverlayLayer::InTree,
2083 parent_overlay: None,
2084 on_dismiss: None,
2085 fade_duration: Some(Duration::from_millis(150)),
2086 });
2087 mgr.set_content_bounds(id, Size::new(100.0, 50.0));
2088 let point = Point::new(50.0, 25.0);
2089
2090 assert_eq!(mgr.hit_test(point), Some(id));
2092 assert!(mgr.active_ids().contains(&id));
2093
2094 mgr.attach_fade(id, Signal::new(1.0), Duration::from_millis(150));
2096
2097 let dismissed = mgr.dismiss(id);
2100 assert!(dismissed.is_empty(), "fade-out defers removal");
2101 assert_eq!(mgr.stack.len(), 1, "overlay lingers during the fade");
2102
2103 assert_eq!(
2105 mgr.hit_test(point),
2106 None,
2107 "fading overlay no longer eats clicks"
2108 );
2109 assert!(!mgr.active_ids().contains(&id));
2110 }
2111
2112 #[test]
2113 fn centered_placement_uses_viewport_center() {
2114 let mut mgr = OverlayManager::new();
2115 let id = mgr.show(OverlayRequest {
2116 content_id: fake_id(10),
2117 anchor: fake_id(1),
2118 placement: OverlayPlacement::Centered,
2119 dismiss: DismissBehavior::Manual,
2120 layer: OverlayLayer::InTree,
2121 parent_overlay: None,
2122 on_dismiss: None,
2123 fade_duration: None,
2124 });
2125
2126 mgr.set_content_bounds(id, Size::new(240.0, 120.0));
2127 mgr.position_overlays(
2128 |_| Some(Rect::new(0.0, 0.0, 10.0, 10.0)),
2129 (800.0, 600.0),
2130 LayoutDirection::LeftToRight,
2131 );
2132
2133 let bounds = mgr
2134 .stack
2135 .iter()
2136 .find(|overlay| overlay.id == id)
2137 .unwrap()
2138 .bounds;
2139 assert!((bounds.x - 280.0).abs() < 0.01);
2140 assert!((bounds.y - 240.0).abs() < 0.01);
2141 }
2142
2143 #[test]
2144 fn bottom_center_placement_uses_viewport_bottom_margin() {
2145 let mut mgr = OverlayManager::new();
2146 let id = mgr.show(OverlayRequest {
2147 content_id: fake_id(10),
2148 anchor: fake_id(1),
2149 placement: OverlayPlacement::BottomCenter,
2150 dismiss: DismissBehavior::Manual,
2151 layer: OverlayLayer::InTree,
2152 parent_overlay: None,
2153 on_dismiss: None,
2154 fade_duration: None,
2155 });
2156
2157 mgr.set_content_bounds(id, Size::new(240.0, 64.0));
2158 mgr.position_overlays(
2159 |_| Some(Rect::new(0.0, 0.0, 10.0, 10.0)),
2160 (800.0, 600.0),
2161 LayoutDirection::LeftToRight,
2162 );
2163
2164 let bounds = mgr
2165 .stack
2166 .iter()
2167 .find(|overlay| overlay.id == id)
2168 .unwrap()
2169 .bounds;
2170 assert!((bounds.x - 280.0).abs() < 0.01);
2171 assert!((bounds.y - 512.0).abs() < 0.01);
2172 }
2173
2174 fn show_corner_overlay(
2177 mgr: &mut OverlayManager,
2178 corner: Corner,
2179 margin: Vec2,
2180 size: Size,
2181 ) -> OverlayId {
2182 let id = mgr.show(OverlayRequest {
2183 content_id: fake_id(10),
2184 anchor: fake_id(1),
2185 placement: OverlayPlacement::ViewportCorner { corner, margin },
2186 dismiss: DismissBehavior::Manual,
2187 layer: OverlayLayer::InTree,
2188 parent_overlay: None,
2189 on_dismiss: None,
2190 fade_duration: None,
2191 });
2192 mgr.set_content_bounds(id, size);
2193 id
2194 }
2195
2196 fn overlay_bounds(mgr: &OverlayManager, id: OverlayId) -> Rect {
2197 mgr.stack.iter().find(|o| o.id == id).unwrap().bounds
2198 }
2199
2200 #[test]
2201 fn viewport_corner_top_leading_ltr() {
2202 let mut mgr = OverlayManager::new();
2203 let id = show_corner_overlay(
2204 &mut mgr,
2205 Corner::TopLeading,
2206 Vec2::new(24.0, 24.0),
2207 Size::new(380.0, 100.0),
2208 );
2209 mgr.position_overlays(
2210 |_| Some(Rect::ZERO),
2211 (800.0, 600.0),
2212 LayoutDirection::LeftToRight,
2213 );
2214 let b = overlay_bounds(&mgr, id);
2215 assert!((b.x - 24.0).abs() < 0.01, "x = {}", b.x);
2216 assert!((b.y - 24.0).abs() < 0.01, "y = {}", b.y);
2217 }
2218
2219 #[test]
2220 fn viewport_corner_top_trailing_ltr() {
2221 let mut mgr = OverlayManager::new();
2222 let id = show_corner_overlay(
2223 &mut mgr,
2224 Corner::TopTrailing,
2225 Vec2::new(24.0, 24.0),
2226 Size::new(380.0, 100.0),
2227 );
2228 mgr.position_overlays(
2229 |_| Some(Rect::ZERO),
2230 (800.0, 600.0),
2231 LayoutDirection::LeftToRight,
2232 );
2233 let b = overlay_bounds(&mgr, id);
2234 assert!((b.x - 396.0).abs() < 0.01, "x = {}", b.x);
2236 assert!((b.y - 24.0).abs() < 0.01);
2237 }
2238
2239 #[test]
2240 fn viewport_corner_bottom_leading_ltr() {
2241 let mut mgr = OverlayManager::new();
2242 let id = show_corner_overlay(
2243 &mut mgr,
2244 Corner::BottomLeading,
2245 Vec2::new(24.0, 24.0),
2246 Size::new(380.0, 100.0),
2247 );
2248 mgr.position_overlays(
2249 |_| Some(Rect::ZERO),
2250 (800.0, 600.0),
2251 LayoutDirection::LeftToRight,
2252 );
2253 let b = overlay_bounds(&mgr, id);
2254 assert!((b.x - 24.0).abs() < 0.01);
2256 assert!((b.y - 476.0).abs() < 0.01, "y = {}", b.y);
2257 }
2258
2259 #[test]
2260 fn viewport_corner_bottom_trailing_ltr() {
2261 let mut mgr = OverlayManager::new();
2262 let id = show_corner_overlay(
2263 &mut mgr,
2264 Corner::BottomTrailing,
2265 Vec2::new(24.0, 24.0),
2266 Size::new(380.0, 100.0),
2267 );
2268 mgr.position_overlays(
2269 |_| Some(Rect::ZERO),
2270 (800.0, 600.0),
2271 LayoutDirection::LeftToRight,
2272 );
2273 let b = overlay_bounds(&mgr, id);
2274 assert!((b.x - 396.0).abs() < 0.01);
2275 assert!((b.y - 476.0).abs() < 0.01);
2276 }
2277
2278 #[test]
2279 fn viewport_corner_top_trailing_rtl_flips_to_left() {
2280 let mut mgr = OverlayManager::new();
2281 let id = show_corner_overlay(
2282 &mut mgr,
2283 Corner::TopTrailing,
2284 Vec2::new(24.0, 24.0),
2285 Size::new(380.0, 100.0),
2286 );
2287 mgr.position_overlays(
2288 |_| Some(Rect::ZERO),
2289 (800.0, 600.0),
2290 LayoutDirection::RightToLeft,
2291 );
2292 let b = overlay_bounds(&mgr, id);
2293 assert!((b.x - 24.0).abs() < 0.01, "x = {}", b.x);
2295 assert!((b.y - 24.0).abs() < 0.01);
2296 }
2297
2298 #[test]
2299 fn viewport_corner_bottom_leading_rtl_flips_to_right() {
2300 let mut mgr = OverlayManager::new();
2301 let id = show_corner_overlay(
2302 &mut mgr,
2303 Corner::BottomLeading,
2304 Vec2::new(24.0, 24.0),
2305 Size::new(380.0, 100.0),
2306 );
2307 mgr.position_overlays(
2308 |_| Some(Rect::ZERO),
2309 (800.0, 600.0),
2310 LayoutDirection::RightToLeft,
2311 );
2312 let b = overlay_bounds(&mgr, id);
2313 assert!((b.x - 396.0).abs() < 0.01, "x = {}", b.x);
2314 assert!((b.y - 476.0).abs() < 0.01);
2315 }
2316
2317 #[test]
2318 fn viewport_corner_ignores_anchor_bounds() {
2319 let mut mgr = OverlayManager::new();
2320 let id = show_corner_overlay(
2321 &mut mgr,
2322 Corner::BottomTrailing,
2323 Vec2::new(0.0, 0.0),
2324 Size::new(100.0, 100.0),
2325 );
2326 mgr.position_overlays(
2328 |_| Some(Rect::new(123.0, 456.0, 7.0, 8.0)),
2329 (800.0, 600.0),
2330 LayoutDirection::LeftToRight,
2331 );
2332 let b = overlay_bounds(&mgr, id);
2333 assert_eq!((b.x, b.y), (700.0, 500.0));
2334 }
2335
2336 #[test]
2337 fn near_anchor_horizontal_is_direction_aware() {
2338 let anchor = Rect::new(600.0, 100.0, 100.0, 20.0);
2344 let resolved_x = |dir: LayoutDirection| {
2345 let mut mgr = OverlayManager::new();
2346 let id = mgr.show(OverlayRequest {
2347 content_id: fake_id(10),
2348 anchor: fake_id(1),
2349 placement: OverlayPlacement::NearAnchor {
2350 offset: Vec2::new(0.0, 8.0),
2351 },
2352 dismiss: DismissBehavior::Manual,
2353 layer: OverlayLayer::InTree,
2354 parent_overlay: None,
2355 on_dismiss: None,
2356 fade_duration: None,
2357 });
2358 mgr.set_content_bounds(id, Size::new(200.0, 50.0));
2359 mgr.position_overlays(|_| Some(anchor), (800.0, 600.0), dir);
2360 overlay_bounds(&mgr, id).x
2361 };
2362 assert!(
2364 (resolved_x(LayoutDirection::LeftToRight) - 600.0).abs() < 0.01,
2365 "LTR x = {}",
2366 resolved_x(LayoutDirection::LeftToRight)
2367 );
2368 assert!(
2370 (resolved_x(LayoutDirection::RightToLeft) - 500.0).abs() < 0.01,
2371 "RTL x = {}",
2372 resolved_x(LayoutDirection::RightToLeft)
2373 );
2374 }
2375
2376 #[test]
2379 fn pause_auto_dismiss_removes_overlay_from_deadline_set() {
2380 let mut mgr = OverlayManager::new();
2381 let id = mgr.show_for(
2382 OverlayRequest {
2383 content_id: fake_id(10),
2384 anchor: fake_id(1),
2385 placement: OverlayPlacement::Centered,
2386 dismiss: DismissBehavior::Manual,
2387 layer: OverlayLayer::InTree,
2388 parent_overlay: None,
2389 on_dismiss: None,
2390 fade_duration: None,
2391 },
2392 Duration::from_secs(10),
2393 );
2394 assert!(mgr.next_auto_dismiss_deadline().is_some());
2395 assert!(!mgr.is_auto_dismiss_paused(id));
2396
2397 mgr.pause_auto_dismiss(id);
2398 assert!(mgr.is_auto_dismiss_paused(id));
2399 assert!(
2400 mgr.next_auto_dismiss_deadline().is_none(),
2401 "paused overlay must drop out of the deadline-min query"
2402 );
2403
2404 mgr.resume_auto_dismiss(id);
2405 assert!(!mgr.is_auto_dismiss_paused(id));
2406 assert!(mgr.next_auto_dismiss_deadline().is_some());
2407 }
2408
2409 #[test]
2410 fn pause_then_resume_restores_remaining_time() {
2411 let mut mgr = OverlayManager::new();
2412 let id = mgr.show_for(
2413 OverlayRequest {
2414 content_id: fake_id(11),
2415 anchor: fake_id(1),
2416 placement: OverlayPlacement::Centered,
2417 dismiss: DismissBehavior::Manual,
2418 layer: OverlayLayer::InTree,
2419 parent_overlay: None,
2420 on_dismiss: None,
2421 fade_duration: None,
2422 },
2423 Duration::from_secs(10),
2424 );
2425
2426 mgr.pause_auto_dismiss(id);
2427 let overlay = mgr.stack.iter().find(|o| o.id == id).unwrap();
2430 let remaining = overlay.paused_remaining.unwrap();
2431 assert!(
2432 remaining >= Duration::from_secs(9),
2433 "remaining should be near the original 10s, got {remaining:?}"
2434 );
2435 assert!(remaining <= Duration::from_secs(10));
2436
2437 mgr.resume_auto_dismiss(id);
2438 let overlay = mgr.stack.iter().find(|o| o.id == id).unwrap();
2439 assert_eq!(overlay.auto_dismiss_after, Some(remaining));
2443 assert!(overlay.paused_remaining.is_none());
2444 }
2445
2446 #[test]
2447 fn pause_is_idempotent() {
2448 let mut mgr = OverlayManager::new();
2449 let id = mgr.show_for(
2450 OverlayRequest {
2451 content_id: fake_id(12),
2452 anchor: fake_id(1),
2453 placement: OverlayPlacement::Centered,
2454 dismiss: DismissBehavior::Manual,
2455 layer: OverlayLayer::InTree,
2456 parent_overlay: None,
2457 on_dismiss: None,
2458 fade_duration: None,
2459 },
2460 Duration::from_secs(10),
2461 );
2462 mgr.pause_auto_dismiss(id);
2463 let first_remaining = mgr.stack[0].paused_remaining;
2464 mgr.pause_auto_dismiss(id); let second_remaining = mgr.stack[0].paused_remaining;
2466 assert_eq!(
2467 first_remaining, second_remaining,
2468 "double-pause must preserve the original stashed remaining"
2469 );
2470 }
2471
2472 #[test]
2473 fn resume_on_unpaused_is_noop() {
2474 let mut mgr = OverlayManager::new();
2475 let id = mgr.show_for(
2476 OverlayRequest {
2477 content_id: fake_id(13),
2478 anchor: fake_id(1),
2479 placement: OverlayPlacement::Centered,
2480 dismiss: DismissBehavior::Manual,
2481 layer: OverlayLayer::InTree,
2482 parent_overlay: None,
2483 on_dismiss: None,
2484 fade_duration: None,
2485 },
2486 Duration::from_secs(10),
2487 );
2488 let before = mgr.stack[0].auto_dismiss_after;
2489 mgr.resume_auto_dismiss(id); let after = mgr.stack[0].auto_dismiss_after;
2491 assert_eq!(before, after);
2492 }
2493
2494 #[test]
2495 fn pause_on_persistent_overlay_is_noop() {
2496 let mut mgr = OverlayManager::new();
2497 let id = mgr.show(OverlayRequest {
2498 content_id: fake_id(14),
2499 anchor: fake_id(1),
2500 placement: OverlayPlacement::Centered,
2501 dismiss: DismissBehavior::Manual,
2502 layer: OverlayLayer::InTree,
2503 parent_overlay: None,
2504 on_dismiss: None,
2505 fade_duration: None,
2506 });
2507 mgr.pause_auto_dismiss(id);
2509 assert!(!mgr.is_auto_dismiss_paused(id));
2510 assert!(mgr.stack[0].paused_remaining.is_none());
2511 }
2512
2513 #[test]
2514 fn pause_on_unknown_id_is_noop() {
2515 let mut mgr = OverlayManager::new();
2516 mgr.pause_auto_dismiss(OverlayId::new(9999)); mgr.resume_auto_dismiss(OverlayId::new(9999));
2518 }
2519
2520 #[test]
2521 fn viewport_corner_zero_margin_snaps_to_edge() {
2522 let mut mgr = OverlayManager::new();
2523 let id = show_corner_overlay(
2524 &mut mgr,
2525 Corner::TopLeading,
2526 Vec2::ZERO,
2527 Size::new(50.0, 50.0),
2528 );
2529 mgr.position_overlays(
2530 |_| Some(Rect::ZERO),
2531 (800.0, 600.0),
2532 LayoutDirection::LeftToRight,
2533 );
2534 let b = overlay_bounds(&mgr, id);
2535 assert_eq!((b.x, b.y), (0.0, 0.0));
2536 }
2537}