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 dwell_promotes: false,
222 armed_by_focus: false,
223 armed_by_hold: false,
224 suppressed_until_focus_leaves: false,
225 placement,
226 });
227 }
228
229 pub(crate) fn set_tooltip_description_owner(
248 &mut self,
249 anchor_id: WidgetId,
250 owner_id: WidgetId,
251 ) {
252 if let Some(entry) = self
253 .tooltips
254 .iter_mut()
255 .find(|entry| entry.anchor_id == anchor_id)
256 {
257 entry.description_owner_id = owner_id;
258 }
259 }
260
261 fn tooltip_overlay_placement(
268 placement: crate::overlay::TooltipPlacement,
269 ) -> crate::overlay::OverlayPlacement {
270 match placement {
271 crate::overlay::TooltipPlacement::Below => {
272 crate::overlay::OverlayPlacement::NearAnchor {
273 offset: teksilo_canvas::Vec2::new(0.0, 8.0),
274 }
275 }
276 crate::overlay::TooltipPlacement::Side => {
277 crate::overlay::OverlayPlacement::TrailingEdge
278 }
279 }
280 }
281
282 pub(super) fn process_tooltips(&mut self) {
283 let sim_now = self.sim_clock;
284 let session_active = self.tooltip_session_active_sim(sim_now);
285 self.process_tooltips_impl(
286 |entry| {
287 entry
288 .hover_start
289 .map(|start| sim_now.saturating_duration_since(start))
290 },
291 session_active,
292 true,
293 );
294 }
295
296 pub(super) fn process_tooltips_real(&mut self) {
297 let real_now = std::time::Instant::now();
298 let session_active = self.tooltip_session_active_real(real_now);
299 self.process_tooltips_impl(
300 |entry| {
301 entry
302 .real_hover_start
303 .map(|start| real_now.saturating_duration_since(start))
304 },
305 session_active,
306 false,
307 );
308 }
309
310 fn any_tooltip_in_session(&self) -> bool {
319 self.tooltips
320 .iter()
321 .any(|e| e.overlay_id.is_some() && !e.is_sticky)
322 }
323
324 fn tooltip_session_active_sim(&self, now: std::time::Instant) -> bool {
326 self.any_tooltip_in_session()
327 || self
328 .tooltip_session_until_sim
329 .is_some_and(|until| now < until)
330 }
331
332 fn tooltip_session_active_real(&self, now: std::time::Instant) -> bool {
334 self.any_tooltip_in_session()
335 || self
336 .tooltip_session_until_real
337 .is_some_and(|until| now < until)
338 }
339
340 fn effective_tooltip_delay(
354 &self,
355 entry_delay: std::time::Duration,
356 session_active: bool,
357 ) -> std::time::Duration {
358 if !session_active {
359 return entry_delay;
360 }
361 let motion = &self.theme.motion;
362 let base = motion.tooltip_delay.as_secs_f64();
363 if base <= 0.0 {
364 return entry_delay.min(motion.tooltip_reshow_delay);
367 }
368 let ratio = motion.tooltip_reshow_delay.as_secs_f64() / base;
369 entry_delay.mul_f64(ratio).min(entry_delay)
371 }
372
373 fn process_tooltips_impl(
374 &mut self,
375 elapsed_fn: impl Fn(&TooltipEntry) -> Option<std::time::Duration>,
376 session_active: bool,
377 use_sim_clock: bool,
378 ) {
379 let dismissed_indices: Vec<usize> = self
385 .tooltips
386 .iter()
387 .enumerate()
388 .filter_map(|(i, e)| match e.overlay_id {
389 Some(oid) if !self.overlay_manager.stack.iter().any(|o| o.id == oid) => Some(i),
390 _ => None,
391 })
392 .collect();
393 let any_dismissed = !dismissed_indices.is_empty();
394 for i in dismissed_indices {
395 let e = &mut self.tooltips[i];
396 e.overlay_id = None;
397 e.is_sticky = false;
398 e.promoted_by_focus = false;
399 e.armed_by_hold = false;
400 e.shown_at_sim = None;
401 e.shown_at_real = None;
402 e.hover_origin = None;
403 if let Some(sink) = e.shown_at_sink.as_ref() {
404 sink.set(None);
405 }
406 }
407 if any_dismissed {
410 let grace = super::TOOLTIP_SESSION_GRACE;
411 if use_sim_clock {
412 self.tooltip_session_until_sim = Some(self.sim_clock + grace);
413 } else {
414 self.tooltip_session_until_real = Some(std::time::Instant::now() + grace);
415 }
416 }
417
418 let session_active = session_active
423 || self.any_tooltip_in_session()
424 || if use_sim_clock {
425 self.tooltip_session_active_sim(self.sim_clock)
426 } else {
427 self.tooltip_session_active_real(std::time::Instant::now())
428 };
429
430 let mut to_show = Vec::new();
431 let pending: Vec<usize> = self
434 .tooltips
435 .iter()
436 .enumerate()
437 .filter(|(_, entry)| entry.overlay_id.is_none())
438 .filter(|(_, entry)| self.arena.is_active(entry.anchor_id))
439 .filter_map(|(i, entry)| {
440 let delay = self.effective_tooltip_delay(entry.delay, session_active);
441 let elapsed = elapsed_fn(entry)?;
442 (elapsed >= delay).then_some(i)
443 })
444 .collect();
445 for index in pending {
446 let content_id = self.tooltips[index].content_id;
451 self.materialize_deferred(content_id);
452 let body_id = self.tooltip_content_node(content_id);
463 if !self
464 .arena
465 .get(body_id)
466 .is_none_or(|node| node.widget.tooltip_has_content())
467 {
468 let entry = &mut self.tooltips[index];
469 entry.hover_start = None;
470 entry.real_hover_start = None;
471 entry.hover_origin = None;
472 continue;
473 }
474 let entry = &mut self.tooltips[index];
475 to_show.push((
476 entry.anchor_id,
477 entry.content_id,
478 entry.placement,
479 entry.armed_by_focus,
480 entry.armed_by_hold,
481 ));
482 entry.hover_start = None;
483 entry.real_hover_start = None;
484 entry.hover_origin = None;
485 }
486 let sim_now = self.sim_clock;
487 let real_now = std::time::Instant::now();
488 let fade_duration = if self.prefers_reduced_motion || session_active {
495 None
496 } else {
497 Some(self.theme.motion.duration_fast)
498 };
499 for (anchor_id, content_id, placement, by_focus, by_hold) in to_show {
500 self.arena.activate(content_id);
501 let dismiss = if by_focus || by_hold {
510 crate::overlay::DismissBehavior::EscapeOrClickOutside
511 } else {
512 crate::overlay::DismissBehavior::PointerLeave {
513 delay: std::time::Duration::from_millis(100),
514 }
515 };
516 let mut request = crate::overlay::OverlayRequest::new(
517 content_id,
518 anchor_id,
519 Self::tooltip_overlay_placement(placement),
520 dismiss,
521 );
522 request.fade_duration = fade_duration;
523 let oid = if by_hold {
524 self.show_overlay_for(request, super::touch_route::TOUCH_TOOLTIP_DISMISS)
525 } else {
526 self.show_overlay(request)
527 };
528 if by_focus && let Some(focused) = self.focused {
529 self.record_top_focus_restore(focused);
530 }
531 if let Some(entry) = self
532 .tooltips
533 .iter_mut()
534 .find(|e| e.content_id == content_id)
535 {
536 entry.overlay_id = Some(oid);
537 entry.shown_at_sim = Some(sim_now);
538 entry.shown_at_real = Some(real_now);
539 entry.promoted_by_focus = by_focus;
540 entry.dwell_promotes = !by_focus;
543 if let Some(sink) = entry.shown_at_sink.as_ref() {
544 sink.set((!by_focus).then_some(real_now));
545 }
546 }
547 }
548
549 let mut to_mark_paint: Vec<WidgetId> = Vec::new();
563 let mut to_promote: Vec<WidgetId> = Vec::new();
564 for entry in &self.tooltips {
565 let Some(sticky_after) = entry.sticky_after else {
566 continue;
567 };
568 if entry.overlay_id.is_none() || entry.is_sticky || !entry.dwell_promotes {
569 continue;
570 }
571 let elapsed = entry
572 .shown_at_real
573 .map(|t| real_now.saturating_duration_since(t));
574 let elapsed = match elapsed {
575 Some(e) => e,
576 None => continue,
577 };
578 to_mark_paint.push(entry.content_id);
582 if elapsed >= sticky_after {
583 to_promote.push(entry.content_id);
584 }
585 }
586 for id in to_mark_paint {
587 self.arena.mark_subtree_needs_paint(id);
588 }
589 for content_id in to_promote {
590 self.promote_tooltip_to_sticky(content_id);
591 }
592 }
593
594 pub(super) fn process_delayed_overlays(&mut self) {
595 let sim_now = self.sim_clock;
596 let mut noop = crate::window::NoopWindowOps;
597 self.process_delayed_overlays_impl(
598 |p| sim_now.saturating_duration_since(p.sim_requested_at),
599 &mut noop,
600 );
601 }
602
603 pub(super) fn process_delayed_overlays_real(&mut self, ops: &mut dyn crate::window::WindowOps) {
604 let real_now = std::time::Instant::now();
605 self.process_delayed_overlays_impl(
606 |p| real_now.saturating_duration_since(p.real_requested_at),
607 &mut *ops,
608 );
609 }
610
611 fn process_delayed_overlays_impl(
612 &mut self,
613 elapsed_fn: impl Fn(&PendingDelayedOverlay) -> std::time::Duration,
614 ops: &mut dyn crate::window::WindowOps,
615 ) {
616 let mut ready_indices = Vec::new();
617 for (index, pending) in self.pending_delayed_overlays.iter().enumerate() {
618 if elapsed_fn(pending) >= pending.delay {
619 ready_indices.push(index);
620 }
621 }
622
623 let mut ready = Vec::new();
624 for &index in ready_indices.iter().rev() {
625 ready.push(self.pending_delayed_overlays.remove(index));
626 }
627 ready.reverse();
628
629 let any_shown = !ready.is_empty();
630 for pending in ready {
631 let content_id = pending.request.content_id;
632 if pending.replace_siblings {
639 let anchor = pending.request.anchor;
640 self.dismiss_child_overlays_for_source(anchor, Some(content_id), &mut *ops);
641 }
642 self.arena.activate(content_id);
643 let current_focus = self.focused;
644 self.overlay_manager.show(pending.request);
645 if let Some(focus_id) = current_focus {
646 self.record_top_focus_restore(focus_id);
647 }
648 self.arena.mark_needs_paint(content_id);
649 if let Some(focus_target) = pending.focus_target
653 && self.arena.is_active(focus_target)
654 {
655 let target = self.exact_focus_target(focus_target);
656 self.focus_ops(target, &mut *ops);
657 }
658 }
659 if any_shown {
660 self.a11y_dirty = true;
661 }
662 }
663
664 pub(super) fn overlay_ancestor_for_widget(
665 &self,
666 widget_id: WidgetId,
667 ) -> Option<crate::overlay::OverlayId> {
668 self.overlay_manager
669 .stack
670 .iter()
671 .rev()
672 .find(|overlay| self.is_descendant_of(widget_id, overlay.content_id))
673 .map(|overlay| overlay.id)
674 }
675
676 pub(super) fn modal_overlay_for_widget(
677 &self,
678 widget_id: WidgetId,
679 ) -> Option<crate::overlay::OverlayId> {
680 let mut current = self.overlay_ancestor_for_widget(widget_id);
681
682 while let Some(overlay_id) = current {
683 let overlay = self.overlay_manager.overlay(overlay_id)?;
684 if overlay.placement.is_modal() {
685 return Some(overlay_id);
686 }
687 current = overlay.parent_overlay;
688 }
689
690 None
691 }
692
693 pub fn is_behind_modal(&self, widget_id: WidgetId) -> bool {
721 let Some(modal) = self.overlay_manager.topmost_modal() else {
722 return false;
723 };
724 let stack = &self.overlay_manager.stack;
725 let Some(at) = stack.iter().position(|overlay| overlay.id == modal.id) else {
726 return false;
727 };
728 let (below, above) = stack.split_at(at);
729 let in_modal = |id: WidgetId| {
730 above
731 .iter()
732 .any(|overlay| self.is_descendant_of(id, overlay.content_id))
733 };
734 !(in_modal(widget_id)
735 || below.iter().any(|overlay| {
736 self.is_descendant_of(widget_id, overlay.content_id) && in_modal(overlay.anchor)
737 }))
738 }
739
740 fn menu_ancestor_for_widget(&self, widget_id: WidgetId) -> Option<WidgetId> {
741 let mut current = Some(widget_id);
742 while let Some(id) = current {
743 if let Some(node) = self.arena.get(id) {
744 let mut builder = AccessNodeBuilder::new();
745 node.widget.accessibility(&mut builder);
746 if builder.role() == accesskit::Role::Menu {
747 return Some(id);
748 }
749 }
750 current = self.arena.parent(id);
751 }
752 None
753 }
754
755 pub(super) fn dismiss_child_overlays_for_source(
756 &mut self,
757 source_widget: WidgetId,
758 preserve_content: Option<WidgetId>,
759 ops: &mut dyn crate::window::WindowOps,
760 ) {
761 if let Some(parent_overlay) = self.overlay_ancestor_for_widget(source_widget) {
762 let preserve_overlay = preserve_content
763 .and_then(|content_id| self.overlay_manager.find_by_content(content_id));
764 let mark = self.dismissal_mark();
765 let (dismissed, focus_restore) = self
766 .overlay_manager
767 .dismiss_descendants_of(parent_overlay, preserve_overlay);
768 self.dormant_dismissed_content(&dismissed, &mut *ops);
769 self.return_focus_after_dismissal(focus_restore, mark, &mut *ops);
770 return;
771 }
772
773 let Some(menu_root) = self.menu_ancestor_for_widget(source_widget) else {
774 return;
775 };
776 let preserve_overlay = preserve_content
777 .and_then(|content_id| self.overlay_manager.find_by_content(content_id));
778 let overlay_ids: Vec<crate::overlay::OverlayId> = self
779 .overlay_manager
780 .stack
781 .iter()
782 .filter(|overlay| {
783 overlay.parent_overlay.is_none()
784 && self.is_descendant_of(overlay.anchor, menu_root)
785 && !preserve_overlay.is_some_and(|keep| {
786 overlay.id == keep
787 || self.overlay_manager.is_descendant_of(overlay.id, keep)
788 })
789 })
790 .map(|overlay| overlay.id)
791 .collect();
792
793 for overlay_id in overlay_ids {
794 let mark = self.dismissal_mark();
795 let (dismissed, focus_restore) =
796 self.overlay_manager.dismiss_with_focus_restore(overlay_id);
797 self.dormant_dismissed_content(&dismissed, &mut *ops);
798 self.return_focus_after_dismissal(focus_restore, mark, &mut *ops);
799 }
800 }
801
802 pub(super) fn dismiss_self_overlay_chain_for_source(
814 &mut self,
815 source_widget: WidgetId,
816 ops: &mut dyn crate::window::WindowOps,
817 ) {
818 let Some(start) = self.overlay_ancestor_for_widget(source_widget) else {
819 return;
820 };
821
822 let mut topmost_to_dismiss: Option<crate::overlay::OverlayId> = None;
823 let mut current = Some(start);
824 while let Some(overlay_id) = current {
825 let Some(overlay) = self.overlay_manager.overlay(overlay_id) else {
826 break;
827 };
828 if self.overlay_is_host_surface(overlay_id) {
829 break;
830 }
831 topmost_to_dismiss = Some(overlay_id);
832 current = overlay.parent_overlay;
833 }
834
835 if let Some(target) = topmost_to_dismiss {
836 let mark = self.dismissal_mark();
837 let (dismissed, focus_restore) =
838 self.overlay_manager.dismiss_with_focus_restore(target);
839 self.dormant_dismissed_content(&dismissed, &mut *ops);
840 self.return_focus_after_dismissal(focus_restore, mark, &mut *ops);
841 }
842 }
843
844 fn overlay_is_anchor_positioned(placement: &crate::overlay::OverlayPlacement) -> bool {
861 use crate::overlay::OverlayPlacement as P;
862 match placement {
863 P::Below
864 | P::Above
865 | P::TrailingEdge
866 | P::AtPointer(_)
867 | P::AtPointerAvoiding { .. }
868 | P::NearAnchor { .. }
869 | P::BelowPreferred => true,
870 P::Centered
876 | P::TopCenter { .. }
877 | P::BottomCenter
878 | P::ViewportCorner { .. }
879 | P::FullViewport
880 | P::AboveSelection { .. }
881 | P::BelowSelection { .. } => false,
882 }
883 }
884
885 fn overlay_follows_focus_out(&self, overlay_id: crate::overlay::OverlayId) -> bool {
925 let Some(overlay) = self.overlay_manager.overlay(overlay_id) else {
926 return false;
927 };
928 if !Self::overlay_is_anchor_positioned(&overlay.placement) {
929 return false;
930 }
931 if overlay.is_dismissing() {
932 return false;
933 }
934 if overlay.inert {
935 return false;
936 }
937 !self
938 .tooltips
939 .iter()
940 .any(|entry| entry.content_id == overlay.content_id)
941 }
942
943 fn overlay_orbit_for_widget(&self, widget_id: WidgetId) -> Option<crate::overlay::OverlayId> {
966 self.overlay_manager
967 .stack
968 .iter()
969 .rev()
970 .find(|overlay| {
971 self.is_descendant_of(widget_id, overlay.content_id)
972 || (self.overlay_follows_focus_out(overlay.id)
973 && self.is_descendant_of(widget_id, overlay.anchor))
974 })
975 .map(|overlay| overlay.id)
976 }
977
978 pub(super) fn dismiss_overlays_left_by_focus(
1005 &mut self,
1006 old: Option<WidgetId>,
1007 new_focus: Option<WidgetId>,
1008 ops: &mut dyn crate::window::WindowOps,
1009 ) {
1010 let Some(old) = old else {
1011 return;
1012 };
1013 let Some(old_overlay) = self.overlay_orbit_for_widget(old) else {
1014 return;
1015 };
1016 let new_overlay = new_focus.and_then(|id| self.overlay_ancestor_for_widget(id));
1024
1025 if let Some(new_overlay) = new_overlay
1028 && (new_overlay == old_overlay
1029 || self
1030 .overlay_manager
1031 .is_descendant_of(new_overlay, old_overlay))
1032 {
1033 return;
1034 }
1035
1036 if !self.overlay_follows_focus_out(old_overlay) {
1037 return;
1038 }
1039
1040 let mut topmost = old_overlay;
1041 let mut current = self
1042 .overlay_manager
1043 .overlay(old_overlay)
1044 .and_then(|overlay| overlay.parent_overlay);
1045 while let Some(overlay_id) = current {
1046 if Some(overlay_id) == new_overlay {
1050 break;
1051 }
1052 if self.overlay_is_host_surface(overlay_id) {
1055 break;
1056 }
1057 if !self.overlay_follows_focus_out(overlay_id) {
1058 break;
1059 }
1060 topmost = overlay_id;
1061 current = self
1062 .overlay_manager
1063 .overlay(overlay_id)
1064 .and_then(|overlay| overlay.parent_overlay);
1065 }
1066
1067 let dismissed = self.overlay_manager.dismiss(topmost);
1068 self.dormant_dismissed_content(&dismissed, &mut *ops);
1069 }
1070
1071 pub(super) fn dismiss_all_overlays_except_hosts(
1085 &mut self,
1086 ops: &mut dyn crate::window::WindowOps,
1087 ) {
1088 let to_dismiss: Vec<crate::overlay::OverlayId> = self
1089 .overlay_manager
1090 .stack
1091 .iter()
1092 .map(|overlay| overlay.id)
1093 .filter(|&id| !self.overlay_is_host_surface(id))
1094 .collect();
1095
1096 let mark = self.dismissal_mark();
1104 let mut restore = None;
1105 for overlay_id in to_dismiss {
1106 let (dismissed, focus_restore) =
1107 self.overlay_manager.dismiss_with_focus_restore(overlay_id);
1108 self.dormant_dismissed_content(&dismissed, &mut *ops);
1109 restore = restore.or(focus_restore);
1110 }
1111 if mark.focused_before().is_some() && self.focus_left_nowhere() {
1114 self.return_focus_after_dismissal(restore, mark, &mut *ops);
1115 }
1116 }
1117
1118 pub(super) fn overlay_is_host_surface(&self, overlay_id: crate::overlay::OverlayId) -> bool {
1124 let Some(overlay) = self.overlay_manager.overlay(overlay_id) else {
1125 return false;
1126 };
1127 if overlay.placement.is_modal() {
1136 return true;
1137 }
1138 let Some(node) = self.arena.get(overlay.content_id) else {
1139 return false;
1140 };
1141 let role = node
1149 .access_overrides
1150 .as_ref()
1151 .and_then(|o| o.role)
1152 .unwrap_or_else(|| {
1153 let mut builder = AccessNodeBuilder::new();
1154 node.widget.accessibility(&mut builder);
1155 builder.role()
1156 });
1157 matches!(
1158 role,
1159 accesskit::Role::Tooltip
1160 | accesskit::Role::Dialog
1161 | accesskit::Role::AlertDialog
1162 | accesskit::Role::MenuBar
1163 )
1164 }
1165
1166 pub(super) fn dismiss_modal_for_source(
1167 &mut self,
1168 source_widget: WidgetId,
1169 ops: &mut dyn crate::window::WindowOps,
1170 ) -> bool {
1171 let Some(modal_overlay) = self.modal_overlay_for_widget(source_widget) else {
1172 return false;
1173 };
1174
1175 let mark = self.dismissal_mark();
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 self.return_focus_after_dismissal(focus_restore, mark, &mut *ops);
1181 true
1182 }
1183
1184 pub fn is_descendant_of(&self, widget_id: WidgetId, ancestor: WidgetId) -> bool {
1185 if widget_id == ancestor {
1186 return true;
1187 }
1188 let mut current = self.arena.parent(widget_id);
1189 while let Some(parent_id) = current {
1190 if parent_id == ancestor {
1191 return true;
1192 }
1193 current = self.arena.parent(parent_id);
1194 }
1195 false
1196 }
1197
1198 fn is_overlay_content_root(&self, widget_id: WidgetId) -> bool {
1205 self.overlay_manager.find_by_content(widget_id).is_some()
1206 }
1207
1208 pub(super) fn tooltip_focus_enter(&mut self, widget_id: WidgetId) {
1236 type ToShow = (WidgetId, WidgetId, crate::overlay::TooltipPlacement);
1259 let mut direct: Vec<ToShow> = Vec::new();
1260 let mut reverse: Vec<ToShow> = Vec::new();
1261 for e in &self.tooltips {
1262 if e.sticky_after.is_none() || e.overlay_id.is_some() || e.suppressed_until_focus_leaves
1263 {
1264 continue;
1265 }
1266 if self.is_descendant_of(widget_id, e.anchor_id) {
1267 direct.push((e.anchor_id, e.content_id, e.placement));
1268 } else if self.is_descendant_of(e.anchor_id, widget_id) {
1269 reverse.push((e.anchor_id, e.content_id, e.placement));
1270 }
1271 }
1272 let mut to_show = direct;
1273 if reverse.len() == 1 && !self.is_overlay_content_root(widget_id) {
1277 to_show.extend(reverse);
1278 }
1279
1280 let sim_now = self.sim_clock;
1289 let real_now = std::time::Instant::now();
1290 for (_anchor_id, content_id, _placement) in to_show {
1291 if let Some(entry) = self
1292 .tooltips
1293 .iter_mut()
1294 .find(|e| e.content_id == content_id)
1295 {
1296 entry.hover_start = Some(sim_now);
1297 entry.real_hover_start = Some(real_now);
1298 entry.hover_origin = None;
1299 entry.armed_by_focus = true;
1300 entry.armed_by_hold = false;
1301 }
1302 }
1303 }
1304
1305 pub(super) fn show_highlight_tooltip(
1315 &mut self,
1316 item_id: WidgetId,
1317 ops: &mut dyn crate::window::WindowOps,
1318 ) {
1319 self.clear_highlight_tooltip(&mut *ops);
1322
1323 let found = self.tooltips.iter().find_map(|e| {
1327 if e.overlay_id.is_none() && self.is_descendant_of(e.anchor_id, item_id) {
1328 Some((e.anchor_id, e.content_id, e.placement))
1329 } else {
1330 None
1331 }
1332 });
1333 let Some((anchor_id, content_id, placement)) = found else {
1334 return;
1335 };
1336
1337 let parent_overlay = self.overlay_ancestor_for_widget(item_id);
1338 self.arena.activate(content_id);
1339 let fade_duration = if self.prefers_reduced_motion {
1340 None
1341 } else {
1342 Some(self.theme.motion.duration_fast)
1343 };
1344 let mut request = crate::overlay::OverlayRequest::new(
1345 content_id,
1346 anchor_id,
1347 Self::tooltip_overlay_placement(placement),
1348 crate::overlay::DismissBehavior::Manual,
1349 );
1350 request.parent_overlay = parent_overlay;
1351 request.fade_duration = fade_duration;
1352 let oid = self.show_overlay(request);
1353 let real_now = std::time::Instant::now();
1354 let sim_now = self.sim_clock;
1355 if let Some(entry) = self
1356 .tooltips
1357 .iter_mut()
1358 .find(|e| e.content_id == content_id)
1359 {
1360 entry.overlay_id = Some(oid);
1361 entry.shown_at_sim = Some(sim_now);
1362 entry.shown_at_real = Some(real_now);
1363 if let Some(sink) = entry.shown_at_sink.as_ref() {
1364 sink.set(Some(real_now));
1365 }
1366 }
1367 self.highlight_tooltip = Some((oid, content_id));
1368 }
1369
1370 pub(super) fn clear_highlight_tooltip(&mut self, ops: &mut dyn crate::window::WindowOps) {
1375 let Some((oid, content_id)) = self.highlight_tooltip.take() else {
1376 return;
1377 };
1378 if self.overlay_manager.stack.iter().any(|o| o.id == oid) {
1379 let dismissed = self.overlay_manager.dismiss(oid);
1380 self.dormant_dismissed_content(&dismissed, &mut *ops);
1381 }
1382 if let Some(entry) = self
1383 .tooltips
1384 .iter_mut()
1385 .find(|e| e.content_id == content_id)
1386 {
1387 entry.overlay_id = None;
1388 entry.shown_at_sim = None;
1389 entry.shown_at_real = None;
1390 if let Some(sink) = entry.shown_at_sink.as_ref() {
1391 sink.set(None);
1392 }
1393 }
1394 }
1395
1396 pub(super) fn tooltip_focus_leave_outside(
1407 &mut self,
1408 new_focus: Option<WidgetId>,
1409 ops: &mut dyn crate::window::WindowOps,
1410 ) {
1411 let to_dismiss: Vec<crate::overlay::OverlayId> = self
1412 .tooltips
1413 .iter()
1414 .filter(|e| e.promoted_by_focus && e.overlay_id.is_some())
1415 .filter(|e| {
1416 let in_scope = new_focus
1417 .map(|nf| {
1418 self.is_descendant_of(nf, e.anchor_id)
1423 || self.is_descendant_of(e.anchor_id, nf)
1424 || self.is_descendant_of(nf, e.content_id)
1425 })
1426 .unwrap_or(false);
1427 !in_scope
1428 })
1429 .filter_map(|e| e.overlay_id)
1430 .collect();
1431
1432 for oid in to_dismiss {
1433 let dismissed = self.overlay_manager.dismiss(oid);
1434 self.dormant_dismissed_content(&dismissed, &mut *ops);
1435 }
1436
1437 for e in &mut self.tooltips {
1440 if e.armed_by_focus && e.overlay_id.is_none() {
1441 e.hover_start = None;
1442 e.real_hover_start = None;
1443 e.armed_by_focus = false;
1444 }
1445 }
1446
1447 let served: Vec<usize> = self
1451 .tooltips
1452 .iter()
1453 .enumerate()
1454 .filter(|(_, e)| e.suppressed_until_focus_leaves)
1455 .filter(|(_, e)| {
1456 let still_inside = new_focus
1457 .map(|nf| {
1458 self.is_descendant_of(nf, e.anchor_id)
1459 || self.is_descendant_of(nf, e.content_id)
1460 })
1461 .unwrap_or(false);
1462 !still_inside
1463 })
1464 .map(|(i, _)| i)
1465 .collect();
1466 for i in served {
1467 self.tooltips[i].suppressed_until_focus_leaves = false;
1468 }
1469 }
1470
1471 fn tooltip_hover_targets_anchor(&self, widget_id: WidgetId, anchor_id: WidgetId) -> bool {
1491 if !self.is_descendant_of(widget_id, anchor_id) {
1492 return false;
1493 }
1494 if let Some(overlay_id) = self.overlay_ancestor_for_widget(widget_id)
1495 && let Some(content_id) = self
1496 .overlay_manager
1497 .overlay(overlay_id)
1498 .map(|o| o.content_id)
1499 && !self.is_descendant_of(anchor_id, content_id)
1500 {
1501 return false;
1502 }
1503 true
1504 }
1505
1506 pub(super) fn tooltip_index_for(&self, widget_id: WidgetId) -> Option<usize> {
1524 self.tooltips
1525 .iter()
1526 .enumerate()
1527 .filter(|(_, entry)| self.tooltip_hover_targets_anchor(widget_id, entry.anchor_id))
1528 .filter_map(|(index, entry)| {
1529 self.ancestor_distance(widget_id, entry.anchor_id)
1530 .map(|depth| (depth, index))
1531 })
1532 .min()
1533 .map(|(_, index)| index)
1534 }
1535
1536 pub(super) fn tooltip_pointer_enter(&mut self, widget_id: WidgetId) {
1537 let Some(index) = self.tooltip_index_for(widget_id) else {
1538 return;
1539 };
1540 if self.tooltips[index].overlay_id.is_some() {
1544 let entry = &mut self.tooltips[index];
1545 if entry.promoted_by_focus && !entry.is_sticky && !entry.dwell_promotes {
1546 let real_now = std::time::Instant::now();
1547 entry.dwell_promotes = true;
1548 entry.shown_at_sim = Some(self.sim_clock);
1549 entry.shown_at_real = Some(real_now);
1550 if let Some(sink) = entry.shown_at_sink.as_ref() {
1551 sink.set(Some(real_now));
1552 }
1553 }
1554 return;
1555 }
1556 self.tooltips[index].hover_start = Some(self.sim_clock);
1557 self.tooltips[index].real_hover_start = Some(std::time::Instant::now());
1558 self.tooltips[index].hover_origin = self.hover_owner_position();
1559 self.tooltips[index].armed_by_focus = false;
1560 self.tooltips[index].armed_by_hold = false;
1561 self.arena.mark_needs_paint(self.tooltips[index].anchor_id);
1562 }
1563
1564 fn ancestor_distance(&self, widget_id: WidgetId, ancestor: WidgetId) -> Option<usize> {
1567 let mut current = widget_id;
1568 let mut depth = 0usize;
1569 loop {
1570 if current == ancestor {
1571 return Some(depth);
1572 }
1573 current = self.arena.parent(current)?;
1574 depth += 1;
1575 }
1576 }
1577
1578 pub(super) fn tooltip_pointer_press(&mut self, at: Option<teksilo_canvas::Point>) {
1593 if at.is_some() {
1596 self.tooltip_dwell_held = true;
1597 }
1598 let mut to_dismiss = Vec::new();
1599 let candidates: Vec<(usize, Option<crate::overlay::OverlayId>)> = self
1600 .tooltips
1601 .iter()
1602 .enumerate()
1603 .map(|(i, e)| (i, if e.is_sticky { None } else { e.overlay_id }))
1604 .collect();
1605 for (index, overlay_id) in candidates {
1606 let body = self.tooltip_content_node(self.tooltips[index].content_id);
1613 let takes_hits = !self
1614 .arena
1615 .get(body)
1616 .is_some_and(|node| node.hit_transparent);
1617 let inside = match (overlay_id, at) {
1618 (Some(oid), Some(pos)) => {
1619 takes_hits
1620 && self
1621 .overlay_content_bounds(oid)
1622 .is_some_and(|rect| rect.contains(pos))
1623 }
1624 _ => false,
1625 };
1626 if inside {
1627 continue;
1628 }
1629 let entry = &mut self.tooltips[index];
1630 entry.hover_start = None;
1631 entry.real_hover_start = None;
1632 entry.hover_origin = None;
1633 if let Some(oid) = overlay_id {
1634 to_dismiss.push(oid);
1635 }
1636 }
1637 for overlay_id in to_dismiss {
1638 self.dismiss_overlay(overlay_id);
1639 }
1640 }
1641
1642 pub(super) fn tooltip_escape_pressed(&mut self) {
1664 self.tooltip_pointer_press(None);
1667 }
1668
1669 pub(super) fn inert_overlays_escape_pressed(&mut self, ops: &mut dyn crate::window::WindowOps) {
1680 let dismissed = self.overlay_manager.dismiss_inert_on_escape();
1681 if !dismissed.is_empty() {
1682 self.dormant_dismissed_content(&dismissed, &mut *ops);
1683 self.a11y_dirty = true;
1684 }
1685 }
1686
1687 pub(super) fn tooltip_window_deactivated(&mut self) {
1694 self.tooltip_pointer_press(None);
1697 }
1698
1699 pub(crate) fn tooltip_cancel_pending_dwell(&mut self) {
1707 for entry in &mut self.tooltips {
1708 if entry.overlay_id.is_none() {
1709 entry.hover_start = None;
1710 entry.real_hover_start = None;
1711 entry.hover_origin = None;
1712 }
1713 }
1714 }
1715
1716 pub(super) fn tooltip_pointer_moved(
1720 &mut self,
1721 widget_id: WidgetId,
1722 position: teksilo_canvas::Point,
1723 ) {
1724 let matching: Vec<usize> = self
1725 .tooltips
1726 .iter()
1727 .enumerate()
1728 .filter(|(_, entry)| {
1729 entry.overlay_id.is_none()
1730 && entry.hover_start.is_some()
1731 && !entry.armed_by_focus
1732 && self.tooltip_hover_targets_anchor(widget_id, entry.anchor_id)
1733 })
1734 .map(|(index, _)| index)
1735 .collect();
1736 if matching.is_empty() {
1737 return;
1738 }
1739 let now = self.sim_clock;
1740 let real_now = std::time::Instant::now();
1741 let slop = super::TOOLTIP_STATIONARY_SLOP;
1742 let slop_sq = slop * slop;
1743 for index in matching {
1744 let origin = match self.tooltips[index].hover_origin {
1745 Some(o) => o,
1746 None => {
1747 self.tooltips[index].hover_origin = Some(position);
1751 continue;
1752 }
1753 };
1754 let dx = position.x - origin.x;
1755 let dy = position.y - origin.y;
1756 if dx * dx + dy * dy <= slop_sq {
1757 continue;
1758 }
1759 self.tooltips[index].hover_start = Some(now);
1761 self.tooltips[index].real_hover_start = Some(real_now);
1762 self.tooltips[index].hover_origin = Some(position);
1763 }
1764 }
1765
1766 pub fn promote_tooltip_to_sticky(&mut self, content_id: WidgetId) {
1784 let Some(entry) = self
1785 .tooltips
1786 .iter_mut()
1787 .find(|entry| entry.content_id == content_id)
1788 else {
1789 return;
1790 };
1791 if entry.is_sticky {
1792 return;
1793 }
1794 entry.is_sticky = true;
1795 let overlay_id = entry.overlay_id;
1796 if let Some(overlay_id) = overlay_id {
1797 self.overlay_manager.set_dismiss(
1798 overlay_id,
1799 crate::overlay::DismissBehavior::EscapeOrClickOutside,
1800 );
1801 }
1802 }
1803
1804 pub(super) fn tooltip_pointer_leave(
1805 &mut self,
1806 widget_id: WidgetId,
1807 _ops: &mut dyn crate::window::WindowOps,
1808 ) {
1809 let matching: Vec<usize> = self
1810 .tooltips
1811 .iter()
1812 .enumerate()
1813 .filter(|(_, entry)| self.is_descendant_of(widget_id, entry.anchor_id))
1814 .map(|(index, _)| index)
1815 .collect();
1816 for index in matching {
1817 let entry = &mut self.tooltips[index];
1821 if entry.promoted_by_focus && !entry.is_sticky && entry.dwell_promotes {
1822 entry.dwell_promotes = false;
1823 if let Some(sink) = entry.shown_at_sink.as_ref() {
1824 sink.set(None);
1825 }
1826 }
1827 self.tooltips[index].hover_start = None;
1830 self.tooltips[index].real_hover_start = None;
1831 self.tooltips[index].hover_origin = None;
1832 }
1844 }
1845
1846 pub fn next_timer_deadline(&self) -> Option<std::time::Instant> {
1852 self.next_timer_deadline_source().map(|(at, _)| at)
1853 }
1854
1855 #[doc(hidden)]
1863 pub fn next_timer_deadline_source(&self) -> Option<(std::time::Instant, &'static str)> {
1864 let now = std::time::Instant::now();
1865 let session_active = self.tooltip_session_active_real(now);
1866 let tooltip_deadline = self
1867 .tooltips
1868 .iter()
1869 .filter(|entry| entry.overlay_id.is_none())
1870 .filter_map(|entry| {
1871 let start = entry.real_hover_start?;
1872 let delay = self.effective_tooltip_delay(entry.delay, session_active);
1873 Some(start + delay)
1874 })
1875 .min();
1876
1877 let ref_time = self.last_frame_time.unwrap_or_else(std::time::Instant::now);
1898 let dwell_tooltip_deadline = self
1899 .tooltips
1900 .iter()
1901 .filter_map(|entry| {
1902 let sticky_after = entry.sticky_after?;
1903 let shown_at = entry.shown_at_real?;
1904 if entry.overlay_id.is_none() || entry.is_sticky || !entry.dwell_promotes {
1905 return None;
1906 }
1907 let dwell_step = sticky_after / super::TOOLTIP_DWELL_STEPS;
1908 if dwell_step.is_zero() {
1909 return None;
1910 }
1911 let elapsed = ref_time.saturating_duration_since(shown_at);
1912 if elapsed >= sticky_after {
1913 return None;
1914 }
1915 let steps_passed = (elapsed.as_millis() / dwell_step.as_millis()) as u32;
1918 let next_step_at = shown_at + dwell_step * (steps_passed + 1);
1919 Some(next_step_at.min(shown_at + sticky_after))
1920 })
1921 .min();
1922 let delayed_overlay_deadline = self
1923 .pending_delayed_overlays
1924 .iter()
1925 .map(|pending| pending.real_requested_at + pending.delay)
1926 .min();
1927 let auto_dismiss_deadline = self.overlay_manager.next_auto_dismiss_deadline();
1928 let pointer_leave_deadline = self.overlay_manager.next_pointer_leave_deadline();
1935 let fade_dismiss_deadline = self.overlay_manager.next_fade_dismiss_deadline();
1942 let animation_deadline = self
1943 .animation_scheduler
1944 .next_deadline(&self.arena, self.paint_epoch);
1945 let animated_quad_deadline = self
1950 .animated_quads
1951 .next_deadline(&self.arena, self.paint_epoch);
1952 let gesture_deadline = self.next_input_deadline();
1958 let wake_at_deadline = self.pending_wake_at.get();
1959 let frame_tick_deadline = self.frame_tick_deadline();
1965
1966 [
1967 (tooltip_deadline, "tooltip"),
1968 (dwell_tooltip_deadline, "tooltip_dwell"),
1969 (delayed_overlay_deadline, "delayed_overlay"),
1970 (auto_dismiss_deadline, "overlay_auto_dismiss"),
1971 (pointer_leave_deadline, "overlay_pointer_leave"),
1972 (fade_dismiss_deadline, "overlay_fade"),
1973 (animation_deadline, "animation"),
1974 (animated_quad_deadline, "animated_quad"),
1975 (gesture_deadline, "gesture"),
1976 (wake_at_deadline, "wake_at"),
1977 (frame_tick_deadline, "frame_tick"),
1978 ]
1979 .into_iter()
1980 .filter_map(|(at, source)| Some((at?, source)))
1981 .min_by_key(|&(at, _)| at)
1982 }
1983
1984 pub fn next_unpainted_deadline(&self) -> Option<std::time::Instant> {
1996 let animation_deadline = self
1997 .animation_scheduler
1998 .next_deadline(&self.arena, self.paint_epoch);
1999 [
2000 self.pending_wake_at.get(),
2001 animation_deadline,
2002 self.frame_tick_deadline(),
2003 ]
2004 .into_iter()
2005 .flatten()
2006 .min()
2007 }
2008
2009 pub fn overlay_manager(&self) -> &crate::overlay::OverlayManager {
2010 &self.overlay_manager
2011 }
2012
2013 pub fn overlay_manager_mut(&mut self) -> &mut crate::overlay::OverlayManager {
2018 &mut self.overlay_manager
2019 }
2020
2021 pub fn active_overlays(&self) -> Vec<crate::overlay::OverlayId> {
2022 self.overlay_manager.active_ids()
2023 }
2024
2025 pub fn overlay_content_bounds(
2034 &self,
2035 id: crate::overlay::OverlayId,
2036 ) -> Option<teksilo_canvas::Rect> {
2037 self.overlay_manager
2038 .stack
2039 .iter()
2040 .find(|o| o.id == id)
2041 .map(|o| o.bounds)
2042 }
2043
2044 pub fn show_overlay(
2045 &mut self,
2046 request: crate::overlay::OverlayRequest,
2047 ) -> crate::overlay::OverlayId {
2048 let fade_duration = request.fade_duration;
2049 let content_id = request.content_id;
2050 let is_modal = request.placement.is_modal();
2051 let id = self.overlay_manager.show(request);
2052 self.cancel_pointers_for_modal(is_modal);
2053 self.a11y_dirty = true;
2061 if let Some(duration) = fade_duration {
2062 self.attach_overlay_fade(id, content_id, duration);
2063 }
2064 id
2065 }
2066
2067 pub fn show_overlay_in_band(
2077 &mut self,
2078 request: crate::overlay::OverlayRequest,
2079 band: crate::overlay::OverlayBand,
2080 ) -> crate::overlay::OverlayId {
2081 let fade_duration = request.fade_duration;
2082 let content_id = request.content_id;
2083 let is_modal = request.placement.is_modal();
2084 let id = self.overlay_manager.show_in_band(request, band);
2085 self.cancel_pointers_for_modal(is_modal);
2086 self.a11y_dirty = true;
2087 if let Some(duration) = fade_duration {
2088 self.attach_overlay_fade(id, content_id, duration);
2089 }
2090 id
2091 }
2092
2093 pub fn show_overlay_from_source(
2096 &mut self,
2097 source_widget: WidgetId,
2098 mut request: crate::overlay::OverlayRequest,
2099 ) -> crate::overlay::OverlayId {
2100 if request.parent_overlay.is_none() {
2101 request.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
2102 }
2103 if let Some(existing) = self.overlay_manager.find_by_content(request.content_id) {
2104 return existing;
2105 }
2106
2107 let fade_duration = request.fade_duration;
2108 let content_id = request.content_id;
2109 let is_modal = request.placement.is_modal();
2110 let current_focus = self.focused;
2111 let id = self.overlay_manager.show(request);
2112 self.cancel_pointers_for_modal(is_modal);
2113 self.a11y_dirty = true;
2114 if let Some(focus_id) = current_focus {
2115 self.record_top_focus_restore(focus_id);
2116 }
2117 if let Some(duration) = fade_duration {
2118 self.attach_overlay_fade(id, content_id, duration);
2119 }
2120 id
2121 }
2122
2123 pub fn show_overlay_for(
2124 &mut self,
2125 request: crate::overlay::OverlayRequest,
2126 duration: std::time::Duration,
2127 ) -> crate::overlay::OverlayId {
2128 let fade_duration = request.fade_duration;
2129 let content_id = request.content_id;
2130 let is_modal = request.placement.is_modal();
2131 let id = self.overlay_manager.show_for(request, duration);
2132 self.cancel_pointers_for_modal(is_modal);
2133 self.overlay_manager.set_shown_at_sim(id, self.sim_clock);
2134 self.a11y_dirty = true;
2135 if let Some(fade) = fade_duration {
2136 self.attach_overlay_fade(id, content_id, fade);
2137 }
2138 id
2139 }
2140
2141 fn attach_overlay_fade(
2149 &mut self,
2150 overlay_id: crate::overlay::OverlayId,
2151 content_id: WidgetId,
2152 duration: std::time::Duration,
2153 ) {
2154 let opacity = crate::signal::Signal::<f32>::new_animated(0.0);
2155 self.register_animated_signal(&opacity, content_id);
2156 self.set_opacity(content_id, opacity.clone());
2157 if self.prefers_reduced_motion() {
2161 opacity.set(1.0);
2162 self.overlay_manager
2163 .attach_fade(overlay_id, opacity, std::time::Duration::ZERO);
2164 return;
2165 }
2166 let _ = opacity.try_animate_with_options(crate::animation::AnimationRequest {
2167 target: 1.0,
2168 duration,
2169 easing: teksilo_tokens::Easing::EaseOut,
2170 frame_interval: None,
2171 looping: false,
2172 epsilon: 0.0,
2173 max_duration: None,
2174 });
2175 self.overlay_manager
2176 .attach_fade(overlay_id, opacity, duration);
2177 }
2178
2179 pub fn dismiss_overlay(&mut self, id: crate::overlay::OverlayId) {
2184 let mut noop = crate::window::NoopWindowOps;
2185 self.dismiss_overlay_with_ops(id, &mut noop);
2186 }
2187
2188 #[doc(hidden)]
2194 pub fn dismiss_all_overlays_with_ops(
2195 &mut self,
2196 reason: crate::overlay::DismissReason,
2197 ops: &mut dyn crate::window::WindowOps,
2198 ) {
2199 let mark = self.dismissal_mark();
2200 let dismissed = self.overlay_manager.dismiss_all_because(reason);
2201 self.dormant_dismissed_content(&dismissed, &mut *ops);
2202 self.return_focus_after_dismissal(None, mark, &mut *ops);
2203 }
2204
2205 pub fn dismiss_overlay_with_ops(
2208 &mut self,
2209 id: crate::overlay::OverlayId,
2210 ops: &mut dyn crate::window::WindowOps,
2211 ) {
2212 let mark = self.dismissal_mark();
2213 let dismissed = self.overlay_manager.dismiss(id);
2214 self.dormant_dismissed_content(&dismissed, &mut *ops);
2215 self.return_focus_after_dismissal(None, mark, &mut *ops);
2216 }
2217
2218 fn cancel_pointers_for_modal(&mut self, is_modal: bool) {
2229 if !is_modal {
2230 return;
2231 }
2232 let mut noop = crate::window::NoopWindowOps;
2233 self.cancel_all_pointers(crate::pointer::CancelReason::ModalOpened, &mut noop);
2234 }
2235
2236 pub(super) fn run_pending_dismiss_callbacks(&mut self, ops: &mut dyn crate::window::WindowOps) {
2246 if self.draining_dismiss.replace(true) {
2251 return;
2252 }
2253 for _ in 0..8 {
2257 let pending = self.overlay_manager.take_pending_dismiss();
2258 if pending.is_empty() {
2259 break;
2260 }
2261 for (cb, reason) in pending {
2262 self.run_with_event_context(&mut *ops, |ctx| cb(reason, ctx));
2263 }
2264 }
2265 self.draining_dismiss.set(false);
2266 }
2267
2268 pub(super) fn dormant_dismissed_content(
2269 &mut self,
2270 content_ids: &[WidgetId],
2271 ops: &mut dyn crate::window::WindowOps,
2272 ) {
2273 self.run_pending_dismiss_callbacks(&mut *ops);
2277 let mut any_tooltip_dismissed = false;
2283 for &id in content_ids {
2284 if let Some(entry) = self.tooltips.iter_mut().find(|e| e.content_id == id) {
2285 entry.suppressed_until_focus_leaves = entry.promoted_by_focus;
2292 entry.overlay_id = None;
2293 entry.is_sticky = false;
2294 entry.hover_start = None;
2295 entry.real_hover_start = None;
2296 entry.hover_origin = None;
2297 entry.shown_at_sim = None;
2298 entry.shown_at_real = None;
2299 entry.promoted_by_focus = false;
2300 entry.armed_by_focus = false;
2301 entry.armed_by_hold = false;
2302 if let Some(sink) = entry.shown_at_sink.as_ref() {
2303 sink.set(None);
2304 }
2305 any_tooltip_dismissed = true;
2306 }
2307 }
2308 if any_tooltip_dismissed {
2309 let grace = super::TOOLTIP_SESSION_GRACE;
2310 self.tooltip_session_until_sim = Some(self.sim_clock + grace);
2311 self.tooltip_session_until_real = Some(std::time::Instant::now() + grace);
2312 }
2313 for &id in content_ids {
2314 let focused_in_subtree = self
2315 .focused
2316 .filter(|focused| self.is_descendant_of(*focused, id));
2317 let hovered_in_subtree = self
2318 .hovered_id()
2319 .filter(|hovered| self.is_descendant_of(*hovered, id));
2320
2321 if let Some(focused) = focused_in_subtree {
2322 self.dispatch_to_widget(focused, &WidgetEvent::FocusLost, &mut *ops);
2323 if self
2324 .focused
2325 .is_some_and(|current| self.is_descendant_of(current, id))
2326 {
2327 let old = self.focused;
2328 self.set_focused(None);
2329 self.focus_origin = None;
2330 self.update_focus_within_signals(old, None);
2331 self.update_view_focus_signals(old, None);
2332 }
2333 }
2334
2335 self.cancel_pointers_in_subtree(
2341 id,
2342 crate::pointer::CancelReason::OverlayDismissed,
2343 &mut *ops,
2344 );
2345 let _parked = self.arena.set_dormant(id);
2346
2347 if hovered_in_subtree.is_some() {
2348 let old = self.hovered_id();
2349 self.set_hovered(None);
2350 self.update_hover_within_signals(old, None);
2351 }
2352 }
2353 if !content_ids.is_empty() {
2354 self.cached_frame = None;
2355 self.a11y_dirty = true;
2356 self.flush_activation_signals();
2360 }
2361 }
2362
2363 pub fn is_visible(&self, id: WidgetId) -> bool {
2364 self.arena.is_active(id)
2365 }
2366}
2367
2368#[cfg(test)]
2369mod tests {
2370 use super::*;
2371 use crate::test_widgets::FillWidget;
2372 use crate::test_widgets::StackWidget;
2373
2374 #[test]
2377 fn the_next_timer_deadline_is_named_for_its_timer() {
2378 let mut tree = WidgetTree::new();
2379 tree.add(FillWidget::new());
2380 tree.layout(SizeProposal::exact(100.0, 50.0));
2381 assert_eq!(tree.next_timer_deadline_source(), None, "an idle tree");
2382
2383 let blink = std::time::Instant::now() + std::time::Duration::from_millis(500);
2384 tree.request_wake_at(blink);
2385 assert_eq!(tree.next_timer_deadline_source(), Some((blink, "wake_at")));
2386 assert_eq!(tree.next_timer_deadline(), Some(blink));
2387
2388 tree.request_frame();
2389 let (at, source) = tree.next_timer_deadline_source().expect("a frame is due");
2390 assert_eq!(source, "frame_tick", "the frame is due before the blink");
2391 assert!(at < blink);
2392 }
2393
2394 #[test]
2395 fn is_visible_reflects_dormancy() {
2396 let mut tree = WidgetTree::new();
2397 let widget = tree.add(FillWidget::new());
2398 tree.layout(SizeProposal::exact(100.0, 50.0));
2399
2400 assert!(tree.is_visible(widget));
2401 tree.set_dormant(widget);
2402 assert!(!tree.is_visible(widget));
2403 tree.activate(widget);
2404 assert!(tree.is_visible(widget));
2405 }
2406
2407 #[test]
2408 fn frame_tick_deadline_caps_per_frame_effects_at_60hz() {
2409 let mut tree = WidgetTree::new();
2410 assert!(tree.frame_tick_deadline().is_none());
2412
2413 tree.request_frame();
2416 let t0 = std::time::Instant::now();
2418 tree.last_frame_time = Some(t0);
2419
2420 let deadline = tree
2421 .frame_tick_deadline()
2422 .expect("an armed per-frame effect must publish a deadline");
2423 let interval = deadline.saturating_duration_since(t0);
2424 assert!(
2427 interval >= std::time::Duration::from_micros(16_000)
2428 && interval <= std::time::Duration::from_micros(17_500),
2429 "per-frame effects must pace at ~60 Hz (got {interval:?})"
2430 );
2431
2432 assert_eq!(
2435 tree.next_timer_deadline(),
2436 Some(deadline),
2437 "frame-tick deadline must be surfaced by next_timer_deadline"
2438 );
2439 }
2440
2441 #[test]
2442 fn throttled_subscriber_stretches_deadline_but_per_frame_wins_min() {
2443 use crate::test_widgets::{FillWidget, StackWidget};
2444 let mut tree = WidgetTree::new();
2445 let w = tree.add(FillWidget::new());
2446 let w2 = tree.add(FillWidget::new());
2447 tree.add(StackWidget::new().child(w).child(w2));
2448 let _throttled =
2450 tree.subscribe_frame_tick_throttled(w, std::time::Duration::from_millis(1500));
2451 tree.layout(SizeProposal::exact(200.0, 100.0));
2455 let _ = tree.render();
2456 assert!(
2457 !tree.frame_tick_requested.get(),
2458 "precondition: nothing but the subscriber wants this frame"
2459 );
2460 let t0 = tree.last_frame_time.expect("a frame has run");
2461
2462 let d = tree.frame_tick_deadline().expect("armed");
2464 let dt = d.saturating_duration_since(t0);
2465 assert!(
2466 dt >= std::time::Duration::from_millis(1490)
2467 && dt <= std::time::Duration::from_millis(1510),
2468 "a lone throttled subscriber must pace at its interval (~1.5 s), got {dt:?}"
2469 );
2470
2471 let _per_frame = tree.subscribe_frame_tick(w2);
2476 let d2 = tree.frame_tick_deadline().expect("armed");
2477 let dt2 = d2.saturating_duration_since(t0);
2478 assert!(
2479 dt2 <= std::time::Duration::from_millis(20),
2480 "a per-frame subscriber must pull the shared deadline to 60 Hz, got {dt2:?}"
2481 );
2482 }
2483
2484 #[test]
2490 fn a_raw_frame_request_is_not_paced_by_a_throttled_subscriber() {
2491 use crate::test_widgets::FillWidget;
2492 const NORMAL_FRAME: std::time::Duration = std::time::Duration::from_micros(17_500);
2493 const CLOCK: std::time::Duration = std::time::Duration::from_secs(60);
2494 fn frame(tree: &mut WidgetTree) {
2495 tree.layout(SizeProposal::exact(200.0, 100.0));
2496 let _ = tree.render();
2497 }
2498 fn wait(tree: &WidgetTree) -> std::time::Duration {
2499 let deadline = tree.frame_tick_deadline().expect("armed");
2500 deadline.saturating_duration_since(tree.last_frame_time.expect("a frame has run"))
2501 }
2502
2503 let mut tree = WidgetTree::new();
2504 let clock = tree.add(FillWidget::new());
2505 let _tick = tree.subscribe_frame_tick_throttled(clock, CLOCK);
2506 frame(&mut tree);
2507 assert!(
2508 wait(&tree) >= CLOCK - NORMAL_FRAME,
2509 "precondition: the painted clock paces the loop at its own interval"
2510 );
2511
2512 for request in [
2515 (|tree: &WidgetTree| tree.request_frame()) as fn(&WidgetTree),
2516 |tree: &WidgetTree| tree.frame_request_handle().set(true),
2517 ] {
2518 request(&tree);
2519 assert!(
2520 wait(&tree) <= NORMAL_FRAME,
2521 "a requested frame must not wait on the clock (waited {:?})",
2522 wait(&tree)
2523 );
2524 assert_eq!(tree.next_timer_deadline(), tree.frame_tick_deadline());
2525
2526 frame(&mut tree);
2527 assert!(
2528 wait(&tree) >= CLOCK - NORMAL_FRAME,
2529 "once the request is served the clock paces alone again, rather than the loop \
2530 free-running at 60 Hz (waited {:?})",
2531 wait(&tree)
2532 );
2533 }
2534 }
2535
2536 #[test]
2545 fn the_subscriber_arm_is_spent_by_its_tick_and_ends_with_its_owner() {
2546 use crate::test_widgets::StackWidget;
2547 fn frame(tree: &mut WidgetTree) {
2548 tree.layout(SizeProposal::exact(200.0, 100.0));
2549 let _ = tree.render();
2550 }
2551
2552 let mut tree = WidgetTree::new();
2553 let owner = tree.add(FillWidget::new());
2554 let sibling = tree.add(FillWidget::new());
2555 tree.add(StackWidget::new().child(owner).child(sibling));
2556 let ticks = std::rc::Rc::new(std::cell::Cell::new(0_u32));
2557 let _count = tree.frame_tick().observe({
2558 let ticks = ticks.clone();
2559 move |_| ticks.set(ticks.get() + 1)
2560 });
2561 let _sub = tree.subscribe_frame_tick(owner);
2562
2563 frame(&mut tree);
2564 assert!(
2565 tree.frame_requested(),
2566 "a render that painted the subscriber arms the next tick"
2567 );
2568 assert!(
2569 !tree.frame_tick_requested.get(),
2570 "precondition: armed by the subscribers' flag, not by a raw request"
2571 );
2572
2573 let before = ticks.get();
2575 tree.layout(SizeProposal::exact(200.0, 100.0));
2576 assert_eq!(
2577 ticks.get(),
2578 before + 1,
2579 "the armed tick fires on the next layout"
2580 );
2581 assert!(
2582 !tree.frame_requested(),
2583 "the layout that fired the tick has spent the arm"
2584 );
2585 let _ = tree.render();
2586 assert!(
2587 tree.frame_requested(),
2588 "the owner painted again and re-armed"
2589 );
2590
2591 let before = ticks.get();
2593 tree.advance_time(std::time::Duration::from_millis(16));
2594 assert_eq!(ticks.get(), before + 1, "advance_time fires an armed tick");
2595 assert!(
2596 !tree.frame_requested(),
2597 "advance_time spends the arm as layout does"
2598 );
2599 tree.resume_real_time();
2600
2601 frame(&mut tree);
2604 tree.set_dormant(owner);
2605 assert!(
2606 tree.frame_tick_deadline().is_some(),
2607 "the tick armed before the owner was parked is still due"
2608 );
2609 let before = ticks.get();
2610 frame(&mut tree);
2611 assert_eq!(ticks.get(), before + 1);
2612 assert!(
2613 !tree.frame_requested(),
2614 "a parked subscriber must not re-arm the tick"
2615 );
2616 assert_eq!(
2617 tree.next_timer_deadline(),
2618 None,
2619 "with its only subscriber parked the loop sleeps"
2620 );
2621 }
2622
2623 #[test]
2624 fn show_and_dismiss_overlay() {
2625 let mut tree = WidgetTree::new();
2626 let anchor = tree.add(FillWidget::new());
2627 let content = tree.add(FillWidget::new().label("Overlay"));
2628 tree.layout(SizeProposal::exact(200.0, 100.0));
2629
2630 assert!(tree.active_overlays().is_empty());
2631
2632 let id = tree.show_overlay(crate::overlay::OverlayRequest::new(
2633 content,
2634 anchor,
2635 crate::overlay::OverlayPlacement::Below,
2636 crate::overlay::DismissBehavior::Manual,
2637 ));
2638
2639 assert_eq!(tree.active_overlays().len(), 1);
2640
2641 tree.dismiss_overlay(id);
2642 assert!(tree.active_overlays().is_empty());
2643 assert!(!tree.is_visible(content));
2644 }
2645
2646 #[test]
2651 fn modal_centered_overlay_is_a_host_surface() {
2652 let mut tree = WidgetTree::new();
2653 let anchor = tree.add(FillWidget::new());
2654 let modal_content = tree.add(FillWidget::new().label("Modal"));
2655 let dropdown_content = tree.add(FillWidget::new().label("Dropdown"));
2656 tree.layout(SizeProposal::exact(200.0, 100.0));
2657
2658 let modal = tree.show_overlay(crate::overlay::OverlayRequest::new(
2659 modal_content,
2660 anchor,
2661 crate::overlay::OverlayPlacement::Centered,
2662 crate::overlay::DismissBehavior::EscapeOrClickOutside,
2663 ));
2664 let dropdown = tree.show_overlay(
2665 crate::overlay::OverlayRequest::new(
2666 dropdown_content,
2667 anchor,
2668 crate::overlay::OverlayPlacement::Below,
2669 crate::overlay::DismissBehavior::EscapeOrClickOutside,
2670 )
2671 .parent_overlay(modal),
2672 );
2673
2674 assert!(
2675 tree.overlay_is_host_surface(modal),
2676 "a Centered modal overlay must be treated as a host surface"
2677 );
2678 assert!(
2679 !tree.overlay_is_host_surface(dropdown),
2680 "a plain dropdown overlay is not a host surface"
2681 );
2682 }
2683
2684 fn overlay_over_a_field(tree: &mut WidgetTree) -> (WidgetId, WidgetId, WidgetId) {
2686 let field = tree.add(FillWidget::new().focusable());
2687 let inside = tree.add(FillWidget::new().focusable());
2688 let content = tree.add(StackWidget::new().child(inside));
2689 tree.layout(SizeProposal::exact(200.0, 100.0));
2690 tree.focus(field);
2691 tree.show_overlay_from_source(
2692 field,
2693 crate::overlay::OverlayRequest::new(
2694 content,
2695 field,
2696 crate::overlay::OverlayPlacement::Below,
2697 crate::overlay::DismissBehavior::EscapeOrClickOutside,
2698 ),
2699 );
2700 tree.layout(SizeProposal::exact(200.0, 100.0));
2701 (field, content, inside)
2702 }
2703
2704 fn dismiss_all_except_hosts(tree: &mut WidgetTree) {
2705 tree.run_with_event_context(&mut crate::window::NoopWindowOps, |ctx| {
2706 ctx.dismiss_all_except_hosts();
2707 });
2708 }
2709
2710 #[test]
2716 fn dismissing_all_except_hosts_gives_back_the_focus_it_took_away() {
2717 let mut tree = WidgetTree::new();
2718 let (field, content, inside) = overlay_over_a_field(&mut tree);
2719 let nested = tree.add(FillWidget::new().focusable());
2720 tree.layout(SizeProposal::exact(200.0, 100.0));
2721 tree.focus(inside);
2722 tree.show_overlay_from_source(
2723 inside,
2724 crate::overlay::OverlayRequest::new(
2725 nested,
2726 inside,
2727 crate::overlay::OverlayPlacement::Below,
2728 crate::overlay::DismissBehavior::EscapeOrClickOutside,
2729 ),
2730 );
2731 tree.focus(nested);
2732 assert!(tree.is_active(content), "precondition");
2733 dismiss_all_except_hosts(&mut tree);
2734 assert!(tree.active_overlays().is_empty());
2735 assert_eq!(tree.focused(), Some(field));
2736 }
2737
2738 #[test]
2743 fn dismissing_all_except_hosts_gives_focus_back_as_the_lowest_overlay_found_it() {
2744 let mut tree = WidgetTree::new();
2745 let first_field = tree.add(FillWidget::new().focusable());
2746 let second_field = tree.add(FillWidget::new().focusable());
2747 let open_over = |tree: &mut WidgetTree, field: WidgetId| {
2748 let inside = tree.add(FillWidget::new().focusable());
2749 let content = tree.add(StackWidget::new().child(inside));
2750 tree.layout(SizeProposal::exact(200.0, 100.0));
2751 tree.focus(field);
2752 tree.show_overlay_from_source(
2754 field,
2755 crate::overlay::OverlayRequest::new(
2756 content,
2757 field,
2758 crate::overlay::OverlayPlacement::BottomCenter,
2759 crate::overlay::DismissBehavior::Manual,
2760 ),
2761 );
2762 tree.layout(SizeProposal::exact(200.0, 100.0));
2763 inside
2764 };
2765 open_over(&mut tree, first_field);
2766 let second_inside = open_over(&mut tree, second_field);
2767 tree.focus(second_inside);
2768 assert_eq!(tree.active_overlays().len(), 2, "two overlays, unrelated");
2769 dismiss_all_except_hosts(&mut tree);
2770 assert!(tree.active_overlays().is_empty());
2771 assert_eq!(
2772 tree.focused(),
2773 Some(first_field),
2774 "the lowest overlay's focus, not the top one's"
2775 );
2776 }
2777
2778 #[test]
2780 fn dismissing_all_except_hosts_leaves_focus_outside_them_alone() {
2781 let mut tree = WidgetTree::new();
2782 let (_field, _content, _inside) = overlay_over_a_field(&mut tree);
2783 let other = tree.add(FillWidget::new().focusable());
2784 tree.layout(SizeProposal::exact(200.0, 100.0));
2785 tree.focus(other);
2786 dismiss_all_except_hosts(&mut tree);
2787 assert_eq!(tree.focused(), Some(other));
2788 }
2789
2790 #[test]
2791 fn escape_dismisses_topmost_overlay() {
2792 let mut tree = WidgetTree::new();
2793 let anchor = tree.add(FillWidget::new().focusable());
2794 let content = tree.add(FillWidget::new());
2795 tree.layout(SizeProposal::exact(200.0, 100.0));
2796 tree.focus(anchor);
2797
2798 tree.show_overlay(crate::overlay::OverlayRequest::new(
2799 content,
2800 anchor,
2801 crate::overlay::OverlayPlacement::Below,
2802 crate::overlay::DismissBehavior::EscapeOrClickOutside,
2803 ));
2804
2805 assert_eq!(tree.active_overlays().len(), 1);
2806
2807 tree.press_key(Key::Escape, Modifiers::NONE);
2808 assert!(tree.active_overlays().is_empty());
2809 assert!(!tree.is_visible(content));
2810 }
2811
2812 fn show_two_nested_overlays(tree: &mut WidgetTree) -> (WidgetId, WidgetId) {
2815 let anchor = tree.add(FillWidget::new());
2816 let c1 = tree.add(FillWidget::new());
2817 let c2 = tree.add(FillWidget::new());
2818 tree.layout(SizeProposal::exact(200.0, 100.0));
2819 let o1 = tree.show_overlay(crate::overlay::OverlayRequest::new(
2820 c1,
2821 anchor,
2822 crate::overlay::OverlayPlacement::Below,
2823 crate::overlay::DismissBehavior::Manual,
2824 ));
2825 tree.show_overlay(
2826 crate::overlay::OverlayRequest::new(
2827 c2,
2828 c1,
2829 crate::overlay::OverlayPlacement::Below,
2830 crate::overlay::DismissBehavior::Manual,
2831 )
2832 .parent_overlay(o1),
2833 );
2834 (c1, c2)
2835 }
2836
2837 #[test]
2838 fn nested_overlay_back_key_dismisses_with_arrow_left_under_ltr() {
2839 let mut tree = WidgetTree::new();
2840 let _ = show_two_nested_overlays(&mut tree);
2841 assert_eq!(tree.active_overlays().len(), 2);
2842
2843 tree.press_key(Key::ArrowRight, Modifiers::NONE);
2845 assert_eq!(tree.active_overlays().len(), 2);
2846
2847 tree.press_key(Key::ArrowLeft, Modifiers::NONE);
2849 assert_eq!(tree.active_overlays().len(), 1);
2850 }
2851
2852 #[test]
2853 fn nested_overlay_back_key_flips_to_arrow_right_under_rtl() {
2854 let mut tree = WidgetTree::new();
2855 tree.set_layout_direction(crate::environment::LayoutDirection::RightToLeft);
2856 let _ = show_two_nested_overlays(&mut tree);
2857 assert_eq!(tree.active_overlays().len(), 2);
2858
2859 tree.press_key(Key::ArrowLeft, Modifiers::NONE);
2862 assert_eq!(tree.active_overlays().len(), 2);
2863
2864 tree.press_key(Key::ArrowRight, Modifiers::NONE);
2866 assert_eq!(tree.active_overlays().len(), 1);
2867 }
2868
2869 fn show_inert_overlay(tree: &mut WidgetTree, anchor: WidgetId) -> WidgetId {
2871 let content = tree.add(FillWidget::new());
2872 tree.layout(SizeProposal::exact(200.0, 100.0));
2873 tree.show_overlay(
2874 crate::overlay::OverlayRequest::new(
2875 content,
2876 anchor,
2877 crate::overlay::OverlayPlacement::Below,
2878 crate::overlay::DismissBehavior::EscapeKey,
2879 )
2880 .inert(),
2881 );
2882 content
2883 }
2884
2885 #[test]
2890 fn back_key_does_not_count_an_inert_overlay_as_a_menu_level() {
2891 for inert_first in [true, false] {
2892 let mut tree = WidgetTree::new();
2893 let anchor = tree.add(FillWidget::new());
2894 let menu = tree.add(FillWidget::new());
2895 tree.layout(SizeProposal::exact(200.0, 100.0));
2896 let show_menu = |tree: &mut WidgetTree| {
2897 tree.show_overlay(crate::overlay::OverlayRequest::new(
2898 menu,
2899 anchor,
2900 crate::overlay::OverlayPlacement::Below,
2901 crate::overlay::DismissBehavior::Manual,
2902 ));
2903 };
2904 let inert = if inert_first {
2905 let inert = show_inert_overlay(&mut tree, anchor);
2906 show_menu(&mut tree);
2907 inert
2908 } else {
2909 show_menu(&mut tree);
2910 show_inert_overlay(&mut tree, anchor)
2911 };
2912 assert_eq!(tree.active_overlays().len(), 2);
2913
2914 tree.press_key(Key::ArrowLeft, Modifiers::NONE);
2915 assert_eq!(
2916 tree.active_overlays().len(),
2917 2,
2918 "inert shown first: {inert_first}: one menu is not a cascade"
2919 );
2920 assert!(tree.is_visible(menu) && tree.is_visible(inert));
2921 }
2922 }
2923
2924 #[test]
2927 fn back_key_closes_the_submenu_beneath_an_inert_overlay() {
2928 let mut tree = WidgetTree::new();
2929 let (parent, submenu) = show_two_nested_overlays(&mut tree);
2930 let inert = show_inert_overlay(&mut tree, parent);
2931 assert_eq!(tree.active_overlays().len(), 3);
2932
2933 tree.press_key(Key::ArrowLeft, Modifiers::NONE);
2934 assert!(!tree.is_visible(submenu), "the submenu closes");
2935 assert!(tree.is_visible(parent), "its parent stays open");
2936 assert!(
2937 tree.is_visible(inert),
2938 "the inert overlay is not what closes"
2939 );
2940 }
2941
2942 #[derive(Debug)]
2945 struct KeyProbe {
2946 seen: std::rc::Rc<std::cell::RefCell<Vec<Key>>>,
2947 }
2948
2949 impl Widget for KeyProbe {
2950 fn layout_response(
2951 &self,
2952 proposal: SizeProposal,
2953 _ctx: &crate::widget::LayoutContext,
2954 ) -> crate::widget::LayoutResponse {
2955 proposal.resolve(0.0, 0.0).into()
2956 }
2957
2958 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
2959 let seen = self.seen.clone();
2960 ctx.apply_self_handlers(
2961 crate::widget_builder::HandlerSet::new()
2962 .focusable(true)
2963 .on_key(move |event, _ctx| match event {
2964 WidgetEvent::KeyDown { key, .. } => {
2965 seen.borrow_mut().push(*key);
2966 crate::event::EventResponse::Handled
2967 }
2968 _ => crate::event::EventResponse::Ignored,
2969 }),
2970 );
2971 Vec::new()
2972 }
2973 }
2974
2975 #[test]
2987 fn back_key_ignores_text_affordance_overlays_and_reaches_the_editor() {
2988 let mut tree = WidgetTree::new();
2989 let seen = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
2990 let editor = tree.add(KeyProbe { seen: seen.clone() });
2991 let handles_a = tree.add(FillWidget::new());
2992 let handles_b = tree.add(FillWidget::new());
2993 tree.layout(SizeProposal::exact(200.0, 100.0));
2994 tree.focus(editor);
2995
2996 for content in [handles_a, handles_b] {
2998 tree.show_overlay_in_band(
2999 crate::overlay::OverlayRequest::new(
3000 content,
3001 editor,
3002 crate::overlay::OverlayPlacement::FullViewport,
3003 crate::overlay::DismissBehavior::Manual,
3004 ),
3005 crate::overlay::OverlayBand::TextAffordance,
3006 );
3007 }
3008 assert_eq!(tree.active_overlays().len(), 2);
3009
3010 tree.press_key(Key::ArrowLeft, Modifiers::NONE);
3011
3012 assert_eq!(
3013 tree.active_overlays().len(),
3014 2,
3015 "a text affordance is not a menu cascade level — the back key must \
3016 leave both hosts standing"
3017 );
3018 assert_eq!(
3019 seen.borrow().as_slice(),
3020 &[Key::ArrowLeft],
3021 "ArrowLeft must reach the focused editor; swallowing it is the bug \
3022 that left the caret unable to step left in any dual-pane tab"
3023 );
3024 }
3025
3026 #[test]
3030 fn back_key_still_closes_a_submenu_over_a_mounted_text_affordance() {
3031 let mut tree = WidgetTree::new();
3032 let affordance = tree.add(FillWidget::new());
3033 let anchor = tree.add(FillWidget::new());
3034 let menu = tree.add(FillWidget::new());
3035 let submenu = tree.add(FillWidget::new());
3036 tree.layout(SizeProposal::exact(200.0, 100.0));
3037 tree.show_overlay_in_band(
3038 crate::overlay::OverlayRequest::new(
3039 affordance,
3040 anchor,
3041 crate::overlay::OverlayPlacement::FullViewport,
3042 crate::overlay::DismissBehavior::Manual,
3043 ),
3044 crate::overlay::OverlayBand::TextAffordance,
3045 );
3046 let parent = tree.show_overlay(crate::overlay::OverlayRequest::new(
3047 menu,
3048 anchor,
3049 crate::overlay::OverlayPlacement::Below,
3050 crate::overlay::DismissBehavior::Manual,
3051 ));
3052 tree.show_overlay(
3053 crate::overlay::OverlayRequest::new(
3054 submenu,
3055 menu,
3056 crate::overlay::OverlayPlacement::Below,
3057 crate::overlay::DismissBehavior::Manual,
3058 )
3059 .parent_overlay(parent),
3060 );
3061 assert_eq!(tree.active_overlays().len(), 3);
3062
3063 tree.press_key(Key::ArrowLeft, Modifiers::NONE);
3064 assert_eq!(
3065 tree.active_overlays().len(),
3066 2,
3067 "the submenu must still close: the affordance host narrows the \
3068 count, it does not disarm the back key"
3069 );
3070 }
3071
3072 #[test]
3077 fn back_key_does_not_dismiss_a_dialog_on_top_of_a_modal() {
3078 let mut tree = WidgetTree::new();
3079 let anchor = tree.add(FillWidget::new());
3080 let scrim1 = tree.add(FillWidget::new());
3081 let scrim2 = tree.add(FillWidget::new());
3082 let dialog = tree.add(FillWidget::new());
3083 tree.layout(SizeProposal::exact(200.0, 100.0));
3084 for c in [scrim1, scrim2] {
3086 tree.show_overlay(crate::overlay::OverlayRequest::new(
3087 c,
3088 anchor,
3089 crate::overlay::OverlayPlacement::Below,
3090 crate::overlay::DismissBehavior::Manual,
3091 ));
3092 }
3093 tree.show_overlay(crate::overlay::OverlayRequest::new(
3095 dialog,
3096 anchor,
3097 crate::overlay::OverlayPlacement::Centered,
3098 crate::overlay::DismissBehavior::Manual,
3099 ));
3100 assert_eq!(tree.active_overlays().len(), 3);
3101
3102 tree.press_key(Key::ArrowLeft, Modifiers::NONE);
3105 assert_eq!(
3106 tree.active_overlays().len(),
3107 3,
3108 "the back key must not close a dialog sitting on top of a modal"
3109 );
3110 }
3111
3112 #[test]
3113 fn escape_does_not_dismiss_manual_overlay() {
3114 let mut tree = WidgetTree::new();
3115 let anchor = tree.add(FillWidget::new().focusable());
3116 let content = tree.add(FillWidget::new());
3117 tree.layout(SizeProposal::exact(200.0, 100.0));
3118 tree.focus(anchor);
3119
3120 tree.show_overlay(crate::overlay::OverlayRequest::new(
3121 content,
3122 anchor,
3123 crate::overlay::OverlayPlacement::Below,
3124 crate::overlay::DismissBehavior::Manual,
3125 ));
3126
3127 assert_eq!(tree.active_overlays().len(), 1);
3128
3129 tree.press_key(Key::Escape, Modifiers::NONE);
3130 assert_eq!(tree.active_overlays().len(), 1);
3132 }
3133
3134 #[test]
3135 fn click_outside_dismisses_overlay() {
3136 let mut tree = WidgetTree::new();
3137 let anchor = tree.add(FillWidget::new());
3138 let content = tree.add(FillWidget::new());
3139 tree.layout(SizeProposal::exact(200.0, 100.0));
3140
3141 let overlay = tree.show_overlay(crate::overlay::OverlayRequest::new(
3142 content,
3143 anchor,
3144 crate::overlay::OverlayPlacement::Below,
3145 crate::overlay::DismissBehavior::ClickOutside,
3146 ));
3147
3148 tree.overlay_manager
3149 .set_content_bounds(overlay, teksilo_canvas::Size::new(100.0, 50.0));
3150
3151 assert_eq!(tree.active_overlays().len(), 1);
3152
3153 tree.dispatch_event(WidgetEvent::pointer_down(
3154 Point::new(500.0, 500.0),
3155 PointerButton::Primary,
3156 Modifiers::NONE,
3157 ));
3158 assert!(tree.active_overlays().is_empty());
3159 assert!(!tree.is_visible(content));
3160 }
3161
3162 #[test]
3163 fn cascade_dismissal() {
3164 let mut tree = WidgetTree::new();
3165 let anchor = tree.add(FillWidget::new());
3166 let content_a = tree.add(FillWidget::new());
3167 let content_b = tree.add(FillWidget::new());
3168 tree.layout(SizeProposal::exact(200.0, 100.0));
3169
3170 let parent = tree.show_overlay(crate::overlay::OverlayRequest::new(
3171 content_a,
3172 anchor,
3173 crate::overlay::OverlayPlacement::Below,
3174 crate::overlay::DismissBehavior::Manual,
3175 ));
3176 tree.show_overlay(
3177 crate::overlay::OverlayRequest::new(
3178 content_b,
3179 content_a,
3180 crate::overlay::OverlayPlacement::TrailingEdge,
3181 crate::overlay::DismissBehavior::Manual,
3182 )
3183 .parent_overlay(parent),
3184 );
3185
3186 assert_eq!(tree.active_overlays().len(), 2);
3187
3188 tree.dismiss_overlay(parent);
3189 assert!(tree.active_overlays().is_empty());
3190 assert!(!tree.is_visible(content_a));
3191 assert!(!tree.is_visible(content_b));
3192 }
3193
3194 #[test]
3195 fn dismissed_overlay_content_is_dormant_and_invisible() {
3196 let mut tree = WidgetTree::new();
3197 let anchor = tree.add(FillWidget::new());
3198 let content = tree.add(FillWidget::new());
3199 tree.layout(SizeProposal::exact(800.0, 600.0));
3200
3201 let id = tree.show_overlay(crate::overlay::OverlayRequest::new(
3202 content,
3203 anchor,
3204 crate::overlay::OverlayPlacement::Below,
3205 crate::overlay::DismissBehavior::Manual,
3206 ));
3207
3208 tree.layout(SizeProposal::exact(800.0, 600.0));
3209
3210 tree.dismiss_overlay(id);
3211 assert!(!tree.is_visible(content));
3212
3213 tree.layout(SizeProposal::exact(800.0, 600.0));
3214
3215 let center = tree.bounds(content).center();
3216 let hit = tree.hit_test(center);
3217 assert_ne!(hit, Some(content));
3218
3219 let _frame = tree.render();
3220 assert!(!tree.is_visible(content));
3221 }
3222
3223 #[test]
3224 fn tooltip_appears_after_delay() {
3225 let mut tree = WidgetTree::new();
3226 let anchor = tree.add(FillWidget::new());
3227 let tooltip = tree.add(FillWidget::new().label("Tooltip text"));
3228 tree.layout(SizeProposal::exact(200.0, 100.0));
3229
3230 tree.attach_tooltip(anchor, tooltip, std::time::Duration::from_millis(500));
3231
3232 let center = tree.bounds(anchor).center();
3233 tree.pointer_move(center);
3234 assert!(tree.active_overlays().is_empty());
3235
3236 tree.advance_time(std::time::Duration::from_millis(600));
3237
3238 assert_eq!(tree.active_overlays().len(), 1);
3239 assert!(tree.find_by_label("Tooltip text").is_some());
3240 }
3241
3242 #[test]
3243 fn tooltip_survives_theme_switch() {
3244 let mut tree = WidgetTree::new().with_theme(crate::presets::intui::light());
3249 let anchor = tree.add(FillWidget::new());
3250 let tooltip = tree.add(FillWidget::new().label("Tip"));
3251 tree.layout(SizeProposal::exact(200.0, 100.0));
3252
3253 tree.attach_tooltip(anchor, tooltip, std::time::Duration::from_millis(500));
3254
3255 tree.set_theme(crate::presets::intui::dark());
3256
3257 tree.pointer_move(tree.bounds(anchor).center());
3258 tree.advance_time(std::time::Duration::from_millis(600));
3259
3260 assert_eq!(tree.active_overlays().len(), 1);
3261 assert!(tree.find_by_label("Tip").is_some());
3262 }
3263
3264 #[test]
3265 fn tooltip_suppressed_when_hovering_anchor_owned_overlay_content() {
3266 let mut tree = WidgetTree::new();
3274 let anchor = tree.add(FillWidget::new());
3275 let panel = tree.add_child(anchor, FillWidget::new());
3278 let row = tree.add_child(panel, FillWidget::new());
3279 let tip = tree.add(FillWidget::new().label("Tip"));
3280 tree.layout(SizeProposal::exact(200.0, 100.0));
3281
3282 let delay = std::time::Duration::from_millis(100);
3283 tree.attach_tooltip(anchor, tip, delay);
3284
3285 tree.tooltip_pointer_enter(anchor);
3288 tree.advance_time(delay + std::time::Duration::from_millis(50));
3289 assert_eq!(
3290 tree.active_overlays().len(),
3291 1,
3292 "tooltip should appear when hovering the anchor itself"
3293 );
3294 tree.pointer_move(Point::new(500.0, 500.0));
3297 tree.advance_time(std::time::Duration::from_millis(150));
3298 assert!(tree.active_overlays().is_empty());
3299
3300 tree.show_overlay(crate::overlay::OverlayRequest::new(
3302 panel,
3303 anchor,
3304 crate::overlay::OverlayPlacement::Below,
3305 crate::overlay::DismissBehavior::Manual,
3306 ));
3307 assert_eq!(tree.active_overlays().len(), 1);
3308
3309 tree.tooltip_pointer_enter(row);
3312 tree.advance_time(delay + std::time::Duration::from_millis(50));
3313 assert_eq!(
3314 tree.active_overlays().len(),
3315 1,
3316 "anchor tooltip must not leak onto its own overlay's rows"
3317 );
3318 }
3319
3320 #[test]
3321 fn tooltip_inside_overlay_still_fires() {
3322 let mut tree = WidgetTree::new();
3326 let host = tree.add(FillWidget::new());
3327 let panel = tree.add_child(host, FillWidget::new());
3328 let row = tree.add_child(panel, FillWidget::new());
3329 let tip = tree.add(FillWidget::new().label("Row tip"));
3330 tree.layout(SizeProposal::exact(200.0, 100.0));
3331
3332 let delay = std::time::Duration::from_millis(100);
3333 tree.attach_tooltip(row, tip, delay);
3335
3336 tree.show_overlay(crate::overlay::OverlayRequest::new(
3337 panel,
3338 host,
3339 crate::overlay::OverlayPlacement::Below,
3340 crate::overlay::DismissBehavior::Manual,
3341 ));
3342 assert_eq!(tree.active_overlays().len(), 1);
3343
3344 tree.tooltip_pointer_enter(row);
3345 tree.advance_time(delay + std::time::Duration::from_millis(50));
3346 assert_eq!(
3347 tree.active_overlays().len(),
3348 2,
3349 "a row's own tooltip should still fire inside an overlay"
3350 );
3351 }
3352
3353 #[test]
3354 fn tooltip_dismissed_on_pointer_leave() {
3355 let mut tree = WidgetTree::new();
3356 let anchor = tree.add(FillWidget::new());
3357 let tooltip = tree.add(FillWidget::new().label("Tip"));
3358 tree.layout(SizeProposal::exact(200.0, 100.0));
3359
3360 tree.attach_tooltip(anchor, tooltip, std::time::Duration::from_millis(500));
3361
3362 tree.pointer_move(tree.bounds(anchor).center());
3363 tree.advance_time(std::time::Duration::from_millis(600));
3364 assert_eq!(tree.active_overlays().len(), 1);
3365
3366 tree.pointer_move(Point::new(500.0, 500.0));
3371 assert_eq!(
3372 tree.active_overlays().len(),
3373 1,
3374 "tooltip persists briefly after anchor-leave (hoverable grace)"
3375 );
3376 tree.advance_time(std::time::Duration::from_millis(150));
3377 assert!(
3378 tree.active_overlays().is_empty(),
3379 "tooltip dismissed after the 100ms grace outside anchor+overlay"
3380 );
3381 }
3382
3383 #[test]
3384 fn tooltip_not_shown_if_pointer_leaves_before_delay() {
3385 let mut tree = WidgetTree::new();
3386 let anchor = tree.add(FillWidget::new());
3387 let tooltip = tree.add(FillWidget::new().label("Tip"));
3388 tree.layout(SizeProposal::exact(200.0, 100.0));
3389
3390 tree.attach_tooltip(anchor, tooltip, std::time::Duration::from_millis(500));
3391
3392 tree.pointer_move(tree.bounds(anchor).center());
3393 tree.advance_time(std::time::Duration::from_millis(200));
3394 tree.pointer_move(Point::new(500.0, 500.0));
3395
3396 tree.advance_time(std::time::Duration::from_millis(500));
3397 assert!(tree.active_overlays().is_empty());
3398 }
3399
3400 #[test]
3401 fn tooltip_reshow_uses_short_delay_after_prior_tip() {
3402 let mut tree = WidgetTree::new();
3405 let a = tree.add(FillWidget::new());
3406 let b = tree.add(FillWidget::new());
3407 let tip_a = tree.add(FillWidget::new().label("Tip A"));
3408 let tip_b = tree.add(FillWidget::new().label("Tip B"));
3409 tree.layout(SizeProposal::exact(400.0, 200.0));
3410
3411 let delay = std::time::Duration::from_millis(500);
3412 tree.attach_tooltip(a, tip_a, delay);
3413 tree.attach_tooltip(b, tip_b, delay);
3414
3415 tree.tooltip_pointer_enter(a);
3418 tree.advance_time(std::time::Duration::from_millis(150));
3419 assert!(
3420 tree.active_overlays().is_empty(),
3421 "must not appear before full initial delay"
3422 );
3423 tree.advance_time(std::time::Duration::from_millis(400));
3424 assert_eq!(
3425 tree.active_overlays().len(),
3426 1,
3427 "first tip after initial delay"
3428 );
3429 assert!(tree.find_by_label("Tip A").is_some());
3430
3431 let mut noop = crate::window::NoopWindowOps;
3433 tree.tooltip_pointer_leave(a, &mut noop);
3434 tree.tooltip_pointer_enter(b);
3435 tree.advance_time(std::time::Duration::from_millis(120));
3436 assert!(
3437 tree.find_by_label("Tip B").is_some(),
3438 "second tip should use the short reshow delay while session is warm"
3439 );
3440 }
3441
3442 #[test]
3443 fn reshow_delay_reverts_to_full_after_the_session_grace_expires() {
3444 let mut tree = WidgetTree::new();
3450 tree.set_accessibility_preferences(false, true, 1.0);
3454 let a = tree.add(FillWidget::new());
3455 let b = tree.add(FillWidget::new());
3456 let tip_a = tree.add(FillWidget::new().label("Tip A"));
3457 let tip_b = tree.add(FillWidget::new().label("Tip B"));
3458 tree.layout(SizeProposal::exact(400.0, 200.0));
3459
3460 let delay = std::time::Duration::from_millis(500);
3461 tree.attach_tooltip(a, tip_a, delay);
3462 tree.attach_tooltip(b, tip_b, delay);
3463
3464 tree.tooltip_pointer_enter(a);
3466 tree.advance_time(std::time::Duration::from_millis(550));
3467 assert!(tree.find_by_label("Tip A").is_some(), "first tip shown");
3468
3469 tree.pointer_move(Point::new(900.0, 900.0));
3470 tree.advance_time(std::time::Duration::from_millis(150));
3471 assert!(tree.active_overlays().is_empty(), "A dismissed on leave");
3472
3473 tree.advance_time(super::TOOLTIP_SESSION_GRACE + std::time::Duration::from_millis(50));
3475
3476 tree.tooltip_pointer_enter(b);
3477 tree.advance_time(std::time::Duration::from_millis(150));
3478 assert!(
3479 tree.active_overlays().is_empty(),
3480 "session went cold — B must pay the FULL delay, not the 100 ms reshow"
3481 );
3482 tree.advance_time(std::time::Duration::from_millis(400));
3483 assert!(
3484 tree.find_by_label("Tip B").is_some(),
3485 "B still appears once its full delay elapses"
3486 );
3487 }
3488
3489 #[test]
3490 fn warm_reshow_scales_the_delay_rather_than_flattening_every_tier() {
3491 let mut tree = WidgetTree::new();
3497 let light = tree.add(FillWidget::new());
3498 let heavy = tree.add(FillWidget::new());
3499 let tip_light = tree.add(FillWidget::new().label("Light"));
3500 let tip_heavy = tree.add(FillWidget::new().label("Heavy"));
3501 tree.layout(SizeProposal::exact(400.0, 200.0));
3502
3503 tree.attach_tooltip(light, tip_light, std::time::Duration::from_millis(500));
3504 tree.attach_tooltip(heavy, tip_heavy, std::time::Duration::from_millis(700));
3505
3506 tree.tooltip_pointer_enter(light);
3508 tree.advance_time(std::time::Duration::from_millis(550));
3509 assert!(tree.find_by_label("Light").is_some(), "session warm");
3510
3511 let mut noop = crate::window::NoopWindowOps;
3512 tree.tooltip_pointer_leave(light, &mut noop);
3513 tree.tooltip_pointer_enter(heavy);
3514
3515 tree.advance_time(std::time::Duration::from_millis(120));
3517 assert!(
3518 tree.find_by_label("Heavy").is_none(),
3519 "a heavy tooltip must not fire at the light tier's reshow delay"
3520 );
3521 tree.advance_time(std::time::Duration::from_millis(40));
3523 assert!(
3524 tree.find_by_label("Heavy").is_some(),
3525 "heavy reshow is the scaled 140 ms"
3526 );
3527 }
3528
3529 #[test]
3530 fn a_pinned_sticky_tooltip_does_not_hold_the_session_warm() {
3531 let mut tree = WidgetTree::new();
3535 tree.set_accessibility_preferences(false, true, 1.0); let a = tree.add(FillWidget::new());
3537 let b = tree.add(FillWidget::new());
3538 let tip_a = tree.add(FillWidget::new().label("Pinned"));
3539 let tip_b = tree.add(FillWidget::new().label("Other"));
3540 tree.layout(SizeProposal::exact(400.0, 200.0));
3541
3542 tree.attach_tooltip_with_sticky(
3543 a,
3544 tip_a,
3545 std::time::Duration::from_millis(500),
3546 Some(std::time::Duration::from_millis(100)),
3547 );
3548 tree.attach_tooltip(b, tip_b, std::time::Duration::from_millis(500));
3549
3550 tree.tooltip_pointer_enter(a);
3551 tree.advance_time(std::time::Duration::from_millis(550));
3552 assert!(tree.find_by_label("Pinned").is_some());
3553 tree.promote_tooltip_to_sticky(tip_a);
3554
3555 tree.advance_time(super::TOOLTIP_SESSION_GRACE + std::time::Duration::from_millis(50));
3557 tree.tooltip_pointer_enter(b);
3558 tree.advance_time(std::time::Duration::from_millis(150));
3559 assert!(
3560 tree.find_by_label("Other").is_none(),
3561 "a pinned sticky must not keep every other anchor on the reshow path"
3562 );
3563 }
3564
3565 #[test]
3566 fn reattaching_a_tooltip_does_not_grow_the_entry_table() {
3567 let mut tree = WidgetTree::new();
3573 let anchor = tree.add(FillWidget::new());
3574 tree.layout(SizeProposal::exact(200.0, 100.0));
3575
3576 let delay = std::time::Duration::from_millis(100);
3577 for _ in 0..25 {
3578 let tip = tree.add(FillWidget::new().label("Tip"));
3580 tree.attach_tooltip(anchor, tip, delay);
3581 assert_eq!(
3582 tree.tooltip_entry_count(),
3583 1,
3584 "an anchor must own exactly one tooltip entry across rebuilds"
3585 );
3586 }
3587
3588 tree.tooltip_pointer_enter(anchor);
3590 tree.advance_time(delay + std::time::Duration::from_millis(50));
3591 assert_eq!(
3592 tree.active_overlays().len(),
3593 1,
3594 "latest tooltip still shows"
3595 );
3596 }
3597
3598 #[test]
3599 fn destroying_an_anchor_reaps_its_tooltip_entry() {
3600 let mut tree = WidgetTree::new();
3603 let host = tree.add(FillWidget::new());
3604 let anchor = tree.add_child(host, FillWidget::new());
3605 let tip = tree.add(FillWidget::new().label("Tip"));
3606 tree.layout(SizeProposal::exact(200.0, 100.0));
3607
3608 tree.attach_tooltip(anchor, tip, std::time::Duration::from_millis(100));
3609 assert_eq!(tree.tooltip_entry_count(), 1);
3610
3611 tree.destroy_subtree(anchor);
3612 assert_eq!(
3613 tree.tooltip_entry_count(),
3614 0,
3615 "destroying the anchor must reap its entry and content node"
3616 );
3617 }
3618
3619 #[test]
3620 fn a_leaving_tooltip_schedules_a_wake_for_its_dismissal() {
3621 let mut tree = WidgetTree::new();
3626 tree.set_accessibility_preferences(false, true, 1.0); let anchor = tree.add(FillWidget::new());
3628 let tip = tree.add(FillWidget::new().label("Tip"));
3629 tree.layout(SizeProposal::exact(400.0, 200.0));
3630
3631 tree.attach_tooltip(anchor, tip, std::time::Duration::from_millis(100));
3632 tree.pointer_move(tree.bounds(anchor).center());
3633 tree.advance_time(std::time::Duration::from_millis(150));
3634 assert_eq!(tree.active_overlays().len(), 1, "tooltip shown");
3635
3636 assert!(
3638 tree.next_timer_deadline().is_none(),
3639 "a settled tooltip under the pointer schedules nothing"
3640 );
3641
3642 tree.pointer_move(Point::new(900.0, 900.0));
3645 assert!(
3646 tree.next_timer_deadline().is_some(),
3647 "the PointerLeave grace must contribute a wake deadline"
3648 );
3649 }
3650
3651 #[test]
3662 fn a_fading_out_overlay_schedules_a_wake_for_its_deferred_removal() {
3663 let mut tree = WidgetTree::new();
3664 let anchor = tree.add(FillWidget::new());
3665 let content = tree.add(FillWidget::new().label("Faded"));
3666 tree.layout(SizeProposal::exact(200.0, 100.0));
3667
3668 let id = tree.show_overlay(
3669 crate::overlay::OverlayRequest::new(
3670 content,
3671 anchor,
3672 crate::overlay::OverlayPlacement::Below,
3673 crate::overlay::DismissBehavior::Manual,
3674 )
3675 .with_fade(std::time::Duration::from_millis(200)),
3676 );
3677 tree.dismiss_overlay(id);
3678 assert!(
3679 tree.is_visible(content),
3680 "precondition: the removal is deferred, so there is something to wake for"
3681 );
3682
3683 let expected = tree.overlay_manager.next_fade_dismiss_deadline();
3689 assert!(
3690 expected.is_some(),
3691 "precondition: the fade start was stamped, so there is a deadline to compare against"
3692 );
3693 assert_eq!(
3694 tree.next_timer_deadline(),
3695 expected,
3696 "the wake deadline must be the deferred removal's own, not merely some deadline"
3697 );
3698 }
3699
3700 #[test]
3701 fn pressing_cancels_a_pending_dwell_and_dismisses_a_shown_tooltip() {
3702 let mut tree = WidgetTree::new();
3703 let anchor = tree.add(FillWidget::new());
3704 let tip = tree.add(FillWidget::new().label("Tip"));
3705 tree.layout(SizeProposal::exact(400.0, 200.0));
3706
3707 let delay = std::time::Duration::from_millis(500);
3708 tree.attach_tooltip(anchor, tip, delay);
3709
3710 tree.pointer_move(tree.bounds(anchor).center());
3713 tree.advance_time(std::time::Duration::from_millis(300));
3714 tree.tooltip_pointer_press(None);
3715 tree.advance_time(std::time::Duration::from_millis(400));
3716 assert!(
3717 tree.active_overlays().is_empty(),
3718 "a press must cancel the pending dwell, not merely delay it"
3719 );
3720
3721 tree.tooltip_pointer_enter(anchor);
3724 tree.advance_time(delay + std::time::Duration::from_millis(50));
3725 assert_eq!(tree.active_overlays().len(), 1, "tooltip shown again");
3726 tree.tooltip_pointer_press(Some(tree.bounds(anchor).center()));
3727 assert!(
3728 tree.active_overlays().is_empty(),
3729 "a press must dismiss the shown tooltip"
3730 );
3731 }
3732
3733 #[test]
3734 fn a_pointer_that_left_the_window_by_a_jump_arms_no_tooltip() {
3735 let mut tree = WidgetTree::new();
3741 let anchor = tree.add(FillWidget::new());
3742 let tip = tree.add(FillWidget::new().label("Tip"));
3743 tree.layout(SizeProposal::exact(400.0, 200.0));
3744 let delay = std::time::Duration::from_millis(300);
3745 tree.attach_tooltip(anchor, tip, delay);
3746
3747 let centre = tree.bounds(anchor).center();
3748 tree.pointer_move(centre);
3749 tree.dispatch_event(WidgetEvent::pointer_down(
3750 centre,
3751 PointerButton::Primary,
3752 Modifiers::NONE,
3753 ));
3754 tree.dispatch_event(WidgetEvent::pointer_up(
3755 centre,
3756 PointerButton::Primary,
3757 Modifiers::NONE,
3758 ));
3759
3760 let mut noop = crate::window::NoopWindowOps;
3761 tree.pointer_left_window(&mut noop);
3762 tree.arena.mark_needs_layout(anchor);
3763 tree.arena.mark_ancestors_need_layout(anchor);
3764 tree.layout(SizeProposal::exact(400.0, 200.0));
3765 tree.advance_time(delay * 3);
3766 assert!(
3767 tree.active_overlays().is_empty(),
3768 "no tooltip may open once the pointer has left the window"
3769 );
3770 assert_eq!(
3771 tree.pending_tooltip_count(),
3772 0,
3773 "no dwell may be left running either"
3774 );
3775
3776 tree.pointer_move(Point::new(centre.x + 1.0, centre.y));
3778 tree.advance_time(delay * 2);
3779 assert_eq!(tree.active_overlays().len(), 1, "a pointer inside arms it");
3780 }
3781
3782 #[test]
3783 fn a_click_that_rebuilds_the_anchor_does_not_start_its_dwell_again() {
3784 let mut tree = WidgetTree::new();
3790 let anchor = tree.add(FillWidget::new());
3791 let tip = tree.add(FillWidget::new().label("Tip"));
3792 tree.layout(SizeProposal::exact(400.0, 200.0));
3793 let delay = std::time::Duration::from_millis(300);
3794 tree.attach_tooltip(anchor, tip, delay);
3795
3796 let centre = tree.bounds(anchor).center();
3797 tree.pointer_move(centre);
3798 tree.dispatch_event(WidgetEvent::pointer_down(
3799 centre,
3800 PointerButton::Primary,
3801 Modifiers::NONE,
3802 ));
3803 tree.dispatch_event(WidgetEvent::pointer_up(
3804 centre,
3805 PointerButton::Primary,
3806 Modifiers::NONE,
3807 ));
3808
3809 tree.set_hovered(None);
3811 tree.arena.mark_needs_layout(anchor);
3812 tree.arena.mark_ancestors_need_layout(anchor);
3813 tree.layout(SizeProposal::exact(400.0, 200.0));
3814 assert!(tree.hovered_id().is_some(), "hover is recovered");
3815 tree.advance_time(delay * 3);
3816 assert!(
3817 tree.active_overlays().is_empty(),
3818 "no tooltip right after a click on its anchor"
3819 );
3820
3821 tree.pointer_left_window(&mut crate::window::NoopWindowOps);
3823 tree.pointer_move(centre);
3824 tree.advance_time(delay * 2);
3825 assert_eq!(tree.active_overlays().len(), 1);
3826 }
3827
3828 #[test]
3829 fn window_deactivation_retires_hover_tooltips() {
3830 let mut tree = WidgetTree::new();
3831 let anchor = tree.add(FillWidget::new());
3832 let tip = tree.add(FillWidget::new().label("Tip"));
3833 tree.layout(SizeProposal::exact(400.0, 200.0));
3834
3835 tree.attach_tooltip(anchor, tip, std::time::Duration::from_millis(100));
3836 tree.pointer_move(tree.bounds(anchor).center());
3837 tree.advance_time(std::time::Duration::from_millis(150));
3838 assert_eq!(tree.active_overlays().len(), 1);
3839
3840 tree.set_window_active(false);
3841 assert!(
3842 tree.active_overlays().is_empty(),
3843 "a tooltip must not float over another window's chrome"
3844 );
3845 }
3846
3847 #[test]
3848 fn only_the_innermost_anchor_arms_its_dwell() {
3849 let mut tree = WidgetTree::new();
3852 let panel = tree.add(FillWidget::new());
3853 let row = tree.add_child(panel, FillWidget::new());
3854 let panel_tip = tree.add(FillWidget::new().label("Panel"));
3855 let row_tip = tree.add(FillWidget::new().label("Row"));
3856 tree.layout(SizeProposal::exact(400.0, 200.0));
3857
3858 let delay = std::time::Duration::from_millis(100);
3859 tree.attach_tooltip(panel, panel_tip, delay);
3860 tree.attach_tooltip(row, row_tip, delay);
3861
3862 tree.tooltip_pointer_enter(row);
3863 tree.advance_time(delay + std::time::Duration::from_millis(50));
3864
3865 assert_eq!(
3866 tree.active_overlays().len(),
3867 1,
3868 "exactly one tooltip may open for a hover"
3869 );
3870 assert!(
3871 tree.find_by_label("Row").is_some(),
3872 "the innermost anchor wins"
3873 );
3874 }
3875
3876 #[test]
3877 fn escape_dismisses_a_hover_tooltip_and_falls_through_to_the_menu_below() {
3878 let mut tree = WidgetTree::new();
3882 tree.set_accessibility_preferences(false, true, 1.0); let anchor = tree.add(FillWidget::new());
3884 let menu = tree.add(FillWidget::new());
3885 let tip = tree.add(FillWidget::new().label("Tip"));
3886 tree.layout(SizeProposal::exact(400.0, 200.0));
3887
3888 let menu_overlay = tree.show_overlay(crate::overlay::OverlayRequest::new(
3889 menu,
3890 anchor,
3891 crate::overlay::OverlayPlacement::Below,
3892 crate::overlay::DismissBehavior::EscapeOrClickOutside,
3893 ));
3894 assert_eq!(tree.active_overlays().len(), 1);
3895
3896 tree.attach_tooltip(anchor, tip, std::time::Duration::from_millis(100));
3898 tree.tooltip_pointer_enter(anchor);
3899 tree.advance_time(std::time::Duration::from_millis(150));
3900 assert_eq!(
3901 tree.active_overlays().len(),
3902 2,
3903 "tooltip sits above the menu"
3904 );
3905
3906 let dismissed = tree.overlay_manager.try_dismiss_top_on_escape();
3908 assert!(dismissed.is_some(), "Escape must dismiss the hover tooltip");
3909 assert_eq!(tree.active_overlays().len(), 1);
3910
3911 let dismissed = tree.overlay_manager.try_dismiss_top_on_escape();
3913 assert_eq!(
3914 dismissed.map(|(id, _, _)| id),
3915 Some(menu_overlay),
3916 "Escape must then reach the menu underneath"
3917 );
3918 assert!(tree.active_overlays().is_empty());
3919 }
3920
3921 #[test]
3922 fn tooltip_timer_restarts_when_pointer_keeps_moving() {
3923 let mut tree = WidgetTree::new();
3926 let anchor = tree.add(FillWidget::new());
3927 let tooltip = tree.add(FillWidget::new().label("Tip"));
3928 tree.layout(SizeProposal::exact(200.0, 100.0));
3929
3930 tree.attach_tooltip(anchor, tooltip, std::time::Duration::from_millis(500));
3931
3932 let start = tree.bounds(anchor).center();
3933 tree.pointer_move(start);
3934 tree.advance_time(std::time::Duration::from_millis(300));
3935 tree.pointer_move(Point::new(start.x + 20.0, start.y));
3937 tree.advance_time(std::time::Duration::from_millis(300));
3940 assert!(
3941 tree.active_overlays().is_empty(),
3942 "moving past stationary slop must restart the delay"
3943 );
3944 tree.advance_time(std::time::Duration::from_millis(250));
3945 assert_eq!(
3946 tree.active_overlays().len(),
3947 1,
3948 "tooltip appears after a full delay of stillness"
3949 );
3950 }
3951
3952 #[test]
3953 fn timed_overlay_auto_dismisses_after_duration() {
3954 let mut tree = WidgetTree::new();
3955 let anchor = tree.add(FillWidget::new());
3956 let content = tree.add(FillWidget::new().label("Toast"));
3957 tree.layout(SizeProposal::exact(200.0, 100.0));
3958
3959 tree.show_overlay_for(
3960 crate::overlay::OverlayRequest::new(
3961 content,
3962 anchor,
3963 crate::overlay::OverlayPlacement::Below,
3964 crate::overlay::DismissBehavior::Manual,
3965 ),
3966 std::time::Duration::from_millis(300),
3967 );
3968
3969 assert_eq!(tree.active_overlays().len(), 1);
3970
3971 tree.advance_time(std::time::Duration::from_millis(200));
3972 assert_eq!(tree.active_overlays().len(), 1);
3973
3974 tree.advance_time(std::time::Duration::from_millis(150));
3975 assert!(tree.active_overlays().is_empty());
3976 assert!(!tree.is_visible(content));
3977 }
3978
3979 #[test]
3980 fn fade_dismiss_keeps_overlay_off_active_list_immediately() {
3981 let mut tree = WidgetTree::new();
3989 let anchor = tree.add(FillWidget::new());
3990 let content = tree.add(FillWidget::new().label("Faded"));
3991 tree.layout(SizeProposal::exact(200.0, 100.0));
3992
3993 let id = tree.show_overlay(
3994 crate::overlay::OverlayRequest::new(
3995 content,
3996 anchor,
3997 crate::overlay::OverlayPlacement::Below,
3998 crate::overlay::DismissBehavior::Manual,
3999 )
4000 .with_fade(std::time::Duration::from_millis(100)),
4001 );
4002 assert_eq!(tree.active_overlays().len(), 1);
4003
4004 tree.dismiss_overlay(id);
4005 assert!(tree.active_overlays().is_empty());
4010 }
4011
4012 #[test]
4013 fn fade_dismiss_defers_content_dormancy_until_sim_tween_completes() {
4014 let mut tree = WidgetTree::new();
4020 let anchor = tree.add(FillWidget::new());
4021 let content = tree.add(FillWidget::new().label("Faded"));
4022 tree.layout(SizeProposal::exact(200.0, 100.0));
4023
4024 let id = tree.show_overlay(
4025 crate::overlay::OverlayRequest::new(
4026 content,
4027 anchor,
4028 crate::overlay::OverlayPlacement::Below,
4029 crate::overlay::DismissBehavior::Manual,
4030 )
4031 .with_fade(std::time::Duration::from_millis(100)),
4032 );
4033 tree.advance_time(std::time::Duration::from_secs(1));
4039
4040 tree.dismiss_overlay(id);
4041 assert!(
4042 tree.is_visible(content),
4043 "content stays active during fade-out"
4044 );
4045
4046 tree.advance_time(std::time::Duration::from_millis(60));
4050 assert!(
4051 tree.is_visible(content),
4052 "60 ms into a 100 ms tween the content is still up"
4053 );
4054
4055 tree.advance_time(std::time::Duration::from_millis(90));
4056 assert!(
4057 !tree.is_visible(content),
4058 "after sim-time past the tween window, deferred removal fires"
4059 );
4060 }
4061
4062 #[test]
4074 fn an_auto_dismissed_fade_starts_at_the_instant_the_dismiss_ran() {
4075 let mut tree = WidgetTree::new();
4076 let anchor = tree.add(FillWidget::new());
4077 let content = tree.add(FillWidget::new().label("Toast"));
4078 tree.layout(SizeProposal::exact(200.0, 100.0));
4079
4080 tree.show_overlay_for(
4081 crate::overlay::OverlayRequest::new(
4082 content,
4083 anchor,
4084 crate::overlay::OverlayPlacement::Below,
4085 crate::overlay::DismissBehavior::Manual,
4086 )
4087 .with_fade(std::time::Duration::from_millis(100)),
4088 std::time::Duration::from_millis(500),
4089 );
4090 assert_eq!(tree.active_overlays().len(), 1);
4091
4092 tree.advance_time(std::time::Duration::from_millis(600));
4096 assert!(
4097 tree.active_overlays().is_empty(),
4098 "precondition: the auto-dismiss fired inside this advance"
4099 );
4100 assert!(
4101 tree.is_visible(content),
4102 "the fade must start at the instant the dismiss ran, so the \
4103 content survives the frame that dismissed it"
4104 );
4105
4106 tree.advance_time(std::time::Duration::from_millis(150));
4107 assert!(
4108 !tree.is_visible(content),
4109 "and is reaped once the tween's own window has passed"
4110 );
4111 }
4112
4113 #[derive(Debug)]
4119 struct Block(f32, f32);
4120
4121 impl Widget for Block {
4122 fn layout_response(
4123 &self,
4124 _proposal: SizeProposal,
4125 _ctx: &LayoutContext,
4126 ) -> crate::widget::LayoutResponse {
4127 teksilo_canvas::Size::new(self.0, self.1).into()
4128 }
4129 }
4130
4131 fn panel_and_an_overlay_above(
4135 panel_dismiss: crate::overlay::DismissBehavior,
4136 top: impl Widget + 'static,
4137 top_dismiss: crate::overlay::DismissBehavior,
4138 top_at: Point,
4139 inert: bool,
4140 ) -> (WidgetTree, WidgetId, WidgetId) {
4141 let mut tree = WidgetTree::new();
4142 let panel = tree.add(Block(100.0, 60.0));
4143 let top = tree.add(top);
4144 let beneath = tree.add(FillWidget::new());
4148 tree.show_overlay(crate::overlay::OverlayRequest::new(
4149 panel,
4150 beneath,
4151 crate::overlay::OverlayPlacement::AtPointer(Point::new(20.0, 20.0)),
4152 panel_dismiss,
4153 ));
4154 let request = crate::overlay::OverlayRequest::new(
4155 top,
4156 beneath,
4157 crate::overlay::OverlayPlacement::AtPointer(top_at),
4158 top_dismiss,
4159 );
4160 tree.show_overlay(if inert { request.inert() } else { request });
4161 tree.layout(SizeProposal::exact(400.0, 200.0));
4162 (tree, panel, top)
4163 }
4164
4165 fn panel_and_an_inert_overlay(
4168 panel_dismiss: crate::overlay::DismissBehavior,
4169 inert_dismiss: crate::overlay::DismissBehavior,
4170 inert_at: Point,
4171 ) -> (WidgetTree, WidgetId, WidgetId) {
4172 panel_and_an_overlay_above(
4173 panel_dismiss,
4174 Block(60.0, 40.0),
4175 inert_dismiss,
4176 inert_at,
4177 true,
4178 )
4179 }
4180
4181 #[test]
4182 fn an_inert_overlay_hands_the_point_to_the_overlay_beneath_it() {
4183 let (tree, panel, inert) = panel_and_an_inert_overlay(
4184 crate::overlay::DismissBehavior::Manual,
4185 crate::overlay::DismissBehavior::EscapeKey,
4186 Point::new(30.0, 30.0),
4187 );
4188 let at = Point::new(40.0, 40.0);
4189 let over = tree.overlay_manager.find_by_content(inert).unwrap();
4190 assert!(
4191 tree.overlay_content_bounds(over).unwrap().contains(at),
4192 "fixture: the inert overlay covers the point"
4193 );
4194 assert_eq!(
4195 tree.hit_test(at),
4196 Some(panel),
4197 "the panel under it takes the point, not the window under both"
4198 );
4199 }
4200
4201 #[test]
4202 fn a_press_on_an_inert_overlay_is_outside_the_overlays_it_is_outside_of() {
4203 let (mut tree, panel, inert) = panel_and_an_inert_overlay(
4204 crate::overlay::DismissBehavior::ClickOutside,
4205 crate::overlay::DismissBehavior::EscapeKey,
4206 Point::new(200.0, 100.0),
4207 );
4208 let at = Point::new(220.0, 110.0);
4209 let over = tree.overlay_manager.find_by_content(inert).unwrap();
4210 assert!(
4211 tree.overlay_content_bounds(over).unwrap().contains(at),
4212 "fixture: the press lands on the inert overlay"
4213 );
4214 tree.pointer_down_button(at, crate::event::PointerButton::Primary);
4215 assert_eq!(
4216 tree.overlay_manager.find_by_content(panel),
4217 None,
4218 "the press was outside the panel, whatever floats over that spot",
4219 );
4220 }
4221
4222 #[test]
4223 fn an_inert_overlay_is_not_an_interactive_rect() {
4224 let (tree, panel, _inert) = panel_and_an_inert_overlay(
4227 crate::overlay::DismissBehavior::Manual,
4228 crate::overlay::DismissBehavior::EscapeKey,
4229 Point::new(200.0, 100.0),
4230 );
4231 let panel_bounds = tree
4232 .overlay_content_bounds(tree.overlay_manager.find_by_content(panel).unwrap())
4233 .unwrap();
4234 assert_eq!(tree.overlay_manager.interactive_rects(), vec![panel_bounds]);
4235 }
4236
4237 #[test]
4238 fn escape_steps_over_an_inert_overlay_to_the_one_beneath() {
4239 let (mut tree, panel, inert) = panel_and_an_inert_overlay(
4242 crate::overlay::DismissBehavior::EscapeKey,
4243 crate::overlay::DismissBehavior::Manual,
4244 Point::new(200.0, 100.0),
4245 );
4246 tree.press_key(Key::Escape, Modifiers::NONE);
4247 assert_eq!(
4248 tree.overlay_manager.find_by_content(panel),
4249 None,
4250 "the panel beneath took the Escape"
4251 );
4252 assert!(
4253 tree.overlay_manager.find_by_content(inert).is_some(),
4254 "and the inert one, which Escape may not close, is still up"
4255 );
4256 }
4257
4258 #[test]
4259 fn an_inert_overlay_over_the_window_hands_the_point_to_the_window() {
4260 let mut tree = WidgetTree::new();
4264 let page = tree.add(FillWidget::new());
4265 let inert = tree.add(Block(60.0, 40.0));
4266 tree.show_overlay(
4267 crate::overlay::OverlayRequest::new(
4268 inert,
4269 page,
4270 crate::overlay::OverlayPlacement::AtPointer(Point::new(200.0, 100.0)),
4271 crate::overlay::DismissBehavior::EscapeKey,
4272 )
4273 .inert(),
4274 );
4275 tree.layout(SizeProposal::exact(400.0, 200.0));
4276 let at = Point::new(220.0, 110.0);
4277 assert!(
4278 tree.overlay_content_bounds(tree.overlay_manager.find_by_content(inert).unwrap())
4279 .unwrap()
4280 .contains(at),
4281 "fixture: the inert overlay covers the point"
4282 );
4283 assert_eq!(tree.hit_test(at), Some(page));
4284 tree.pointer_move(at);
4285 assert_eq!(tree.hovered(), Some(page), "and so does a hover");
4286 }
4287
4288 #[test]
4289 fn a_root_that_is_hit_transparent_and_pass_through_is_not_inert() {
4290 use crate::widget_builder::WidgetBuilder;
4291
4292 let flagged = || {
4296 Block(60.0, 40.0)
4297 .hit_transparent(true)
4298 .event_pass_through(true)
4299 };
4300 let at = Point::new(220.0, 110.0);
4301
4302 let (mut tree, panel, top) = panel_and_an_overlay_above(
4303 crate::overlay::DismissBehavior::ClickOutside,
4304 flagged(),
4305 crate::overlay::DismissBehavior::Manual,
4306 Point::new(200.0, 100.0),
4307 false,
4308 );
4309 let over = tree.overlay_manager.find_by_content(top).unwrap();
4310 assert!(
4311 tree.overlay_content_bounds(over).unwrap().contains(at),
4312 "fixture: the press lands on the flagged overlay"
4313 );
4314 assert_eq!(
4315 tree.overlay_manager.hit_test(at),
4316 Some(over),
4317 "the stack still answers with the overlay"
4318 );
4319 assert_eq!(
4320 tree.overlay_manager.interactive_rects().len(),
4321 2,
4322 "which is an interactive rect"
4323 );
4324 tree.pointer_down_button(at, crate::event::PointerButton::Primary);
4325 tree.pointer_up_button(at, crate::event::PointerButton::Primary);
4326 assert!(
4327 tree.overlay_manager.find_by_content(panel).is_some(),
4328 "a press on it is inside the stack, so the panel beneath stays"
4329 );
4330
4331 let (mut tree, panel, top) = panel_and_an_overlay_above(
4332 crate::overlay::DismissBehavior::EscapeKey,
4333 flagged(),
4334 crate::overlay::DismissBehavior::Manual,
4335 Point::new(200.0, 100.0),
4336 false,
4337 );
4338 tree.press_key(Key::Escape, Modifiers::NONE);
4339 assert!(
4340 tree.overlay_manager.find_by_content(panel).is_some()
4341 && tree.overlay_manager.find_by_content(top).is_some(),
4342 "a `Manual` overlay owns the Escape: nothing beneath it closes"
4343 );
4344
4345 let (mut tree, panel, top) = panel_and_an_overlay_above(
4346 crate::overlay::DismissBehavior::EscapeKey,
4347 flagged(),
4348 crate::overlay::DismissBehavior::EscapeKey,
4349 Point::new(200.0, 100.0),
4350 false,
4351 );
4352 tree.press_key(Key::Escape, Modifiers::NONE);
4353 assert_eq!(
4354 tree.overlay_manager.find_by_content(top),
4355 None,
4356 "an `EscapeKey` one is what the Escape closes"
4357 );
4358 assert!(
4359 tree.overlay_manager.find_by_content(panel).is_some(),
4360 "and it is spent there"
4361 );
4362 }
4363
4364 #[test]
4365 fn an_inert_overlay_records_no_focus_to_give_back_on_any_show_path() {
4366 type Show = fn(&mut WidgetTree, crate::overlay::OverlayRequest, WidgetId);
4367 fn from_a_handler(
4368 tree: &mut WidgetTree,
4369 show: impl FnOnce(&mut crate::widget::EventContext),
4370 ) {
4371 tree.run_with_event_context(&mut crate::window::NoopWindowOps, show);
4372 }
4373 let paths: [(&str, Show); 5] = [
4374 ("show_overlay", |tree, request, _| {
4375 from_a_handler(tree, |ctx| ctx.show_overlay(request))
4376 }),
4377 ("show_overlay_for", |tree, request, _| {
4378 from_a_handler(tree, |ctx| {
4379 ctx.show_overlay_for(request, std::time::Duration::from_secs(60))
4380 })
4381 }),
4382 ("show_overlay_with_reveal", |tree, request, _| {
4383 from_a_handler(tree, |ctx| {
4384 ctx.show_overlay_with_reveal(
4385 request,
4386 crate::signal::Signal::new(0.0),
4387 std::time::Duration::from_millis(100),
4388 )
4389 })
4390 }),
4391 ("show_overlay_after", |tree, request, _| {
4392 from_a_handler(tree, |ctx| {
4393 ctx.show_overlay_after(request, std::time::Duration::ZERO)
4394 });
4395 tree.layout(SizeProposal::exact(400.0, 200.0));
4396 }),
4397 ("show_overlay_from_source", |tree, request, field| {
4398 tree.show_overlay_from_source(field, request);
4399 }),
4400 ];
4401 for (path, show) in paths {
4402 let mut tree = WidgetTree::new();
4403 let field = tree.add(FillWidget::new().focusable());
4404 let inert = tree.add(Block(60.0, 40.0));
4405 let ordinary = tree.add(Block(60.0, 40.0));
4406 tree.layout(SizeProposal::exact(400.0, 200.0));
4407 tree.focus(field);
4408 for (content, inert) in [(inert, true), (ordinary, false)] {
4409 let request = crate::overlay::OverlayRequest::new(
4410 content,
4411 field,
4412 crate::overlay::OverlayPlacement::AtPointer(Point::new(20.0, 20.0)),
4413 crate::overlay::DismissBehavior::EscapeKey,
4414 );
4415 show(
4416 &mut tree,
4417 if inert { request.inert() } else { request },
4418 field,
4419 );
4420 }
4421 let restore = |content: WidgetId| {
4422 tree.overlay_manager
4423 .stack
4424 .iter()
4425 .find(|o| o.content_id == content)
4426 .map(|o| o.focus_restore)
4427 };
4428 assert_eq!(
4429 restore(ordinary),
4430 Some(Some(field)),
4431 "{path}: an ordinary one does"
4432 );
4433 assert_eq!(
4434 restore(inert),
4435 Some(None),
4436 "{path}: an inert one took no focus"
4437 );
4438 }
4439 }
4440}