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 request = crate::overlay::OverlayRequest {
516 content_id,
517 anchor: anchor_id,
518 placement: Self::tooltip_overlay_placement(placement),
519 dismiss,
520 layer: crate::overlay::OverlayLayer::InTree,
521 parent_overlay: None,
522 on_dismiss: None,
523 fade_duration,
524 };
525 let oid = if by_hold {
526 self.show_overlay_for(request, super::touch_route::TOUCH_TOOLTIP_DISMISS)
527 } else {
528 self.show_overlay(request)
529 };
530 if by_focus && let Some(focused) = self.focused {
531 self.overlay_manager.set_top_focus_restore(focused);
532 }
533 if let Some(entry) = self
534 .tooltips
535 .iter_mut()
536 .find(|e| e.content_id == content_id)
537 {
538 entry.overlay_id = Some(oid);
539 entry.shown_at_sim = Some(sim_now);
540 entry.shown_at_real = Some(real_now);
541 entry.promoted_by_focus = by_focus;
542 if let Some(sink) = entry.shown_at_sink.as_ref() {
543 sink.set(Some(real_now));
544 }
545 }
546 }
547
548 let mut to_mark_paint: Vec<WidgetId> = Vec::new();
562 let mut to_promote: Vec<WidgetId> = Vec::new();
563 for entry in &self.tooltips {
564 let Some(sticky_after) = entry.sticky_after else {
565 continue;
566 };
567 if entry.overlay_id.is_none() || entry.is_sticky {
568 continue;
569 }
570 let elapsed = entry
571 .shown_at_real
572 .map(|t| real_now.saturating_duration_since(t));
573 let elapsed = match elapsed {
574 Some(e) => e,
575 None => continue,
576 };
577 to_mark_paint.push(entry.content_id);
581 if elapsed >= sticky_after {
582 to_promote.push(entry.content_id);
583 }
584 }
585 for id in to_mark_paint {
586 self.arena.mark_subtree_needs_paint(id);
587 }
588 for content_id in to_promote {
589 self.promote_tooltip_to_sticky(content_id);
590 }
591 }
592
593 pub(super) fn process_delayed_overlays(&mut self) {
594 let sim_now = self.sim_clock;
595 let mut noop = crate::window::NoopWindowOps;
596 self.process_delayed_overlays_impl(
597 |p| sim_now.saturating_duration_since(p.sim_requested_at),
598 &mut noop,
599 );
600 }
601
602 pub(super) fn process_delayed_overlays_real(&mut self, ops: &mut dyn crate::window::WindowOps) {
603 let real_now = std::time::Instant::now();
604 self.process_delayed_overlays_impl(
605 |p| real_now.saturating_duration_since(p.real_requested_at),
606 &mut *ops,
607 );
608 }
609
610 fn process_delayed_overlays_impl(
611 &mut self,
612 elapsed_fn: impl Fn(&PendingDelayedOverlay) -> std::time::Duration,
613 ops: &mut dyn crate::window::WindowOps,
614 ) {
615 let mut ready_indices = Vec::new();
616 for (index, pending) in self.pending_delayed_overlays.iter().enumerate() {
617 if elapsed_fn(pending) >= pending.delay {
618 ready_indices.push(index);
619 }
620 }
621
622 let mut ready = Vec::new();
623 for &index in ready_indices.iter().rev() {
624 ready.push(self.pending_delayed_overlays.remove(index));
625 }
626 ready.reverse();
627
628 let any_shown = !ready.is_empty();
629 for pending in ready {
630 let content_id = pending.request.content_id;
631 if pending.replace_siblings {
638 let anchor = pending.request.anchor;
639 self.dismiss_child_overlays_for_source(anchor, Some(content_id), &mut *ops);
640 }
641 self.arena.activate(content_id);
642 let current_focus = self.focused;
643 self.overlay_manager.show(pending.request);
644 if let Some(focus_id) = current_focus {
645 self.overlay_manager.set_top_focus_restore(focus_id);
646 }
647 self.arena.mark_needs_paint(content_id);
648 if let Some(focus_target) = pending.focus_target
649 && self.arena.is_active(focus_target)
650 {
651 self.focus_ops(focus_target, &mut *ops);
652 }
653 }
654 if any_shown {
655 self.a11y_dirty = true;
656 }
657 }
658
659 pub(super) fn overlay_ancestor_for_widget(
660 &self,
661 widget_id: WidgetId,
662 ) -> Option<crate::overlay::OverlayId> {
663 self.overlay_manager
664 .stack
665 .iter()
666 .rev()
667 .find(|overlay| self.is_descendant_of(widget_id, overlay.content_id))
668 .map(|overlay| overlay.id)
669 }
670
671 pub(super) fn modal_overlay_for_widget(
672 &self,
673 widget_id: WidgetId,
674 ) -> Option<crate::overlay::OverlayId> {
675 let mut current = self.overlay_ancestor_for_widget(widget_id);
676
677 while let Some(overlay_id) = current {
678 let overlay = self.overlay_manager.overlay(overlay_id)?;
679 if matches!(
680 overlay.placement,
681 crate::overlay::OverlayPlacement::Centered
682 ) {
683 return Some(overlay_id);
684 }
685 current = overlay.parent_overlay;
686 }
687
688 None
689 }
690
691 fn menu_ancestor_for_widget(&self, widget_id: WidgetId) -> Option<WidgetId> {
692 let mut current = Some(widget_id);
693 while let Some(id) = current {
694 if let Some(node) = self.arena.get(id) {
695 let mut builder = AccessNodeBuilder::new();
696 node.widget.accessibility(&mut builder);
697 if builder.role() == accesskit::Role::Menu {
698 return Some(id);
699 }
700 }
701 current = self.arena.parent(id);
702 }
703 None
704 }
705
706 pub(super) fn dismiss_child_overlays_for_source(
707 &mut self,
708 source_widget: WidgetId,
709 preserve_content: Option<WidgetId>,
710 ops: &mut dyn crate::window::WindowOps,
711 ) {
712 if let Some(parent_overlay) = self.overlay_ancestor_for_widget(source_widget) {
713 let preserve_overlay = preserve_content
714 .and_then(|content_id| self.overlay_manager.find_by_content(content_id));
715 let (dismissed, focus_restore) = self
716 .overlay_manager
717 .dismiss_descendants_of(parent_overlay, preserve_overlay);
718 self.dormant_dismissed_content(&dismissed, &mut *ops);
719 if let Some(restore_id) = focus_restore
720 && self.arena.is_active(restore_id)
721 {
722 self.focus_ops(restore_id, &mut *ops);
723 }
724 return;
725 }
726
727 let Some(menu_root) = self.menu_ancestor_for_widget(source_widget) else {
728 return;
729 };
730 let preserve_overlay = preserve_content
731 .and_then(|content_id| self.overlay_manager.find_by_content(content_id));
732 let overlay_ids: Vec<crate::overlay::OverlayId> = self
733 .overlay_manager
734 .stack
735 .iter()
736 .filter(|overlay| {
737 overlay.parent_overlay.is_none()
738 && self.is_descendant_of(overlay.anchor, menu_root)
739 && !preserve_overlay.is_some_and(|keep| {
740 overlay.id == keep
741 || self.overlay_manager.is_descendant_of(overlay.id, keep)
742 })
743 })
744 .map(|overlay| overlay.id)
745 .collect();
746
747 for overlay_id in overlay_ids {
748 let (dismissed, focus_restore) =
749 self.overlay_manager.dismiss_with_focus_restore(overlay_id);
750 self.dormant_dismissed_content(&dismissed, &mut *ops);
751 if let Some(restore_id) = focus_restore
752 && self.arena.is_active(restore_id)
753 {
754 self.focus_ops(restore_id, &mut *ops);
755 }
756 }
757 }
758
759 pub(super) fn dismiss_self_overlay_chain_for_source(
771 &mut self,
772 source_widget: WidgetId,
773 ops: &mut dyn crate::window::WindowOps,
774 ) {
775 let Some(start) = self.overlay_ancestor_for_widget(source_widget) else {
776 return;
777 };
778
779 let mut topmost_to_dismiss: Option<crate::overlay::OverlayId> = None;
780 let mut current = Some(start);
781 while let Some(overlay_id) = current {
782 let Some(overlay) = self.overlay_manager.overlay(overlay_id) else {
783 break;
784 };
785 if self.overlay_is_host_surface(overlay_id) {
786 break;
787 }
788 topmost_to_dismiss = Some(overlay_id);
789 current = overlay.parent_overlay;
790 }
791
792 if let Some(target) = topmost_to_dismiss {
793 let (dismissed, focus_restore) =
794 self.overlay_manager.dismiss_with_focus_restore(target);
795 self.dormant_dismissed_content(&dismissed, &mut *ops);
796 if let Some(restore_id) = focus_restore
797 && self.arena.is_active(restore_id)
798 {
799 self.focus_ops(restore_id, &mut *ops);
800 }
801 }
802 }
803
804 fn overlay_is_anchor_positioned(placement: &crate::overlay::OverlayPlacement) -> bool {
821 use crate::overlay::OverlayPlacement as P;
822 match placement {
823 P::Below
824 | P::Above
825 | P::TrailingEdge
826 | P::AtPointer(_)
827 | P::AtPointerAvoiding { .. }
828 | P::NearAnchor { .. }
829 | P::BelowPreferred => true,
830 P::Centered
835 | P::BottomCenter
836 | P::ViewportCorner { .. }
837 | P::FullViewport
838 | P::AboveSelection { .. } => false,
839 }
840 }
841
842 fn overlay_follows_focus_out(&self, overlay_id: crate::overlay::OverlayId) -> bool {
877 let Some(overlay) = self.overlay_manager.overlay(overlay_id) else {
878 return false;
879 };
880 if !Self::overlay_is_anchor_positioned(&overlay.placement) {
881 return false;
882 }
883 if overlay.is_dismissing() {
884 return false;
885 }
886 !self
887 .tooltips
888 .iter()
889 .any(|entry| entry.content_id == overlay.content_id)
890 }
891
892 fn overlay_orbit_for_widget(&self, widget_id: WidgetId) -> Option<crate::overlay::OverlayId> {
915 self.overlay_manager
916 .stack
917 .iter()
918 .rev()
919 .find(|overlay| {
920 self.is_descendant_of(widget_id, overlay.content_id)
921 || (self.overlay_follows_focus_out(overlay.id)
922 && self.is_descendant_of(widget_id, overlay.anchor))
923 })
924 .map(|overlay| overlay.id)
925 }
926
927 pub(super) fn dismiss_overlays_left_by_focus(
954 &mut self,
955 old: Option<WidgetId>,
956 new_focus: WidgetId,
957 ops: &mut dyn crate::window::WindowOps,
958 ) {
959 let Some(old) = old else {
960 return;
961 };
962 let Some(old_overlay) = self.overlay_orbit_for_widget(old) else {
963 return;
964 };
965 let new_overlay = self.overlay_ancestor_for_widget(new_focus);
971
972 if let Some(new_overlay) = new_overlay
975 && (new_overlay == old_overlay
976 || self
977 .overlay_manager
978 .is_descendant_of(new_overlay, old_overlay))
979 {
980 return;
981 }
982
983 if !self.overlay_follows_focus_out(old_overlay) {
984 return;
985 }
986
987 let mut topmost = old_overlay;
988 let mut current = self
989 .overlay_manager
990 .overlay(old_overlay)
991 .and_then(|overlay| overlay.parent_overlay);
992 while let Some(overlay_id) = current {
993 if Some(overlay_id) == new_overlay {
997 break;
998 }
999 if self.overlay_is_host_surface(overlay_id) {
1002 break;
1003 }
1004 if !self.overlay_follows_focus_out(overlay_id) {
1005 break;
1006 }
1007 topmost = overlay_id;
1008 current = self
1009 .overlay_manager
1010 .overlay(overlay_id)
1011 .and_then(|overlay| overlay.parent_overlay);
1012 }
1013
1014 let dismissed = self.overlay_manager.dismiss(topmost);
1015 self.dormant_dismissed_content(&dismissed, &mut *ops);
1016 }
1017
1018 pub(super) fn dismiss_all_overlays_except_hosts(
1029 &mut self,
1030 ops: &mut dyn crate::window::WindowOps,
1031 ) {
1032 let to_dismiss: Vec<crate::overlay::OverlayId> = self
1033 .overlay_manager
1034 .stack
1035 .iter()
1036 .map(|overlay| overlay.id)
1037 .filter(|&id| !self.overlay_is_host_surface(id))
1038 .collect();
1039
1040 for overlay_id in to_dismiss {
1041 let dismissed = self.overlay_manager.dismiss(overlay_id);
1042 self.dormant_dismissed_content(&dismissed, &mut *ops);
1043 }
1044 }
1045
1046 pub(super) fn overlay_is_host_surface(&self, overlay_id: crate::overlay::OverlayId) -> bool {
1052 let Some(overlay) = self.overlay_manager.overlay(overlay_id) else {
1053 return false;
1054 };
1055 if matches!(
1064 overlay.placement,
1065 crate::overlay::OverlayPlacement::Centered
1066 ) {
1067 return true;
1068 }
1069 let Some(node) = self.arena.get(overlay.content_id) else {
1070 return false;
1071 };
1072 let role = node
1080 .access_overrides
1081 .as_ref()
1082 .and_then(|o| o.role)
1083 .unwrap_or_else(|| {
1084 let mut builder = AccessNodeBuilder::new();
1085 node.widget.accessibility(&mut builder);
1086 builder.role()
1087 });
1088 matches!(
1089 role,
1090 accesskit::Role::Tooltip
1091 | accesskit::Role::Dialog
1092 | accesskit::Role::AlertDialog
1093 | accesskit::Role::MenuBar
1094 )
1095 }
1096
1097 pub(super) fn dismiss_modal_for_source(
1098 &mut self,
1099 source_widget: WidgetId,
1100 ops: &mut dyn crate::window::WindowOps,
1101 ) -> bool {
1102 let Some(modal_overlay) = self.modal_overlay_for_widget(source_widget) else {
1103 return false;
1104 };
1105
1106 let (dismissed, focus_restore) = self
1107 .overlay_manager
1108 .dismiss_with_focus_restore(modal_overlay);
1109 self.dormant_dismissed_content(&dismissed, &mut *ops);
1110 if let Some(restore_id) = focus_restore
1111 && self.arena.is_active(restore_id)
1112 {
1113 self.focus_ops(restore_id, &mut *ops);
1114 }
1115 true
1116 }
1117
1118 pub fn is_descendant_of(&self, widget_id: WidgetId, ancestor: WidgetId) -> bool {
1119 if widget_id == ancestor {
1120 return true;
1121 }
1122 let mut current = self.arena.parent(widget_id);
1123 while let Some(parent_id) = current {
1124 if parent_id == ancestor {
1125 return true;
1126 }
1127 current = self.arena.parent(parent_id);
1128 }
1129 false
1130 }
1131
1132 fn is_overlay_content_root(&self, widget_id: WidgetId) -> bool {
1139 self.overlay_manager.find_by_content(widget_id).is_some()
1140 }
1141
1142 pub(super) fn tooltip_focus_enter(&mut self, widget_id: WidgetId) {
1171 type ToShow = (WidgetId, WidgetId, crate::overlay::TooltipPlacement);
1194 let mut direct: Vec<ToShow> = Vec::new();
1195 let mut reverse: Vec<ToShow> = Vec::new();
1196 for e in &self.tooltips {
1197 if e.sticky_after.is_none() || e.overlay_id.is_some() || e.suppressed_until_focus_leaves
1198 {
1199 continue;
1200 }
1201 if self.is_descendant_of(widget_id, e.anchor_id) {
1202 direct.push((e.anchor_id, e.content_id, e.placement));
1203 } else if self.is_descendant_of(e.anchor_id, widget_id) {
1204 reverse.push((e.anchor_id, e.content_id, e.placement));
1205 }
1206 }
1207 let mut to_show = direct;
1208 if reverse.len() == 1 && !self.is_overlay_content_root(widget_id) {
1212 to_show.extend(reverse);
1213 }
1214
1215 let sim_now = self.sim_clock;
1224 let real_now = std::time::Instant::now();
1225 for (_anchor_id, content_id, _placement) in to_show {
1226 if let Some(entry) = self
1227 .tooltips
1228 .iter_mut()
1229 .find(|e| e.content_id == content_id)
1230 {
1231 entry.hover_start = Some(sim_now);
1232 entry.real_hover_start = Some(real_now);
1233 entry.hover_origin = None;
1234 entry.armed_by_focus = true;
1235 entry.armed_by_hold = false;
1236 }
1237 }
1238 }
1239
1240 pub(super) fn show_highlight_tooltip(
1250 &mut self,
1251 item_id: WidgetId,
1252 ops: &mut dyn crate::window::WindowOps,
1253 ) {
1254 self.clear_highlight_tooltip(&mut *ops);
1257
1258 let found = self.tooltips.iter().find_map(|e| {
1262 if e.overlay_id.is_none() && self.is_descendant_of(e.anchor_id, item_id) {
1263 Some((e.anchor_id, e.content_id, e.placement))
1264 } else {
1265 None
1266 }
1267 });
1268 let Some((anchor_id, content_id, placement)) = found else {
1269 return;
1270 };
1271
1272 let parent_overlay = self.overlay_ancestor_for_widget(item_id);
1273 self.arena.activate(content_id);
1274 let fade_duration = if self.prefers_reduced_motion {
1275 None
1276 } else {
1277 Some(self.theme.motion.duration_fast)
1278 };
1279 let oid = self.show_overlay(crate::overlay::OverlayRequest {
1280 content_id,
1281 anchor: anchor_id,
1282 placement: Self::tooltip_overlay_placement(placement),
1283 dismiss: crate::overlay::DismissBehavior::Manual,
1284 layer: crate::overlay::OverlayLayer::InTree,
1285 parent_overlay,
1286 on_dismiss: None,
1287 fade_duration,
1288 });
1289 let real_now = std::time::Instant::now();
1290 let sim_now = self.sim_clock;
1291 if let Some(entry) = self
1292 .tooltips
1293 .iter_mut()
1294 .find(|e| e.content_id == content_id)
1295 {
1296 entry.overlay_id = Some(oid);
1297 entry.shown_at_sim = Some(sim_now);
1298 entry.shown_at_real = Some(real_now);
1299 if let Some(sink) = entry.shown_at_sink.as_ref() {
1300 sink.set(Some(real_now));
1301 }
1302 }
1303 self.highlight_tooltip = Some((oid, content_id));
1304 }
1305
1306 pub(super) fn clear_highlight_tooltip(&mut self, ops: &mut dyn crate::window::WindowOps) {
1311 let Some((oid, content_id)) = self.highlight_tooltip.take() else {
1312 return;
1313 };
1314 if self.overlay_manager.stack.iter().any(|o| o.id == oid) {
1315 let dismissed = self.overlay_manager.dismiss(oid);
1316 self.dormant_dismissed_content(&dismissed, &mut *ops);
1317 }
1318 if let Some(entry) = self
1319 .tooltips
1320 .iter_mut()
1321 .find(|e| e.content_id == content_id)
1322 {
1323 entry.overlay_id = None;
1324 entry.shown_at_sim = None;
1325 entry.shown_at_real = None;
1326 if let Some(sink) = entry.shown_at_sink.as_ref() {
1327 sink.set(None);
1328 }
1329 }
1330 }
1331
1332 pub(super) fn tooltip_focus_leave_outside(
1343 &mut self,
1344 new_focus: Option<WidgetId>,
1345 ops: &mut dyn crate::window::WindowOps,
1346 ) {
1347 let to_dismiss: Vec<crate::overlay::OverlayId> = self
1348 .tooltips
1349 .iter()
1350 .filter(|e| e.promoted_by_focus && e.overlay_id.is_some())
1351 .filter(|e| {
1352 let in_scope = new_focus
1353 .map(|nf| {
1354 self.is_descendant_of(nf, e.anchor_id)
1359 || self.is_descendant_of(e.anchor_id, nf)
1360 || self.is_descendant_of(nf, e.content_id)
1361 })
1362 .unwrap_or(false);
1363 !in_scope
1364 })
1365 .filter_map(|e| e.overlay_id)
1366 .collect();
1367
1368 for oid in to_dismiss {
1369 let dismissed = self.overlay_manager.dismiss(oid);
1370 self.dormant_dismissed_content(&dismissed, &mut *ops);
1371 }
1372
1373 for e in &mut self.tooltips {
1376 if e.armed_by_focus && e.overlay_id.is_none() {
1377 e.hover_start = None;
1378 e.real_hover_start = None;
1379 e.armed_by_focus = false;
1380 }
1381 }
1382
1383 let served: Vec<usize> = self
1387 .tooltips
1388 .iter()
1389 .enumerate()
1390 .filter(|(_, e)| e.suppressed_until_focus_leaves)
1391 .filter(|(_, e)| {
1392 let still_inside = new_focus
1393 .map(|nf| {
1394 self.is_descendant_of(nf, e.anchor_id)
1395 || self.is_descendant_of(nf, e.content_id)
1396 })
1397 .unwrap_or(false);
1398 !still_inside
1399 })
1400 .map(|(i, _)| i)
1401 .collect();
1402 for i in served {
1403 self.tooltips[i].suppressed_until_focus_leaves = false;
1404 }
1405 }
1406
1407 fn tooltip_hover_targets_anchor(&self, widget_id: WidgetId, anchor_id: WidgetId) -> bool {
1427 if !self.is_descendant_of(widget_id, anchor_id) {
1428 return false;
1429 }
1430 if let Some(overlay_id) = self.overlay_ancestor_for_widget(widget_id)
1431 && let Some(content_id) = self
1432 .overlay_manager
1433 .overlay(overlay_id)
1434 .map(|o| o.content_id)
1435 && !self.is_descendant_of(anchor_id, content_id)
1436 {
1437 return false;
1438 }
1439 true
1440 }
1441
1442 pub(super) fn tooltip_index_for(&self, widget_id: WidgetId) -> Option<usize> {
1460 self.tooltips
1461 .iter()
1462 .enumerate()
1463 .filter(|(_, entry)| self.tooltip_hover_targets_anchor(widget_id, entry.anchor_id))
1464 .filter_map(|(index, entry)| {
1465 self.ancestor_distance(widget_id, entry.anchor_id)
1466 .map(|depth| (depth, index))
1467 })
1468 .min()
1469 .map(|(_, index)| index)
1470 }
1471
1472 pub(super) fn tooltip_pointer_enter(&mut self, widget_id: WidgetId) {
1473 let Some(index) = self.tooltip_index_for(widget_id) else {
1474 return;
1475 };
1476 if self.tooltips[index].overlay_id.is_some() {
1478 return;
1479 }
1480 self.tooltips[index].hover_start = Some(self.sim_clock);
1481 self.tooltips[index].real_hover_start = Some(std::time::Instant::now());
1482 self.tooltips[index].hover_origin = self.hover_owner_position();
1483 self.tooltips[index].armed_by_focus = false;
1484 self.tooltips[index].armed_by_hold = false;
1485 self.arena.mark_needs_paint(self.tooltips[index].anchor_id);
1486 }
1487
1488 fn ancestor_distance(&self, widget_id: WidgetId, ancestor: WidgetId) -> Option<usize> {
1491 let mut current = widget_id;
1492 let mut depth = 0usize;
1493 loop {
1494 if current == ancestor {
1495 return Some(depth);
1496 }
1497 current = self.arena.parent(current)?;
1498 depth += 1;
1499 }
1500 }
1501
1502 pub(super) fn tooltip_pointer_press(&mut self, at: Option<teksilo_canvas::Point>) {
1517 let mut to_dismiss = Vec::new();
1518 let candidates: Vec<(usize, Option<crate::overlay::OverlayId>)> = self
1519 .tooltips
1520 .iter()
1521 .enumerate()
1522 .map(|(i, e)| (i, if e.is_sticky { None } else { e.overlay_id }))
1523 .collect();
1524 for (index, overlay_id) in candidates {
1525 let inside = match (overlay_id, at) {
1530 (Some(oid), Some(pos)) => self
1531 .overlay_content_bounds(oid)
1532 .is_some_and(|rect| rect.contains(pos)),
1533 _ => false,
1534 };
1535 if inside {
1536 continue;
1537 }
1538 let entry = &mut self.tooltips[index];
1539 entry.hover_start = None;
1540 entry.real_hover_start = None;
1541 entry.hover_origin = None;
1542 if let Some(oid) = overlay_id {
1543 to_dismiss.push(oid);
1544 }
1545 }
1546 for overlay_id in to_dismiss {
1547 self.dismiss_overlay(overlay_id);
1548 }
1549 }
1550
1551 pub(super) fn tooltip_escape_pressed(&mut self) {
1573 self.tooltip_pointer_press(None);
1576 }
1577
1578 pub(super) fn tooltip_window_deactivated(&mut self) {
1585 self.tooltip_pointer_press(None);
1588 }
1589
1590 pub(crate) fn tooltip_cancel_pending_dwell(&mut self) {
1598 for entry in &mut self.tooltips {
1599 if entry.overlay_id.is_none() {
1600 entry.hover_start = None;
1601 entry.real_hover_start = None;
1602 entry.hover_origin = None;
1603 }
1604 }
1605 }
1606
1607 pub(super) fn tooltip_pointer_moved(
1611 &mut self,
1612 widget_id: WidgetId,
1613 position: teksilo_canvas::Point,
1614 ) {
1615 let matching: Vec<usize> = self
1616 .tooltips
1617 .iter()
1618 .enumerate()
1619 .filter(|(_, entry)| {
1620 entry.overlay_id.is_none()
1621 && entry.hover_start.is_some()
1622 && !entry.armed_by_focus
1623 && self.tooltip_hover_targets_anchor(widget_id, entry.anchor_id)
1624 })
1625 .map(|(index, _)| index)
1626 .collect();
1627 if matching.is_empty() {
1628 return;
1629 }
1630 let now = self.sim_clock;
1631 let real_now = std::time::Instant::now();
1632 let slop = super::TOOLTIP_STATIONARY_SLOP;
1633 let slop_sq = slop * slop;
1634 for index in matching {
1635 let origin = match self.tooltips[index].hover_origin {
1636 Some(o) => o,
1637 None => {
1638 self.tooltips[index].hover_origin = Some(position);
1642 continue;
1643 }
1644 };
1645 let dx = position.x - origin.x;
1646 let dy = position.y - origin.y;
1647 if dx * dx + dy * dy <= slop_sq {
1648 continue;
1649 }
1650 self.tooltips[index].hover_start = Some(now);
1652 self.tooltips[index].real_hover_start = Some(real_now);
1653 self.tooltips[index].hover_origin = Some(position);
1654 }
1655 }
1656
1657 pub fn promote_tooltip_to_sticky(&mut self, content_id: WidgetId) {
1674 let Some(entry) = self
1675 .tooltips
1676 .iter_mut()
1677 .find(|entry| entry.content_id == content_id)
1678 else {
1679 return;
1680 };
1681 if entry.is_sticky {
1682 return;
1683 }
1684 entry.is_sticky = true;
1685 let overlay_id = entry.overlay_id;
1686 if let Some(overlay_id) = overlay_id {
1687 self.overlay_manager.set_dismiss(
1688 overlay_id,
1689 crate::overlay::DismissBehavior::EscapeOrClickOutside,
1690 );
1691 }
1692 }
1693
1694 pub(super) fn tooltip_pointer_leave(
1695 &mut self,
1696 widget_id: WidgetId,
1697 _ops: &mut dyn crate::window::WindowOps,
1698 ) {
1699 let matching: Vec<usize> = self
1700 .tooltips
1701 .iter()
1702 .enumerate()
1703 .filter(|(_, entry)| self.is_descendant_of(widget_id, entry.anchor_id))
1704 .map(|(index, _)| index)
1705 .collect();
1706 for index in matching {
1707 self.tooltips[index].hover_start = None;
1710 self.tooltips[index].real_hover_start = None;
1711 self.tooltips[index].hover_origin = None;
1712 }
1724 }
1725
1726 pub fn next_timer_deadline(&self) -> Option<std::time::Instant> {
1728 let now = std::time::Instant::now();
1729 let session_active = self.tooltip_session_active_real(now);
1730 let tooltip_deadline = self
1731 .tooltips
1732 .iter()
1733 .filter(|entry| entry.overlay_id.is_none())
1734 .filter_map(|entry| {
1735 let start = entry.real_hover_start?;
1736 let delay = self.effective_tooltip_delay(entry.delay, session_active);
1737 Some(start + delay)
1738 })
1739 .min();
1740
1741 let ref_time = self.last_frame_time.unwrap_or_else(std::time::Instant::now);
1762 let dwell_tooltip_deadline = self
1763 .tooltips
1764 .iter()
1765 .filter_map(|entry| {
1766 let sticky_after = entry.sticky_after?;
1767 let shown_at = entry.shown_at_real?;
1768 if entry.overlay_id.is_none() || entry.is_sticky {
1769 return None;
1770 }
1771 let dwell_step = sticky_after / super::TOOLTIP_DWELL_STEPS;
1772 if dwell_step.is_zero() {
1773 return None;
1774 }
1775 let elapsed = ref_time.saturating_duration_since(shown_at);
1776 if elapsed >= sticky_after {
1777 return None;
1778 }
1779 let steps_passed = (elapsed.as_millis() / dwell_step.as_millis()) as u32;
1782 let next_step_at = shown_at + dwell_step * (steps_passed + 1);
1783 Some(next_step_at.min(shown_at + sticky_after))
1784 })
1785 .min();
1786 let delayed_overlay_deadline = self
1787 .pending_delayed_overlays
1788 .iter()
1789 .map(|pending| pending.real_requested_at + pending.delay)
1790 .min();
1791 let auto_dismiss_deadline = self.overlay_manager.next_auto_dismiss_deadline();
1792 let pointer_leave_deadline = self.overlay_manager.next_pointer_leave_deadline();
1799 let fade_dismiss_deadline = self.overlay_manager.next_fade_dismiss_deadline();
1806 let animation_deadline = self
1807 .animation_scheduler
1808 .next_deadline(&self.arena, self.paint_epoch);
1809 let animated_quad_deadline = self
1814 .animated_quads
1815 .next_deadline(&self.arena, self.paint_epoch);
1816 let gesture_deadline = self.next_input_deadline();
1822 let wake_at_deadline = self.pending_wake_at.get();
1823 let frame_tick_deadline = self.frame_tick_deadline();
1829
1830 [
1831 tooltip_deadline,
1832 dwell_tooltip_deadline,
1833 delayed_overlay_deadline,
1834 auto_dismiss_deadline,
1835 pointer_leave_deadline,
1836 fade_dismiss_deadline,
1837 animation_deadline,
1838 animated_quad_deadline,
1839 gesture_deadline,
1840 wake_at_deadline,
1841 frame_tick_deadline,
1842 ]
1843 .into_iter()
1844 .flatten()
1845 .min()
1846 }
1847
1848 pub fn overlay_manager(&self) -> &crate::overlay::OverlayManager {
1849 &self.overlay_manager
1850 }
1851
1852 pub fn overlay_manager_mut(&mut self) -> &mut crate::overlay::OverlayManager {
1857 &mut self.overlay_manager
1858 }
1859
1860 pub fn active_overlays(&self) -> Vec<crate::overlay::OverlayId> {
1861 self.overlay_manager.active_ids()
1862 }
1863
1864 pub fn overlay_content_bounds(
1873 &self,
1874 id: crate::overlay::OverlayId,
1875 ) -> Option<teksilo_canvas::Rect> {
1876 self.overlay_manager
1877 .stack
1878 .iter()
1879 .find(|o| o.id == id)
1880 .map(|o| o.bounds)
1881 }
1882
1883 pub fn show_overlay(
1884 &mut self,
1885 request: crate::overlay::OverlayRequest,
1886 ) -> crate::overlay::OverlayId {
1887 let fade_duration = request.fade_duration;
1888 let content_id = request.content_id;
1889 let is_modal = matches!(
1890 request.placement,
1891 crate::overlay::OverlayPlacement::Centered
1892 );
1893 let id = self.overlay_manager.show(request);
1894 self.cancel_pointers_for_modal(is_modal);
1895 self.a11y_dirty = true;
1903 if let Some(duration) = fade_duration {
1904 self.attach_overlay_fade(id, content_id, duration);
1905 }
1906 id
1907 }
1908
1909 pub fn show_overlay_in_band(
1919 &mut self,
1920 request: crate::overlay::OverlayRequest,
1921 band: crate::overlay::OverlayBand,
1922 ) -> crate::overlay::OverlayId {
1923 let fade_duration = request.fade_duration;
1924 let content_id = request.content_id;
1925 let is_modal = matches!(
1926 request.placement,
1927 crate::overlay::OverlayPlacement::Centered
1928 );
1929 let id = self.overlay_manager.show_in_band(request, band);
1930 self.cancel_pointers_for_modal(is_modal);
1931 self.a11y_dirty = true;
1932 if let Some(duration) = fade_duration {
1933 self.attach_overlay_fade(id, content_id, duration);
1934 }
1935 id
1936 }
1937
1938 pub fn show_overlay_from_source(
1941 &mut self,
1942 source_widget: WidgetId,
1943 mut request: crate::overlay::OverlayRequest,
1944 ) -> crate::overlay::OverlayId {
1945 if request.parent_overlay.is_none() {
1946 request.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
1947 }
1948 if let Some(existing) = self.overlay_manager.find_by_content(request.content_id) {
1949 return existing;
1950 }
1951
1952 let fade_duration = request.fade_duration;
1953 let content_id = request.content_id;
1954 let is_modal = matches!(
1955 request.placement,
1956 crate::overlay::OverlayPlacement::Centered
1957 );
1958 let current_focus = self.focused;
1959 let id = self.overlay_manager.show(request);
1960 self.cancel_pointers_for_modal(is_modal);
1961 self.a11y_dirty = true;
1962 if let Some(focus_id) = current_focus {
1963 self.overlay_manager.set_top_focus_restore(focus_id);
1964 }
1965 if let Some(duration) = fade_duration {
1966 self.attach_overlay_fade(id, content_id, duration);
1967 }
1968 id
1969 }
1970
1971 pub fn show_overlay_for(
1972 &mut self,
1973 request: crate::overlay::OverlayRequest,
1974 duration: std::time::Duration,
1975 ) -> crate::overlay::OverlayId {
1976 let fade_duration = request.fade_duration;
1977 let content_id = request.content_id;
1978 let is_modal = matches!(
1979 request.placement,
1980 crate::overlay::OverlayPlacement::Centered
1981 );
1982 let id = self.overlay_manager.show_for(request, duration);
1983 self.cancel_pointers_for_modal(is_modal);
1984 self.overlay_manager.set_shown_at_sim(id, self.sim_clock);
1985 self.a11y_dirty = true;
1986 if let Some(fade) = fade_duration {
1987 self.attach_overlay_fade(id, content_id, fade);
1988 }
1989 id
1990 }
1991
1992 fn attach_overlay_fade(
2000 &mut self,
2001 overlay_id: crate::overlay::OverlayId,
2002 content_id: WidgetId,
2003 duration: std::time::Duration,
2004 ) {
2005 let opacity = crate::signal::Signal::<f32>::new_animated(0.0);
2006 self.register_animated_signal(&opacity, content_id);
2007 self.set_opacity(content_id, opacity.clone());
2008 if self.prefers_reduced_motion() {
2012 opacity.set(1.0);
2013 self.overlay_manager
2014 .attach_fade(overlay_id, opacity, std::time::Duration::ZERO);
2015 return;
2016 }
2017 let _ = opacity.try_animate_with_options(crate::animation::AnimationRequest {
2018 target: 1.0,
2019 duration,
2020 easing: teksilo_tokens::Easing::EaseOut,
2021 frame_interval: None,
2022 looping: false,
2023 epsilon: 0.0,
2024 max_duration: None,
2025 });
2026 self.overlay_manager
2027 .attach_fade(overlay_id, opacity, duration);
2028 }
2029
2030 pub fn dismiss_overlay(&mut self, id: crate::overlay::OverlayId) {
2035 let mut noop = crate::window::NoopWindowOps;
2036 self.dismiss_overlay_with_ops(id, &mut noop);
2037 }
2038
2039 pub fn dismiss_overlay_with_ops(
2042 &mut self,
2043 id: crate::overlay::OverlayId,
2044 ops: &mut dyn crate::window::WindowOps,
2045 ) {
2046 let dismissed = self.overlay_manager.dismiss(id);
2047 self.dormant_dismissed_content(&dismissed, &mut *ops);
2048 }
2049
2050 fn cancel_pointers_for_modal(&mut self, is_modal: bool) {
2061 if !is_modal {
2062 return;
2063 }
2064 let mut noop = crate::window::NoopWindowOps;
2065 self.cancel_all_pointers(crate::pointer::CancelReason::ModalOpened, &mut noop);
2066 }
2067
2068 pub(super) fn dormant_dismissed_content(
2069 &mut self,
2070 content_ids: &[WidgetId],
2071 ops: &mut dyn crate::window::WindowOps,
2072 ) {
2073 let mut any_tooltip_dismissed = false;
2079 for &id in content_ids {
2080 if let Some(entry) = self.tooltips.iter_mut().find(|e| e.content_id == id) {
2081 entry.suppressed_until_focus_leaves = entry.promoted_by_focus;
2088 entry.overlay_id = None;
2089 entry.is_sticky = false;
2090 entry.hover_start = None;
2091 entry.real_hover_start = None;
2092 entry.hover_origin = None;
2093 entry.shown_at_sim = None;
2094 entry.shown_at_real = None;
2095 entry.promoted_by_focus = false;
2096 entry.armed_by_focus = false;
2097 entry.armed_by_hold = false;
2098 if let Some(sink) = entry.shown_at_sink.as_ref() {
2099 sink.set(None);
2100 }
2101 any_tooltip_dismissed = true;
2102 }
2103 }
2104 if any_tooltip_dismissed {
2105 let grace = super::TOOLTIP_SESSION_GRACE;
2106 self.tooltip_session_until_sim = Some(self.sim_clock + grace);
2107 self.tooltip_session_until_real = Some(std::time::Instant::now() + grace);
2108 }
2109 for &id in content_ids {
2110 let focused_in_subtree = self
2111 .focused
2112 .filter(|focused| self.is_descendant_of(*focused, id));
2113 let hovered_in_subtree = self
2114 .hovered_id()
2115 .filter(|hovered| self.is_descendant_of(*hovered, id));
2116
2117 if let Some(focused) = focused_in_subtree {
2118 self.dispatch_to_widget(focused, &WidgetEvent::FocusLost, &mut *ops);
2119 if self
2120 .focused
2121 .is_some_and(|current| self.is_descendant_of(current, id))
2122 {
2123 let old = self.focused;
2124 self.set_focused(None);
2125 self.focus_origin = None;
2126 self.update_focus_within_signals(old, None);
2127 self.update_view_focus_signals(old, None);
2128 }
2129 }
2130
2131 self.cancel_pointers_in_subtree(
2137 id,
2138 crate::pointer::CancelReason::OverlayDismissed,
2139 &mut *ops,
2140 );
2141 let _parked = self.arena.set_dormant(id);
2142
2143 if hovered_in_subtree.is_some() {
2144 let old = self.hovered_id();
2145 self.set_hovered(None);
2146 self.update_hover_within_signals(old, None);
2147 }
2148 }
2149 if !content_ids.is_empty() {
2150 self.cached_frame = None;
2151 self.a11y_dirty = true;
2152 }
2153 }
2154
2155 pub fn is_visible(&self, id: WidgetId) -> bool {
2156 self.arena.is_active(id)
2157 }
2158}
2159
2160#[cfg(test)]
2161mod tests {
2162 use super::*;
2163 use crate::test_widgets::FillWidget;
2164
2165 #[test]
2166 fn is_visible_reflects_dormancy() {
2167 let mut tree = WidgetTree::new();
2168 let widget = tree.add(FillWidget::new());
2169 tree.layout(SizeProposal::exact(100.0, 50.0));
2170
2171 assert!(tree.is_visible(widget));
2172 tree.set_dormant(widget);
2173 assert!(!tree.is_visible(widget));
2174 tree.activate(widget);
2175 assert!(tree.is_visible(widget));
2176 }
2177
2178 #[test]
2179 fn frame_tick_deadline_caps_per_frame_effects_at_60hz() {
2180 let mut tree = WidgetTree::new();
2181 assert!(tree.frame_tick_deadline().is_none());
2183
2184 tree.request_frame();
2187 let t0 = std::time::Instant::now();
2189 tree.last_frame_time = Some(t0);
2190
2191 let deadline = tree
2192 .frame_tick_deadline()
2193 .expect("an armed per-frame effect must publish a deadline");
2194 let interval = deadline.saturating_duration_since(t0);
2195 assert!(
2198 interval >= std::time::Duration::from_micros(16_000)
2199 && interval <= std::time::Duration::from_micros(17_500),
2200 "per-frame effects must pace at ~60 Hz (got {interval:?})"
2201 );
2202
2203 assert_eq!(
2206 tree.next_timer_deadline(),
2207 Some(deadline),
2208 "frame-tick deadline must be surfaced by next_timer_deadline"
2209 );
2210 }
2211
2212 #[test]
2213 fn throttled_subscriber_stretches_deadline_but_per_frame_wins_min() {
2214 use crate::test_widgets::FillWidget;
2215 let mut tree = WidgetTree::new();
2216 let w = tree.add(FillWidget::new());
2217 let _throttled =
2219 tree.subscribe_frame_tick_throttled(w, std::time::Duration::from_millis(1500));
2220 tree.request_frame();
2221 let t0 = std::time::Instant::now();
2222 tree.last_frame_time = Some(t0);
2223
2224 let d = tree.frame_tick_deadline().expect("armed");
2227 let dt = d.saturating_duration_since(t0);
2228 assert!(
2229 dt >= std::time::Duration::from_millis(1490)
2230 && dt <= std::time::Duration::from_millis(1510),
2231 "a lone throttled subscriber must pace at its interval (~1.5 s), got {dt:?}"
2232 );
2233
2234 let w2 = tree.add(FillWidget::new());
2238 let _per_frame = tree.subscribe_frame_tick(w2);
2239 let d2 = tree.frame_tick_deadline().expect("armed");
2240 let dt2 = d2.saturating_duration_since(t0);
2241 assert!(
2242 dt2 <= std::time::Duration::from_millis(20),
2243 "a per-frame subscriber must pull the shared deadline to 60 Hz, got {dt2:?}"
2244 );
2245 }
2246
2247 #[test]
2248 fn show_and_dismiss_overlay() {
2249 let mut tree = WidgetTree::new();
2250 let anchor = tree.add(FillWidget::new());
2251 let content = tree.add(FillWidget::new().label("Overlay"));
2252 tree.layout(SizeProposal::exact(200.0, 100.0));
2253
2254 assert!(tree.active_overlays().is_empty());
2255
2256 let id = tree.show_overlay(crate::overlay::OverlayRequest {
2257 content_id: content,
2258 anchor,
2259 placement: crate::overlay::OverlayPlacement::Below,
2260 dismiss: crate::overlay::DismissBehavior::Manual,
2261 layer: crate::overlay::OverlayLayer::InTree,
2262 parent_overlay: None,
2263 on_dismiss: None,
2264 fade_duration: None,
2265 });
2266
2267 assert_eq!(tree.active_overlays().len(), 1);
2268
2269 tree.dismiss_overlay(id);
2270 assert!(tree.active_overlays().is_empty());
2271 assert!(!tree.is_visible(content));
2272 }
2273
2274 #[test]
2279 fn modal_centered_overlay_is_a_host_surface() {
2280 let mut tree = WidgetTree::new();
2281 let anchor = tree.add(FillWidget::new());
2282 let modal_content = tree.add(FillWidget::new().label("Modal"));
2283 let dropdown_content = tree.add(FillWidget::new().label("Dropdown"));
2284 tree.layout(SizeProposal::exact(200.0, 100.0));
2285
2286 let modal = tree.show_overlay(crate::overlay::OverlayRequest {
2287 content_id: modal_content,
2288 anchor,
2289 placement: crate::overlay::OverlayPlacement::Centered,
2290 dismiss: crate::overlay::DismissBehavior::EscapeOrClickOutside,
2291 layer: crate::overlay::OverlayLayer::InTree,
2292 parent_overlay: None,
2293 on_dismiss: None,
2294 fade_duration: None,
2295 });
2296 let dropdown = tree.show_overlay(crate::overlay::OverlayRequest {
2297 content_id: dropdown_content,
2298 anchor,
2299 placement: crate::overlay::OverlayPlacement::Below,
2300 dismiss: crate::overlay::DismissBehavior::EscapeOrClickOutside,
2301 layer: crate::overlay::OverlayLayer::InTree,
2302 parent_overlay: Some(modal),
2303 on_dismiss: None,
2304 fade_duration: None,
2305 });
2306
2307 assert!(
2308 tree.overlay_is_host_surface(modal),
2309 "a Centered modal overlay must be treated as a host surface"
2310 );
2311 assert!(
2312 !tree.overlay_is_host_surface(dropdown),
2313 "a plain dropdown overlay is not a host surface"
2314 );
2315 }
2316
2317 #[test]
2318 fn escape_dismisses_topmost_overlay() {
2319 let mut tree = WidgetTree::new();
2320 let anchor = tree.add(FillWidget::new().focusable());
2321 let content = tree.add(FillWidget::new());
2322 tree.layout(SizeProposal::exact(200.0, 100.0));
2323 tree.focus(anchor);
2324
2325 tree.show_overlay(crate::overlay::OverlayRequest {
2326 content_id: content,
2327 anchor,
2328 placement: crate::overlay::OverlayPlacement::Below,
2329 dismiss: crate::overlay::DismissBehavior::EscapeOrClickOutside,
2330 layer: crate::overlay::OverlayLayer::InTree,
2331 parent_overlay: None,
2332 on_dismiss: None,
2333 fade_duration: None,
2334 });
2335
2336 assert_eq!(tree.active_overlays().len(), 1);
2337
2338 tree.press_key(Key::Escape, Modifiers::NONE);
2339 assert!(tree.active_overlays().is_empty());
2340 assert!(!tree.is_visible(content));
2341 }
2342
2343 fn show_two_nested_overlays(tree: &mut WidgetTree) -> (WidgetId, WidgetId) {
2346 let anchor = tree.add(FillWidget::new());
2347 let c1 = tree.add(FillWidget::new());
2348 let c2 = tree.add(FillWidget::new());
2349 tree.layout(SizeProposal::exact(200.0, 100.0));
2350 let o1 = tree.show_overlay(crate::overlay::OverlayRequest {
2351 content_id: c1,
2352 anchor,
2353 placement: crate::overlay::OverlayPlacement::Below,
2354 dismiss: crate::overlay::DismissBehavior::Manual,
2355 layer: crate::overlay::OverlayLayer::InTree,
2356 parent_overlay: None,
2357 on_dismiss: None,
2358 fade_duration: None,
2359 });
2360 tree.show_overlay(crate::overlay::OverlayRequest {
2361 content_id: c2,
2362 anchor: c1,
2363 placement: crate::overlay::OverlayPlacement::Below,
2364 dismiss: crate::overlay::DismissBehavior::Manual,
2365 layer: crate::overlay::OverlayLayer::InTree,
2366 parent_overlay: Some(o1),
2367 on_dismiss: None,
2368 fade_duration: None,
2369 });
2370 (c1, c2)
2371 }
2372
2373 #[test]
2374 fn nested_overlay_back_key_dismisses_with_arrow_left_under_ltr() {
2375 let mut tree = WidgetTree::new();
2376 let _ = show_two_nested_overlays(&mut tree);
2377 assert_eq!(tree.active_overlays().len(), 2);
2378
2379 tree.press_key(Key::ArrowRight, Modifiers::NONE);
2381 assert_eq!(tree.active_overlays().len(), 2);
2382
2383 tree.press_key(Key::ArrowLeft, Modifiers::NONE);
2385 assert_eq!(tree.active_overlays().len(), 1);
2386 }
2387
2388 #[test]
2389 fn nested_overlay_back_key_flips_to_arrow_right_under_rtl() {
2390 let mut tree = WidgetTree::new();
2391 tree.set_layout_direction(crate::environment::LayoutDirection::RightToLeft);
2392 let _ = show_two_nested_overlays(&mut tree);
2393 assert_eq!(tree.active_overlays().len(), 2);
2394
2395 tree.press_key(Key::ArrowLeft, Modifiers::NONE);
2398 assert_eq!(tree.active_overlays().len(), 2);
2399
2400 tree.press_key(Key::ArrowRight, Modifiers::NONE);
2402 assert_eq!(tree.active_overlays().len(), 1);
2403 }
2404
2405 #[derive(Debug)]
2408 struct KeyProbe {
2409 seen: std::rc::Rc<std::cell::RefCell<Vec<Key>>>,
2410 }
2411
2412 impl Widget for KeyProbe {
2413 fn layout_response(
2414 &self,
2415 proposal: SizeProposal,
2416 _ctx: &crate::widget::LayoutContext,
2417 ) -> crate::widget::LayoutResponse {
2418 proposal.resolve(0.0, 0.0).into()
2419 }
2420
2421 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
2422 let seen = self.seen.clone();
2423 ctx.apply_self_handlers(
2424 crate::widget_builder::HandlerSet::new()
2425 .focusable(true)
2426 .on_key(move |event, _ctx| match event {
2427 WidgetEvent::KeyDown { key, .. } => {
2428 seen.borrow_mut().push(*key);
2429 crate::event::EventResponse::Handled
2430 }
2431 _ => crate::event::EventResponse::Ignored,
2432 }),
2433 );
2434 Vec::new()
2435 }
2436 }
2437
2438 #[test]
2450 fn back_key_ignores_text_affordance_overlays_and_reaches_the_editor() {
2451 let mut tree = WidgetTree::new();
2452 let seen = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
2453 let editor = tree.add(KeyProbe { seen: seen.clone() });
2454 let handles_a = tree.add(FillWidget::new());
2455 let handles_b = tree.add(FillWidget::new());
2456 tree.layout(SizeProposal::exact(200.0, 100.0));
2457 tree.focus(editor);
2458
2459 for content in [handles_a, handles_b] {
2461 tree.show_overlay_in_band(
2462 crate::overlay::OverlayRequest {
2463 content_id: content,
2464 anchor: editor,
2465 placement: crate::overlay::OverlayPlacement::FullViewport,
2466 dismiss: crate::overlay::DismissBehavior::Manual,
2467 layer: crate::overlay::OverlayLayer::InTree,
2468 parent_overlay: None,
2469 on_dismiss: None,
2470 fade_duration: None,
2471 },
2472 crate::overlay::OverlayBand::TextAffordance,
2473 );
2474 }
2475 assert_eq!(tree.active_overlays().len(), 2);
2476
2477 tree.press_key(Key::ArrowLeft, Modifiers::NONE);
2478
2479 assert_eq!(
2480 tree.active_overlays().len(),
2481 2,
2482 "a text affordance is not a menu cascade level — the back key must \
2483 leave both hosts standing"
2484 );
2485 assert_eq!(
2486 seen.borrow().as_slice(),
2487 &[Key::ArrowLeft],
2488 "ArrowLeft must reach the focused editor; swallowing it is the bug \
2489 that left the caret unable to step left in any dual-pane tab"
2490 );
2491 }
2492
2493 #[test]
2497 fn back_key_still_closes_a_submenu_over_a_mounted_text_affordance() {
2498 let mut tree = WidgetTree::new();
2499 let affordance = tree.add(FillWidget::new());
2500 let anchor = tree.add(FillWidget::new());
2501 let menu = tree.add(FillWidget::new());
2502 let submenu = tree.add(FillWidget::new());
2503 tree.layout(SizeProposal::exact(200.0, 100.0));
2504 tree.show_overlay_in_band(
2505 crate::overlay::OverlayRequest {
2506 content_id: affordance,
2507 anchor,
2508 placement: crate::overlay::OverlayPlacement::FullViewport,
2509 dismiss: crate::overlay::DismissBehavior::Manual,
2510 layer: crate::overlay::OverlayLayer::InTree,
2511 parent_overlay: None,
2512 on_dismiss: None,
2513 fade_duration: None,
2514 },
2515 crate::overlay::OverlayBand::TextAffordance,
2516 );
2517 let parent = tree.show_overlay(crate::overlay::OverlayRequest {
2518 content_id: menu,
2519 anchor,
2520 placement: crate::overlay::OverlayPlacement::Below,
2521 dismiss: crate::overlay::DismissBehavior::Manual,
2522 layer: crate::overlay::OverlayLayer::InTree,
2523 parent_overlay: None,
2524 on_dismiss: None,
2525 fade_duration: None,
2526 });
2527 tree.show_overlay(crate::overlay::OverlayRequest {
2528 content_id: submenu,
2529 anchor: menu,
2530 placement: crate::overlay::OverlayPlacement::Below,
2531 dismiss: crate::overlay::DismissBehavior::Manual,
2532 layer: crate::overlay::OverlayLayer::InTree,
2533 parent_overlay: Some(parent),
2534 on_dismiss: None,
2535 fade_duration: None,
2536 });
2537 assert_eq!(tree.active_overlays().len(), 3);
2538
2539 tree.press_key(Key::ArrowLeft, Modifiers::NONE);
2540 assert_eq!(
2541 tree.active_overlays().len(),
2542 2,
2543 "the submenu must still close: the affordance host narrows the \
2544 count, it does not disarm the back key"
2545 );
2546 }
2547
2548 #[test]
2553 fn back_key_does_not_dismiss_a_dialog_on_top_of_a_modal() {
2554 let mut tree = WidgetTree::new();
2555 let anchor = tree.add(FillWidget::new());
2556 let scrim1 = tree.add(FillWidget::new());
2557 let scrim2 = tree.add(FillWidget::new());
2558 let dialog = tree.add(FillWidget::new());
2559 tree.layout(SizeProposal::exact(200.0, 100.0));
2560 for c in [scrim1, scrim2] {
2562 tree.show_overlay(crate::overlay::OverlayRequest {
2563 content_id: c,
2564 anchor,
2565 placement: crate::overlay::OverlayPlacement::Below,
2566 dismiss: crate::overlay::DismissBehavior::Manual,
2567 layer: crate::overlay::OverlayLayer::InTree,
2568 parent_overlay: None,
2569 on_dismiss: None,
2570 fade_duration: None,
2571 });
2572 }
2573 tree.show_overlay(crate::overlay::OverlayRequest {
2575 content_id: dialog,
2576 anchor,
2577 placement: crate::overlay::OverlayPlacement::Centered,
2578 dismiss: crate::overlay::DismissBehavior::Manual,
2579 layer: crate::overlay::OverlayLayer::InTree,
2580 parent_overlay: None,
2581 on_dismiss: None,
2582 fade_duration: None,
2583 });
2584 assert_eq!(tree.active_overlays().len(), 3);
2585
2586 tree.press_key(Key::ArrowLeft, Modifiers::NONE);
2589 assert_eq!(
2590 tree.active_overlays().len(),
2591 3,
2592 "the back key must not close a dialog sitting on top of a modal"
2593 );
2594 }
2595
2596 #[test]
2597 fn escape_does_not_dismiss_manual_overlay() {
2598 let mut tree = WidgetTree::new();
2599 let anchor = tree.add(FillWidget::new().focusable());
2600 let content = tree.add(FillWidget::new());
2601 tree.layout(SizeProposal::exact(200.0, 100.0));
2602 tree.focus(anchor);
2603
2604 tree.show_overlay(crate::overlay::OverlayRequest {
2605 content_id: content,
2606 anchor,
2607 placement: crate::overlay::OverlayPlacement::Below,
2608 dismiss: crate::overlay::DismissBehavior::Manual,
2609 layer: crate::overlay::OverlayLayer::InTree,
2610 parent_overlay: None,
2611 on_dismiss: None,
2612 fade_duration: None,
2613 });
2614
2615 assert_eq!(tree.active_overlays().len(), 1);
2616
2617 tree.press_key(Key::Escape, Modifiers::NONE);
2618 assert_eq!(tree.active_overlays().len(), 1);
2620 }
2621
2622 #[test]
2623 fn click_outside_dismisses_overlay() {
2624 let mut tree = WidgetTree::new();
2625 let anchor = tree.add(FillWidget::new());
2626 let content = tree.add(FillWidget::new());
2627 tree.layout(SizeProposal::exact(200.0, 100.0));
2628
2629 let overlay = tree.show_overlay(crate::overlay::OverlayRequest {
2630 content_id: content,
2631 anchor,
2632 placement: crate::overlay::OverlayPlacement::Below,
2633 dismiss: crate::overlay::DismissBehavior::ClickOutside,
2634 layer: crate::overlay::OverlayLayer::InTree,
2635 parent_overlay: None,
2636 on_dismiss: None,
2637 fade_duration: None,
2638 });
2639
2640 tree.overlay_manager
2641 .set_content_bounds(overlay, teksilo_canvas::Size::new(100.0, 50.0));
2642
2643 assert_eq!(tree.active_overlays().len(), 1);
2644
2645 tree.dispatch_event(WidgetEvent::pointer_down(
2646 Point::new(500.0, 500.0),
2647 PointerButton::Primary,
2648 Modifiers::NONE,
2649 ));
2650 assert!(tree.active_overlays().is_empty());
2651 assert!(!tree.is_visible(content));
2652 }
2653
2654 #[test]
2655 fn cascade_dismissal() {
2656 let mut tree = WidgetTree::new();
2657 let anchor = tree.add(FillWidget::new());
2658 let content_a = tree.add(FillWidget::new());
2659 let content_b = tree.add(FillWidget::new());
2660 tree.layout(SizeProposal::exact(200.0, 100.0));
2661
2662 let parent = tree.show_overlay(crate::overlay::OverlayRequest {
2663 content_id: content_a,
2664 anchor,
2665 placement: crate::overlay::OverlayPlacement::Below,
2666 dismiss: crate::overlay::DismissBehavior::Manual,
2667 layer: crate::overlay::OverlayLayer::InTree,
2668 parent_overlay: None,
2669 on_dismiss: None,
2670 fade_duration: None,
2671 });
2672 tree.show_overlay(crate::overlay::OverlayRequest {
2673 content_id: content_b,
2674 anchor: content_a,
2675 placement: crate::overlay::OverlayPlacement::TrailingEdge,
2676 dismiss: crate::overlay::DismissBehavior::Manual,
2677 layer: crate::overlay::OverlayLayer::InTree,
2678 parent_overlay: Some(parent),
2679 on_dismiss: None,
2680 fade_duration: None,
2681 });
2682
2683 assert_eq!(tree.active_overlays().len(), 2);
2684
2685 tree.dismiss_overlay(parent);
2686 assert!(tree.active_overlays().is_empty());
2687 assert!(!tree.is_visible(content_a));
2688 assert!(!tree.is_visible(content_b));
2689 }
2690
2691 #[test]
2692 fn dismissed_overlay_content_is_dormant_and_invisible() {
2693 let mut tree = WidgetTree::new();
2694 let anchor = tree.add(FillWidget::new());
2695 let content = tree.add(FillWidget::new());
2696 tree.layout(SizeProposal::exact(800.0, 600.0));
2697
2698 let id = tree.show_overlay(crate::overlay::OverlayRequest {
2699 content_id: content,
2700 anchor,
2701 placement: crate::overlay::OverlayPlacement::Below,
2702 dismiss: crate::overlay::DismissBehavior::Manual,
2703 layer: crate::overlay::OverlayLayer::InTree,
2704 parent_overlay: None,
2705 on_dismiss: None,
2706 fade_duration: None,
2707 });
2708
2709 tree.layout(SizeProposal::exact(800.0, 600.0));
2710
2711 tree.dismiss_overlay(id);
2712 assert!(!tree.is_visible(content));
2713
2714 tree.layout(SizeProposal::exact(800.0, 600.0));
2715
2716 let center = tree.bounds(content).center();
2717 let hit = tree.hit_test(center);
2718 assert_ne!(hit, Some(content));
2719
2720 let _frame = tree.render();
2721 assert!(!tree.is_visible(content));
2722 }
2723
2724 #[test]
2725 fn tooltip_appears_after_delay() {
2726 let mut tree = WidgetTree::new();
2727 let anchor = tree.add(FillWidget::new());
2728 let tooltip = tree.add(FillWidget::new().label("Tooltip text"));
2729 tree.layout(SizeProposal::exact(200.0, 100.0));
2730
2731 tree.attach_tooltip(anchor, tooltip, std::time::Duration::from_millis(500));
2732
2733 let center = tree.bounds(anchor).center();
2734 tree.pointer_move(center);
2735 assert!(tree.active_overlays().is_empty());
2736
2737 tree.advance_time(std::time::Duration::from_millis(600));
2738
2739 assert_eq!(tree.active_overlays().len(), 1);
2740 assert!(tree.find_by_label("Tooltip text").is_some());
2741 }
2742
2743 #[test]
2744 fn tooltip_survives_theme_switch() {
2745 let mut tree = WidgetTree::new().with_theme(crate::presets::intui::light());
2750 let anchor = tree.add(FillWidget::new());
2751 let tooltip = tree.add(FillWidget::new().label("Tip"));
2752 tree.layout(SizeProposal::exact(200.0, 100.0));
2753
2754 tree.attach_tooltip(anchor, tooltip, std::time::Duration::from_millis(500));
2755
2756 tree.set_theme(crate::presets::intui::dark());
2757
2758 tree.pointer_move(tree.bounds(anchor).center());
2759 tree.advance_time(std::time::Duration::from_millis(600));
2760
2761 assert_eq!(tree.active_overlays().len(), 1);
2762 assert!(tree.find_by_label("Tip").is_some());
2763 }
2764
2765 #[test]
2766 fn tooltip_suppressed_when_hovering_anchor_owned_overlay_content() {
2767 let mut tree = WidgetTree::new();
2775 let anchor = tree.add(FillWidget::new());
2776 let panel = tree.add_child(anchor, FillWidget::new());
2779 let row = tree.add_child(panel, FillWidget::new());
2780 let tip = tree.add(FillWidget::new().label("Tip"));
2781 tree.layout(SizeProposal::exact(200.0, 100.0));
2782
2783 let delay = std::time::Duration::from_millis(100);
2784 tree.attach_tooltip(anchor, tip, delay);
2785
2786 tree.tooltip_pointer_enter(anchor);
2789 tree.advance_time(delay + std::time::Duration::from_millis(50));
2790 assert_eq!(
2791 tree.active_overlays().len(),
2792 1,
2793 "tooltip should appear when hovering the anchor itself"
2794 );
2795 tree.pointer_move(Point::new(500.0, 500.0));
2798 tree.advance_time(std::time::Duration::from_millis(150));
2799 assert!(tree.active_overlays().is_empty());
2800
2801 tree.show_overlay(crate::overlay::OverlayRequest {
2803 content_id: panel,
2804 anchor,
2805 placement: crate::overlay::OverlayPlacement::Below,
2806 dismiss: crate::overlay::DismissBehavior::Manual,
2807 layer: crate::overlay::OverlayLayer::InTree,
2808 parent_overlay: None,
2809 on_dismiss: None,
2810 fade_duration: None,
2811 });
2812 assert_eq!(tree.active_overlays().len(), 1);
2813
2814 tree.tooltip_pointer_enter(row);
2817 tree.advance_time(delay + std::time::Duration::from_millis(50));
2818 assert_eq!(
2819 tree.active_overlays().len(),
2820 1,
2821 "anchor tooltip must not leak onto its own overlay's rows"
2822 );
2823 }
2824
2825 #[test]
2826 fn tooltip_inside_overlay_still_fires() {
2827 let mut tree = WidgetTree::new();
2831 let host = tree.add(FillWidget::new());
2832 let panel = tree.add_child(host, FillWidget::new());
2833 let row = tree.add_child(panel, FillWidget::new());
2834 let tip = tree.add(FillWidget::new().label("Row tip"));
2835 tree.layout(SizeProposal::exact(200.0, 100.0));
2836
2837 let delay = std::time::Duration::from_millis(100);
2838 tree.attach_tooltip(row, tip, delay);
2840
2841 tree.show_overlay(crate::overlay::OverlayRequest {
2842 content_id: panel,
2843 anchor: host,
2844 placement: crate::overlay::OverlayPlacement::Below,
2845 dismiss: crate::overlay::DismissBehavior::Manual,
2846 layer: crate::overlay::OverlayLayer::InTree,
2847 parent_overlay: None,
2848 on_dismiss: None,
2849 fade_duration: None,
2850 });
2851 assert_eq!(tree.active_overlays().len(), 1);
2852
2853 tree.tooltip_pointer_enter(row);
2854 tree.advance_time(delay + std::time::Duration::from_millis(50));
2855 assert_eq!(
2856 tree.active_overlays().len(),
2857 2,
2858 "a row's own tooltip should still fire inside an overlay"
2859 );
2860 }
2861
2862 #[test]
2863 fn tooltip_dismissed_on_pointer_leave() {
2864 let mut tree = WidgetTree::new();
2865 let anchor = tree.add(FillWidget::new());
2866 let tooltip = tree.add(FillWidget::new().label("Tip"));
2867 tree.layout(SizeProposal::exact(200.0, 100.0));
2868
2869 tree.attach_tooltip(anchor, tooltip, std::time::Duration::from_millis(500));
2870
2871 tree.pointer_move(tree.bounds(anchor).center());
2872 tree.advance_time(std::time::Duration::from_millis(600));
2873 assert_eq!(tree.active_overlays().len(), 1);
2874
2875 tree.pointer_move(Point::new(500.0, 500.0));
2880 assert_eq!(
2881 tree.active_overlays().len(),
2882 1,
2883 "tooltip persists briefly after anchor-leave (hoverable grace)"
2884 );
2885 tree.advance_time(std::time::Duration::from_millis(150));
2886 assert!(
2887 tree.active_overlays().is_empty(),
2888 "tooltip dismissed after the 100ms grace outside anchor+overlay"
2889 );
2890 }
2891
2892 #[test]
2893 fn tooltip_not_shown_if_pointer_leaves_before_delay() {
2894 let mut tree = WidgetTree::new();
2895 let anchor = tree.add(FillWidget::new());
2896 let tooltip = tree.add(FillWidget::new().label("Tip"));
2897 tree.layout(SizeProposal::exact(200.0, 100.0));
2898
2899 tree.attach_tooltip(anchor, tooltip, std::time::Duration::from_millis(500));
2900
2901 tree.pointer_move(tree.bounds(anchor).center());
2902 tree.advance_time(std::time::Duration::from_millis(200));
2903 tree.pointer_move(Point::new(500.0, 500.0));
2904
2905 tree.advance_time(std::time::Duration::from_millis(500));
2906 assert!(tree.active_overlays().is_empty());
2907 }
2908
2909 #[test]
2910 fn tooltip_reshow_uses_short_delay_after_prior_tip() {
2911 let mut tree = WidgetTree::new();
2914 let a = tree.add(FillWidget::new());
2915 let b = tree.add(FillWidget::new());
2916 let tip_a = tree.add(FillWidget::new().label("Tip A"));
2917 let tip_b = tree.add(FillWidget::new().label("Tip B"));
2918 tree.layout(SizeProposal::exact(400.0, 200.0));
2919
2920 let delay = std::time::Duration::from_millis(500);
2921 tree.attach_tooltip(a, tip_a, delay);
2922 tree.attach_tooltip(b, tip_b, delay);
2923
2924 tree.tooltip_pointer_enter(a);
2927 tree.advance_time(std::time::Duration::from_millis(150));
2928 assert!(
2929 tree.active_overlays().is_empty(),
2930 "must not appear before full initial delay"
2931 );
2932 tree.advance_time(std::time::Duration::from_millis(400));
2933 assert_eq!(
2934 tree.active_overlays().len(),
2935 1,
2936 "first tip after initial delay"
2937 );
2938 assert!(tree.find_by_label("Tip A").is_some());
2939
2940 let mut noop = crate::window::NoopWindowOps;
2942 tree.tooltip_pointer_leave(a, &mut noop);
2943 tree.tooltip_pointer_enter(b);
2944 tree.advance_time(std::time::Duration::from_millis(120));
2945 assert!(
2946 tree.find_by_label("Tip B").is_some(),
2947 "second tip should use the short reshow delay while session is warm"
2948 );
2949 }
2950
2951 #[test]
2952 fn reshow_delay_reverts_to_full_after_the_session_grace_expires() {
2953 let mut tree = WidgetTree::new();
2959 tree.set_accessibility_preferences(false, true, 1.0);
2963 let a = tree.add(FillWidget::new());
2964 let b = tree.add(FillWidget::new());
2965 let tip_a = tree.add(FillWidget::new().label("Tip A"));
2966 let tip_b = tree.add(FillWidget::new().label("Tip B"));
2967 tree.layout(SizeProposal::exact(400.0, 200.0));
2968
2969 let delay = std::time::Duration::from_millis(500);
2970 tree.attach_tooltip(a, tip_a, delay);
2971 tree.attach_tooltip(b, tip_b, delay);
2972
2973 tree.tooltip_pointer_enter(a);
2975 tree.advance_time(std::time::Duration::from_millis(550));
2976 assert!(tree.find_by_label("Tip A").is_some(), "first tip shown");
2977
2978 tree.pointer_move(Point::new(900.0, 900.0));
2979 tree.advance_time(std::time::Duration::from_millis(150));
2980 assert!(tree.active_overlays().is_empty(), "A dismissed on leave");
2981
2982 tree.advance_time(super::TOOLTIP_SESSION_GRACE + std::time::Duration::from_millis(50));
2984
2985 tree.tooltip_pointer_enter(b);
2986 tree.advance_time(std::time::Duration::from_millis(150));
2987 assert!(
2988 tree.active_overlays().is_empty(),
2989 "session went cold — B must pay the FULL delay, not the 100 ms reshow"
2990 );
2991 tree.advance_time(std::time::Duration::from_millis(400));
2992 assert!(
2993 tree.find_by_label("Tip B").is_some(),
2994 "B still appears once its full delay elapses"
2995 );
2996 }
2997
2998 #[test]
2999 fn warm_reshow_scales_the_delay_rather_than_flattening_every_tier() {
3000 let mut tree = WidgetTree::new();
3006 let light = tree.add(FillWidget::new());
3007 let heavy = tree.add(FillWidget::new());
3008 let tip_light = tree.add(FillWidget::new().label("Light"));
3009 let tip_heavy = tree.add(FillWidget::new().label("Heavy"));
3010 tree.layout(SizeProposal::exact(400.0, 200.0));
3011
3012 tree.attach_tooltip(light, tip_light, std::time::Duration::from_millis(500));
3013 tree.attach_tooltip(heavy, tip_heavy, std::time::Duration::from_millis(700));
3014
3015 tree.tooltip_pointer_enter(light);
3017 tree.advance_time(std::time::Duration::from_millis(550));
3018 assert!(tree.find_by_label("Light").is_some(), "session warm");
3019
3020 let mut noop = crate::window::NoopWindowOps;
3021 tree.tooltip_pointer_leave(light, &mut noop);
3022 tree.tooltip_pointer_enter(heavy);
3023
3024 tree.advance_time(std::time::Duration::from_millis(120));
3026 assert!(
3027 tree.find_by_label("Heavy").is_none(),
3028 "a heavy tooltip must not fire at the light tier's reshow delay"
3029 );
3030 tree.advance_time(std::time::Duration::from_millis(40));
3032 assert!(
3033 tree.find_by_label("Heavy").is_some(),
3034 "heavy reshow is the scaled 140 ms"
3035 );
3036 }
3037
3038 #[test]
3039 fn a_pinned_sticky_tooltip_does_not_hold_the_session_warm() {
3040 let mut tree = WidgetTree::new();
3044 tree.set_accessibility_preferences(false, true, 1.0); let a = tree.add(FillWidget::new());
3046 let b = tree.add(FillWidget::new());
3047 let tip_a = tree.add(FillWidget::new().label("Pinned"));
3048 let tip_b = tree.add(FillWidget::new().label("Other"));
3049 tree.layout(SizeProposal::exact(400.0, 200.0));
3050
3051 tree.attach_tooltip_with_sticky(
3052 a,
3053 tip_a,
3054 std::time::Duration::from_millis(500),
3055 Some(std::time::Duration::from_millis(100)),
3056 );
3057 tree.attach_tooltip(b, tip_b, std::time::Duration::from_millis(500));
3058
3059 tree.tooltip_pointer_enter(a);
3060 tree.advance_time(std::time::Duration::from_millis(550));
3061 assert!(tree.find_by_label("Pinned").is_some());
3062 tree.promote_tooltip_to_sticky(tip_a);
3063
3064 tree.advance_time(super::TOOLTIP_SESSION_GRACE + std::time::Duration::from_millis(50));
3066 tree.tooltip_pointer_enter(b);
3067 tree.advance_time(std::time::Duration::from_millis(150));
3068 assert!(
3069 tree.find_by_label("Other").is_none(),
3070 "a pinned sticky must not keep every other anchor on the reshow path"
3071 );
3072 }
3073
3074 #[test]
3075 fn reattaching_a_tooltip_does_not_grow_the_entry_table() {
3076 let mut tree = WidgetTree::new();
3082 let anchor = tree.add(FillWidget::new());
3083 tree.layout(SizeProposal::exact(200.0, 100.0));
3084
3085 let delay = std::time::Duration::from_millis(100);
3086 for _ in 0..25 {
3087 let tip = tree.add(FillWidget::new().label("Tip"));
3089 tree.attach_tooltip(anchor, tip, delay);
3090 assert_eq!(
3091 tree.tooltip_entry_count(),
3092 1,
3093 "an anchor must own exactly one tooltip entry across rebuilds"
3094 );
3095 }
3096
3097 tree.tooltip_pointer_enter(anchor);
3099 tree.advance_time(delay + std::time::Duration::from_millis(50));
3100 assert_eq!(
3101 tree.active_overlays().len(),
3102 1,
3103 "latest tooltip still shows"
3104 );
3105 }
3106
3107 #[test]
3108 fn destroying_an_anchor_reaps_its_tooltip_entry() {
3109 let mut tree = WidgetTree::new();
3112 let host = tree.add(FillWidget::new());
3113 let anchor = tree.add_child(host, FillWidget::new());
3114 let tip = tree.add(FillWidget::new().label("Tip"));
3115 tree.layout(SizeProposal::exact(200.0, 100.0));
3116
3117 tree.attach_tooltip(anchor, tip, std::time::Duration::from_millis(100));
3118 assert_eq!(tree.tooltip_entry_count(), 1);
3119
3120 tree.destroy_subtree(anchor);
3121 assert_eq!(
3122 tree.tooltip_entry_count(),
3123 0,
3124 "destroying the anchor must reap its entry and content node"
3125 );
3126 }
3127
3128 #[test]
3129 fn a_leaving_tooltip_schedules_a_wake_for_its_dismissal() {
3130 let mut tree = WidgetTree::new();
3135 tree.set_accessibility_preferences(false, true, 1.0); let anchor = tree.add(FillWidget::new());
3137 let tip = tree.add(FillWidget::new().label("Tip"));
3138 tree.layout(SizeProposal::exact(400.0, 200.0));
3139
3140 tree.attach_tooltip(anchor, tip, std::time::Duration::from_millis(100));
3141 tree.pointer_move(tree.bounds(anchor).center());
3142 tree.advance_time(std::time::Duration::from_millis(150));
3143 assert_eq!(tree.active_overlays().len(), 1, "tooltip shown");
3144
3145 assert!(
3147 tree.next_timer_deadline().is_none(),
3148 "a settled tooltip under the pointer schedules nothing"
3149 );
3150
3151 tree.pointer_move(Point::new(900.0, 900.0));
3154 assert!(
3155 tree.next_timer_deadline().is_some(),
3156 "the PointerLeave grace must contribute a wake deadline"
3157 );
3158 }
3159
3160 #[test]
3171 fn a_fading_out_overlay_schedules_a_wake_for_its_deferred_removal() {
3172 let mut tree = WidgetTree::new();
3173 let anchor = tree.add(FillWidget::new());
3174 let content = tree.add(FillWidget::new().label("Faded"));
3175 tree.layout(SizeProposal::exact(200.0, 100.0));
3176
3177 let id = tree.show_overlay(crate::overlay::OverlayRequest {
3178 content_id: content,
3179 anchor,
3180 placement: crate::overlay::OverlayPlacement::Below,
3181 dismiss: crate::overlay::DismissBehavior::Manual,
3182 layer: crate::overlay::OverlayLayer::InTree,
3183 parent_overlay: None,
3184 on_dismiss: None,
3185 fade_duration: Some(std::time::Duration::from_millis(200)),
3186 });
3187 tree.dismiss_overlay(id);
3188 assert!(
3189 tree.is_visible(content),
3190 "precondition: the removal is deferred, so there is something to wake for"
3191 );
3192
3193 let expected = tree.overlay_manager.next_fade_dismiss_deadline();
3199 assert!(
3200 expected.is_some(),
3201 "precondition: the fade start was stamped, so there is a deadline to compare against"
3202 );
3203 assert_eq!(
3204 tree.next_timer_deadline(),
3205 expected,
3206 "the wake deadline must be the deferred removal's own, not merely some deadline"
3207 );
3208 }
3209
3210 #[test]
3211 fn pressing_cancels_a_pending_dwell_and_dismisses_a_shown_tooltip() {
3212 let mut tree = WidgetTree::new();
3213 let anchor = tree.add(FillWidget::new());
3214 let tip = tree.add(FillWidget::new().label("Tip"));
3215 tree.layout(SizeProposal::exact(400.0, 200.0));
3216
3217 let delay = std::time::Duration::from_millis(500);
3218 tree.attach_tooltip(anchor, tip, delay);
3219
3220 tree.pointer_move(tree.bounds(anchor).center());
3223 tree.advance_time(std::time::Duration::from_millis(300));
3224 tree.tooltip_pointer_press(None);
3225 tree.advance_time(std::time::Duration::from_millis(400));
3226 assert!(
3227 tree.active_overlays().is_empty(),
3228 "a press must cancel the pending dwell, not merely delay it"
3229 );
3230
3231 tree.tooltip_pointer_enter(anchor);
3234 tree.advance_time(delay + std::time::Duration::from_millis(50));
3235 assert_eq!(tree.active_overlays().len(), 1, "tooltip shown again");
3236 tree.tooltip_pointer_press(Some(tree.bounds(anchor).center()));
3237 assert!(
3238 tree.active_overlays().is_empty(),
3239 "a press must dismiss the shown tooltip"
3240 );
3241 }
3242
3243 #[test]
3244 fn window_deactivation_retires_hover_tooltips() {
3245 let mut tree = WidgetTree::new();
3246 let anchor = tree.add(FillWidget::new());
3247 let tip = tree.add(FillWidget::new().label("Tip"));
3248 tree.layout(SizeProposal::exact(400.0, 200.0));
3249
3250 tree.attach_tooltip(anchor, tip, std::time::Duration::from_millis(100));
3251 tree.pointer_move(tree.bounds(anchor).center());
3252 tree.advance_time(std::time::Duration::from_millis(150));
3253 assert_eq!(tree.active_overlays().len(), 1);
3254
3255 tree.set_window_active(false);
3256 assert!(
3257 tree.active_overlays().is_empty(),
3258 "a tooltip must not float over another window's chrome"
3259 );
3260 }
3261
3262 #[test]
3263 fn only_the_innermost_anchor_arms_its_dwell() {
3264 let mut tree = WidgetTree::new();
3267 let panel = tree.add(FillWidget::new());
3268 let row = tree.add_child(panel, FillWidget::new());
3269 let panel_tip = tree.add(FillWidget::new().label("Panel"));
3270 let row_tip = tree.add(FillWidget::new().label("Row"));
3271 tree.layout(SizeProposal::exact(400.0, 200.0));
3272
3273 let delay = std::time::Duration::from_millis(100);
3274 tree.attach_tooltip(panel, panel_tip, delay);
3275 tree.attach_tooltip(row, row_tip, delay);
3276
3277 tree.tooltip_pointer_enter(row);
3278 tree.advance_time(delay + std::time::Duration::from_millis(50));
3279
3280 assert_eq!(
3281 tree.active_overlays().len(),
3282 1,
3283 "exactly one tooltip may open for a hover"
3284 );
3285 assert!(
3286 tree.find_by_label("Row").is_some(),
3287 "the innermost anchor wins"
3288 );
3289 }
3290
3291 #[test]
3292 fn escape_dismisses_a_hover_tooltip_and_falls_through_to_the_menu_below() {
3293 let mut tree = WidgetTree::new();
3297 tree.set_accessibility_preferences(false, true, 1.0); let anchor = tree.add(FillWidget::new());
3299 let menu = tree.add(FillWidget::new());
3300 let tip = tree.add(FillWidget::new().label("Tip"));
3301 tree.layout(SizeProposal::exact(400.0, 200.0));
3302
3303 let menu_overlay = tree.show_overlay(crate::overlay::OverlayRequest {
3304 content_id: menu,
3305 anchor,
3306 placement: crate::overlay::OverlayPlacement::Below,
3307 dismiss: crate::overlay::DismissBehavior::EscapeOrClickOutside,
3308 layer: crate::overlay::OverlayLayer::InTree,
3309 parent_overlay: None,
3310 on_dismiss: None,
3311 fade_duration: None,
3312 });
3313 assert_eq!(tree.active_overlays().len(), 1);
3314
3315 tree.attach_tooltip(anchor, tip, std::time::Duration::from_millis(100));
3317 tree.tooltip_pointer_enter(anchor);
3318 tree.advance_time(std::time::Duration::from_millis(150));
3319 assert_eq!(
3320 tree.active_overlays().len(),
3321 2,
3322 "tooltip sits above the menu"
3323 );
3324
3325 let dismissed = tree.overlay_manager.try_dismiss_top_on_escape();
3327 assert!(dismissed.is_some(), "Escape must dismiss the hover tooltip");
3328 assert_eq!(tree.active_overlays().len(), 1);
3329
3330 let dismissed = tree.overlay_manager.try_dismiss_top_on_escape();
3332 assert_eq!(
3333 dismissed.map(|(id, _, _)| id),
3334 Some(menu_overlay),
3335 "Escape must then reach the menu underneath"
3336 );
3337 assert!(tree.active_overlays().is_empty());
3338 }
3339
3340 #[test]
3341 fn tooltip_timer_restarts_when_pointer_keeps_moving() {
3342 let mut tree = WidgetTree::new();
3345 let anchor = tree.add(FillWidget::new());
3346 let tooltip = tree.add(FillWidget::new().label("Tip"));
3347 tree.layout(SizeProposal::exact(200.0, 100.0));
3348
3349 tree.attach_tooltip(anchor, tooltip, std::time::Duration::from_millis(500));
3350
3351 let start = tree.bounds(anchor).center();
3352 tree.pointer_move(start);
3353 tree.advance_time(std::time::Duration::from_millis(300));
3354 tree.pointer_move(Point::new(start.x + 20.0, start.y));
3356 tree.advance_time(std::time::Duration::from_millis(300));
3359 assert!(
3360 tree.active_overlays().is_empty(),
3361 "moving past stationary slop must restart the delay"
3362 );
3363 tree.advance_time(std::time::Duration::from_millis(250));
3364 assert_eq!(
3365 tree.active_overlays().len(),
3366 1,
3367 "tooltip appears after a full delay of stillness"
3368 );
3369 }
3370
3371 #[test]
3372 fn timed_overlay_auto_dismisses_after_duration() {
3373 let mut tree = WidgetTree::new();
3374 let anchor = tree.add(FillWidget::new());
3375 let content = tree.add(FillWidget::new().label("Toast"));
3376 tree.layout(SizeProposal::exact(200.0, 100.0));
3377
3378 tree.show_overlay_for(
3379 crate::overlay::OverlayRequest {
3380 content_id: content,
3381 anchor,
3382 placement: crate::overlay::OverlayPlacement::Below,
3383 dismiss: crate::overlay::DismissBehavior::Manual,
3384 layer: crate::overlay::OverlayLayer::InTree,
3385 parent_overlay: None,
3386 on_dismiss: None,
3387 fade_duration: None,
3388 },
3389 std::time::Duration::from_millis(300),
3390 );
3391
3392 assert_eq!(tree.active_overlays().len(), 1);
3393
3394 tree.advance_time(std::time::Duration::from_millis(200));
3395 assert_eq!(tree.active_overlays().len(), 1);
3396
3397 tree.advance_time(std::time::Duration::from_millis(150));
3398 assert!(tree.active_overlays().is_empty());
3399 assert!(!tree.is_visible(content));
3400 }
3401
3402 #[test]
3403 fn fade_dismiss_keeps_overlay_off_active_list_immediately() {
3404 let mut tree = WidgetTree::new();
3412 let anchor = tree.add(FillWidget::new());
3413 let content = tree.add(FillWidget::new().label("Faded"));
3414 tree.layout(SizeProposal::exact(200.0, 100.0));
3415
3416 let id = tree.show_overlay(crate::overlay::OverlayRequest {
3417 content_id: content,
3418 anchor,
3419 placement: crate::overlay::OverlayPlacement::Below,
3420 dismiss: crate::overlay::DismissBehavior::Manual,
3421 layer: crate::overlay::OverlayLayer::InTree,
3422 parent_overlay: None,
3423 on_dismiss: None,
3424 fade_duration: Some(std::time::Duration::from_millis(100)),
3425 });
3426 assert_eq!(tree.active_overlays().len(), 1);
3427
3428 tree.dismiss_overlay(id);
3429 assert!(tree.active_overlays().is_empty());
3434 }
3435
3436 #[test]
3437 fn fade_dismiss_defers_content_dormancy_until_sim_tween_completes() {
3438 let mut tree = WidgetTree::new();
3444 let anchor = tree.add(FillWidget::new());
3445 let content = tree.add(FillWidget::new().label("Faded"));
3446 tree.layout(SizeProposal::exact(200.0, 100.0));
3447
3448 let id = tree.show_overlay(crate::overlay::OverlayRequest {
3449 content_id: content,
3450 anchor,
3451 placement: crate::overlay::OverlayPlacement::Below,
3452 dismiss: crate::overlay::DismissBehavior::Manual,
3453 layer: crate::overlay::OverlayLayer::InTree,
3454 parent_overlay: None,
3455 on_dismiss: None,
3456 fade_duration: Some(std::time::Duration::from_millis(100)),
3457 });
3458 tree.advance_time(std::time::Duration::from_secs(1));
3464
3465 tree.dismiss_overlay(id);
3466 assert!(
3467 tree.is_visible(content),
3468 "content stays active during fade-out"
3469 );
3470
3471 tree.advance_time(std::time::Duration::from_millis(60));
3475 assert!(
3476 tree.is_visible(content),
3477 "60 ms into a 100 ms tween the content is still up"
3478 );
3479
3480 tree.advance_time(std::time::Duration::from_millis(90));
3481 assert!(
3482 !tree.is_visible(content),
3483 "after sim-time past the tween window, deferred removal fires"
3484 );
3485 }
3486
3487 #[test]
3499 fn an_auto_dismissed_fade_starts_at_the_instant_the_dismiss_ran() {
3500 let mut tree = WidgetTree::new();
3501 let anchor = tree.add(FillWidget::new());
3502 let content = tree.add(FillWidget::new().label("Toast"));
3503 tree.layout(SizeProposal::exact(200.0, 100.0));
3504
3505 tree.show_overlay_for(
3506 crate::overlay::OverlayRequest {
3507 content_id: content,
3508 anchor,
3509 placement: crate::overlay::OverlayPlacement::Below,
3510 dismiss: crate::overlay::DismissBehavior::Manual,
3511 layer: crate::overlay::OverlayLayer::InTree,
3512 parent_overlay: None,
3513 on_dismiss: None,
3514 fade_duration: Some(std::time::Duration::from_millis(100)),
3515 },
3516 std::time::Duration::from_millis(500),
3517 );
3518 assert_eq!(tree.active_overlays().len(), 1);
3519
3520 tree.advance_time(std::time::Duration::from_millis(600));
3524 assert!(
3525 tree.active_overlays().is_empty(),
3526 "precondition: the auto-dismiss fired inside this advance"
3527 );
3528 assert!(
3529 tree.is_visible(content),
3530 "the fade must start at the instant the dismiss ran, so the \
3531 content survives the frame that dismissed it"
3532 );
3533
3534 tree.advance_time(std::time::Duration::from_millis(150));
3535 assert!(
3536 !tree.is_visible(content),
3537 "and is reaped once the tween's own window has passed"
3538 );
3539 }
3540}