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
879 | P::BottomCenter
880 | P::ViewportCorner { .. }
881 | P::FullViewport
882 | P::AboveSelection { .. } => false,
883 }
884 }
885
886 fn overlay_follows_focus_out(&self, overlay_id: crate::overlay::OverlayId) -> bool {
926 let Some(overlay) = self.overlay_manager.overlay(overlay_id) else {
927 return false;
928 };
929 if !Self::overlay_is_anchor_positioned(&overlay.placement) {
930 return false;
931 }
932 if overlay.is_dismissing() {
933 return false;
934 }
935 if overlay.inert {
936 return false;
937 }
938 !self
939 .tooltips
940 .iter()
941 .any(|entry| entry.content_id == overlay.content_id)
942 }
943
944 fn overlay_orbit_for_widget(&self, widget_id: WidgetId) -> Option<crate::overlay::OverlayId> {
967 self.overlay_manager
968 .stack
969 .iter()
970 .rev()
971 .find(|overlay| {
972 self.is_descendant_of(widget_id, overlay.content_id)
973 || (self.overlay_follows_focus_out(overlay.id)
974 && self.is_descendant_of(widget_id, overlay.anchor))
975 })
976 .map(|overlay| overlay.id)
977 }
978
979 pub(super) fn dismiss_overlays_left_by_focus(
1006 &mut self,
1007 old: Option<WidgetId>,
1008 new_focus: WidgetId,
1009 ops: &mut dyn crate::window::WindowOps,
1010 ) {
1011 let Some(old) = old else {
1012 return;
1013 };
1014 let Some(old_overlay) = self.overlay_orbit_for_widget(old) else {
1015 return;
1016 };
1017 let new_overlay = self.overlay_ancestor_for_widget(new_focus);
1023
1024 if let Some(new_overlay) = new_overlay
1027 && (new_overlay == old_overlay
1028 || self
1029 .overlay_manager
1030 .is_descendant_of(new_overlay, old_overlay))
1031 {
1032 return;
1033 }
1034
1035 if !self.overlay_follows_focus_out(old_overlay) {
1036 return;
1037 }
1038
1039 let mut topmost = old_overlay;
1040 let mut current = self
1041 .overlay_manager
1042 .overlay(old_overlay)
1043 .and_then(|overlay| overlay.parent_overlay);
1044 while let Some(overlay_id) = current {
1045 if Some(overlay_id) == new_overlay {
1049 break;
1050 }
1051 if self.overlay_is_host_surface(overlay_id) {
1054 break;
1055 }
1056 if !self.overlay_follows_focus_out(overlay_id) {
1057 break;
1058 }
1059 topmost = overlay_id;
1060 current = self
1061 .overlay_manager
1062 .overlay(overlay_id)
1063 .and_then(|overlay| overlay.parent_overlay);
1064 }
1065
1066 let dismissed = self.overlay_manager.dismiss(topmost);
1067 self.dormant_dismissed_content(&dismissed, &mut *ops);
1068 }
1069
1070 pub(super) fn dismiss_all_overlays_except_hosts(
1081 &mut self,
1082 ops: &mut dyn crate::window::WindowOps,
1083 ) {
1084 let to_dismiss: Vec<crate::overlay::OverlayId> = self
1085 .overlay_manager
1086 .stack
1087 .iter()
1088 .map(|overlay| overlay.id)
1089 .filter(|&id| !self.overlay_is_host_surface(id))
1090 .collect();
1091
1092 for overlay_id in to_dismiss {
1093 let dismissed = self.overlay_manager.dismiss(overlay_id);
1094 self.dormant_dismissed_content(&dismissed, &mut *ops);
1095 }
1096 }
1097
1098 pub(super) fn overlay_is_host_surface(&self, overlay_id: crate::overlay::OverlayId) -> bool {
1104 let Some(overlay) = self.overlay_manager.overlay(overlay_id) else {
1105 return false;
1106 };
1107 if matches!(
1116 overlay.placement,
1117 crate::overlay::OverlayPlacement::Centered
1118 ) {
1119 return true;
1120 }
1121 let Some(node) = self.arena.get(overlay.content_id) else {
1122 return false;
1123 };
1124 let role = node
1132 .access_overrides
1133 .as_ref()
1134 .and_then(|o| o.role)
1135 .unwrap_or_else(|| {
1136 let mut builder = AccessNodeBuilder::new();
1137 node.widget.accessibility(&mut builder);
1138 builder.role()
1139 });
1140 matches!(
1141 role,
1142 accesskit::Role::Tooltip
1143 | accesskit::Role::Dialog
1144 | accesskit::Role::AlertDialog
1145 | accesskit::Role::MenuBar
1146 )
1147 }
1148
1149 pub(super) fn dismiss_modal_for_source(
1150 &mut self,
1151 source_widget: WidgetId,
1152 ops: &mut dyn crate::window::WindowOps,
1153 ) -> bool {
1154 let Some(modal_overlay) = self.modal_overlay_for_widget(source_widget) else {
1155 return false;
1156 };
1157
1158 let (dismissed, focus_restore) = self
1159 .overlay_manager
1160 .dismiss_with_focus_restore(modal_overlay);
1161 self.dormant_dismissed_content(&dismissed, &mut *ops);
1162 if let Some(restore_id) = focus_restore
1163 && self.arena.is_active(restore_id)
1164 {
1165 self.focus_ops(restore_id, &mut *ops);
1166 }
1167 true
1168 }
1169
1170 pub fn is_descendant_of(&self, widget_id: WidgetId, ancestor: WidgetId) -> bool {
1171 if widget_id == ancestor {
1172 return true;
1173 }
1174 let mut current = self.arena.parent(widget_id);
1175 while let Some(parent_id) = current {
1176 if parent_id == ancestor {
1177 return true;
1178 }
1179 current = self.arena.parent(parent_id);
1180 }
1181 false
1182 }
1183
1184 fn is_overlay_content_root(&self, widget_id: WidgetId) -> bool {
1191 self.overlay_manager.find_by_content(widget_id).is_some()
1192 }
1193
1194 pub(super) fn tooltip_focus_enter(&mut self, widget_id: WidgetId) {
1225 type ToShow = (WidgetId, WidgetId, crate::overlay::TooltipPlacement);
1248 let mut direct: Vec<ToShow> = Vec::new();
1249 let mut reverse: Vec<ToShow> = Vec::new();
1250 for e in &self.tooltips {
1251 if e.sticky_after.is_none() || e.overlay_id.is_some() || e.suppressed_until_focus_leaves
1252 {
1253 continue;
1254 }
1255 if self.is_descendant_of(widget_id, e.anchor_id) {
1256 direct.push((e.anchor_id, e.content_id, e.placement));
1257 } else if self.is_descendant_of(e.anchor_id, widget_id) {
1258 reverse.push((e.anchor_id, e.content_id, e.placement));
1259 }
1260 }
1261 let mut to_show = direct;
1262 if reverse.len() == 1 && !self.is_overlay_content_root(widget_id) {
1266 to_show.extend(reverse);
1267 }
1268
1269 let sim_now = self.sim_clock;
1278 let real_now = std::time::Instant::now();
1279 for (_anchor_id, content_id, _placement) in to_show {
1280 if let Some(entry) = self
1281 .tooltips
1282 .iter_mut()
1283 .find(|e| e.content_id == content_id)
1284 {
1285 entry.hover_start = Some(sim_now);
1286 entry.real_hover_start = Some(real_now);
1287 entry.hover_origin = None;
1288 entry.armed_by_focus = true;
1289 entry.armed_by_hold = false;
1290 }
1291 }
1292 }
1293
1294 pub(super) fn show_highlight_tooltip(
1304 &mut self,
1305 item_id: WidgetId,
1306 ops: &mut dyn crate::window::WindowOps,
1307 ) {
1308 self.clear_highlight_tooltip(&mut *ops);
1311
1312 let found = self.tooltips.iter().find_map(|e| {
1316 if e.overlay_id.is_none() && self.is_descendant_of(e.anchor_id, item_id) {
1317 Some((e.anchor_id, e.content_id, e.placement))
1318 } else {
1319 None
1320 }
1321 });
1322 let Some((anchor_id, content_id, placement)) = found else {
1323 return;
1324 };
1325
1326 let parent_overlay = self.overlay_ancestor_for_widget(item_id);
1327 self.arena.activate(content_id);
1328 let fade_duration = if self.prefers_reduced_motion {
1329 None
1330 } else {
1331 Some(self.theme.motion.duration_fast)
1332 };
1333 let mut request = crate::overlay::OverlayRequest::new(
1334 content_id,
1335 anchor_id,
1336 Self::tooltip_overlay_placement(placement),
1337 crate::overlay::DismissBehavior::Manual,
1338 );
1339 request.parent_overlay = parent_overlay;
1340 request.fade_duration = fade_duration;
1341 let oid = self.show_overlay(request);
1342 let real_now = std::time::Instant::now();
1343 let sim_now = self.sim_clock;
1344 if let Some(entry) = self
1345 .tooltips
1346 .iter_mut()
1347 .find(|e| e.content_id == content_id)
1348 {
1349 entry.overlay_id = Some(oid);
1350 entry.shown_at_sim = Some(sim_now);
1351 entry.shown_at_real = Some(real_now);
1352 if let Some(sink) = entry.shown_at_sink.as_ref() {
1353 sink.set(Some(real_now));
1354 }
1355 }
1356 self.highlight_tooltip = Some((oid, content_id));
1357 }
1358
1359 pub(super) fn clear_highlight_tooltip(&mut self, ops: &mut dyn crate::window::WindowOps) {
1364 let Some((oid, content_id)) = self.highlight_tooltip.take() else {
1365 return;
1366 };
1367 if self.overlay_manager.stack.iter().any(|o| o.id == oid) {
1368 let dismissed = self.overlay_manager.dismiss(oid);
1369 self.dormant_dismissed_content(&dismissed, &mut *ops);
1370 }
1371 if let Some(entry) = self
1372 .tooltips
1373 .iter_mut()
1374 .find(|e| e.content_id == content_id)
1375 {
1376 entry.overlay_id = None;
1377 entry.shown_at_sim = None;
1378 entry.shown_at_real = None;
1379 if let Some(sink) = entry.shown_at_sink.as_ref() {
1380 sink.set(None);
1381 }
1382 }
1383 }
1384
1385 pub(super) fn tooltip_focus_leave_outside(
1396 &mut self,
1397 new_focus: Option<WidgetId>,
1398 ops: &mut dyn crate::window::WindowOps,
1399 ) {
1400 let to_dismiss: Vec<crate::overlay::OverlayId> = self
1401 .tooltips
1402 .iter()
1403 .filter(|e| e.promoted_by_focus && e.overlay_id.is_some())
1404 .filter(|e| {
1405 let in_scope = new_focus
1406 .map(|nf| {
1407 self.is_descendant_of(nf, e.anchor_id)
1412 || self.is_descendant_of(e.anchor_id, nf)
1413 || self.is_descendant_of(nf, e.content_id)
1414 })
1415 .unwrap_or(false);
1416 !in_scope
1417 })
1418 .filter_map(|e| e.overlay_id)
1419 .collect();
1420
1421 for oid in to_dismiss {
1422 let dismissed = self.overlay_manager.dismiss(oid);
1423 self.dormant_dismissed_content(&dismissed, &mut *ops);
1424 }
1425
1426 for e in &mut self.tooltips {
1429 if e.armed_by_focus && e.overlay_id.is_none() {
1430 e.hover_start = None;
1431 e.real_hover_start = None;
1432 e.armed_by_focus = false;
1433 }
1434 }
1435
1436 let served: Vec<usize> = self
1440 .tooltips
1441 .iter()
1442 .enumerate()
1443 .filter(|(_, e)| e.suppressed_until_focus_leaves)
1444 .filter(|(_, e)| {
1445 let still_inside = new_focus
1446 .map(|nf| {
1447 self.is_descendant_of(nf, e.anchor_id)
1448 || self.is_descendant_of(nf, e.content_id)
1449 })
1450 .unwrap_or(false);
1451 !still_inside
1452 })
1453 .map(|(i, _)| i)
1454 .collect();
1455 for i in served {
1456 self.tooltips[i].suppressed_until_focus_leaves = false;
1457 }
1458 }
1459
1460 fn tooltip_hover_targets_anchor(&self, widget_id: WidgetId, anchor_id: WidgetId) -> bool {
1480 if !self.is_descendant_of(widget_id, anchor_id) {
1481 return false;
1482 }
1483 if let Some(overlay_id) = self.overlay_ancestor_for_widget(widget_id)
1484 && let Some(content_id) = self
1485 .overlay_manager
1486 .overlay(overlay_id)
1487 .map(|o| o.content_id)
1488 && !self.is_descendant_of(anchor_id, content_id)
1489 {
1490 return false;
1491 }
1492 true
1493 }
1494
1495 pub(super) fn tooltip_index_for(&self, widget_id: WidgetId) -> Option<usize> {
1513 self.tooltips
1514 .iter()
1515 .enumerate()
1516 .filter(|(_, entry)| self.tooltip_hover_targets_anchor(widget_id, entry.anchor_id))
1517 .filter_map(|(index, entry)| {
1518 self.ancestor_distance(widget_id, entry.anchor_id)
1519 .map(|depth| (depth, index))
1520 })
1521 .min()
1522 .map(|(_, index)| index)
1523 }
1524
1525 pub(super) fn tooltip_pointer_enter(&mut self, widget_id: WidgetId) {
1526 let Some(index) = self.tooltip_index_for(widget_id) else {
1527 return;
1528 };
1529 if self.tooltips[index].overlay_id.is_some() {
1531 return;
1532 }
1533 self.tooltips[index].hover_start = Some(self.sim_clock);
1534 self.tooltips[index].real_hover_start = Some(std::time::Instant::now());
1535 self.tooltips[index].hover_origin = self.hover_owner_position();
1536 self.tooltips[index].armed_by_focus = false;
1537 self.tooltips[index].armed_by_hold = false;
1538 self.arena.mark_needs_paint(self.tooltips[index].anchor_id);
1539 }
1540
1541 fn ancestor_distance(&self, widget_id: WidgetId, ancestor: WidgetId) -> Option<usize> {
1544 let mut current = widget_id;
1545 let mut depth = 0usize;
1546 loop {
1547 if current == ancestor {
1548 return Some(depth);
1549 }
1550 current = self.arena.parent(current)?;
1551 depth += 1;
1552 }
1553 }
1554
1555 pub(super) fn tooltip_pointer_press(&mut self, at: Option<teksilo_canvas::Point>) {
1570 let mut to_dismiss = Vec::new();
1571 let candidates: Vec<(usize, Option<crate::overlay::OverlayId>)> = self
1572 .tooltips
1573 .iter()
1574 .enumerate()
1575 .map(|(i, e)| (i, if e.is_sticky { None } else { e.overlay_id }))
1576 .collect();
1577 for (index, overlay_id) in candidates {
1578 let inside = match (overlay_id, at) {
1583 (Some(oid), Some(pos)) => self
1584 .overlay_content_bounds(oid)
1585 .is_some_and(|rect| rect.contains(pos)),
1586 _ => false,
1587 };
1588 if inside {
1589 continue;
1590 }
1591 let entry = &mut self.tooltips[index];
1592 entry.hover_start = None;
1593 entry.real_hover_start = None;
1594 entry.hover_origin = None;
1595 if let Some(oid) = overlay_id {
1596 to_dismiss.push(oid);
1597 }
1598 }
1599 for overlay_id in to_dismiss {
1600 self.dismiss_overlay(overlay_id);
1601 }
1602 }
1603
1604 pub(super) fn tooltip_escape_pressed(&mut self) {
1626 self.tooltip_pointer_press(None);
1629 }
1630
1631 pub(super) fn inert_overlays_escape_pressed(&mut self, ops: &mut dyn crate::window::WindowOps) {
1642 let dismissed = self.overlay_manager.dismiss_inert_on_escape();
1643 if !dismissed.is_empty() {
1644 self.dormant_dismissed_content(&dismissed, &mut *ops);
1645 self.a11y_dirty = true;
1646 }
1647 }
1648
1649 pub(super) fn tooltip_window_deactivated(&mut self) {
1656 self.tooltip_pointer_press(None);
1659 }
1660
1661 pub(crate) fn tooltip_cancel_pending_dwell(&mut self) {
1669 for entry in &mut self.tooltips {
1670 if entry.overlay_id.is_none() {
1671 entry.hover_start = None;
1672 entry.real_hover_start = None;
1673 entry.hover_origin = None;
1674 }
1675 }
1676 }
1677
1678 pub(super) fn tooltip_pointer_moved(
1682 &mut self,
1683 widget_id: WidgetId,
1684 position: teksilo_canvas::Point,
1685 ) {
1686 let matching: Vec<usize> = self
1687 .tooltips
1688 .iter()
1689 .enumerate()
1690 .filter(|(_, entry)| {
1691 entry.overlay_id.is_none()
1692 && entry.hover_start.is_some()
1693 && !entry.armed_by_focus
1694 && self.tooltip_hover_targets_anchor(widget_id, entry.anchor_id)
1695 })
1696 .map(|(index, _)| index)
1697 .collect();
1698 if matching.is_empty() {
1699 return;
1700 }
1701 let now = self.sim_clock;
1702 let real_now = std::time::Instant::now();
1703 let slop = super::TOOLTIP_STATIONARY_SLOP;
1704 let slop_sq = slop * slop;
1705 for index in matching {
1706 let origin = match self.tooltips[index].hover_origin {
1707 Some(o) => o,
1708 None => {
1709 self.tooltips[index].hover_origin = Some(position);
1713 continue;
1714 }
1715 };
1716 let dx = position.x - origin.x;
1717 let dy = position.y - origin.y;
1718 if dx * dx + dy * dy <= slop_sq {
1719 continue;
1720 }
1721 self.tooltips[index].hover_start = Some(now);
1723 self.tooltips[index].real_hover_start = Some(real_now);
1724 self.tooltips[index].hover_origin = Some(position);
1725 }
1726 }
1727
1728 pub fn promote_tooltip_to_sticky(&mut self, content_id: WidgetId) {
1746 let Some(entry) = self
1747 .tooltips
1748 .iter_mut()
1749 .find(|entry| entry.content_id == content_id)
1750 else {
1751 return;
1752 };
1753 if entry.is_sticky {
1754 return;
1755 }
1756 entry.is_sticky = true;
1757 let overlay_id = entry.overlay_id;
1758 if let Some(overlay_id) = overlay_id {
1759 self.overlay_manager.set_dismiss(
1760 overlay_id,
1761 crate::overlay::DismissBehavior::EscapeOrClickOutside,
1762 );
1763 }
1764 }
1765
1766 pub(super) fn tooltip_pointer_leave(
1767 &mut self,
1768 widget_id: WidgetId,
1769 _ops: &mut dyn crate::window::WindowOps,
1770 ) {
1771 let matching: Vec<usize> = self
1772 .tooltips
1773 .iter()
1774 .enumerate()
1775 .filter(|(_, entry)| self.is_descendant_of(widget_id, entry.anchor_id))
1776 .map(|(index, _)| index)
1777 .collect();
1778 for index in matching {
1779 self.tooltips[index].hover_start = None;
1782 self.tooltips[index].real_hover_start = None;
1783 self.tooltips[index].hover_origin = None;
1784 }
1796 }
1797
1798 pub fn next_timer_deadline(&self) -> Option<std::time::Instant> {
1804 let now = std::time::Instant::now();
1805 let session_active = self.tooltip_session_active_real(now);
1806 let tooltip_deadline = self
1807 .tooltips
1808 .iter()
1809 .filter(|entry| entry.overlay_id.is_none())
1810 .filter_map(|entry| {
1811 let start = entry.real_hover_start?;
1812 let delay = self.effective_tooltip_delay(entry.delay, session_active);
1813 Some(start + delay)
1814 })
1815 .min();
1816
1817 let ref_time = self.last_frame_time.unwrap_or_else(std::time::Instant::now);
1838 let dwell_tooltip_deadline = self
1839 .tooltips
1840 .iter()
1841 .filter_map(|entry| {
1842 let sticky_after = entry.sticky_after?;
1843 let shown_at = entry.shown_at_real?;
1844 if entry.overlay_id.is_none() || entry.is_sticky {
1845 return None;
1846 }
1847 let dwell_step = sticky_after / super::TOOLTIP_DWELL_STEPS;
1848 if dwell_step.is_zero() {
1849 return None;
1850 }
1851 let elapsed = ref_time.saturating_duration_since(shown_at);
1852 if elapsed >= sticky_after {
1853 return None;
1854 }
1855 let steps_passed = (elapsed.as_millis() / dwell_step.as_millis()) as u32;
1858 let next_step_at = shown_at + dwell_step * (steps_passed + 1);
1859 Some(next_step_at.min(shown_at + sticky_after))
1860 })
1861 .min();
1862 let delayed_overlay_deadline = self
1863 .pending_delayed_overlays
1864 .iter()
1865 .map(|pending| pending.real_requested_at + pending.delay)
1866 .min();
1867 let auto_dismiss_deadline = self.overlay_manager.next_auto_dismiss_deadline();
1868 let pointer_leave_deadline = self.overlay_manager.next_pointer_leave_deadline();
1875 let fade_dismiss_deadline = self.overlay_manager.next_fade_dismiss_deadline();
1882 let animation_deadline = self
1883 .animation_scheduler
1884 .next_deadline(&self.arena, self.paint_epoch);
1885 let animated_quad_deadline = self
1890 .animated_quads
1891 .next_deadline(&self.arena, self.paint_epoch);
1892 let gesture_deadline = self.next_input_deadline();
1898 let wake_at_deadline = self.pending_wake_at.get();
1899 let frame_tick_deadline = self.frame_tick_deadline();
1905
1906 [
1907 tooltip_deadline,
1908 dwell_tooltip_deadline,
1909 delayed_overlay_deadline,
1910 auto_dismiss_deadline,
1911 pointer_leave_deadline,
1912 fade_dismiss_deadline,
1913 animation_deadline,
1914 animated_quad_deadline,
1915 gesture_deadline,
1916 wake_at_deadline,
1917 frame_tick_deadline,
1918 ]
1919 .into_iter()
1920 .flatten()
1921 .min()
1922 }
1923
1924 pub fn overlay_manager(&self) -> &crate::overlay::OverlayManager {
1925 &self.overlay_manager
1926 }
1927
1928 pub fn overlay_manager_mut(&mut self) -> &mut crate::overlay::OverlayManager {
1933 &mut self.overlay_manager
1934 }
1935
1936 pub fn active_overlays(&self) -> Vec<crate::overlay::OverlayId> {
1937 self.overlay_manager.active_ids()
1938 }
1939
1940 pub fn overlay_content_bounds(
1949 &self,
1950 id: crate::overlay::OverlayId,
1951 ) -> Option<teksilo_canvas::Rect> {
1952 self.overlay_manager
1953 .stack
1954 .iter()
1955 .find(|o| o.id == id)
1956 .map(|o| o.bounds)
1957 }
1958
1959 pub fn show_overlay(
1960 &mut self,
1961 request: crate::overlay::OverlayRequest,
1962 ) -> crate::overlay::OverlayId {
1963 let fade_duration = request.fade_duration;
1964 let content_id = request.content_id;
1965 let is_modal = matches!(
1966 request.placement,
1967 crate::overlay::OverlayPlacement::Centered
1968 );
1969 let id = self.overlay_manager.show(request);
1970 self.cancel_pointers_for_modal(is_modal);
1971 self.a11y_dirty = true;
1979 if let Some(duration) = fade_duration {
1980 self.attach_overlay_fade(id, content_id, duration);
1981 }
1982 id
1983 }
1984
1985 pub fn show_overlay_in_band(
1995 &mut self,
1996 request: crate::overlay::OverlayRequest,
1997 band: crate::overlay::OverlayBand,
1998 ) -> crate::overlay::OverlayId {
1999 let fade_duration = request.fade_duration;
2000 let content_id = request.content_id;
2001 let is_modal = matches!(
2002 request.placement,
2003 crate::overlay::OverlayPlacement::Centered
2004 );
2005 let id = self.overlay_manager.show_in_band(request, band);
2006 self.cancel_pointers_for_modal(is_modal);
2007 self.a11y_dirty = true;
2008 if let Some(duration) = fade_duration {
2009 self.attach_overlay_fade(id, content_id, duration);
2010 }
2011 id
2012 }
2013
2014 pub fn show_overlay_from_source(
2017 &mut self,
2018 source_widget: WidgetId,
2019 mut request: crate::overlay::OverlayRequest,
2020 ) -> crate::overlay::OverlayId {
2021 if request.parent_overlay.is_none() {
2022 request.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
2023 }
2024 if let Some(existing) = self.overlay_manager.find_by_content(request.content_id) {
2025 return existing;
2026 }
2027
2028 let fade_duration = request.fade_duration;
2029 let content_id = request.content_id;
2030 let is_modal = matches!(
2031 request.placement,
2032 crate::overlay::OverlayPlacement::Centered
2033 );
2034 let current_focus = self.focused;
2035 let id = self.overlay_manager.show(request);
2036 self.cancel_pointers_for_modal(is_modal);
2037 self.a11y_dirty = true;
2038 if let Some(focus_id) = current_focus {
2039 self.overlay_manager.set_top_focus_restore(focus_id);
2040 }
2041 if let Some(duration) = fade_duration {
2042 self.attach_overlay_fade(id, content_id, duration);
2043 }
2044 id
2045 }
2046
2047 pub fn show_overlay_for(
2048 &mut self,
2049 request: crate::overlay::OverlayRequest,
2050 duration: std::time::Duration,
2051 ) -> crate::overlay::OverlayId {
2052 let fade_duration = request.fade_duration;
2053 let content_id = request.content_id;
2054 let is_modal = matches!(
2055 request.placement,
2056 crate::overlay::OverlayPlacement::Centered
2057 );
2058 let id = self.overlay_manager.show_for(request, duration);
2059 self.cancel_pointers_for_modal(is_modal);
2060 self.overlay_manager.set_shown_at_sim(id, self.sim_clock);
2061 self.a11y_dirty = true;
2062 if let Some(fade) = fade_duration {
2063 self.attach_overlay_fade(id, content_id, fade);
2064 }
2065 id
2066 }
2067
2068 fn attach_overlay_fade(
2076 &mut self,
2077 overlay_id: crate::overlay::OverlayId,
2078 content_id: WidgetId,
2079 duration: std::time::Duration,
2080 ) {
2081 let opacity = crate::signal::Signal::<f32>::new_animated(0.0);
2082 self.register_animated_signal(&opacity, content_id);
2083 self.set_opacity(content_id, opacity.clone());
2084 if self.prefers_reduced_motion() {
2088 opacity.set(1.0);
2089 self.overlay_manager
2090 .attach_fade(overlay_id, opacity, std::time::Duration::ZERO);
2091 return;
2092 }
2093 let _ = opacity.try_animate_with_options(crate::animation::AnimationRequest {
2094 target: 1.0,
2095 duration,
2096 easing: teksilo_tokens::Easing::EaseOut,
2097 frame_interval: None,
2098 looping: false,
2099 epsilon: 0.0,
2100 max_duration: None,
2101 });
2102 self.overlay_manager
2103 .attach_fade(overlay_id, opacity, duration);
2104 }
2105
2106 pub fn dismiss_overlay(&mut self, id: crate::overlay::OverlayId) {
2111 let mut noop = crate::window::NoopWindowOps;
2112 self.dismiss_overlay_with_ops(id, &mut noop);
2113 }
2114
2115 pub fn dismiss_overlay_with_ops(
2118 &mut self,
2119 id: crate::overlay::OverlayId,
2120 ops: &mut dyn crate::window::WindowOps,
2121 ) {
2122 let dismissed = self.overlay_manager.dismiss(id);
2123 self.dormant_dismissed_content(&dismissed, &mut *ops);
2124 }
2125
2126 fn cancel_pointers_for_modal(&mut self, is_modal: bool) {
2137 if !is_modal {
2138 return;
2139 }
2140 let mut noop = crate::window::NoopWindowOps;
2141 self.cancel_all_pointers(crate::pointer::CancelReason::ModalOpened, &mut noop);
2142 }
2143
2144 pub(super) fn run_pending_dismiss_callbacks(&mut self, ops: &mut dyn crate::window::WindowOps) {
2154 if self.draining_dismiss.replace(true) {
2159 return;
2160 }
2161 for _ in 0..8 {
2165 let pending = self.overlay_manager.take_pending_dismiss();
2166 if pending.is_empty() {
2167 break;
2168 }
2169 for (cb, reason) in pending {
2170 self.run_with_event_context(&mut *ops, |ctx| cb(reason, ctx));
2171 }
2172 }
2173 self.draining_dismiss.set(false);
2174 }
2175
2176 pub(super) fn dormant_dismissed_content(
2177 &mut self,
2178 content_ids: &[WidgetId],
2179 ops: &mut dyn crate::window::WindowOps,
2180 ) {
2181 self.run_pending_dismiss_callbacks(&mut *ops);
2185 let mut any_tooltip_dismissed = false;
2191 for &id in content_ids {
2192 if let Some(entry) = self.tooltips.iter_mut().find(|e| e.content_id == id) {
2193 entry.suppressed_until_focus_leaves = entry.promoted_by_focus;
2200 entry.overlay_id = None;
2201 entry.is_sticky = false;
2202 entry.hover_start = None;
2203 entry.real_hover_start = None;
2204 entry.hover_origin = None;
2205 entry.shown_at_sim = None;
2206 entry.shown_at_real = None;
2207 entry.promoted_by_focus = false;
2208 entry.armed_by_focus = false;
2209 entry.armed_by_hold = false;
2210 if let Some(sink) = entry.shown_at_sink.as_ref() {
2211 sink.set(None);
2212 }
2213 any_tooltip_dismissed = true;
2214 }
2215 }
2216 if any_tooltip_dismissed {
2217 let grace = super::TOOLTIP_SESSION_GRACE;
2218 self.tooltip_session_until_sim = Some(self.sim_clock + grace);
2219 self.tooltip_session_until_real = Some(std::time::Instant::now() + grace);
2220 }
2221 for &id in content_ids {
2222 let focused_in_subtree = self
2223 .focused
2224 .filter(|focused| self.is_descendant_of(*focused, id));
2225 let hovered_in_subtree = self
2226 .hovered_id()
2227 .filter(|hovered| self.is_descendant_of(*hovered, id));
2228
2229 if let Some(focused) = focused_in_subtree {
2230 self.dispatch_to_widget(focused, &WidgetEvent::FocusLost, &mut *ops);
2231 if self
2232 .focused
2233 .is_some_and(|current| self.is_descendant_of(current, id))
2234 {
2235 let old = self.focused;
2236 self.set_focused(None);
2237 self.focus_origin = None;
2238 self.update_focus_within_signals(old, None);
2239 self.update_view_focus_signals(old, None);
2240 }
2241 }
2242
2243 self.cancel_pointers_in_subtree(
2249 id,
2250 crate::pointer::CancelReason::OverlayDismissed,
2251 &mut *ops,
2252 );
2253 let _parked = self.arena.set_dormant(id);
2254
2255 if hovered_in_subtree.is_some() {
2256 let old = self.hovered_id();
2257 self.set_hovered(None);
2258 self.update_hover_within_signals(old, None);
2259 }
2260 }
2261 if !content_ids.is_empty() {
2262 self.cached_frame = None;
2263 self.a11y_dirty = true;
2264 }
2265 }
2266
2267 pub fn is_visible(&self, id: WidgetId) -> bool {
2268 self.arena.is_active(id)
2269 }
2270}
2271
2272#[cfg(test)]
2273mod tests {
2274 use super::*;
2275 use crate::test_widgets::FillWidget;
2276
2277 #[test]
2278 fn is_visible_reflects_dormancy() {
2279 let mut tree = WidgetTree::new();
2280 let widget = tree.add(FillWidget::new());
2281 tree.layout(SizeProposal::exact(100.0, 50.0));
2282
2283 assert!(tree.is_visible(widget));
2284 tree.set_dormant(widget);
2285 assert!(!tree.is_visible(widget));
2286 tree.activate(widget);
2287 assert!(tree.is_visible(widget));
2288 }
2289
2290 #[test]
2291 fn frame_tick_deadline_caps_per_frame_effects_at_60hz() {
2292 let mut tree = WidgetTree::new();
2293 assert!(tree.frame_tick_deadline().is_none());
2295
2296 tree.request_frame();
2299 let t0 = std::time::Instant::now();
2301 tree.last_frame_time = Some(t0);
2302
2303 let deadline = tree
2304 .frame_tick_deadline()
2305 .expect("an armed per-frame effect must publish a deadline");
2306 let interval = deadline.saturating_duration_since(t0);
2307 assert!(
2310 interval >= std::time::Duration::from_micros(16_000)
2311 && interval <= std::time::Duration::from_micros(17_500),
2312 "per-frame effects must pace at ~60 Hz (got {interval:?})"
2313 );
2314
2315 assert_eq!(
2318 tree.next_timer_deadline(),
2319 Some(deadline),
2320 "frame-tick deadline must be surfaced by next_timer_deadline"
2321 );
2322 }
2323
2324 #[test]
2325 fn throttled_subscriber_stretches_deadline_but_per_frame_wins_min() {
2326 use crate::test_widgets::{FillWidget, StackWidget};
2327 let mut tree = WidgetTree::new();
2328 let w = tree.add(FillWidget::new());
2329 let w2 = tree.add(FillWidget::new());
2330 tree.add(StackWidget::new().child(w).child(w2));
2331 let _throttled =
2333 tree.subscribe_frame_tick_throttled(w, std::time::Duration::from_millis(1500));
2334 tree.layout(SizeProposal::exact(200.0, 100.0));
2338 let _ = tree.render();
2339 assert!(
2340 !tree.frame_tick_requested.get(),
2341 "precondition: nothing but the subscriber wants this frame"
2342 );
2343 let t0 = tree.last_frame_time.expect("a frame has run");
2344
2345 let d = tree.frame_tick_deadline().expect("armed");
2347 let dt = d.saturating_duration_since(t0);
2348 assert!(
2349 dt >= std::time::Duration::from_millis(1490)
2350 && dt <= std::time::Duration::from_millis(1510),
2351 "a lone throttled subscriber must pace at its interval (~1.5 s), got {dt:?}"
2352 );
2353
2354 let _per_frame = tree.subscribe_frame_tick(w2);
2359 let d2 = tree.frame_tick_deadline().expect("armed");
2360 let dt2 = d2.saturating_duration_since(t0);
2361 assert!(
2362 dt2 <= std::time::Duration::from_millis(20),
2363 "a per-frame subscriber must pull the shared deadline to 60 Hz, got {dt2:?}"
2364 );
2365 }
2366
2367 #[test]
2373 fn a_raw_frame_request_is_not_paced_by_a_throttled_subscriber() {
2374 use crate::test_widgets::FillWidget;
2375 const NORMAL_FRAME: std::time::Duration = std::time::Duration::from_micros(17_500);
2376 const CLOCK: std::time::Duration = std::time::Duration::from_secs(60);
2377 fn frame(tree: &mut WidgetTree) {
2378 tree.layout(SizeProposal::exact(200.0, 100.0));
2379 let _ = tree.render();
2380 }
2381 fn wait(tree: &WidgetTree) -> std::time::Duration {
2382 let deadline = tree.frame_tick_deadline().expect("armed");
2383 deadline.saturating_duration_since(tree.last_frame_time.expect("a frame has run"))
2384 }
2385
2386 let mut tree = WidgetTree::new();
2387 let clock = tree.add(FillWidget::new());
2388 let _tick = tree.subscribe_frame_tick_throttled(clock, CLOCK);
2389 frame(&mut tree);
2390 assert!(
2391 wait(&tree) >= CLOCK - NORMAL_FRAME,
2392 "precondition: the painted clock paces the loop at its own interval"
2393 );
2394
2395 for request in [
2398 (|tree: &WidgetTree| tree.request_frame()) as fn(&WidgetTree),
2399 |tree: &WidgetTree| tree.frame_request_handle().set(true),
2400 ] {
2401 request(&tree);
2402 assert!(
2403 wait(&tree) <= NORMAL_FRAME,
2404 "a requested frame must not wait on the clock (waited {:?})",
2405 wait(&tree)
2406 );
2407 assert_eq!(tree.next_timer_deadline(), tree.frame_tick_deadline());
2408
2409 frame(&mut tree);
2410 assert!(
2411 wait(&tree) >= CLOCK - NORMAL_FRAME,
2412 "once the request is served the clock paces alone again, rather than the loop \
2413 free-running at 60 Hz (waited {:?})",
2414 wait(&tree)
2415 );
2416 }
2417 }
2418
2419 #[test]
2428 fn the_subscriber_arm_is_spent_by_its_tick_and_ends_with_its_owner() {
2429 use crate::test_widgets::StackWidget;
2430 fn frame(tree: &mut WidgetTree) {
2431 tree.layout(SizeProposal::exact(200.0, 100.0));
2432 let _ = tree.render();
2433 }
2434
2435 let mut tree = WidgetTree::new();
2436 let owner = tree.add(FillWidget::new());
2437 let sibling = tree.add(FillWidget::new());
2438 tree.add(StackWidget::new().child(owner).child(sibling));
2439 let ticks = std::rc::Rc::new(std::cell::Cell::new(0_u32));
2440 let _count = tree.frame_tick().observe({
2441 let ticks = ticks.clone();
2442 move |_| ticks.set(ticks.get() + 1)
2443 });
2444 let _sub = tree.subscribe_frame_tick(owner);
2445
2446 frame(&mut tree);
2447 assert!(
2448 tree.frame_requested(),
2449 "a render that painted the subscriber arms the next tick"
2450 );
2451 assert!(
2452 !tree.frame_tick_requested.get(),
2453 "precondition: armed by the subscribers' flag, not by a raw request"
2454 );
2455
2456 let before = ticks.get();
2458 tree.layout(SizeProposal::exact(200.0, 100.0));
2459 assert_eq!(
2460 ticks.get(),
2461 before + 1,
2462 "the armed tick fires on the next layout"
2463 );
2464 assert!(
2465 !tree.frame_requested(),
2466 "the layout that fired the tick has spent the arm"
2467 );
2468 let _ = tree.render();
2469 assert!(
2470 tree.frame_requested(),
2471 "the owner painted again and re-armed"
2472 );
2473
2474 let before = ticks.get();
2476 tree.advance_time(std::time::Duration::from_millis(16));
2477 assert_eq!(ticks.get(), before + 1, "advance_time fires an armed tick");
2478 assert!(
2479 !tree.frame_requested(),
2480 "advance_time spends the arm as layout does"
2481 );
2482 tree.resume_real_time();
2483
2484 frame(&mut tree);
2487 tree.set_dormant(owner);
2488 assert!(
2489 tree.frame_tick_deadline().is_some(),
2490 "the tick armed before the owner was parked is still due"
2491 );
2492 let before = ticks.get();
2493 frame(&mut tree);
2494 assert_eq!(ticks.get(), before + 1);
2495 assert!(
2496 !tree.frame_requested(),
2497 "a parked subscriber must not re-arm the tick"
2498 );
2499 assert_eq!(
2500 tree.next_timer_deadline(),
2501 None,
2502 "with its only subscriber parked the loop sleeps"
2503 );
2504 }
2505
2506 #[test]
2507 fn show_and_dismiss_overlay() {
2508 let mut tree = WidgetTree::new();
2509 let anchor = tree.add(FillWidget::new());
2510 let content = tree.add(FillWidget::new().label("Overlay"));
2511 tree.layout(SizeProposal::exact(200.0, 100.0));
2512
2513 assert!(tree.active_overlays().is_empty());
2514
2515 let id = tree.show_overlay(crate::overlay::OverlayRequest::new(
2516 content,
2517 anchor,
2518 crate::overlay::OverlayPlacement::Below,
2519 crate::overlay::DismissBehavior::Manual,
2520 ));
2521
2522 assert_eq!(tree.active_overlays().len(), 1);
2523
2524 tree.dismiss_overlay(id);
2525 assert!(tree.active_overlays().is_empty());
2526 assert!(!tree.is_visible(content));
2527 }
2528
2529 #[test]
2534 fn modal_centered_overlay_is_a_host_surface() {
2535 let mut tree = WidgetTree::new();
2536 let anchor = tree.add(FillWidget::new());
2537 let modal_content = tree.add(FillWidget::new().label("Modal"));
2538 let dropdown_content = tree.add(FillWidget::new().label("Dropdown"));
2539 tree.layout(SizeProposal::exact(200.0, 100.0));
2540
2541 let modal = tree.show_overlay(crate::overlay::OverlayRequest::new(
2542 modal_content,
2543 anchor,
2544 crate::overlay::OverlayPlacement::Centered,
2545 crate::overlay::DismissBehavior::EscapeOrClickOutside,
2546 ));
2547 let dropdown = tree.show_overlay(
2548 crate::overlay::OverlayRequest::new(
2549 dropdown_content,
2550 anchor,
2551 crate::overlay::OverlayPlacement::Below,
2552 crate::overlay::DismissBehavior::EscapeOrClickOutside,
2553 )
2554 .parent_overlay(modal),
2555 );
2556
2557 assert!(
2558 tree.overlay_is_host_surface(modal),
2559 "a Centered modal overlay must be treated as a host surface"
2560 );
2561 assert!(
2562 !tree.overlay_is_host_surface(dropdown),
2563 "a plain dropdown overlay is not a host surface"
2564 );
2565 }
2566
2567 #[test]
2568 fn escape_dismisses_topmost_overlay() {
2569 let mut tree = WidgetTree::new();
2570 let anchor = tree.add(FillWidget::new().focusable());
2571 let content = tree.add(FillWidget::new());
2572 tree.layout(SizeProposal::exact(200.0, 100.0));
2573 tree.focus(anchor);
2574
2575 tree.show_overlay(crate::overlay::OverlayRequest::new(
2576 content,
2577 anchor,
2578 crate::overlay::OverlayPlacement::Below,
2579 crate::overlay::DismissBehavior::EscapeOrClickOutside,
2580 ));
2581
2582 assert_eq!(tree.active_overlays().len(), 1);
2583
2584 tree.press_key(Key::Escape, Modifiers::NONE);
2585 assert!(tree.active_overlays().is_empty());
2586 assert!(!tree.is_visible(content));
2587 }
2588
2589 fn show_two_nested_overlays(tree: &mut WidgetTree) -> (WidgetId, WidgetId) {
2592 let anchor = tree.add(FillWidget::new());
2593 let c1 = tree.add(FillWidget::new());
2594 let c2 = tree.add(FillWidget::new());
2595 tree.layout(SizeProposal::exact(200.0, 100.0));
2596 let o1 = tree.show_overlay(crate::overlay::OverlayRequest::new(
2597 c1,
2598 anchor,
2599 crate::overlay::OverlayPlacement::Below,
2600 crate::overlay::DismissBehavior::Manual,
2601 ));
2602 tree.show_overlay(
2603 crate::overlay::OverlayRequest::new(
2604 c2,
2605 c1,
2606 crate::overlay::OverlayPlacement::Below,
2607 crate::overlay::DismissBehavior::Manual,
2608 )
2609 .parent_overlay(o1),
2610 );
2611 (c1, c2)
2612 }
2613
2614 #[test]
2615 fn nested_overlay_back_key_dismisses_with_arrow_left_under_ltr() {
2616 let mut tree = WidgetTree::new();
2617 let _ = show_two_nested_overlays(&mut tree);
2618 assert_eq!(tree.active_overlays().len(), 2);
2619
2620 tree.press_key(Key::ArrowRight, Modifiers::NONE);
2622 assert_eq!(tree.active_overlays().len(), 2);
2623
2624 tree.press_key(Key::ArrowLeft, Modifiers::NONE);
2626 assert_eq!(tree.active_overlays().len(), 1);
2627 }
2628
2629 #[test]
2630 fn nested_overlay_back_key_flips_to_arrow_right_under_rtl() {
2631 let mut tree = WidgetTree::new();
2632 tree.set_layout_direction(crate::environment::LayoutDirection::RightToLeft);
2633 let _ = show_two_nested_overlays(&mut tree);
2634 assert_eq!(tree.active_overlays().len(), 2);
2635
2636 tree.press_key(Key::ArrowLeft, Modifiers::NONE);
2639 assert_eq!(tree.active_overlays().len(), 2);
2640
2641 tree.press_key(Key::ArrowRight, Modifiers::NONE);
2643 assert_eq!(tree.active_overlays().len(), 1);
2644 }
2645
2646 fn show_inert_overlay(tree: &mut WidgetTree, anchor: WidgetId) -> WidgetId {
2648 let content = tree.add(FillWidget::new());
2649 tree.layout(SizeProposal::exact(200.0, 100.0));
2650 tree.show_overlay(
2651 crate::overlay::OverlayRequest::new(
2652 content,
2653 anchor,
2654 crate::overlay::OverlayPlacement::Below,
2655 crate::overlay::DismissBehavior::EscapeKey,
2656 )
2657 .inert(),
2658 );
2659 content
2660 }
2661
2662 #[test]
2667 fn back_key_does_not_count_an_inert_overlay_as_a_menu_level() {
2668 for inert_first in [true, false] {
2669 let mut tree = WidgetTree::new();
2670 let anchor = tree.add(FillWidget::new());
2671 let menu = tree.add(FillWidget::new());
2672 tree.layout(SizeProposal::exact(200.0, 100.0));
2673 let show_menu = |tree: &mut WidgetTree| {
2674 tree.show_overlay(crate::overlay::OverlayRequest::new(
2675 menu,
2676 anchor,
2677 crate::overlay::OverlayPlacement::Below,
2678 crate::overlay::DismissBehavior::Manual,
2679 ));
2680 };
2681 let inert = if inert_first {
2682 let inert = show_inert_overlay(&mut tree, anchor);
2683 show_menu(&mut tree);
2684 inert
2685 } else {
2686 show_menu(&mut tree);
2687 show_inert_overlay(&mut tree, anchor)
2688 };
2689 assert_eq!(tree.active_overlays().len(), 2);
2690
2691 tree.press_key(Key::ArrowLeft, Modifiers::NONE);
2692 assert_eq!(
2693 tree.active_overlays().len(),
2694 2,
2695 "inert shown first: {inert_first}: one menu is not a cascade"
2696 );
2697 assert!(tree.is_visible(menu) && tree.is_visible(inert));
2698 }
2699 }
2700
2701 #[test]
2704 fn back_key_closes_the_submenu_beneath_an_inert_overlay() {
2705 let mut tree = WidgetTree::new();
2706 let (parent, submenu) = show_two_nested_overlays(&mut tree);
2707 let inert = show_inert_overlay(&mut tree, parent);
2708 assert_eq!(tree.active_overlays().len(), 3);
2709
2710 tree.press_key(Key::ArrowLeft, Modifiers::NONE);
2711 assert!(!tree.is_visible(submenu), "the submenu closes");
2712 assert!(tree.is_visible(parent), "its parent stays open");
2713 assert!(
2714 tree.is_visible(inert),
2715 "the inert overlay is not what closes"
2716 );
2717 }
2718
2719 #[derive(Debug)]
2722 struct KeyProbe {
2723 seen: std::rc::Rc<std::cell::RefCell<Vec<Key>>>,
2724 }
2725
2726 impl Widget for KeyProbe {
2727 fn layout_response(
2728 &self,
2729 proposal: SizeProposal,
2730 _ctx: &crate::widget::LayoutContext,
2731 ) -> crate::widget::LayoutResponse {
2732 proposal.resolve(0.0, 0.0).into()
2733 }
2734
2735 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
2736 let seen = self.seen.clone();
2737 ctx.apply_self_handlers(
2738 crate::widget_builder::HandlerSet::new()
2739 .focusable(true)
2740 .on_key(move |event, _ctx| match event {
2741 WidgetEvent::KeyDown { key, .. } => {
2742 seen.borrow_mut().push(*key);
2743 crate::event::EventResponse::Handled
2744 }
2745 _ => crate::event::EventResponse::Ignored,
2746 }),
2747 );
2748 Vec::new()
2749 }
2750 }
2751
2752 #[test]
2764 fn back_key_ignores_text_affordance_overlays_and_reaches_the_editor() {
2765 let mut tree = WidgetTree::new();
2766 let seen = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
2767 let editor = tree.add(KeyProbe { seen: seen.clone() });
2768 let handles_a = tree.add(FillWidget::new());
2769 let handles_b = tree.add(FillWidget::new());
2770 tree.layout(SizeProposal::exact(200.0, 100.0));
2771 tree.focus(editor);
2772
2773 for content in [handles_a, handles_b] {
2775 tree.show_overlay_in_band(
2776 crate::overlay::OverlayRequest::new(
2777 content,
2778 editor,
2779 crate::overlay::OverlayPlacement::FullViewport,
2780 crate::overlay::DismissBehavior::Manual,
2781 ),
2782 crate::overlay::OverlayBand::TextAffordance,
2783 );
2784 }
2785 assert_eq!(tree.active_overlays().len(), 2);
2786
2787 tree.press_key(Key::ArrowLeft, Modifiers::NONE);
2788
2789 assert_eq!(
2790 tree.active_overlays().len(),
2791 2,
2792 "a text affordance is not a menu cascade level — the back key must \
2793 leave both hosts standing"
2794 );
2795 assert_eq!(
2796 seen.borrow().as_slice(),
2797 &[Key::ArrowLeft],
2798 "ArrowLeft must reach the focused editor; swallowing it is the bug \
2799 that left the caret unable to step left in any dual-pane tab"
2800 );
2801 }
2802
2803 #[test]
2807 fn back_key_still_closes_a_submenu_over_a_mounted_text_affordance() {
2808 let mut tree = WidgetTree::new();
2809 let affordance = tree.add(FillWidget::new());
2810 let anchor = tree.add(FillWidget::new());
2811 let menu = tree.add(FillWidget::new());
2812 let submenu = tree.add(FillWidget::new());
2813 tree.layout(SizeProposal::exact(200.0, 100.0));
2814 tree.show_overlay_in_band(
2815 crate::overlay::OverlayRequest::new(
2816 affordance,
2817 anchor,
2818 crate::overlay::OverlayPlacement::FullViewport,
2819 crate::overlay::DismissBehavior::Manual,
2820 ),
2821 crate::overlay::OverlayBand::TextAffordance,
2822 );
2823 let parent = tree.show_overlay(crate::overlay::OverlayRequest::new(
2824 menu,
2825 anchor,
2826 crate::overlay::OverlayPlacement::Below,
2827 crate::overlay::DismissBehavior::Manual,
2828 ));
2829 tree.show_overlay(
2830 crate::overlay::OverlayRequest::new(
2831 submenu,
2832 menu,
2833 crate::overlay::OverlayPlacement::Below,
2834 crate::overlay::DismissBehavior::Manual,
2835 )
2836 .parent_overlay(parent),
2837 );
2838 assert_eq!(tree.active_overlays().len(), 3);
2839
2840 tree.press_key(Key::ArrowLeft, Modifiers::NONE);
2841 assert_eq!(
2842 tree.active_overlays().len(),
2843 2,
2844 "the submenu must still close: the affordance host narrows the \
2845 count, it does not disarm the back key"
2846 );
2847 }
2848
2849 #[test]
2854 fn back_key_does_not_dismiss_a_dialog_on_top_of_a_modal() {
2855 let mut tree = WidgetTree::new();
2856 let anchor = tree.add(FillWidget::new());
2857 let scrim1 = tree.add(FillWidget::new());
2858 let scrim2 = tree.add(FillWidget::new());
2859 let dialog = tree.add(FillWidget::new());
2860 tree.layout(SizeProposal::exact(200.0, 100.0));
2861 for c in [scrim1, scrim2] {
2863 tree.show_overlay(crate::overlay::OverlayRequest::new(
2864 c,
2865 anchor,
2866 crate::overlay::OverlayPlacement::Below,
2867 crate::overlay::DismissBehavior::Manual,
2868 ));
2869 }
2870 tree.show_overlay(crate::overlay::OverlayRequest::new(
2872 dialog,
2873 anchor,
2874 crate::overlay::OverlayPlacement::Centered,
2875 crate::overlay::DismissBehavior::Manual,
2876 ));
2877 assert_eq!(tree.active_overlays().len(), 3);
2878
2879 tree.press_key(Key::ArrowLeft, Modifiers::NONE);
2882 assert_eq!(
2883 tree.active_overlays().len(),
2884 3,
2885 "the back key must not close a dialog sitting on top of a modal"
2886 );
2887 }
2888
2889 #[test]
2890 fn escape_does_not_dismiss_manual_overlay() {
2891 let mut tree = WidgetTree::new();
2892 let anchor = tree.add(FillWidget::new().focusable());
2893 let content = tree.add(FillWidget::new());
2894 tree.layout(SizeProposal::exact(200.0, 100.0));
2895 tree.focus(anchor);
2896
2897 tree.show_overlay(crate::overlay::OverlayRequest::new(
2898 content,
2899 anchor,
2900 crate::overlay::OverlayPlacement::Below,
2901 crate::overlay::DismissBehavior::Manual,
2902 ));
2903
2904 assert_eq!(tree.active_overlays().len(), 1);
2905
2906 tree.press_key(Key::Escape, Modifiers::NONE);
2907 assert_eq!(tree.active_overlays().len(), 1);
2909 }
2910
2911 #[test]
2912 fn click_outside_dismisses_overlay() {
2913 let mut tree = WidgetTree::new();
2914 let anchor = tree.add(FillWidget::new());
2915 let content = tree.add(FillWidget::new());
2916 tree.layout(SizeProposal::exact(200.0, 100.0));
2917
2918 let overlay = tree.show_overlay(crate::overlay::OverlayRequest::new(
2919 content,
2920 anchor,
2921 crate::overlay::OverlayPlacement::Below,
2922 crate::overlay::DismissBehavior::ClickOutside,
2923 ));
2924
2925 tree.overlay_manager
2926 .set_content_bounds(overlay, teksilo_canvas::Size::new(100.0, 50.0));
2927
2928 assert_eq!(tree.active_overlays().len(), 1);
2929
2930 tree.dispatch_event(WidgetEvent::pointer_down(
2931 Point::new(500.0, 500.0),
2932 PointerButton::Primary,
2933 Modifiers::NONE,
2934 ));
2935 assert!(tree.active_overlays().is_empty());
2936 assert!(!tree.is_visible(content));
2937 }
2938
2939 #[test]
2940 fn cascade_dismissal() {
2941 let mut tree = WidgetTree::new();
2942 let anchor = tree.add(FillWidget::new());
2943 let content_a = tree.add(FillWidget::new());
2944 let content_b = tree.add(FillWidget::new());
2945 tree.layout(SizeProposal::exact(200.0, 100.0));
2946
2947 let parent = tree.show_overlay(crate::overlay::OverlayRequest::new(
2948 content_a,
2949 anchor,
2950 crate::overlay::OverlayPlacement::Below,
2951 crate::overlay::DismissBehavior::Manual,
2952 ));
2953 tree.show_overlay(
2954 crate::overlay::OverlayRequest::new(
2955 content_b,
2956 content_a,
2957 crate::overlay::OverlayPlacement::TrailingEdge,
2958 crate::overlay::DismissBehavior::Manual,
2959 )
2960 .parent_overlay(parent),
2961 );
2962
2963 assert_eq!(tree.active_overlays().len(), 2);
2964
2965 tree.dismiss_overlay(parent);
2966 assert!(tree.active_overlays().is_empty());
2967 assert!(!tree.is_visible(content_a));
2968 assert!(!tree.is_visible(content_b));
2969 }
2970
2971 #[test]
2972 fn dismissed_overlay_content_is_dormant_and_invisible() {
2973 let mut tree = WidgetTree::new();
2974 let anchor = tree.add(FillWidget::new());
2975 let content = tree.add(FillWidget::new());
2976 tree.layout(SizeProposal::exact(800.0, 600.0));
2977
2978 let id = tree.show_overlay(crate::overlay::OverlayRequest::new(
2979 content,
2980 anchor,
2981 crate::overlay::OverlayPlacement::Below,
2982 crate::overlay::DismissBehavior::Manual,
2983 ));
2984
2985 tree.layout(SizeProposal::exact(800.0, 600.0));
2986
2987 tree.dismiss_overlay(id);
2988 assert!(!tree.is_visible(content));
2989
2990 tree.layout(SizeProposal::exact(800.0, 600.0));
2991
2992 let center = tree.bounds(content).center();
2993 let hit = tree.hit_test(center);
2994 assert_ne!(hit, Some(content));
2995
2996 let _frame = tree.render();
2997 assert!(!tree.is_visible(content));
2998 }
2999
3000 #[test]
3001 fn tooltip_appears_after_delay() {
3002 let mut tree = WidgetTree::new();
3003 let anchor = tree.add(FillWidget::new());
3004 let tooltip = tree.add(FillWidget::new().label("Tooltip text"));
3005 tree.layout(SizeProposal::exact(200.0, 100.0));
3006
3007 tree.attach_tooltip(anchor, tooltip, std::time::Duration::from_millis(500));
3008
3009 let center = tree.bounds(anchor).center();
3010 tree.pointer_move(center);
3011 assert!(tree.active_overlays().is_empty());
3012
3013 tree.advance_time(std::time::Duration::from_millis(600));
3014
3015 assert_eq!(tree.active_overlays().len(), 1);
3016 assert!(tree.find_by_label("Tooltip text").is_some());
3017 }
3018
3019 #[test]
3020 fn tooltip_survives_theme_switch() {
3021 let mut tree = WidgetTree::new().with_theme(crate::presets::intui::light());
3026 let anchor = tree.add(FillWidget::new());
3027 let tooltip = tree.add(FillWidget::new().label("Tip"));
3028 tree.layout(SizeProposal::exact(200.0, 100.0));
3029
3030 tree.attach_tooltip(anchor, tooltip, std::time::Duration::from_millis(500));
3031
3032 tree.set_theme(crate::presets::intui::dark());
3033
3034 tree.pointer_move(tree.bounds(anchor).center());
3035 tree.advance_time(std::time::Duration::from_millis(600));
3036
3037 assert_eq!(tree.active_overlays().len(), 1);
3038 assert!(tree.find_by_label("Tip").is_some());
3039 }
3040
3041 #[test]
3042 fn tooltip_suppressed_when_hovering_anchor_owned_overlay_content() {
3043 let mut tree = WidgetTree::new();
3051 let anchor = tree.add(FillWidget::new());
3052 let panel = tree.add_child(anchor, FillWidget::new());
3055 let row = tree.add_child(panel, FillWidget::new());
3056 let tip = tree.add(FillWidget::new().label("Tip"));
3057 tree.layout(SizeProposal::exact(200.0, 100.0));
3058
3059 let delay = std::time::Duration::from_millis(100);
3060 tree.attach_tooltip(anchor, tip, delay);
3061
3062 tree.tooltip_pointer_enter(anchor);
3065 tree.advance_time(delay + std::time::Duration::from_millis(50));
3066 assert_eq!(
3067 tree.active_overlays().len(),
3068 1,
3069 "tooltip should appear when hovering the anchor itself"
3070 );
3071 tree.pointer_move(Point::new(500.0, 500.0));
3074 tree.advance_time(std::time::Duration::from_millis(150));
3075 assert!(tree.active_overlays().is_empty());
3076
3077 tree.show_overlay(crate::overlay::OverlayRequest::new(
3079 panel,
3080 anchor,
3081 crate::overlay::OverlayPlacement::Below,
3082 crate::overlay::DismissBehavior::Manual,
3083 ));
3084 assert_eq!(tree.active_overlays().len(), 1);
3085
3086 tree.tooltip_pointer_enter(row);
3089 tree.advance_time(delay + std::time::Duration::from_millis(50));
3090 assert_eq!(
3091 tree.active_overlays().len(),
3092 1,
3093 "anchor tooltip must not leak onto its own overlay's rows"
3094 );
3095 }
3096
3097 #[test]
3098 fn tooltip_inside_overlay_still_fires() {
3099 let mut tree = WidgetTree::new();
3103 let host = tree.add(FillWidget::new());
3104 let panel = tree.add_child(host, FillWidget::new());
3105 let row = tree.add_child(panel, FillWidget::new());
3106 let tip = tree.add(FillWidget::new().label("Row tip"));
3107 tree.layout(SizeProposal::exact(200.0, 100.0));
3108
3109 let delay = std::time::Duration::from_millis(100);
3110 tree.attach_tooltip(row, tip, delay);
3112
3113 tree.show_overlay(crate::overlay::OverlayRequest::new(
3114 panel,
3115 host,
3116 crate::overlay::OverlayPlacement::Below,
3117 crate::overlay::DismissBehavior::Manual,
3118 ));
3119 assert_eq!(tree.active_overlays().len(), 1);
3120
3121 tree.tooltip_pointer_enter(row);
3122 tree.advance_time(delay + std::time::Duration::from_millis(50));
3123 assert_eq!(
3124 tree.active_overlays().len(),
3125 2,
3126 "a row's own tooltip should still fire inside an overlay"
3127 );
3128 }
3129
3130 #[test]
3131 fn tooltip_dismissed_on_pointer_leave() {
3132 let mut tree = WidgetTree::new();
3133 let anchor = tree.add(FillWidget::new());
3134 let tooltip = tree.add(FillWidget::new().label("Tip"));
3135 tree.layout(SizeProposal::exact(200.0, 100.0));
3136
3137 tree.attach_tooltip(anchor, tooltip, std::time::Duration::from_millis(500));
3138
3139 tree.pointer_move(tree.bounds(anchor).center());
3140 tree.advance_time(std::time::Duration::from_millis(600));
3141 assert_eq!(tree.active_overlays().len(), 1);
3142
3143 tree.pointer_move(Point::new(500.0, 500.0));
3148 assert_eq!(
3149 tree.active_overlays().len(),
3150 1,
3151 "tooltip persists briefly after anchor-leave (hoverable grace)"
3152 );
3153 tree.advance_time(std::time::Duration::from_millis(150));
3154 assert!(
3155 tree.active_overlays().is_empty(),
3156 "tooltip dismissed after the 100ms grace outside anchor+overlay"
3157 );
3158 }
3159
3160 #[test]
3161 fn tooltip_not_shown_if_pointer_leaves_before_delay() {
3162 let mut tree = WidgetTree::new();
3163 let anchor = tree.add(FillWidget::new());
3164 let tooltip = tree.add(FillWidget::new().label("Tip"));
3165 tree.layout(SizeProposal::exact(200.0, 100.0));
3166
3167 tree.attach_tooltip(anchor, tooltip, std::time::Duration::from_millis(500));
3168
3169 tree.pointer_move(tree.bounds(anchor).center());
3170 tree.advance_time(std::time::Duration::from_millis(200));
3171 tree.pointer_move(Point::new(500.0, 500.0));
3172
3173 tree.advance_time(std::time::Duration::from_millis(500));
3174 assert!(tree.active_overlays().is_empty());
3175 }
3176
3177 #[test]
3178 fn tooltip_reshow_uses_short_delay_after_prior_tip() {
3179 let mut tree = WidgetTree::new();
3182 let a = tree.add(FillWidget::new());
3183 let b = tree.add(FillWidget::new());
3184 let tip_a = tree.add(FillWidget::new().label("Tip A"));
3185 let tip_b = tree.add(FillWidget::new().label("Tip B"));
3186 tree.layout(SizeProposal::exact(400.0, 200.0));
3187
3188 let delay = std::time::Duration::from_millis(500);
3189 tree.attach_tooltip(a, tip_a, delay);
3190 tree.attach_tooltip(b, tip_b, delay);
3191
3192 tree.tooltip_pointer_enter(a);
3195 tree.advance_time(std::time::Duration::from_millis(150));
3196 assert!(
3197 tree.active_overlays().is_empty(),
3198 "must not appear before full initial delay"
3199 );
3200 tree.advance_time(std::time::Duration::from_millis(400));
3201 assert_eq!(
3202 tree.active_overlays().len(),
3203 1,
3204 "first tip after initial delay"
3205 );
3206 assert!(tree.find_by_label("Tip A").is_some());
3207
3208 let mut noop = crate::window::NoopWindowOps;
3210 tree.tooltip_pointer_leave(a, &mut noop);
3211 tree.tooltip_pointer_enter(b);
3212 tree.advance_time(std::time::Duration::from_millis(120));
3213 assert!(
3214 tree.find_by_label("Tip B").is_some(),
3215 "second tip should use the short reshow delay while session is warm"
3216 );
3217 }
3218
3219 #[test]
3220 fn reshow_delay_reverts_to_full_after_the_session_grace_expires() {
3221 let mut tree = WidgetTree::new();
3227 tree.set_accessibility_preferences(false, true, 1.0);
3231 let a = tree.add(FillWidget::new());
3232 let b = tree.add(FillWidget::new());
3233 let tip_a = tree.add(FillWidget::new().label("Tip A"));
3234 let tip_b = tree.add(FillWidget::new().label("Tip B"));
3235 tree.layout(SizeProposal::exact(400.0, 200.0));
3236
3237 let delay = std::time::Duration::from_millis(500);
3238 tree.attach_tooltip(a, tip_a, delay);
3239 tree.attach_tooltip(b, tip_b, delay);
3240
3241 tree.tooltip_pointer_enter(a);
3243 tree.advance_time(std::time::Duration::from_millis(550));
3244 assert!(tree.find_by_label("Tip A").is_some(), "first tip shown");
3245
3246 tree.pointer_move(Point::new(900.0, 900.0));
3247 tree.advance_time(std::time::Duration::from_millis(150));
3248 assert!(tree.active_overlays().is_empty(), "A dismissed on leave");
3249
3250 tree.advance_time(super::TOOLTIP_SESSION_GRACE + std::time::Duration::from_millis(50));
3252
3253 tree.tooltip_pointer_enter(b);
3254 tree.advance_time(std::time::Duration::from_millis(150));
3255 assert!(
3256 tree.active_overlays().is_empty(),
3257 "session went cold — B must pay the FULL delay, not the 100 ms reshow"
3258 );
3259 tree.advance_time(std::time::Duration::from_millis(400));
3260 assert!(
3261 tree.find_by_label("Tip B").is_some(),
3262 "B still appears once its full delay elapses"
3263 );
3264 }
3265
3266 #[test]
3267 fn warm_reshow_scales_the_delay_rather_than_flattening_every_tier() {
3268 let mut tree = WidgetTree::new();
3274 let light = tree.add(FillWidget::new());
3275 let heavy = tree.add(FillWidget::new());
3276 let tip_light = tree.add(FillWidget::new().label("Light"));
3277 let tip_heavy = tree.add(FillWidget::new().label("Heavy"));
3278 tree.layout(SizeProposal::exact(400.0, 200.0));
3279
3280 tree.attach_tooltip(light, tip_light, std::time::Duration::from_millis(500));
3281 tree.attach_tooltip(heavy, tip_heavy, std::time::Duration::from_millis(700));
3282
3283 tree.tooltip_pointer_enter(light);
3285 tree.advance_time(std::time::Duration::from_millis(550));
3286 assert!(tree.find_by_label("Light").is_some(), "session warm");
3287
3288 let mut noop = crate::window::NoopWindowOps;
3289 tree.tooltip_pointer_leave(light, &mut noop);
3290 tree.tooltip_pointer_enter(heavy);
3291
3292 tree.advance_time(std::time::Duration::from_millis(120));
3294 assert!(
3295 tree.find_by_label("Heavy").is_none(),
3296 "a heavy tooltip must not fire at the light tier's reshow delay"
3297 );
3298 tree.advance_time(std::time::Duration::from_millis(40));
3300 assert!(
3301 tree.find_by_label("Heavy").is_some(),
3302 "heavy reshow is the scaled 140 ms"
3303 );
3304 }
3305
3306 #[test]
3307 fn a_pinned_sticky_tooltip_does_not_hold_the_session_warm() {
3308 let mut tree = WidgetTree::new();
3312 tree.set_accessibility_preferences(false, true, 1.0); let a = tree.add(FillWidget::new());
3314 let b = tree.add(FillWidget::new());
3315 let tip_a = tree.add(FillWidget::new().label("Pinned"));
3316 let tip_b = tree.add(FillWidget::new().label("Other"));
3317 tree.layout(SizeProposal::exact(400.0, 200.0));
3318
3319 tree.attach_tooltip_with_sticky(
3320 a,
3321 tip_a,
3322 std::time::Duration::from_millis(500),
3323 Some(std::time::Duration::from_millis(100)),
3324 );
3325 tree.attach_tooltip(b, tip_b, std::time::Duration::from_millis(500));
3326
3327 tree.tooltip_pointer_enter(a);
3328 tree.advance_time(std::time::Duration::from_millis(550));
3329 assert!(tree.find_by_label("Pinned").is_some());
3330 tree.promote_tooltip_to_sticky(tip_a);
3331
3332 tree.advance_time(super::TOOLTIP_SESSION_GRACE + std::time::Duration::from_millis(50));
3334 tree.tooltip_pointer_enter(b);
3335 tree.advance_time(std::time::Duration::from_millis(150));
3336 assert!(
3337 tree.find_by_label("Other").is_none(),
3338 "a pinned sticky must not keep every other anchor on the reshow path"
3339 );
3340 }
3341
3342 #[test]
3343 fn reattaching_a_tooltip_does_not_grow_the_entry_table() {
3344 let mut tree = WidgetTree::new();
3350 let anchor = tree.add(FillWidget::new());
3351 tree.layout(SizeProposal::exact(200.0, 100.0));
3352
3353 let delay = std::time::Duration::from_millis(100);
3354 for _ in 0..25 {
3355 let tip = tree.add(FillWidget::new().label("Tip"));
3357 tree.attach_tooltip(anchor, tip, delay);
3358 assert_eq!(
3359 tree.tooltip_entry_count(),
3360 1,
3361 "an anchor must own exactly one tooltip entry across rebuilds"
3362 );
3363 }
3364
3365 tree.tooltip_pointer_enter(anchor);
3367 tree.advance_time(delay + std::time::Duration::from_millis(50));
3368 assert_eq!(
3369 tree.active_overlays().len(),
3370 1,
3371 "latest tooltip still shows"
3372 );
3373 }
3374
3375 #[test]
3376 fn destroying_an_anchor_reaps_its_tooltip_entry() {
3377 let mut tree = WidgetTree::new();
3380 let host = tree.add(FillWidget::new());
3381 let anchor = tree.add_child(host, FillWidget::new());
3382 let tip = tree.add(FillWidget::new().label("Tip"));
3383 tree.layout(SizeProposal::exact(200.0, 100.0));
3384
3385 tree.attach_tooltip(anchor, tip, std::time::Duration::from_millis(100));
3386 assert_eq!(tree.tooltip_entry_count(), 1);
3387
3388 tree.destroy_subtree(anchor);
3389 assert_eq!(
3390 tree.tooltip_entry_count(),
3391 0,
3392 "destroying the anchor must reap its entry and content node"
3393 );
3394 }
3395
3396 #[test]
3397 fn a_leaving_tooltip_schedules_a_wake_for_its_dismissal() {
3398 let mut tree = WidgetTree::new();
3403 tree.set_accessibility_preferences(false, true, 1.0); let anchor = tree.add(FillWidget::new());
3405 let tip = tree.add(FillWidget::new().label("Tip"));
3406 tree.layout(SizeProposal::exact(400.0, 200.0));
3407
3408 tree.attach_tooltip(anchor, tip, std::time::Duration::from_millis(100));
3409 tree.pointer_move(tree.bounds(anchor).center());
3410 tree.advance_time(std::time::Duration::from_millis(150));
3411 assert_eq!(tree.active_overlays().len(), 1, "tooltip shown");
3412
3413 assert!(
3415 tree.next_timer_deadline().is_none(),
3416 "a settled tooltip under the pointer schedules nothing"
3417 );
3418
3419 tree.pointer_move(Point::new(900.0, 900.0));
3422 assert!(
3423 tree.next_timer_deadline().is_some(),
3424 "the PointerLeave grace must contribute a wake deadline"
3425 );
3426 }
3427
3428 #[test]
3439 fn a_fading_out_overlay_schedules_a_wake_for_its_deferred_removal() {
3440 let mut tree = WidgetTree::new();
3441 let anchor = tree.add(FillWidget::new());
3442 let content = tree.add(FillWidget::new().label("Faded"));
3443 tree.layout(SizeProposal::exact(200.0, 100.0));
3444
3445 let id = tree.show_overlay(
3446 crate::overlay::OverlayRequest::new(
3447 content,
3448 anchor,
3449 crate::overlay::OverlayPlacement::Below,
3450 crate::overlay::DismissBehavior::Manual,
3451 )
3452 .with_fade(std::time::Duration::from_millis(200)),
3453 );
3454 tree.dismiss_overlay(id);
3455 assert!(
3456 tree.is_visible(content),
3457 "precondition: the removal is deferred, so there is something to wake for"
3458 );
3459
3460 let expected = tree.overlay_manager.next_fade_dismiss_deadline();
3466 assert!(
3467 expected.is_some(),
3468 "precondition: the fade start was stamped, so there is a deadline to compare against"
3469 );
3470 assert_eq!(
3471 tree.next_timer_deadline(),
3472 expected,
3473 "the wake deadline must be the deferred removal's own, not merely some deadline"
3474 );
3475 }
3476
3477 #[test]
3478 fn pressing_cancels_a_pending_dwell_and_dismisses_a_shown_tooltip() {
3479 let mut tree = WidgetTree::new();
3480 let anchor = tree.add(FillWidget::new());
3481 let tip = tree.add(FillWidget::new().label("Tip"));
3482 tree.layout(SizeProposal::exact(400.0, 200.0));
3483
3484 let delay = std::time::Duration::from_millis(500);
3485 tree.attach_tooltip(anchor, tip, delay);
3486
3487 tree.pointer_move(tree.bounds(anchor).center());
3490 tree.advance_time(std::time::Duration::from_millis(300));
3491 tree.tooltip_pointer_press(None);
3492 tree.advance_time(std::time::Duration::from_millis(400));
3493 assert!(
3494 tree.active_overlays().is_empty(),
3495 "a press must cancel the pending dwell, not merely delay it"
3496 );
3497
3498 tree.tooltip_pointer_enter(anchor);
3501 tree.advance_time(delay + std::time::Duration::from_millis(50));
3502 assert_eq!(tree.active_overlays().len(), 1, "tooltip shown again");
3503 tree.tooltip_pointer_press(Some(tree.bounds(anchor).center()));
3504 assert!(
3505 tree.active_overlays().is_empty(),
3506 "a press must dismiss the shown tooltip"
3507 );
3508 }
3509
3510 #[test]
3511 fn window_deactivation_retires_hover_tooltips() {
3512 let mut tree = WidgetTree::new();
3513 let anchor = tree.add(FillWidget::new());
3514 let tip = tree.add(FillWidget::new().label("Tip"));
3515 tree.layout(SizeProposal::exact(400.0, 200.0));
3516
3517 tree.attach_tooltip(anchor, tip, std::time::Duration::from_millis(100));
3518 tree.pointer_move(tree.bounds(anchor).center());
3519 tree.advance_time(std::time::Duration::from_millis(150));
3520 assert_eq!(tree.active_overlays().len(), 1);
3521
3522 tree.set_window_active(false);
3523 assert!(
3524 tree.active_overlays().is_empty(),
3525 "a tooltip must not float over another window's chrome"
3526 );
3527 }
3528
3529 #[test]
3530 fn only_the_innermost_anchor_arms_its_dwell() {
3531 let mut tree = WidgetTree::new();
3534 let panel = tree.add(FillWidget::new());
3535 let row = tree.add_child(panel, FillWidget::new());
3536 let panel_tip = tree.add(FillWidget::new().label("Panel"));
3537 let row_tip = tree.add(FillWidget::new().label("Row"));
3538 tree.layout(SizeProposal::exact(400.0, 200.0));
3539
3540 let delay = std::time::Duration::from_millis(100);
3541 tree.attach_tooltip(panel, panel_tip, delay);
3542 tree.attach_tooltip(row, row_tip, delay);
3543
3544 tree.tooltip_pointer_enter(row);
3545 tree.advance_time(delay + std::time::Duration::from_millis(50));
3546
3547 assert_eq!(
3548 tree.active_overlays().len(),
3549 1,
3550 "exactly one tooltip may open for a hover"
3551 );
3552 assert!(
3553 tree.find_by_label("Row").is_some(),
3554 "the innermost anchor wins"
3555 );
3556 }
3557
3558 #[test]
3559 fn escape_dismisses_a_hover_tooltip_and_falls_through_to_the_menu_below() {
3560 let mut tree = WidgetTree::new();
3564 tree.set_accessibility_preferences(false, true, 1.0); let anchor = tree.add(FillWidget::new());
3566 let menu = tree.add(FillWidget::new());
3567 let tip = tree.add(FillWidget::new().label("Tip"));
3568 tree.layout(SizeProposal::exact(400.0, 200.0));
3569
3570 let menu_overlay = tree.show_overlay(crate::overlay::OverlayRequest::new(
3571 menu,
3572 anchor,
3573 crate::overlay::OverlayPlacement::Below,
3574 crate::overlay::DismissBehavior::EscapeOrClickOutside,
3575 ));
3576 assert_eq!(tree.active_overlays().len(), 1);
3577
3578 tree.attach_tooltip(anchor, tip, std::time::Duration::from_millis(100));
3580 tree.tooltip_pointer_enter(anchor);
3581 tree.advance_time(std::time::Duration::from_millis(150));
3582 assert_eq!(
3583 tree.active_overlays().len(),
3584 2,
3585 "tooltip sits above the menu"
3586 );
3587
3588 let dismissed = tree.overlay_manager.try_dismiss_top_on_escape();
3590 assert!(dismissed.is_some(), "Escape must dismiss the hover tooltip");
3591 assert_eq!(tree.active_overlays().len(), 1);
3592
3593 let dismissed = tree.overlay_manager.try_dismiss_top_on_escape();
3595 assert_eq!(
3596 dismissed.map(|(id, _, _)| id),
3597 Some(menu_overlay),
3598 "Escape must then reach the menu underneath"
3599 );
3600 assert!(tree.active_overlays().is_empty());
3601 }
3602
3603 #[test]
3604 fn tooltip_timer_restarts_when_pointer_keeps_moving() {
3605 let mut tree = WidgetTree::new();
3608 let anchor = tree.add(FillWidget::new());
3609 let tooltip = tree.add(FillWidget::new().label("Tip"));
3610 tree.layout(SizeProposal::exact(200.0, 100.0));
3611
3612 tree.attach_tooltip(anchor, tooltip, std::time::Duration::from_millis(500));
3613
3614 let start = tree.bounds(anchor).center();
3615 tree.pointer_move(start);
3616 tree.advance_time(std::time::Duration::from_millis(300));
3617 tree.pointer_move(Point::new(start.x + 20.0, start.y));
3619 tree.advance_time(std::time::Duration::from_millis(300));
3622 assert!(
3623 tree.active_overlays().is_empty(),
3624 "moving past stationary slop must restart the delay"
3625 );
3626 tree.advance_time(std::time::Duration::from_millis(250));
3627 assert_eq!(
3628 tree.active_overlays().len(),
3629 1,
3630 "tooltip appears after a full delay of stillness"
3631 );
3632 }
3633
3634 #[test]
3635 fn timed_overlay_auto_dismisses_after_duration() {
3636 let mut tree = WidgetTree::new();
3637 let anchor = tree.add(FillWidget::new());
3638 let content = tree.add(FillWidget::new().label("Toast"));
3639 tree.layout(SizeProposal::exact(200.0, 100.0));
3640
3641 tree.show_overlay_for(
3642 crate::overlay::OverlayRequest::new(
3643 content,
3644 anchor,
3645 crate::overlay::OverlayPlacement::Below,
3646 crate::overlay::DismissBehavior::Manual,
3647 ),
3648 std::time::Duration::from_millis(300),
3649 );
3650
3651 assert_eq!(tree.active_overlays().len(), 1);
3652
3653 tree.advance_time(std::time::Duration::from_millis(200));
3654 assert_eq!(tree.active_overlays().len(), 1);
3655
3656 tree.advance_time(std::time::Duration::from_millis(150));
3657 assert!(tree.active_overlays().is_empty());
3658 assert!(!tree.is_visible(content));
3659 }
3660
3661 #[test]
3662 fn fade_dismiss_keeps_overlay_off_active_list_immediately() {
3663 let mut tree = WidgetTree::new();
3671 let anchor = tree.add(FillWidget::new());
3672 let content = tree.add(FillWidget::new().label("Faded"));
3673 tree.layout(SizeProposal::exact(200.0, 100.0));
3674
3675 let id = tree.show_overlay(
3676 crate::overlay::OverlayRequest::new(
3677 content,
3678 anchor,
3679 crate::overlay::OverlayPlacement::Below,
3680 crate::overlay::DismissBehavior::Manual,
3681 )
3682 .with_fade(std::time::Duration::from_millis(100)),
3683 );
3684 assert_eq!(tree.active_overlays().len(), 1);
3685
3686 tree.dismiss_overlay(id);
3687 assert!(tree.active_overlays().is_empty());
3692 }
3693
3694 #[test]
3695 fn fade_dismiss_defers_content_dormancy_until_sim_tween_completes() {
3696 let mut tree = WidgetTree::new();
3702 let anchor = tree.add(FillWidget::new());
3703 let content = tree.add(FillWidget::new().label("Faded"));
3704 tree.layout(SizeProposal::exact(200.0, 100.0));
3705
3706 let id = tree.show_overlay(
3707 crate::overlay::OverlayRequest::new(
3708 content,
3709 anchor,
3710 crate::overlay::OverlayPlacement::Below,
3711 crate::overlay::DismissBehavior::Manual,
3712 )
3713 .with_fade(std::time::Duration::from_millis(100)),
3714 );
3715 tree.advance_time(std::time::Duration::from_secs(1));
3721
3722 tree.dismiss_overlay(id);
3723 assert!(
3724 tree.is_visible(content),
3725 "content stays active during fade-out"
3726 );
3727
3728 tree.advance_time(std::time::Duration::from_millis(60));
3732 assert!(
3733 tree.is_visible(content),
3734 "60 ms into a 100 ms tween the content is still up"
3735 );
3736
3737 tree.advance_time(std::time::Duration::from_millis(90));
3738 assert!(
3739 !tree.is_visible(content),
3740 "after sim-time past the tween window, deferred removal fires"
3741 );
3742 }
3743
3744 #[test]
3756 fn an_auto_dismissed_fade_starts_at_the_instant_the_dismiss_ran() {
3757 let mut tree = WidgetTree::new();
3758 let anchor = tree.add(FillWidget::new());
3759 let content = tree.add(FillWidget::new().label("Toast"));
3760 tree.layout(SizeProposal::exact(200.0, 100.0));
3761
3762 tree.show_overlay_for(
3763 crate::overlay::OverlayRequest::new(
3764 content,
3765 anchor,
3766 crate::overlay::OverlayPlacement::Below,
3767 crate::overlay::DismissBehavior::Manual,
3768 )
3769 .with_fade(std::time::Duration::from_millis(100)),
3770 std::time::Duration::from_millis(500),
3771 );
3772 assert_eq!(tree.active_overlays().len(), 1);
3773
3774 tree.advance_time(std::time::Duration::from_millis(600));
3778 assert!(
3779 tree.active_overlays().is_empty(),
3780 "precondition: the auto-dismiss fired inside this advance"
3781 );
3782 assert!(
3783 tree.is_visible(content),
3784 "the fade must start at the instant the dismiss ran, so the \
3785 content survives the frame that dismissed it"
3786 );
3787
3788 tree.advance_time(std::time::Duration::from_millis(150));
3789 assert!(
3790 !tree.is_visible(content),
3791 "and is reaped once the tween's own window has passed"
3792 );
3793 }
3794
3795 #[derive(Debug)]
3801 struct Block(f32, f32);
3802
3803 impl Widget for Block {
3804 fn layout_response(
3805 &self,
3806 _proposal: SizeProposal,
3807 _ctx: &LayoutContext,
3808 ) -> crate::widget::LayoutResponse {
3809 teksilo_canvas::Size::new(self.0, self.1).into()
3810 }
3811 }
3812
3813 fn panel_and_an_overlay_above(
3817 panel_dismiss: crate::overlay::DismissBehavior,
3818 top: impl Widget + 'static,
3819 top_dismiss: crate::overlay::DismissBehavior,
3820 top_at: Point,
3821 inert: bool,
3822 ) -> (WidgetTree, WidgetId, WidgetId) {
3823 let mut tree = WidgetTree::new();
3824 let panel = tree.add(Block(100.0, 60.0));
3825 let top = tree.add(top);
3826 let beneath = tree.add(FillWidget::new());
3830 tree.show_overlay(crate::overlay::OverlayRequest::new(
3831 panel,
3832 beneath,
3833 crate::overlay::OverlayPlacement::AtPointer(Point::new(20.0, 20.0)),
3834 panel_dismiss,
3835 ));
3836 let request = crate::overlay::OverlayRequest::new(
3837 top,
3838 beneath,
3839 crate::overlay::OverlayPlacement::AtPointer(top_at),
3840 top_dismiss,
3841 );
3842 tree.show_overlay(if inert { request.inert() } else { request });
3843 tree.layout(SizeProposal::exact(400.0, 200.0));
3844 (tree, panel, top)
3845 }
3846
3847 fn panel_and_an_inert_overlay(
3850 panel_dismiss: crate::overlay::DismissBehavior,
3851 inert_dismiss: crate::overlay::DismissBehavior,
3852 inert_at: Point,
3853 ) -> (WidgetTree, WidgetId, WidgetId) {
3854 panel_and_an_overlay_above(
3855 panel_dismiss,
3856 Block(60.0, 40.0),
3857 inert_dismiss,
3858 inert_at,
3859 true,
3860 )
3861 }
3862
3863 #[test]
3864 fn an_inert_overlay_hands_the_point_to_the_overlay_beneath_it() {
3865 let (tree, panel, inert) = panel_and_an_inert_overlay(
3866 crate::overlay::DismissBehavior::Manual,
3867 crate::overlay::DismissBehavior::EscapeKey,
3868 Point::new(30.0, 30.0),
3869 );
3870 let at = Point::new(40.0, 40.0);
3871 let over = tree.overlay_manager.find_by_content(inert).unwrap();
3872 assert!(
3873 tree.overlay_content_bounds(over).unwrap().contains(at),
3874 "fixture: the inert overlay covers the point"
3875 );
3876 assert_eq!(
3877 tree.hit_test(at),
3878 Some(panel),
3879 "the panel under it takes the point, not the window under both"
3880 );
3881 }
3882
3883 #[test]
3884 fn a_press_on_an_inert_overlay_is_outside_the_overlays_it_is_outside_of() {
3885 let (mut tree, panel, inert) = panel_and_an_inert_overlay(
3886 crate::overlay::DismissBehavior::ClickOutside,
3887 crate::overlay::DismissBehavior::EscapeKey,
3888 Point::new(200.0, 100.0),
3889 );
3890 let at = Point::new(220.0, 110.0);
3891 let over = tree.overlay_manager.find_by_content(inert).unwrap();
3892 assert!(
3893 tree.overlay_content_bounds(over).unwrap().contains(at),
3894 "fixture: the press lands on the inert overlay"
3895 );
3896 tree.pointer_down_button(at, crate::event::PointerButton::Primary);
3897 assert_eq!(
3898 tree.overlay_manager.find_by_content(panel),
3899 None,
3900 "the press was outside the panel, whatever floats over that spot",
3901 );
3902 }
3903
3904 #[test]
3905 fn an_inert_overlay_is_not_an_interactive_rect() {
3906 let (tree, panel, _inert) = panel_and_an_inert_overlay(
3909 crate::overlay::DismissBehavior::Manual,
3910 crate::overlay::DismissBehavior::EscapeKey,
3911 Point::new(200.0, 100.0),
3912 );
3913 let panel_bounds = tree
3914 .overlay_content_bounds(tree.overlay_manager.find_by_content(panel).unwrap())
3915 .unwrap();
3916 assert_eq!(tree.overlay_manager.interactive_rects(), vec![panel_bounds]);
3917 }
3918
3919 #[test]
3920 fn escape_steps_over_an_inert_overlay_to_the_one_beneath() {
3921 let (mut tree, panel, inert) = panel_and_an_inert_overlay(
3924 crate::overlay::DismissBehavior::EscapeKey,
3925 crate::overlay::DismissBehavior::Manual,
3926 Point::new(200.0, 100.0),
3927 );
3928 tree.press_key(Key::Escape, Modifiers::NONE);
3929 assert_eq!(
3930 tree.overlay_manager.find_by_content(panel),
3931 None,
3932 "the panel beneath took the Escape"
3933 );
3934 assert!(
3935 tree.overlay_manager.find_by_content(inert).is_some(),
3936 "and the inert one, which Escape may not close, is still up"
3937 );
3938 }
3939
3940 #[test]
3941 fn an_inert_overlay_over_the_window_hands_the_point_to_the_window() {
3942 let mut tree = WidgetTree::new();
3946 let page = tree.add(FillWidget::new());
3947 let inert = tree.add(Block(60.0, 40.0));
3948 tree.show_overlay(
3949 crate::overlay::OverlayRequest::new(
3950 inert,
3951 page,
3952 crate::overlay::OverlayPlacement::AtPointer(Point::new(200.0, 100.0)),
3953 crate::overlay::DismissBehavior::EscapeKey,
3954 )
3955 .inert(),
3956 );
3957 tree.layout(SizeProposal::exact(400.0, 200.0));
3958 let at = Point::new(220.0, 110.0);
3959 assert!(
3960 tree.overlay_content_bounds(tree.overlay_manager.find_by_content(inert).unwrap())
3961 .unwrap()
3962 .contains(at),
3963 "fixture: the inert overlay covers the point"
3964 );
3965 assert_eq!(tree.hit_test(at), Some(page));
3966 tree.pointer_move(at);
3967 assert_eq!(tree.hovered(), Some(page), "and so does a hover");
3968 }
3969
3970 #[test]
3971 fn a_root_that_is_hit_transparent_and_pass_through_is_not_inert() {
3972 use crate::widget_builder::WidgetBuilder;
3973
3974 let flagged = || {
3978 Block(60.0, 40.0)
3979 .hit_transparent(true)
3980 .event_pass_through(true)
3981 };
3982 let at = Point::new(220.0, 110.0);
3983
3984 let (mut tree, panel, top) = panel_and_an_overlay_above(
3985 crate::overlay::DismissBehavior::ClickOutside,
3986 flagged(),
3987 crate::overlay::DismissBehavior::Manual,
3988 Point::new(200.0, 100.0),
3989 false,
3990 );
3991 let over = tree.overlay_manager.find_by_content(top).unwrap();
3992 assert!(
3993 tree.overlay_content_bounds(over).unwrap().contains(at),
3994 "fixture: the press lands on the flagged overlay"
3995 );
3996 assert_eq!(
3997 tree.overlay_manager.hit_test(at),
3998 Some(over),
3999 "the stack still answers with the overlay"
4000 );
4001 assert_eq!(
4002 tree.overlay_manager.interactive_rects().len(),
4003 2,
4004 "which is an interactive rect"
4005 );
4006 tree.pointer_down_button(at, crate::event::PointerButton::Primary);
4007 tree.pointer_up_button(at, crate::event::PointerButton::Primary);
4008 assert!(
4009 tree.overlay_manager.find_by_content(panel).is_some(),
4010 "a press on it is inside the stack, so the panel beneath stays"
4011 );
4012
4013 let (mut tree, panel, top) = panel_and_an_overlay_above(
4014 crate::overlay::DismissBehavior::EscapeKey,
4015 flagged(),
4016 crate::overlay::DismissBehavior::Manual,
4017 Point::new(200.0, 100.0),
4018 false,
4019 );
4020 tree.press_key(Key::Escape, Modifiers::NONE);
4021 assert!(
4022 tree.overlay_manager.find_by_content(panel).is_some()
4023 && tree.overlay_manager.find_by_content(top).is_some(),
4024 "a `Manual` overlay owns the Escape: nothing beneath it closes"
4025 );
4026
4027 let (mut tree, panel, top) = panel_and_an_overlay_above(
4028 crate::overlay::DismissBehavior::EscapeKey,
4029 flagged(),
4030 crate::overlay::DismissBehavior::EscapeKey,
4031 Point::new(200.0, 100.0),
4032 false,
4033 );
4034 tree.press_key(Key::Escape, Modifiers::NONE);
4035 assert_eq!(
4036 tree.overlay_manager.find_by_content(top),
4037 None,
4038 "an `EscapeKey` one is what the Escape closes"
4039 );
4040 assert!(
4041 tree.overlay_manager.find_by_content(panel).is_some(),
4042 "and it is spent there"
4043 );
4044 }
4045
4046 #[test]
4047 fn an_inert_overlay_records_no_focus_to_give_back_on_any_show_path() {
4048 type Show = fn(&mut WidgetTree, crate::overlay::OverlayRequest, WidgetId);
4049 fn from_a_handler(
4050 tree: &mut WidgetTree,
4051 show: impl FnOnce(&mut crate::widget::EventContext),
4052 ) {
4053 tree.run_with_event_context(&mut crate::window::NoopWindowOps, show);
4054 }
4055 let paths: [(&str, Show); 5] = [
4056 ("show_overlay", |tree, request, _| {
4057 from_a_handler(tree, |ctx| ctx.show_overlay(request))
4058 }),
4059 ("show_overlay_for", |tree, request, _| {
4060 from_a_handler(tree, |ctx| {
4061 ctx.show_overlay_for(request, std::time::Duration::from_secs(60))
4062 })
4063 }),
4064 ("show_overlay_with_reveal", |tree, request, _| {
4065 from_a_handler(tree, |ctx| {
4066 ctx.show_overlay_with_reveal(
4067 request,
4068 crate::signal::Signal::new(0.0),
4069 std::time::Duration::from_millis(100),
4070 )
4071 })
4072 }),
4073 ("show_overlay_after", |tree, request, _| {
4074 from_a_handler(tree, |ctx| {
4075 ctx.show_overlay_after(request, std::time::Duration::ZERO)
4076 });
4077 tree.layout(SizeProposal::exact(400.0, 200.0));
4078 }),
4079 ("show_overlay_from_source", |tree, request, field| {
4080 tree.show_overlay_from_source(field, request);
4081 }),
4082 ];
4083 for (path, show) in paths {
4084 let mut tree = WidgetTree::new();
4085 let field = tree.add(FillWidget::new().focusable());
4086 let inert = tree.add(Block(60.0, 40.0));
4087 let ordinary = tree.add(Block(60.0, 40.0));
4088 tree.layout(SizeProposal::exact(400.0, 200.0));
4089 tree.focus(field);
4090 for (content, inert) in [(inert, true), (ordinary, false)] {
4091 let request = crate::overlay::OverlayRequest::new(
4092 content,
4093 field,
4094 crate::overlay::OverlayPlacement::AtPointer(Point::new(20.0, 20.0)),
4095 crate::overlay::DismissBehavior::EscapeKey,
4096 );
4097 show(
4098 &mut tree,
4099 if inert { request.inert() } else { request },
4100 field,
4101 );
4102 }
4103 let restore = |content: WidgetId| {
4104 tree.overlay_manager
4105 .stack
4106 .iter()
4107 .find(|o| o.content_id == content)
4108 .map(|o| o.focus_restore)
4109 };
4110 assert_eq!(
4111 restore(ordinary),
4112 Some(Some(field)),
4113 "{path}: an ordinary one does"
4114 );
4115 assert_eq!(
4116 restore(inert),
4117 Some(None),
4118 "{path}: an inert one took no focus"
4119 );
4120 }
4121 }
4122}