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 matches!(
677 overlay.placement,
678 crate::overlay::OverlayPlacement::Centered
679 ) {
680 return Some(overlay_id);
681 }
682 current = overlay.parent_overlay;
683 }
684
685 None
686 }
687
688 pub fn is_behind_modal(&self, widget_id: WidgetId) -> bool {
716 let Some(modal) = self.overlay_manager.topmost_centered() else {
717 return false;
718 };
719 let stack = &self.overlay_manager.stack;
720 let Some(at) = stack.iter().position(|overlay| overlay.id == modal.id) else {
721 return false;
722 };
723 let (below, above) = stack.split_at(at);
724 let in_modal = |id: WidgetId| {
725 above
726 .iter()
727 .any(|overlay| self.is_descendant_of(id, overlay.content_id))
728 };
729 !(in_modal(widget_id)
730 || below.iter().any(|overlay| {
731 self.is_descendant_of(widget_id, overlay.content_id) && in_modal(overlay.anchor)
732 }))
733 }
734
735 fn menu_ancestor_for_widget(&self, widget_id: WidgetId) -> Option<WidgetId> {
736 let mut current = Some(widget_id);
737 while let Some(id) = current {
738 if let Some(node) = self.arena.get(id) {
739 let mut builder = AccessNodeBuilder::new();
740 node.widget.accessibility(&mut builder);
741 if builder.role() == accesskit::Role::Menu {
742 return Some(id);
743 }
744 }
745 current = self.arena.parent(id);
746 }
747 None
748 }
749
750 pub(super) fn dismiss_child_overlays_for_source(
751 &mut self,
752 source_widget: WidgetId,
753 preserve_content: Option<WidgetId>,
754 ops: &mut dyn crate::window::WindowOps,
755 ) {
756 if let Some(parent_overlay) = self.overlay_ancestor_for_widget(source_widget) {
757 let preserve_overlay = preserve_content
758 .and_then(|content_id| self.overlay_manager.find_by_content(content_id));
759 let (dismissed, focus_restore) = self
760 .overlay_manager
761 .dismiss_descendants_of(parent_overlay, preserve_overlay);
762 self.dormant_dismissed_content(&dismissed, &mut *ops);
763 if let Some(restore_id) = focus_restore
764 && self.arena.is_active(restore_id)
765 {
766 self.focus_ops(restore_id, &mut *ops);
767 }
768 return;
769 }
770
771 let Some(menu_root) = self.menu_ancestor_for_widget(source_widget) else {
772 return;
773 };
774 let preserve_overlay = preserve_content
775 .and_then(|content_id| self.overlay_manager.find_by_content(content_id));
776 let overlay_ids: Vec<crate::overlay::OverlayId> = self
777 .overlay_manager
778 .stack
779 .iter()
780 .filter(|overlay| {
781 overlay.parent_overlay.is_none()
782 && self.is_descendant_of(overlay.anchor, menu_root)
783 && !preserve_overlay.is_some_and(|keep| {
784 overlay.id == keep
785 || self.overlay_manager.is_descendant_of(overlay.id, keep)
786 })
787 })
788 .map(|overlay| overlay.id)
789 .collect();
790
791 for overlay_id in overlay_ids {
792 let (dismissed, focus_restore) =
793 self.overlay_manager.dismiss_with_focus_restore(overlay_id);
794 self.dormant_dismissed_content(&dismissed, &mut *ops);
795 if let Some(restore_id) = focus_restore
796 && self.arena.is_active(restore_id)
797 {
798 self.focus_ops(restore_id, &mut *ops);
799 }
800 }
801 }
802
803 pub(super) fn dismiss_self_overlay_chain_for_source(
815 &mut self,
816 source_widget: WidgetId,
817 ops: &mut dyn crate::window::WindowOps,
818 ) {
819 let Some(start) = self.overlay_ancestor_for_widget(source_widget) else {
820 return;
821 };
822
823 let mut topmost_to_dismiss: Option<crate::overlay::OverlayId> = None;
824 let mut current = Some(start);
825 while let Some(overlay_id) = current {
826 let Some(overlay) = self.overlay_manager.overlay(overlay_id) else {
827 break;
828 };
829 if self.overlay_is_host_surface(overlay_id) {
830 break;
831 }
832 topmost_to_dismiss = Some(overlay_id);
833 current = overlay.parent_overlay;
834 }
835
836 if let Some(target) = topmost_to_dismiss {
837 let (dismissed, focus_restore) =
838 self.overlay_manager.dismiss_with_focus_restore(target);
839 self.dormant_dismissed_content(&dismissed, &mut *ops);
840 if let Some(restore_id) = focus_restore
841 && self.arena.is_active(restore_id)
842 {
843 self.focus_ops(restore_id, &mut *ops);
844 }
845 }
846 }
847
848 fn overlay_is_anchor_positioned(placement: &crate::overlay::OverlayPlacement) -> bool {
865 use crate::overlay::OverlayPlacement as P;
866 match placement {
867 P::Below
868 | P::Above
869 | P::TrailingEdge
870 | P::AtPointer(_)
871 | P::AtPointerAvoiding { .. }
872 | P::NearAnchor { .. }
873 | P::BelowPreferred => true,
874 P::Centered
880 | P::BottomCenter
881 | P::ViewportCorner { .. }
882 | P::FullViewport
883 | P::AboveSelection { .. }
884 | P::BelowSelection { .. } => false,
885 }
886 }
887
888 fn overlay_follows_focus_out(&self, overlay_id: crate::overlay::OverlayId) -> bool {
928 let Some(overlay) = self.overlay_manager.overlay(overlay_id) else {
929 return false;
930 };
931 if !Self::overlay_is_anchor_positioned(&overlay.placement) {
932 return false;
933 }
934 if overlay.is_dismissing() {
935 return false;
936 }
937 if overlay.inert {
938 return false;
939 }
940 !self
941 .tooltips
942 .iter()
943 .any(|entry| entry.content_id == overlay.content_id)
944 }
945
946 fn overlay_orbit_for_widget(&self, widget_id: WidgetId) -> Option<crate::overlay::OverlayId> {
969 self.overlay_manager
970 .stack
971 .iter()
972 .rev()
973 .find(|overlay| {
974 self.is_descendant_of(widget_id, overlay.content_id)
975 || (self.overlay_follows_focus_out(overlay.id)
976 && self.is_descendant_of(widget_id, overlay.anchor))
977 })
978 .map(|overlay| overlay.id)
979 }
980
981 pub(super) fn dismiss_overlays_left_by_focus(
1008 &mut self,
1009 old: Option<WidgetId>,
1010 new_focus: Option<WidgetId>,
1011 ops: &mut dyn crate::window::WindowOps,
1012 ) {
1013 let Some(old) = old else {
1014 return;
1015 };
1016 let Some(old_overlay) = self.overlay_orbit_for_widget(old) else {
1017 return;
1018 };
1019 let new_overlay = new_focus.and_then(|id| self.overlay_ancestor_for_widget(id));
1027
1028 if let Some(new_overlay) = new_overlay
1031 && (new_overlay == old_overlay
1032 || self
1033 .overlay_manager
1034 .is_descendant_of(new_overlay, old_overlay))
1035 {
1036 return;
1037 }
1038
1039 if !self.overlay_follows_focus_out(old_overlay) {
1040 return;
1041 }
1042
1043 let mut topmost = old_overlay;
1044 let mut current = self
1045 .overlay_manager
1046 .overlay(old_overlay)
1047 .and_then(|overlay| overlay.parent_overlay);
1048 while let Some(overlay_id) = current {
1049 if Some(overlay_id) == new_overlay {
1053 break;
1054 }
1055 if self.overlay_is_host_surface(overlay_id) {
1058 break;
1059 }
1060 if !self.overlay_follows_focus_out(overlay_id) {
1061 break;
1062 }
1063 topmost = overlay_id;
1064 current = self
1065 .overlay_manager
1066 .overlay(overlay_id)
1067 .and_then(|overlay| overlay.parent_overlay);
1068 }
1069
1070 let dismissed = self.overlay_manager.dismiss(topmost);
1071 self.dormant_dismissed_content(&dismissed, &mut *ops);
1072 }
1073
1074 pub(super) fn dismiss_all_overlays_except_hosts(
1085 &mut self,
1086 ops: &mut dyn crate::window::WindowOps,
1087 ) {
1088 let to_dismiss: Vec<crate::overlay::OverlayId> = self
1089 .overlay_manager
1090 .stack
1091 .iter()
1092 .map(|overlay| overlay.id)
1093 .filter(|&id| !self.overlay_is_host_surface(id))
1094 .collect();
1095
1096 for overlay_id in to_dismiss {
1097 let dismissed = self.overlay_manager.dismiss(overlay_id);
1098 self.dormant_dismissed_content(&dismissed, &mut *ops);
1099 }
1100 }
1101
1102 pub(super) fn overlay_is_host_surface(&self, overlay_id: crate::overlay::OverlayId) -> bool {
1108 let Some(overlay) = self.overlay_manager.overlay(overlay_id) else {
1109 return false;
1110 };
1111 if matches!(
1120 overlay.placement,
1121 crate::overlay::OverlayPlacement::Centered
1122 ) {
1123 return true;
1124 }
1125 let Some(node) = self.arena.get(overlay.content_id) else {
1126 return false;
1127 };
1128 let role = node
1136 .access_overrides
1137 .as_ref()
1138 .and_then(|o| o.role)
1139 .unwrap_or_else(|| {
1140 let mut builder = AccessNodeBuilder::new();
1141 node.widget.accessibility(&mut builder);
1142 builder.role()
1143 });
1144 matches!(
1145 role,
1146 accesskit::Role::Tooltip
1147 | accesskit::Role::Dialog
1148 | accesskit::Role::AlertDialog
1149 | accesskit::Role::MenuBar
1150 )
1151 }
1152
1153 pub(super) fn dismiss_modal_for_source(
1154 &mut self,
1155 source_widget: WidgetId,
1156 ops: &mut dyn crate::window::WindowOps,
1157 ) -> bool {
1158 let Some(modal_overlay) = self.modal_overlay_for_widget(source_widget) else {
1159 return false;
1160 };
1161
1162 let (dismissed, focus_restore) = self
1163 .overlay_manager
1164 .dismiss_with_focus_restore(modal_overlay);
1165 self.dormant_dismissed_content(&dismissed, &mut *ops);
1166 if let Some(restore_id) = focus_restore
1167 && self.arena.is_active(restore_id)
1168 {
1169 self.focus_ops(restore_id, &mut *ops);
1170 }
1171 true
1172 }
1173
1174 pub fn is_descendant_of(&self, widget_id: WidgetId, ancestor: WidgetId) -> bool {
1175 if widget_id == ancestor {
1176 return true;
1177 }
1178 let mut current = self.arena.parent(widget_id);
1179 while let Some(parent_id) = current {
1180 if parent_id == ancestor {
1181 return true;
1182 }
1183 current = self.arena.parent(parent_id);
1184 }
1185 false
1186 }
1187
1188 fn is_overlay_content_root(&self, widget_id: WidgetId) -> bool {
1195 self.overlay_manager.find_by_content(widget_id).is_some()
1196 }
1197
1198 pub(super) fn tooltip_focus_enter(&mut self, widget_id: WidgetId) {
1229 type ToShow = (WidgetId, WidgetId, crate::overlay::TooltipPlacement);
1252 let mut direct: Vec<ToShow> = Vec::new();
1253 let mut reverse: Vec<ToShow> = Vec::new();
1254 for e in &self.tooltips {
1255 if e.sticky_after.is_none() || e.overlay_id.is_some() || e.suppressed_until_focus_leaves
1256 {
1257 continue;
1258 }
1259 if self.is_descendant_of(widget_id, e.anchor_id) {
1260 direct.push((e.anchor_id, e.content_id, e.placement));
1261 } else if self.is_descendant_of(e.anchor_id, widget_id) {
1262 reverse.push((e.anchor_id, e.content_id, e.placement));
1263 }
1264 }
1265 let mut to_show = direct;
1266 if reverse.len() == 1 && !self.is_overlay_content_root(widget_id) {
1270 to_show.extend(reverse);
1271 }
1272
1273 let sim_now = self.sim_clock;
1282 let real_now = std::time::Instant::now();
1283 for (_anchor_id, content_id, _placement) in to_show {
1284 if let Some(entry) = self
1285 .tooltips
1286 .iter_mut()
1287 .find(|e| e.content_id == content_id)
1288 {
1289 entry.hover_start = Some(sim_now);
1290 entry.real_hover_start = Some(real_now);
1291 entry.hover_origin = None;
1292 entry.armed_by_focus = true;
1293 entry.armed_by_hold = false;
1294 }
1295 }
1296 }
1297
1298 pub(super) fn show_highlight_tooltip(
1308 &mut self,
1309 item_id: WidgetId,
1310 ops: &mut dyn crate::window::WindowOps,
1311 ) {
1312 self.clear_highlight_tooltip(&mut *ops);
1315
1316 let found = self.tooltips.iter().find_map(|e| {
1320 if e.overlay_id.is_none() && self.is_descendant_of(e.anchor_id, item_id) {
1321 Some((e.anchor_id, e.content_id, e.placement))
1322 } else {
1323 None
1324 }
1325 });
1326 let Some((anchor_id, content_id, placement)) = found else {
1327 return;
1328 };
1329
1330 let parent_overlay = self.overlay_ancestor_for_widget(item_id);
1331 self.arena.activate(content_id);
1332 let fade_duration = if self.prefers_reduced_motion {
1333 None
1334 } else {
1335 Some(self.theme.motion.duration_fast)
1336 };
1337 let mut request = crate::overlay::OverlayRequest::new(
1338 content_id,
1339 anchor_id,
1340 Self::tooltip_overlay_placement(placement),
1341 crate::overlay::DismissBehavior::Manual,
1342 );
1343 request.parent_overlay = parent_overlay;
1344 request.fade_duration = fade_duration;
1345 let oid = self.show_overlay(request);
1346 let real_now = std::time::Instant::now();
1347 let sim_now = self.sim_clock;
1348 if let Some(entry) = self
1349 .tooltips
1350 .iter_mut()
1351 .find(|e| e.content_id == content_id)
1352 {
1353 entry.overlay_id = Some(oid);
1354 entry.shown_at_sim = Some(sim_now);
1355 entry.shown_at_real = Some(real_now);
1356 if let Some(sink) = entry.shown_at_sink.as_ref() {
1357 sink.set(Some(real_now));
1358 }
1359 }
1360 self.highlight_tooltip = Some((oid, content_id));
1361 }
1362
1363 pub(super) fn clear_highlight_tooltip(&mut self, ops: &mut dyn crate::window::WindowOps) {
1368 let Some((oid, content_id)) = self.highlight_tooltip.take() else {
1369 return;
1370 };
1371 if self.overlay_manager.stack.iter().any(|o| o.id == oid) {
1372 let dismissed = self.overlay_manager.dismiss(oid);
1373 self.dormant_dismissed_content(&dismissed, &mut *ops);
1374 }
1375 if let Some(entry) = self
1376 .tooltips
1377 .iter_mut()
1378 .find(|e| e.content_id == content_id)
1379 {
1380 entry.overlay_id = None;
1381 entry.shown_at_sim = None;
1382 entry.shown_at_real = None;
1383 if let Some(sink) = entry.shown_at_sink.as_ref() {
1384 sink.set(None);
1385 }
1386 }
1387 }
1388
1389 pub(super) fn tooltip_focus_leave_outside(
1400 &mut self,
1401 new_focus: Option<WidgetId>,
1402 ops: &mut dyn crate::window::WindowOps,
1403 ) {
1404 let to_dismiss: Vec<crate::overlay::OverlayId> = self
1405 .tooltips
1406 .iter()
1407 .filter(|e| e.promoted_by_focus && e.overlay_id.is_some())
1408 .filter(|e| {
1409 let in_scope = new_focus
1410 .map(|nf| {
1411 self.is_descendant_of(nf, e.anchor_id)
1416 || self.is_descendant_of(e.anchor_id, nf)
1417 || self.is_descendant_of(nf, e.content_id)
1418 })
1419 .unwrap_or(false);
1420 !in_scope
1421 })
1422 .filter_map(|e| e.overlay_id)
1423 .collect();
1424
1425 for oid in to_dismiss {
1426 let dismissed = self.overlay_manager.dismiss(oid);
1427 self.dormant_dismissed_content(&dismissed, &mut *ops);
1428 }
1429
1430 for e in &mut self.tooltips {
1433 if e.armed_by_focus && e.overlay_id.is_none() {
1434 e.hover_start = None;
1435 e.real_hover_start = None;
1436 e.armed_by_focus = false;
1437 }
1438 }
1439
1440 let served: Vec<usize> = self
1444 .tooltips
1445 .iter()
1446 .enumerate()
1447 .filter(|(_, e)| e.suppressed_until_focus_leaves)
1448 .filter(|(_, e)| {
1449 let still_inside = new_focus
1450 .map(|nf| {
1451 self.is_descendant_of(nf, e.anchor_id)
1452 || self.is_descendant_of(nf, e.content_id)
1453 })
1454 .unwrap_or(false);
1455 !still_inside
1456 })
1457 .map(|(i, _)| i)
1458 .collect();
1459 for i in served {
1460 self.tooltips[i].suppressed_until_focus_leaves = false;
1461 }
1462 }
1463
1464 fn tooltip_hover_targets_anchor(&self, widget_id: WidgetId, anchor_id: WidgetId) -> bool {
1484 if !self.is_descendant_of(widget_id, anchor_id) {
1485 return false;
1486 }
1487 if let Some(overlay_id) = self.overlay_ancestor_for_widget(widget_id)
1488 && let Some(content_id) = self
1489 .overlay_manager
1490 .overlay(overlay_id)
1491 .map(|o| o.content_id)
1492 && !self.is_descendant_of(anchor_id, content_id)
1493 {
1494 return false;
1495 }
1496 true
1497 }
1498
1499 pub(super) fn tooltip_index_for(&self, widget_id: WidgetId) -> Option<usize> {
1517 self.tooltips
1518 .iter()
1519 .enumerate()
1520 .filter(|(_, entry)| self.tooltip_hover_targets_anchor(widget_id, entry.anchor_id))
1521 .filter_map(|(index, entry)| {
1522 self.ancestor_distance(widget_id, entry.anchor_id)
1523 .map(|depth| (depth, index))
1524 })
1525 .min()
1526 .map(|(_, index)| index)
1527 }
1528
1529 pub(super) fn tooltip_pointer_enter(&mut self, widget_id: WidgetId) {
1530 let Some(index) = self.tooltip_index_for(widget_id) else {
1531 return;
1532 };
1533 if self.tooltips[index].overlay_id.is_some() {
1535 return;
1536 }
1537 self.tooltips[index].hover_start = Some(self.sim_clock);
1538 self.tooltips[index].real_hover_start = Some(std::time::Instant::now());
1539 self.tooltips[index].hover_origin = self.hover_owner_position();
1540 self.tooltips[index].armed_by_focus = false;
1541 self.tooltips[index].armed_by_hold = false;
1542 self.arena.mark_needs_paint(self.tooltips[index].anchor_id);
1543 }
1544
1545 fn ancestor_distance(&self, widget_id: WidgetId, ancestor: WidgetId) -> Option<usize> {
1548 let mut current = widget_id;
1549 let mut depth = 0usize;
1550 loop {
1551 if current == ancestor {
1552 return Some(depth);
1553 }
1554 current = self.arena.parent(current)?;
1555 depth += 1;
1556 }
1557 }
1558
1559 pub(super) fn tooltip_pointer_press(&mut self, at: Option<teksilo_canvas::Point>) {
1574 let mut to_dismiss = Vec::new();
1575 let candidates: Vec<(usize, Option<crate::overlay::OverlayId>)> = self
1576 .tooltips
1577 .iter()
1578 .enumerate()
1579 .map(|(i, e)| (i, if e.is_sticky { None } else { e.overlay_id }))
1580 .collect();
1581 for (index, overlay_id) in candidates {
1582 let inside = match (overlay_id, at) {
1587 (Some(oid), Some(pos)) => self
1588 .overlay_content_bounds(oid)
1589 .is_some_and(|rect| rect.contains(pos)),
1590 _ => false,
1591 };
1592 if inside {
1593 continue;
1594 }
1595 let entry = &mut self.tooltips[index];
1596 entry.hover_start = None;
1597 entry.real_hover_start = None;
1598 entry.hover_origin = None;
1599 if let Some(oid) = overlay_id {
1600 to_dismiss.push(oid);
1601 }
1602 }
1603 for overlay_id in to_dismiss {
1604 self.dismiss_overlay(overlay_id);
1605 }
1606 }
1607
1608 pub(super) fn tooltip_escape_pressed(&mut self) {
1630 self.tooltip_pointer_press(None);
1633 }
1634
1635 pub(super) fn inert_overlays_escape_pressed(&mut self, ops: &mut dyn crate::window::WindowOps) {
1646 let dismissed = self.overlay_manager.dismiss_inert_on_escape();
1647 if !dismissed.is_empty() {
1648 self.dormant_dismissed_content(&dismissed, &mut *ops);
1649 self.a11y_dirty = true;
1650 }
1651 }
1652
1653 pub(super) fn tooltip_window_deactivated(&mut self) {
1660 self.tooltip_pointer_press(None);
1663 }
1664
1665 pub(crate) fn tooltip_cancel_pending_dwell(&mut self) {
1673 for entry in &mut self.tooltips {
1674 if entry.overlay_id.is_none() {
1675 entry.hover_start = None;
1676 entry.real_hover_start = None;
1677 entry.hover_origin = None;
1678 }
1679 }
1680 }
1681
1682 pub(super) fn tooltip_pointer_moved(
1686 &mut self,
1687 widget_id: WidgetId,
1688 position: teksilo_canvas::Point,
1689 ) {
1690 let matching: Vec<usize> = self
1691 .tooltips
1692 .iter()
1693 .enumerate()
1694 .filter(|(_, entry)| {
1695 entry.overlay_id.is_none()
1696 && entry.hover_start.is_some()
1697 && !entry.armed_by_focus
1698 && self.tooltip_hover_targets_anchor(widget_id, entry.anchor_id)
1699 })
1700 .map(|(index, _)| index)
1701 .collect();
1702 if matching.is_empty() {
1703 return;
1704 }
1705 let now = self.sim_clock;
1706 let real_now = std::time::Instant::now();
1707 let slop = super::TOOLTIP_STATIONARY_SLOP;
1708 let slop_sq = slop * slop;
1709 for index in matching {
1710 let origin = match self.tooltips[index].hover_origin {
1711 Some(o) => o,
1712 None => {
1713 self.tooltips[index].hover_origin = Some(position);
1717 continue;
1718 }
1719 };
1720 let dx = position.x - origin.x;
1721 let dy = position.y - origin.y;
1722 if dx * dx + dy * dy <= slop_sq {
1723 continue;
1724 }
1725 self.tooltips[index].hover_start = Some(now);
1727 self.tooltips[index].real_hover_start = Some(real_now);
1728 self.tooltips[index].hover_origin = Some(position);
1729 }
1730 }
1731
1732 pub fn promote_tooltip_to_sticky(&mut self, content_id: WidgetId) {
1750 let Some(entry) = self
1751 .tooltips
1752 .iter_mut()
1753 .find(|entry| entry.content_id == content_id)
1754 else {
1755 return;
1756 };
1757 if entry.is_sticky {
1758 return;
1759 }
1760 entry.is_sticky = true;
1761 let overlay_id = entry.overlay_id;
1762 if let Some(overlay_id) = overlay_id {
1763 self.overlay_manager.set_dismiss(
1764 overlay_id,
1765 crate::overlay::DismissBehavior::EscapeOrClickOutside,
1766 );
1767 }
1768 }
1769
1770 pub(super) fn tooltip_pointer_leave(
1771 &mut self,
1772 widget_id: WidgetId,
1773 _ops: &mut dyn crate::window::WindowOps,
1774 ) {
1775 let matching: Vec<usize> = self
1776 .tooltips
1777 .iter()
1778 .enumerate()
1779 .filter(|(_, entry)| self.is_descendant_of(widget_id, entry.anchor_id))
1780 .map(|(index, _)| index)
1781 .collect();
1782 for index in matching {
1783 self.tooltips[index].hover_start = None;
1786 self.tooltips[index].real_hover_start = None;
1787 self.tooltips[index].hover_origin = None;
1788 }
1800 }
1801
1802 pub fn next_timer_deadline(&self) -> Option<std::time::Instant> {
1808 let now = std::time::Instant::now();
1809 let session_active = self.tooltip_session_active_real(now);
1810 let tooltip_deadline = self
1811 .tooltips
1812 .iter()
1813 .filter(|entry| entry.overlay_id.is_none())
1814 .filter_map(|entry| {
1815 let start = entry.real_hover_start?;
1816 let delay = self.effective_tooltip_delay(entry.delay, session_active);
1817 Some(start + delay)
1818 })
1819 .min();
1820
1821 let ref_time = self.last_frame_time.unwrap_or_else(std::time::Instant::now);
1842 let dwell_tooltip_deadline = self
1843 .tooltips
1844 .iter()
1845 .filter_map(|entry| {
1846 let sticky_after = entry.sticky_after?;
1847 let shown_at = entry.shown_at_real?;
1848 if entry.overlay_id.is_none() || entry.is_sticky {
1849 return None;
1850 }
1851 let dwell_step = sticky_after / super::TOOLTIP_DWELL_STEPS;
1852 if dwell_step.is_zero() {
1853 return None;
1854 }
1855 let elapsed = ref_time.saturating_duration_since(shown_at);
1856 if elapsed >= sticky_after {
1857 return None;
1858 }
1859 let steps_passed = (elapsed.as_millis() / dwell_step.as_millis()) as u32;
1862 let next_step_at = shown_at + dwell_step * (steps_passed + 1);
1863 Some(next_step_at.min(shown_at + sticky_after))
1864 })
1865 .min();
1866 let delayed_overlay_deadline = self
1867 .pending_delayed_overlays
1868 .iter()
1869 .map(|pending| pending.real_requested_at + pending.delay)
1870 .min();
1871 let auto_dismiss_deadline = self.overlay_manager.next_auto_dismiss_deadline();
1872 let pointer_leave_deadline = self.overlay_manager.next_pointer_leave_deadline();
1879 let fade_dismiss_deadline = self.overlay_manager.next_fade_dismiss_deadline();
1886 let animation_deadline = self
1887 .animation_scheduler
1888 .next_deadline(&self.arena, self.paint_epoch);
1889 let animated_quad_deadline = self
1894 .animated_quads
1895 .next_deadline(&self.arena, self.paint_epoch);
1896 let gesture_deadline = self.next_input_deadline();
1902 let wake_at_deadline = self.pending_wake_at.get();
1903 let frame_tick_deadline = self.frame_tick_deadline();
1909
1910 [
1911 tooltip_deadline,
1912 dwell_tooltip_deadline,
1913 delayed_overlay_deadline,
1914 auto_dismiss_deadline,
1915 pointer_leave_deadline,
1916 fade_dismiss_deadline,
1917 animation_deadline,
1918 animated_quad_deadline,
1919 gesture_deadline,
1920 wake_at_deadline,
1921 frame_tick_deadline,
1922 ]
1923 .into_iter()
1924 .flatten()
1925 .min()
1926 }
1927
1928 pub fn overlay_manager(&self) -> &crate::overlay::OverlayManager {
1929 &self.overlay_manager
1930 }
1931
1932 pub fn overlay_manager_mut(&mut self) -> &mut crate::overlay::OverlayManager {
1937 &mut self.overlay_manager
1938 }
1939
1940 pub fn active_overlays(&self) -> Vec<crate::overlay::OverlayId> {
1941 self.overlay_manager.active_ids()
1942 }
1943
1944 pub fn overlay_content_bounds(
1953 &self,
1954 id: crate::overlay::OverlayId,
1955 ) -> Option<teksilo_canvas::Rect> {
1956 self.overlay_manager
1957 .stack
1958 .iter()
1959 .find(|o| o.id == id)
1960 .map(|o| o.bounds)
1961 }
1962
1963 pub fn show_overlay(
1964 &mut self,
1965 request: crate::overlay::OverlayRequest,
1966 ) -> crate::overlay::OverlayId {
1967 let fade_duration = request.fade_duration;
1968 let content_id = request.content_id;
1969 let is_modal = matches!(
1970 request.placement,
1971 crate::overlay::OverlayPlacement::Centered
1972 );
1973 let id = self.overlay_manager.show(request);
1974 self.cancel_pointers_for_modal(is_modal);
1975 self.a11y_dirty = true;
1983 if let Some(duration) = fade_duration {
1984 self.attach_overlay_fade(id, content_id, duration);
1985 }
1986 id
1987 }
1988
1989 pub fn show_overlay_in_band(
1999 &mut self,
2000 request: crate::overlay::OverlayRequest,
2001 band: crate::overlay::OverlayBand,
2002 ) -> crate::overlay::OverlayId {
2003 let fade_duration = request.fade_duration;
2004 let content_id = request.content_id;
2005 let is_modal = matches!(
2006 request.placement,
2007 crate::overlay::OverlayPlacement::Centered
2008 );
2009 let id = self.overlay_manager.show_in_band(request, band);
2010 self.cancel_pointers_for_modal(is_modal);
2011 self.a11y_dirty = true;
2012 if let Some(duration) = fade_duration {
2013 self.attach_overlay_fade(id, content_id, duration);
2014 }
2015 id
2016 }
2017
2018 pub fn show_overlay_from_source(
2021 &mut self,
2022 source_widget: WidgetId,
2023 mut request: crate::overlay::OverlayRequest,
2024 ) -> crate::overlay::OverlayId {
2025 if request.parent_overlay.is_none() {
2026 request.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
2027 }
2028 if let Some(existing) = self.overlay_manager.find_by_content(request.content_id) {
2029 return existing;
2030 }
2031
2032 let fade_duration = request.fade_duration;
2033 let content_id = request.content_id;
2034 let is_modal = matches!(
2035 request.placement,
2036 crate::overlay::OverlayPlacement::Centered
2037 );
2038 let current_focus = self.focused;
2039 let id = self.overlay_manager.show(request);
2040 self.cancel_pointers_for_modal(is_modal);
2041 self.a11y_dirty = true;
2042 if let Some(focus_id) = current_focus {
2043 self.overlay_manager.set_top_focus_restore(focus_id);
2044 }
2045 if let Some(duration) = fade_duration {
2046 self.attach_overlay_fade(id, content_id, duration);
2047 }
2048 id
2049 }
2050
2051 pub fn show_overlay_for(
2052 &mut self,
2053 request: crate::overlay::OverlayRequest,
2054 duration: std::time::Duration,
2055 ) -> crate::overlay::OverlayId {
2056 let fade_duration = request.fade_duration;
2057 let content_id = request.content_id;
2058 let is_modal = matches!(
2059 request.placement,
2060 crate::overlay::OverlayPlacement::Centered
2061 );
2062 let id = self.overlay_manager.show_for(request, duration);
2063 self.cancel_pointers_for_modal(is_modal);
2064 self.overlay_manager.set_shown_at_sim(id, self.sim_clock);
2065 self.a11y_dirty = true;
2066 if let Some(fade) = fade_duration {
2067 self.attach_overlay_fade(id, content_id, fade);
2068 }
2069 id
2070 }
2071
2072 fn attach_overlay_fade(
2080 &mut self,
2081 overlay_id: crate::overlay::OverlayId,
2082 content_id: WidgetId,
2083 duration: std::time::Duration,
2084 ) {
2085 let opacity = crate::signal::Signal::<f32>::new_animated(0.0);
2086 self.register_animated_signal(&opacity, content_id);
2087 self.set_opacity(content_id, opacity.clone());
2088 if self.prefers_reduced_motion() {
2092 opacity.set(1.0);
2093 self.overlay_manager
2094 .attach_fade(overlay_id, opacity, std::time::Duration::ZERO);
2095 return;
2096 }
2097 let _ = opacity.try_animate_with_options(crate::animation::AnimationRequest {
2098 target: 1.0,
2099 duration,
2100 easing: teksilo_tokens::Easing::EaseOut,
2101 frame_interval: None,
2102 looping: false,
2103 epsilon: 0.0,
2104 max_duration: None,
2105 });
2106 self.overlay_manager
2107 .attach_fade(overlay_id, opacity, duration);
2108 }
2109
2110 pub fn dismiss_overlay(&mut self, id: crate::overlay::OverlayId) {
2115 let mut noop = crate::window::NoopWindowOps;
2116 self.dismiss_overlay_with_ops(id, &mut noop);
2117 }
2118
2119 pub fn dismiss_overlay_with_ops(
2122 &mut self,
2123 id: crate::overlay::OverlayId,
2124 ops: &mut dyn crate::window::WindowOps,
2125 ) {
2126 let dismissed = self.overlay_manager.dismiss(id);
2127 self.dormant_dismissed_content(&dismissed, &mut *ops);
2128 }
2129
2130 fn cancel_pointers_for_modal(&mut self, is_modal: bool) {
2141 if !is_modal {
2142 return;
2143 }
2144 let mut noop = crate::window::NoopWindowOps;
2145 self.cancel_all_pointers(crate::pointer::CancelReason::ModalOpened, &mut noop);
2146 }
2147
2148 pub(super) fn run_pending_dismiss_callbacks(&mut self, ops: &mut dyn crate::window::WindowOps) {
2158 if self.draining_dismiss.replace(true) {
2163 return;
2164 }
2165 for _ in 0..8 {
2169 let pending = self.overlay_manager.take_pending_dismiss();
2170 if pending.is_empty() {
2171 break;
2172 }
2173 for (cb, reason) in pending {
2174 self.run_with_event_context(&mut *ops, |ctx| cb(reason, ctx));
2175 }
2176 }
2177 self.draining_dismiss.set(false);
2178 }
2179
2180 pub(super) fn dormant_dismissed_content(
2181 &mut self,
2182 content_ids: &[WidgetId],
2183 ops: &mut dyn crate::window::WindowOps,
2184 ) {
2185 self.run_pending_dismiss_callbacks(&mut *ops);
2189 let mut any_tooltip_dismissed = false;
2195 for &id in content_ids {
2196 if let Some(entry) = self.tooltips.iter_mut().find(|e| e.content_id == id) {
2197 entry.suppressed_until_focus_leaves = entry.promoted_by_focus;
2204 entry.overlay_id = None;
2205 entry.is_sticky = false;
2206 entry.hover_start = None;
2207 entry.real_hover_start = None;
2208 entry.hover_origin = None;
2209 entry.shown_at_sim = None;
2210 entry.shown_at_real = None;
2211 entry.promoted_by_focus = false;
2212 entry.armed_by_focus = false;
2213 entry.armed_by_hold = false;
2214 if let Some(sink) = entry.shown_at_sink.as_ref() {
2215 sink.set(None);
2216 }
2217 any_tooltip_dismissed = true;
2218 }
2219 }
2220 if any_tooltip_dismissed {
2221 let grace = super::TOOLTIP_SESSION_GRACE;
2222 self.tooltip_session_until_sim = Some(self.sim_clock + grace);
2223 self.tooltip_session_until_real = Some(std::time::Instant::now() + grace);
2224 }
2225 for &id in content_ids {
2226 let focused_in_subtree = self
2227 .focused
2228 .filter(|focused| self.is_descendant_of(*focused, id));
2229 let hovered_in_subtree = self
2230 .hovered_id()
2231 .filter(|hovered| self.is_descendant_of(*hovered, id));
2232
2233 if let Some(focused) = focused_in_subtree {
2234 self.dispatch_to_widget(focused, &WidgetEvent::FocusLost, &mut *ops);
2235 if self
2236 .focused
2237 .is_some_and(|current| self.is_descendant_of(current, id))
2238 {
2239 let old = self.focused;
2240 self.set_focused(None);
2241 self.focus_origin = None;
2242 self.update_focus_within_signals(old, None);
2243 self.update_view_focus_signals(old, None);
2244 }
2245 }
2246
2247 self.cancel_pointers_in_subtree(
2253 id,
2254 crate::pointer::CancelReason::OverlayDismissed,
2255 &mut *ops,
2256 );
2257 let _parked = self.arena.set_dormant(id);
2258
2259 if hovered_in_subtree.is_some() {
2260 let old = self.hovered_id();
2261 self.set_hovered(None);
2262 self.update_hover_within_signals(old, None);
2263 }
2264 }
2265 if !content_ids.is_empty() {
2266 self.cached_frame = None;
2267 self.a11y_dirty = true;
2268 }
2269 }
2270
2271 pub fn is_visible(&self, id: WidgetId) -> bool {
2272 self.arena.is_active(id)
2273 }
2274}
2275
2276#[cfg(test)]
2277mod tests {
2278 use super::*;
2279 use crate::test_widgets::FillWidget;
2280
2281 #[test]
2282 fn is_visible_reflects_dormancy() {
2283 let mut tree = WidgetTree::new();
2284 let widget = tree.add(FillWidget::new());
2285 tree.layout(SizeProposal::exact(100.0, 50.0));
2286
2287 assert!(tree.is_visible(widget));
2288 tree.set_dormant(widget);
2289 assert!(!tree.is_visible(widget));
2290 tree.activate(widget);
2291 assert!(tree.is_visible(widget));
2292 }
2293
2294 #[test]
2295 fn frame_tick_deadline_caps_per_frame_effects_at_60hz() {
2296 let mut tree = WidgetTree::new();
2297 assert!(tree.frame_tick_deadline().is_none());
2299
2300 tree.request_frame();
2303 let t0 = std::time::Instant::now();
2305 tree.last_frame_time = Some(t0);
2306
2307 let deadline = tree
2308 .frame_tick_deadline()
2309 .expect("an armed per-frame effect must publish a deadline");
2310 let interval = deadline.saturating_duration_since(t0);
2311 assert!(
2314 interval >= std::time::Duration::from_micros(16_000)
2315 && interval <= std::time::Duration::from_micros(17_500),
2316 "per-frame effects must pace at ~60 Hz (got {interval:?})"
2317 );
2318
2319 assert_eq!(
2322 tree.next_timer_deadline(),
2323 Some(deadline),
2324 "frame-tick deadline must be surfaced by next_timer_deadline"
2325 );
2326 }
2327
2328 #[test]
2329 fn throttled_subscriber_stretches_deadline_but_per_frame_wins_min() {
2330 use crate::test_widgets::{FillWidget, StackWidget};
2331 let mut tree = WidgetTree::new();
2332 let w = tree.add(FillWidget::new());
2333 let w2 = tree.add(FillWidget::new());
2334 tree.add(StackWidget::new().child(w).child(w2));
2335 let _throttled =
2337 tree.subscribe_frame_tick_throttled(w, std::time::Duration::from_millis(1500));
2338 tree.layout(SizeProposal::exact(200.0, 100.0));
2342 let _ = tree.render();
2343 assert!(
2344 !tree.frame_tick_requested.get(),
2345 "precondition: nothing but the subscriber wants this frame"
2346 );
2347 let t0 = tree.last_frame_time.expect("a frame has run");
2348
2349 let d = tree.frame_tick_deadline().expect("armed");
2351 let dt = d.saturating_duration_since(t0);
2352 assert!(
2353 dt >= std::time::Duration::from_millis(1490)
2354 && dt <= std::time::Duration::from_millis(1510),
2355 "a lone throttled subscriber must pace at its interval (~1.5 s), got {dt:?}"
2356 );
2357
2358 let _per_frame = tree.subscribe_frame_tick(w2);
2363 let d2 = tree.frame_tick_deadline().expect("armed");
2364 let dt2 = d2.saturating_duration_since(t0);
2365 assert!(
2366 dt2 <= std::time::Duration::from_millis(20),
2367 "a per-frame subscriber must pull the shared deadline to 60 Hz, got {dt2:?}"
2368 );
2369 }
2370
2371 #[test]
2377 fn a_raw_frame_request_is_not_paced_by_a_throttled_subscriber() {
2378 use crate::test_widgets::FillWidget;
2379 const NORMAL_FRAME: std::time::Duration = std::time::Duration::from_micros(17_500);
2380 const CLOCK: std::time::Duration = std::time::Duration::from_secs(60);
2381 fn frame(tree: &mut WidgetTree) {
2382 tree.layout(SizeProposal::exact(200.0, 100.0));
2383 let _ = tree.render();
2384 }
2385 fn wait(tree: &WidgetTree) -> std::time::Duration {
2386 let deadline = tree.frame_tick_deadline().expect("armed");
2387 deadline.saturating_duration_since(tree.last_frame_time.expect("a frame has run"))
2388 }
2389
2390 let mut tree = WidgetTree::new();
2391 let clock = tree.add(FillWidget::new());
2392 let _tick = tree.subscribe_frame_tick_throttled(clock, CLOCK);
2393 frame(&mut tree);
2394 assert!(
2395 wait(&tree) >= CLOCK - NORMAL_FRAME,
2396 "precondition: the painted clock paces the loop at its own interval"
2397 );
2398
2399 for request in [
2402 (|tree: &WidgetTree| tree.request_frame()) as fn(&WidgetTree),
2403 |tree: &WidgetTree| tree.frame_request_handle().set(true),
2404 ] {
2405 request(&tree);
2406 assert!(
2407 wait(&tree) <= NORMAL_FRAME,
2408 "a requested frame must not wait on the clock (waited {:?})",
2409 wait(&tree)
2410 );
2411 assert_eq!(tree.next_timer_deadline(), tree.frame_tick_deadline());
2412
2413 frame(&mut tree);
2414 assert!(
2415 wait(&tree) >= CLOCK - NORMAL_FRAME,
2416 "once the request is served the clock paces alone again, rather than the loop \
2417 free-running at 60 Hz (waited {:?})",
2418 wait(&tree)
2419 );
2420 }
2421 }
2422
2423 #[test]
2432 fn the_subscriber_arm_is_spent_by_its_tick_and_ends_with_its_owner() {
2433 use crate::test_widgets::StackWidget;
2434 fn frame(tree: &mut WidgetTree) {
2435 tree.layout(SizeProposal::exact(200.0, 100.0));
2436 let _ = tree.render();
2437 }
2438
2439 let mut tree = WidgetTree::new();
2440 let owner = tree.add(FillWidget::new());
2441 let sibling = tree.add(FillWidget::new());
2442 tree.add(StackWidget::new().child(owner).child(sibling));
2443 let ticks = std::rc::Rc::new(std::cell::Cell::new(0_u32));
2444 let _count = tree.frame_tick().observe({
2445 let ticks = ticks.clone();
2446 move |_| ticks.set(ticks.get() + 1)
2447 });
2448 let _sub = tree.subscribe_frame_tick(owner);
2449
2450 frame(&mut tree);
2451 assert!(
2452 tree.frame_requested(),
2453 "a render that painted the subscriber arms the next tick"
2454 );
2455 assert!(
2456 !tree.frame_tick_requested.get(),
2457 "precondition: armed by the subscribers' flag, not by a raw request"
2458 );
2459
2460 let before = ticks.get();
2462 tree.layout(SizeProposal::exact(200.0, 100.0));
2463 assert_eq!(
2464 ticks.get(),
2465 before + 1,
2466 "the armed tick fires on the next layout"
2467 );
2468 assert!(
2469 !tree.frame_requested(),
2470 "the layout that fired the tick has spent the arm"
2471 );
2472 let _ = tree.render();
2473 assert!(
2474 tree.frame_requested(),
2475 "the owner painted again and re-armed"
2476 );
2477
2478 let before = ticks.get();
2480 tree.advance_time(std::time::Duration::from_millis(16));
2481 assert_eq!(ticks.get(), before + 1, "advance_time fires an armed tick");
2482 assert!(
2483 !tree.frame_requested(),
2484 "advance_time spends the arm as layout does"
2485 );
2486 tree.resume_real_time();
2487
2488 frame(&mut tree);
2491 tree.set_dormant(owner);
2492 assert!(
2493 tree.frame_tick_deadline().is_some(),
2494 "the tick armed before the owner was parked is still due"
2495 );
2496 let before = ticks.get();
2497 frame(&mut tree);
2498 assert_eq!(ticks.get(), before + 1);
2499 assert!(
2500 !tree.frame_requested(),
2501 "a parked subscriber must not re-arm the tick"
2502 );
2503 assert_eq!(
2504 tree.next_timer_deadline(),
2505 None,
2506 "with its only subscriber parked the loop sleeps"
2507 );
2508 }
2509
2510 #[test]
2511 fn show_and_dismiss_overlay() {
2512 let mut tree = WidgetTree::new();
2513 let anchor = tree.add(FillWidget::new());
2514 let content = tree.add(FillWidget::new().label("Overlay"));
2515 tree.layout(SizeProposal::exact(200.0, 100.0));
2516
2517 assert!(tree.active_overlays().is_empty());
2518
2519 let id = tree.show_overlay(crate::overlay::OverlayRequest::new(
2520 content,
2521 anchor,
2522 crate::overlay::OverlayPlacement::Below,
2523 crate::overlay::DismissBehavior::Manual,
2524 ));
2525
2526 assert_eq!(tree.active_overlays().len(), 1);
2527
2528 tree.dismiss_overlay(id);
2529 assert!(tree.active_overlays().is_empty());
2530 assert!(!tree.is_visible(content));
2531 }
2532
2533 #[test]
2538 fn modal_centered_overlay_is_a_host_surface() {
2539 let mut tree = WidgetTree::new();
2540 let anchor = tree.add(FillWidget::new());
2541 let modal_content = tree.add(FillWidget::new().label("Modal"));
2542 let dropdown_content = tree.add(FillWidget::new().label("Dropdown"));
2543 tree.layout(SizeProposal::exact(200.0, 100.0));
2544
2545 let modal = tree.show_overlay(crate::overlay::OverlayRequest::new(
2546 modal_content,
2547 anchor,
2548 crate::overlay::OverlayPlacement::Centered,
2549 crate::overlay::DismissBehavior::EscapeOrClickOutside,
2550 ));
2551 let dropdown = tree.show_overlay(
2552 crate::overlay::OverlayRequest::new(
2553 dropdown_content,
2554 anchor,
2555 crate::overlay::OverlayPlacement::Below,
2556 crate::overlay::DismissBehavior::EscapeOrClickOutside,
2557 )
2558 .parent_overlay(modal),
2559 );
2560
2561 assert!(
2562 tree.overlay_is_host_surface(modal),
2563 "a Centered modal overlay must be treated as a host surface"
2564 );
2565 assert!(
2566 !tree.overlay_is_host_surface(dropdown),
2567 "a plain dropdown overlay is not a host surface"
2568 );
2569 }
2570
2571 #[test]
2572 fn escape_dismisses_topmost_overlay() {
2573 let mut tree = WidgetTree::new();
2574 let anchor = tree.add(FillWidget::new().focusable());
2575 let content = tree.add(FillWidget::new());
2576 tree.layout(SizeProposal::exact(200.0, 100.0));
2577 tree.focus(anchor);
2578
2579 tree.show_overlay(crate::overlay::OverlayRequest::new(
2580 content,
2581 anchor,
2582 crate::overlay::OverlayPlacement::Below,
2583 crate::overlay::DismissBehavior::EscapeOrClickOutside,
2584 ));
2585
2586 assert_eq!(tree.active_overlays().len(), 1);
2587
2588 tree.press_key(Key::Escape, Modifiers::NONE);
2589 assert!(tree.active_overlays().is_empty());
2590 assert!(!tree.is_visible(content));
2591 }
2592
2593 fn show_two_nested_overlays(tree: &mut WidgetTree) -> (WidgetId, WidgetId) {
2596 let anchor = tree.add(FillWidget::new());
2597 let c1 = tree.add(FillWidget::new());
2598 let c2 = tree.add(FillWidget::new());
2599 tree.layout(SizeProposal::exact(200.0, 100.0));
2600 let o1 = tree.show_overlay(crate::overlay::OverlayRequest::new(
2601 c1,
2602 anchor,
2603 crate::overlay::OverlayPlacement::Below,
2604 crate::overlay::DismissBehavior::Manual,
2605 ));
2606 tree.show_overlay(
2607 crate::overlay::OverlayRequest::new(
2608 c2,
2609 c1,
2610 crate::overlay::OverlayPlacement::Below,
2611 crate::overlay::DismissBehavior::Manual,
2612 )
2613 .parent_overlay(o1),
2614 );
2615 (c1, c2)
2616 }
2617
2618 #[test]
2619 fn nested_overlay_back_key_dismisses_with_arrow_left_under_ltr() {
2620 let mut tree = WidgetTree::new();
2621 let _ = show_two_nested_overlays(&mut tree);
2622 assert_eq!(tree.active_overlays().len(), 2);
2623
2624 tree.press_key(Key::ArrowRight, Modifiers::NONE);
2626 assert_eq!(tree.active_overlays().len(), 2);
2627
2628 tree.press_key(Key::ArrowLeft, Modifiers::NONE);
2630 assert_eq!(tree.active_overlays().len(), 1);
2631 }
2632
2633 #[test]
2634 fn nested_overlay_back_key_flips_to_arrow_right_under_rtl() {
2635 let mut tree = WidgetTree::new();
2636 tree.set_layout_direction(crate::environment::LayoutDirection::RightToLeft);
2637 let _ = show_two_nested_overlays(&mut tree);
2638 assert_eq!(tree.active_overlays().len(), 2);
2639
2640 tree.press_key(Key::ArrowLeft, Modifiers::NONE);
2643 assert_eq!(tree.active_overlays().len(), 2);
2644
2645 tree.press_key(Key::ArrowRight, Modifiers::NONE);
2647 assert_eq!(tree.active_overlays().len(), 1);
2648 }
2649
2650 fn show_inert_overlay(tree: &mut WidgetTree, anchor: WidgetId) -> WidgetId {
2652 let content = tree.add(FillWidget::new());
2653 tree.layout(SizeProposal::exact(200.0, 100.0));
2654 tree.show_overlay(
2655 crate::overlay::OverlayRequest::new(
2656 content,
2657 anchor,
2658 crate::overlay::OverlayPlacement::Below,
2659 crate::overlay::DismissBehavior::EscapeKey,
2660 )
2661 .inert(),
2662 );
2663 content
2664 }
2665
2666 #[test]
2671 fn back_key_does_not_count_an_inert_overlay_as_a_menu_level() {
2672 for inert_first in [true, false] {
2673 let mut tree = WidgetTree::new();
2674 let anchor = tree.add(FillWidget::new());
2675 let menu = tree.add(FillWidget::new());
2676 tree.layout(SizeProposal::exact(200.0, 100.0));
2677 let show_menu = |tree: &mut WidgetTree| {
2678 tree.show_overlay(crate::overlay::OverlayRequest::new(
2679 menu,
2680 anchor,
2681 crate::overlay::OverlayPlacement::Below,
2682 crate::overlay::DismissBehavior::Manual,
2683 ));
2684 };
2685 let inert = if inert_first {
2686 let inert = show_inert_overlay(&mut tree, anchor);
2687 show_menu(&mut tree);
2688 inert
2689 } else {
2690 show_menu(&mut tree);
2691 show_inert_overlay(&mut tree, anchor)
2692 };
2693 assert_eq!(tree.active_overlays().len(), 2);
2694
2695 tree.press_key(Key::ArrowLeft, Modifiers::NONE);
2696 assert_eq!(
2697 tree.active_overlays().len(),
2698 2,
2699 "inert shown first: {inert_first}: one menu is not a cascade"
2700 );
2701 assert!(tree.is_visible(menu) && tree.is_visible(inert));
2702 }
2703 }
2704
2705 #[test]
2708 fn back_key_closes_the_submenu_beneath_an_inert_overlay() {
2709 let mut tree = WidgetTree::new();
2710 let (parent, submenu) = show_two_nested_overlays(&mut tree);
2711 let inert = show_inert_overlay(&mut tree, parent);
2712 assert_eq!(tree.active_overlays().len(), 3);
2713
2714 tree.press_key(Key::ArrowLeft, Modifiers::NONE);
2715 assert!(!tree.is_visible(submenu), "the submenu closes");
2716 assert!(tree.is_visible(parent), "its parent stays open");
2717 assert!(
2718 tree.is_visible(inert),
2719 "the inert overlay is not what closes"
2720 );
2721 }
2722
2723 #[derive(Debug)]
2726 struct KeyProbe {
2727 seen: std::rc::Rc<std::cell::RefCell<Vec<Key>>>,
2728 }
2729
2730 impl Widget for KeyProbe {
2731 fn layout_response(
2732 &self,
2733 proposal: SizeProposal,
2734 _ctx: &crate::widget::LayoutContext,
2735 ) -> crate::widget::LayoutResponse {
2736 proposal.resolve(0.0, 0.0).into()
2737 }
2738
2739 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
2740 let seen = self.seen.clone();
2741 ctx.apply_self_handlers(
2742 crate::widget_builder::HandlerSet::new()
2743 .focusable(true)
2744 .on_key(move |event, _ctx| match event {
2745 WidgetEvent::KeyDown { key, .. } => {
2746 seen.borrow_mut().push(*key);
2747 crate::event::EventResponse::Handled
2748 }
2749 _ => crate::event::EventResponse::Ignored,
2750 }),
2751 );
2752 Vec::new()
2753 }
2754 }
2755
2756 #[test]
2768 fn back_key_ignores_text_affordance_overlays_and_reaches_the_editor() {
2769 let mut tree = WidgetTree::new();
2770 let seen = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
2771 let editor = tree.add(KeyProbe { seen: seen.clone() });
2772 let handles_a = tree.add(FillWidget::new());
2773 let handles_b = tree.add(FillWidget::new());
2774 tree.layout(SizeProposal::exact(200.0, 100.0));
2775 tree.focus(editor);
2776
2777 for content in [handles_a, handles_b] {
2779 tree.show_overlay_in_band(
2780 crate::overlay::OverlayRequest::new(
2781 content,
2782 editor,
2783 crate::overlay::OverlayPlacement::FullViewport,
2784 crate::overlay::DismissBehavior::Manual,
2785 ),
2786 crate::overlay::OverlayBand::TextAffordance,
2787 );
2788 }
2789 assert_eq!(tree.active_overlays().len(), 2);
2790
2791 tree.press_key(Key::ArrowLeft, Modifiers::NONE);
2792
2793 assert_eq!(
2794 tree.active_overlays().len(),
2795 2,
2796 "a text affordance is not a menu cascade level — the back key must \
2797 leave both hosts standing"
2798 );
2799 assert_eq!(
2800 seen.borrow().as_slice(),
2801 &[Key::ArrowLeft],
2802 "ArrowLeft must reach the focused editor; swallowing it is the bug \
2803 that left the caret unable to step left in any dual-pane tab"
2804 );
2805 }
2806
2807 #[test]
2811 fn back_key_still_closes_a_submenu_over_a_mounted_text_affordance() {
2812 let mut tree = WidgetTree::new();
2813 let affordance = tree.add(FillWidget::new());
2814 let anchor = tree.add(FillWidget::new());
2815 let menu = tree.add(FillWidget::new());
2816 let submenu = tree.add(FillWidget::new());
2817 tree.layout(SizeProposal::exact(200.0, 100.0));
2818 tree.show_overlay_in_band(
2819 crate::overlay::OverlayRequest::new(
2820 affordance,
2821 anchor,
2822 crate::overlay::OverlayPlacement::FullViewport,
2823 crate::overlay::DismissBehavior::Manual,
2824 ),
2825 crate::overlay::OverlayBand::TextAffordance,
2826 );
2827 let parent = tree.show_overlay(crate::overlay::OverlayRequest::new(
2828 menu,
2829 anchor,
2830 crate::overlay::OverlayPlacement::Below,
2831 crate::overlay::DismissBehavior::Manual,
2832 ));
2833 tree.show_overlay(
2834 crate::overlay::OverlayRequest::new(
2835 submenu,
2836 menu,
2837 crate::overlay::OverlayPlacement::Below,
2838 crate::overlay::DismissBehavior::Manual,
2839 )
2840 .parent_overlay(parent),
2841 );
2842 assert_eq!(tree.active_overlays().len(), 3);
2843
2844 tree.press_key(Key::ArrowLeft, Modifiers::NONE);
2845 assert_eq!(
2846 tree.active_overlays().len(),
2847 2,
2848 "the submenu must still close: the affordance host narrows the \
2849 count, it does not disarm the back key"
2850 );
2851 }
2852
2853 #[test]
2858 fn back_key_does_not_dismiss_a_dialog_on_top_of_a_modal() {
2859 let mut tree = WidgetTree::new();
2860 let anchor = tree.add(FillWidget::new());
2861 let scrim1 = tree.add(FillWidget::new());
2862 let scrim2 = tree.add(FillWidget::new());
2863 let dialog = tree.add(FillWidget::new());
2864 tree.layout(SizeProposal::exact(200.0, 100.0));
2865 for c in [scrim1, scrim2] {
2867 tree.show_overlay(crate::overlay::OverlayRequest::new(
2868 c,
2869 anchor,
2870 crate::overlay::OverlayPlacement::Below,
2871 crate::overlay::DismissBehavior::Manual,
2872 ));
2873 }
2874 tree.show_overlay(crate::overlay::OverlayRequest::new(
2876 dialog,
2877 anchor,
2878 crate::overlay::OverlayPlacement::Centered,
2879 crate::overlay::DismissBehavior::Manual,
2880 ));
2881 assert_eq!(tree.active_overlays().len(), 3);
2882
2883 tree.press_key(Key::ArrowLeft, Modifiers::NONE);
2886 assert_eq!(
2887 tree.active_overlays().len(),
2888 3,
2889 "the back key must not close a dialog sitting on top of a modal"
2890 );
2891 }
2892
2893 #[test]
2894 fn escape_does_not_dismiss_manual_overlay() {
2895 let mut tree = WidgetTree::new();
2896 let anchor = tree.add(FillWidget::new().focusable());
2897 let content = tree.add(FillWidget::new());
2898 tree.layout(SizeProposal::exact(200.0, 100.0));
2899 tree.focus(anchor);
2900
2901 tree.show_overlay(crate::overlay::OverlayRequest::new(
2902 content,
2903 anchor,
2904 crate::overlay::OverlayPlacement::Below,
2905 crate::overlay::DismissBehavior::Manual,
2906 ));
2907
2908 assert_eq!(tree.active_overlays().len(), 1);
2909
2910 tree.press_key(Key::Escape, Modifiers::NONE);
2911 assert_eq!(tree.active_overlays().len(), 1);
2913 }
2914
2915 #[test]
2916 fn click_outside_dismisses_overlay() {
2917 let mut tree = WidgetTree::new();
2918 let anchor = tree.add(FillWidget::new());
2919 let content = tree.add(FillWidget::new());
2920 tree.layout(SizeProposal::exact(200.0, 100.0));
2921
2922 let overlay = tree.show_overlay(crate::overlay::OverlayRequest::new(
2923 content,
2924 anchor,
2925 crate::overlay::OverlayPlacement::Below,
2926 crate::overlay::DismissBehavior::ClickOutside,
2927 ));
2928
2929 tree.overlay_manager
2930 .set_content_bounds(overlay, teksilo_canvas::Size::new(100.0, 50.0));
2931
2932 assert_eq!(tree.active_overlays().len(), 1);
2933
2934 tree.dispatch_event(WidgetEvent::pointer_down(
2935 Point::new(500.0, 500.0),
2936 PointerButton::Primary,
2937 Modifiers::NONE,
2938 ));
2939 assert!(tree.active_overlays().is_empty());
2940 assert!(!tree.is_visible(content));
2941 }
2942
2943 #[test]
2944 fn cascade_dismissal() {
2945 let mut tree = WidgetTree::new();
2946 let anchor = tree.add(FillWidget::new());
2947 let content_a = tree.add(FillWidget::new());
2948 let content_b = tree.add(FillWidget::new());
2949 tree.layout(SizeProposal::exact(200.0, 100.0));
2950
2951 let parent = tree.show_overlay(crate::overlay::OverlayRequest::new(
2952 content_a,
2953 anchor,
2954 crate::overlay::OverlayPlacement::Below,
2955 crate::overlay::DismissBehavior::Manual,
2956 ));
2957 tree.show_overlay(
2958 crate::overlay::OverlayRequest::new(
2959 content_b,
2960 content_a,
2961 crate::overlay::OverlayPlacement::TrailingEdge,
2962 crate::overlay::DismissBehavior::Manual,
2963 )
2964 .parent_overlay(parent),
2965 );
2966
2967 assert_eq!(tree.active_overlays().len(), 2);
2968
2969 tree.dismiss_overlay(parent);
2970 assert!(tree.active_overlays().is_empty());
2971 assert!(!tree.is_visible(content_a));
2972 assert!(!tree.is_visible(content_b));
2973 }
2974
2975 #[test]
2976 fn dismissed_overlay_content_is_dormant_and_invisible() {
2977 let mut tree = WidgetTree::new();
2978 let anchor = tree.add(FillWidget::new());
2979 let content = tree.add(FillWidget::new());
2980 tree.layout(SizeProposal::exact(800.0, 600.0));
2981
2982 let id = tree.show_overlay(crate::overlay::OverlayRequest::new(
2983 content,
2984 anchor,
2985 crate::overlay::OverlayPlacement::Below,
2986 crate::overlay::DismissBehavior::Manual,
2987 ));
2988
2989 tree.layout(SizeProposal::exact(800.0, 600.0));
2990
2991 tree.dismiss_overlay(id);
2992 assert!(!tree.is_visible(content));
2993
2994 tree.layout(SizeProposal::exact(800.0, 600.0));
2995
2996 let center = tree.bounds(content).center();
2997 let hit = tree.hit_test(center);
2998 assert_ne!(hit, Some(content));
2999
3000 let _frame = tree.render();
3001 assert!(!tree.is_visible(content));
3002 }
3003
3004 #[test]
3005 fn tooltip_appears_after_delay() {
3006 let mut tree = WidgetTree::new();
3007 let anchor = tree.add(FillWidget::new());
3008 let tooltip = tree.add(FillWidget::new().label("Tooltip text"));
3009 tree.layout(SizeProposal::exact(200.0, 100.0));
3010
3011 tree.attach_tooltip(anchor, tooltip, std::time::Duration::from_millis(500));
3012
3013 let center = tree.bounds(anchor).center();
3014 tree.pointer_move(center);
3015 assert!(tree.active_overlays().is_empty());
3016
3017 tree.advance_time(std::time::Duration::from_millis(600));
3018
3019 assert_eq!(tree.active_overlays().len(), 1);
3020 assert!(tree.find_by_label("Tooltip text").is_some());
3021 }
3022
3023 #[test]
3024 fn tooltip_survives_theme_switch() {
3025 let mut tree = WidgetTree::new().with_theme(crate::presets::intui::light());
3030 let anchor = tree.add(FillWidget::new());
3031 let tooltip = tree.add(FillWidget::new().label("Tip"));
3032 tree.layout(SizeProposal::exact(200.0, 100.0));
3033
3034 tree.attach_tooltip(anchor, tooltip, std::time::Duration::from_millis(500));
3035
3036 tree.set_theme(crate::presets::intui::dark());
3037
3038 tree.pointer_move(tree.bounds(anchor).center());
3039 tree.advance_time(std::time::Duration::from_millis(600));
3040
3041 assert_eq!(tree.active_overlays().len(), 1);
3042 assert!(tree.find_by_label("Tip").is_some());
3043 }
3044
3045 #[test]
3046 fn tooltip_suppressed_when_hovering_anchor_owned_overlay_content() {
3047 let mut tree = WidgetTree::new();
3055 let anchor = tree.add(FillWidget::new());
3056 let panel = tree.add_child(anchor, FillWidget::new());
3059 let row = tree.add_child(panel, FillWidget::new());
3060 let tip = tree.add(FillWidget::new().label("Tip"));
3061 tree.layout(SizeProposal::exact(200.0, 100.0));
3062
3063 let delay = std::time::Duration::from_millis(100);
3064 tree.attach_tooltip(anchor, tip, delay);
3065
3066 tree.tooltip_pointer_enter(anchor);
3069 tree.advance_time(delay + std::time::Duration::from_millis(50));
3070 assert_eq!(
3071 tree.active_overlays().len(),
3072 1,
3073 "tooltip should appear when hovering the anchor itself"
3074 );
3075 tree.pointer_move(Point::new(500.0, 500.0));
3078 tree.advance_time(std::time::Duration::from_millis(150));
3079 assert!(tree.active_overlays().is_empty());
3080
3081 tree.show_overlay(crate::overlay::OverlayRequest::new(
3083 panel,
3084 anchor,
3085 crate::overlay::OverlayPlacement::Below,
3086 crate::overlay::DismissBehavior::Manual,
3087 ));
3088 assert_eq!(tree.active_overlays().len(), 1);
3089
3090 tree.tooltip_pointer_enter(row);
3093 tree.advance_time(delay + std::time::Duration::from_millis(50));
3094 assert_eq!(
3095 tree.active_overlays().len(),
3096 1,
3097 "anchor tooltip must not leak onto its own overlay's rows"
3098 );
3099 }
3100
3101 #[test]
3102 fn tooltip_inside_overlay_still_fires() {
3103 let mut tree = WidgetTree::new();
3107 let host = tree.add(FillWidget::new());
3108 let panel = tree.add_child(host, FillWidget::new());
3109 let row = tree.add_child(panel, FillWidget::new());
3110 let tip = tree.add(FillWidget::new().label("Row tip"));
3111 tree.layout(SizeProposal::exact(200.0, 100.0));
3112
3113 let delay = std::time::Duration::from_millis(100);
3114 tree.attach_tooltip(row, tip, delay);
3116
3117 tree.show_overlay(crate::overlay::OverlayRequest::new(
3118 panel,
3119 host,
3120 crate::overlay::OverlayPlacement::Below,
3121 crate::overlay::DismissBehavior::Manual,
3122 ));
3123 assert_eq!(tree.active_overlays().len(), 1);
3124
3125 tree.tooltip_pointer_enter(row);
3126 tree.advance_time(delay + std::time::Duration::from_millis(50));
3127 assert_eq!(
3128 tree.active_overlays().len(),
3129 2,
3130 "a row's own tooltip should still fire inside an overlay"
3131 );
3132 }
3133
3134 #[test]
3135 fn tooltip_dismissed_on_pointer_leave() {
3136 let mut tree = WidgetTree::new();
3137 let anchor = tree.add(FillWidget::new());
3138 let tooltip = tree.add(FillWidget::new().label("Tip"));
3139 tree.layout(SizeProposal::exact(200.0, 100.0));
3140
3141 tree.attach_tooltip(anchor, tooltip, std::time::Duration::from_millis(500));
3142
3143 tree.pointer_move(tree.bounds(anchor).center());
3144 tree.advance_time(std::time::Duration::from_millis(600));
3145 assert_eq!(tree.active_overlays().len(), 1);
3146
3147 tree.pointer_move(Point::new(500.0, 500.0));
3152 assert_eq!(
3153 tree.active_overlays().len(),
3154 1,
3155 "tooltip persists briefly after anchor-leave (hoverable grace)"
3156 );
3157 tree.advance_time(std::time::Duration::from_millis(150));
3158 assert!(
3159 tree.active_overlays().is_empty(),
3160 "tooltip dismissed after the 100ms grace outside anchor+overlay"
3161 );
3162 }
3163
3164 #[test]
3165 fn tooltip_not_shown_if_pointer_leaves_before_delay() {
3166 let mut tree = WidgetTree::new();
3167 let anchor = tree.add(FillWidget::new());
3168 let tooltip = tree.add(FillWidget::new().label("Tip"));
3169 tree.layout(SizeProposal::exact(200.0, 100.0));
3170
3171 tree.attach_tooltip(anchor, tooltip, std::time::Duration::from_millis(500));
3172
3173 tree.pointer_move(tree.bounds(anchor).center());
3174 tree.advance_time(std::time::Duration::from_millis(200));
3175 tree.pointer_move(Point::new(500.0, 500.0));
3176
3177 tree.advance_time(std::time::Duration::from_millis(500));
3178 assert!(tree.active_overlays().is_empty());
3179 }
3180
3181 #[test]
3182 fn tooltip_reshow_uses_short_delay_after_prior_tip() {
3183 let mut tree = WidgetTree::new();
3186 let a = tree.add(FillWidget::new());
3187 let b = tree.add(FillWidget::new());
3188 let tip_a = tree.add(FillWidget::new().label("Tip A"));
3189 let tip_b = tree.add(FillWidget::new().label("Tip B"));
3190 tree.layout(SizeProposal::exact(400.0, 200.0));
3191
3192 let delay = std::time::Duration::from_millis(500);
3193 tree.attach_tooltip(a, tip_a, delay);
3194 tree.attach_tooltip(b, tip_b, delay);
3195
3196 tree.tooltip_pointer_enter(a);
3199 tree.advance_time(std::time::Duration::from_millis(150));
3200 assert!(
3201 tree.active_overlays().is_empty(),
3202 "must not appear before full initial delay"
3203 );
3204 tree.advance_time(std::time::Duration::from_millis(400));
3205 assert_eq!(
3206 tree.active_overlays().len(),
3207 1,
3208 "first tip after initial delay"
3209 );
3210 assert!(tree.find_by_label("Tip A").is_some());
3211
3212 let mut noop = crate::window::NoopWindowOps;
3214 tree.tooltip_pointer_leave(a, &mut noop);
3215 tree.tooltip_pointer_enter(b);
3216 tree.advance_time(std::time::Duration::from_millis(120));
3217 assert!(
3218 tree.find_by_label("Tip B").is_some(),
3219 "second tip should use the short reshow delay while session is warm"
3220 );
3221 }
3222
3223 #[test]
3224 fn reshow_delay_reverts_to_full_after_the_session_grace_expires() {
3225 let mut tree = WidgetTree::new();
3231 tree.set_accessibility_preferences(false, true, 1.0);
3235 let a = tree.add(FillWidget::new());
3236 let b = tree.add(FillWidget::new());
3237 let tip_a = tree.add(FillWidget::new().label("Tip A"));
3238 let tip_b = tree.add(FillWidget::new().label("Tip B"));
3239 tree.layout(SizeProposal::exact(400.0, 200.0));
3240
3241 let delay = std::time::Duration::from_millis(500);
3242 tree.attach_tooltip(a, tip_a, delay);
3243 tree.attach_tooltip(b, tip_b, delay);
3244
3245 tree.tooltip_pointer_enter(a);
3247 tree.advance_time(std::time::Duration::from_millis(550));
3248 assert!(tree.find_by_label("Tip A").is_some(), "first tip shown");
3249
3250 tree.pointer_move(Point::new(900.0, 900.0));
3251 tree.advance_time(std::time::Duration::from_millis(150));
3252 assert!(tree.active_overlays().is_empty(), "A dismissed on leave");
3253
3254 tree.advance_time(super::TOOLTIP_SESSION_GRACE + std::time::Duration::from_millis(50));
3256
3257 tree.tooltip_pointer_enter(b);
3258 tree.advance_time(std::time::Duration::from_millis(150));
3259 assert!(
3260 tree.active_overlays().is_empty(),
3261 "session went cold — B must pay the FULL delay, not the 100 ms reshow"
3262 );
3263 tree.advance_time(std::time::Duration::from_millis(400));
3264 assert!(
3265 tree.find_by_label("Tip B").is_some(),
3266 "B still appears once its full delay elapses"
3267 );
3268 }
3269
3270 #[test]
3271 fn warm_reshow_scales_the_delay_rather_than_flattening_every_tier() {
3272 let mut tree = WidgetTree::new();
3278 let light = tree.add(FillWidget::new());
3279 let heavy = tree.add(FillWidget::new());
3280 let tip_light = tree.add(FillWidget::new().label("Light"));
3281 let tip_heavy = tree.add(FillWidget::new().label("Heavy"));
3282 tree.layout(SizeProposal::exact(400.0, 200.0));
3283
3284 tree.attach_tooltip(light, tip_light, std::time::Duration::from_millis(500));
3285 tree.attach_tooltip(heavy, tip_heavy, std::time::Duration::from_millis(700));
3286
3287 tree.tooltip_pointer_enter(light);
3289 tree.advance_time(std::time::Duration::from_millis(550));
3290 assert!(tree.find_by_label("Light").is_some(), "session warm");
3291
3292 let mut noop = crate::window::NoopWindowOps;
3293 tree.tooltip_pointer_leave(light, &mut noop);
3294 tree.tooltip_pointer_enter(heavy);
3295
3296 tree.advance_time(std::time::Duration::from_millis(120));
3298 assert!(
3299 tree.find_by_label("Heavy").is_none(),
3300 "a heavy tooltip must not fire at the light tier's reshow delay"
3301 );
3302 tree.advance_time(std::time::Duration::from_millis(40));
3304 assert!(
3305 tree.find_by_label("Heavy").is_some(),
3306 "heavy reshow is the scaled 140 ms"
3307 );
3308 }
3309
3310 #[test]
3311 fn a_pinned_sticky_tooltip_does_not_hold_the_session_warm() {
3312 let mut tree = WidgetTree::new();
3316 tree.set_accessibility_preferences(false, true, 1.0); let a = tree.add(FillWidget::new());
3318 let b = tree.add(FillWidget::new());
3319 let tip_a = tree.add(FillWidget::new().label("Pinned"));
3320 let tip_b = tree.add(FillWidget::new().label("Other"));
3321 tree.layout(SizeProposal::exact(400.0, 200.0));
3322
3323 tree.attach_tooltip_with_sticky(
3324 a,
3325 tip_a,
3326 std::time::Duration::from_millis(500),
3327 Some(std::time::Duration::from_millis(100)),
3328 );
3329 tree.attach_tooltip(b, tip_b, std::time::Duration::from_millis(500));
3330
3331 tree.tooltip_pointer_enter(a);
3332 tree.advance_time(std::time::Duration::from_millis(550));
3333 assert!(tree.find_by_label("Pinned").is_some());
3334 tree.promote_tooltip_to_sticky(tip_a);
3335
3336 tree.advance_time(super::TOOLTIP_SESSION_GRACE + std::time::Duration::from_millis(50));
3338 tree.tooltip_pointer_enter(b);
3339 tree.advance_time(std::time::Duration::from_millis(150));
3340 assert!(
3341 tree.find_by_label("Other").is_none(),
3342 "a pinned sticky must not keep every other anchor on the reshow path"
3343 );
3344 }
3345
3346 #[test]
3347 fn reattaching_a_tooltip_does_not_grow_the_entry_table() {
3348 let mut tree = WidgetTree::new();
3354 let anchor = tree.add(FillWidget::new());
3355 tree.layout(SizeProposal::exact(200.0, 100.0));
3356
3357 let delay = std::time::Duration::from_millis(100);
3358 for _ in 0..25 {
3359 let tip = tree.add(FillWidget::new().label("Tip"));
3361 tree.attach_tooltip(anchor, tip, delay);
3362 assert_eq!(
3363 tree.tooltip_entry_count(),
3364 1,
3365 "an anchor must own exactly one tooltip entry across rebuilds"
3366 );
3367 }
3368
3369 tree.tooltip_pointer_enter(anchor);
3371 tree.advance_time(delay + std::time::Duration::from_millis(50));
3372 assert_eq!(
3373 tree.active_overlays().len(),
3374 1,
3375 "latest tooltip still shows"
3376 );
3377 }
3378
3379 #[test]
3380 fn destroying_an_anchor_reaps_its_tooltip_entry() {
3381 let mut tree = WidgetTree::new();
3384 let host = tree.add(FillWidget::new());
3385 let anchor = tree.add_child(host, FillWidget::new());
3386 let tip = tree.add(FillWidget::new().label("Tip"));
3387 tree.layout(SizeProposal::exact(200.0, 100.0));
3388
3389 tree.attach_tooltip(anchor, tip, std::time::Duration::from_millis(100));
3390 assert_eq!(tree.tooltip_entry_count(), 1);
3391
3392 tree.destroy_subtree(anchor);
3393 assert_eq!(
3394 tree.tooltip_entry_count(),
3395 0,
3396 "destroying the anchor must reap its entry and content node"
3397 );
3398 }
3399
3400 #[test]
3401 fn a_leaving_tooltip_schedules_a_wake_for_its_dismissal() {
3402 let mut tree = WidgetTree::new();
3407 tree.set_accessibility_preferences(false, true, 1.0); let anchor = tree.add(FillWidget::new());
3409 let tip = tree.add(FillWidget::new().label("Tip"));
3410 tree.layout(SizeProposal::exact(400.0, 200.0));
3411
3412 tree.attach_tooltip(anchor, tip, std::time::Duration::from_millis(100));
3413 tree.pointer_move(tree.bounds(anchor).center());
3414 tree.advance_time(std::time::Duration::from_millis(150));
3415 assert_eq!(tree.active_overlays().len(), 1, "tooltip shown");
3416
3417 assert!(
3419 tree.next_timer_deadline().is_none(),
3420 "a settled tooltip under the pointer schedules nothing"
3421 );
3422
3423 tree.pointer_move(Point::new(900.0, 900.0));
3426 assert!(
3427 tree.next_timer_deadline().is_some(),
3428 "the PointerLeave grace must contribute a wake deadline"
3429 );
3430 }
3431
3432 #[test]
3443 fn a_fading_out_overlay_schedules_a_wake_for_its_deferred_removal() {
3444 let mut tree = WidgetTree::new();
3445 let anchor = tree.add(FillWidget::new());
3446 let content = tree.add(FillWidget::new().label("Faded"));
3447 tree.layout(SizeProposal::exact(200.0, 100.0));
3448
3449 let id = tree.show_overlay(
3450 crate::overlay::OverlayRequest::new(
3451 content,
3452 anchor,
3453 crate::overlay::OverlayPlacement::Below,
3454 crate::overlay::DismissBehavior::Manual,
3455 )
3456 .with_fade(std::time::Duration::from_millis(200)),
3457 );
3458 tree.dismiss_overlay(id);
3459 assert!(
3460 tree.is_visible(content),
3461 "precondition: the removal is deferred, so there is something to wake for"
3462 );
3463
3464 let expected = tree.overlay_manager.next_fade_dismiss_deadline();
3470 assert!(
3471 expected.is_some(),
3472 "precondition: the fade start was stamped, so there is a deadline to compare against"
3473 );
3474 assert_eq!(
3475 tree.next_timer_deadline(),
3476 expected,
3477 "the wake deadline must be the deferred removal's own, not merely some deadline"
3478 );
3479 }
3480
3481 #[test]
3482 fn pressing_cancels_a_pending_dwell_and_dismisses_a_shown_tooltip() {
3483 let mut tree = WidgetTree::new();
3484 let anchor = tree.add(FillWidget::new());
3485 let tip = tree.add(FillWidget::new().label("Tip"));
3486 tree.layout(SizeProposal::exact(400.0, 200.0));
3487
3488 let delay = std::time::Duration::from_millis(500);
3489 tree.attach_tooltip(anchor, tip, delay);
3490
3491 tree.pointer_move(tree.bounds(anchor).center());
3494 tree.advance_time(std::time::Duration::from_millis(300));
3495 tree.tooltip_pointer_press(None);
3496 tree.advance_time(std::time::Duration::from_millis(400));
3497 assert!(
3498 tree.active_overlays().is_empty(),
3499 "a press must cancel the pending dwell, not merely delay it"
3500 );
3501
3502 tree.tooltip_pointer_enter(anchor);
3505 tree.advance_time(delay + std::time::Duration::from_millis(50));
3506 assert_eq!(tree.active_overlays().len(), 1, "tooltip shown again");
3507 tree.tooltip_pointer_press(Some(tree.bounds(anchor).center()));
3508 assert!(
3509 tree.active_overlays().is_empty(),
3510 "a press must dismiss the shown tooltip"
3511 );
3512 }
3513
3514 #[test]
3515 fn window_deactivation_retires_hover_tooltips() {
3516 let mut tree = WidgetTree::new();
3517 let anchor = tree.add(FillWidget::new());
3518 let tip = tree.add(FillWidget::new().label("Tip"));
3519 tree.layout(SizeProposal::exact(400.0, 200.0));
3520
3521 tree.attach_tooltip(anchor, tip, std::time::Duration::from_millis(100));
3522 tree.pointer_move(tree.bounds(anchor).center());
3523 tree.advance_time(std::time::Duration::from_millis(150));
3524 assert_eq!(tree.active_overlays().len(), 1);
3525
3526 tree.set_window_active(false);
3527 assert!(
3528 tree.active_overlays().is_empty(),
3529 "a tooltip must not float over another window's chrome"
3530 );
3531 }
3532
3533 #[test]
3534 fn only_the_innermost_anchor_arms_its_dwell() {
3535 let mut tree = WidgetTree::new();
3538 let panel = tree.add(FillWidget::new());
3539 let row = tree.add_child(panel, FillWidget::new());
3540 let panel_tip = tree.add(FillWidget::new().label("Panel"));
3541 let row_tip = tree.add(FillWidget::new().label("Row"));
3542 tree.layout(SizeProposal::exact(400.0, 200.0));
3543
3544 let delay = std::time::Duration::from_millis(100);
3545 tree.attach_tooltip(panel, panel_tip, delay);
3546 tree.attach_tooltip(row, row_tip, delay);
3547
3548 tree.tooltip_pointer_enter(row);
3549 tree.advance_time(delay + std::time::Duration::from_millis(50));
3550
3551 assert_eq!(
3552 tree.active_overlays().len(),
3553 1,
3554 "exactly one tooltip may open for a hover"
3555 );
3556 assert!(
3557 tree.find_by_label("Row").is_some(),
3558 "the innermost anchor wins"
3559 );
3560 }
3561
3562 #[test]
3563 fn escape_dismisses_a_hover_tooltip_and_falls_through_to_the_menu_below() {
3564 let mut tree = WidgetTree::new();
3568 tree.set_accessibility_preferences(false, true, 1.0); let anchor = tree.add(FillWidget::new());
3570 let menu = tree.add(FillWidget::new());
3571 let tip = tree.add(FillWidget::new().label("Tip"));
3572 tree.layout(SizeProposal::exact(400.0, 200.0));
3573
3574 let menu_overlay = tree.show_overlay(crate::overlay::OverlayRequest::new(
3575 menu,
3576 anchor,
3577 crate::overlay::OverlayPlacement::Below,
3578 crate::overlay::DismissBehavior::EscapeOrClickOutside,
3579 ));
3580 assert_eq!(tree.active_overlays().len(), 1);
3581
3582 tree.attach_tooltip(anchor, tip, std::time::Duration::from_millis(100));
3584 tree.tooltip_pointer_enter(anchor);
3585 tree.advance_time(std::time::Duration::from_millis(150));
3586 assert_eq!(
3587 tree.active_overlays().len(),
3588 2,
3589 "tooltip sits above the menu"
3590 );
3591
3592 let dismissed = tree.overlay_manager.try_dismiss_top_on_escape();
3594 assert!(dismissed.is_some(), "Escape must dismiss the hover tooltip");
3595 assert_eq!(tree.active_overlays().len(), 1);
3596
3597 let dismissed = tree.overlay_manager.try_dismiss_top_on_escape();
3599 assert_eq!(
3600 dismissed.map(|(id, _, _)| id),
3601 Some(menu_overlay),
3602 "Escape must then reach the menu underneath"
3603 );
3604 assert!(tree.active_overlays().is_empty());
3605 }
3606
3607 #[test]
3608 fn tooltip_timer_restarts_when_pointer_keeps_moving() {
3609 let mut tree = WidgetTree::new();
3612 let anchor = tree.add(FillWidget::new());
3613 let tooltip = tree.add(FillWidget::new().label("Tip"));
3614 tree.layout(SizeProposal::exact(200.0, 100.0));
3615
3616 tree.attach_tooltip(anchor, tooltip, std::time::Duration::from_millis(500));
3617
3618 let start = tree.bounds(anchor).center();
3619 tree.pointer_move(start);
3620 tree.advance_time(std::time::Duration::from_millis(300));
3621 tree.pointer_move(Point::new(start.x + 20.0, start.y));
3623 tree.advance_time(std::time::Duration::from_millis(300));
3626 assert!(
3627 tree.active_overlays().is_empty(),
3628 "moving past stationary slop must restart the delay"
3629 );
3630 tree.advance_time(std::time::Duration::from_millis(250));
3631 assert_eq!(
3632 tree.active_overlays().len(),
3633 1,
3634 "tooltip appears after a full delay of stillness"
3635 );
3636 }
3637
3638 #[test]
3639 fn timed_overlay_auto_dismisses_after_duration() {
3640 let mut tree = WidgetTree::new();
3641 let anchor = tree.add(FillWidget::new());
3642 let content = tree.add(FillWidget::new().label("Toast"));
3643 tree.layout(SizeProposal::exact(200.0, 100.0));
3644
3645 tree.show_overlay_for(
3646 crate::overlay::OverlayRequest::new(
3647 content,
3648 anchor,
3649 crate::overlay::OverlayPlacement::Below,
3650 crate::overlay::DismissBehavior::Manual,
3651 ),
3652 std::time::Duration::from_millis(300),
3653 );
3654
3655 assert_eq!(tree.active_overlays().len(), 1);
3656
3657 tree.advance_time(std::time::Duration::from_millis(200));
3658 assert_eq!(tree.active_overlays().len(), 1);
3659
3660 tree.advance_time(std::time::Duration::from_millis(150));
3661 assert!(tree.active_overlays().is_empty());
3662 assert!(!tree.is_visible(content));
3663 }
3664
3665 #[test]
3666 fn fade_dismiss_keeps_overlay_off_active_list_immediately() {
3667 let mut tree = WidgetTree::new();
3675 let anchor = tree.add(FillWidget::new());
3676 let content = tree.add(FillWidget::new().label("Faded"));
3677 tree.layout(SizeProposal::exact(200.0, 100.0));
3678
3679 let id = tree.show_overlay(
3680 crate::overlay::OverlayRequest::new(
3681 content,
3682 anchor,
3683 crate::overlay::OverlayPlacement::Below,
3684 crate::overlay::DismissBehavior::Manual,
3685 )
3686 .with_fade(std::time::Duration::from_millis(100)),
3687 );
3688 assert_eq!(tree.active_overlays().len(), 1);
3689
3690 tree.dismiss_overlay(id);
3691 assert!(tree.active_overlays().is_empty());
3696 }
3697
3698 #[test]
3699 fn fade_dismiss_defers_content_dormancy_until_sim_tween_completes() {
3700 let mut tree = WidgetTree::new();
3706 let anchor = tree.add(FillWidget::new());
3707 let content = tree.add(FillWidget::new().label("Faded"));
3708 tree.layout(SizeProposal::exact(200.0, 100.0));
3709
3710 let id = tree.show_overlay(
3711 crate::overlay::OverlayRequest::new(
3712 content,
3713 anchor,
3714 crate::overlay::OverlayPlacement::Below,
3715 crate::overlay::DismissBehavior::Manual,
3716 )
3717 .with_fade(std::time::Duration::from_millis(100)),
3718 );
3719 tree.advance_time(std::time::Duration::from_secs(1));
3725
3726 tree.dismiss_overlay(id);
3727 assert!(
3728 tree.is_visible(content),
3729 "content stays active during fade-out"
3730 );
3731
3732 tree.advance_time(std::time::Duration::from_millis(60));
3736 assert!(
3737 tree.is_visible(content),
3738 "60 ms into a 100 ms tween the content is still up"
3739 );
3740
3741 tree.advance_time(std::time::Duration::from_millis(90));
3742 assert!(
3743 !tree.is_visible(content),
3744 "after sim-time past the tween window, deferred removal fires"
3745 );
3746 }
3747
3748 #[test]
3760 fn an_auto_dismissed_fade_starts_at_the_instant_the_dismiss_ran() {
3761 let mut tree = WidgetTree::new();
3762 let anchor = tree.add(FillWidget::new());
3763 let content = tree.add(FillWidget::new().label("Toast"));
3764 tree.layout(SizeProposal::exact(200.0, 100.0));
3765
3766 tree.show_overlay_for(
3767 crate::overlay::OverlayRequest::new(
3768 content,
3769 anchor,
3770 crate::overlay::OverlayPlacement::Below,
3771 crate::overlay::DismissBehavior::Manual,
3772 )
3773 .with_fade(std::time::Duration::from_millis(100)),
3774 std::time::Duration::from_millis(500),
3775 );
3776 assert_eq!(tree.active_overlays().len(), 1);
3777
3778 tree.advance_time(std::time::Duration::from_millis(600));
3782 assert!(
3783 tree.active_overlays().is_empty(),
3784 "precondition: the auto-dismiss fired inside this advance"
3785 );
3786 assert!(
3787 tree.is_visible(content),
3788 "the fade must start at the instant the dismiss ran, so the \
3789 content survives the frame that dismissed it"
3790 );
3791
3792 tree.advance_time(std::time::Duration::from_millis(150));
3793 assert!(
3794 !tree.is_visible(content),
3795 "and is reaped once the tween's own window has passed"
3796 );
3797 }
3798
3799 #[derive(Debug)]
3805 struct Block(f32, f32);
3806
3807 impl Widget for Block {
3808 fn layout_response(
3809 &self,
3810 _proposal: SizeProposal,
3811 _ctx: &LayoutContext,
3812 ) -> crate::widget::LayoutResponse {
3813 teksilo_canvas::Size::new(self.0, self.1).into()
3814 }
3815 }
3816
3817 fn panel_and_an_overlay_above(
3821 panel_dismiss: crate::overlay::DismissBehavior,
3822 top: impl Widget + 'static,
3823 top_dismiss: crate::overlay::DismissBehavior,
3824 top_at: Point,
3825 inert: bool,
3826 ) -> (WidgetTree, WidgetId, WidgetId) {
3827 let mut tree = WidgetTree::new();
3828 let panel = tree.add(Block(100.0, 60.0));
3829 let top = tree.add(top);
3830 let beneath = tree.add(FillWidget::new());
3834 tree.show_overlay(crate::overlay::OverlayRequest::new(
3835 panel,
3836 beneath,
3837 crate::overlay::OverlayPlacement::AtPointer(Point::new(20.0, 20.0)),
3838 panel_dismiss,
3839 ));
3840 let request = crate::overlay::OverlayRequest::new(
3841 top,
3842 beneath,
3843 crate::overlay::OverlayPlacement::AtPointer(top_at),
3844 top_dismiss,
3845 );
3846 tree.show_overlay(if inert { request.inert() } else { request });
3847 tree.layout(SizeProposal::exact(400.0, 200.0));
3848 (tree, panel, top)
3849 }
3850
3851 fn panel_and_an_inert_overlay(
3854 panel_dismiss: crate::overlay::DismissBehavior,
3855 inert_dismiss: crate::overlay::DismissBehavior,
3856 inert_at: Point,
3857 ) -> (WidgetTree, WidgetId, WidgetId) {
3858 panel_and_an_overlay_above(
3859 panel_dismiss,
3860 Block(60.0, 40.0),
3861 inert_dismiss,
3862 inert_at,
3863 true,
3864 )
3865 }
3866
3867 #[test]
3868 fn an_inert_overlay_hands_the_point_to_the_overlay_beneath_it() {
3869 let (tree, panel, inert) = panel_and_an_inert_overlay(
3870 crate::overlay::DismissBehavior::Manual,
3871 crate::overlay::DismissBehavior::EscapeKey,
3872 Point::new(30.0, 30.0),
3873 );
3874 let at = Point::new(40.0, 40.0);
3875 let over = tree.overlay_manager.find_by_content(inert).unwrap();
3876 assert!(
3877 tree.overlay_content_bounds(over).unwrap().contains(at),
3878 "fixture: the inert overlay covers the point"
3879 );
3880 assert_eq!(
3881 tree.hit_test(at),
3882 Some(panel),
3883 "the panel under it takes the point, not the window under both"
3884 );
3885 }
3886
3887 #[test]
3888 fn a_press_on_an_inert_overlay_is_outside_the_overlays_it_is_outside_of() {
3889 let (mut tree, panel, inert) = panel_and_an_inert_overlay(
3890 crate::overlay::DismissBehavior::ClickOutside,
3891 crate::overlay::DismissBehavior::EscapeKey,
3892 Point::new(200.0, 100.0),
3893 );
3894 let at = Point::new(220.0, 110.0);
3895 let over = tree.overlay_manager.find_by_content(inert).unwrap();
3896 assert!(
3897 tree.overlay_content_bounds(over).unwrap().contains(at),
3898 "fixture: the press lands on the inert overlay"
3899 );
3900 tree.pointer_down_button(at, crate::event::PointerButton::Primary);
3901 assert_eq!(
3902 tree.overlay_manager.find_by_content(panel),
3903 None,
3904 "the press was outside the panel, whatever floats over that spot",
3905 );
3906 }
3907
3908 #[test]
3909 fn an_inert_overlay_is_not_an_interactive_rect() {
3910 let (tree, panel, _inert) = panel_and_an_inert_overlay(
3913 crate::overlay::DismissBehavior::Manual,
3914 crate::overlay::DismissBehavior::EscapeKey,
3915 Point::new(200.0, 100.0),
3916 );
3917 let panel_bounds = tree
3918 .overlay_content_bounds(tree.overlay_manager.find_by_content(panel).unwrap())
3919 .unwrap();
3920 assert_eq!(tree.overlay_manager.interactive_rects(), vec![panel_bounds]);
3921 }
3922
3923 #[test]
3924 fn escape_steps_over_an_inert_overlay_to_the_one_beneath() {
3925 let (mut tree, panel, inert) = panel_and_an_inert_overlay(
3928 crate::overlay::DismissBehavior::EscapeKey,
3929 crate::overlay::DismissBehavior::Manual,
3930 Point::new(200.0, 100.0),
3931 );
3932 tree.press_key(Key::Escape, Modifiers::NONE);
3933 assert_eq!(
3934 tree.overlay_manager.find_by_content(panel),
3935 None,
3936 "the panel beneath took the Escape"
3937 );
3938 assert!(
3939 tree.overlay_manager.find_by_content(inert).is_some(),
3940 "and the inert one, which Escape may not close, is still up"
3941 );
3942 }
3943
3944 #[test]
3945 fn an_inert_overlay_over_the_window_hands_the_point_to_the_window() {
3946 let mut tree = WidgetTree::new();
3950 let page = tree.add(FillWidget::new());
3951 let inert = tree.add(Block(60.0, 40.0));
3952 tree.show_overlay(
3953 crate::overlay::OverlayRequest::new(
3954 inert,
3955 page,
3956 crate::overlay::OverlayPlacement::AtPointer(Point::new(200.0, 100.0)),
3957 crate::overlay::DismissBehavior::EscapeKey,
3958 )
3959 .inert(),
3960 );
3961 tree.layout(SizeProposal::exact(400.0, 200.0));
3962 let at = Point::new(220.0, 110.0);
3963 assert!(
3964 tree.overlay_content_bounds(tree.overlay_manager.find_by_content(inert).unwrap())
3965 .unwrap()
3966 .contains(at),
3967 "fixture: the inert overlay covers the point"
3968 );
3969 assert_eq!(tree.hit_test(at), Some(page));
3970 tree.pointer_move(at);
3971 assert_eq!(tree.hovered(), Some(page), "and so does a hover");
3972 }
3973
3974 #[test]
3975 fn a_root_that_is_hit_transparent_and_pass_through_is_not_inert() {
3976 use crate::widget_builder::WidgetBuilder;
3977
3978 let flagged = || {
3982 Block(60.0, 40.0)
3983 .hit_transparent(true)
3984 .event_pass_through(true)
3985 };
3986 let at = Point::new(220.0, 110.0);
3987
3988 let (mut tree, panel, top) = panel_and_an_overlay_above(
3989 crate::overlay::DismissBehavior::ClickOutside,
3990 flagged(),
3991 crate::overlay::DismissBehavior::Manual,
3992 Point::new(200.0, 100.0),
3993 false,
3994 );
3995 let over = tree.overlay_manager.find_by_content(top).unwrap();
3996 assert!(
3997 tree.overlay_content_bounds(over).unwrap().contains(at),
3998 "fixture: the press lands on the flagged overlay"
3999 );
4000 assert_eq!(
4001 tree.overlay_manager.hit_test(at),
4002 Some(over),
4003 "the stack still answers with the overlay"
4004 );
4005 assert_eq!(
4006 tree.overlay_manager.interactive_rects().len(),
4007 2,
4008 "which is an interactive rect"
4009 );
4010 tree.pointer_down_button(at, crate::event::PointerButton::Primary);
4011 tree.pointer_up_button(at, crate::event::PointerButton::Primary);
4012 assert!(
4013 tree.overlay_manager.find_by_content(panel).is_some(),
4014 "a press on it is inside the stack, so the panel beneath stays"
4015 );
4016
4017 let (mut tree, panel, top) = panel_and_an_overlay_above(
4018 crate::overlay::DismissBehavior::EscapeKey,
4019 flagged(),
4020 crate::overlay::DismissBehavior::Manual,
4021 Point::new(200.0, 100.0),
4022 false,
4023 );
4024 tree.press_key(Key::Escape, Modifiers::NONE);
4025 assert!(
4026 tree.overlay_manager.find_by_content(panel).is_some()
4027 && tree.overlay_manager.find_by_content(top).is_some(),
4028 "a `Manual` overlay owns the Escape: nothing beneath it closes"
4029 );
4030
4031 let (mut tree, panel, top) = panel_and_an_overlay_above(
4032 crate::overlay::DismissBehavior::EscapeKey,
4033 flagged(),
4034 crate::overlay::DismissBehavior::EscapeKey,
4035 Point::new(200.0, 100.0),
4036 false,
4037 );
4038 tree.press_key(Key::Escape, Modifiers::NONE);
4039 assert_eq!(
4040 tree.overlay_manager.find_by_content(top),
4041 None,
4042 "an `EscapeKey` one is what the Escape closes"
4043 );
4044 assert!(
4045 tree.overlay_manager.find_by_content(panel).is_some(),
4046 "and it is spent there"
4047 );
4048 }
4049
4050 #[test]
4051 fn an_inert_overlay_records_no_focus_to_give_back_on_any_show_path() {
4052 type Show = fn(&mut WidgetTree, crate::overlay::OverlayRequest, WidgetId);
4053 fn from_a_handler(
4054 tree: &mut WidgetTree,
4055 show: impl FnOnce(&mut crate::widget::EventContext),
4056 ) {
4057 tree.run_with_event_context(&mut crate::window::NoopWindowOps, show);
4058 }
4059 let paths: [(&str, Show); 5] = [
4060 ("show_overlay", |tree, request, _| {
4061 from_a_handler(tree, |ctx| ctx.show_overlay(request))
4062 }),
4063 ("show_overlay_for", |tree, request, _| {
4064 from_a_handler(tree, |ctx| {
4065 ctx.show_overlay_for(request, std::time::Duration::from_secs(60))
4066 })
4067 }),
4068 ("show_overlay_with_reveal", |tree, request, _| {
4069 from_a_handler(tree, |ctx| {
4070 ctx.show_overlay_with_reveal(
4071 request,
4072 crate::signal::Signal::new(0.0),
4073 std::time::Duration::from_millis(100),
4074 )
4075 })
4076 }),
4077 ("show_overlay_after", |tree, request, _| {
4078 from_a_handler(tree, |ctx| {
4079 ctx.show_overlay_after(request, std::time::Duration::ZERO)
4080 });
4081 tree.layout(SizeProposal::exact(400.0, 200.0));
4082 }),
4083 ("show_overlay_from_source", |tree, request, field| {
4084 tree.show_overlay_from_source(field, request);
4085 }),
4086 ];
4087 for (path, show) in paths {
4088 let mut tree = WidgetTree::new();
4089 let field = tree.add(FillWidget::new().focusable());
4090 let inert = tree.add(Block(60.0, 40.0));
4091 let ordinary = tree.add(Block(60.0, 40.0));
4092 tree.layout(SizeProposal::exact(400.0, 200.0));
4093 tree.focus(field);
4094 for (content, inert) in [(inert, true), (ordinary, false)] {
4095 let request = crate::overlay::OverlayRequest::new(
4096 content,
4097 field,
4098 crate::overlay::OverlayPlacement::AtPointer(Point::new(20.0, 20.0)),
4099 crate::overlay::DismissBehavior::EscapeKey,
4100 );
4101 show(
4102 &mut tree,
4103 if inert { request.inert() } else { request },
4104 field,
4105 );
4106 }
4107 let restore = |content: WidgetId| {
4108 tree.overlay_manager
4109 .stack
4110 .iter()
4111 .find(|o| o.content_id == content)
4112 .map(|o| o.focus_restore)
4113 };
4114 assert_eq!(
4115 restore(ordinary),
4116 Some(Some(field)),
4117 "{path}: an ordinary one does"
4118 );
4119 assert_eq!(
4120 restore(inert),
4121 Some(None),
4122 "{path}: an inert one took no focus"
4123 );
4124 }
4125 }
4126}