1use super::*;
5
6use crate::accessibility::AccessNodeBuilder;
7
8impl WidgetTree {
9 pub fn attach_tooltip(
12 &mut self,
13 anchor_id: WidgetId,
14 content_id: WidgetId,
15 delay: std::time::Duration,
16 ) {
17 self.attach_tooltip_inner(
18 anchor_id,
19 content_id,
20 delay,
21 None,
22 None,
23 crate::overlay::TooltipPlacement::Below,
24 );
25 }
26
27 pub fn attach_tooltip_with_placement(
32 &mut self,
33 anchor_id: WidgetId,
34 content_id: WidgetId,
35 delay: std::time::Duration,
36 placement: crate::overlay::TooltipPlacement,
37 ) {
38 self.attach_tooltip_inner(anchor_id, content_id, delay, None, None, placement);
39 }
40
41 pub fn attach_tooltip_with_sticky(
53 &mut self,
54 anchor_id: WidgetId,
55 content_id: WidgetId,
56 delay: std::time::Duration,
57 sticky_after: Option<std::time::Duration>,
58 ) {
59 self.attach_tooltip_inner(
60 anchor_id,
61 content_id,
62 delay,
63 sticky_after,
64 None,
65 crate::overlay::TooltipPlacement::Below,
66 );
67 }
68
69 pub fn attach_tooltip_with_sticky_sink(
75 &mut self,
76 anchor_id: WidgetId,
77 content_id: WidgetId,
78 delay: std::time::Duration,
79 sticky_after: Option<std::time::Duration>,
80 shown_at_sink: std::rc::Rc<std::cell::Cell<Option<std::time::Instant>>>,
81 ) {
82 self.attach_tooltip_inner(
83 anchor_id,
84 content_id,
85 delay,
86 sticky_after,
87 Some(shown_at_sink),
88 crate::overlay::TooltipPlacement::Below,
89 );
90 }
91
92 pub fn attach_tooltip_with_sticky_sink_placement(
97 &mut self,
98 anchor_id: WidgetId,
99 content_id: WidgetId,
100 delay: std::time::Duration,
101 sticky_after: Option<std::time::Duration>,
102 shown_at_sink: std::rc::Rc<std::cell::Cell<Option<std::time::Instant>>>,
103 placement: crate::overlay::TooltipPlacement,
104 ) {
105 self.attach_tooltip_inner(
106 anchor_id,
107 content_id,
108 delay,
109 sticky_after,
110 Some(shown_at_sink),
111 placement,
112 );
113 }
114
115 fn retire_tooltips_for_anchor(&mut self, anchor_id: WidgetId, keep_content_id: WidgetId) {
130 let stale: Vec<(WidgetId, Option<crate::overlay::OverlayId>)> = self
131 .tooltips
132 .iter()
133 .filter(|entry| entry.anchor_id == anchor_id && entry.content_id != keep_content_id)
134 .map(|entry| (entry.content_id, entry.overlay_id))
135 .collect();
136 if stale.is_empty() {
137 return;
138 }
139 self.tooltips
140 .retain(|entry| entry.anchor_id != anchor_id || entry.content_id == keep_content_id);
141 for (content_id, overlay_id) in stale {
142 if let Some(overlay_id) = overlay_id {
146 self.dismiss_overlay(overlay_id);
147 }
148 self.destroy_subtree(content_id);
149 }
150 }
151
152 pub(super) fn retire_tooltips_of_destroyed_anchor(&mut self, anchor_id: WidgetId) {
159 let stale: Vec<(WidgetId, Option<crate::overlay::OverlayId>)> = self
160 .tooltips
161 .iter()
162 .filter(|entry| entry.anchor_id == anchor_id)
163 .map(|entry| (entry.content_id, entry.overlay_id))
164 .collect();
165 if stale.is_empty() {
166 return;
167 }
168 self.tooltips.retain(|entry| entry.anchor_id != anchor_id);
169 for (content_id, overlay_id) in stale {
170 if let Some(overlay_id) = overlay_id {
171 self.dismiss_overlay(overlay_id);
172 }
173 self.destroy_subtree(content_id);
174 }
175 }
176
177 fn attach_tooltip_inner(
178 &mut self,
179 anchor_id: WidgetId,
180 content_id: WidgetId,
181 delay: std::time::Duration,
182 sticky_after: Option<std::time::Duration>,
183 shown_at_sink: Option<std::rc::Rc<std::cell::Cell<Option<std::time::Instant>>>>,
184 placement: crate::overlay::TooltipPlacement,
185 ) {
186 self.retire_tooltips_for_anchor(anchor_id, content_id);
187 if let Some(entry) = self
190 .tooltips
191 .iter_mut()
192 .find(|entry| entry.anchor_id == anchor_id && entry.content_id == content_id)
193 {
194 entry.delay = delay;
195 entry.sticky_after = sticky_after;
196 entry.placement = placement;
197 if shown_at_sink.is_some() {
198 entry.shown_at_sink = shown_at_sink;
199 }
200 return;
201 }
202 self.arena.set_dormant(content_id);
203 self.tooltips.push(TooltipEntry {
204 anchor_id,
205 content_id,
206 description_owner_id: anchor_id,
210 delay,
211 hover_start: None,
212 real_hover_start: None,
213 hover_origin: None,
214 overlay_id: None,
215 sticky_after,
216 is_sticky: false,
217 shown_at_sim: None,
218 shown_at_real: None,
219 shown_at_sink,
220 promoted_by_focus: false,
221 armed_by_focus: false,
222 armed_by_hold: false,
223 suppressed_until_focus_leaves: false,
224 placement,
225 });
226 }
227
228 pub(crate) fn set_tooltip_description_owner(
247 &mut self,
248 anchor_id: WidgetId,
249 owner_id: WidgetId,
250 ) {
251 if let Some(entry) = self
252 .tooltips
253 .iter_mut()
254 .find(|entry| entry.anchor_id == anchor_id)
255 {
256 entry.description_owner_id = owner_id;
257 }
258 }
259
260 fn tooltip_overlay_placement(
267 placement: crate::overlay::TooltipPlacement,
268 ) -> crate::overlay::OverlayPlacement {
269 match placement {
270 crate::overlay::TooltipPlacement::Below => {
271 crate::overlay::OverlayPlacement::NearAnchor {
272 offset: teksilo_canvas::Vec2::new(0.0, 8.0),
273 }
274 }
275 crate::overlay::TooltipPlacement::Side => {
276 crate::overlay::OverlayPlacement::TrailingEdge
277 }
278 }
279 }
280
281 pub(super) fn process_tooltips(&mut self) {
282 let sim_now = self.sim_clock;
283 let session_active = self.tooltip_session_active_sim(sim_now);
284 self.process_tooltips_impl(
285 |entry| {
286 entry
287 .hover_start
288 .map(|start| sim_now.saturating_duration_since(start))
289 },
290 session_active,
291 true,
292 );
293 }
294
295 pub(super) fn process_tooltips_real(&mut self) {
296 let real_now = std::time::Instant::now();
297 let session_active = self.tooltip_session_active_real(real_now);
298 self.process_tooltips_impl(
299 |entry| {
300 entry
301 .real_hover_start
302 .map(|start| real_now.saturating_duration_since(start))
303 },
304 session_active,
305 false,
306 );
307 }
308
309 fn any_tooltip_in_session(&self) -> bool {
318 self.tooltips
319 .iter()
320 .any(|e| e.overlay_id.is_some() && !e.is_sticky)
321 }
322
323 fn tooltip_session_active_sim(&self, now: std::time::Instant) -> bool {
325 self.any_tooltip_in_session()
326 || self
327 .tooltip_session_until_sim
328 .is_some_and(|until| now < until)
329 }
330
331 fn tooltip_session_active_real(&self, now: std::time::Instant) -> bool {
333 self.any_tooltip_in_session()
334 || self
335 .tooltip_session_until_real
336 .is_some_and(|until| now < until)
337 }
338
339 fn effective_tooltip_delay(
353 &self,
354 entry_delay: std::time::Duration,
355 session_active: bool,
356 ) -> std::time::Duration {
357 if !session_active {
358 return entry_delay;
359 }
360 let motion = &self.theme.motion;
361 let base = motion.tooltip_delay.as_secs_f64();
362 if base <= 0.0 {
363 return entry_delay.min(motion.tooltip_reshow_delay);
366 }
367 let ratio = motion.tooltip_reshow_delay.as_secs_f64() / base;
368 entry_delay.mul_f64(ratio).min(entry_delay)
370 }
371
372 fn process_tooltips_impl(
373 &mut self,
374 elapsed_fn: impl Fn(&TooltipEntry) -> Option<std::time::Duration>,
375 session_active: bool,
376 use_sim_clock: bool,
377 ) {
378 let dismissed_indices: Vec<usize> = self
384 .tooltips
385 .iter()
386 .enumerate()
387 .filter_map(|(i, e)| match e.overlay_id {
388 Some(oid) if !self.overlay_manager.stack.iter().any(|o| o.id == oid) => Some(i),
389 _ => None,
390 })
391 .collect();
392 let any_dismissed = !dismissed_indices.is_empty();
393 for i in dismissed_indices {
394 let e = &mut self.tooltips[i];
395 e.overlay_id = None;
396 e.is_sticky = false;
397 e.promoted_by_focus = false;
398 e.armed_by_hold = false;
399 e.shown_at_sim = None;
400 e.shown_at_real = None;
401 e.hover_origin = None;
402 if let Some(sink) = e.shown_at_sink.as_ref() {
403 sink.set(None);
404 }
405 }
406 if any_dismissed {
409 let grace = super::TOOLTIP_SESSION_GRACE;
410 if use_sim_clock {
411 self.tooltip_session_until_sim = Some(self.sim_clock + grace);
412 } else {
413 self.tooltip_session_until_real = Some(std::time::Instant::now() + grace);
414 }
415 }
416
417 let session_active = session_active
422 || self.any_tooltip_in_session()
423 || if use_sim_clock {
424 self.tooltip_session_active_sim(self.sim_clock)
425 } else {
426 self.tooltip_session_active_real(std::time::Instant::now())
427 };
428
429 let mut to_show = Vec::new();
430 let pending: Vec<usize> = self
433 .tooltips
434 .iter()
435 .enumerate()
436 .filter(|(_, entry)| entry.overlay_id.is_none())
437 .filter(|(_, entry)| self.arena.is_active(entry.anchor_id))
438 .filter_map(|(i, entry)| {
439 let delay = self.effective_tooltip_delay(entry.delay, session_active);
440 let elapsed = elapsed_fn(entry)?;
441 (elapsed >= delay).then_some(i)
442 })
443 .collect();
444 for index in pending {
445 let content_id = self.tooltips[index].content_id;
450 self.materialize_deferred(content_id);
451 let body_id = self.tooltip_content_node(content_id);
462 if !self
463 .arena
464 .get(body_id)
465 .is_none_or(|node| node.widget.tooltip_has_content())
466 {
467 let entry = &mut self.tooltips[index];
468 entry.hover_start = None;
469 entry.real_hover_start = None;
470 entry.hover_origin = None;
471 continue;
472 }
473 let entry = &mut self.tooltips[index];
474 to_show.push((
475 entry.anchor_id,
476 entry.content_id,
477 entry.placement,
478 entry.armed_by_focus,
479 entry.armed_by_hold,
480 ));
481 entry.hover_start = None;
482 entry.real_hover_start = None;
483 entry.hover_origin = None;
484 }
485 let sim_now = self.sim_clock;
486 let real_now = std::time::Instant::now();
487 let fade_duration = if self.prefers_reduced_motion || session_active {
494 None
495 } else {
496 Some(self.theme.motion.duration_fast)
497 };
498 for (anchor_id, content_id, placement, by_focus, by_hold) in to_show {
499 self.arena.activate(content_id);
500 let dismiss = if by_focus || by_hold {
509 crate::overlay::DismissBehavior::EscapeOrClickOutside
510 } else {
511 crate::overlay::DismissBehavior::PointerLeave {
512 delay: std::time::Duration::from_millis(100),
513 }
514 };
515 let mut request = crate::overlay::OverlayRequest::new(
516 content_id,
517 anchor_id,
518 Self::tooltip_overlay_placement(placement),
519 dismiss,
520 );
521 request.fade_duration = fade_duration;
522 let oid = if by_hold {
523 self.show_overlay_for(request, super::touch_route::TOUCH_TOOLTIP_DISMISS)
524 } else {
525 self.show_overlay(request)
526 };
527 if by_focus && let Some(focused) = self.focused {
528 self.overlay_manager.set_top_focus_restore(focused);
529 }
530 if let Some(entry) = self
531 .tooltips
532 .iter_mut()
533 .find(|e| e.content_id == content_id)
534 {
535 entry.overlay_id = Some(oid);
536 entry.shown_at_sim = Some(sim_now);
537 entry.shown_at_real = Some(real_now);
538 entry.promoted_by_focus = by_focus;
539 if let Some(sink) = entry.shown_at_sink.as_ref() {
540 sink.set(Some(real_now));
541 }
542 }
543 }
544
545 let mut to_mark_paint: Vec<WidgetId> = Vec::new();
559 let mut to_promote: Vec<WidgetId> = Vec::new();
560 for entry in &self.tooltips {
561 let Some(sticky_after) = entry.sticky_after else {
562 continue;
563 };
564 if entry.overlay_id.is_none() || entry.is_sticky {
565 continue;
566 }
567 let elapsed = entry
568 .shown_at_real
569 .map(|t| real_now.saturating_duration_since(t));
570 let elapsed = match elapsed {
571 Some(e) => e,
572 None => continue,
573 };
574 to_mark_paint.push(entry.content_id);
578 if elapsed >= sticky_after {
579 to_promote.push(entry.content_id);
580 }
581 }
582 for id in to_mark_paint {
583 self.arena.mark_subtree_needs_paint(id);
584 }
585 for content_id in to_promote {
586 self.promote_tooltip_to_sticky(content_id);
587 }
588 }
589
590 pub(super) fn process_delayed_overlays(&mut self) {
591 let sim_now = self.sim_clock;
592 let mut noop = crate::window::NoopWindowOps;
593 self.process_delayed_overlays_impl(
594 |p| sim_now.saturating_duration_since(p.sim_requested_at),
595 &mut noop,
596 );
597 }
598
599 pub(super) fn process_delayed_overlays_real(&mut self, ops: &mut dyn crate::window::WindowOps) {
600 let real_now = std::time::Instant::now();
601 self.process_delayed_overlays_impl(
602 |p| real_now.saturating_duration_since(p.real_requested_at),
603 &mut *ops,
604 );
605 }
606
607 fn process_delayed_overlays_impl(
608 &mut self,
609 elapsed_fn: impl Fn(&PendingDelayedOverlay) -> std::time::Duration,
610 ops: &mut dyn crate::window::WindowOps,
611 ) {
612 let mut ready_indices = Vec::new();
613 for (index, pending) in self.pending_delayed_overlays.iter().enumerate() {
614 if elapsed_fn(pending) >= pending.delay {
615 ready_indices.push(index);
616 }
617 }
618
619 let mut ready = Vec::new();
620 for &index in ready_indices.iter().rev() {
621 ready.push(self.pending_delayed_overlays.remove(index));
622 }
623 ready.reverse();
624
625 let any_shown = !ready.is_empty();
626 for pending in ready {
627 let content_id = pending.request.content_id;
628 if pending.replace_siblings {
635 let anchor = pending.request.anchor;
636 self.dismiss_child_overlays_for_source(anchor, Some(content_id), &mut *ops);
637 }
638 self.arena.activate(content_id);
639 let current_focus = self.focused;
640 self.overlay_manager.show(pending.request);
641 if let Some(focus_id) = current_focus {
642 self.overlay_manager.set_top_focus_restore(focus_id);
643 }
644 self.arena.mark_needs_paint(content_id);
645 if let Some(focus_target) = pending.focus_target
646 && self.arena.is_active(focus_target)
647 {
648 self.focus_ops(focus_target, &mut *ops);
649 }
650 }
651 if any_shown {
652 self.a11y_dirty = true;
653 }
654 }
655
656 pub(super) fn overlay_ancestor_for_widget(
657 &self,
658 widget_id: WidgetId,
659 ) -> Option<crate::overlay::OverlayId> {
660 self.overlay_manager
661 .stack
662 .iter()
663 .rev()
664 .find(|overlay| self.is_descendant_of(widget_id, overlay.content_id))
665 .map(|overlay| overlay.id)
666 }
667
668 pub(super) fn modal_overlay_for_widget(
669 &self,
670 widget_id: WidgetId,
671 ) -> Option<crate::overlay::OverlayId> {
672 let mut current = self.overlay_ancestor_for_widget(widget_id);
673
674 while let Some(overlay_id) = current {
675 let overlay = self.overlay_manager.overlay(overlay_id)?;
676 if overlay.placement.is_modal() {
677 return Some(overlay_id);
678 }
679 current = overlay.parent_overlay;
680 }
681
682 None
683 }
684
685 pub fn is_behind_modal(&self, widget_id: WidgetId) -> bool {
713 let Some(modal) = self.overlay_manager.topmost_modal() else {
714 return false;
715 };
716 let stack = &self.overlay_manager.stack;
717 let Some(at) = stack.iter().position(|overlay| overlay.id == modal.id) else {
718 return false;
719 };
720 let (below, above) = stack.split_at(at);
721 let in_modal = |id: WidgetId| {
722 above
723 .iter()
724 .any(|overlay| self.is_descendant_of(id, overlay.content_id))
725 };
726 !(in_modal(widget_id)
727 || below.iter().any(|overlay| {
728 self.is_descendant_of(widget_id, overlay.content_id) && in_modal(overlay.anchor)
729 }))
730 }
731
732 fn menu_ancestor_for_widget(&self, widget_id: WidgetId) -> Option<WidgetId> {
733 let mut current = Some(widget_id);
734 while let Some(id) = current {
735 if let Some(node) = self.arena.get(id) {
736 let mut builder = AccessNodeBuilder::new();
737 node.widget.accessibility(&mut builder);
738 if builder.role() == accesskit::Role::Menu {
739 return Some(id);
740 }
741 }
742 current = self.arena.parent(id);
743 }
744 None
745 }
746
747 pub(super) fn dismiss_child_overlays_for_source(
748 &mut self,
749 source_widget: WidgetId,
750 preserve_content: Option<WidgetId>,
751 ops: &mut dyn crate::window::WindowOps,
752 ) {
753 if let Some(parent_overlay) = self.overlay_ancestor_for_widget(source_widget) {
754 let preserve_overlay = preserve_content
755 .and_then(|content_id| self.overlay_manager.find_by_content(content_id));
756 let (dismissed, focus_restore) = self
757 .overlay_manager
758 .dismiss_descendants_of(parent_overlay, preserve_overlay);
759 self.dormant_dismissed_content(&dismissed, &mut *ops);
760 if let Some(restore_id) = focus_restore
761 && self.arena.is_active(restore_id)
762 {
763 self.focus_ops(restore_id, &mut *ops);
764 }
765 return;
766 }
767
768 let Some(menu_root) = self.menu_ancestor_for_widget(source_widget) else {
769 return;
770 };
771 let preserve_overlay = preserve_content
772 .and_then(|content_id| self.overlay_manager.find_by_content(content_id));
773 let overlay_ids: Vec<crate::overlay::OverlayId> = self
774 .overlay_manager
775 .stack
776 .iter()
777 .filter(|overlay| {
778 overlay.parent_overlay.is_none()
779 && self.is_descendant_of(overlay.anchor, menu_root)
780 && !preserve_overlay.is_some_and(|keep| {
781 overlay.id == keep
782 || self.overlay_manager.is_descendant_of(overlay.id, keep)
783 })
784 })
785 .map(|overlay| overlay.id)
786 .collect();
787
788 for overlay_id in overlay_ids {
789 let (dismissed, focus_restore) =
790 self.overlay_manager.dismiss_with_focus_restore(overlay_id);
791 self.dormant_dismissed_content(&dismissed, &mut *ops);
792 if let Some(restore_id) = focus_restore
793 && self.arena.is_active(restore_id)
794 {
795 self.focus_ops(restore_id, &mut *ops);
796 }
797 }
798 }
799
800 pub(super) fn dismiss_self_overlay_chain_for_source(
812 &mut self,
813 source_widget: WidgetId,
814 ops: &mut dyn crate::window::WindowOps,
815 ) {
816 let Some(start) = self.overlay_ancestor_for_widget(source_widget) else {
817 return;
818 };
819
820 let mut topmost_to_dismiss: Option<crate::overlay::OverlayId> = None;
821 let mut current = Some(start);
822 while let Some(overlay_id) = current {
823 let Some(overlay) = self.overlay_manager.overlay(overlay_id) else {
824 break;
825 };
826 if self.overlay_is_host_surface(overlay_id) {
827 break;
828 }
829 topmost_to_dismiss = Some(overlay_id);
830 current = overlay.parent_overlay;
831 }
832
833 if let Some(target) = topmost_to_dismiss {
834 let (dismissed, focus_restore) =
835 self.overlay_manager.dismiss_with_focus_restore(target);
836 self.dormant_dismissed_content(&dismissed, &mut *ops);
837 if let Some(restore_id) = focus_restore
838 && self.arena.is_active(restore_id)
839 {
840 self.focus_ops(restore_id, &mut *ops);
841 }
842 }
843 }
844
845 fn overlay_is_anchor_positioned(placement: &crate::overlay::OverlayPlacement) -> bool {
862 use crate::overlay::OverlayPlacement as P;
863 match placement {
864 P::Below
865 | P::Above
866 | P::TrailingEdge
867 | P::AtPointer(_)
868 | P::AtPointerAvoiding { .. }
869 | P::NearAnchor { .. }
870 | P::BelowPreferred => true,
871 P::Centered
877 | P::TopCenter { .. }
878 | P::BottomCenter
879 | P::ViewportCorner { .. }
880 | P::FullViewport
881 | P::AboveSelection { .. }
882 | P::BelowSelection { .. } => false,
883 }
884 }
885
886 fn overlay_follows_focus_out(&self, overlay_id: crate::overlay::OverlayId) -> bool {
926 let Some(overlay) = self.overlay_manager.overlay(overlay_id) else {
927 return false;
928 };
929 if !Self::overlay_is_anchor_positioned(&overlay.placement) {
930 return false;
931 }
932 if overlay.is_dismissing() {
933 return false;
934 }
935 if overlay.inert {
936 return false;
937 }
938 !self
939 .tooltips
940 .iter()
941 .any(|entry| entry.content_id == overlay.content_id)
942 }
943
944 fn overlay_orbit_for_widget(&self, widget_id: WidgetId) -> Option<crate::overlay::OverlayId> {
967 self.overlay_manager
968 .stack
969 .iter()
970 .rev()
971 .find(|overlay| {
972 self.is_descendant_of(widget_id, overlay.content_id)
973 || (self.overlay_follows_focus_out(overlay.id)
974 && self.is_descendant_of(widget_id, overlay.anchor))
975 })
976 .map(|overlay| overlay.id)
977 }
978
979 pub(super) fn dismiss_overlays_left_by_focus(
1006 &mut self,
1007 old: Option<WidgetId>,
1008 new_focus: Option<WidgetId>,
1009 ops: &mut dyn crate::window::WindowOps,
1010 ) {
1011 let Some(old) = old else {
1012 return;
1013 };
1014 let Some(old_overlay) = self.overlay_orbit_for_widget(old) else {
1015 return;
1016 };
1017 let new_overlay = new_focus.and_then(|id| self.overlay_ancestor_for_widget(id));
1025
1026 if let Some(new_overlay) = new_overlay
1029 && (new_overlay == old_overlay
1030 || self
1031 .overlay_manager
1032 .is_descendant_of(new_overlay, old_overlay))
1033 {
1034 return;
1035 }
1036
1037 if !self.overlay_follows_focus_out(old_overlay) {
1038 return;
1039 }
1040
1041 let mut topmost = old_overlay;
1042 let mut current = self
1043 .overlay_manager
1044 .overlay(old_overlay)
1045 .and_then(|overlay| overlay.parent_overlay);
1046 while let Some(overlay_id) = current {
1047 if Some(overlay_id) == new_overlay {
1051 break;
1052 }
1053 if self.overlay_is_host_surface(overlay_id) {
1056 break;
1057 }
1058 if !self.overlay_follows_focus_out(overlay_id) {
1059 break;
1060 }
1061 topmost = overlay_id;
1062 current = self
1063 .overlay_manager
1064 .overlay(overlay_id)
1065 .and_then(|overlay| overlay.parent_overlay);
1066 }
1067
1068 let dismissed = self.overlay_manager.dismiss(topmost);
1069 self.dormant_dismissed_content(&dismissed, &mut *ops);
1070 }
1071
1072 pub(super) fn dismiss_all_overlays_except_hosts(
1086 &mut self,
1087 ops: &mut dyn crate::window::WindowOps,
1088 ) {
1089 let to_dismiss: Vec<crate::overlay::OverlayId> = self
1090 .overlay_manager
1091 .stack
1092 .iter()
1093 .map(|overlay| overlay.id)
1094 .filter(|&id| !self.overlay_is_host_surface(id))
1095 .collect();
1096
1097 let focus_before = self.focused;
1103 let mut restore = None;
1104 for overlay_id in to_dismiss {
1105 let (dismissed, focus_restore) =
1106 self.overlay_manager.dismiss_with_focus_restore(overlay_id);
1107 self.dormant_dismissed_content(&dismissed, &mut *ops);
1108 restore = restore.or(focus_restore);
1109 }
1110 if focus_before.is_some()
1111 && self.focused.is_none()
1112 && let Some(restore_id) = restore
1113 && self.arena.is_active(restore_id)
1114 {
1115 self.focus_ops(restore_id, &mut *ops);
1116 }
1117 }
1118
1119 pub(super) fn overlay_is_host_surface(&self, overlay_id: crate::overlay::OverlayId) -> bool {
1125 let Some(overlay) = self.overlay_manager.overlay(overlay_id) else {
1126 return false;
1127 };
1128 if overlay.placement.is_modal() {
1137 return true;
1138 }
1139 let Some(node) = self.arena.get(overlay.content_id) else {
1140 return false;
1141 };
1142 let role = node
1150 .access_overrides
1151 .as_ref()
1152 .and_then(|o| o.role)
1153 .unwrap_or_else(|| {
1154 let mut builder = AccessNodeBuilder::new();
1155 node.widget.accessibility(&mut builder);
1156 builder.role()
1157 });
1158 matches!(
1159 role,
1160 accesskit::Role::Tooltip
1161 | accesskit::Role::Dialog
1162 | accesskit::Role::AlertDialog
1163 | accesskit::Role::MenuBar
1164 )
1165 }
1166
1167 pub(super) fn dismiss_modal_for_source(
1168 &mut self,
1169 source_widget: WidgetId,
1170 ops: &mut dyn crate::window::WindowOps,
1171 ) -> bool {
1172 let Some(modal_overlay) = self.modal_overlay_for_widget(source_widget) else {
1173 return false;
1174 };
1175
1176 let (dismissed, focus_restore) = self
1177 .overlay_manager
1178 .dismiss_with_focus_restore(modal_overlay);
1179 self.dormant_dismissed_content(&dismissed, &mut *ops);
1180 if let Some(restore_id) = focus_restore
1181 && self.arena.is_active(restore_id)
1182 {
1183 self.focus_ops(restore_id, &mut *ops);
1184 }
1185 true
1186 }
1187
1188 pub fn is_descendant_of(&self, widget_id: WidgetId, ancestor: WidgetId) -> bool {
1189 if widget_id == ancestor {
1190 return true;
1191 }
1192 let mut current = self.arena.parent(widget_id);
1193 while let Some(parent_id) = current {
1194 if parent_id == ancestor {
1195 return true;
1196 }
1197 current = self.arena.parent(parent_id);
1198 }
1199 false
1200 }
1201
1202 fn is_overlay_content_root(&self, widget_id: WidgetId) -> bool {
1209 self.overlay_manager.find_by_content(widget_id).is_some()
1210 }
1211
1212 pub(super) fn tooltip_focus_enter(&mut self, widget_id: WidgetId) {
1243 type ToShow = (WidgetId, WidgetId, crate::overlay::TooltipPlacement);
1266 let mut direct: Vec<ToShow> = Vec::new();
1267 let mut reverse: Vec<ToShow> = Vec::new();
1268 for e in &self.tooltips {
1269 if e.sticky_after.is_none() || e.overlay_id.is_some() || e.suppressed_until_focus_leaves
1270 {
1271 continue;
1272 }
1273 if self.is_descendant_of(widget_id, e.anchor_id) {
1274 direct.push((e.anchor_id, e.content_id, e.placement));
1275 } else if self.is_descendant_of(e.anchor_id, widget_id) {
1276 reverse.push((e.anchor_id, e.content_id, e.placement));
1277 }
1278 }
1279 let mut to_show = direct;
1280 if reverse.len() == 1 && !self.is_overlay_content_root(widget_id) {
1284 to_show.extend(reverse);
1285 }
1286
1287 let sim_now = self.sim_clock;
1296 let real_now = std::time::Instant::now();
1297 for (_anchor_id, content_id, _placement) in to_show {
1298 if let Some(entry) = self
1299 .tooltips
1300 .iter_mut()
1301 .find(|e| e.content_id == content_id)
1302 {
1303 entry.hover_start = Some(sim_now);
1304 entry.real_hover_start = Some(real_now);
1305 entry.hover_origin = None;
1306 entry.armed_by_focus = true;
1307 entry.armed_by_hold = false;
1308 }
1309 }
1310 }
1311
1312 pub(super) fn show_highlight_tooltip(
1322 &mut self,
1323 item_id: WidgetId,
1324 ops: &mut dyn crate::window::WindowOps,
1325 ) {
1326 self.clear_highlight_tooltip(&mut *ops);
1329
1330 let found = self.tooltips.iter().find_map(|e| {
1334 if e.overlay_id.is_none() && self.is_descendant_of(e.anchor_id, item_id) {
1335 Some((e.anchor_id, e.content_id, e.placement))
1336 } else {
1337 None
1338 }
1339 });
1340 let Some((anchor_id, content_id, placement)) = found else {
1341 return;
1342 };
1343
1344 let parent_overlay = self.overlay_ancestor_for_widget(item_id);
1345 self.arena.activate(content_id);
1346 let fade_duration = if self.prefers_reduced_motion {
1347 None
1348 } else {
1349 Some(self.theme.motion.duration_fast)
1350 };
1351 let mut request = crate::overlay::OverlayRequest::new(
1352 content_id,
1353 anchor_id,
1354 Self::tooltip_overlay_placement(placement),
1355 crate::overlay::DismissBehavior::Manual,
1356 );
1357 request.parent_overlay = parent_overlay;
1358 request.fade_duration = fade_duration;
1359 let oid = self.show_overlay(request);
1360 let real_now = std::time::Instant::now();
1361 let sim_now = self.sim_clock;
1362 if let Some(entry) = self
1363 .tooltips
1364 .iter_mut()
1365 .find(|e| e.content_id == content_id)
1366 {
1367 entry.overlay_id = Some(oid);
1368 entry.shown_at_sim = Some(sim_now);
1369 entry.shown_at_real = Some(real_now);
1370 if let Some(sink) = entry.shown_at_sink.as_ref() {
1371 sink.set(Some(real_now));
1372 }
1373 }
1374 self.highlight_tooltip = Some((oid, content_id));
1375 }
1376
1377 pub(super) fn clear_highlight_tooltip(&mut self, ops: &mut dyn crate::window::WindowOps) {
1382 let Some((oid, content_id)) = self.highlight_tooltip.take() else {
1383 return;
1384 };
1385 if self.overlay_manager.stack.iter().any(|o| o.id == oid) {
1386 let dismissed = self.overlay_manager.dismiss(oid);
1387 self.dormant_dismissed_content(&dismissed, &mut *ops);
1388 }
1389 if let Some(entry) = self
1390 .tooltips
1391 .iter_mut()
1392 .find(|e| e.content_id == content_id)
1393 {
1394 entry.overlay_id = None;
1395 entry.shown_at_sim = None;
1396 entry.shown_at_real = None;
1397 if let Some(sink) = entry.shown_at_sink.as_ref() {
1398 sink.set(None);
1399 }
1400 }
1401 }
1402
1403 pub(super) fn tooltip_focus_leave_outside(
1414 &mut self,
1415 new_focus: Option<WidgetId>,
1416 ops: &mut dyn crate::window::WindowOps,
1417 ) {
1418 let to_dismiss: Vec<crate::overlay::OverlayId> = self
1419 .tooltips
1420 .iter()
1421 .filter(|e| e.promoted_by_focus && e.overlay_id.is_some())
1422 .filter(|e| {
1423 let in_scope = new_focus
1424 .map(|nf| {
1425 self.is_descendant_of(nf, e.anchor_id)
1430 || self.is_descendant_of(e.anchor_id, nf)
1431 || self.is_descendant_of(nf, e.content_id)
1432 })
1433 .unwrap_or(false);
1434 !in_scope
1435 })
1436 .filter_map(|e| e.overlay_id)
1437 .collect();
1438
1439 for oid in to_dismiss {
1440 let dismissed = self.overlay_manager.dismiss(oid);
1441 self.dormant_dismissed_content(&dismissed, &mut *ops);
1442 }
1443
1444 for e in &mut self.tooltips {
1447 if e.armed_by_focus && e.overlay_id.is_none() {
1448 e.hover_start = None;
1449 e.real_hover_start = None;
1450 e.armed_by_focus = false;
1451 }
1452 }
1453
1454 let served: Vec<usize> = self
1458 .tooltips
1459 .iter()
1460 .enumerate()
1461 .filter(|(_, e)| e.suppressed_until_focus_leaves)
1462 .filter(|(_, e)| {
1463 let still_inside = new_focus
1464 .map(|nf| {
1465 self.is_descendant_of(nf, e.anchor_id)
1466 || self.is_descendant_of(nf, e.content_id)
1467 })
1468 .unwrap_or(false);
1469 !still_inside
1470 })
1471 .map(|(i, _)| i)
1472 .collect();
1473 for i in served {
1474 self.tooltips[i].suppressed_until_focus_leaves = false;
1475 }
1476 }
1477
1478 fn tooltip_hover_targets_anchor(&self, widget_id: WidgetId, anchor_id: WidgetId) -> bool {
1498 if !self.is_descendant_of(widget_id, anchor_id) {
1499 return false;
1500 }
1501 if let Some(overlay_id) = self.overlay_ancestor_for_widget(widget_id)
1502 && let Some(content_id) = self
1503 .overlay_manager
1504 .overlay(overlay_id)
1505 .map(|o| o.content_id)
1506 && !self.is_descendant_of(anchor_id, content_id)
1507 {
1508 return false;
1509 }
1510 true
1511 }
1512
1513 pub(super) fn tooltip_index_for(&self, widget_id: WidgetId) -> Option<usize> {
1531 self.tooltips
1532 .iter()
1533 .enumerate()
1534 .filter(|(_, entry)| self.tooltip_hover_targets_anchor(widget_id, entry.anchor_id))
1535 .filter_map(|(index, entry)| {
1536 self.ancestor_distance(widget_id, entry.anchor_id)
1537 .map(|depth| (depth, index))
1538 })
1539 .min()
1540 .map(|(_, index)| index)
1541 }
1542
1543 pub(super) fn tooltip_pointer_enter(&mut self, widget_id: WidgetId) {
1544 let Some(index) = self.tooltip_index_for(widget_id) else {
1545 return;
1546 };
1547 if self.tooltips[index].overlay_id.is_some() {
1549 return;
1550 }
1551 self.tooltips[index].hover_start = Some(self.sim_clock);
1552 self.tooltips[index].real_hover_start = Some(std::time::Instant::now());
1553 self.tooltips[index].hover_origin = self.hover_owner_position();
1554 self.tooltips[index].armed_by_focus = false;
1555 self.tooltips[index].armed_by_hold = false;
1556 self.arena.mark_needs_paint(self.tooltips[index].anchor_id);
1557 }
1558
1559 fn ancestor_distance(&self, widget_id: WidgetId, ancestor: WidgetId) -> Option<usize> {
1562 let mut current = widget_id;
1563 let mut depth = 0usize;
1564 loop {
1565 if current == ancestor {
1566 return Some(depth);
1567 }
1568 current = self.arena.parent(current)?;
1569 depth += 1;
1570 }
1571 }
1572
1573 pub(super) fn tooltip_pointer_press(&mut self, at: Option<teksilo_canvas::Point>) {
1588 let mut to_dismiss = Vec::new();
1589 let candidates: Vec<(usize, Option<crate::overlay::OverlayId>)> = self
1590 .tooltips
1591 .iter()
1592 .enumerate()
1593 .map(|(i, e)| (i, if e.is_sticky { None } else { e.overlay_id }))
1594 .collect();
1595 for (index, overlay_id) in candidates {
1596 let inside = match (overlay_id, at) {
1601 (Some(oid), Some(pos)) => self
1602 .overlay_content_bounds(oid)
1603 .is_some_and(|rect| rect.contains(pos)),
1604 _ => false,
1605 };
1606 if inside {
1607 continue;
1608 }
1609 let entry = &mut self.tooltips[index];
1610 entry.hover_start = None;
1611 entry.real_hover_start = None;
1612 entry.hover_origin = None;
1613 if let Some(oid) = overlay_id {
1614 to_dismiss.push(oid);
1615 }
1616 }
1617 for overlay_id in to_dismiss {
1618 self.dismiss_overlay(overlay_id);
1619 }
1620 }
1621
1622 pub(super) fn tooltip_escape_pressed(&mut self) {
1644 self.tooltip_pointer_press(None);
1647 }
1648
1649 pub(super) fn inert_overlays_escape_pressed(&mut self, ops: &mut dyn crate::window::WindowOps) {
1660 let dismissed = self.overlay_manager.dismiss_inert_on_escape();
1661 if !dismissed.is_empty() {
1662 self.dormant_dismissed_content(&dismissed, &mut *ops);
1663 self.a11y_dirty = true;
1664 }
1665 }
1666
1667 pub(super) fn tooltip_window_deactivated(&mut self) {
1674 self.tooltip_pointer_press(None);
1677 }
1678
1679 pub(crate) fn tooltip_cancel_pending_dwell(&mut self) {
1687 for entry in &mut self.tooltips {
1688 if entry.overlay_id.is_none() {
1689 entry.hover_start = None;
1690 entry.real_hover_start = None;
1691 entry.hover_origin = None;
1692 }
1693 }
1694 }
1695
1696 pub(super) fn tooltip_pointer_moved(
1700 &mut self,
1701 widget_id: WidgetId,
1702 position: teksilo_canvas::Point,
1703 ) {
1704 let matching: Vec<usize> = self
1705 .tooltips
1706 .iter()
1707 .enumerate()
1708 .filter(|(_, entry)| {
1709 entry.overlay_id.is_none()
1710 && entry.hover_start.is_some()
1711 && !entry.armed_by_focus
1712 && self.tooltip_hover_targets_anchor(widget_id, entry.anchor_id)
1713 })
1714 .map(|(index, _)| index)
1715 .collect();
1716 if matching.is_empty() {
1717 return;
1718 }
1719 let now = self.sim_clock;
1720 let real_now = std::time::Instant::now();
1721 let slop = super::TOOLTIP_STATIONARY_SLOP;
1722 let slop_sq = slop * slop;
1723 for index in matching {
1724 let origin = match self.tooltips[index].hover_origin {
1725 Some(o) => o,
1726 None => {
1727 self.tooltips[index].hover_origin = Some(position);
1731 continue;
1732 }
1733 };
1734 let dx = position.x - origin.x;
1735 let dy = position.y - origin.y;
1736 if dx * dx + dy * dy <= slop_sq {
1737 continue;
1738 }
1739 self.tooltips[index].hover_start = Some(now);
1741 self.tooltips[index].real_hover_start = Some(real_now);
1742 self.tooltips[index].hover_origin = Some(position);
1743 }
1744 }
1745
1746 pub fn promote_tooltip_to_sticky(&mut self, content_id: WidgetId) {
1764 let Some(entry) = self
1765 .tooltips
1766 .iter_mut()
1767 .find(|entry| entry.content_id == content_id)
1768 else {
1769 return;
1770 };
1771 if entry.is_sticky {
1772 return;
1773 }
1774 entry.is_sticky = true;
1775 let overlay_id = entry.overlay_id;
1776 if let Some(overlay_id) = overlay_id {
1777 self.overlay_manager.set_dismiss(
1778 overlay_id,
1779 crate::overlay::DismissBehavior::EscapeOrClickOutside,
1780 );
1781 }
1782 }
1783
1784 pub(super) fn tooltip_pointer_leave(
1785 &mut self,
1786 widget_id: WidgetId,
1787 _ops: &mut dyn crate::window::WindowOps,
1788 ) {
1789 let matching: Vec<usize> = self
1790 .tooltips
1791 .iter()
1792 .enumerate()
1793 .filter(|(_, entry)| self.is_descendant_of(widget_id, entry.anchor_id))
1794 .map(|(index, _)| index)
1795 .collect();
1796 for index in matching {
1797 self.tooltips[index].hover_start = None;
1800 self.tooltips[index].real_hover_start = None;
1801 self.tooltips[index].hover_origin = None;
1802 }
1814 }
1815
1816 pub fn next_timer_deadline(&self) -> Option<std::time::Instant> {
1822 let now = std::time::Instant::now();
1823 let session_active = self.tooltip_session_active_real(now);
1824 let tooltip_deadline = self
1825 .tooltips
1826 .iter()
1827 .filter(|entry| entry.overlay_id.is_none())
1828 .filter_map(|entry| {
1829 let start = entry.real_hover_start?;
1830 let delay = self.effective_tooltip_delay(entry.delay, session_active);
1831 Some(start + delay)
1832 })
1833 .min();
1834
1835 let ref_time = self.last_frame_time.unwrap_or_else(std::time::Instant::now);
1856 let dwell_tooltip_deadline = self
1857 .tooltips
1858 .iter()
1859 .filter_map(|entry| {
1860 let sticky_after = entry.sticky_after?;
1861 let shown_at = entry.shown_at_real?;
1862 if entry.overlay_id.is_none() || entry.is_sticky {
1863 return None;
1864 }
1865 let dwell_step = sticky_after / super::TOOLTIP_DWELL_STEPS;
1866 if dwell_step.is_zero() {
1867 return None;
1868 }
1869 let elapsed = ref_time.saturating_duration_since(shown_at);
1870 if elapsed >= sticky_after {
1871 return None;
1872 }
1873 let steps_passed = (elapsed.as_millis() / dwell_step.as_millis()) as u32;
1876 let next_step_at = shown_at + dwell_step * (steps_passed + 1);
1877 Some(next_step_at.min(shown_at + sticky_after))
1878 })
1879 .min();
1880 let delayed_overlay_deadline = self
1881 .pending_delayed_overlays
1882 .iter()
1883 .map(|pending| pending.real_requested_at + pending.delay)
1884 .min();
1885 let auto_dismiss_deadline = self.overlay_manager.next_auto_dismiss_deadline();
1886 let pointer_leave_deadline = self.overlay_manager.next_pointer_leave_deadline();
1893 let fade_dismiss_deadline = self.overlay_manager.next_fade_dismiss_deadline();
1900 let animation_deadline = self
1901 .animation_scheduler
1902 .next_deadline(&self.arena, self.paint_epoch);
1903 let animated_quad_deadline = self
1908 .animated_quads
1909 .next_deadline(&self.arena, self.paint_epoch);
1910 let gesture_deadline = self.next_input_deadline();
1916 let wake_at_deadline = self.pending_wake_at.get();
1917 let frame_tick_deadline = self.frame_tick_deadline();
1923
1924 [
1925 tooltip_deadline,
1926 dwell_tooltip_deadline,
1927 delayed_overlay_deadline,
1928 auto_dismiss_deadline,
1929 pointer_leave_deadline,
1930 fade_dismiss_deadline,
1931 animation_deadline,
1932 animated_quad_deadline,
1933 gesture_deadline,
1934 wake_at_deadline,
1935 frame_tick_deadline,
1936 ]
1937 .into_iter()
1938 .flatten()
1939 .min()
1940 }
1941
1942 pub fn next_unpainted_deadline(&self) -> Option<std::time::Instant> {
1954 let animation_deadline = self
1955 .animation_scheduler
1956 .next_deadline(&self.arena, self.paint_epoch);
1957 [
1958 self.pending_wake_at.get(),
1959 animation_deadline,
1960 self.frame_tick_deadline(),
1961 ]
1962 .into_iter()
1963 .flatten()
1964 .min()
1965 }
1966
1967 pub fn overlay_manager(&self) -> &crate::overlay::OverlayManager {
1968 &self.overlay_manager
1969 }
1970
1971 pub fn overlay_manager_mut(&mut self) -> &mut crate::overlay::OverlayManager {
1976 &mut self.overlay_manager
1977 }
1978
1979 pub fn active_overlays(&self) -> Vec<crate::overlay::OverlayId> {
1980 self.overlay_manager.active_ids()
1981 }
1982
1983 pub fn overlay_content_bounds(
1992 &self,
1993 id: crate::overlay::OverlayId,
1994 ) -> Option<teksilo_canvas::Rect> {
1995 self.overlay_manager
1996 .stack
1997 .iter()
1998 .find(|o| o.id == id)
1999 .map(|o| o.bounds)
2000 }
2001
2002 pub fn show_overlay(
2003 &mut self,
2004 request: crate::overlay::OverlayRequest,
2005 ) -> crate::overlay::OverlayId {
2006 let fade_duration = request.fade_duration;
2007 let content_id = request.content_id;
2008 let is_modal = request.placement.is_modal();
2009 let id = self.overlay_manager.show(request);
2010 self.cancel_pointers_for_modal(is_modal);
2011 self.a11y_dirty = true;
2019 if let Some(duration) = fade_duration {
2020 self.attach_overlay_fade(id, content_id, duration);
2021 }
2022 id
2023 }
2024
2025 pub fn show_overlay_in_band(
2035 &mut self,
2036 request: crate::overlay::OverlayRequest,
2037 band: crate::overlay::OverlayBand,
2038 ) -> crate::overlay::OverlayId {
2039 let fade_duration = request.fade_duration;
2040 let content_id = request.content_id;
2041 let is_modal = request.placement.is_modal();
2042 let id = self.overlay_manager.show_in_band(request, band);
2043 self.cancel_pointers_for_modal(is_modal);
2044 self.a11y_dirty = true;
2045 if let Some(duration) = fade_duration {
2046 self.attach_overlay_fade(id, content_id, duration);
2047 }
2048 id
2049 }
2050
2051 pub fn show_overlay_from_source(
2054 &mut self,
2055 source_widget: WidgetId,
2056 mut request: crate::overlay::OverlayRequest,
2057 ) -> crate::overlay::OverlayId {
2058 if request.parent_overlay.is_none() {
2059 request.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
2060 }
2061 if let Some(existing) = self.overlay_manager.find_by_content(request.content_id) {
2062 return existing;
2063 }
2064
2065 let fade_duration = request.fade_duration;
2066 let content_id = request.content_id;
2067 let is_modal = request.placement.is_modal();
2068 let current_focus = self.focused;
2069 let id = self.overlay_manager.show(request);
2070 self.cancel_pointers_for_modal(is_modal);
2071 self.a11y_dirty = true;
2072 if let Some(focus_id) = current_focus {
2073 self.overlay_manager.set_top_focus_restore(focus_id);
2074 }
2075 if let Some(duration) = fade_duration {
2076 self.attach_overlay_fade(id, content_id, duration);
2077 }
2078 id
2079 }
2080
2081 pub fn show_overlay_for(
2082 &mut self,
2083 request: crate::overlay::OverlayRequest,
2084 duration: std::time::Duration,
2085 ) -> crate::overlay::OverlayId {
2086 let fade_duration = request.fade_duration;
2087 let content_id = request.content_id;
2088 let is_modal = request.placement.is_modal();
2089 let id = self.overlay_manager.show_for(request, duration);
2090 self.cancel_pointers_for_modal(is_modal);
2091 self.overlay_manager.set_shown_at_sim(id, self.sim_clock);
2092 self.a11y_dirty = true;
2093 if let Some(fade) = fade_duration {
2094 self.attach_overlay_fade(id, content_id, fade);
2095 }
2096 id
2097 }
2098
2099 fn attach_overlay_fade(
2107 &mut self,
2108 overlay_id: crate::overlay::OverlayId,
2109 content_id: WidgetId,
2110 duration: std::time::Duration,
2111 ) {
2112 let opacity = crate::signal::Signal::<f32>::new_animated(0.0);
2113 self.register_animated_signal(&opacity, content_id);
2114 self.set_opacity(content_id, opacity.clone());
2115 if self.prefers_reduced_motion() {
2119 opacity.set(1.0);
2120 self.overlay_manager
2121 .attach_fade(overlay_id, opacity, std::time::Duration::ZERO);
2122 return;
2123 }
2124 let _ = opacity.try_animate_with_options(crate::animation::AnimationRequest {
2125 target: 1.0,
2126 duration,
2127 easing: teksilo_tokens::Easing::EaseOut,
2128 frame_interval: None,
2129 looping: false,
2130 epsilon: 0.0,
2131 max_duration: None,
2132 });
2133 self.overlay_manager
2134 .attach_fade(overlay_id, opacity, duration);
2135 }
2136
2137 pub fn dismiss_overlay(&mut self, id: crate::overlay::OverlayId) {
2142 let mut noop = crate::window::NoopWindowOps;
2143 self.dismiss_overlay_with_ops(id, &mut noop);
2144 }
2145
2146 #[doc(hidden)]
2152 pub fn dismiss_all_overlays_with_ops(
2153 &mut self,
2154 reason: crate::overlay::DismissReason,
2155 ops: &mut dyn crate::window::WindowOps,
2156 ) {
2157 let dismissed = self.overlay_manager.dismiss_all_because(reason);
2158 self.dormant_dismissed_content(&dismissed, &mut *ops);
2159 }
2160
2161 pub fn dismiss_overlay_with_ops(
2164 &mut self,
2165 id: crate::overlay::OverlayId,
2166 ops: &mut dyn crate::window::WindowOps,
2167 ) {
2168 let dismissed = self.overlay_manager.dismiss(id);
2169 self.dormant_dismissed_content(&dismissed, &mut *ops);
2170 }
2171
2172 fn cancel_pointers_for_modal(&mut self, is_modal: bool) {
2183 if !is_modal {
2184 return;
2185 }
2186 let mut noop = crate::window::NoopWindowOps;
2187 self.cancel_all_pointers(crate::pointer::CancelReason::ModalOpened, &mut noop);
2188 }
2189
2190 pub(super) fn run_pending_dismiss_callbacks(&mut self, ops: &mut dyn crate::window::WindowOps) {
2200 if self.draining_dismiss.replace(true) {
2205 return;
2206 }
2207 for _ in 0..8 {
2211 let pending = self.overlay_manager.take_pending_dismiss();
2212 if pending.is_empty() {
2213 break;
2214 }
2215 for (cb, reason) in pending {
2216 self.run_with_event_context(&mut *ops, |ctx| cb(reason, ctx));
2217 }
2218 }
2219 self.draining_dismiss.set(false);
2220 }
2221
2222 pub(super) fn dormant_dismissed_content(
2223 &mut self,
2224 content_ids: &[WidgetId],
2225 ops: &mut dyn crate::window::WindowOps,
2226 ) {
2227 self.run_pending_dismiss_callbacks(&mut *ops);
2231 let mut any_tooltip_dismissed = false;
2237 for &id in content_ids {
2238 if let Some(entry) = self.tooltips.iter_mut().find(|e| e.content_id == id) {
2239 entry.suppressed_until_focus_leaves = entry.promoted_by_focus;
2246 entry.overlay_id = None;
2247 entry.is_sticky = false;
2248 entry.hover_start = None;
2249 entry.real_hover_start = None;
2250 entry.hover_origin = None;
2251 entry.shown_at_sim = None;
2252 entry.shown_at_real = None;
2253 entry.promoted_by_focus = false;
2254 entry.armed_by_focus = false;
2255 entry.armed_by_hold = false;
2256 if let Some(sink) = entry.shown_at_sink.as_ref() {
2257 sink.set(None);
2258 }
2259 any_tooltip_dismissed = true;
2260 }
2261 }
2262 if any_tooltip_dismissed {
2263 let grace = super::TOOLTIP_SESSION_GRACE;
2264 self.tooltip_session_until_sim = Some(self.sim_clock + grace);
2265 self.tooltip_session_until_real = Some(std::time::Instant::now() + grace);
2266 }
2267 for &id in content_ids {
2268 let focused_in_subtree = self
2269 .focused
2270 .filter(|focused| self.is_descendant_of(*focused, id));
2271 let hovered_in_subtree = self
2272 .hovered_id()
2273 .filter(|hovered| self.is_descendant_of(*hovered, id));
2274
2275 if let Some(focused) = focused_in_subtree {
2276 self.dispatch_to_widget(focused, &WidgetEvent::FocusLost, &mut *ops);
2277 if self
2278 .focused
2279 .is_some_and(|current| self.is_descendant_of(current, id))
2280 {
2281 let old = self.focused;
2282 self.set_focused(None);
2283 self.focus_origin = None;
2284 self.update_focus_within_signals(old, None);
2285 self.update_view_focus_signals(old, None);
2286 }
2287 }
2288
2289 self.cancel_pointers_in_subtree(
2295 id,
2296 crate::pointer::CancelReason::OverlayDismissed,
2297 &mut *ops,
2298 );
2299 let _parked = self.arena.set_dormant(id);
2300
2301 if hovered_in_subtree.is_some() {
2302 let old = self.hovered_id();
2303 self.set_hovered(None);
2304 self.update_hover_within_signals(old, None);
2305 }
2306 }
2307 if !content_ids.is_empty() {
2308 self.cached_frame = None;
2309 self.a11y_dirty = true;
2310 }
2311 }
2312
2313 pub fn is_visible(&self, id: WidgetId) -> bool {
2314 self.arena.is_active(id)
2315 }
2316}
2317
2318#[cfg(test)]
2319mod tests {
2320 use super::*;
2321 use crate::test_widgets::FillWidget;
2322 use crate::test_widgets::StackWidget;
2323
2324 #[test]
2325 fn is_visible_reflects_dormancy() {
2326 let mut tree = WidgetTree::new();
2327 let widget = tree.add(FillWidget::new());
2328 tree.layout(SizeProposal::exact(100.0, 50.0));
2329
2330 assert!(tree.is_visible(widget));
2331 tree.set_dormant(widget);
2332 assert!(!tree.is_visible(widget));
2333 tree.activate(widget);
2334 assert!(tree.is_visible(widget));
2335 }
2336
2337 #[test]
2338 fn frame_tick_deadline_caps_per_frame_effects_at_60hz() {
2339 let mut tree = WidgetTree::new();
2340 assert!(tree.frame_tick_deadline().is_none());
2342
2343 tree.request_frame();
2346 let t0 = std::time::Instant::now();
2348 tree.last_frame_time = Some(t0);
2349
2350 let deadline = tree
2351 .frame_tick_deadline()
2352 .expect("an armed per-frame effect must publish a deadline");
2353 let interval = deadline.saturating_duration_since(t0);
2354 assert!(
2357 interval >= std::time::Duration::from_micros(16_000)
2358 && interval <= std::time::Duration::from_micros(17_500),
2359 "per-frame effects must pace at ~60 Hz (got {interval:?})"
2360 );
2361
2362 assert_eq!(
2365 tree.next_timer_deadline(),
2366 Some(deadline),
2367 "frame-tick deadline must be surfaced by next_timer_deadline"
2368 );
2369 }
2370
2371 #[test]
2372 fn throttled_subscriber_stretches_deadline_but_per_frame_wins_min() {
2373 use crate::test_widgets::{FillWidget, StackWidget};
2374 let mut tree = WidgetTree::new();
2375 let w = tree.add(FillWidget::new());
2376 let w2 = tree.add(FillWidget::new());
2377 tree.add(StackWidget::new().child(w).child(w2));
2378 let _throttled =
2380 tree.subscribe_frame_tick_throttled(w, std::time::Duration::from_millis(1500));
2381 tree.layout(SizeProposal::exact(200.0, 100.0));
2385 let _ = tree.render();
2386 assert!(
2387 !tree.frame_tick_requested.get(),
2388 "precondition: nothing but the subscriber wants this frame"
2389 );
2390 let t0 = tree.last_frame_time.expect("a frame has run");
2391
2392 let d = tree.frame_tick_deadline().expect("armed");
2394 let dt = d.saturating_duration_since(t0);
2395 assert!(
2396 dt >= std::time::Duration::from_millis(1490)
2397 && dt <= std::time::Duration::from_millis(1510),
2398 "a lone throttled subscriber must pace at its interval (~1.5 s), got {dt:?}"
2399 );
2400
2401 let _per_frame = tree.subscribe_frame_tick(w2);
2406 let d2 = tree.frame_tick_deadline().expect("armed");
2407 let dt2 = d2.saturating_duration_since(t0);
2408 assert!(
2409 dt2 <= std::time::Duration::from_millis(20),
2410 "a per-frame subscriber must pull the shared deadline to 60 Hz, got {dt2:?}"
2411 );
2412 }
2413
2414 #[test]
2420 fn a_raw_frame_request_is_not_paced_by_a_throttled_subscriber() {
2421 use crate::test_widgets::FillWidget;
2422 const NORMAL_FRAME: std::time::Duration = std::time::Duration::from_micros(17_500);
2423 const CLOCK: std::time::Duration = std::time::Duration::from_secs(60);
2424 fn frame(tree: &mut WidgetTree) {
2425 tree.layout(SizeProposal::exact(200.0, 100.0));
2426 let _ = tree.render();
2427 }
2428 fn wait(tree: &WidgetTree) -> std::time::Duration {
2429 let deadline = tree.frame_tick_deadline().expect("armed");
2430 deadline.saturating_duration_since(tree.last_frame_time.expect("a frame has run"))
2431 }
2432
2433 let mut tree = WidgetTree::new();
2434 let clock = tree.add(FillWidget::new());
2435 let _tick = tree.subscribe_frame_tick_throttled(clock, CLOCK);
2436 frame(&mut tree);
2437 assert!(
2438 wait(&tree) >= CLOCK - NORMAL_FRAME,
2439 "precondition: the painted clock paces the loop at its own interval"
2440 );
2441
2442 for request in [
2445 (|tree: &WidgetTree| tree.request_frame()) as fn(&WidgetTree),
2446 |tree: &WidgetTree| tree.frame_request_handle().set(true),
2447 ] {
2448 request(&tree);
2449 assert!(
2450 wait(&tree) <= NORMAL_FRAME,
2451 "a requested frame must not wait on the clock (waited {:?})",
2452 wait(&tree)
2453 );
2454 assert_eq!(tree.next_timer_deadline(), tree.frame_tick_deadline());
2455
2456 frame(&mut tree);
2457 assert!(
2458 wait(&tree) >= CLOCK - NORMAL_FRAME,
2459 "once the request is served the clock paces alone again, rather than the loop \
2460 free-running at 60 Hz (waited {:?})",
2461 wait(&tree)
2462 );
2463 }
2464 }
2465
2466 #[test]
2475 fn the_subscriber_arm_is_spent_by_its_tick_and_ends_with_its_owner() {
2476 use crate::test_widgets::StackWidget;
2477 fn frame(tree: &mut WidgetTree) {
2478 tree.layout(SizeProposal::exact(200.0, 100.0));
2479 let _ = tree.render();
2480 }
2481
2482 let mut tree = WidgetTree::new();
2483 let owner = tree.add(FillWidget::new());
2484 let sibling = tree.add(FillWidget::new());
2485 tree.add(StackWidget::new().child(owner).child(sibling));
2486 let ticks = std::rc::Rc::new(std::cell::Cell::new(0_u32));
2487 let _count = tree.frame_tick().observe({
2488 let ticks = ticks.clone();
2489 move |_| ticks.set(ticks.get() + 1)
2490 });
2491 let _sub = tree.subscribe_frame_tick(owner);
2492
2493 frame(&mut tree);
2494 assert!(
2495 tree.frame_requested(),
2496 "a render that painted the subscriber arms the next tick"
2497 );
2498 assert!(
2499 !tree.frame_tick_requested.get(),
2500 "precondition: armed by the subscribers' flag, not by a raw request"
2501 );
2502
2503 let before = ticks.get();
2505 tree.layout(SizeProposal::exact(200.0, 100.0));
2506 assert_eq!(
2507 ticks.get(),
2508 before + 1,
2509 "the armed tick fires on the next layout"
2510 );
2511 assert!(
2512 !tree.frame_requested(),
2513 "the layout that fired the tick has spent the arm"
2514 );
2515 let _ = tree.render();
2516 assert!(
2517 tree.frame_requested(),
2518 "the owner painted again and re-armed"
2519 );
2520
2521 let before = ticks.get();
2523 tree.advance_time(std::time::Duration::from_millis(16));
2524 assert_eq!(ticks.get(), before + 1, "advance_time fires an armed tick");
2525 assert!(
2526 !tree.frame_requested(),
2527 "advance_time spends the arm as layout does"
2528 );
2529 tree.resume_real_time();
2530
2531 frame(&mut tree);
2534 tree.set_dormant(owner);
2535 assert!(
2536 tree.frame_tick_deadline().is_some(),
2537 "the tick armed before the owner was parked is still due"
2538 );
2539 let before = ticks.get();
2540 frame(&mut tree);
2541 assert_eq!(ticks.get(), before + 1);
2542 assert!(
2543 !tree.frame_requested(),
2544 "a parked subscriber must not re-arm the tick"
2545 );
2546 assert_eq!(
2547 tree.next_timer_deadline(),
2548 None,
2549 "with its only subscriber parked the loop sleeps"
2550 );
2551 }
2552
2553 #[test]
2554 fn show_and_dismiss_overlay() {
2555 let mut tree = WidgetTree::new();
2556 let anchor = tree.add(FillWidget::new());
2557 let content = tree.add(FillWidget::new().label("Overlay"));
2558 tree.layout(SizeProposal::exact(200.0, 100.0));
2559
2560 assert!(tree.active_overlays().is_empty());
2561
2562 let id = tree.show_overlay(crate::overlay::OverlayRequest::new(
2563 content,
2564 anchor,
2565 crate::overlay::OverlayPlacement::Below,
2566 crate::overlay::DismissBehavior::Manual,
2567 ));
2568
2569 assert_eq!(tree.active_overlays().len(), 1);
2570
2571 tree.dismiss_overlay(id);
2572 assert!(tree.active_overlays().is_empty());
2573 assert!(!tree.is_visible(content));
2574 }
2575
2576 #[test]
2581 fn modal_centered_overlay_is_a_host_surface() {
2582 let mut tree = WidgetTree::new();
2583 let anchor = tree.add(FillWidget::new());
2584 let modal_content = tree.add(FillWidget::new().label("Modal"));
2585 let dropdown_content = tree.add(FillWidget::new().label("Dropdown"));
2586 tree.layout(SizeProposal::exact(200.0, 100.0));
2587
2588 let modal = tree.show_overlay(crate::overlay::OverlayRequest::new(
2589 modal_content,
2590 anchor,
2591 crate::overlay::OverlayPlacement::Centered,
2592 crate::overlay::DismissBehavior::EscapeOrClickOutside,
2593 ));
2594 let dropdown = tree.show_overlay(
2595 crate::overlay::OverlayRequest::new(
2596 dropdown_content,
2597 anchor,
2598 crate::overlay::OverlayPlacement::Below,
2599 crate::overlay::DismissBehavior::EscapeOrClickOutside,
2600 )
2601 .parent_overlay(modal),
2602 );
2603
2604 assert!(
2605 tree.overlay_is_host_surface(modal),
2606 "a Centered modal overlay must be treated as a host surface"
2607 );
2608 assert!(
2609 !tree.overlay_is_host_surface(dropdown),
2610 "a plain dropdown overlay is not a host surface"
2611 );
2612 }
2613
2614 fn overlay_over_a_field(tree: &mut WidgetTree) -> (WidgetId, WidgetId, WidgetId) {
2616 let field = tree.add(FillWidget::new().focusable());
2617 let inside = tree.add(FillWidget::new().focusable());
2618 let content = tree.add(StackWidget::new().child(inside));
2619 tree.layout(SizeProposal::exact(200.0, 100.0));
2620 tree.focus(field);
2621 tree.show_overlay_from_source(
2622 field,
2623 crate::overlay::OverlayRequest::new(
2624 content,
2625 field,
2626 crate::overlay::OverlayPlacement::Below,
2627 crate::overlay::DismissBehavior::EscapeOrClickOutside,
2628 ),
2629 );
2630 tree.layout(SizeProposal::exact(200.0, 100.0));
2631 (field, content, inside)
2632 }
2633
2634 fn dismiss_all_except_hosts(tree: &mut WidgetTree) {
2635 tree.run_with_event_context(&mut crate::window::NoopWindowOps, |ctx| {
2636 ctx.dismiss_all_except_hosts();
2637 });
2638 }
2639
2640 #[test]
2646 fn dismissing_all_except_hosts_gives_back_the_focus_it_took_away() {
2647 let mut tree = WidgetTree::new();
2648 let (field, content, inside) = overlay_over_a_field(&mut tree);
2649 let nested = tree.add(FillWidget::new().focusable());
2650 tree.layout(SizeProposal::exact(200.0, 100.0));
2651 tree.focus(inside);
2652 tree.show_overlay_from_source(
2653 inside,
2654 crate::overlay::OverlayRequest::new(
2655 nested,
2656 inside,
2657 crate::overlay::OverlayPlacement::Below,
2658 crate::overlay::DismissBehavior::EscapeOrClickOutside,
2659 ),
2660 );
2661 tree.focus(nested);
2662 assert!(tree.is_active(content), "precondition");
2663 dismiss_all_except_hosts(&mut tree);
2664 assert!(tree.active_overlays().is_empty());
2665 assert_eq!(tree.focused(), Some(field));
2666 }
2667
2668 #[test]
2673 fn dismissing_all_except_hosts_gives_focus_back_as_the_lowest_overlay_found_it() {
2674 let mut tree = WidgetTree::new();
2675 let first_field = tree.add(FillWidget::new().focusable());
2676 let second_field = tree.add(FillWidget::new().focusable());
2677 let open_over = |tree: &mut WidgetTree, field: WidgetId| {
2678 let inside = tree.add(FillWidget::new().focusable());
2679 let content = tree.add(StackWidget::new().child(inside));
2680 tree.layout(SizeProposal::exact(200.0, 100.0));
2681 tree.focus(field);
2682 tree.show_overlay_from_source(
2684 field,
2685 crate::overlay::OverlayRequest::new(
2686 content,
2687 field,
2688 crate::overlay::OverlayPlacement::BottomCenter,
2689 crate::overlay::DismissBehavior::Manual,
2690 ),
2691 );
2692 tree.layout(SizeProposal::exact(200.0, 100.0));
2693 inside
2694 };
2695 open_over(&mut tree, first_field);
2696 let second_inside = open_over(&mut tree, second_field);
2697 tree.focus(second_inside);
2698 assert_eq!(tree.active_overlays().len(), 2, "two overlays, unrelated");
2699 dismiss_all_except_hosts(&mut tree);
2700 assert!(tree.active_overlays().is_empty());
2701 assert_eq!(
2702 tree.focused(),
2703 Some(first_field),
2704 "the lowest overlay's focus, not the top one's"
2705 );
2706 }
2707
2708 #[test]
2710 fn dismissing_all_except_hosts_leaves_focus_outside_them_alone() {
2711 let mut tree = WidgetTree::new();
2712 let (_field, _content, _inside) = overlay_over_a_field(&mut tree);
2713 let other = tree.add(FillWidget::new().focusable());
2714 tree.layout(SizeProposal::exact(200.0, 100.0));
2715 tree.focus(other);
2716 dismiss_all_except_hosts(&mut tree);
2717 assert_eq!(tree.focused(), Some(other));
2718 }
2719
2720 #[test]
2721 fn escape_dismisses_topmost_overlay() {
2722 let mut tree = WidgetTree::new();
2723 let anchor = tree.add(FillWidget::new().focusable());
2724 let content = tree.add(FillWidget::new());
2725 tree.layout(SizeProposal::exact(200.0, 100.0));
2726 tree.focus(anchor);
2727
2728 tree.show_overlay(crate::overlay::OverlayRequest::new(
2729 content,
2730 anchor,
2731 crate::overlay::OverlayPlacement::Below,
2732 crate::overlay::DismissBehavior::EscapeOrClickOutside,
2733 ));
2734
2735 assert_eq!(tree.active_overlays().len(), 1);
2736
2737 tree.press_key(Key::Escape, Modifiers::NONE);
2738 assert!(tree.active_overlays().is_empty());
2739 assert!(!tree.is_visible(content));
2740 }
2741
2742 fn show_two_nested_overlays(tree: &mut WidgetTree) -> (WidgetId, WidgetId) {
2745 let anchor = tree.add(FillWidget::new());
2746 let c1 = tree.add(FillWidget::new());
2747 let c2 = tree.add(FillWidget::new());
2748 tree.layout(SizeProposal::exact(200.0, 100.0));
2749 let o1 = tree.show_overlay(crate::overlay::OverlayRequest::new(
2750 c1,
2751 anchor,
2752 crate::overlay::OverlayPlacement::Below,
2753 crate::overlay::DismissBehavior::Manual,
2754 ));
2755 tree.show_overlay(
2756 crate::overlay::OverlayRequest::new(
2757 c2,
2758 c1,
2759 crate::overlay::OverlayPlacement::Below,
2760 crate::overlay::DismissBehavior::Manual,
2761 )
2762 .parent_overlay(o1),
2763 );
2764 (c1, c2)
2765 }
2766
2767 #[test]
2768 fn nested_overlay_back_key_dismisses_with_arrow_left_under_ltr() {
2769 let mut tree = WidgetTree::new();
2770 let _ = show_two_nested_overlays(&mut tree);
2771 assert_eq!(tree.active_overlays().len(), 2);
2772
2773 tree.press_key(Key::ArrowRight, Modifiers::NONE);
2775 assert_eq!(tree.active_overlays().len(), 2);
2776
2777 tree.press_key(Key::ArrowLeft, Modifiers::NONE);
2779 assert_eq!(tree.active_overlays().len(), 1);
2780 }
2781
2782 #[test]
2783 fn nested_overlay_back_key_flips_to_arrow_right_under_rtl() {
2784 let mut tree = WidgetTree::new();
2785 tree.set_layout_direction(crate::environment::LayoutDirection::RightToLeft);
2786 let _ = show_two_nested_overlays(&mut tree);
2787 assert_eq!(tree.active_overlays().len(), 2);
2788
2789 tree.press_key(Key::ArrowLeft, Modifiers::NONE);
2792 assert_eq!(tree.active_overlays().len(), 2);
2793
2794 tree.press_key(Key::ArrowRight, Modifiers::NONE);
2796 assert_eq!(tree.active_overlays().len(), 1);
2797 }
2798
2799 fn show_inert_overlay(tree: &mut WidgetTree, anchor: WidgetId) -> WidgetId {
2801 let content = tree.add(FillWidget::new());
2802 tree.layout(SizeProposal::exact(200.0, 100.0));
2803 tree.show_overlay(
2804 crate::overlay::OverlayRequest::new(
2805 content,
2806 anchor,
2807 crate::overlay::OverlayPlacement::Below,
2808 crate::overlay::DismissBehavior::EscapeKey,
2809 )
2810 .inert(),
2811 );
2812 content
2813 }
2814
2815 #[test]
2820 fn back_key_does_not_count_an_inert_overlay_as_a_menu_level() {
2821 for inert_first in [true, false] {
2822 let mut tree = WidgetTree::new();
2823 let anchor = tree.add(FillWidget::new());
2824 let menu = tree.add(FillWidget::new());
2825 tree.layout(SizeProposal::exact(200.0, 100.0));
2826 let show_menu = |tree: &mut WidgetTree| {
2827 tree.show_overlay(crate::overlay::OverlayRequest::new(
2828 menu,
2829 anchor,
2830 crate::overlay::OverlayPlacement::Below,
2831 crate::overlay::DismissBehavior::Manual,
2832 ));
2833 };
2834 let inert = if inert_first {
2835 let inert = show_inert_overlay(&mut tree, anchor);
2836 show_menu(&mut tree);
2837 inert
2838 } else {
2839 show_menu(&mut tree);
2840 show_inert_overlay(&mut tree, anchor)
2841 };
2842 assert_eq!(tree.active_overlays().len(), 2);
2843
2844 tree.press_key(Key::ArrowLeft, Modifiers::NONE);
2845 assert_eq!(
2846 tree.active_overlays().len(),
2847 2,
2848 "inert shown first: {inert_first}: one menu is not a cascade"
2849 );
2850 assert!(tree.is_visible(menu) && tree.is_visible(inert));
2851 }
2852 }
2853
2854 #[test]
2857 fn back_key_closes_the_submenu_beneath_an_inert_overlay() {
2858 let mut tree = WidgetTree::new();
2859 let (parent, submenu) = show_two_nested_overlays(&mut tree);
2860 let inert = show_inert_overlay(&mut tree, parent);
2861 assert_eq!(tree.active_overlays().len(), 3);
2862
2863 tree.press_key(Key::ArrowLeft, Modifiers::NONE);
2864 assert!(!tree.is_visible(submenu), "the submenu closes");
2865 assert!(tree.is_visible(parent), "its parent stays open");
2866 assert!(
2867 tree.is_visible(inert),
2868 "the inert overlay is not what closes"
2869 );
2870 }
2871
2872 #[derive(Debug)]
2875 struct KeyProbe {
2876 seen: std::rc::Rc<std::cell::RefCell<Vec<Key>>>,
2877 }
2878
2879 impl Widget for KeyProbe {
2880 fn layout_response(
2881 &self,
2882 proposal: SizeProposal,
2883 _ctx: &crate::widget::LayoutContext,
2884 ) -> crate::widget::LayoutResponse {
2885 proposal.resolve(0.0, 0.0).into()
2886 }
2887
2888 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
2889 let seen = self.seen.clone();
2890 ctx.apply_self_handlers(
2891 crate::widget_builder::HandlerSet::new()
2892 .focusable(true)
2893 .on_key(move |event, _ctx| match event {
2894 WidgetEvent::KeyDown { key, .. } => {
2895 seen.borrow_mut().push(*key);
2896 crate::event::EventResponse::Handled
2897 }
2898 _ => crate::event::EventResponse::Ignored,
2899 }),
2900 );
2901 Vec::new()
2902 }
2903 }
2904
2905 #[test]
2917 fn back_key_ignores_text_affordance_overlays_and_reaches_the_editor() {
2918 let mut tree = WidgetTree::new();
2919 let seen = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
2920 let editor = tree.add(KeyProbe { seen: seen.clone() });
2921 let handles_a = tree.add(FillWidget::new());
2922 let handles_b = tree.add(FillWidget::new());
2923 tree.layout(SizeProposal::exact(200.0, 100.0));
2924 tree.focus(editor);
2925
2926 for content in [handles_a, handles_b] {
2928 tree.show_overlay_in_band(
2929 crate::overlay::OverlayRequest::new(
2930 content,
2931 editor,
2932 crate::overlay::OverlayPlacement::FullViewport,
2933 crate::overlay::DismissBehavior::Manual,
2934 ),
2935 crate::overlay::OverlayBand::TextAffordance,
2936 );
2937 }
2938 assert_eq!(tree.active_overlays().len(), 2);
2939
2940 tree.press_key(Key::ArrowLeft, Modifiers::NONE);
2941
2942 assert_eq!(
2943 tree.active_overlays().len(),
2944 2,
2945 "a text affordance is not a menu cascade level — the back key must \
2946 leave both hosts standing"
2947 );
2948 assert_eq!(
2949 seen.borrow().as_slice(),
2950 &[Key::ArrowLeft],
2951 "ArrowLeft must reach the focused editor; swallowing it is the bug \
2952 that left the caret unable to step left in any dual-pane tab"
2953 );
2954 }
2955
2956 #[test]
2960 fn back_key_still_closes_a_submenu_over_a_mounted_text_affordance() {
2961 let mut tree = WidgetTree::new();
2962 let affordance = tree.add(FillWidget::new());
2963 let anchor = tree.add(FillWidget::new());
2964 let menu = tree.add(FillWidget::new());
2965 let submenu = tree.add(FillWidget::new());
2966 tree.layout(SizeProposal::exact(200.0, 100.0));
2967 tree.show_overlay_in_band(
2968 crate::overlay::OverlayRequest::new(
2969 affordance,
2970 anchor,
2971 crate::overlay::OverlayPlacement::FullViewport,
2972 crate::overlay::DismissBehavior::Manual,
2973 ),
2974 crate::overlay::OverlayBand::TextAffordance,
2975 );
2976 let parent = tree.show_overlay(crate::overlay::OverlayRequest::new(
2977 menu,
2978 anchor,
2979 crate::overlay::OverlayPlacement::Below,
2980 crate::overlay::DismissBehavior::Manual,
2981 ));
2982 tree.show_overlay(
2983 crate::overlay::OverlayRequest::new(
2984 submenu,
2985 menu,
2986 crate::overlay::OverlayPlacement::Below,
2987 crate::overlay::DismissBehavior::Manual,
2988 )
2989 .parent_overlay(parent),
2990 );
2991 assert_eq!(tree.active_overlays().len(), 3);
2992
2993 tree.press_key(Key::ArrowLeft, Modifiers::NONE);
2994 assert_eq!(
2995 tree.active_overlays().len(),
2996 2,
2997 "the submenu must still close: the affordance host narrows the \
2998 count, it does not disarm the back key"
2999 );
3000 }
3001
3002 #[test]
3007 fn back_key_does_not_dismiss_a_dialog_on_top_of_a_modal() {
3008 let mut tree = WidgetTree::new();
3009 let anchor = tree.add(FillWidget::new());
3010 let scrim1 = tree.add(FillWidget::new());
3011 let scrim2 = tree.add(FillWidget::new());
3012 let dialog = tree.add(FillWidget::new());
3013 tree.layout(SizeProposal::exact(200.0, 100.0));
3014 for c in [scrim1, scrim2] {
3016 tree.show_overlay(crate::overlay::OverlayRequest::new(
3017 c,
3018 anchor,
3019 crate::overlay::OverlayPlacement::Below,
3020 crate::overlay::DismissBehavior::Manual,
3021 ));
3022 }
3023 tree.show_overlay(crate::overlay::OverlayRequest::new(
3025 dialog,
3026 anchor,
3027 crate::overlay::OverlayPlacement::Centered,
3028 crate::overlay::DismissBehavior::Manual,
3029 ));
3030 assert_eq!(tree.active_overlays().len(), 3);
3031
3032 tree.press_key(Key::ArrowLeft, Modifiers::NONE);
3035 assert_eq!(
3036 tree.active_overlays().len(),
3037 3,
3038 "the back key must not close a dialog sitting on top of a modal"
3039 );
3040 }
3041
3042 #[test]
3043 fn escape_does_not_dismiss_manual_overlay() {
3044 let mut tree = WidgetTree::new();
3045 let anchor = tree.add(FillWidget::new().focusable());
3046 let content = tree.add(FillWidget::new());
3047 tree.layout(SizeProposal::exact(200.0, 100.0));
3048 tree.focus(anchor);
3049
3050 tree.show_overlay(crate::overlay::OverlayRequest::new(
3051 content,
3052 anchor,
3053 crate::overlay::OverlayPlacement::Below,
3054 crate::overlay::DismissBehavior::Manual,
3055 ));
3056
3057 assert_eq!(tree.active_overlays().len(), 1);
3058
3059 tree.press_key(Key::Escape, Modifiers::NONE);
3060 assert_eq!(tree.active_overlays().len(), 1);
3062 }
3063
3064 #[test]
3065 fn click_outside_dismisses_overlay() {
3066 let mut tree = WidgetTree::new();
3067 let anchor = tree.add(FillWidget::new());
3068 let content = tree.add(FillWidget::new());
3069 tree.layout(SizeProposal::exact(200.0, 100.0));
3070
3071 let overlay = tree.show_overlay(crate::overlay::OverlayRequest::new(
3072 content,
3073 anchor,
3074 crate::overlay::OverlayPlacement::Below,
3075 crate::overlay::DismissBehavior::ClickOutside,
3076 ));
3077
3078 tree.overlay_manager
3079 .set_content_bounds(overlay, teksilo_canvas::Size::new(100.0, 50.0));
3080
3081 assert_eq!(tree.active_overlays().len(), 1);
3082
3083 tree.dispatch_event(WidgetEvent::pointer_down(
3084 Point::new(500.0, 500.0),
3085 PointerButton::Primary,
3086 Modifiers::NONE,
3087 ));
3088 assert!(tree.active_overlays().is_empty());
3089 assert!(!tree.is_visible(content));
3090 }
3091
3092 #[test]
3093 fn cascade_dismissal() {
3094 let mut tree = WidgetTree::new();
3095 let anchor = tree.add(FillWidget::new());
3096 let content_a = tree.add(FillWidget::new());
3097 let content_b = tree.add(FillWidget::new());
3098 tree.layout(SizeProposal::exact(200.0, 100.0));
3099
3100 let parent = tree.show_overlay(crate::overlay::OverlayRequest::new(
3101 content_a,
3102 anchor,
3103 crate::overlay::OverlayPlacement::Below,
3104 crate::overlay::DismissBehavior::Manual,
3105 ));
3106 tree.show_overlay(
3107 crate::overlay::OverlayRequest::new(
3108 content_b,
3109 content_a,
3110 crate::overlay::OverlayPlacement::TrailingEdge,
3111 crate::overlay::DismissBehavior::Manual,
3112 )
3113 .parent_overlay(parent),
3114 );
3115
3116 assert_eq!(tree.active_overlays().len(), 2);
3117
3118 tree.dismiss_overlay(parent);
3119 assert!(tree.active_overlays().is_empty());
3120 assert!(!tree.is_visible(content_a));
3121 assert!(!tree.is_visible(content_b));
3122 }
3123
3124 #[test]
3125 fn dismissed_overlay_content_is_dormant_and_invisible() {
3126 let mut tree = WidgetTree::new();
3127 let anchor = tree.add(FillWidget::new());
3128 let content = tree.add(FillWidget::new());
3129 tree.layout(SizeProposal::exact(800.0, 600.0));
3130
3131 let id = tree.show_overlay(crate::overlay::OverlayRequest::new(
3132 content,
3133 anchor,
3134 crate::overlay::OverlayPlacement::Below,
3135 crate::overlay::DismissBehavior::Manual,
3136 ));
3137
3138 tree.layout(SizeProposal::exact(800.0, 600.0));
3139
3140 tree.dismiss_overlay(id);
3141 assert!(!tree.is_visible(content));
3142
3143 tree.layout(SizeProposal::exact(800.0, 600.0));
3144
3145 let center = tree.bounds(content).center();
3146 let hit = tree.hit_test(center);
3147 assert_ne!(hit, Some(content));
3148
3149 let _frame = tree.render();
3150 assert!(!tree.is_visible(content));
3151 }
3152
3153 #[test]
3154 fn tooltip_appears_after_delay() {
3155 let mut tree = WidgetTree::new();
3156 let anchor = tree.add(FillWidget::new());
3157 let tooltip = tree.add(FillWidget::new().label("Tooltip text"));
3158 tree.layout(SizeProposal::exact(200.0, 100.0));
3159
3160 tree.attach_tooltip(anchor, tooltip, std::time::Duration::from_millis(500));
3161
3162 let center = tree.bounds(anchor).center();
3163 tree.pointer_move(center);
3164 assert!(tree.active_overlays().is_empty());
3165
3166 tree.advance_time(std::time::Duration::from_millis(600));
3167
3168 assert_eq!(tree.active_overlays().len(), 1);
3169 assert!(tree.find_by_label("Tooltip text").is_some());
3170 }
3171
3172 #[test]
3173 fn tooltip_survives_theme_switch() {
3174 let mut tree = WidgetTree::new().with_theme(crate::presets::intui::light());
3179 let anchor = tree.add(FillWidget::new());
3180 let tooltip = tree.add(FillWidget::new().label("Tip"));
3181 tree.layout(SizeProposal::exact(200.0, 100.0));
3182
3183 tree.attach_tooltip(anchor, tooltip, std::time::Duration::from_millis(500));
3184
3185 tree.set_theme(crate::presets::intui::dark());
3186
3187 tree.pointer_move(tree.bounds(anchor).center());
3188 tree.advance_time(std::time::Duration::from_millis(600));
3189
3190 assert_eq!(tree.active_overlays().len(), 1);
3191 assert!(tree.find_by_label("Tip").is_some());
3192 }
3193
3194 #[test]
3195 fn tooltip_suppressed_when_hovering_anchor_owned_overlay_content() {
3196 let mut tree = WidgetTree::new();
3204 let anchor = tree.add(FillWidget::new());
3205 let panel = tree.add_child(anchor, FillWidget::new());
3208 let row = tree.add_child(panel, FillWidget::new());
3209 let tip = tree.add(FillWidget::new().label("Tip"));
3210 tree.layout(SizeProposal::exact(200.0, 100.0));
3211
3212 let delay = std::time::Duration::from_millis(100);
3213 tree.attach_tooltip(anchor, tip, delay);
3214
3215 tree.tooltip_pointer_enter(anchor);
3218 tree.advance_time(delay + std::time::Duration::from_millis(50));
3219 assert_eq!(
3220 tree.active_overlays().len(),
3221 1,
3222 "tooltip should appear when hovering the anchor itself"
3223 );
3224 tree.pointer_move(Point::new(500.0, 500.0));
3227 tree.advance_time(std::time::Duration::from_millis(150));
3228 assert!(tree.active_overlays().is_empty());
3229
3230 tree.show_overlay(crate::overlay::OverlayRequest::new(
3232 panel,
3233 anchor,
3234 crate::overlay::OverlayPlacement::Below,
3235 crate::overlay::DismissBehavior::Manual,
3236 ));
3237 assert_eq!(tree.active_overlays().len(), 1);
3238
3239 tree.tooltip_pointer_enter(row);
3242 tree.advance_time(delay + std::time::Duration::from_millis(50));
3243 assert_eq!(
3244 tree.active_overlays().len(),
3245 1,
3246 "anchor tooltip must not leak onto its own overlay's rows"
3247 );
3248 }
3249
3250 #[test]
3251 fn tooltip_inside_overlay_still_fires() {
3252 let mut tree = WidgetTree::new();
3256 let host = tree.add(FillWidget::new());
3257 let panel = tree.add_child(host, FillWidget::new());
3258 let row = tree.add_child(panel, FillWidget::new());
3259 let tip = tree.add(FillWidget::new().label("Row tip"));
3260 tree.layout(SizeProposal::exact(200.0, 100.0));
3261
3262 let delay = std::time::Duration::from_millis(100);
3263 tree.attach_tooltip(row, tip, delay);
3265
3266 tree.show_overlay(crate::overlay::OverlayRequest::new(
3267 panel,
3268 host,
3269 crate::overlay::OverlayPlacement::Below,
3270 crate::overlay::DismissBehavior::Manual,
3271 ));
3272 assert_eq!(tree.active_overlays().len(), 1);
3273
3274 tree.tooltip_pointer_enter(row);
3275 tree.advance_time(delay + std::time::Duration::from_millis(50));
3276 assert_eq!(
3277 tree.active_overlays().len(),
3278 2,
3279 "a row's own tooltip should still fire inside an overlay"
3280 );
3281 }
3282
3283 #[test]
3284 fn tooltip_dismissed_on_pointer_leave() {
3285 let mut tree = WidgetTree::new();
3286 let anchor = tree.add(FillWidget::new());
3287 let tooltip = tree.add(FillWidget::new().label("Tip"));
3288 tree.layout(SizeProposal::exact(200.0, 100.0));
3289
3290 tree.attach_tooltip(anchor, tooltip, std::time::Duration::from_millis(500));
3291
3292 tree.pointer_move(tree.bounds(anchor).center());
3293 tree.advance_time(std::time::Duration::from_millis(600));
3294 assert_eq!(tree.active_overlays().len(), 1);
3295
3296 tree.pointer_move(Point::new(500.0, 500.0));
3301 assert_eq!(
3302 tree.active_overlays().len(),
3303 1,
3304 "tooltip persists briefly after anchor-leave (hoverable grace)"
3305 );
3306 tree.advance_time(std::time::Duration::from_millis(150));
3307 assert!(
3308 tree.active_overlays().is_empty(),
3309 "tooltip dismissed after the 100ms grace outside anchor+overlay"
3310 );
3311 }
3312
3313 #[test]
3314 fn tooltip_not_shown_if_pointer_leaves_before_delay() {
3315 let mut tree = WidgetTree::new();
3316 let anchor = tree.add(FillWidget::new());
3317 let tooltip = tree.add(FillWidget::new().label("Tip"));
3318 tree.layout(SizeProposal::exact(200.0, 100.0));
3319
3320 tree.attach_tooltip(anchor, tooltip, std::time::Duration::from_millis(500));
3321
3322 tree.pointer_move(tree.bounds(anchor).center());
3323 tree.advance_time(std::time::Duration::from_millis(200));
3324 tree.pointer_move(Point::new(500.0, 500.0));
3325
3326 tree.advance_time(std::time::Duration::from_millis(500));
3327 assert!(tree.active_overlays().is_empty());
3328 }
3329
3330 #[test]
3331 fn tooltip_reshow_uses_short_delay_after_prior_tip() {
3332 let mut tree = WidgetTree::new();
3335 let a = tree.add(FillWidget::new());
3336 let b = tree.add(FillWidget::new());
3337 let tip_a = tree.add(FillWidget::new().label("Tip A"));
3338 let tip_b = tree.add(FillWidget::new().label("Tip B"));
3339 tree.layout(SizeProposal::exact(400.0, 200.0));
3340
3341 let delay = std::time::Duration::from_millis(500);
3342 tree.attach_tooltip(a, tip_a, delay);
3343 tree.attach_tooltip(b, tip_b, delay);
3344
3345 tree.tooltip_pointer_enter(a);
3348 tree.advance_time(std::time::Duration::from_millis(150));
3349 assert!(
3350 tree.active_overlays().is_empty(),
3351 "must not appear before full initial delay"
3352 );
3353 tree.advance_time(std::time::Duration::from_millis(400));
3354 assert_eq!(
3355 tree.active_overlays().len(),
3356 1,
3357 "first tip after initial delay"
3358 );
3359 assert!(tree.find_by_label("Tip A").is_some());
3360
3361 let mut noop = crate::window::NoopWindowOps;
3363 tree.tooltip_pointer_leave(a, &mut noop);
3364 tree.tooltip_pointer_enter(b);
3365 tree.advance_time(std::time::Duration::from_millis(120));
3366 assert!(
3367 tree.find_by_label("Tip B").is_some(),
3368 "second tip should use the short reshow delay while session is warm"
3369 );
3370 }
3371
3372 #[test]
3373 fn reshow_delay_reverts_to_full_after_the_session_grace_expires() {
3374 let mut tree = WidgetTree::new();
3380 tree.set_accessibility_preferences(false, true, 1.0);
3384 let a = tree.add(FillWidget::new());
3385 let b = tree.add(FillWidget::new());
3386 let tip_a = tree.add(FillWidget::new().label("Tip A"));
3387 let tip_b = tree.add(FillWidget::new().label("Tip B"));
3388 tree.layout(SizeProposal::exact(400.0, 200.0));
3389
3390 let delay = std::time::Duration::from_millis(500);
3391 tree.attach_tooltip(a, tip_a, delay);
3392 tree.attach_tooltip(b, tip_b, delay);
3393
3394 tree.tooltip_pointer_enter(a);
3396 tree.advance_time(std::time::Duration::from_millis(550));
3397 assert!(tree.find_by_label("Tip A").is_some(), "first tip shown");
3398
3399 tree.pointer_move(Point::new(900.0, 900.0));
3400 tree.advance_time(std::time::Duration::from_millis(150));
3401 assert!(tree.active_overlays().is_empty(), "A dismissed on leave");
3402
3403 tree.advance_time(super::TOOLTIP_SESSION_GRACE + std::time::Duration::from_millis(50));
3405
3406 tree.tooltip_pointer_enter(b);
3407 tree.advance_time(std::time::Duration::from_millis(150));
3408 assert!(
3409 tree.active_overlays().is_empty(),
3410 "session went cold — B must pay the FULL delay, not the 100 ms reshow"
3411 );
3412 tree.advance_time(std::time::Duration::from_millis(400));
3413 assert!(
3414 tree.find_by_label("Tip B").is_some(),
3415 "B still appears once its full delay elapses"
3416 );
3417 }
3418
3419 #[test]
3420 fn warm_reshow_scales_the_delay_rather_than_flattening_every_tier() {
3421 let mut tree = WidgetTree::new();
3427 let light = tree.add(FillWidget::new());
3428 let heavy = tree.add(FillWidget::new());
3429 let tip_light = tree.add(FillWidget::new().label("Light"));
3430 let tip_heavy = tree.add(FillWidget::new().label("Heavy"));
3431 tree.layout(SizeProposal::exact(400.0, 200.0));
3432
3433 tree.attach_tooltip(light, tip_light, std::time::Duration::from_millis(500));
3434 tree.attach_tooltip(heavy, tip_heavy, std::time::Duration::from_millis(700));
3435
3436 tree.tooltip_pointer_enter(light);
3438 tree.advance_time(std::time::Duration::from_millis(550));
3439 assert!(tree.find_by_label("Light").is_some(), "session warm");
3440
3441 let mut noop = crate::window::NoopWindowOps;
3442 tree.tooltip_pointer_leave(light, &mut noop);
3443 tree.tooltip_pointer_enter(heavy);
3444
3445 tree.advance_time(std::time::Duration::from_millis(120));
3447 assert!(
3448 tree.find_by_label("Heavy").is_none(),
3449 "a heavy tooltip must not fire at the light tier's reshow delay"
3450 );
3451 tree.advance_time(std::time::Duration::from_millis(40));
3453 assert!(
3454 tree.find_by_label("Heavy").is_some(),
3455 "heavy reshow is the scaled 140 ms"
3456 );
3457 }
3458
3459 #[test]
3460 fn a_pinned_sticky_tooltip_does_not_hold_the_session_warm() {
3461 let mut tree = WidgetTree::new();
3465 tree.set_accessibility_preferences(false, true, 1.0); let a = tree.add(FillWidget::new());
3467 let b = tree.add(FillWidget::new());
3468 let tip_a = tree.add(FillWidget::new().label("Pinned"));
3469 let tip_b = tree.add(FillWidget::new().label("Other"));
3470 tree.layout(SizeProposal::exact(400.0, 200.0));
3471
3472 tree.attach_tooltip_with_sticky(
3473 a,
3474 tip_a,
3475 std::time::Duration::from_millis(500),
3476 Some(std::time::Duration::from_millis(100)),
3477 );
3478 tree.attach_tooltip(b, tip_b, std::time::Duration::from_millis(500));
3479
3480 tree.tooltip_pointer_enter(a);
3481 tree.advance_time(std::time::Duration::from_millis(550));
3482 assert!(tree.find_by_label("Pinned").is_some());
3483 tree.promote_tooltip_to_sticky(tip_a);
3484
3485 tree.advance_time(super::TOOLTIP_SESSION_GRACE + std::time::Duration::from_millis(50));
3487 tree.tooltip_pointer_enter(b);
3488 tree.advance_time(std::time::Duration::from_millis(150));
3489 assert!(
3490 tree.find_by_label("Other").is_none(),
3491 "a pinned sticky must not keep every other anchor on the reshow path"
3492 );
3493 }
3494
3495 #[test]
3496 fn reattaching_a_tooltip_does_not_grow_the_entry_table() {
3497 let mut tree = WidgetTree::new();
3503 let anchor = tree.add(FillWidget::new());
3504 tree.layout(SizeProposal::exact(200.0, 100.0));
3505
3506 let delay = std::time::Duration::from_millis(100);
3507 for _ in 0..25 {
3508 let tip = tree.add(FillWidget::new().label("Tip"));
3510 tree.attach_tooltip(anchor, tip, delay);
3511 assert_eq!(
3512 tree.tooltip_entry_count(),
3513 1,
3514 "an anchor must own exactly one tooltip entry across rebuilds"
3515 );
3516 }
3517
3518 tree.tooltip_pointer_enter(anchor);
3520 tree.advance_time(delay + std::time::Duration::from_millis(50));
3521 assert_eq!(
3522 tree.active_overlays().len(),
3523 1,
3524 "latest tooltip still shows"
3525 );
3526 }
3527
3528 #[test]
3529 fn destroying_an_anchor_reaps_its_tooltip_entry() {
3530 let mut tree = WidgetTree::new();
3533 let host = tree.add(FillWidget::new());
3534 let anchor = tree.add_child(host, FillWidget::new());
3535 let tip = tree.add(FillWidget::new().label("Tip"));
3536 tree.layout(SizeProposal::exact(200.0, 100.0));
3537
3538 tree.attach_tooltip(anchor, tip, std::time::Duration::from_millis(100));
3539 assert_eq!(tree.tooltip_entry_count(), 1);
3540
3541 tree.destroy_subtree(anchor);
3542 assert_eq!(
3543 tree.tooltip_entry_count(),
3544 0,
3545 "destroying the anchor must reap its entry and content node"
3546 );
3547 }
3548
3549 #[test]
3550 fn a_leaving_tooltip_schedules_a_wake_for_its_dismissal() {
3551 let mut tree = WidgetTree::new();
3556 tree.set_accessibility_preferences(false, true, 1.0); let anchor = tree.add(FillWidget::new());
3558 let tip = tree.add(FillWidget::new().label("Tip"));
3559 tree.layout(SizeProposal::exact(400.0, 200.0));
3560
3561 tree.attach_tooltip(anchor, tip, std::time::Duration::from_millis(100));
3562 tree.pointer_move(tree.bounds(anchor).center());
3563 tree.advance_time(std::time::Duration::from_millis(150));
3564 assert_eq!(tree.active_overlays().len(), 1, "tooltip shown");
3565
3566 assert!(
3568 tree.next_timer_deadline().is_none(),
3569 "a settled tooltip under the pointer schedules nothing"
3570 );
3571
3572 tree.pointer_move(Point::new(900.0, 900.0));
3575 assert!(
3576 tree.next_timer_deadline().is_some(),
3577 "the PointerLeave grace must contribute a wake deadline"
3578 );
3579 }
3580
3581 #[test]
3592 fn a_fading_out_overlay_schedules_a_wake_for_its_deferred_removal() {
3593 let mut tree = WidgetTree::new();
3594 let anchor = tree.add(FillWidget::new());
3595 let content = tree.add(FillWidget::new().label("Faded"));
3596 tree.layout(SizeProposal::exact(200.0, 100.0));
3597
3598 let id = tree.show_overlay(
3599 crate::overlay::OverlayRequest::new(
3600 content,
3601 anchor,
3602 crate::overlay::OverlayPlacement::Below,
3603 crate::overlay::DismissBehavior::Manual,
3604 )
3605 .with_fade(std::time::Duration::from_millis(200)),
3606 );
3607 tree.dismiss_overlay(id);
3608 assert!(
3609 tree.is_visible(content),
3610 "precondition: the removal is deferred, so there is something to wake for"
3611 );
3612
3613 let expected = tree.overlay_manager.next_fade_dismiss_deadline();
3619 assert!(
3620 expected.is_some(),
3621 "precondition: the fade start was stamped, so there is a deadline to compare against"
3622 );
3623 assert_eq!(
3624 tree.next_timer_deadline(),
3625 expected,
3626 "the wake deadline must be the deferred removal's own, not merely some deadline"
3627 );
3628 }
3629
3630 #[test]
3631 fn pressing_cancels_a_pending_dwell_and_dismisses_a_shown_tooltip() {
3632 let mut tree = WidgetTree::new();
3633 let anchor = tree.add(FillWidget::new());
3634 let tip = tree.add(FillWidget::new().label("Tip"));
3635 tree.layout(SizeProposal::exact(400.0, 200.0));
3636
3637 let delay = std::time::Duration::from_millis(500);
3638 tree.attach_tooltip(anchor, tip, delay);
3639
3640 tree.pointer_move(tree.bounds(anchor).center());
3643 tree.advance_time(std::time::Duration::from_millis(300));
3644 tree.tooltip_pointer_press(None);
3645 tree.advance_time(std::time::Duration::from_millis(400));
3646 assert!(
3647 tree.active_overlays().is_empty(),
3648 "a press must cancel the pending dwell, not merely delay it"
3649 );
3650
3651 tree.tooltip_pointer_enter(anchor);
3654 tree.advance_time(delay + std::time::Duration::from_millis(50));
3655 assert_eq!(tree.active_overlays().len(), 1, "tooltip shown again");
3656 tree.tooltip_pointer_press(Some(tree.bounds(anchor).center()));
3657 assert!(
3658 tree.active_overlays().is_empty(),
3659 "a press must dismiss the shown tooltip"
3660 );
3661 }
3662
3663 #[test]
3664 fn window_deactivation_retires_hover_tooltips() {
3665 let mut tree = WidgetTree::new();
3666 let anchor = tree.add(FillWidget::new());
3667 let tip = tree.add(FillWidget::new().label("Tip"));
3668 tree.layout(SizeProposal::exact(400.0, 200.0));
3669
3670 tree.attach_tooltip(anchor, tip, std::time::Duration::from_millis(100));
3671 tree.pointer_move(tree.bounds(anchor).center());
3672 tree.advance_time(std::time::Duration::from_millis(150));
3673 assert_eq!(tree.active_overlays().len(), 1);
3674
3675 tree.set_window_active(false);
3676 assert!(
3677 tree.active_overlays().is_empty(),
3678 "a tooltip must not float over another window's chrome"
3679 );
3680 }
3681
3682 #[test]
3683 fn only_the_innermost_anchor_arms_its_dwell() {
3684 let mut tree = WidgetTree::new();
3687 let panel = tree.add(FillWidget::new());
3688 let row = tree.add_child(panel, FillWidget::new());
3689 let panel_tip = tree.add(FillWidget::new().label("Panel"));
3690 let row_tip = tree.add(FillWidget::new().label("Row"));
3691 tree.layout(SizeProposal::exact(400.0, 200.0));
3692
3693 let delay = std::time::Duration::from_millis(100);
3694 tree.attach_tooltip(panel, panel_tip, delay);
3695 tree.attach_tooltip(row, row_tip, delay);
3696
3697 tree.tooltip_pointer_enter(row);
3698 tree.advance_time(delay + std::time::Duration::from_millis(50));
3699
3700 assert_eq!(
3701 tree.active_overlays().len(),
3702 1,
3703 "exactly one tooltip may open for a hover"
3704 );
3705 assert!(
3706 tree.find_by_label("Row").is_some(),
3707 "the innermost anchor wins"
3708 );
3709 }
3710
3711 #[test]
3712 fn escape_dismisses_a_hover_tooltip_and_falls_through_to_the_menu_below() {
3713 let mut tree = WidgetTree::new();
3717 tree.set_accessibility_preferences(false, true, 1.0); let anchor = tree.add(FillWidget::new());
3719 let menu = tree.add(FillWidget::new());
3720 let tip = tree.add(FillWidget::new().label("Tip"));
3721 tree.layout(SizeProposal::exact(400.0, 200.0));
3722
3723 let menu_overlay = tree.show_overlay(crate::overlay::OverlayRequest::new(
3724 menu,
3725 anchor,
3726 crate::overlay::OverlayPlacement::Below,
3727 crate::overlay::DismissBehavior::EscapeOrClickOutside,
3728 ));
3729 assert_eq!(tree.active_overlays().len(), 1);
3730
3731 tree.attach_tooltip(anchor, tip, std::time::Duration::from_millis(100));
3733 tree.tooltip_pointer_enter(anchor);
3734 tree.advance_time(std::time::Duration::from_millis(150));
3735 assert_eq!(
3736 tree.active_overlays().len(),
3737 2,
3738 "tooltip sits above the menu"
3739 );
3740
3741 let dismissed = tree.overlay_manager.try_dismiss_top_on_escape();
3743 assert!(dismissed.is_some(), "Escape must dismiss the hover tooltip");
3744 assert_eq!(tree.active_overlays().len(), 1);
3745
3746 let dismissed = tree.overlay_manager.try_dismiss_top_on_escape();
3748 assert_eq!(
3749 dismissed.map(|(id, _, _)| id),
3750 Some(menu_overlay),
3751 "Escape must then reach the menu underneath"
3752 );
3753 assert!(tree.active_overlays().is_empty());
3754 }
3755
3756 #[test]
3757 fn tooltip_timer_restarts_when_pointer_keeps_moving() {
3758 let mut tree = WidgetTree::new();
3761 let anchor = tree.add(FillWidget::new());
3762 let tooltip = tree.add(FillWidget::new().label("Tip"));
3763 tree.layout(SizeProposal::exact(200.0, 100.0));
3764
3765 tree.attach_tooltip(anchor, tooltip, std::time::Duration::from_millis(500));
3766
3767 let start = tree.bounds(anchor).center();
3768 tree.pointer_move(start);
3769 tree.advance_time(std::time::Duration::from_millis(300));
3770 tree.pointer_move(Point::new(start.x + 20.0, start.y));
3772 tree.advance_time(std::time::Duration::from_millis(300));
3775 assert!(
3776 tree.active_overlays().is_empty(),
3777 "moving past stationary slop must restart the delay"
3778 );
3779 tree.advance_time(std::time::Duration::from_millis(250));
3780 assert_eq!(
3781 tree.active_overlays().len(),
3782 1,
3783 "tooltip appears after a full delay of stillness"
3784 );
3785 }
3786
3787 #[test]
3788 fn timed_overlay_auto_dismisses_after_duration() {
3789 let mut tree = WidgetTree::new();
3790 let anchor = tree.add(FillWidget::new());
3791 let content = tree.add(FillWidget::new().label("Toast"));
3792 tree.layout(SizeProposal::exact(200.0, 100.0));
3793
3794 tree.show_overlay_for(
3795 crate::overlay::OverlayRequest::new(
3796 content,
3797 anchor,
3798 crate::overlay::OverlayPlacement::Below,
3799 crate::overlay::DismissBehavior::Manual,
3800 ),
3801 std::time::Duration::from_millis(300),
3802 );
3803
3804 assert_eq!(tree.active_overlays().len(), 1);
3805
3806 tree.advance_time(std::time::Duration::from_millis(200));
3807 assert_eq!(tree.active_overlays().len(), 1);
3808
3809 tree.advance_time(std::time::Duration::from_millis(150));
3810 assert!(tree.active_overlays().is_empty());
3811 assert!(!tree.is_visible(content));
3812 }
3813
3814 #[test]
3815 fn fade_dismiss_keeps_overlay_off_active_list_immediately() {
3816 let mut tree = WidgetTree::new();
3824 let anchor = tree.add(FillWidget::new());
3825 let content = tree.add(FillWidget::new().label("Faded"));
3826 tree.layout(SizeProposal::exact(200.0, 100.0));
3827
3828 let id = tree.show_overlay(
3829 crate::overlay::OverlayRequest::new(
3830 content,
3831 anchor,
3832 crate::overlay::OverlayPlacement::Below,
3833 crate::overlay::DismissBehavior::Manual,
3834 )
3835 .with_fade(std::time::Duration::from_millis(100)),
3836 );
3837 assert_eq!(tree.active_overlays().len(), 1);
3838
3839 tree.dismiss_overlay(id);
3840 assert!(tree.active_overlays().is_empty());
3845 }
3846
3847 #[test]
3848 fn fade_dismiss_defers_content_dormancy_until_sim_tween_completes() {
3849 let mut tree = WidgetTree::new();
3855 let anchor = tree.add(FillWidget::new());
3856 let content = tree.add(FillWidget::new().label("Faded"));
3857 tree.layout(SizeProposal::exact(200.0, 100.0));
3858
3859 let id = tree.show_overlay(
3860 crate::overlay::OverlayRequest::new(
3861 content,
3862 anchor,
3863 crate::overlay::OverlayPlacement::Below,
3864 crate::overlay::DismissBehavior::Manual,
3865 )
3866 .with_fade(std::time::Duration::from_millis(100)),
3867 );
3868 tree.advance_time(std::time::Duration::from_secs(1));
3874
3875 tree.dismiss_overlay(id);
3876 assert!(
3877 tree.is_visible(content),
3878 "content stays active during fade-out"
3879 );
3880
3881 tree.advance_time(std::time::Duration::from_millis(60));
3885 assert!(
3886 tree.is_visible(content),
3887 "60 ms into a 100 ms tween the content is still up"
3888 );
3889
3890 tree.advance_time(std::time::Duration::from_millis(90));
3891 assert!(
3892 !tree.is_visible(content),
3893 "after sim-time past the tween window, deferred removal fires"
3894 );
3895 }
3896
3897 #[test]
3909 fn an_auto_dismissed_fade_starts_at_the_instant_the_dismiss_ran() {
3910 let mut tree = WidgetTree::new();
3911 let anchor = tree.add(FillWidget::new());
3912 let content = tree.add(FillWidget::new().label("Toast"));
3913 tree.layout(SizeProposal::exact(200.0, 100.0));
3914
3915 tree.show_overlay_for(
3916 crate::overlay::OverlayRequest::new(
3917 content,
3918 anchor,
3919 crate::overlay::OverlayPlacement::Below,
3920 crate::overlay::DismissBehavior::Manual,
3921 )
3922 .with_fade(std::time::Duration::from_millis(100)),
3923 std::time::Duration::from_millis(500),
3924 );
3925 assert_eq!(tree.active_overlays().len(), 1);
3926
3927 tree.advance_time(std::time::Duration::from_millis(600));
3931 assert!(
3932 tree.active_overlays().is_empty(),
3933 "precondition: the auto-dismiss fired inside this advance"
3934 );
3935 assert!(
3936 tree.is_visible(content),
3937 "the fade must start at the instant the dismiss ran, so the \
3938 content survives the frame that dismissed it"
3939 );
3940
3941 tree.advance_time(std::time::Duration::from_millis(150));
3942 assert!(
3943 !tree.is_visible(content),
3944 "and is reaped once the tween's own window has passed"
3945 );
3946 }
3947
3948 #[derive(Debug)]
3954 struct Block(f32, f32);
3955
3956 impl Widget for Block {
3957 fn layout_response(
3958 &self,
3959 _proposal: SizeProposal,
3960 _ctx: &LayoutContext,
3961 ) -> crate::widget::LayoutResponse {
3962 teksilo_canvas::Size::new(self.0, self.1).into()
3963 }
3964 }
3965
3966 fn panel_and_an_overlay_above(
3970 panel_dismiss: crate::overlay::DismissBehavior,
3971 top: impl Widget + 'static,
3972 top_dismiss: crate::overlay::DismissBehavior,
3973 top_at: Point,
3974 inert: bool,
3975 ) -> (WidgetTree, WidgetId, WidgetId) {
3976 let mut tree = WidgetTree::new();
3977 let panel = tree.add(Block(100.0, 60.0));
3978 let top = tree.add(top);
3979 let beneath = tree.add(FillWidget::new());
3983 tree.show_overlay(crate::overlay::OverlayRequest::new(
3984 panel,
3985 beneath,
3986 crate::overlay::OverlayPlacement::AtPointer(Point::new(20.0, 20.0)),
3987 panel_dismiss,
3988 ));
3989 let request = crate::overlay::OverlayRequest::new(
3990 top,
3991 beneath,
3992 crate::overlay::OverlayPlacement::AtPointer(top_at),
3993 top_dismiss,
3994 );
3995 tree.show_overlay(if inert { request.inert() } else { request });
3996 tree.layout(SizeProposal::exact(400.0, 200.0));
3997 (tree, panel, top)
3998 }
3999
4000 fn panel_and_an_inert_overlay(
4003 panel_dismiss: crate::overlay::DismissBehavior,
4004 inert_dismiss: crate::overlay::DismissBehavior,
4005 inert_at: Point,
4006 ) -> (WidgetTree, WidgetId, WidgetId) {
4007 panel_and_an_overlay_above(
4008 panel_dismiss,
4009 Block(60.0, 40.0),
4010 inert_dismiss,
4011 inert_at,
4012 true,
4013 )
4014 }
4015
4016 #[test]
4017 fn an_inert_overlay_hands_the_point_to_the_overlay_beneath_it() {
4018 let (tree, panel, inert) = panel_and_an_inert_overlay(
4019 crate::overlay::DismissBehavior::Manual,
4020 crate::overlay::DismissBehavior::EscapeKey,
4021 Point::new(30.0, 30.0),
4022 );
4023 let at = Point::new(40.0, 40.0);
4024 let over = tree.overlay_manager.find_by_content(inert).unwrap();
4025 assert!(
4026 tree.overlay_content_bounds(over).unwrap().contains(at),
4027 "fixture: the inert overlay covers the point"
4028 );
4029 assert_eq!(
4030 tree.hit_test(at),
4031 Some(panel),
4032 "the panel under it takes the point, not the window under both"
4033 );
4034 }
4035
4036 #[test]
4037 fn a_press_on_an_inert_overlay_is_outside_the_overlays_it_is_outside_of() {
4038 let (mut tree, panel, inert) = panel_and_an_inert_overlay(
4039 crate::overlay::DismissBehavior::ClickOutside,
4040 crate::overlay::DismissBehavior::EscapeKey,
4041 Point::new(200.0, 100.0),
4042 );
4043 let at = Point::new(220.0, 110.0);
4044 let over = tree.overlay_manager.find_by_content(inert).unwrap();
4045 assert!(
4046 tree.overlay_content_bounds(over).unwrap().contains(at),
4047 "fixture: the press lands on the inert overlay"
4048 );
4049 tree.pointer_down_button(at, crate::event::PointerButton::Primary);
4050 assert_eq!(
4051 tree.overlay_manager.find_by_content(panel),
4052 None,
4053 "the press was outside the panel, whatever floats over that spot",
4054 );
4055 }
4056
4057 #[test]
4058 fn an_inert_overlay_is_not_an_interactive_rect() {
4059 let (tree, panel, _inert) = panel_and_an_inert_overlay(
4062 crate::overlay::DismissBehavior::Manual,
4063 crate::overlay::DismissBehavior::EscapeKey,
4064 Point::new(200.0, 100.0),
4065 );
4066 let panel_bounds = tree
4067 .overlay_content_bounds(tree.overlay_manager.find_by_content(panel).unwrap())
4068 .unwrap();
4069 assert_eq!(tree.overlay_manager.interactive_rects(), vec![panel_bounds]);
4070 }
4071
4072 #[test]
4073 fn escape_steps_over_an_inert_overlay_to_the_one_beneath() {
4074 let (mut tree, panel, inert) = panel_and_an_inert_overlay(
4077 crate::overlay::DismissBehavior::EscapeKey,
4078 crate::overlay::DismissBehavior::Manual,
4079 Point::new(200.0, 100.0),
4080 );
4081 tree.press_key(Key::Escape, Modifiers::NONE);
4082 assert_eq!(
4083 tree.overlay_manager.find_by_content(panel),
4084 None,
4085 "the panel beneath took the Escape"
4086 );
4087 assert!(
4088 tree.overlay_manager.find_by_content(inert).is_some(),
4089 "and the inert one, which Escape may not close, is still up"
4090 );
4091 }
4092
4093 #[test]
4094 fn an_inert_overlay_over_the_window_hands_the_point_to_the_window() {
4095 let mut tree = WidgetTree::new();
4099 let page = tree.add(FillWidget::new());
4100 let inert = tree.add(Block(60.0, 40.0));
4101 tree.show_overlay(
4102 crate::overlay::OverlayRequest::new(
4103 inert,
4104 page,
4105 crate::overlay::OverlayPlacement::AtPointer(Point::new(200.0, 100.0)),
4106 crate::overlay::DismissBehavior::EscapeKey,
4107 )
4108 .inert(),
4109 );
4110 tree.layout(SizeProposal::exact(400.0, 200.0));
4111 let at = Point::new(220.0, 110.0);
4112 assert!(
4113 tree.overlay_content_bounds(tree.overlay_manager.find_by_content(inert).unwrap())
4114 .unwrap()
4115 .contains(at),
4116 "fixture: the inert overlay covers the point"
4117 );
4118 assert_eq!(tree.hit_test(at), Some(page));
4119 tree.pointer_move(at);
4120 assert_eq!(tree.hovered(), Some(page), "and so does a hover");
4121 }
4122
4123 #[test]
4124 fn a_root_that_is_hit_transparent_and_pass_through_is_not_inert() {
4125 use crate::widget_builder::WidgetBuilder;
4126
4127 let flagged = || {
4131 Block(60.0, 40.0)
4132 .hit_transparent(true)
4133 .event_pass_through(true)
4134 };
4135 let at = Point::new(220.0, 110.0);
4136
4137 let (mut tree, panel, top) = panel_and_an_overlay_above(
4138 crate::overlay::DismissBehavior::ClickOutside,
4139 flagged(),
4140 crate::overlay::DismissBehavior::Manual,
4141 Point::new(200.0, 100.0),
4142 false,
4143 );
4144 let over = tree.overlay_manager.find_by_content(top).unwrap();
4145 assert!(
4146 tree.overlay_content_bounds(over).unwrap().contains(at),
4147 "fixture: the press lands on the flagged overlay"
4148 );
4149 assert_eq!(
4150 tree.overlay_manager.hit_test(at),
4151 Some(over),
4152 "the stack still answers with the overlay"
4153 );
4154 assert_eq!(
4155 tree.overlay_manager.interactive_rects().len(),
4156 2,
4157 "which is an interactive rect"
4158 );
4159 tree.pointer_down_button(at, crate::event::PointerButton::Primary);
4160 tree.pointer_up_button(at, crate::event::PointerButton::Primary);
4161 assert!(
4162 tree.overlay_manager.find_by_content(panel).is_some(),
4163 "a press on it is inside the stack, so the panel beneath stays"
4164 );
4165
4166 let (mut tree, panel, top) = panel_and_an_overlay_above(
4167 crate::overlay::DismissBehavior::EscapeKey,
4168 flagged(),
4169 crate::overlay::DismissBehavior::Manual,
4170 Point::new(200.0, 100.0),
4171 false,
4172 );
4173 tree.press_key(Key::Escape, Modifiers::NONE);
4174 assert!(
4175 tree.overlay_manager.find_by_content(panel).is_some()
4176 && tree.overlay_manager.find_by_content(top).is_some(),
4177 "a `Manual` overlay owns the Escape: nothing beneath it closes"
4178 );
4179
4180 let (mut tree, panel, top) = panel_and_an_overlay_above(
4181 crate::overlay::DismissBehavior::EscapeKey,
4182 flagged(),
4183 crate::overlay::DismissBehavior::EscapeKey,
4184 Point::new(200.0, 100.0),
4185 false,
4186 );
4187 tree.press_key(Key::Escape, Modifiers::NONE);
4188 assert_eq!(
4189 tree.overlay_manager.find_by_content(top),
4190 None,
4191 "an `EscapeKey` one is what the Escape closes"
4192 );
4193 assert!(
4194 tree.overlay_manager.find_by_content(panel).is_some(),
4195 "and it is spent there"
4196 );
4197 }
4198
4199 #[test]
4200 fn an_inert_overlay_records_no_focus_to_give_back_on_any_show_path() {
4201 type Show = fn(&mut WidgetTree, crate::overlay::OverlayRequest, WidgetId);
4202 fn from_a_handler(
4203 tree: &mut WidgetTree,
4204 show: impl FnOnce(&mut crate::widget::EventContext),
4205 ) {
4206 tree.run_with_event_context(&mut crate::window::NoopWindowOps, show);
4207 }
4208 let paths: [(&str, Show); 5] = [
4209 ("show_overlay", |tree, request, _| {
4210 from_a_handler(tree, |ctx| ctx.show_overlay(request))
4211 }),
4212 ("show_overlay_for", |tree, request, _| {
4213 from_a_handler(tree, |ctx| {
4214 ctx.show_overlay_for(request, std::time::Duration::from_secs(60))
4215 })
4216 }),
4217 ("show_overlay_with_reveal", |tree, request, _| {
4218 from_a_handler(tree, |ctx| {
4219 ctx.show_overlay_with_reveal(
4220 request,
4221 crate::signal::Signal::new(0.0),
4222 std::time::Duration::from_millis(100),
4223 )
4224 })
4225 }),
4226 ("show_overlay_after", |tree, request, _| {
4227 from_a_handler(tree, |ctx| {
4228 ctx.show_overlay_after(request, std::time::Duration::ZERO)
4229 });
4230 tree.layout(SizeProposal::exact(400.0, 200.0));
4231 }),
4232 ("show_overlay_from_source", |tree, request, field| {
4233 tree.show_overlay_from_source(field, request);
4234 }),
4235 ];
4236 for (path, show) in paths {
4237 let mut tree = WidgetTree::new();
4238 let field = tree.add(FillWidget::new().focusable());
4239 let inert = tree.add(Block(60.0, 40.0));
4240 let ordinary = tree.add(Block(60.0, 40.0));
4241 tree.layout(SizeProposal::exact(400.0, 200.0));
4242 tree.focus(field);
4243 for (content, inert) in [(inert, true), (ordinary, false)] {
4244 let request = crate::overlay::OverlayRequest::new(
4245 content,
4246 field,
4247 crate::overlay::OverlayPlacement::AtPointer(Point::new(20.0, 20.0)),
4248 crate::overlay::DismissBehavior::EscapeKey,
4249 );
4250 show(
4251 &mut tree,
4252 if inert { request.inert() } else { request },
4253 field,
4254 );
4255 }
4256 let restore = |content: WidgetId| {
4257 tree.overlay_manager
4258 .stack
4259 .iter()
4260 .find(|o| o.content_id == content)
4261 .map(|o| o.focus_restore)
4262 };
4263 assert_eq!(
4264 restore(ordinary),
4265 Some(Some(field)),
4266 "{path}: an ordinary one does"
4267 );
4268 assert_eq!(
4269 restore(inert),
4270 Some(None),
4271 "{path}: an inert one took no focus"
4272 );
4273 }
4274 }
4275}