1use super::*;
5
6use crate::accessibility::AccessNodeBuilder;
7
8impl WidgetTree {
9 pub fn attach_tooltip(
12 &mut self,
13 anchor_id: WidgetId,
14 content_id: WidgetId,
15 delay: std::time::Duration,
16 ) {
17 self.attach_tooltip_inner(
18 anchor_id,
19 content_id,
20 delay,
21 None,
22 None,
23 crate::overlay::TooltipPlacement::Below,
24 );
25 }
26
27 pub fn attach_tooltip_with_placement(
32 &mut self,
33 anchor_id: WidgetId,
34 content_id: WidgetId,
35 delay: std::time::Duration,
36 placement: crate::overlay::TooltipPlacement,
37 ) {
38 self.attach_tooltip_inner(anchor_id, content_id, delay, None, None, placement);
39 }
40
41 pub fn attach_tooltip_with_sticky(
53 &mut self,
54 anchor_id: WidgetId,
55 content_id: WidgetId,
56 delay: std::time::Duration,
57 sticky_after: Option<std::time::Duration>,
58 ) {
59 self.attach_tooltip_inner(
60 anchor_id,
61 content_id,
62 delay,
63 sticky_after,
64 None,
65 crate::overlay::TooltipPlacement::Below,
66 );
67 }
68
69 pub fn attach_tooltip_with_sticky_sink(
75 &mut self,
76 anchor_id: WidgetId,
77 content_id: WidgetId,
78 delay: std::time::Duration,
79 sticky_after: Option<std::time::Duration>,
80 shown_at_sink: std::rc::Rc<std::cell::Cell<Option<std::time::Instant>>>,
81 ) {
82 self.attach_tooltip_inner(
83 anchor_id,
84 content_id,
85 delay,
86 sticky_after,
87 Some(shown_at_sink),
88 crate::overlay::TooltipPlacement::Below,
89 );
90 }
91
92 pub fn attach_tooltip_with_sticky_sink_placement(
97 &mut self,
98 anchor_id: WidgetId,
99 content_id: WidgetId,
100 delay: std::time::Duration,
101 sticky_after: Option<std::time::Duration>,
102 shown_at_sink: std::rc::Rc<std::cell::Cell<Option<std::time::Instant>>>,
103 placement: crate::overlay::TooltipPlacement,
104 ) {
105 self.attach_tooltip_inner(
106 anchor_id,
107 content_id,
108 delay,
109 sticky_after,
110 Some(shown_at_sink),
111 placement,
112 );
113 }
114
115 fn retire_tooltips_for_anchor(&mut self, anchor_id: WidgetId, keep_content_id: WidgetId) {
130 let stale: Vec<(WidgetId, Option<crate::overlay::OverlayId>)> = self
131 .tooltips
132 .iter()
133 .filter(|entry| entry.anchor_id == anchor_id && entry.content_id != keep_content_id)
134 .map(|entry| (entry.content_id, entry.overlay_id))
135 .collect();
136 if stale.is_empty() {
137 return;
138 }
139 self.tooltips
140 .retain(|entry| entry.anchor_id != anchor_id || entry.content_id == keep_content_id);
141 for (content_id, overlay_id) in stale {
142 if let Some(overlay_id) = overlay_id {
146 self.dismiss_overlay(overlay_id);
147 }
148 self.destroy_subtree(content_id);
149 }
150 }
151
152 pub(super) fn retire_tooltips_of_destroyed_anchor(&mut self, anchor_id: WidgetId) {
159 let stale: Vec<(WidgetId, Option<crate::overlay::OverlayId>)> = self
160 .tooltips
161 .iter()
162 .filter(|entry| entry.anchor_id == anchor_id)
163 .map(|entry| (entry.content_id, entry.overlay_id))
164 .collect();
165 if stale.is_empty() {
166 return;
167 }
168 self.tooltips.retain(|entry| entry.anchor_id != anchor_id);
169 for (content_id, overlay_id) in stale {
170 if let Some(overlay_id) = overlay_id {
171 self.dismiss_overlay(overlay_id);
172 }
173 self.destroy_subtree(content_id);
174 }
175 }
176
177 fn attach_tooltip_inner(
178 &mut self,
179 anchor_id: WidgetId,
180 content_id: WidgetId,
181 delay: std::time::Duration,
182 sticky_after: Option<std::time::Duration>,
183 shown_at_sink: Option<std::rc::Rc<std::cell::Cell<Option<std::time::Instant>>>>,
184 placement: crate::overlay::TooltipPlacement,
185 ) {
186 self.retire_tooltips_for_anchor(anchor_id, content_id);
187 if let Some(entry) = self
190 .tooltips
191 .iter_mut()
192 .find(|entry| entry.anchor_id == anchor_id && entry.content_id == content_id)
193 {
194 entry.delay = delay;
195 entry.sticky_after = sticky_after;
196 entry.placement = placement;
197 if shown_at_sink.is_some() {
198 entry.shown_at_sink = shown_at_sink;
199 }
200 return;
201 }
202 self.arena.set_dormant(content_id);
203 self.tooltips.push(TooltipEntry {
204 anchor_id,
205 content_id,
206 description_owner_id: anchor_id,
210 delay,
211 hover_start: None,
212 real_hover_start: None,
213 hover_origin: None,
214 overlay_id: None,
215 sticky_after,
216 is_sticky: false,
217 shown_at_sim: None,
218 shown_at_real: None,
219 shown_at_sink,
220 promoted_by_focus: false,
221 armed_by_focus: false,
222 armed_by_hold: false,
223 suppressed_until_focus_leaves: false,
224 placement,
225 });
226 }
227
228 pub(crate) fn set_tooltip_description_owner(
247 &mut self,
248 anchor_id: WidgetId,
249 owner_id: WidgetId,
250 ) {
251 if let Some(entry) = self
252 .tooltips
253 .iter_mut()
254 .find(|entry| entry.anchor_id == anchor_id)
255 {
256 entry.description_owner_id = owner_id;
257 }
258 }
259
260 fn tooltip_overlay_placement(
267 placement: crate::overlay::TooltipPlacement,
268 ) -> crate::overlay::OverlayPlacement {
269 match placement {
270 crate::overlay::TooltipPlacement::Below => {
271 crate::overlay::OverlayPlacement::NearAnchor {
272 offset: teksilo_canvas::Vec2::new(0.0, 8.0),
273 }
274 }
275 crate::overlay::TooltipPlacement::Side => {
276 crate::overlay::OverlayPlacement::TrailingEdge
277 }
278 }
279 }
280
281 pub(super) fn process_tooltips(&mut self) {
282 let sim_now = self.sim_clock;
283 let session_active = self.tooltip_session_active_sim(sim_now);
284 self.process_tooltips_impl(
285 |entry| {
286 entry
287 .hover_start
288 .map(|start| sim_now.saturating_duration_since(start))
289 },
290 session_active,
291 true,
292 );
293 }
294
295 pub(super) fn process_tooltips_real(&mut self) {
296 let real_now = std::time::Instant::now();
297 let session_active = self.tooltip_session_active_real(real_now);
298 self.process_tooltips_impl(
299 |entry| {
300 entry
301 .real_hover_start
302 .map(|start| real_now.saturating_duration_since(start))
303 },
304 session_active,
305 false,
306 );
307 }
308
309 fn any_tooltip_in_session(&self) -> bool {
318 self.tooltips
319 .iter()
320 .any(|e| e.overlay_id.is_some() && !e.is_sticky)
321 }
322
323 fn tooltip_session_active_sim(&self, now: std::time::Instant) -> bool {
325 self.any_tooltip_in_session()
326 || self
327 .tooltip_session_until_sim
328 .is_some_and(|until| now < until)
329 }
330
331 fn tooltip_session_active_real(&self, now: std::time::Instant) -> bool {
333 self.any_tooltip_in_session()
334 || self
335 .tooltip_session_until_real
336 .is_some_and(|until| now < until)
337 }
338
339 fn effective_tooltip_delay(
353 &self,
354 entry_delay: std::time::Duration,
355 session_active: bool,
356 ) -> std::time::Duration {
357 if !session_active {
358 return entry_delay;
359 }
360 let motion = &self.theme.motion;
361 let base = motion.tooltip_delay.as_secs_f64();
362 if base <= 0.0 {
363 return entry_delay.min(motion.tooltip_reshow_delay);
366 }
367 let ratio = motion.tooltip_reshow_delay.as_secs_f64() / base;
368 entry_delay.mul_f64(ratio).min(entry_delay)
370 }
371
372 fn process_tooltips_impl(
373 &mut self,
374 elapsed_fn: impl Fn(&TooltipEntry) -> Option<std::time::Duration>,
375 session_active: bool,
376 use_sim_clock: bool,
377 ) {
378 let dismissed_indices: Vec<usize> = self
384 .tooltips
385 .iter()
386 .enumerate()
387 .filter_map(|(i, e)| match e.overlay_id {
388 Some(oid) if !self.overlay_manager.stack.iter().any(|o| o.id == oid) => Some(i),
389 _ => None,
390 })
391 .collect();
392 let any_dismissed = !dismissed_indices.is_empty();
393 for i in dismissed_indices {
394 let e = &mut self.tooltips[i];
395 e.overlay_id = None;
396 e.is_sticky = false;
397 e.promoted_by_focus = false;
398 e.armed_by_hold = false;
399 e.shown_at_sim = None;
400 e.shown_at_real = None;
401 e.hover_origin = None;
402 if let Some(sink) = e.shown_at_sink.as_ref() {
403 sink.set(None);
404 }
405 }
406 if any_dismissed {
409 let grace = super::TOOLTIP_SESSION_GRACE;
410 if use_sim_clock {
411 self.tooltip_session_until_sim = Some(self.sim_clock + grace);
412 } else {
413 self.tooltip_session_until_real = Some(std::time::Instant::now() + grace);
414 }
415 }
416
417 let session_active = session_active
422 || self.any_tooltip_in_session()
423 || if use_sim_clock {
424 self.tooltip_session_active_sim(self.sim_clock)
425 } else {
426 self.tooltip_session_active_real(std::time::Instant::now())
427 };
428
429 let mut to_show = Vec::new();
430 let pending: Vec<usize> = self
433 .tooltips
434 .iter()
435 .enumerate()
436 .filter(|(_, entry)| entry.overlay_id.is_none())
437 .filter(|(_, entry)| self.arena.is_active(entry.anchor_id))
438 .filter_map(|(i, entry)| {
439 let delay = self.effective_tooltip_delay(entry.delay, session_active);
440 let elapsed = elapsed_fn(entry)?;
441 (elapsed >= delay).then_some(i)
442 })
443 .collect();
444 for index in pending {
445 let content_id = self.tooltips[index].content_id;
450 self.materialize_deferred(content_id);
451 let body_id = self.tooltip_content_node(content_id);
462 if !self
463 .arena
464 .get(body_id)
465 .is_none_or(|node| node.widget.tooltip_has_content())
466 {
467 let entry = &mut self.tooltips[index];
468 entry.hover_start = None;
469 entry.real_hover_start = None;
470 entry.hover_origin = None;
471 continue;
472 }
473 let entry = &mut self.tooltips[index];
474 to_show.push((
475 entry.anchor_id,
476 entry.content_id,
477 entry.placement,
478 entry.armed_by_focus,
479 entry.armed_by_hold,
480 ));
481 entry.hover_start = None;
482 entry.real_hover_start = None;
483 entry.hover_origin = None;
484 }
485 let sim_now = self.sim_clock;
486 let real_now = std::time::Instant::now();
487 let fade_duration = if self.prefers_reduced_motion || session_active {
494 None
495 } else {
496 Some(self.theme.motion.duration_fast)
497 };
498 for (anchor_id, content_id, placement, by_focus, by_hold) in to_show {
499 self.arena.activate(content_id);
500 let dismiss = if by_focus || by_hold {
509 crate::overlay::DismissBehavior::EscapeOrClickOutside
510 } else {
511 crate::overlay::DismissBehavior::PointerLeave {
512 delay: std::time::Duration::from_millis(100),
513 }
514 };
515 let mut request = crate::overlay::OverlayRequest::new(
516 content_id,
517 anchor_id,
518 Self::tooltip_overlay_placement(placement),
519 dismiss,
520 );
521 request.fade_duration = fade_duration;
522 let oid = if by_hold {
523 self.show_overlay_for(request, super::touch_route::TOUCH_TOOLTIP_DISMISS)
524 } else {
525 self.show_overlay(request)
526 };
527 if by_focus && let Some(focused) = self.focused {
528 self.overlay_manager.set_top_focus_restore(focused);
529 }
530 if let Some(entry) = self
531 .tooltips
532 .iter_mut()
533 .find(|e| e.content_id == content_id)
534 {
535 entry.overlay_id = Some(oid);
536 entry.shown_at_sim = Some(sim_now);
537 entry.shown_at_real = Some(real_now);
538 entry.promoted_by_focus = by_focus;
539 if let Some(sink) = entry.shown_at_sink.as_ref() {
540 sink.set(Some(real_now));
541 }
542 }
543 }
544
545 let mut to_mark_paint: Vec<WidgetId> = Vec::new();
559 let mut to_promote: Vec<WidgetId> = Vec::new();
560 for entry in &self.tooltips {
561 let Some(sticky_after) = entry.sticky_after else {
562 continue;
563 };
564 if entry.overlay_id.is_none() || entry.is_sticky {
565 continue;
566 }
567 let elapsed = entry
568 .shown_at_real
569 .map(|t| real_now.saturating_duration_since(t));
570 let elapsed = match elapsed {
571 Some(e) => e,
572 None => continue,
573 };
574 to_mark_paint.push(entry.content_id);
578 if elapsed >= sticky_after {
579 to_promote.push(entry.content_id);
580 }
581 }
582 for id in to_mark_paint {
583 self.arena.mark_subtree_needs_paint(id);
584 }
585 for content_id in to_promote {
586 self.promote_tooltip_to_sticky(content_id);
587 }
588 }
589
590 pub(super) fn process_delayed_overlays(&mut self) {
591 let sim_now = self.sim_clock;
592 let mut noop = crate::window::NoopWindowOps;
593 self.process_delayed_overlays_impl(
594 |p| sim_now.saturating_duration_since(p.sim_requested_at),
595 &mut noop,
596 );
597 }
598
599 pub(super) fn process_delayed_overlays_real(&mut self, ops: &mut dyn crate::window::WindowOps) {
600 let real_now = std::time::Instant::now();
601 self.process_delayed_overlays_impl(
602 |p| real_now.saturating_duration_since(p.real_requested_at),
603 &mut *ops,
604 );
605 }
606
607 fn process_delayed_overlays_impl(
608 &mut self,
609 elapsed_fn: impl Fn(&PendingDelayedOverlay) -> std::time::Duration,
610 ops: &mut dyn crate::window::WindowOps,
611 ) {
612 let mut ready_indices = Vec::new();
613 for (index, pending) in self.pending_delayed_overlays.iter().enumerate() {
614 if elapsed_fn(pending) >= pending.delay {
615 ready_indices.push(index);
616 }
617 }
618
619 let mut ready = Vec::new();
620 for &index in ready_indices.iter().rev() {
621 ready.push(self.pending_delayed_overlays.remove(index));
622 }
623 ready.reverse();
624
625 let any_shown = !ready.is_empty();
626 for pending in ready {
627 let content_id = pending.request.content_id;
628 if pending.replace_siblings {
635 let anchor = pending.request.anchor;
636 self.dismiss_child_overlays_for_source(anchor, Some(content_id), &mut *ops);
637 }
638 self.arena.activate(content_id);
639 let current_focus = self.focused;
640 self.overlay_manager.show(pending.request);
641 if let Some(focus_id) = current_focus {
642 self.overlay_manager.set_top_focus_restore(focus_id);
643 }
644 self.arena.mark_needs_paint(content_id);
645 if let Some(focus_target) = pending.focus_target
646 && self.arena.is_active(focus_target)
647 {
648 self.focus_ops(focus_target, &mut *ops);
649 }
650 }
651 if any_shown {
652 self.a11y_dirty = true;
653 }
654 }
655
656 pub(super) fn overlay_ancestor_for_widget(
657 &self,
658 widget_id: WidgetId,
659 ) -> Option<crate::overlay::OverlayId> {
660 self.overlay_manager
661 .stack
662 .iter()
663 .rev()
664 .find(|overlay| self.is_descendant_of(widget_id, overlay.content_id))
665 .map(|overlay| overlay.id)
666 }
667
668 pub(super) fn modal_overlay_for_widget(
669 &self,
670 widget_id: WidgetId,
671 ) -> Option<crate::overlay::OverlayId> {
672 let mut current = self.overlay_ancestor_for_widget(widget_id);
673
674 while let Some(overlay_id) = current {
675 let overlay = self.overlay_manager.overlay(overlay_id)?;
676 if overlay.placement.is_modal() {
677 return Some(overlay_id);
678 }
679 current = overlay.parent_overlay;
680 }
681
682 None
683 }
684
685 pub fn is_behind_modal(&self, widget_id: WidgetId) -> bool {
713 let Some(modal) = self.overlay_manager.topmost_modal() else {
714 return false;
715 };
716 let stack = &self.overlay_manager.stack;
717 let Some(at) = stack.iter().position(|overlay| overlay.id == modal.id) else {
718 return false;
719 };
720 let (below, above) = stack.split_at(at);
721 let in_modal = |id: WidgetId| {
722 above
723 .iter()
724 .any(|overlay| self.is_descendant_of(id, overlay.content_id))
725 };
726 !(in_modal(widget_id)
727 || below.iter().any(|overlay| {
728 self.is_descendant_of(widget_id, overlay.content_id) && in_modal(overlay.anchor)
729 }))
730 }
731
732 fn menu_ancestor_for_widget(&self, widget_id: WidgetId) -> Option<WidgetId> {
733 let mut current = Some(widget_id);
734 while let Some(id) = current {
735 if let Some(node) = self.arena.get(id) {
736 let mut builder = AccessNodeBuilder::new();
737 node.widget.accessibility(&mut builder);
738 if builder.role() == accesskit::Role::Menu {
739 return Some(id);
740 }
741 }
742 current = self.arena.parent(id);
743 }
744 None
745 }
746
747 pub(super) fn dismiss_child_overlays_for_source(
748 &mut self,
749 source_widget: WidgetId,
750 preserve_content: Option<WidgetId>,
751 ops: &mut dyn crate::window::WindowOps,
752 ) {
753 if let Some(parent_overlay) = self.overlay_ancestor_for_widget(source_widget) {
754 let preserve_overlay = preserve_content
755 .and_then(|content_id| self.overlay_manager.find_by_content(content_id));
756 let (dismissed, focus_restore) = self
757 .overlay_manager
758 .dismiss_descendants_of(parent_overlay, preserve_overlay);
759 self.dormant_dismissed_content(&dismissed, &mut *ops);
760 if let Some(restore_id) = focus_restore
761 && self.arena.is_active(restore_id)
762 {
763 self.focus_ops(restore_id, &mut *ops);
764 }
765 return;
766 }
767
768 let Some(menu_root) = self.menu_ancestor_for_widget(source_widget) else {
769 return;
770 };
771 let preserve_overlay = preserve_content
772 .and_then(|content_id| self.overlay_manager.find_by_content(content_id));
773 let overlay_ids: Vec<crate::overlay::OverlayId> = self
774 .overlay_manager
775 .stack
776 .iter()
777 .filter(|overlay| {
778 overlay.parent_overlay.is_none()
779 && self.is_descendant_of(overlay.anchor, menu_root)
780 && !preserve_overlay.is_some_and(|keep| {
781 overlay.id == keep
782 || self.overlay_manager.is_descendant_of(overlay.id, keep)
783 })
784 })
785 .map(|overlay| overlay.id)
786 .collect();
787
788 for overlay_id in overlay_ids {
789 let (dismissed, focus_restore) =
790 self.overlay_manager.dismiss_with_focus_restore(overlay_id);
791 self.dormant_dismissed_content(&dismissed, &mut *ops);
792 if let Some(restore_id) = focus_restore
793 && self.arena.is_active(restore_id)
794 {
795 self.focus_ops(restore_id, &mut *ops);
796 }
797 }
798 }
799
800 pub(super) fn dismiss_self_overlay_chain_for_source(
812 &mut self,
813 source_widget: WidgetId,
814 ops: &mut dyn crate::window::WindowOps,
815 ) {
816 let Some(start) = self.overlay_ancestor_for_widget(source_widget) else {
817 return;
818 };
819
820 let mut topmost_to_dismiss: Option<crate::overlay::OverlayId> = None;
821 let mut current = Some(start);
822 while let Some(overlay_id) = current {
823 let Some(overlay) = self.overlay_manager.overlay(overlay_id) else {
824 break;
825 };
826 if self.overlay_is_host_surface(overlay_id) {
827 break;
828 }
829 topmost_to_dismiss = Some(overlay_id);
830 current = overlay.parent_overlay;
831 }
832
833 if let Some(target) = topmost_to_dismiss {
834 let (dismissed, focus_restore) =
835 self.overlay_manager.dismiss_with_focus_restore(target);
836 self.dormant_dismissed_content(&dismissed, &mut *ops);
837 if let Some(restore_id) = focus_restore
838 && self.arena.is_active(restore_id)
839 {
840 self.focus_ops(restore_id, &mut *ops);
841 }
842 }
843 }
844
845 fn overlay_is_anchor_positioned(placement: &crate::overlay::OverlayPlacement) -> bool {
862 use crate::overlay::OverlayPlacement as P;
863 match placement {
864 P::Below
865 | P::Above
866 | P::TrailingEdge
867 | P::AtPointer(_)
868 | P::AtPointerAvoiding { .. }
869 | P::NearAnchor { .. }
870 | P::BelowPreferred => true,
871 P::Centered
877 | P::TopCenter { .. }
878 | P::BottomCenter
879 | P::ViewportCorner { .. }
880 | P::FullViewport
881 | P::AboveSelection { .. }
882 | P::BelowSelection { .. } => false,
883 }
884 }
885
886 fn overlay_follows_focus_out(&self, overlay_id: crate::overlay::OverlayId) -> bool {
926 let Some(overlay) = self.overlay_manager.overlay(overlay_id) else {
927 return false;
928 };
929 if !Self::overlay_is_anchor_positioned(&overlay.placement) {
930 return false;
931 }
932 if overlay.is_dismissing() {
933 return false;
934 }
935 if overlay.inert {
936 return false;
937 }
938 !self
939 .tooltips
940 .iter()
941 .any(|entry| entry.content_id == overlay.content_id)
942 }
943
944 fn overlay_orbit_for_widget(&self, widget_id: WidgetId) -> Option<crate::overlay::OverlayId> {
967 self.overlay_manager
968 .stack
969 .iter()
970 .rev()
971 .find(|overlay| {
972 self.is_descendant_of(widget_id, overlay.content_id)
973 || (self.overlay_follows_focus_out(overlay.id)
974 && self.is_descendant_of(widget_id, overlay.anchor))
975 })
976 .map(|overlay| overlay.id)
977 }
978
979 pub(super) fn dismiss_overlays_left_by_focus(
1006 &mut self,
1007 old: Option<WidgetId>,
1008 new_focus: Option<WidgetId>,
1009 ops: &mut dyn crate::window::WindowOps,
1010 ) {
1011 let Some(old) = old else {
1012 return;
1013 };
1014 let Some(old_overlay) = self.overlay_orbit_for_widget(old) else {
1015 return;
1016 };
1017 let new_overlay = new_focus.and_then(|id| self.overlay_ancestor_for_widget(id));
1025
1026 if let Some(new_overlay) = new_overlay
1029 && (new_overlay == old_overlay
1030 || self
1031 .overlay_manager
1032 .is_descendant_of(new_overlay, old_overlay))
1033 {
1034 return;
1035 }
1036
1037 if !self.overlay_follows_focus_out(old_overlay) {
1038 return;
1039 }
1040
1041 let mut topmost = old_overlay;
1042 let mut current = self
1043 .overlay_manager
1044 .overlay(old_overlay)
1045 .and_then(|overlay| overlay.parent_overlay);
1046 while let Some(overlay_id) = current {
1047 if Some(overlay_id) == new_overlay {
1051 break;
1052 }
1053 if self.overlay_is_host_surface(overlay_id) {
1056 break;
1057 }
1058 if !self.overlay_follows_focus_out(overlay_id) {
1059 break;
1060 }
1061 topmost = overlay_id;
1062 current = self
1063 .overlay_manager
1064 .overlay(overlay_id)
1065 .and_then(|overlay| overlay.parent_overlay);
1066 }
1067
1068 let dismissed = self.overlay_manager.dismiss(topmost);
1069 self.dormant_dismissed_content(&dismissed, &mut *ops);
1070 }
1071
1072 pub(super) fn dismiss_all_overlays_except_hosts(
1083 &mut self,
1084 ops: &mut dyn crate::window::WindowOps,
1085 ) {
1086 let to_dismiss: Vec<crate::overlay::OverlayId> = self
1087 .overlay_manager
1088 .stack
1089 .iter()
1090 .map(|overlay| overlay.id)
1091 .filter(|&id| !self.overlay_is_host_surface(id))
1092 .collect();
1093
1094 for overlay_id in to_dismiss {
1095 let dismissed = self.overlay_manager.dismiss(overlay_id);
1096 self.dormant_dismissed_content(&dismissed, &mut *ops);
1097 }
1098 }
1099
1100 pub(super) fn overlay_is_host_surface(&self, overlay_id: crate::overlay::OverlayId) -> bool {
1106 let Some(overlay) = self.overlay_manager.overlay(overlay_id) else {
1107 return false;
1108 };
1109 if overlay.placement.is_modal() {
1118 return true;
1119 }
1120 let Some(node) = self.arena.get(overlay.content_id) else {
1121 return false;
1122 };
1123 let role = node
1131 .access_overrides
1132 .as_ref()
1133 .and_then(|o| o.role)
1134 .unwrap_or_else(|| {
1135 let mut builder = AccessNodeBuilder::new();
1136 node.widget.accessibility(&mut builder);
1137 builder.role()
1138 });
1139 matches!(
1140 role,
1141 accesskit::Role::Tooltip
1142 | accesskit::Role::Dialog
1143 | accesskit::Role::AlertDialog
1144 | accesskit::Role::MenuBar
1145 )
1146 }
1147
1148 pub(super) fn dismiss_modal_for_source(
1149 &mut self,
1150 source_widget: WidgetId,
1151 ops: &mut dyn crate::window::WindowOps,
1152 ) -> bool {
1153 let Some(modal_overlay) = self.modal_overlay_for_widget(source_widget) else {
1154 return false;
1155 };
1156
1157 let (dismissed, focus_restore) = self
1158 .overlay_manager
1159 .dismiss_with_focus_restore(modal_overlay);
1160 self.dormant_dismissed_content(&dismissed, &mut *ops);
1161 if let Some(restore_id) = focus_restore
1162 && self.arena.is_active(restore_id)
1163 {
1164 self.focus_ops(restore_id, &mut *ops);
1165 }
1166 true
1167 }
1168
1169 pub fn is_descendant_of(&self, widget_id: WidgetId, ancestor: WidgetId) -> bool {
1170 if widget_id == ancestor {
1171 return true;
1172 }
1173 let mut current = self.arena.parent(widget_id);
1174 while let Some(parent_id) = current {
1175 if parent_id == ancestor {
1176 return true;
1177 }
1178 current = self.arena.parent(parent_id);
1179 }
1180 false
1181 }
1182
1183 fn is_overlay_content_root(&self, widget_id: WidgetId) -> bool {
1190 self.overlay_manager.find_by_content(widget_id).is_some()
1191 }
1192
1193 pub(super) fn tooltip_focus_enter(&mut self, widget_id: WidgetId) {
1224 type ToShow = (WidgetId, WidgetId, crate::overlay::TooltipPlacement);
1247 let mut direct: Vec<ToShow> = Vec::new();
1248 let mut reverse: Vec<ToShow> = Vec::new();
1249 for e in &self.tooltips {
1250 if e.sticky_after.is_none() || e.overlay_id.is_some() || e.suppressed_until_focus_leaves
1251 {
1252 continue;
1253 }
1254 if self.is_descendant_of(widget_id, e.anchor_id) {
1255 direct.push((e.anchor_id, e.content_id, e.placement));
1256 } else if self.is_descendant_of(e.anchor_id, widget_id) {
1257 reverse.push((e.anchor_id, e.content_id, e.placement));
1258 }
1259 }
1260 let mut to_show = direct;
1261 if reverse.len() == 1 && !self.is_overlay_content_root(widget_id) {
1265 to_show.extend(reverse);
1266 }
1267
1268 let sim_now = self.sim_clock;
1277 let real_now = std::time::Instant::now();
1278 for (_anchor_id, content_id, _placement) in to_show {
1279 if let Some(entry) = self
1280 .tooltips
1281 .iter_mut()
1282 .find(|e| e.content_id == content_id)
1283 {
1284 entry.hover_start = Some(sim_now);
1285 entry.real_hover_start = Some(real_now);
1286 entry.hover_origin = None;
1287 entry.armed_by_focus = true;
1288 entry.armed_by_hold = false;
1289 }
1290 }
1291 }
1292
1293 pub(super) fn show_highlight_tooltip(
1303 &mut self,
1304 item_id: WidgetId,
1305 ops: &mut dyn crate::window::WindowOps,
1306 ) {
1307 self.clear_highlight_tooltip(&mut *ops);
1310
1311 let found = self.tooltips.iter().find_map(|e| {
1315 if e.overlay_id.is_none() && self.is_descendant_of(e.anchor_id, item_id) {
1316 Some((e.anchor_id, e.content_id, e.placement))
1317 } else {
1318 None
1319 }
1320 });
1321 let Some((anchor_id, content_id, placement)) = found else {
1322 return;
1323 };
1324
1325 let parent_overlay = self.overlay_ancestor_for_widget(item_id);
1326 self.arena.activate(content_id);
1327 let fade_duration = if self.prefers_reduced_motion {
1328 None
1329 } else {
1330 Some(self.theme.motion.duration_fast)
1331 };
1332 let mut request = crate::overlay::OverlayRequest::new(
1333 content_id,
1334 anchor_id,
1335 Self::tooltip_overlay_placement(placement),
1336 crate::overlay::DismissBehavior::Manual,
1337 );
1338 request.parent_overlay = parent_overlay;
1339 request.fade_duration = fade_duration;
1340 let oid = self.show_overlay(request);
1341 let real_now = std::time::Instant::now();
1342 let sim_now = self.sim_clock;
1343 if let Some(entry) = self
1344 .tooltips
1345 .iter_mut()
1346 .find(|e| e.content_id == content_id)
1347 {
1348 entry.overlay_id = Some(oid);
1349 entry.shown_at_sim = Some(sim_now);
1350 entry.shown_at_real = Some(real_now);
1351 if let Some(sink) = entry.shown_at_sink.as_ref() {
1352 sink.set(Some(real_now));
1353 }
1354 }
1355 self.highlight_tooltip = Some((oid, content_id));
1356 }
1357
1358 pub(super) fn clear_highlight_tooltip(&mut self, ops: &mut dyn crate::window::WindowOps) {
1363 let Some((oid, content_id)) = self.highlight_tooltip.take() else {
1364 return;
1365 };
1366 if self.overlay_manager.stack.iter().any(|o| o.id == oid) {
1367 let dismissed = self.overlay_manager.dismiss(oid);
1368 self.dormant_dismissed_content(&dismissed, &mut *ops);
1369 }
1370 if let Some(entry) = self
1371 .tooltips
1372 .iter_mut()
1373 .find(|e| e.content_id == content_id)
1374 {
1375 entry.overlay_id = None;
1376 entry.shown_at_sim = None;
1377 entry.shown_at_real = None;
1378 if let Some(sink) = entry.shown_at_sink.as_ref() {
1379 sink.set(None);
1380 }
1381 }
1382 }
1383
1384 pub(super) fn tooltip_focus_leave_outside(
1395 &mut self,
1396 new_focus: Option<WidgetId>,
1397 ops: &mut dyn crate::window::WindowOps,
1398 ) {
1399 let to_dismiss: Vec<crate::overlay::OverlayId> = self
1400 .tooltips
1401 .iter()
1402 .filter(|e| e.promoted_by_focus && e.overlay_id.is_some())
1403 .filter(|e| {
1404 let in_scope = new_focus
1405 .map(|nf| {
1406 self.is_descendant_of(nf, e.anchor_id)
1411 || self.is_descendant_of(e.anchor_id, nf)
1412 || self.is_descendant_of(nf, e.content_id)
1413 })
1414 .unwrap_or(false);
1415 !in_scope
1416 })
1417 .filter_map(|e| e.overlay_id)
1418 .collect();
1419
1420 for oid in to_dismiss {
1421 let dismissed = self.overlay_manager.dismiss(oid);
1422 self.dormant_dismissed_content(&dismissed, &mut *ops);
1423 }
1424
1425 for e in &mut self.tooltips {
1428 if e.armed_by_focus && e.overlay_id.is_none() {
1429 e.hover_start = None;
1430 e.real_hover_start = None;
1431 e.armed_by_focus = false;
1432 }
1433 }
1434
1435 let served: Vec<usize> = self
1439 .tooltips
1440 .iter()
1441 .enumerate()
1442 .filter(|(_, e)| e.suppressed_until_focus_leaves)
1443 .filter(|(_, e)| {
1444 let still_inside = new_focus
1445 .map(|nf| {
1446 self.is_descendant_of(nf, e.anchor_id)
1447 || self.is_descendant_of(nf, e.content_id)
1448 })
1449 .unwrap_or(false);
1450 !still_inside
1451 })
1452 .map(|(i, _)| i)
1453 .collect();
1454 for i in served {
1455 self.tooltips[i].suppressed_until_focus_leaves = false;
1456 }
1457 }
1458
1459 fn tooltip_hover_targets_anchor(&self, widget_id: WidgetId, anchor_id: WidgetId) -> bool {
1479 if !self.is_descendant_of(widget_id, anchor_id) {
1480 return false;
1481 }
1482 if let Some(overlay_id) = self.overlay_ancestor_for_widget(widget_id)
1483 && let Some(content_id) = self
1484 .overlay_manager
1485 .overlay(overlay_id)
1486 .map(|o| o.content_id)
1487 && !self.is_descendant_of(anchor_id, content_id)
1488 {
1489 return false;
1490 }
1491 true
1492 }
1493
1494 pub(super) fn tooltip_index_for(&self, widget_id: WidgetId) -> Option<usize> {
1512 self.tooltips
1513 .iter()
1514 .enumerate()
1515 .filter(|(_, entry)| self.tooltip_hover_targets_anchor(widget_id, entry.anchor_id))
1516 .filter_map(|(index, entry)| {
1517 self.ancestor_distance(widget_id, entry.anchor_id)
1518 .map(|depth| (depth, index))
1519 })
1520 .min()
1521 .map(|(_, index)| index)
1522 }
1523
1524 pub(super) fn tooltip_pointer_enter(&mut self, widget_id: WidgetId) {
1525 let Some(index) = self.tooltip_index_for(widget_id) else {
1526 return;
1527 };
1528 if self.tooltips[index].overlay_id.is_some() {
1530 return;
1531 }
1532 self.tooltips[index].hover_start = Some(self.sim_clock);
1533 self.tooltips[index].real_hover_start = Some(std::time::Instant::now());
1534 self.tooltips[index].hover_origin = self.hover_owner_position();
1535 self.tooltips[index].armed_by_focus = false;
1536 self.tooltips[index].armed_by_hold = false;
1537 self.arena.mark_needs_paint(self.tooltips[index].anchor_id);
1538 }
1539
1540 fn ancestor_distance(&self, widget_id: WidgetId, ancestor: WidgetId) -> Option<usize> {
1543 let mut current = widget_id;
1544 let mut depth = 0usize;
1545 loop {
1546 if current == ancestor {
1547 return Some(depth);
1548 }
1549 current = self.arena.parent(current)?;
1550 depth += 1;
1551 }
1552 }
1553
1554 pub(super) fn tooltip_pointer_press(&mut self, at: Option<teksilo_canvas::Point>) {
1569 let mut to_dismiss = Vec::new();
1570 let candidates: Vec<(usize, Option<crate::overlay::OverlayId>)> = self
1571 .tooltips
1572 .iter()
1573 .enumerate()
1574 .map(|(i, e)| (i, if e.is_sticky { None } else { e.overlay_id }))
1575 .collect();
1576 for (index, overlay_id) in candidates {
1577 let inside = match (overlay_id, at) {
1582 (Some(oid), Some(pos)) => self
1583 .overlay_content_bounds(oid)
1584 .is_some_and(|rect| rect.contains(pos)),
1585 _ => false,
1586 };
1587 if inside {
1588 continue;
1589 }
1590 let entry = &mut self.tooltips[index];
1591 entry.hover_start = None;
1592 entry.real_hover_start = None;
1593 entry.hover_origin = None;
1594 if let Some(oid) = overlay_id {
1595 to_dismiss.push(oid);
1596 }
1597 }
1598 for overlay_id in to_dismiss {
1599 self.dismiss_overlay(overlay_id);
1600 }
1601 }
1602
1603 pub(super) fn tooltip_escape_pressed(&mut self) {
1625 self.tooltip_pointer_press(None);
1628 }
1629
1630 pub(super) fn inert_overlays_escape_pressed(&mut self, ops: &mut dyn crate::window::WindowOps) {
1641 let dismissed = self.overlay_manager.dismiss_inert_on_escape();
1642 if !dismissed.is_empty() {
1643 self.dormant_dismissed_content(&dismissed, &mut *ops);
1644 self.a11y_dirty = true;
1645 }
1646 }
1647
1648 pub(super) fn tooltip_window_deactivated(&mut self) {
1655 self.tooltip_pointer_press(None);
1658 }
1659
1660 pub(crate) fn tooltip_cancel_pending_dwell(&mut self) {
1668 for entry in &mut self.tooltips {
1669 if entry.overlay_id.is_none() {
1670 entry.hover_start = None;
1671 entry.real_hover_start = None;
1672 entry.hover_origin = None;
1673 }
1674 }
1675 }
1676
1677 pub(super) fn tooltip_pointer_moved(
1681 &mut self,
1682 widget_id: WidgetId,
1683 position: teksilo_canvas::Point,
1684 ) {
1685 let matching: Vec<usize> = self
1686 .tooltips
1687 .iter()
1688 .enumerate()
1689 .filter(|(_, entry)| {
1690 entry.overlay_id.is_none()
1691 && entry.hover_start.is_some()
1692 && !entry.armed_by_focus
1693 && self.tooltip_hover_targets_anchor(widget_id, entry.anchor_id)
1694 })
1695 .map(|(index, _)| index)
1696 .collect();
1697 if matching.is_empty() {
1698 return;
1699 }
1700 let now = self.sim_clock;
1701 let real_now = std::time::Instant::now();
1702 let slop = super::TOOLTIP_STATIONARY_SLOP;
1703 let slop_sq = slop * slop;
1704 for index in matching {
1705 let origin = match self.tooltips[index].hover_origin {
1706 Some(o) => o,
1707 None => {
1708 self.tooltips[index].hover_origin = Some(position);
1712 continue;
1713 }
1714 };
1715 let dx = position.x - origin.x;
1716 let dy = position.y - origin.y;
1717 if dx * dx + dy * dy <= slop_sq {
1718 continue;
1719 }
1720 self.tooltips[index].hover_start = Some(now);
1722 self.tooltips[index].real_hover_start = Some(real_now);
1723 self.tooltips[index].hover_origin = Some(position);
1724 }
1725 }
1726
1727 pub fn promote_tooltip_to_sticky(&mut self, content_id: WidgetId) {
1745 let Some(entry) = self
1746 .tooltips
1747 .iter_mut()
1748 .find(|entry| entry.content_id == content_id)
1749 else {
1750 return;
1751 };
1752 if entry.is_sticky {
1753 return;
1754 }
1755 entry.is_sticky = true;
1756 let overlay_id = entry.overlay_id;
1757 if let Some(overlay_id) = overlay_id {
1758 self.overlay_manager.set_dismiss(
1759 overlay_id,
1760 crate::overlay::DismissBehavior::EscapeOrClickOutside,
1761 );
1762 }
1763 }
1764
1765 pub(super) fn tooltip_pointer_leave(
1766 &mut self,
1767 widget_id: WidgetId,
1768 _ops: &mut dyn crate::window::WindowOps,
1769 ) {
1770 let matching: Vec<usize> = self
1771 .tooltips
1772 .iter()
1773 .enumerate()
1774 .filter(|(_, entry)| self.is_descendant_of(widget_id, entry.anchor_id))
1775 .map(|(index, _)| index)
1776 .collect();
1777 for index in matching {
1778 self.tooltips[index].hover_start = None;
1781 self.tooltips[index].real_hover_start = None;
1782 self.tooltips[index].hover_origin = None;
1783 }
1795 }
1796
1797 pub fn next_timer_deadline(&self) -> Option<std::time::Instant> {
1803 let now = std::time::Instant::now();
1804 let session_active = self.tooltip_session_active_real(now);
1805 let tooltip_deadline = self
1806 .tooltips
1807 .iter()
1808 .filter(|entry| entry.overlay_id.is_none())
1809 .filter_map(|entry| {
1810 let start = entry.real_hover_start?;
1811 let delay = self.effective_tooltip_delay(entry.delay, session_active);
1812 Some(start + delay)
1813 })
1814 .min();
1815
1816 let ref_time = self.last_frame_time.unwrap_or_else(std::time::Instant::now);
1837 let dwell_tooltip_deadline = self
1838 .tooltips
1839 .iter()
1840 .filter_map(|entry| {
1841 let sticky_after = entry.sticky_after?;
1842 let shown_at = entry.shown_at_real?;
1843 if entry.overlay_id.is_none() || entry.is_sticky {
1844 return None;
1845 }
1846 let dwell_step = sticky_after / super::TOOLTIP_DWELL_STEPS;
1847 if dwell_step.is_zero() {
1848 return None;
1849 }
1850 let elapsed = ref_time.saturating_duration_since(shown_at);
1851 if elapsed >= sticky_after {
1852 return None;
1853 }
1854 let steps_passed = (elapsed.as_millis() / dwell_step.as_millis()) as u32;
1857 let next_step_at = shown_at + dwell_step * (steps_passed + 1);
1858 Some(next_step_at.min(shown_at + sticky_after))
1859 })
1860 .min();
1861 let delayed_overlay_deadline = self
1862 .pending_delayed_overlays
1863 .iter()
1864 .map(|pending| pending.real_requested_at + pending.delay)
1865 .min();
1866 let auto_dismiss_deadline = self.overlay_manager.next_auto_dismiss_deadline();
1867 let pointer_leave_deadline = self.overlay_manager.next_pointer_leave_deadline();
1874 let fade_dismiss_deadline = self.overlay_manager.next_fade_dismiss_deadline();
1881 let animation_deadline = self
1882 .animation_scheduler
1883 .next_deadline(&self.arena, self.paint_epoch);
1884 let animated_quad_deadline = self
1889 .animated_quads
1890 .next_deadline(&self.arena, self.paint_epoch);
1891 let gesture_deadline = self.next_input_deadline();
1897 let wake_at_deadline = self.pending_wake_at.get();
1898 let frame_tick_deadline = self.frame_tick_deadline();
1904
1905 [
1906 tooltip_deadline,
1907 dwell_tooltip_deadline,
1908 delayed_overlay_deadline,
1909 auto_dismiss_deadline,
1910 pointer_leave_deadline,
1911 fade_dismiss_deadline,
1912 animation_deadline,
1913 animated_quad_deadline,
1914 gesture_deadline,
1915 wake_at_deadline,
1916 frame_tick_deadline,
1917 ]
1918 .into_iter()
1919 .flatten()
1920 .min()
1921 }
1922
1923 pub fn overlay_manager(&self) -> &crate::overlay::OverlayManager {
1924 &self.overlay_manager
1925 }
1926
1927 pub fn overlay_manager_mut(&mut self) -> &mut crate::overlay::OverlayManager {
1932 &mut self.overlay_manager
1933 }
1934
1935 pub fn active_overlays(&self) -> Vec<crate::overlay::OverlayId> {
1936 self.overlay_manager.active_ids()
1937 }
1938
1939 pub fn overlay_content_bounds(
1948 &self,
1949 id: crate::overlay::OverlayId,
1950 ) -> Option<teksilo_canvas::Rect> {
1951 self.overlay_manager
1952 .stack
1953 .iter()
1954 .find(|o| o.id == id)
1955 .map(|o| o.bounds)
1956 }
1957
1958 pub fn show_overlay(
1959 &mut self,
1960 request: crate::overlay::OverlayRequest,
1961 ) -> crate::overlay::OverlayId {
1962 let fade_duration = request.fade_duration;
1963 let content_id = request.content_id;
1964 let is_modal = request.placement.is_modal();
1965 let id = self.overlay_manager.show(request);
1966 self.cancel_pointers_for_modal(is_modal);
1967 self.a11y_dirty = true;
1975 if let Some(duration) = fade_duration {
1976 self.attach_overlay_fade(id, content_id, duration);
1977 }
1978 id
1979 }
1980
1981 pub fn show_overlay_in_band(
1991 &mut self,
1992 request: crate::overlay::OverlayRequest,
1993 band: crate::overlay::OverlayBand,
1994 ) -> crate::overlay::OverlayId {
1995 let fade_duration = request.fade_duration;
1996 let content_id = request.content_id;
1997 let is_modal = request.placement.is_modal();
1998 let id = self.overlay_manager.show_in_band(request, band);
1999 self.cancel_pointers_for_modal(is_modal);
2000 self.a11y_dirty = true;
2001 if let Some(duration) = fade_duration {
2002 self.attach_overlay_fade(id, content_id, duration);
2003 }
2004 id
2005 }
2006
2007 pub fn show_overlay_from_source(
2010 &mut self,
2011 source_widget: WidgetId,
2012 mut request: crate::overlay::OverlayRequest,
2013 ) -> crate::overlay::OverlayId {
2014 if request.parent_overlay.is_none() {
2015 request.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
2016 }
2017 if let Some(existing) = self.overlay_manager.find_by_content(request.content_id) {
2018 return existing;
2019 }
2020
2021 let fade_duration = request.fade_duration;
2022 let content_id = request.content_id;
2023 let is_modal = request.placement.is_modal();
2024 let current_focus = self.focused;
2025 let id = self.overlay_manager.show(request);
2026 self.cancel_pointers_for_modal(is_modal);
2027 self.a11y_dirty = true;
2028 if let Some(focus_id) = current_focus {
2029 self.overlay_manager.set_top_focus_restore(focus_id);
2030 }
2031 if let Some(duration) = fade_duration {
2032 self.attach_overlay_fade(id, content_id, duration);
2033 }
2034 id
2035 }
2036
2037 pub fn show_overlay_for(
2038 &mut self,
2039 request: crate::overlay::OverlayRequest,
2040 duration: std::time::Duration,
2041 ) -> crate::overlay::OverlayId {
2042 let fade_duration = request.fade_duration;
2043 let content_id = request.content_id;
2044 let is_modal = request.placement.is_modal();
2045 let id = self.overlay_manager.show_for(request, duration);
2046 self.cancel_pointers_for_modal(is_modal);
2047 self.overlay_manager.set_shown_at_sim(id, self.sim_clock);
2048 self.a11y_dirty = true;
2049 if let Some(fade) = fade_duration {
2050 self.attach_overlay_fade(id, content_id, fade);
2051 }
2052 id
2053 }
2054
2055 fn attach_overlay_fade(
2063 &mut self,
2064 overlay_id: crate::overlay::OverlayId,
2065 content_id: WidgetId,
2066 duration: std::time::Duration,
2067 ) {
2068 let opacity = crate::signal::Signal::<f32>::new_animated(0.0);
2069 self.register_animated_signal(&opacity, content_id);
2070 self.set_opacity(content_id, opacity.clone());
2071 if self.prefers_reduced_motion() {
2075 opacity.set(1.0);
2076 self.overlay_manager
2077 .attach_fade(overlay_id, opacity, std::time::Duration::ZERO);
2078 return;
2079 }
2080 let _ = opacity.try_animate_with_options(crate::animation::AnimationRequest {
2081 target: 1.0,
2082 duration,
2083 easing: teksilo_tokens::Easing::EaseOut,
2084 frame_interval: None,
2085 looping: false,
2086 epsilon: 0.0,
2087 max_duration: None,
2088 });
2089 self.overlay_manager
2090 .attach_fade(overlay_id, opacity, duration);
2091 }
2092
2093 pub fn dismiss_overlay(&mut self, id: crate::overlay::OverlayId) {
2098 let mut noop = crate::window::NoopWindowOps;
2099 self.dismiss_overlay_with_ops(id, &mut noop);
2100 }
2101
2102 pub fn dismiss_overlay_with_ops(
2105 &mut self,
2106 id: crate::overlay::OverlayId,
2107 ops: &mut dyn crate::window::WindowOps,
2108 ) {
2109 let dismissed = self.overlay_manager.dismiss(id);
2110 self.dormant_dismissed_content(&dismissed, &mut *ops);
2111 }
2112
2113 fn cancel_pointers_for_modal(&mut self, is_modal: bool) {
2124 if !is_modal {
2125 return;
2126 }
2127 let mut noop = crate::window::NoopWindowOps;
2128 self.cancel_all_pointers(crate::pointer::CancelReason::ModalOpened, &mut noop);
2129 }
2130
2131 pub(super) fn run_pending_dismiss_callbacks(&mut self, ops: &mut dyn crate::window::WindowOps) {
2141 if self.draining_dismiss.replace(true) {
2146 return;
2147 }
2148 for _ in 0..8 {
2152 let pending = self.overlay_manager.take_pending_dismiss();
2153 if pending.is_empty() {
2154 break;
2155 }
2156 for (cb, reason) in pending {
2157 self.run_with_event_context(&mut *ops, |ctx| cb(reason, ctx));
2158 }
2159 }
2160 self.draining_dismiss.set(false);
2161 }
2162
2163 pub(super) fn dormant_dismissed_content(
2164 &mut self,
2165 content_ids: &[WidgetId],
2166 ops: &mut dyn crate::window::WindowOps,
2167 ) {
2168 self.run_pending_dismiss_callbacks(&mut *ops);
2172 let mut any_tooltip_dismissed = false;
2178 for &id in content_ids {
2179 if let Some(entry) = self.tooltips.iter_mut().find(|e| e.content_id == id) {
2180 entry.suppressed_until_focus_leaves = entry.promoted_by_focus;
2187 entry.overlay_id = None;
2188 entry.is_sticky = false;
2189 entry.hover_start = None;
2190 entry.real_hover_start = None;
2191 entry.hover_origin = None;
2192 entry.shown_at_sim = None;
2193 entry.shown_at_real = None;
2194 entry.promoted_by_focus = false;
2195 entry.armed_by_focus = false;
2196 entry.armed_by_hold = false;
2197 if let Some(sink) = entry.shown_at_sink.as_ref() {
2198 sink.set(None);
2199 }
2200 any_tooltip_dismissed = true;
2201 }
2202 }
2203 if any_tooltip_dismissed {
2204 let grace = super::TOOLTIP_SESSION_GRACE;
2205 self.tooltip_session_until_sim = Some(self.sim_clock + grace);
2206 self.tooltip_session_until_real = Some(std::time::Instant::now() + grace);
2207 }
2208 for &id in content_ids {
2209 let focused_in_subtree = self
2210 .focused
2211 .filter(|focused| self.is_descendant_of(*focused, id));
2212 let hovered_in_subtree = self
2213 .hovered_id()
2214 .filter(|hovered| self.is_descendant_of(*hovered, id));
2215
2216 if let Some(focused) = focused_in_subtree {
2217 self.dispatch_to_widget(focused, &WidgetEvent::FocusLost, &mut *ops);
2218 if self
2219 .focused
2220 .is_some_and(|current| self.is_descendant_of(current, id))
2221 {
2222 let old = self.focused;
2223 self.set_focused(None);
2224 self.focus_origin = None;
2225 self.update_focus_within_signals(old, None);
2226 self.update_view_focus_signals(old, None);
2227 }
2228 }
2229
2230 self.cancel_pointers_in_subtree(
2236 id,
2237 crate::pointer::CancelReason::OverlayDismissed,
2238 &mut *ops,
2239 );
2240 let _parked = self.arena.set_dormant(id);
2241
2242 if hovered_in_subtree.is_some() {
2243 let old = self.hovered_id();
2244 self.set_hovered(None);
2245 self.update_hover_within_signals(old, None);
2246 }
2247 }
2248 if !content_ids.is_empty() {
2249 self.cached_frame = None;
2250 self.a11y_dirty = true;
2251 }
2252 }
2253
2254 pub fn is_visible(&self, id: WidgetId) -> bool {
2255 self.arena.is_active(id)
2256 }
2257}
2258
2259#[cfg(test)]
2260mod tests {
2261 use super::*;
2262 use crate::test_widgets::FillWidget;
2263
2264 #[test]
2265 fn is_visible_reflects_dormancy() {
2266 let mut tree = WidgetTree::new();
2267 let widget = tree.add(FillWidget::new());
2268 tree.layout(SizeProposal::exact(100.0, 50.0));
2269
2270 assert!(tree.is_visible(widget));
2271 tree.set_dormant(widget);
2272 assert!(!tree.is_visible(widget));
2273 tree.activate(widget);
2274 assert!(tree.is_visible(widget));
2275 }
2276
2277 #[test]
2278 fn frame_tick_deadline_caps_per_frame_effects_at_60hz() {
2279 let mut tree = WidgetTree::new();
2280 assert!(tree.frame_tick_deadline().is_none());
2282
2283 tree.request_frame();
2286 let t0 = std::time::Instant::now();
2288 tree.last_frame_time = Some(t0);
2289
2290 let deadline = tree
2291 .frame_tick_deadline()
2292 .expect("an armed per-frame effect must publish a deadline");
2293 let interval = deadline.saturating_duration_since(t0);
2294 assert!(
2297 interval >= std::time::Duration::from_micros(16_000)
2298 && interval <= std::time::Duration::from_micros(17_500),
2299 "per-frame effects must pace at ~60 Hz (got {interval:?})"
2300 );
2301
2302 assert_eq!(
2305 tree.next_timer_deadline(),
2306 Some(deadline),
2307 "frame-tick deadline must be surfaced by next_timer_deadline"
2308 );
2309 }
2310
2311 #[test]
2312 fn throttled_subscriber_stretches_deadline_but_per_frame_wins_min() {
2313 use crate::test_widgets::{FillWidget, StackWidget};
2314 let mut tree = WidgetTree::new();
2315 let w = tree.add(FillWidget::new());
2316 let w2 = tree.add(FillWidget::new());
2317 tree.add(StackWidget::new().child(w).child(w2));
2318 let _throttled =
2320 tree.subscribe_frame_tick_throttled(w, std::time::Duration::from_millis(1500));
2321 tree.layout(SizeProposal::exact(200.0, 100.0));
2325 let _ = tree.render();
2326 assert!(
2327 !tree.frame_tick_requested.get(),
2328 "precondition: nothing but the subscriber wants this frame"
2329 );
2330 let t0 = tree.last_frame_time.expect("a frame has run");
2331
2332 let d = tree.frame_tick_deadline().expect("armed");
2334 let dt = d.saturating_duration_since(t0);
2335 assert!(
2336 dt >= std::time::Duration::from_millis(1490)
2337 && dt <= std::time::Duration::from_millis(1510),
2338 "a lone throttled subscriber must pace at its interval (~1.5 s), got {dt:?}"
2339 );
2340
2341 let _per_frame = tree.subscribe_frame_tick(w2);
2346 let d2 = tree.frame_tick_deadline().expect("armed");
2347 let dt2 = d2.saturating_duration_since(t0);
2348 assert!(
2349 dt2 <= std::time::Duration::from_millis(20),
2350 "a per-frame subscriber must pull the shared deadline to 60 Hz, got {dt2:?}"
2351 );
2352 }
2353
2354 #[test]
2360 fn a_raw_frame_request_is_not_paced_by_a_throttled_subscriber() {
2361 use crate::test_widgets::FillWidget;
2362 const NORMAL_FRAME: std::time::Duration = std::time::Duration::from_micros(17_500);
2363 const CLOCK: std::time::Duration = std::time::Duration::from_secs(60);
2364 fn frame(tree: &mut WidgetTree) {
2365 tree.layout(SizeProposal::exact(200.0, 100.0));
2366 let _ = tree.render();
2367 }
2368 fn wait(tree: &WidgetTree) -> std::time::Duration {
2369 let deadline = tree.frame_tick_deadline().expect("armed");
2370 deadline.saturating_duration_since(tree.last_frame_time.expect("a frame has run"))
2371 }
2372
2373 let mut tree = WidgetTree::new();
2374 let clock = tree.add(FillWidget::new());
2375 let _tick = tree.subscribe_frame_tick_throttled(clock, CLOCK);
2376 frame(&mut tree);
2377 assert!(
2378 wait(&tree) >= CLOCK - NORMAL_FRAME,
2379 "precondition: the painted clock paces the loop at its own interval"
2380 );
2381
2382 for request in [
2385 (|tree: &WidgetTree| tree.request_frame()) as fn(&WidgetTree),
2386 |tree: &WidgetTree| tree.frame_request_handle().set(true),
2387 ] {
2388 request(&tree);
2389 assert!(
2390 wait(&tree) <= NORMAL_FRAME,
2391 "a requested frame must not wait on the clock (waited {:?})",
2392 wait(&tree)
2393 );
2394 assert_eq!(tree.next_timer_deadline(), tree.frame_tick_deadline());
2395
2396 frame(&mut tree);
2397 assert!(
2398 wait(&tree) >= CLOCK - NORMAL_FRAME,
2399 "once the request is served the clock paces alone again, rather than the loop \
2400 free-running at 60 Hz (waited {:?})",
2401 wait(&tree)
2402 );
2403 }
2404 }
2405
2406 #[test]
2415 fn the_subscriber_arm_is_spent_by_its_tick_and_ends_with_its_owner() {
2416 use crate::test_widgets::StackWidget;
2417 fn frame(tree: &mut WidgetTree) {
2418 tree.layout(SizeProposal::exact(200.0, 100.0));
2419 let _ = tree.render();
2420 }
2421
2422 let mut tree = WidgetTree::new();
2423 let owner = tree.add(FillWidget::new());
2424 let sibling = tree.add(FillWidget::new());
2425 tree.add(StackWidget::new().child(owner).child(sibling));
2426 let ticks = std::rc::Rc::new(std::cell::Cell::new(0_u32));
2427 let _count = tree.frame_tick().observe({
2428 let ticks = ticks.clone();
2429 move |_| ticks.set(ticks.get() + 1)
2430 });
2431 let _sub = tree.subscribe_frame_tick(owner);
2432
2433 frame(&mut tree);
2434 assert!(
2435 tree.frame_requested(),
2436 "a render that painted the subscriber arms the next tick"
2437 );
2438 assert!(
2439 !tree.frame_tick_requested.get(),
2440 "precondition: armed by the subscribers' flag, not by a raw request"
2441 );
2442
2443 let before = ticks.get();
2445 tree.layout(SizeProposal::exact(200.0, 100.0));
2446 assert_eq!(
2447 ticks.get(),
2448 before + 1,
2449 "the armed tick fires on the next layout"
2450 );
2451 assert!(
2452 !tree.frame_requested(),
2453 "the layout that fired the tick has spent the arm"
2454 );
2455 let _ = tree.render();
2456 assert!(
2457 tree.frame_requested(),
2458 "the owner painted again and re-armed"
2459 );
2460
2461 let before = ticks.get();
2463 tree.advance_time(std::time::Duration::from_millis(16));
2464 assert_eq!(ticks.get(), before + 1, "advance_time fires an armed tick");
2465 assert!(
2466 !tree.frame_requested(),
2467 "advance_time spends the arm as layout does"
2468 );
2469 tree.resume_real_time();
2470
2471 frame(&mut tree);
2474 tree.set_dormant(owner);
2475 assert!(
2476 tree.frame_tick_deadline().is_some(),
2477 "the tick armed before the owner was parked is still due"
2478 );
2479 let before = ticks.get();
2480 frame(&mut tree);
2481 assert_eq!(ticks.get(), before + 1);
2482 assert!(
2483 !tree.frame_requested(),
2484 "a parked subscriber must not re-arm the tick"
2485 );
2486 assert_eq!(
2487 tree.next_timer_deadline(),
2488 None,
2489 "with its only subscriber parked the loop sleeps"
2490 );
2491 }
2492
2493 #[test]
2494 fn show_and_dismiss_overlay() {
2495 let mut tree = WidgetTree::new();
2496 let anchor = tree.add(FillWidget::new());
2497 let content = tree.add(FillWidget::new().label("Overlay"));
2498 tree.layout(SizeProposal::exact(200.0, 100.0));
2499
2500 assert!(tree.active_overlays().is_empty());
2501
2502 let id = tree.show_overlay(crate::overlay::OverlayRequest::new(
2503 content,
2504 anchor,
2505 crate::overlay::OverlayPlacement::Below,
2506 crate::overlay::DismissBehavior::Manual,
2507 ));
2508
2509 assert_eq!(tree.active_overlays().len(), 1);
2510
2511 tree.dismiss_overlay(id);
2512 assert!(tree.active_overlays().is_empty());
2513 assert!(!tree.is_visible(content));
2514 }
2515
2516 #[test]
2521 fn modal_centered_overlay_is_a_host_surface() {
2522 let mut tree = WidgetTree::new();
2523 let anchor = tree.add(FillWidget::new());
2524 let modal_content = tree.add(FillWidget::new().label("Modal"));
2525 let dropdown_content = tree.add(FillWidget::new().label("Dropdown"));
2526 tree.layout(SizeProposal::exact(200.0, 100.0));
2527
2528 let modal = tree.show_overlay(crate::overlay::OverlayRequest::new(
2529 modal_content,
2530 anchor,
2531 crate::overlay::OverlayPlacement::Centered,
2532 crate::overlay::DismissBehavior::EscapeOrClickOutside,
2533 ));
2534 let dropdown = tree.show_overlay(
2535 crate::overlay::OverlayRequest::new(
2536 dropdown_content,
2537 anchor,
2538 crate::overlay::OverlayPlacement::Below,
2539 crate::overlay::DismissBehavior::EscapeOrClickOutside,
2540 )
2541 .parent_overlay(modal),
2542 );
2543
2544 assert!(
2545 tree.overlay_is_host_surface(modal),
2546 "a Centered modal overlay must be treated as a host surface"
2547 );
2548 assert!(
2549 !tree.overlay_is_host_surface(dropdown),
2550 "a plain dropdown overlay is not a host surface"
2551 );
2552 }
2553
2554 #[test]
2555 fn escape_dismisses_topmost_overlay() {
2556 let mut tree = WidgetTree::new();
2557 let anchor = tree.add(FillWidget::new().focusable());
2558 let content = tree.add(FillWidget::new());
2559 tree.layout(SizeProposal::exact(200.0, 100.0));
2560 tree.focus(anchor);
2561
2562 tree.show_overlay(crate::overlay::OverlayRequest::new(
2563 content,
2564 anchor,
2565 crate::overlay::OverlayPlacement::Below,
2566 crate::overlay::DismissBehavior::EscapeOrClickOutside,
2567 ));
2568
2569 assert_eq!(tree.active_overlays().len(), 1);
2570
2571 tree.press_key(Key::Escape, Modifiers::NONE);
2572 assert!(tree.active_overlays().is_empty());
2573 assert!(!tree.is_visible(content));
2574 }
2575
2576 fn show_two_nested_overlays(tree: &mut WidgetTree) -> (WidgetId, WidgetId) {
2579 let anchor = tree.add(FillWidget::new());
2580 let c1 = tree.add(FillWidget::new());
2581 let c2 = tree.add(FillWidget::new());
2582 tree.layout(SizeProposal::exact(200.0, 100.0));
2583 let o1 = tree.show_overlay(crate::overlay::OverlayRequest::new(
2584 c1,
2585 anchor,
2586 crate::overlay::OverlayPlacement::Below,
2587 crate::overlay::DismissBehavior::Manual,
2588 ));
2589 tree.show_overlay(
2590 crate::overlay::OverlayRequest::new(
2591 c2,
2592 c1,
2593 crate::overlay::OverlayPlacement::Below,
2594 crate::overlay::DismissBehavior::Manual,
2595 )
2596 .parent_overlay(o1),
2597 );
2598 (c1, c2)
2599 }
2600
2601 #[test]
2602 fn nested_overlay_back_key_dismisses_with_arrow_left_under_ltr() {
2603 let mut tree = WidgetTree::new();
2604 let _ = show_two_nested_overlays(&mut tree);
2605 assert_eq!(tree.active_overlays().len(), 2);
2606
2607 tree.press_key(Key::ArrowRight, Modifiers::NONE);
2609 assert_eq!(tree.active_overlays().len(), 2);
2610
2611 tree.press_key(Key::ArrowLeft, Modifiers::NONE);
2613 assert_eq!(tree.active_overlays().len(), 1);
2614 }
2615
2616 #[test]
2617 fn nested_overlay_back_key_flips_to_arrow_right_under_rtl() {
2618 let mut tree = WidgetTree::new();
2619 tree.set_layout_direction(crate::environment::LayoutDirection::RightToLeft);
2620 let _ = show_two_nested_overlays(&mut tree);
2621 assert_eq!(tree.active_overlays().len(), 2);
2622
2623 tree.press_key(Key::ArrowLeft, Modifiers::NONE);
2626 assert_eq!(tree.active_overlays().len(), 2);
2627
2628 tree.press_key(Key::ArrowRight, Modifiers::NONE);
2630 assert_eq!(tree.active_overlays().len(), 1);
2631 }
2632
2633 fn show_inert_overlay(tree: &mut WidgetTree, anchor: WidgetId) -> WidgetId {
2635 let content = tree.add(FillWidget::new());
2636 tree.layout(SizeProposal::exact(200.0, 100.0));
2637 tree.show_overlay(
2638 crate::overlay::OverlayRequest::new(
2639 content,
2640 anchor,
2641 crate::overlay::OverlayPlacement::Below,
2642 crate::overlay::DismissBehavior::EscapeKey,
2643 )
2644 .inert(),
2645 );
2646 content
2647 }
2648
2649 #[test]
2654 fn back_key_does_not_count_an_inert_overlay_as_a_menu_level() {
2655 for inert_first in [true, false] {
2656 let mut tree = WidgetTree::new();
2657 let anchor = tree.add(FillWidget::new());
2658 let menu = tree.add(FillWidget::new());
2659 tree.layout(SizeProposal::exact(200.0, 100.0));
2660 let show_menu = |tree: &mut WidgetTree| {
2661 tree.show_overlay(crate::overlay::OverlayRequest::new(
2662 menu,
2663 anchor,
2664 crate::overlay::OverlayPlacement::Below,
2665 crate::overlay::DismissBehavior::Manual,
2666 ));
2667 };
2668 let inert = if inert_first {
2669 let inert = show_inert_overlay(&mut tree, anchor);
2670 show_menu(&mut tree);
2671 inert
2672 } else {
2673 show_menu(&mut tree);
2674 show_inert_overlay(&mut tree, anchor)
2675 };
2676 assert_eq!(tree.active_overlays().len(), 2);
2677
2678 tree.press_key(Key::ArrowLeft, Modifiers::NONE);
2679 assert_eq!(
2680 tree.active_overlays().len(),
2681 2,
2682 "inert shown first: {inert_first}: one menu is not a cascade"
2683 );
2684 assert!(tree.is_visible(menu) && tree.is_visible(inert));
2685 }
2686 }
2687
2688 #[test]
2691 fn back_key_closes_the_submenu_beneath_an_inert_overlay() {
2692 let mut tree = WidgetTree::new();
2693 let (parent, submenu) = show_two_nested_overlays(&mut tree);
2694 let inert = show_inert_overlay(&mut tree, parent);
2695 assert_eq!(tree.active_overlays().len(), 3);
2696
2697 tree.press_key(Key::ArrowLeft, Modifiers::NONE);
2698 assert!(!tree.is_visible(submenu), "the submenu closes");
2699 assert!(tree.is_visible(parent), "its parent stays open");
2700 assert!(
2701 tree.is_visible(inert),
2702 "the inert overlay is not what closes"
2703 );
2704 }
2705
2706 #[derive(Debug)]
2709 struct KeyProbe {
2710 seen: std::rc::Rc<std::cell::RefCell<Vec<Key>>>,
2711 }
2712
2713 impl Widget for KeyProbe {
2714 fn layout_response(
2715 &self,
2716 proposal: SizeProposal,
2717 _ctx: &crate::widget::LayoutContext,
2718 ) -> crate::widget::LayoutResponse {
2719 proposal.resolve(0.0, 0.0).into()
2720 }
2721
2722 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
2723 let seen = self.seen.clone();
2724 ctx.apply_self_handlers(
2725 crate::widget_builder::HandlerSet::new()
2726 .focusable(true)
2727 .on_key(move |event, _ctx| match event {
2728 WidgetEvent::KeyDown { key, .. } => {
2729 seen.borrow_mut().push(*key);
2730 crate::event::EventResponse::Handled
2731 }
2732 _ => crate::event::EventResponse::Ignored,
2733 }),
2734 );
2735 Vec::new()
2736 }
2737 }
2738
2739 #[test]
2751 fn back_key_ignores_text_affordance_overlays_and_reaches_the_editor() {
2752 let mut tree = WidgetTree::new();
2753 let seen = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
2754 let editor = tree.add(KeyProbe { seen: seen.clone() });
2755 let handles_a = tree.add(FillWidget::new());
2756 let handles_b = tree.add(FillWidget::new());
2757 tree.layout(SizeProposal::exact(200.0, 100.0));
2758 tree.focus(editor);
2759
2760 for content in [handles_a, handles_b] {
2762 tree.show_overlay_in_band(
2763 crate::overlay::OverlayRequest::new(
2764 content,
2765 editor,
2766 crate::overlay::OverlayPlacement::FullViewport,
2767 crate::overlay::DismissBehavior::Manual,
2768 ),
2769 crate::overlay::OverlayBand::TextAffordance,
2770 );
2771 }
2772 assert_eq!(tree.active_overlays().len(), 2);
2773
2774 tree.press_key(Key::ArrowLeft, Modifiers::NONE);
2775
2776 assert_eq!(
2777 tree.active_overlays().len(),
2778 2,
2779 "a text affordance is not a menu cascade level — the back key must \
2780 leave both hosts standing"
2781 );
2782 assert_eq!(
2783 seen.borrow().as_slice(),
2784 &[Key::ArrowLeft],
2785 "ArrowLeft must reach the focused editor; swallowing it is the bug \
2786 that left the caret unable to step left in any dual-pane tab"
2787 );
2788 }
2789
2790 #[test]
2794 fn back_key_still_closes_a_submenu_over_a_mounted_text_affordance() {
2795 let mut tree = WidgetTree::new();
2796 let affordance = tree.add(FillWidget::new());
2797 let anchor = tree.add(FillWidget::new());
2798 let menu = tree.add(FillWidget::new());
2799 let submenu = tree.add(FillWidget::new());
2800 tree.layout(SizeProposal::exact(200.0, 100.0));
2801 tree.show_overlay_in_band(
2802 crate::overlay::OverlayRequest::new(
2803 affordance,
2804 anchor,
2805 crate::overlay::OverlayPlacement::FullViewport,
2806 crate::overlay::DismissBehavior::Manual,
2807 ),
2808 crate::overlay::OverlayBand::TextAffordance,
2809 );
2810 let parent = tree.show_overlay(crate::overlay::OverlayRequest::new(
2811 menu,
2812 anchor,
2813 crate::overlay::OverlayPlacement::Below,
2814 crate::overlay::DismissBehavior::Manual,
2815 ));
2816 tree.show_overlay(
2817 crate::overlay::OverlayRequest::new(
2818 submenu,
2819 menu,
2820 crate::overlay::OverlayPlacement::Below,
2821 crate::overlay::DismissBehavior::Manual,
2822 )
2823 .parent_overlay(parent),
2824 );
2825 assert_eq!(tree.active_overlays().len(), 3);
2826
2827 tree.press_key(Key::ArrowLeft, Modifiers::NONE);
2828 assert_eq!(
2829 tree.active_overlays().len(),
2830 2,
2831 "the submenu must still close: the affordance host narrows the \
2832 count, it does not disarm the back key"
2833 );
2834 }
2835
2836 #[test]
2841 fn back_key_does_not_dismiss_a_dialog_on_top_of_a_modal() {
2842 let mut tree = WidgetTree::new();
2843 let anchor = tree.add(FillWidget::new());
2844 let scrim1 = tree.add(FillWidget::new());
2845 let scrim2 = tree.add(FillWidget::new());
2846 let dialog = tree.add(FillWidget::new());
2847 tree.layout(SizeProposal::exact(200.0, 100.0));
2848 for c in [scrim1, scrim2] {
2850 tree.show_overlay(crate::overlay::OverlayRequest::new(
2851 c,
2852 anchor,
2853 crate::overlay::OverlayPlacement::Below,
2854 crate::overlay::DismissBehavior::Manual,
2855 ));
2856 }
2857 tree.show_overlay(crate::overlay::OverlayRequest::new(
2859 dialog,
2860 anchor,
2861 crate::overlay::OverlayPlacement::Centered,
2862 crate::overlay::DismissBehavior::Manual,
2863 ));
2864 assert_eq!(tree.active_overlays().len(), 3);
2865
2866 tree.press_key(Key::ArrowLeft, Modifiers::NONE);
2869 assert_eq!(
2870 tree.active_overlays().len(),
2871 3,
2872 "the back key must not close a dialog sitting on top of a modal"
2873 );
2874 }
2875
2876 #[test]
2877 fn escape_does_not_dismiss_manual_overlay() {
2878 let mut tree = WidgetTree::new();
2879 let anchor = tree.add(FillWidget::new().focusable());
2880 let content = tree.add(FillWidget::new());
2881 tree.layout(SizeProposal::exact(200.0, 100.0));
2882 tree.focus(anchor);
2883
2884 tree.show_overlay(crate::overlay::OverlayRequest::new(
2885 content,
2886 anchor,
2887 crate::overlay::OverlayPlacement::Below,
2888 crate::overlay::DismissBehavior::Manual,
2889 ));
2890
2891 assert_eq!(tree.active_overlays().len(), 1);
2892
2893 tree.press_key(Key::Escape, Modifiers::NONE);
2894 assert_eq!(tree.active_overlays().len(), 1);
2896 }
2897
2898 #[test]
2899 fn click_outside_dismisses_overlay() {
2900 let mut tree = WidgetTree::new();
2901 let anchor = tree.add(FillWidget::new());
2902 let content = tree.add(FillWidget::new());
2903 tree.layout(SizeProposal::exact(200.0, 100.0));
2904
2905 let overlay = tree.show_overlay(crate::overlay::OverlayRequest::new(
2906 content,
2907 anchor,
2908 crate::overlay::OverlayPlacement::Below,
2909 crate::overlay::DismissBehavior::ClickOutside,
2910 ));
2911
2912 tree.overlay_manager
2913 .set_content_bounds(overlay, teksilo_canvas::Size::new(100.0, 50.0));
2914
2915 assert_eq!(tree.active_overlays().len(), 1);
2916
2917 tree.dispatch_event(WidgetEvent::pointer_down(
2918 Point::new(500.0, 500.0),
2919 PointerButton::Primary,
2920 Modifiers::NONE,
2921 ));
2922 assert!(tree.active_overlays().is_empty());
2923 assert!(!tree.is_visible(content));
2924 }
2925
2926 #[test]
2927 fn cascade_dismissal() {
2928 let mut tree = WidgetTree::new();
2929 let anchor = tree.add(FillWidget::new());
2930 let content_a = tree.add(FillWidget::new());
2931 let content_b = tree.add(FillWidget::new());
2932 tree.layout(SizeProposal::exact(200.0, 100.0));
2933
2934 let parent = tree.show_overlay(crate::overlay::OverlayRequest::new(
2935 content_a,
2936 anchor,
2937 crate::overlay::OverlayPlacement::Below,
2938 crate::overlay::DismissBehavior::Manual,
2939 ));
2940 tree.show_overlay(
2941 crate::overlay::OverlayRequest::new(
2942 content_b,
2943 content_a,
2944 crate::overlay::OverlayPlacement::TrailingEdge,
2945 crate::overlay::DismissBehavior::Manual,
2946 )
2947 .parent_overlay(parent),
2948 );
2949
2950 assert_eq!(tree.active_overlays().len(), 2);
2951
2952 tree.dismiss_overlay(parent);
2953 assert!(tree.active_overlays().is_empty());
2954 assert!(!tree.is_visible(content_a));
2955 assert!(!tree.is_visible(content_b));
2956 }
2957
2958 #[test]
2959 fn dismissed_overlay_content_is_dormant_and_invisible() {
2960 let mut tree = WidgetTree::new();
2961 let anchor = tree.add(FillWidget::new());
2962 let content = tree.add(FillWidget::new());
2963 tree.layout(SizeProposal::exact(800.0, 600.0));
2964
2965 let id = tree.show_overlay(crate::overlay::OverlayRequest::new(
2966 content,
2967 anchor,
2968 crate::overlay::OverlayPlacement::Below,
2969 crate::overlay::DismissBehavior::Manual,
2970 ));
2971
2972 tree.layout(SizeProposal::exact(800.0, 600.0));
2973
2974 tree.dismiss_overlay(id);
2975 assert!(!tree.is_visible(content));
2976
2977 tree.layout(SizeProposal::exact(800.0, 600.0));
2978
2979 let center = tree.bounds(content).center();
2980 let hit = tree.hit_test(center);
2981 assert_ne!(hit, Some(content));
2982
2983 let _frame = tree.render();
2984 assert!(!tree.is_visible(content));
2985 }
2986
2987 #[test]
2988 fn tooltip_appears_after_delay() {
2989 let mut tree = WidgetTree::new();
2990 let anchor = tree.add(FillWidget::new());
2991 let tooltip = tree.add(FillWidget::new().label("Tooltip text"));
2992 tree.layout(SizeProposal::exact(200.0, 100.0));
2993
2994 tree.attach_tooltip(anchor, tooltip, std::time::Duration::from_millis(500));
2995
2996 let center = tree.bounds(anchor).center();
2997 tree.pointer_move(center);
2998 assert!(tree.active_overlays().is_empty());
2999
3000 tree.advance_time(std::time::Duration::from_millis(600));
3001
3002 assert_eq!(tree.active_overlays().len(), 1);
3003 assert!(tree.find_by_label("Tooltip text").is_some());
3004 }
3005
3006 #[test]
3007 fn tooltip_survives_theme_switch() {
3008 let mut tree = WidgetTree::new().with_theme(crate::presets::intui::light());
3013 let anchor = tree.add(FillWidget::new());
3014 let tooltip = tree.add(FillWidget::new().label("Tip"));
3015 tree.layout(SizeProposal::exact(200.0, 100.0));
3016
3017 tree.attach_tooltip(anchor, tooltip, std::time::Duration::from_millis(500));
3018
3019 tree.set_theme(crate::presets::intui::dark());
3020
3021 tree.pointer_move(tree.bounds(anchor).center());
3022 tree.advance_time(std::time::Duration::from_millis(600));
3023
3024 assert_eq!(tree.active_overlays().len(), 1);
3025 assert!(tree.find_by_label("Tip").is_some());
3026 }
3027
3028 #[test]
3029 fn tooltip_suppressed_when_hovering_anchor_owned_overlay_content() {
3030 let mut tree = WidgetTree::new();
3038 let anchor = tree.add(FillWidget::new());
3039 let panel = tree.add_child(anchor, FillWidget::new());
3042 let row = tree.add_child(panel, FillWidget::new());
3043 let tip = tree.add(FillWidget::new().label("Tip"));
3044 tree.layout(SizeProposal::exact(200.0, 100.0));
3045
3046 let delay = std::time::Duration::from_millis(100);
3047 tree.attach_tooltip(anchor, tip, delay);
3048
3049 tree.tooltip_pointer_enter(anchor);
3052 tree.advance_time(delay + std::time::Duration::from_millis(50));
3053 assert_eq!(
3054 tree.active_overlays().len(),
3055 1,
3056 "tooltip should appear when hovering the anchor itself"
3057 );
3058 tree.pointer_move(Point::new(500.0, 500.0));
3061 tree.advance_time(std::time::Duration::from_millis(150));
3062 assert!(tree.active_overlays().is_empty());
3063
3064 tree.show_overlay(crate::overlay::OverlayRequest::new(
3066 panel,
3067 anchor,
3068 crate::overlay::OverlayPlacement::Below,
3069 crate::overlay::DismissBehavior::Manual,
3070 ));
3071 assert_eq!(tree.active_overlays().len(), 1);
3072
3073 tree.tooltip_pointer_enter(row);
3076 tree.advance_time(delay + std::time::Duration::from_millis(50));
3077 assert_eq!(
3078 tree.active_overlays().len(),
3079 1,
3080 "anchor tooltip must not leak onto its own overlay's rows"
3081 );
3082 }
3083
3084 #[test]
3085 fn tooltip_inside_overlay_still_fires() {
3086 let mut tree = WidgetTree::new();
3090 let host = tree.add(FillWidget::new());
3091 let panel = tree.add_child(host, FillWidget::new());
3092 let row = tree.add_child(panel, FillWidget::new());
3093 let tip = tree.add(FillWidget::new().label("Row tip"));
3094 tree.layout(SizeProposal::exact(200.0, 100.0));
3095
3096 let delay = std::time::Duration::from_millis(100);
3097 tree.attach_tooltip(row, tip, delay);
3099
3100 tree.show_overlay(crate::overlay::OverlayRequest::new(
3101 panel,
3102 host,
3103 crate::overlay::OverlayPlacement::Below,
3104 crate::overlay::DismissBehavior::Manual,
3105 ));
3106 assert_eq!(tree.active_overlays().len(), 1);
3107
3108 tree.tooltip_pointer_enter(row);
3109 tree.advance_time(delay + std::time::Duration::from_millis(50));
3110 assert_eq!(
3111 tree.active_overlays().len(),
3112 2,
3113 "a row's own tooltip should still fire inside an overlay"
3114 );
3115 }
3116
3117 #[test]
3118 fn tooltip_dismissed_on_pointer_leave() {
3119 let mut tree = WidgetTree::new();
3120 let anchor = tree.add(FillWidget::new());
3121 let tooltip = tree.add(FillWidget::new().label("Tip"));
3122 tree.layout(SizeProposal::exact(200.0, 100.0));
3123
3124 tree.attach_tooltip(anchor, tooltip, std::time::Duration::from_millis(500));
3125
3126 tree.pointer_move(tree.bounds(anchor).center());
3127 tree.advance_time(std::time::Duration::from_millis(600));
3128 assert_eq!(tree.active_overlays().len(), 1);
3129
3130 tree.pointer_move(Point::new(500.0, 500.0));
3135 assert_eq!(
3136 tree.active_overlays().len(),
3137 1,
3138 "tooltip persists briefly after anchor-leave (hoverable grace)"
3139 );
3140 tree.advance_time(std::time::Duration::from_millis(150));
3141 assert!(
3142 tree.active_overlays().is_empty(),
3143 "tooltip dismissed after the 100ms grace outside anchor+overlay"
3144 );
3145 }
3146
3147 #[test]
3148 fn tooltip_not_shown_if_pointer_leaves_before_delay() {
3149 let mut tree = WidgetTree::new();
3150 let anchor = tree.add(FillWidget::new());
3151 let tooltip = tree.add(FillWidget::new().label("Tip"));
3152 tree.layout(SizeProposal::exact(200.0, 100.0));
3153
3154 tree.attach_tooltip(anchor, tooltip, std::time::Duration::from_millis(500));
3155
3156 tree.pointer_move(tree.bounds(anchor).center());
3157 tree.advance_time(std::time::Duration::from_millis(200));
3158 tree.pointer_move(Point::new(500.0, 500.0));
3159
3160 tree.advance_time(std::time::Duration::from_millis(500));
3161 assert!(tree.active_overlays().is_empty());
3162 }
3163
3164 #[test]
3165 fn tooltip_reshow_uses_short_delay_after_prior_tip() {
3166 let mut tree = WidgetTree::new();
3169 let a = tree.add(FillWidget::new());
3170 let b = tree.add(FillWidget::new());
3171 let tip_a = tree.add(FillWidget::new().label("Tip A"));
3172 let tip_b = tree.add(FillWidget::new().label("Tip B"));
3173 tree.layout(SizeProposal::exact(400.0, 200.0));
3174
3175 let delay = std::time::Duration::from_millis(500);
3176 tree.attach_tooltip(a, tip_a, delay);
3177 tree.attach_tooltip(b, tip_b, delay);
3178
3179 tree.tooltip_pointer_enter(a);
3182 tree.advance_time(std::time::Duration::from_millis(150));
3183 assert!(
3184 tree.active_overlays().is_empty(),
3185 "must not appear before full initial delay"
3186 );
3187 tree.advance_time(std::time::Duration::from_millis(400));
3188 assert_eq!(
3189 tree.active_overlays().len(),
3190 1,
3191 "first tip after initial delay"
3192 );
3193 assert!(tree.find_by_label("Tip A").is_some());
3194
3195 let mut noop = crate::window::NoopWindowOps;
3197 tree.tooltip_pointer_leave(a, &mut noop);
3198 tree.tooltip_pointer_enter(b);
3199 tree.advance_time(std::time::Duration::from_millis(120));
3200 assert!(
3201 tree.find_by_label("Tip B").is_some(),
3202 "second tip should use the short reshow delay while session is warm"
3203 );
3204 }
3205
3206 #[test]
3207 fn reshow_delay_reverts_to_full_after_the_session_grace_expires() {
3208 let mut tree = WidgetTree::new();
3214 tree.set_accessibility_preferences(false, true, 1.0);
3218 let a = tree.add(FillWidget::new());
3219 let b = tree.add(FillWidget::new());
3220 let tip_a = tree.add(FillWidget::new().label("Tip A"));
3221 let tip_b = tree.add(FillWidget::new().label("Tip B"));
3222 tree.layout(SizeProposal::exact(400.0, 200.0));
3223
3224 let delay = std::time::Duration::from_millis(500);
3225 tree.attach_tooltip(a, tip_a, delay);
3226 tree.attach_tooltip(b, tip_b, delay);
3227
3228 tree.tooltip_pointer_enter(a);
3230 tree.advance_time(std::time::Duration::from_millis(550));
3231 assert!(tree.find_by_label("Tip A").is_some(), "first tip shown");
3232
3233 tree.pointer_move(Point::new(900.0, 900.0));
3234 tree.advance_time(std::time::Duration::from_millis(150));
3235 assert!(tree.active_overlays().is_empty(), "A dismissed on leave");
3236
3237 tree.advance_time(super::TOOLTIP_SESSION_GRACE + std::time::Duration::from_millis(50));
3239
3240 tree.tooltip_pointer_enter(b);
3241 tree.advance_time(std::time::Duration::from_millis(150));
3242 assert!(
3243 tree.active_overlays().is_empty(),
3244 "session went cold — B must pay the FULL delay, not the 100 ms reshow"
3245 );
3246 tree.advance_time(std::time::Duration::from_millis(400));
3247 assert!(
3248 tree.find_by_label("Tip B").is_some(),
3249 "B still appears once its full delay elapses"
3250 );
3251 }
3252
3253 #[test]
3254 fn warm_reshow_scales_the_delay_rather_than_flattening_every_tier() {
3255 let mut tree = WidgetTree::new();
3261 let light = tree.add(FillWidget::new());
3262 let heavy = tree.add(FillWidget::new());
3263 let tip_light = tree.add(FillWidget::new().label("Light"));
3264 let tip_heavy = tree.add(FillWidget::new().label("Heavy"));
3265 tree.layout(SizeProposal::exact(400.0, 200.0));
3266
3267 tree.attach_tooltip(light, tip_light, std::time::Duration::from_millis(500));
3268 tree.attach_tooltip(heavy, tip_heavy, std::time::Duration::from_millis(700));
3269
3270 tree.tooltip_pointer_enter(light);
3272 tree.advance_time(std::time::Duration::from_millis(550));
3273 assert!(tree.find_by_label("Light").is_some(), "session warm");
3274
3275 let mut noop = crate::window::NoopWindowOps;
3276 tree.tooltip_pointer_leave(light, &mut noop);
3277 tree.tooltip_pointer_enter(heavy);
3278
3279 tree.advance_time(std::time::Duration::from_millis(120));
3281 assert!(
3282 tree.find_by_label("Heavy").is_none(),
3283 "a heavy tooltip must not fire at the light tier's reshow delay"
3284 );
3285 tree.advance_time(std::time::Duration::from_millis(40));
3287 assert!(
3288 tree.find_by_label("Heavy").is_some(),
3289 "heavy reshow is the scaled 140 ms"
3290 );
3291 }
3292
3293 #[test]
3294 fn a_pinned_sticky_tooltip_does_not_hold_the_session_warm() {
3295 let mut tree = WidgetTree::new();
3299 tree.set_accessibility_preferences(false, true, 1.0); let a = tree.add(FillWidget::new());
3301 let b = tree.add(FillWidget::new());
3302 let tip_a = tree.add(FillWidget::new().label("Pinned"));
3303 let tip_b = tree.add(FillWidget::new().label("Other"));
3304 tree.layout(SizeProposal::exact(400.0, 200.0));
3305
3306 tree.attach_tooltip_with_sticky(
3307 a,
3308 tip_a,
3309 std::time::Duration::from_millis(500),
3310 Some(std::time::Duration::from_millis(100)),
3311 );
3312 tree.attach_tooltip(b, tip_b, std::time::Duration::from_millis(500));
3313
3314 tree.tooltip_pointer_enter(a);
3315 tree.advance_time(std::time::Duration::from_millis(550));
3316 assert!(tree.find_by_label("Pinned").is_some());
3317 tree.promote_tooltip_to_sticky(tip_a);
3318
3319 tree.advance_time(super::TOOLTIP_SESSION_GRACE + std::time::Duration::from_millis(50));
3321 tree.tooltip_pointer_enter(b);
3322 tree.advance_time(std::time::Duration::from_millis(150));
3323 assert!(
3324 tree.find_by_label("Other").is_none(),
3325 "a pinned sticky must not keep every other anchor on the reshow path"
3326 );
3327 }
3328
3329 #[test]
3330 fn reattaching_a_tooltip_does_not_grow_the_entry_table() {
3331 let mut tree = WidgetTree::new();
3337 let anchor = tree.add(FillWidget::new());
3338 tree.layout(SizeProposal::exact(200.0, 100.0));
3339
3340 let delay = std::time::Duration::from_millis(100);
3341 for _ in 0..25 {
3342 let tip = tree.add(FillWidget::new().label("Tip"));
3344 tree.attach_tooltip(anchor, tip, delay);
3345 assert_eq!(
3346 tree.tooltip_entry_count(),
3347 1,
3348 "an anchor must own exactly one tooltip entry across rebuilds"
3349 );
3350 }
3351
3352 tree.tooltip_pointer_enter(anchor);
3354 tree.advance_time(delay + std::time::Duration::from_millis(50));
3355 assert_eq!(
3356 tree.active_overlays().len(),
3357 1,
3358 "latest tooltip still shows"
3359 );
3360 }
3361
3362 #[test]
3363 fn destroying_an_anchor_reaps_its_tooltip_entry() {
3364 let mut tree = WidgetTree::new();
3367 let host = tree.add(FillWidget::new());
3368 let anchor = tree.add_child(host, FillWidget::new());
3369 let tip = tree.add(FillWidget::new().label("Tip"));
3370 tree.layout(SizeProposal::exact(200.0, 100.0));
3371
3372 tree.attach_tooltip(anchor, tip, std::time::Duration::from_millis(100));
3373 assert_eq!(tree.tooltip_entry_count(), 1);
3374
3375 tree.destroy_subtree(anchor);
3376 assert_eq!(
3377 tree.tooltip_entry_count(),
3378 0,
3379 "destroying the anchor must reap its entry and content node"
3380 );
3381 }
3382
3383 #[test]
3384 fn a_leaving_tooltip_schedules_a_wake_for_its_dismissal() {
3385 let mut tree = WidgetTree::new();
3390 tree.set_accessibility_preferences(false, true, 1.0); let anchor = tree.add(FillWidget::new());
3392 let tip = tree.add(FillWidget::new().label("Tip"));
3393 tree.layout(SizeProposal::exact(400.0, 200.0));
3394
3395 tree.attach_tooltip(anchor, tip, std::time::Duration::from_millis(100));
3396 tree.pointer_move(tree.bounds(anchor).center());
3397 tree.advance_time(std::time::Duration::from_millis(150));
3398 assert_eq!(tree.active_overlays().len(), 1, "tooltip shown");
3399
3400 assert!(
3402 tree.next_timer_deadline().is_none(),
3403 "a settled tooltip under the pointer schedules nothing"
3404 );
3405
3406 tree.pointer_move(Point::new(900.0, 900.0));
3409 assert!(
3410 tree.next_timer_deadline().is_some(),
3411 "the PointerLeave grace must contribute a wake deadline"
3412 );
3413 }
3414
3415 #[test]
3426 fn a_fading_out_overlay_schedules_a_wake_for_its_deferred_removal() {
3427 let mut tree = WidgetTree::new();
3428 let anchor = tree.add(FillWidget::new());
3429 let content = tree.add(FillWidget::new().label("Faded"));
3430 tree.layout(SizeProposal::exact(200.0, 100.0));
3431
3432 let id = tree.show_overlay(
3433 crate::overlay::OverlayRequest::new(
3434 content,
3435 anchor,
3436 crate::overlay::OverlayPlacement::Below,
3437 crate::overlay::DismissBehavior::Manual,
3438 )
3439 .with_fade(std::time::Duration::from_millis(200)),
3440 );
3441 tree.dismiss_overlay(id);
3442 assert!(
3443 tree.is_visible(content),
3444 "precondition: the removal is deferred, so there is something to wake for"
3445 );
3446
3447 let expected = tree.overlay_manager.next_fade_dismiss_deadline();
3453 assert!(
3454 expected.is_some(),
3455 "precondition: the fade start was stamped, so there is a deadline to compare against"
3456 );
3457 assert_eq!(
3458 tree.next_timer_deadline(),
3459 expected,
3460 "the wake deadline must be the deferred removal's own, not merely some deadline"
3461 );
3462 }
3463
3464 #[test]
3465 fn pressing_cancels_a_pending_dwell_and_dismisses_a_shown_tooltip() {
3466 let mut tree = WidgetTree::new();
3467 let anchor = tree.add(FillWidget::new());
3468 let tip = tree.add(FillWidget::new().label("Tip"));
3469 tree.layout(SizeProposal::exact(400.0, 200.0));
3470
3471 let delay = std::time::Duration::from_millis(500);
3472 tree.attach_tooltip(anchor, tip, delay);
3473
3474 tree.pointer_move(tree.bounds(anchor).center());
3477 tree.advance_time(std::time::Duration::from_millis(300));
3478 tree.tooltip_pointer_press(None);
3479 tree.advance_time(std::time::Duration::from_millis(400));
3480 assert!(
3481 tree.active_overlays().is_empty(),
3482 "a press must cancel the pending dwell, not merely delay it"
3483 );
3484
3485 tree.tooltip_pointer_enter(anchor);
3488 tree.advance_time(delay + std::time::Duration::from_millis(50));
3489 assert_eq!(tree.active_overlays().len(), 1, "tooltip shown again");
3490 tree.tooltip_pointer_press(Some(tree.bounds(anchor).center()));
3491 assert!(
3492 tree.active_overlays().is_empty(),
3493 "a press must dismiss the shown tooltip"
3494 );
3495 }
3496
3497 #[test]
3498 fn window_deactivation_retires_hover_tooltips() {
3499 let mut tree = WidgetTree::new();
3500 let anchor = tree.add(FillWidget::new());
3501 let tip = tree.add(FillWidget::new().label("Tip"));
3502 tree.layout(SizeProposal::exact(400.0, 200.0));
3503
3504 tree.attach_tooltip(anchor, tip, std::time::Duration::from_millis(100));
3505 tree.pointer_move(tree.bounds(anchor).center());
3506 tree.advance_time(std::time::Duration::from_millis(150));
3507 assert_eq!(tree.active_overlays().len(), 1);
3508
3509 tree.set_window_active(false);
3510 assert!(
3511 tree.active_overlays().is_empty(),
3512 "a tooltip must not float over another window's chrome"
3513 );
3514 }
3515
3516 #[test]
3517 fn only_the_innermost_anchor_arms_its_dwell() {
3518 let mut tree = WidgetTree::new();
3521 let panel = tree.add(FillWidget::new());
3522 let row = tree.add_child(panel, FillWidget::new());
3523 let panel_tip = tree.add(FillWidget::new().label("Panel"));
3524 let row_tip = tree.add(FillWidget::new().label("Row"));
3525 tree.layout(SizeProposal::exact(400.0, 200.0));
3526
3527 let delay = std::time::Duration::from_millis(100);
3528 tree.attach_tooltip(panel, panel_tip, delay);
3529 tree.attach_tooltip(row, row_tip, delay);
3530
3531 tree.tooltip_pointer_enter(row);
3532 tree.advance_time(delay + std::time::Duration::from_millis(50));
3533
3534 assert_eq!(
3535 tree.active_overlays().len(),
3536 1,
3537 "exactly one tooltip may open for a hover"
3538 );
3539 assert!(
3540 tree.find_by_label("Row").is_some(),
3541 "the innermost anchor wins"
3542 );
3543 }
3544
3545 #[test]
3546 fn escape_dismisses_a_hover_tooltip_and_falls_through_to_the_menu_below() {
3547 let mut tree = WidgetTree::new();
3551 tree.set_accessibility_preferences(false, true, 1.0); let anchor = tree.add(FillWidget::new());
3553 let menu = tree.add(FillWidget::new());
3554 let tip = tree.add(FillWidget::new().label("Tip"));
3555 tree.layout(SizeProposal::exact(400.0, 200.0));
3556
3557 let menu_overlay = tree.show_overlay(crate::overlay::OverlayRequest::new(
3558 menu,
3559 anchor,
3560 crate::overlay::OverlayPlacement::Below,
3561 crate::overlay::DismissBehavior::EscapeOrClickOutside,
3562 ));
3563 assert_eq!(tree.active_overlays().len(), 1);
3564
3565 tree.attach_tooltip(anchor, tip, std::time::Duration::from_millis(100));
3567 tree.tooltip_pointer_enter(anchor);
3568 tree.advance_time(std::time::Duration::from_millis(150));
3569 assert_eq!(
3570 tree.active_overlays().len(),
3571 2,
3572 "tooltip sits above the menu"
3573 );
3574
3575 let dismissed = tree.overlay_manager.try_dismiss_top_on_escape();
3577 assert!(dismissed.is_some(), "Escape must dismiss the hover tooltip");
3578 assert_eq!(tree.active_overlays().len(), 1);
3579
3580 let dismissed = tree.overlay_manager.try_dismiss_top_on_escape();
3582 assert_eq!(
3583 dismissed.map(|(id, _, _)| id),
3584 Some(menu_overlay),
3585 "Escape must then reach the menu underneath"
3586 );
3587 assert!(tree.active_overlays().is_empty());
3588 }
3589
3590 #[test]
3591 fn tooltip_timer_restarts_when_pointer_keeps_moving() {
3592 let mut tree = WidgetTree::new();
3595 let anchor = tree.add(FillWidget::new());
3596 let tooltip = tree.add(FillWidget::new().label("Tip"));
3597 tree.layout(SizeProposal::exact(200.0, 100.0));
3598
3599 tree.attach_tooltip(anchor, tooltip, std::time::Duration::from_millis(500));
3600
3601 let start = tree.bounds(anchor).center();
3602 tree.pointer_move(start);
3603 tree.advance_time(std::time::Duration::from_millis(300));
3604 tree.pointer_move(Point::new(start.x + 20.0, start.y));
3606 tree.advance_time(std::time::Duration::from_millis(300));
3609 assert!(
3610 tree.active_overlays().is_empty(),
3611 "moving past stationary slop must restart the delay"
3612 );
3613 tree.advance_time(std::time::Duration::from_millis(250));
3614 assert_eq!(
3615 tree.active_overlays().len(),
3616 1,
3617 "tooltip appears after a full delay of stillness"
3618 );
3619 }
3620
3621 #[test]
3622 fn timed_overlay_auto_dismisses_after_duration() {
3623 let mut tree = WidgetTree::new();
3624 let anchor = tree.add(FillWidget::new());
3625 let content = tree.add(FillWidget::new().label("Toast"));
3626 tree.layout(SizeProposal::exact(200.0, 100.0));
3627
3628 tree.show_overlay_for(
3629 crate::overlay::OverlayRequest::new(
3630 content,
3631 anchor,
3632 crate::overlay::OverlayPlacement::Below,
3633 crate::overlay::DismissBehavior::Manual,
3634 ),
3635 std::time::Duration::from_millis(300),
3636 );
3637
3638 assert_eq!(tree.active_overlays().len(), 1);
3639
3640 tree.advance_time(std::time::Duration::from_millis(200));
3641 assert_eq!(tree.active_overlays().len(), 1);
3642
3643 tree.advance_time(std::time::Duration::from_millis(150));
3644 assert!(tree.active_overlays().is_empty());
3645 assert!(!tree.is_visible(content));
3646 }
3647
3648 #[test]
3649 fn fade_dismiss_keeps_overlay_off_active_list_immediately() {
3650 let mut tree = WidgetTree::new();
3658 let anchor = tree.add(FillWidget::new());
3659 let content = tree.add(FillWidget::new().label("Faded"));
3660 tree.layout(SizeProposal::exact(200.0, 100.0));
3661
3662 let id = tree.show_overlay(
3663 crate::overlay::OverlayRequest::new(
3664 content,
3665 anchor,
3666 crate::overlay::OverlayPlacement::Below,
3667 crate::overlay::DismissBehavior::Manual,
3668 )
3669 .with_fade(std::time::Duration::from_millis(100)),
3670 );
3671 assert_eq!(tree.active_overlays().len(), 1);
3672
3673 tree.dismiss_overlay(id);
3674 assert!(tree.active_overlays().is_empty());
3679 }
3680
3681 #[test]
3682 fn fade_dismiss_defers_content_dormancy_until_sim_tween_completes() {
3683 let mut tree = WidgetTree::new();
3689 let anchor = tree.add(FillWidget::new());
3690 let content = tree.add(FillWidget::new().label("Faded"));
3691 tree.layout(SizeProposal::exact(200.0, 100.0));
3692
3693 let id = tree.show_overlay(
3694 crate::overlay::OverlayRequest::new(
3695 content,
3696 anchor,
3697 crate::overlay::OverlayPlacement::Below,
3698 crate::overlay::DismissBehavior::Manual,
3699 )
3700 .with_fade(std::time::Duration::from_millis(100)),
3701 );
3702 tree.advance_time(std::time::Duration::from_secs(1));
3708
3709 tree.dismiss_overlay(id);
3710 assert!(
3711 tree.is_visible(content),
3712 "content stays active during fade-out"
3713 );
3714
3715 tree.advance_time(std::time::Duration::from_millis(60));
3719 assert!(
3720 tree.is_visible(content),
3721 "60 ms into a 100 ms tween the content is still up"
3722 );
3723
3724 tree.advance_time(std::time::Duration::from_millis(90));
3725 assert!(
3726 !tree.is_visible(content),
3727 "after sim-time past the tween window, deferred removal fires"
3728 );
3729 }
3730
3731 #[test]
3743 fn an_auto_dismissed_fade_starts_at_the_instant_the_dismiss_ran() {
3744 let mut tree = WidgetTree::new();
3745 let anchor = tree.add(FillWidget::new());
3746 let content = tree.add(FillWidget::new().label("Toast"));
3747 tree.layout(SizeProposal::exact(200.0, 100.0));
3748
3749 tree.show_overlay_for(
3750 crate::overlay::OverlayRequest::new(
3751 content,
3752 anchor,
3753 crate::overlay::OverlayPlacement::Below,
3754 crate::overlay::DismissBehavior::Manual,
3755 )
3756 .with_fade(std::time::Duration::from_millis(100)),
3757 std::time::Duration::from_millis(500),
3758 );
3759 assert_eq!(tree.active_overlays().len(), 1);
3760
3761 tree.advance_time(std::time::Duration::from_millis(600));
3765 assert!(
3766 tree.active_overlays().is_empty(),
3767 "precondition: the auto-dismiss fired inside this advance"
3768 );
3769 assert!(
3770 tree.is_visible(content),
3771 "the fade must start at the instant the dismiss ran, so the \
3772 content survives the frame that dismissed it"
3773 );
3774
3775 tree.advance_time(std::time::Duration::from_millis(150));
3776 assert!(
3777 !tree.is_visible(content),
3778 "and is reaped once the tween's own window has passed"
3779 );
3780 }
3781
3782 #[derive(Debug)]
3788 struct Block(f32, f32);
3789
3790 impl Widget for Block {
3791 fn layout_response(
3792 &self,
3793 _proposal: SizeProposal,
3794 _ctx: &LayoutContext,
3795 ) -> crate::widget::LayoutResponse {
3796 teksilo_canvas::Size::new(self.0, self.1).into()
3797 }
3798 }
3799
3800 fn panel_and_an_overlay_above(
3804 panel_dismiss: crate::overlay::DismissBehavior,
3805 top: impl Widget + 'static,
3806 top_dismiss: crate::overlay::DismissBehavior,
3807 top_at: Point,
3808 inert: bool,
3809 ) -> (WidgetTree, WidgetId, WidgetId) {
3810 let mut tree = WidgetTree::new();
3811 let panel = tree.add(Block(100.0, 60.0));
3812 let top = tree.add(top);
3813 let beneath = tree.add(FillWidget::new());
3817 tree.show_overlay(crate::overlay::OverlayRequest::new(
3818 panel,
3819 beneath,
3820 crate::overlay::OverlayPlacement::AtPointer(Point::new(20.0, 20.0)),
3821 panel_dismiss,
3822 ));
3823 let request = crate::overlay::OverlayRequest::new(
3824 top,
3825 beneath,
3826 crate::overlay::OverlayPlacement::AtPointer(top_at),
3827 top_dismiss,
3828 );
3829 tree.show_overlay(if inert { request.inert() } else { request });
3830 tree.layout(SizeProposal::exact(400.0, 200.0));
3831 (tree, panel, top)
3832 }
3833
3834 fn panel_and_an_inert_overlay(
3837 panel_dismiss: crate::overlay::DismissBehavior,
3838 inert_dismiss: crate::overlay::DismissBehavior,
3839 inert_at: Point,
3840 ) -> (WidgetTree, WidgetId, WidgetId) {
3841 panel_and_an_overlay_above(
3842 panel_dismiss,
3843 Block(60.0, 40.0),
3844 inert_dismiss,
3845 inert_at,
3846 true,
3847 )
3848 }
3849
3850 #[test]
3851 fn an_inert_overlay_hands_the_point_to_the_overlay_beneath_it() {
3852 let (tree, panel, inert) = panel_and_an_inert_overlay(
3853 crate::overlay::DismissBehavior::Manual,
3854 crate::overlay::DismissBehavior::EscapeKey,
3855 Point::new(30.0, 30.0),
3856 );
3857 let at = Point::new(40.0, 40.0);
3858 let over = tree.overlay_manager.find_by_content(inert).unwrap();
3859 assert!(
3860 tree.overlay_content_bounds(over).unwrap().contains(at),
3861 "fixture: the inert overlay covers the point"
3862 );
3863 assert_eq!(
3864 tree.hit_test(at),
3865 Some(panel),
3866 "the panel under it takes the point, not the window under both"
3867 );
3868 }
3869
3870 #[test]
3871 fn a_press_on_an_inert_overlay_is_outside_the_overlays_it_is_outside_of() {
3872 let (mut tree, panel, inert) = panel_and_an_inert_overlay(
3873 crate::overlay::DismissBehavior::ClickOutside,
3874 crate::overlay::DismissBehavior::EscapeKey,
3875 Point::new(200.0, 100.0),
3876 );
3877 let at = Point::new(220.0, 110.0);
3878 let over = tree.overlay_manager.find_by_content(inert).unwrap();
3879 assert!(
3880 tree.overlay_content_bounds(over).unwrap().contains(at),
3881 "fixture: the press lands on the inert overlay"
3882 );
3883 tree.pointer_down_button(at, crate::event::PointerButton::Primary);
3884 assert_eq!(
3885 tree.overlay_manager.find_by_content(panel),
3886 None,
3887 "the press was outside the panel, whatever floats over that spot",
3888 );
3889 }
3890
3891 #[test]
3892 fn an_inert_overlay_is_not_an_interactive_rect() {
3893 let (tree, panel, _inert) = panel_and_an_inert_overlay(
3896 crate::overlay::DismissBehavior::Manual,
3897 crate::overlay::DismissBehavior::EscapeKey,
3898 Point::new(200.0, 100.0),
3899 );
3900 let panel_bounds = tree
3901 .overlay_content_bounds(tree.overlay_manager.find_by_content(panel).unwrap())
3902 .unwrap();
3903 assert_eq!(tree.overlay_manager.interactive_rects(), vec![panel_bounds]);
3904 }
3905
3906 #[test]
3907 fn escape_steps_over_an_inert_overlay_to_the_one_beneath() {
3908 let (mut tree, panel, inert) = panel_and_an_inert_overlay(
3911 crate::overlay::DismissBehavior::EscapeKey,
3912 crate::overlay::DismissBehavior::Manual,
3913 Point::new(200.0, 100.0),
3914 );
3915 tree.press_key(Key::Escape, Modifiers::NONE);
3916 assert_eq!(
3917 tree.overlay_manager.find_by_content(panel),
3918 None,
3919 "the panel beneath took the Escape"
3920 );
3921 assert!(
3922 tree.overlay_manager.find_by_content(inert).is_some(),
3923 "and the inert one, which Escape may not close, is still up"
3924 );
3925 }
3926
3927 #[test]
3928 fn an_inert_overlay_over_the_window_hands_the_point_to_the_window() {
3929 let mut tree = WidgetTree::new();
3933 let page = tree.add(FillWidget::new());
3934 let inert = tree.add(Block(60.0, 40.0));
3935 tree.show_overlay(
3936 crate::overlay::OverlayRequest::new(
3937 inert,
3938 page,
3939 crate::overlay::OverlayPlacement::AtPointer(Point::new(200.0, 100.0)),
3940 crate::overlay::DismissBehavior::EscapeKey,
3941 )
3942 .inert(),
3943 );
3944 tree.layout(SizeProposal::exact(400.0, 200.0));
3945 let at = Point::new(220.0, 110.0);
3946 assert!(
3947 tree.overlay_content_bounds(tree.overlay_manager.find_by_content(inert).unwrap())
3948 .unwrap()
3949 .contains(at),
3950 "fixture: the inert overlay covers the point"
3951 );
3952 assert_eq!(tree.hit_test(at), Some(page));
3953 tree.pointer_move(at);
3954 assert_eq!(tree.hovered(), Some(page), "and so does a hover");
3955 }
3956
3957 #[test]
3958 fn a_root_that_is_hit_transparent_and_pass_through_is_not_inert() {
3959 use crate::widget_builder::WidgetBuilder;
3960
3961 let flagged = || {
3965 Block(60.0, 40.0)
3966 .hit_transparent(true)
3967 .event_pass_through(true)
3968 };
3969 let at = Point::new(220.0, 110.0);
3970
3971 let (mut tree, panel, top) = panel_and_an_overlay_above(
3972 crate::overlay::DismissBehavior::ClickOutside,
3973 flagged(),
3974 crate::overlay::DismissBehavior::Manual,
3975 Point::new(200.0, 100.0),
3976 false,
3977 );
3978 let over = tree.overlay_manager.find_by_content(top).unwrap();
3979 assert!(
3980 tree.overlay_content_bounds(over).unwrap().contains(at),
3981 "fixture: the press lands on the flagged overlay"
3982 );
3983 assert_eq!(
3984 tree.overlay_manager.hit_test(at),
3985 Some(over),
3986 "the stack still answers with the overlay"
3987 );
3988 assert_eq!(
3989 tree.overlay_manager.interactive_rects().len(),
3990 2,
3991 "which is an interactive rect"
3992 );
3993 tree.pointer_down_button(at, crate::event::PointerButton::Primary);
3994 tree.pointer_up_button(at, crate::event::PointerButton::Primary);
3995 assert!(
3996 tree.overlay_manager.find_by_content(panel).is_some(),
3997 "a press on it is inside the stack, so the panel beneath stays"
3998 );
3999
4000 let (mut tree, panel, top) = panel_and_an_overlay_above(
4001 crate::overlay::DismissBehavior::EscapeKey,
4002 flagged(),
4003 crate::overlay::DismissBehavior::Manual,
4004 Point::new(200.0, 100.0),
4005 false,
4006 );
4007 tree.press_key(Key::Escape, Modifiers::NONE);
4008 assert!(
4009 tree.overlay_manager.find_by_content(panel).is_some()
4010 && tree.overlay_manager.find_by_content(top).is_some(),
4011 "a `Manual` overlay owns the Escape: nothing beneath it closes"
4012 );
4013
4014 let (mut tree, panel, top) = panel_and_an_overlay_above(
4015 crate::overlay::DismissBehavior::EscapeKey,
4016 flagged(),
4017 crate::overlay::DismissBehavior::EscapeKey,
4018 Point::new(200.0, 100.0),
4019 false,
4020 );
4021 tree.press_key(Key::Escape, Modifiers::NONE);
4022 assert_eq!(
4023 tree.overlay_manager.find_by_content(top),
4024 None,
4025 "an `EscapeKey` one is what the Escape closes"
4026 );
4027 assert!(
4028 tree.overlay_manager.find_by_content(panel).is_some(),
4029 "and it is spent there"
4030 );
4031 }
4032
4033 #[test]
4034 fn an_inert_overlay_records_no_focus_to_give_back_on_any_show_path() {
4035 type Show = fn(&mut WidgetTree, crate::overlay::OverlayRequest, WidgetId);
4036 fn from_a_handler(
4037 tree: &mut WidgetTree,
4038 show: impl FnOnce(&mut crate::widget::EventContext),
4039 ) {
4040 tree.run_with_event_context(&mut crate::window::NoopWindowOps, show);
4041 }
4042 let paths: [(&str, Show); 5] = [
4043 ("show_overlay", |tree, request, _| {
4044 from_a_handler(tree, |ctx| ctx.show_overlay(request))
4045 }),
4046 ("show_overlay_for", |tree, request, _| {
4047 from_a_handler(tree, |ctx| {
4048 ctx.show_overlay_for(request, std::time::Duration::from_secs(60))
4049 })
4050 }),
4051 ("show_overlay_with_reveal", |tree, request, _| {
4052 from_a_handler(tree, |ctx| {
4053 ctx.show_overlay_with_reveal(
4054 request,
4055 crate::signal::Signal::new(0.0),
4056 std::time::Duration::from_millis(100),
4057 )
4058 })
4059 }),
4060 ("show_overlay_after", |tree, request, _| {
4061 from_a_handler(tree, |ctx| {
4062 ctx.show_overlay_after(request, std::time::Duration::ZERO)
4063 });
4064 tree.layout(SizeProposal::exact(400.0, 200.0));
4065 }),
4066 ("show_overlay_from_source", |tree, request, field| {
4067 tree.show_overlay_from_source(field, request);
4068 }),
4069 ];
4070 for (path, show) in paths {
4071 let mut tree = WidgetTree::new();
4072 let field = tree.add(FillWidget::new().focusable());
4073 let inert = tree.add(Block(60.0, 40.0));
4074 let ordinary = tree.add(Block(60.0, 40.0));
4075 tree.layout(SizeProposal::exact(400.0, 200.0));
4076 tree.focus(field);
4077 for (content, inert) in [(inert, true), (ordinary, false)] {
4078 let request = crate::overlay::OverlayRequest::new(
4079 content,
4080 field,
4081 crate::overlay::OverlayPlacement::AtPointer(Point::new(20.0, 20.0)),
4082 crate::overlay::DismissBehavior::EscapeKey,
4083 );
4084 show(
4085 &mut tree,
4086 if inert { request.inert() } else { request },
4087 field,
4088 );
4089 }
4090 let restore = |content: WidgetId| {
4091 tree.overlay_manager
4092 .stack
4093 .iter()
4094 .find(|o| o.content_id == content)
4095 .map(|o| o.focus_restore)
4096 };
4097 assert_eq!(
4098 restore(ordinary),
4099 Some(Some(field)),
4100 "{path}: an ordinary one does"
4101 );
4102 assert_eq!(
4103 restore(inert),
4104 Some(None),
4105 "{path}: an inert one took no focus"
4106 );
4107 }
4108 }
4109}