1use super::*;
5
6use crate::gesture::{GestureEvent, RawPointerEvent, TapEvent};
7
8fn fire_event_handler_both(
12 external: &mut Option<Box<dyn FnMut(&WidgetEvent, &mut EventContext) -> EventResponse>>,
13 own: &mut Option<Box<dyn FnMut(&WidgetEvent, &mut EventContext) -> EventResponse>>,
14 event: &WidgetEvent,
15 ctx: &mut EventContext,
16) -> EventResponse {
17 let r1 = external
18 .as_mut()
19 .map(|h| h(event, ctx))
20 .unwrap_or(EventResponse::Ignored);
21 let r2 = own
22 .as_mut()
23 .map(|h| h(event, ctx))
24 .unwrap_or(EventResponse::Ignored);
25 if r1 == EventResponse::Handled || r2 == EventResponse::Handled {
26 EventResponse::Handled
27 } else {
28 EventResponse::Ignored
29 }
30}
31
32impl WidgetTree {
33 fn scope_distance_from_focus(&self, focus: WidgetId, scope_id: WidgetId) -> Option<usize> {
37 let mut hops = 0usize;
38 let mut current = Some(focus);
39 while let Some(c) = current {
40 if c == scope_id {
41 return Some(hops);
42 }
43 current = self.arena.parent(c);
44 hops += 1;
45 }
46 None
47 }
48
49 fn select_shortcut_for_focus(
59 &self,
60 candidates: &[(&'static str, crate::shortcut::ShortcutScope, bool)],
61 ) -> Option<(&'static str, crate::shortcut::ShortcutScope, bool)> {
62 use crate::shortcut::ShortcutScope;
63 let mut best: Option<(usize, (&'static str, ShortcutScope, bool))> = None;
64 for &(id, scope, propagate) in candidates {
65 let score = match scope {
70 ShortcutScope::Global => Some(0usize),
71 ShortcutScope::Scoped(scope_id) => self
72 .focused
73 .and_then(|f| self.scope_distance_from_focus(f, scope_id))
74 .map(|hops| usize::MAX - hops),
75 };
76 let Some(score) = score else { continue };
77 if best.as_ref().is_none_or(|(b, _)| score > *b) {
80 best = Some((score, (id, scope, propagate)));
81 }
82 }
83 best.map(|(_, c)| c)
84 }
85
86 pub fn dispatch_event_with_ops(
100 &mut self,
101 event: WidgetEvent,
102 ops: &mut dyn crate::window::WindowOps,
103 ) {
104 self.dispatch_event_impl(event, ops)
105 }
106
107 pub fn dispatch_event(&mut self, event: WidgetEvent) {
113 let mut noop = crate::window::NoopWindowOps;
114 self.dispatch_event_impl(event, &mut noop);
115 }
116
117 fn dispatch_event_impl(&mut self, event: WidgetEvent, ops: &mut dyn crate::window::WindowOps) {
118 match &event {
122 WidgetEvent::KeyDown { .. } if !self.focus_visible.get() => {
123 self.focus_visible.set(true);
124 }
125 WidgetEvent::PointerDown { .. } if self.focus_visible.get() => {
126 self.focus_visible.set(false);
127 }
128 _ => {}
129 }
130
131 let overlay_back_key = match self.layout_direction {
137 crate::environment::LayoutDirection::RightToLeft => Key::ArrowRight,
138 crate::environment::LayoutDirection::LeftToRight => Key::ArrowLeft,
139 };
140 if let WidgetEvent::KeyDown { key, .. } = &event
141 && *key == overlay_back_key
142 {
143 let nested_menu_overlays = {
151 let ids: Vec<_> = self.overlay_manager.stack.iter().map(|o| o.id).collect();
152 ids.into_iter()
153 .filter(|&id| !self.overlay_is_host_surface(id))
154 .count()
155 };
156 let top_id = self.overlay_manager.stack.last().map(|o| o.id);
162 let top_is_back_navigable = top_id.is_some_and(|id| !self.overlay_is_host_surface(id));
163 if nested_menu_overlays > 1 && top_is_back_navigable {
164 if let Some((_id, content_ids, focus_restore)) = self.overlay_manager.dismiss_top()
165 {
166 self.dormant_dismissed_content(&content_ids, &mut *ops);
167 if let Some(restore_id) = focus_restore
168 && self.arena.is_active(restore_id)
169 {
170 self.focus_ops(restore_id, &mut *ops);
171 }
172 }
173 return;
174 }
175 }
176
177 if let WidgetEvent::KeyDown {
183 key: Key::Escape, ..
184 } = &event
185 {
186 self.tooltip_escape_pressed();
187 }
188
189 if let WidgetEvent::KeyDown {
190 key: Key::Escape, ..
191 } = &event
192 && !self.overlay_manager.is_empty()
193 && let Some((_id, content_ids, focus_restore)) =
194 self.overlay_manager.try_dismiss_top_on_escape()
195 {
196 self.dormant_dismissed_content(&content_ids, &mut *ops);
197 if let Some(restore_id) = focus_restore
198 && self.arena.is_active(restore_id)
199 {
200 self.focus_ops(restore_id, &mut *ops);
201 }
202 return;
203 }
204
205 if let WidgetEvent::PointerDown {
206 position, button, ..
207 } = &event
208 {
209 let (dismissed, focus_restore, toggle_anchors) =
210 self.overlay_manager.handle_click_outside(*position);
211 if !dismissed.is_empty() {
212 self.dormant_dismissed_content(&dismissed, &mut *ops);
213 if let Some(restore_id) = focus_restore
214 && self.arena.is_active(restore_id)
215 {
216 self.focus_ops(restore_id, &mut *ops);
217 }
218 let on_toggle_anchor = *button == PointerButton::Primary
228 && toggle_anchors.iter().any(|&anchor| {
229 self.arena.is_active(anchor)
230 && self.arena.bounds(anchor).contains(*position)
231 });
232 if on_toggle_anchor {
233 return;
234 }
235 }
236 }
237
238 if let WidgetEvent::KeyDown { key, modifiers, .. } = &event
249 && let Some(callback) = self.take_key_capture()
250 {
251 let keystroke = crate::shortcut::KeyStroke::new(*key, *modifiers);
252 let mut cap_ctx = self.make_event_context(&mut *ops);
253 callback(keystroke, self.shortcut_registry_mut(), &mut cap_ctx);
254 let anchor = self.focused.or_else(|| self.arena.roots().first().copied());
257 if let Some(anchor_id) = anchor {
258 self.collect_from_ctx(cap_ctx, anchor_id);
259 self.drain_pending_intents(&mut *ops);
260 }
261 return;
262 }
263
264 let focus_captures_keys = matches!(&event, WidgetEvent::KeyDown { .. })
272 && self.focused.is_some_and(|f| self.is_keyboard_capture(f));
273
274 if !focus_captures_keys && let WidgetEvent::KeyDown { key, modifiers, .. } = &event {
289 let keystroke = crate::shortcut::KeyStroke::new(*key, *modifiers);
290 let candidates: Vec<(&'static str, crate::shortcut::ShortcutScope, bool)> = self
299 .shortcut_registry
300 .matches_by_keystroke(keystroke)
301 .map(|eff| {
302 (
303 eff.shortcut.id,
304 eff.shortcut.scope,
305 eff.shortcut.propagate_when_disabled,
306 )
307 })
308 .collect();
309 let lookup = self.select_shortcut_for_focus(&candidates);
310 if let Some((id, scope, propagate_when_disabled)) = lookup {
311 let anchor = match scope {
312 crate::shortcut::ShortcutScope::Global => {
317 self.focused.or_else(|| self.arena.roots().first().copied())
318 }
319 crate::shortcut::ShortcutScope::Scoped(scope_id) => {
320 self.focused.filter(|f| self.is_descendant_of(*f, scope_id))
321 }
322 };
323 if let Some(anchor_id) = anchor {
324 let mut act_ctx = self.make_event_context(&mut *ops);
325 if let Some(intent) =
326 self.shortcut_registry
327 .invoke_on_activate(id, keystroke, &mut act_ctx)
328 {
329 self.collect_from_ctx(act_ctx, anchor_id);
330 let intent = intent.with_source(crate::telemetry::IntentSource::Shortcut);
334 self.enqueue_intent(anchor_id, intent, propagate_when_disabled);
335 self.drain_pending_intents(&mut *ops);
336 return;
337 }
338 }
339 }
344 }
347
348 if self.outbound_drag_source.is_some()
354 && let WidgetEvent::KeyDown {
355 key: Key::Escape, ..
356 } = &event
357 {
358 ops.cancel_os_drag();
359 }
364
365 if self.active_drag.is_some() {
367 match &event {
368 WidgetEvent::PointerMove { position } => {
369 self.handle_drag_move(*position, &mut *ops);
370 return;
371 }
372 WidgetEvent::PointerUp { position, .. } => {
373 self.handle_drag_drop(*position, &mut *ops);
374 return;
375 }
376 WidgetEvent::KeyDown {
377 key: Key::Escape, ..
378 } => {
379 self.cancel_active_drag(&mut *ops);
380 return;
381 }
382 WidgetEvent::Scroll { .. } => {
383 let target_and_pos = self
389 .active_drag
390 .as_ref()
391 .and_then(|d| d.current_target.map(|t| (t, d.current_position)));
392 if let Some((target, _pos)) = target_and_pos {
393 self.dispatch_to_widget(target, &event, &mut *ops);
394 }
395 if let Some((_, pos)) = target_and_pos
396 && self.active_drag.is_some()
397 {
398 self.handle_drag_move(pos, &mut *ops);
399 }
400 return;
401 }
402 _ => {}
403 }
404 }
405
406 if let WidgetEvent::KeyDown { key, modifiers, .. } = &event
415 && is_context_menu_chord(*key, *modifiers)
416 && self.open_context_menu_from_keyboard(&mut *ops)
417 {
418 return;
419 }
420
421 match &event {
422 WidgetEvent::PointerMove { position } => {
423 if let Some(captured) = self.pointer_captured_by {
424 self.dispatch_to_widget(
425 captured,
426 &WidgetEvent::PointerMove {
427 position: *position,
428 },
429 &mut *ops,
430 );
431 if self.active_drag.is_none() {
436 self.advance_drag_observers(
437 &WidgetEvent::PointerMove {
438 position: *position,
439 },
440 &mut *ops,
441 );
442 }
443 } else {
444 self.handle_pointer_move(*position, &mut *ops);
445 }
446 self.update_pointer_leave_overlays(*position, &mut *ops);
447 }
448 WidgetEvent::PointerDown {
449 position, button, ..
450 } => {
451 self.tooltip_pointer_press(Some(*position));
458 if let Some(target) = self.hit_test(*position) {
459 if *button == PointerButton::Secondary
460 && self.show_context_menu_for(target, *position, &mut *ops)
461 {
462 return;
463 }
464 if let Some(focusable) = self.find_focusable_at_or_above(target) {
465 self.focus_with_origin_ops(
466 focusable,
467 crate::focus::FocusOrigin::Pointer,
468 &mut *ops,
469 );
470 }
471 self.dispatch_to_widget(target, &event, &mut *ops);
472 if self.active_drag.is_none()
477 && let Some(captured) = self.pointer_captured_by
478 {
479 self.arm_drag_observers(captured, &event, &mut *ops);
480 }
481 }
482 }
483 WidgetEvent::PointerUp { position, .. } => {
484 self.release_drag_observers(&event, &mut *ops);
491 if let Some(captured) = self.pointer_captured_by {
492 self.dispatch_to_widget(captured, &event, &mut *ops);
493 self.pointer_captured_by = None;
494 } else if let Some(target) = self.hit_test(*position) {
495 self.dispatch_to_widget(target, &event, &mut *ops);
496 }
497 }
498 WidgetEvent::Scroll { .. } => {
499 if let Some(target) = self.hovered.or(self.focused) {
500 self.dispatch_to_widget(target, &event, &mut *ops);
501 }
502 }
503 WidgetEvent::KeyDown { key, modifiers, .. } => {
504 if *key == Key::Tab {
505 let captured_focus = self
520 .focused
521 .is_some_and(|focused| self.is_keyboard_capture(focused));
522 if captured_focus && modifiers.ctrl() {
523 self.cycle_focus(modifiers.shift(), &mut *ops);
524 return;
525 }
526 let handled = self
533 .focused
534 .map(|focused| {
535 self.dispatch_to_widget_returning_handled(focused, &event, &mut *ops)
536 })
537 .unwrap_or(false);
538 if !handled {
539 self.cycle_focus(modifiers.shift(), &mut *ops);
540 }
541 } else if let Some(focused) = self.focused {
542 self.dispatch_to_widget(focused, &event, &mut *ops);
543 }
544 }
545 WidgetEvent::KeyUp { .. }
546 | WidgetEvent::ImeComposition { .. }
547 | WidgetEvent::ImeCommit { .. } => {
548 if let Some(focused) = self.focused {
549 self.dispatch_to_widget(focused, &event, &mut *ops);
550 }
551 }
552 WidgetEvent::AccessAction { target, action, .. } => {
553 if let Some(id) = target.filter(|id| self.arena.is_active(*id)) {
563 if *action == accesskit::Action::Focus {
564 if self.advertises_focus_action(id) {
588 let target = self.first_focusable_descendant(id).unwrap_or(id);
589 self.focus_with_origin_ops(
590 target,
591 crate::focus::FocusOrigin::Programmatic,
592 &mut *ops,
593 );
594 self.access_action_handled = self.focused == Some(target);
597 } else {
598 self.access_action_handled = false;
599 }
600 } else if *action == accesskit::Action::ShowContextMenu {
601 self.access_action_handled =
617 if self.dispatch_to_widget_returning_handled(id, &event, &mut *ops) {
618 true
619 } else {
620 let position = self.arena.bounds(id).center();
621 self.show_context_menu_for(id, position, &mut *ops)
622 };
623 } else {
624 self.access_action_handled =
625 self.dispatch_to_widget_returning_handled(id, &event, &mut *ops);
626 }
627 }
628 }
629 WidgetEvent::Gesture { .. } => {
630 if let Some(target) = self.hovered.or(self.focused) {
631 self.dispatch_to_widget(target, &event, &mut *ops);
632 }
633 }
634 WidgetEvent::ScrollIntoView { .. }
635 | WidgetEvent::PointerEnter
636 | WidgetEvent::PointerLeave
637 | WidgetEvent::FocusGained { .. }
638 | WidgetEvent::FocusLost => {}
639 }
640 self.drain_pending_intents(&mut *ops);
645 }
646
647 fn open_context_menu_from_keyboard(&mut self, ops: &mut dyn crate::window::WindowOps) -> bool {
660 let Some(focused) = self.focused else {
661 return false;
662 };
663 let target = self
664 .arena
665 .get(focused)
666 .and_then(|node| node.widget.context_menu_key_target())
667 .filter(|id| self.arena.is_active(*id))
668 .unwrap_or(focused);
669
670 let bounds = self.bounds(target);
674 let anchor = Point {
675 x: bounds.x + bounds.width / 2.0,
676 y: bounds.y + bounds.height / 2.0,
677 };
678 self.show_context_menu_for(target, anchor, ops)
679 }
680
681 fn show_context_menu_for(
682 &mut self,
683 target: WidgetId,
684 position: Point,
685 ops: &mut dyn crate::window::WindowOps,
686 ) -> bool {
687 let mut ctx = self.make_event_context(&mut *ops);
693 let mut walker = Some(target);
694 let menu_decision: Option<(WidgetId, Box<dyn Widget>)> = loop {
695 let owner_id = {
698 let mut probe = walker;
699 loop {
700 match probe {
701 None => break None,
702 Some(id) => {
703 if self
704 .arena
705 .get(id)
706 .is_some_and(|node| node.context_menu_factory.is_some())
707 {
708 break Some(id);
709 }
710 probe = self.arena.get(id).and_then(|node| node.parent);
711 }
712 }
713 }
714 };
715 let Some(owner_id) = owner_id else {
716 break None;
717 };
718 let outcome: Option<Box<dyn Widget>> = {
725 let node = self
726 .arena
727 .get(owner_id)
728 .expect("owner_id from active arena walk");
729 let factory = node
730 .context_menu_factory
731 .as_ref()
732 .expect("owner_id only set when factory present");
733 factory(position, &mut ctx)
734 };
735 match outcome {
736 Some(menu) => break Some((owner_id, menu)),
737 None => {
738 walker = self.arena.get(owner_id).and_then(|n| n.parent);
740 }
741 }
742 };
743
744 let drain_anchor = menu_decision
748 .as_ref()
749 .map(|(id, _)| *id)
750 .or_else(|| self.arena.roots().first().copied())
751 .unwrap_or(target);
752 self.collect_from_ctx(ctx, drain_anchor);
753
754 let Some((owner_id, menu_widget)) = menu_decision else {
755 return false;
756 };
757
758 let keep: std::collections::HashSet<WidgetId> = self
764 .overlay_manager
765 .stack
766 .iter()
767 .map(|o| o.content_id)
768 .filter(|&content_id| self.is_descendant_of(owner_id, content_id))
769 .collect();
770 let dismissed = self.overlay_manager.dismiss_except(&keep);
771 self.dormant_dismissed_content(&dismissed, &mut *ops);
772
773 let content_id = self.add_boxed(menu_widget);
774 let prev_focus = self.focused;
775 self.overlay_manager.show(crate::overlay::OverlayRequest {
776 content_id,
777 anchor: owner_id,
778 placement: crate::overlay::OverlayPlacement::AtPointer(position),
779 dismiss: crate::overlay::DismissBehavior::EscapeOrClickOutside,
780 layer: crate::overlay::OverlayLayer::InTree,
781 parent_overlay: None,
782 on_dismiss: None,
783 fade_duration: None,
784 });
785 if let Some(focus_id) = prev_focus {
786 self.overlay_manager.set_top_focus_restore(focus_id);
787 }
788 self.focus_ops(content_id, &mut *ops);
789 self.drain_pending_intents(&mut *ops);
794 true
795 }
796
797 fn handle_pointer_move(&mut self, position: Point, ops: &mut dyn crate::window::WindowOps) {
798 self.previous_pointer_position = self.last_pointer_position;
799 self.last_pointer_position = Some(position);
800 let target = self.hit_test(position);
801
802 if target != self.hovered {
803 let previously_hovered = self.hovered;
804 if let Some(old) = self.hovered {
805 self.dispatch_to_widget(old, &WidgetEvent::PointerLeave, &mut *ops);
806 self.tooltip_pointer_leave(old, &mut *ops);
807 }
808 if let Some(new) = target {
809 self.dispatch_to_widget(new, &WidgetEvent::PointerEnter, &mut *ops);
810 self.tooltip_pointer_enter(new);
811 }
812 self.set_hovered(target);
813 self.update_hover_within_signals(previously_hovered, target);
814 } else if let Some(target) = target {
815 self.tooltip_pointer_moved(target, position);
818 }
819
820 if let Some(target) = target {
821 self.dispatch_to_widget(target, &WidgetEvent::PointerMove { position }, &mut *ops);
822 }
823 }
824
825 pub(super) fn dispatch_to_widget(
826 &mut self,
827 target: WidgetId,
828 event: &WidgetEvent,
829 ops: &mut dyn crate::window::WindowOps,
830 ) {
831 self.dispatch_to_widget_returning_handled(target, event, ops);
832 }
833
834 fn widget_has_drag(&self, id: WidgetId) -> bool {
837 self.arena
838 .get(id)
839 .map(|n| n.any_handler(|h| h.on_drag.is_some() || h.on_swipe.is_some()))
840 .unwrap_or(false)
841 }
842
843 fn is_gesture_dead_zone(&self, id: WidgetId) -> bool {
847 self.arena
848 .get(id)
849 .map(|n| n.gesture_dead_zone)
850 .unwrap_or(false)
851 }
852
853 fn is_keyboard_capture(&self, id: WidgetId) -> bool {
857 self.arena
858 .get(id)
859 .map(|n| n.keyboard_capture)
860 .unwrap_or(false)
861 }
862
863 pub(super) fn arm_drag_observers(
874 &mut self,
875 captured: WidgetId,
876 down_event: &WidgetEvent,
877 ops: &mut dyn crate::window::WindowOps,
878 ) {
879 self.drag_observers.clear();
880 if self.widget_has_drag(captured) {
881 return;
882 }
883 if self.is_gesture_dead_zone(captured) {
886 return;
887 }
888 let mut observers = Vec::new();
889 let mut current = self.arena.parent(captured);
890 while let Some(id) = current {
891 if self.is_gesture_dead_zone(id) {
896 break;
897 }
898 if self.widget_has_drag(id) {
899 {
902 let WidgetTree {
903 arena,
904 gesture_owners,
905 ..
906 } = self;
907 if let Some(node) = arena.get_mut(id) {
908 Self::ensure_gesture_arena(node, id, gesture_owners);
909 }
910 }
911 self.observe_drag_on_ancestor(id, down_event, ops);
912 observers.push(id);
913 }
914 current = self.arena.parent(id);
915 }
916 self.drag_observers = observers;
917 }
918
919 pub(super) fn release_drag_observers(
929 &mut self,
930 up_event: &WidgetEvent,
931 ops: &mut dyn crate::window::WindowOps,
932 ) {
933 if self.drag_observers.is_empty() {
934 return;
935 }
936 let observers = std::mem::take(&mut self.drag_observers);
937 for id in &observers {
938 self.observe_drag_on_ancestor(*id, up_event, ops);
942 }
943 }
944
945 pub(super) fn advance_drag_observers(
949 &mut self,
950 move_event: &WidgetEvent,
951 ops: &mut dyn crate::window::WindowOps,
952 ) {
953 if self.drag_observers.is_empty() {
954 return;
955 }
956 let observers = std::mem::take(&mut self.drag_observers);
957 for id in &observers {
958 let recognized = self.observe_drag_on_ancestor(*id, move_event, ops);
959 if recognized || self.active_drag.is_some() {
960 return;
962 }
963 }
964 self.drag_observers = observers;
966 }
967
968 fn observe_drag_on_ancestor(
974 &mut self,
975 id: WidgetId,
976 event: &WidgetEvent,
977 ops: &mut dyn crate::window::WindowOps,
978 ) -> bool {
979 let localized = self.localize_event(id, event);
980 let event = localized.as_ref().unwrap_or(event);
981 let raw = match event {
982 WidgetEvent::PointerDown {
983 position,
984 button,
985 modifiers,
986 } => crate::gesture::RawPointerEvent::Down {
987 position: *position,
988 button: *button,
989 modifiers: *modifiers,
990 },
991 WidgetEvent::PointerMove { position } => crate::gesture::RawPointerEvent::Move {
992 position: *position,
993 },
994 WidgetEvent::PointerUp {
995 position,
996 button,
997 modifiers,
998 } => crate::gesture::RawPointerEvent::Up {
999 position: *position,
1000 button: *button,
1001 modifiers: *modifiers,
1002 },
1003 _ => return false,
1004 };
1005 let mut ctx = self.make_event_context(&mut *ops);
1006 let WidgetTree { arena, .. } = self;
1007 let recognized = if let Some(node) = arena.get_mut(id) {
1008 if let Some(arena_ref) = node.handlers.gesture_arena.as_mut() {
1009 if let Some(gesture) = arena_ref.process(&raw) {
1010 Self::dispatch_recognized_gesture(node, gesture, &mut ctx);
1011 true
1012 } else {
1013 false
1014 }
1015 } else {
1016 false
1017 }
1018 } else {
1019 false
1020 };
1021 self.collect_from_ctx(ctx, id);
1022 recognized
1023 }
1024
1025 fn localize_event(&self, id: WidgetId, event: &WidgetEvent) -> Option<WidgetEvent> {
1037 match event {
1038 WidgetEvent::PointerDown {
1039 position,
1040 button,
1041 modifiers,
1042 } => Some(WidgetEvent::PointerDown {
1043 position: self.arena.local_pointer_position(id, *position),
1044 button: *button,
1045 modifiers: *modifiers,
1046 }),
1047 WidgetEvent::PointerUp {
1048 position,
1049 button,
1050 modifiers,
1051 } => Some(WidgetEvent::PointerUp {
1052 position: self.arena.local_pointer_position(id, *position),
1053 button: *button,
1054 modifiers: *modifiers,
1055 }),
1056 WidgetEvent::PointerMove { position } => Some(WidgetEvent::PointerMove {
1057 position: self.arena.local_pointer_position(id, *position),
1058 }),
1059 WidgetEvent::Gesture { gesture } => Some(WidgetEvent::Gesture {
1060 gesture: self.localize_gesture(id, gesture),
1061 }),
1062 _ => None,
1063 }
1064 }
1065
1066 fn localize_gesture(&self, id: WidgetId, gesture: &GestureEvent) -> GestureEvent {
1073 let loc = |p: teksilo_canvas::Point| self.arena.local_pointer_position(id, p);
1074 let tap = |t: &TapEvent| TapEvent::new(loc(t.position), t.button, t.modifiers);
1075 match gesture {
1076 GestureEvent::Tap(t) => GestureEvent::Tap(tap(t)),
1077 GestureEvent::DoubleTap(t) => GestureEvent::DoubleTap(tap(t)),
1078 GestureEvent::TripleTap(t) => GestureEvent::TripleTap(tap(t)),
1079 GestureEvent::LongPress(t) => GestureEvent::LongPress(tap(t)),
1080 GestureEvent::DragStarted { position, button } => GestureEvent::DragStarted {
1081 position: loc(*position),
1082 button: *button,
1083 },
1084 GestureEvent::DragMoved { position, delta } => GestureEvent::DragMoved {
1085 position: loc(*position),
1086 delta: *delta,
1087 },
1088 GestureEvent::DragEnded { position } => GestureEvent::DragEnded {
1089 position: loc(*position),
1090 },
1091 GestureEvent::PinchStarted { center } => GestureEvent::PinchStarted {
1092 center: loc(*center),
1093 },
1094 GestureEvent::PinchChanged {
1095 center,
1096 scale,
1097 rotation,
1098 } => GestureEvent::PinchChanged {
1099 center: loc(*center),
1100 scale: *scale,
1101 rotation: *rotation,
1102 },
1103 GestureEvent::PinchEnded => GestureEvent::PinchEnded,
1104 GestureEvent::Swipe {
1105 direction,
1106 velocity,
1107 } => GestureEvent::Swipe {
1108 direction: *direction,
1109 velocity: *velocity,
1110 },
1111 }
1112 }
1113
1114 pub(super) fn dispatch_to_widget_returning_handled(
1120 &mut self,
1121 target: WidgetId,
1122 event: &WidgetEvent,
1123 ops: &mut dyn crate::window::WindowOps,
1124 ) -> bool {
1125 if !self.arena.is_enabled(target) {
1126 return false;
1127 }
1128
1129 let mut ancestors = Vec::new();
1130 let mut current = self.arena.parent(target);
1131 while let Some(id) = current {
1132 ancestors.push(id);
1133 current = self.arena.parent(id);
1134 }
1135 ancestors.reverse();
1136
1137 let tap_owner: Option<WidgetId> = if matches!(
1155 event,
1156 WidgetEvent::PointerDown { .. } | WidgetEvent::PointerUp { .. }
1157 ) {
1158 let mut owner = None;
1159 let mut cur = Some(target);
1160 while let Some(id) = cur {
1161 if self.arena.get(id).is_some_and(|n| {
1162 n.any_handler(|h| h.on_tap.is_some() || h.on_long_press.is_some())
1163 }) {
1164 owner = Some(id);
1165 break;
1166 }
1167 cur = self.arena.parent(id);
1168 }
1169 owner
1170 } else {
1171 None
1172 };
1173
1174 for &id in &ancestors {
1175 let mut ctx = self.make_event_context(&mut *ops);
1176 ctx.press_claimed_by_interactive_child =
1177 tap_owner.is_some_and(|owner| owner != id && self.is_descendant_of(owner, id));
1178 let localized = self.localize_event(id, event);
1181 let event = localized.as_ref().unwrap_or(event);
1182 let response = if let Some(node) = self.arena.get_mut(id) {
1183 Self::try_handler_preview(node, event, &mut ctx).unwrap_or(EventResponse::Ignored)
1184 } else {
1185 EventResponse::Ignored
1186 };
1187 self.collect_from_ctx(ctx, id);
1188 if response == EventResponse::Handled {
1189 self.arena.mark_needs_paint(id);
1190 return true;
1191 }
1192 }
1193
1194 let needs_layout_on_handle = matches!(
1195 event,
1196 WidgetEvent::Scroll { .. } | WidgetEvent::ScrollIntoView { .. }
1197 );
1198 let mut current = Some(target);
1199 let mut is_target = true;
1200 while let Some(id) = current {
1201 let mut ctx = self.make_event_context(&mut *ops);
1202 ctx.press_claimed_by_interactive_child =
1203 tap_owner.is_some_and(|owner| owner != id && self.is_descendant_of(owner, id));
1204 let localized = self.localize_event(id, event);
1207 let WidgetTree {
1208 arena,
1209 gesture_owners,
1210 ..
1211 } = self;
1212 let event = localized.as_ref().unwrap_or(event);
1213 let response = if let Some(node) = arena.get_mut(id) {
1214 Self::try_handler_bubble(node, event, &mut ctx, is_target, id, gesture_owners)
1215 .unwrap_or(EventResponse::Ignored)
1216 } else {
1217 EventResponse::Ignored
1218 };
1219 self.collect_from_ctx(ctx, id);
1220 if response == EventResponse::Handled {
1221 if needs_layout_on_handle {
1222 self.arena.mark_needs_layout(id);
1223 } else {
1224 self.arena.mark_needs_paint(id);
1225 }
1226 if !matches!(event, WidgetEvent::PointerEnter | WidgetEvent::PointerLeave) {
1241 return true;
1242 }
1243 }
1244 is_target = false;
1245 current = self.arena.parent(id);
1246 }
1247 false
1248 }
1249
1250 pub(super) fn dispatch_to_widget_direct(
1251 &mut self,
1252 target: WidgetId,
1253 event: &WidgetEvent,
1254 ops: &mut dyn crate::window::WindowOps,
1255 ) {
1256 if !self.arena.is_enabled(target) {
1257 return;
1258 }
1259
1260 let mut ctx = self.make_event_context(&mut *ops);
1261 let WidgetTree {
1262 arena,
1263 gesture_owners,
1264 ..
1265 } = self;
1266 let response = if let Some(node) = arena.get_mut(target) {
1267 Self::try_handler_bubble(node, event, &mut ctx, true, target, gesture_owners)
1268 .unwrap_or(EventResponse::Ignored)
1269 } else {
1270 EventResponse::Ignored
1271 };
1272 self.collect_from_ctx(ctx, target);
1273
1274 if response == EventResponse::Handled {
1275 self.arena.mark_needs_paint(target);
1276 }
1277 }
1278
1279 fn try_handler_preview(
1280 node: &mut crate::arena::WidgetNode,
1281 event: &WidgetEvent,
1282 ctx: &mut EventContext,
1283 ) -> Option<EventResponse> {
1284 match event {
1285 WidgetEvent::KeyDown { .. }
1293 | WidgetEvent::KeyUp { .. }
1294 | WidgetEvent::ImeComposition { .. }
1295 | WidgetEvent::ImeCommit { .. } => {
1296 let has = node.external_handlers.on_key_preview.is_some()
1297 || node.handlers.on_key_preview.is_some();
1298 if !has {
1299 return None;
1300 }
1301 Some(fire_event_handler_both(
1302 &mut node.external_handlers.on_key_preview,
1303 &mut node.handlers.on_key_preview,
1304 event,
1305 ctx,
1306 ))
1307 }
1308 WidgetEvent::PointerEnter | WidgetEvent::PointerLeave => None,
1320 _ => {
1321 let has = node.external_handlers.on_pointer_event.is_some()
1322 || node.handlers.on_pointer_event.is_some();
1323 if !has {
1324 return None;
1325 }
1326 Some(fire_event_handler_both(
1327 &mut node.external_handlers.on_pointer_event,
1328 &mut node.handlers.on_pointer_event,
1329 event,
1330 ctx,
1331 ))
1332 }
1333 }
1334 }
1335
1336 fn try_handler_bubble(
1345 node: &mut crate::arena::WidgetNode,
1346 event: &WidgetEvent,
1347 ctx: &mut EventContext,
1348 fire_on_pointer_event: bool,
1349 node_id: WidgetId,
1350 gesture_owners: &mut std::collections::HashSet<WidgetId>,
1351 ) -> Option<EventResponse> {
1352 match event {
1353 WidgetEvent::PointerEnter => {
1354 if let Some(cursor) = node.node_cursor {
1355 ctx.set_cursor(cursor);
1356 }
1357 let mut fired = false;
1358 if let Some(h) = node.external_handlers.on_hover.as_mut() {
1359 h(true, ctx);
1360 fired = true;
1361 }
1362 if let Some(h) = node.handlers.on_hover.as_mut() {
1363 h(true, ctx);
1364 fired = true;
1365 }
1366 if fired {
1367 Some(EventResponse::Handled)
1368 } else {
1369 node.node_cursor.map(|_| EventResponse::Handled)
1370 }
1371 }
1372 WidgetEvent::PointerLeave => {
1373 if node.node_cursor.is_some() {
1374 ctx.set_cursor(crate::widget::CursorIcon::Default);
1375 }
1376 let mut fired = false;
1377 if let Some(h) = node.external_handlers.on_hover.as_mut() {
1378 h(false, ctx);
1379 fired = true;
1380 }
1381 if let Some(h) = node.handlers.on_hover.as_mut() {
1382 h(false, ctx);
1383 fired = true;
1384 }
1385 if fired {
1386 Some(EventResponse::Handled)
1387 } else {
1388 node.node_cursor.map(|_| EventResponse::Handled)
1389 }
1390 }
1391 WidgetEvent::FocusGained { .. } => {
1392 let mut fired = false;
1393 if let Some(h) = node.external_handlers.on_focus.as_mut() {
1394 h(true, ctx);
1395 fired = true;
1396 }
1397 if let Some(h) = node.handlers.on_focus.as_mut() {
1398 h(true, ctx);
1399 fired = true;
1400 }
1401 fired.then_some(EventResponse::Handled)
1402 }
1403 WidgetEvent::FocusLost => {
1404 let mut fired = false;
1405 if let Some(h) = node.external_handlers.on_focus.as_mut() {
1406 h(false, ctx);
1407 fired = true;
1408 }
1409 if let Some(h) = node.handlers.on_focus.as_mut() {
1410 h(false, ctx);
1411 fired = true;
1412 }
1413 fired.then_some(EventResponse::Handled)
1414 }
1415 WidgetEvent::KeyDown { .. }
1416 | WidgetEvent::KeyUp { .. }
1417 | WidgetEvent::ImeComposition { .. }
1418 | WidgetEvent::ImeCommit { .. } => {
1419 if node.external_handlers.on_key.is_some() || node.handlers.on_key.is_some() {
1420 Some(fire_event_handler_both(
1421 &mut node.external_handlers.on_key,
1422 &mut node.handlers.on_key,
1423 event,
1424 ctx,
1425 ))
1426 } else {
1427 None
1428 }
1429 }
1430 WidgetEvent::Scroll { .. } | WidgetEvent::ScrollIntoView { .. } => {
1431 if node.external_handlers.on_scroll.is_some() || node.handlers.on_scroll.is_some() {
1432 Some(fire_event_handler_both(
1433 &mut node.external_handlers.on_scroll,
1434 &mut node.handlers.on_scroll,
1435 event,
1436 ctx,
1437 ))
1438 } else {
1439 None
1440 }
1441 }
1442 WidgetEvent::AccessAction {
1443 action,
1444 target_node,
1445 data,
1446 ..
1447 } => {
1448 let saved_a11y_source = ctx
1468 .current_source
1469 .replace(crate::telemetry::IntentSource::Accessibility);
1470 let mut any_slot = false;
1471 let mut any_handled = false;
1472 if let Some(h) = node.external_handlers.on_access_action_request.as_mut() {
1473 any_slot = true;
1474 any_handled |=
1475 h(*action, *target_node, data.clone(), ctx) == EventResponse::Handled;
1476 }
1477 if let Some(h) = node.handlers.on_access_action_request.as_mut() {
1478 any_slot = true;
1479 any_handled |=
1480 h(*action, *target_node, data.clone(), ctx) == EventResponse::Handled;
1481 }
1482 if let Some(h) = node.external_handlers.on_access_action.as_mut() {
1483 any_slot = true;
1484 any_handled |= h(*action, ctx) == EventResponse::Handled;
1485 }
1486 if let Some(h) = node.handlers.on_access_action.as_mut() {
1487 any_slot = true;
1488 any_handled |= h(*action, ctx) == EventResponse::Handled;
1489 }
1490 let user_handled = any_slot.then_some(if any_handled {
1491 EventResponse::Handled
1492 } else {
1493 EventResponse::Ignored
1494 });
1495
1496 let mut override_handled = false;
1502 if let Some(ov) = node.access_overrides.as_deref_mut() {
1503 if matches!(action, accesskit::Action::CustomAction) {
1504 if let Some(accesskit::ActionData::CustomAction(idx)) = data
1505 && let Some((_, cb)) = ov.custom_actions.get_mut(*idx as usize)
1506 {
1507 cb(ctx);
1508 override_handled = true;
1509 }
1510 } else {
1511 for (a, cb) in ov.actions.iter_mut() {
1512 if *a == *action {
1513 cb(ctx);
1514 override_handled = true;
1515 }
1516 }
1517 }
1518 }
1519
1520 ctx.current_source = saved_a11y_source;
1521 match (user_handled, override_handled) {
1522 (Some(EventResponse::Handled), _) | (_, true) => Some(EventResponse::Handled),
1523 (Some(EventResponse::Ignored), false) => Some(EventResponse::Ignored),
1524 (None, false) => None,
1525 }
1526 }
1527 WidgetEvent::Gesture { gesture } => {
1528 let matched = matches!(
1532 gesture,
1533 GestureEvent::PinchStarted { .. }
1534 | GestureEvent::PinchChanged { .. }
1535 | GestureEvent::PinchEnded
1536 | GestureEvent::Swipe { .. }
1537 | GestureEvent::DoubleTap { .. }
1538 | GestureEvent::TripleTap { .. }
1539 ) && {
1540 let has_handler = match gesture {
1541 GestureEvent::PinchStarted { .. }
1542 | GestureEvent::PinchChanged { .. }
1543 | GestureEvent::PinchEnded => node.any_handler(|h| h.on_pinch.is_some()),
1544 GestureEvent::Swipe { .. } => node.any_handler(|h| h.on_swipe.is_some()),
1545 GestureEvent::DoubleTap { .. } => {
1546 node.any_handler(|h| h.on_double_tap.is_some())
1547 }
1548 GestureEvent::TripleTap { .. } => {
1549 node.any_handler(|h| h.on_triple_tap.is_some())
1550 }
1551 _ => false,
1552 };
1553 if has_handler {
1554 Self::dispatch_recognized_gesture(node, *gesture, ctx);
1555 }
1556 has_handler
1557 };
1558 if matched {
1559 Some(EventResponse::Handled)
1560 } else {
1561 None
1562 }
1563 }
1564 WidgetEvent::PointerDown {
1565 position,
1566 button,
1567 modifiers,
1568 } => {
1569 if fire_on_pointer_event {
1576 let r = fire_event_handler_both(
1577 &mut node.external_handlers.on_pointer_event,
1578 &mut node.handlers.on_pointer_event,
1579 event,
1580 ctx,
1581 );
1582 if r == EventResponse::Handled {
1583 return Some(EventResponse::Handled);
1584 }
1585 }
1586 Self::ensure_gesture_arena(node, node_id, gesture_owners);
1587 if let Some(arena) = node.handlers.gesture_arena.as_mut() {
1588 ctx.capture_pointer();
1597 let result = arena.process(&RawPointerEvent::Down {
1598 position: *position,
1599 button: *button,
1600 modifiers: *modifiers,
1601 });
1602 if let Some(gesture) = result {
1603 Self::dispatch_recognized_gesture(node, gesture, ctx);
1604 }
1605 return Some(EventResponse::Handled);
1606 }
1607 None
1608 }
1609 WidgetEvent::PointerUp {
1610 position,
1611 button,
1612 modifiers,
1613 } => {
1614 if fire_on_pointer_event {
1615 let r = fire_event_handler_both(
1616 &mut node.external_handlers.on_pointer_event,
1617 &mut node.handlers.on_pointer_event,
1618 event,
1619 ctx,
1620 );
1621 if r == EventResponse::Handled {
1622 return Some(EventResponse::Handled);
1623 }
1624 }
1625 if let Some(arena) = node.handlers.gesture_arena.as_mut() {
1626 let result = arena.process(&RawPointerEvent::Up {
1627 position: *position,
1628 button: *button,
1629 modifiers: *modifiers,
1630 });
1631 if let Some(gesture) = result {
1632 Self::dispatch_recognized_gesture(node, gesture, ctx);
1633 }
1634 return Some(EventResponse::Handled);
1635 }
1636 None
1637 }
1638 WidgetEvent::PointerMove { position } => {
1639 if fire_on_pointer_event {
1640 let r = fire_event_handler_both(
1641 &mut node.external_handlers.on_pointer_event,
1642 &mut node.handlers.on_pointer_event,
1643 event,
1644 ctx,
1645 );
1646 if r == EventResponse::Handled {
1647 return Some(EventResponse::Handled);
1648 }
1649 }
1650 if let Some(arena) = node.handlers.gesture_arena.as_mut() {
1651 let result = arena.process(&RawPointerEvent::Move {
1652 position: *position,
1653 });
1654 if let Some(gesture) = result {
1655 Self::dispatch_recognized_gesture(node, gesture, ctx);
1656 return Some(EventResponse::Handled);
1668 }
1669 return Some(EventResponse::Ignored);
1670 }
1671 None
1672 }
1673 }
1674 }
1675
1676 pub(super) fn collect_from_ctx<'ops>(
1677 &mut self,
1678 mut ctx: EventContext<'ops>,
1679 source_widget: WidgetId,
1680 ) {
1681 let local_ops = ctx.window_ops.take();
1687 let mut noop = crate::window::NoopWindowOps;
1688 let ops: &mut dyn crate::window::WindowOps = match local_ops {
1689 Some(o) => o,
1690 None => &mut noop,
1691 };
1692 if ctx.frame_requested {
1693 self.request_frame();
1694 }
1695 if let Some(cursor) = ctx.cursor_request {
1696 self.current_cursor = cursor;
1697 }
1698 for intent in ctx.pending_intents {
1704 self.enqueue_intent(source_widget, intent, true);
1705 }
1706 if ctx.cancel_key_capture {
1715 self.cancel_key_capture();
1716 }
1717 if let Some(slot) = ctx.pending_key_capture {
1718 self.key_capture = Some(slot);
1719 }
1720 for mutation in ctx.pending_shortcut_mutations {
1722 match mutation {
1723 crate::widget::ShortcutMutation::RebindPrimary { id, keystroke } => {
1724 self.shortcut_registry.rebind_primary(id, keystroke);
1725 }
1726 crate::widget::ShortcutMutation::RebindSecondary { id, keystroke } => {
1727 self.shortcut_registry.rebind_secondary(id, keystroke);
1728 }
1729 crate::widget::ShortcutMutation::ClearOverride { id } => {
1730 self.shortcut_registry.clear_override(&id);
1731 }
1732 }
1733 }
1734 if ctx.close_window_requested {
1735 self.close_window_requested = true;
1736 }
1737 if ctx.force_close_requested {
1738 self.force_close_requested = true;
1739 }
1740 self.pending_modal_requests
1741 .extend(ctx.modal_requests.into_iter().map(|request| {
1742 crate::modal::QueuedModalRequest {
1743 source_widget,
1744 request,
1745 }
1746 }));
1747 if ctx.dismiss_modal && !self.dismiss_modal_for_source(source_widget, &mut *ops) {
1748 self.pending_modal_dismissal = true;
1749 }
1750 for callback in ctx.idle_callbacks {
1751 self.idle_queue.push_boxed(callback);
1752 }
1753 match ctx.dismiss_scope {
1754 Some(crate::widget::DismissScope::All) => {
1755 let dismissed = self.overlay_manager.dismiss_all();
1756 self.dormant_dismissed_content(&dismissed, &mut *ops);
1757 }
1758 Some(crate::widget::DismissScope::AllExceptHosts) => {
1759 self.dismiss_all_overlays_except_hosts(&mut *ops);
1760 }
1761 Some(crate::widget::DismissScope::SelfChain) => {
1762 self.dismiss_self_overlay_chain_for_source(source_widget, &mut *ops);
1763 }
1764 Some(crate::widget::DismissScope::Top) => {
1765 if let Some((_id, content_ids, focus_restore)) = self.overlay_manager.dismiss_top()
1766 {
1767 self.dormant_dismissed_content(&content_ids, &mut *ops);
1768 if let Some(restore_id) = focus_restore
1769 && self.arena.is_active(restore_id)
1770 {
1771 self.focus_ops(restore_id, &mut *ops);
1772 }
1773 }
1774 }
1775 None => {
1776 for id in ctx.overlay_dismissals {
1777 let dismissed = self.overlay_manager.dismiss(id);
1778 self.dormant_dismissed_content(&dismissed, &mut *ops);
1779 }
1780 }
1781 }
1782 for content_id in ctx.overlay_content_dismissals {
1789 if let Some(overlay_id) = self.overlay_manager.find_by_content(content_id) {
1790 let dismissed = self.overlay_manager.dismiss(overlay_id);
1791 self.dormant_dismissed_content(&dismissed, &mut *ops);
1792 }
1793 }
1794 for (id, pause) in ctx.overlay_pause_requests {
1801 if pause {
1802 self.overlay_manager.pause_auto_dismiss(id);
1803 } else {
1804 self.overlay_manager.resume_auto_dismiss(id);
1805 }
1806 }
1807 for preserve_content in ctx.dismiss_descendant_overlays {
1808 self.dismiss_child_overlays_for_source(source_widget, preserve_content, &mut *ops);
1809 }
1810 self.apply_tree_mutations(std::mem::take(&mut ctx.tree_mutations));
1811 if ctx.request_a11y_update {
1812 self.a11y_dirty = true;
1813 }
1814 for (message, politeness) in std::mem::take(&mut ctx.announcements) {
1817 self.announce_with(message, politeness);
1818 }
1819 for mut req in ctx.overlay_requests {
1820 if req.parent_overlay.is_none() {
1821 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
1822 }
1823 if self
1824 .overlay_manager
1825 .find_by_content(req.content_id)
1826 .is_some()
1827 {
1828 continue;
1829 }
1830 let current_focus = self.focused;
1831 self.overlay_manager.show(req);
1832 self.a11y_dirty = true;
1836 if let Some(focus_id) = current_focus {
1837 self.overlay_manager.set_top_focus_restore(focus_id);
1838 }
1839 }
1840 for (mut req, duration) in ctx.timed_overlay_requests {
1841 if req.parent_overlay.is_none() {
1842 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
1843 }
1844 if self
1845 .overlay_manager
1846 .find_by_content(req.content_id)
1847 .is_some()
1848 {
1849 continue;
1850 }
1851 let current_focus = self.focused;
1852 let overlay_id = self.overlay_manager.show_for(req, duration);
1853 self.overlay_manager
1854 .set_shown_at_sim(overlay_id, self.sim_clock);
1855 self.a11y_dirty = true;
1856 if let Some(focus_id) = current_focus {
1857 self.overlay_manager.set_top_focus_restore(focus_id);
1858 }
1859 }
1860 for (mut req, progress, duration) in ctx.reveal_overlay_requests {
1861 if req.parent_overlay.is_none() {
1862 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
1863 }
1864 if self
1865 .overlay_manager
1866 .find_by_content(req.content_id)
1867 .is_some()
1868 {
1869 continue;
1870 }
1871 let content_id = req.content_id;
1872 let current_focus = self.focused;
1873 let overlay_id = self.overlay_manager.show(req);
1874 self.overlay_manager
1875 .set_shown_at_sim(overlay_id, self.sim_clock);
1876 self.a11y_dirty = true;
1877 if let Some(focus_id) = current_focus {
1878 self.overlay_manager.set_top_focus_restore(focus_id);
1879 }
1880 self.register_animated_signal(&progress, content_id);
1886 let _ = progress.try_animate_with_options(crate::animation::AnimationRequest {
1887 target: 1.0,
1888 duration,
1889 easing: teksilo_tokens::Easing::EaseOut,
1890 frame_interval: None,
1891 looping: false,
1892 epsilon: 0.0,
1893 max_duration: None,
1894 });
1895 self.overlay_manager
1896 .attach_fade(overlay_id, progress, duration);
1897 }
1898 if let Some(capture) = ctx.pointer_capture {
1899 if capture {
1900 self.pointer_captured_by = Some(source_widget);
1901 } else {
1902 self.pointer_captured_by = None;
1903 }
1904 }
1905 for (mut request, delay, focus_target, replace_siblings) in ctx.delayed_overlay_requests {
1906 if request.parent_overlay.is_none() {
1907 request.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
1908 }
1909 if self
1910 .overlay_manager
1911 .find_by_content(request.content_id)
1912 .is_some()
1913 {
1914 continue;
1915 }
1916 let content_id = request.content_id;
1917 self.pending_delayed_overlays
1918 .retain(|pending| pending.request.content_id != content_id);
1919 self.pending_delayed_overlays.push(PendingDelayedOverlay {
1920 request,
1921 delay,
1922 focus_target,
1923 replace_siblings,
1924 real_requested_at: std::time::Instant::now(),
1925 sim_requested_at: self.sim_clock,
1926 });
1927 self.arena.mark_needs_paint(source_widget);
1928 }
1929 for content_id in ctx.cancel_delayed_overlays {
1930 self.pending_delayed_overlays
1931 .retain(|pending| pending.request.content_id != content_id);
1932 }
1933 for content_id in ctx.safe_region_arm_requests {
1940 if let Some(apex) = self
1941 .previous_pointer_position
1942 .or(self.last_pointer_position)
1943 {
1944 self.overlay_manager.arm_safe_region(
1945 content_id,
1946 apex,
1947 std::time::Instant::now(),
1948 self.sim_clock,
1949 );
1950 }
1951 }
1952 for id in ctx.repaint_requests {
1953 self.arena.mark_needs_paint(id);
1954 }
1955 for id in ctx.synthetic_clicks {
1956 self.synthesise_tap_with_ops(id, &mut *ops);
1961 }
1962 if let Some(&id) = ctx.focus_requests.last() {
1963 let target = self.first_focusable_descendant(id).unwrap_or(id);
1973 self.focus_ops(target, &mut *ops);
1974 }
1975 if let Some(&id) = ctx.focus_into_requests.last() {
1976 if let Some(target) = self.first_focusable_descendant(id) {
1983 self.focus_ops(target, &mut *ops);
1984 }
1985 }
1986
1987 for req in ctx.scroll_into_view_requests {
1996 self.scroll_rect_into_view(
1997 req.from.unwrap_or(source_widget),
2000 req.rect,
2001 req.margin,
2002 req.align,
2003 req.motion,
2004 &mut *ops,
2005 );
2006 }
2007 for (id, margin) in ctx.scroll_widget_into_view_requests {
2014 if self.arena.get(id).is_some() {
2015 let bounds = self.arena.bounds(id);
2016 self.scroll_rect_into_view(
2017 id,
2018 bounds,
2019 margin,
2020 crate::event::ScrollAlign::Minimal,
2021 crate::event::ScrollMotion::Instant,
2022 &mut *ops,
2023 );
2024 }
2025 }
2026
2027 if let Some(&id) = ctx.highlight_tooltip_requests.last() {
2032 self.show_highlight_tooltip(id, &mut *ops);
2033 }
2034
2035 if let Some((source_widget, payload, preview_widget)) = ctx.drag_start_request {
2037 let (preview_content_id, preview_overlay_id) = if let Some(preview) = preview_widget {
2038 let content_id = self.add_boxed(preview);
2045 let overlay_id = self.overlay_manager.show(crate::overlay::OverlayRequest {
2046 content_id,
2047 anchor: source_widget,
2048 placement: crate::overlay::OverlayPlacement::AtPointer(
2049 teksilo_canvas::Point::ZERO,
2050 ),
2051 dismiss: crate::overlay::DismissBehavior::Manual,
2052 layer: crate::overlay::OverlayLayer::InTree,
2053 parent_overlay: None,
2054 on_dismiss: None,
2055 fade_duration: None,
2056 });
2057 self.arena.mark_needs_layout(content_id);
2061 (Some(content_id), Some(overlay_id))
2062 } else {
2063 (None, None)
2064 };
2065 self.active_drag = Some(crate::drag_state::DragSession {
2066 payload,
2067 source_widget: Some(source_widget),
2068 is_external: false,
2069 current_position: teksilo_canvas::Point::ZERO,
2070 current_target: None,
2071 feedback: crate::drag_state::DropFeedback::NoFeedback,
2072 preview_content_id,
2073 preview_overlay_id,
2074 });
2075 self.pointer_captured_by = Some(source_widget);
2076 self.current_cursor = crate::widget::CursorIcon::Grabbing;
2079 }
2080 if ctx.cancel_drag {
2081 self.cancel_active_drag(&mut *ops);
2082 }
2083
2084 if let Some(theme) = ctx.theme_request {
2086 self.pending_theme_request = Some(theme);
2092 }
2093 if ctx.follow_system_request {
2094 self.pending_follow_system_request = true;
2098 }
2099 if let Some(locale) = ctx.locale_request {
2100 self.pending_locale_request = Some(locale);
2106 }
2107 if let Some(scale) = ctx.text_scale_request {
2108 self.pending_text_scale_request = Some(scale);
2113 }
2114 }
2115
2116 fn apply_tree_mutations(&mut self, mutations: Vec<crate::widget::TreeMutation>) {
2117 use crate::binding::BindingLevel;
2118 use crate::widget::TreeMutation;
2119
2120 for mutation in mutations {
2121 match mutation {
2122 TreeMutation::SetDormant(id) => self.arena.set_dormant(id),
2123 TreeMutation::Activate(id) => self.arena.activate(id),
2124 TreeMutation::Destroy(id) => {
2125 if let Some(overlay_id) = self.overlay_manager().find_by_content(id) {
2138 self.dismiss_overlay(overlay_id);
2139 }
2140 self.destroy_subtree(id);
2141 }
2142 TreeMutation::MaterializeNow(id) => {
2146 if self.arena.get(id).is_some() {
2147 self.rebuild_single_widget(id);
2148 }
2149 }
2150 TreeMutation::RowSpaceActivate { row, fallback } => {
2151 self.keyboard_toggle_in(row).unwrap_or(fallback)();
2159 }
2160 TreeMutation::WithWidgetMut { id, dirty, apply } => {
2161 let existed = if let Some(any) =
2166 self.arena.get_mut(id).and_then(|n| n.widget.as_any_mut())
2167 {
2168 apply(any);
2169 true
2170 } else {
2171 false
2172 };
2173 if existed {
2174 match dirty {
2175 BindingLevel::RepaintOnly => self.arena.mark_needs_paint(id),
2176 BindingLevel::SubtreeRepaint => self.arena.mark_subtree_needs_paint(id),
2177 BindingLevel::Relayout => {
2178 self.arena.mark_needs_layout(id);
2179 self.arena.mark_ancestors_need_layout(id);
2180 }
2181 BindingLevel::Rebuild => {
2182 self.arena.mark_needs_rebuild(id);
2183 self.arena.mark_ancestors_need_layout(id);
2184 }
2185 BindingLevel::AccessibilityOnly => self.a11y_dirty = true,
2186 }
2187 }
2188 }
2189 }
2190 }
2191 }
2192
2193 pub fn hit_test(&self, point: Point) -> Option<WidgetId> {
2194 self.hit_test_excluding_overlay_and_widget(point, None, None)
2195 }
2196
2197 pub fn hit_test_excluding_overlay_and_widget(
2201 &self,
2202 point: Point,
2203 exclude_overlay: Option<crate::overlay::OverlayId>,
2204 exclude_widget: Option<WidgetId>,
2205 ) -> Option<WidgetId> {
2206 if let Some(overlay_id) = self.overlay_manager.hit_test(point) {
2207 if Some(overlay_id) == exclude_overlay {
2208 } else if let Some(overlay) = self.overlay_manager.overlay(overlay_id) {
2210 return self.arena.hit_test_in_subtree_excluding(
2211 overlay.content_id,
2212 point,
2213 exclude_widget,
2214 );
2215 }
2216 }
2217
2218 if self.overlay_manager.topmost_centered().is_some() {
2219 return None;
2220 }
2221
2222 self.arena.hit_test_at(point, exclude_widget)
2225 }
2226}
2227
2228fn is_context_menu_chord(key: Key, modifiers: Modifiers) -> bool {
2246 match key {
2247 Key::ContextMenu => modifiers == Modifiers::NONE,
2248 Key::F10 => modifiers == Modifiers::SHIFT,
2249 #[cfg(target_os = "macos")]
2250 Key::M => modifiers == Modifiers::CTRL | Modifiers::SHIFT,
2251 _ => false,
2252 }
2253}
2254
2255#[cfg(test)]
2256mod tests {
2257 use super::*;
2258 use crate::test_widgets::FillWidget;
2259 use crate::widget::CursorIcon;
2260 use crate::widget_builder::WidgetBuilder;
2261
2262 #[test]
2263 fn pointer_enter_leave_synthesized() {
2264 let mut tree = WidgetTree::new();
2265 let widget = tree.add(FillWidget::new());
2266 tree.layout(SizeProposal::exact(100.0, 50.0));
2267 tree.pointer_move(Point::new(50.0, 25.0));
2268 assert_eq!(tree.hovered, Some(widget));
2269 tree.pointer_move(Point::new(200.0, 200.0));
2270 assert_eq!(tree.hovered, None);
2271 }
2272
2273 #[test]
2274 fn pointer_hover_updates_current_cursor() {
2275 let mut tree = WidgetTree::new();
2276 tree.add(FillWidget::new().cursor(CursorIcon::ColResize));
2277 tree.layout(SizeProposal::exact(100.0, 50.0));
2278
2279 tree.pointer_move(Point::new(50.0, 25.0));
2280 assert_eq!(tree.current_cursor(), CursorIcon::ColResize);
2281
2282 tree.pointer_move(Point::new(200.0, 200.0));
2283 assert_eq!(tree.current_cursor(), CursorIcon::Default);
2284 }
2285
2286 #[derive(Debug)]
2289 struct Bumpable {
2290 value: i32,
2291 }
2292
2293 impl crate::widget::Widget for Bumpable {
2294 fn layout_response(
2295 &self,
2296 proposal: SizeProposal,
2297 _ctx: &crate::widget::LayoutContext,
2298 ) -> crate::widget::LayoutResponse {
2299 proposal.resolve(10.0, 10.0).into()
2300 }
2301 fn as_any(&self) -> Option<&dyn std::any::Any> {
2302 Some(self)
2303 }
2304 fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
2305 Some(self)
2306 }
2307 }
2308
2309 #[test]
2310 fn with_widget_mut_applies_and_dirty_marks() {
2311 let mut tree = WidgetTree::new();
2312 let id = tree.add(Bumpable { value: 0 });
2313 tree.layout(SizeProposal::exact(100.0, 100.0));
2314
2315 let mut ctx = EventContext::new();
2316 ctx.with_widget_mut::<Bumpable>(id, crate::binding::BindingLevel::Relayout, |b| {
2317 b.value = 42;
2318 });
2319 tree.collect_from_ctx(ctx, id);
2320
2321 let value = tree
2322 .widget_as_any(id)
2323 .and_then(|a| a.downcast_ref::<Bumpable>())
2324 .map(|b| b.value);
2325 assert_eq!(
2326 value,
2327 Some(42),
2328 "the deferred closure must mutate the live widget"
2329 );
2330 assert!(
2331 tree.needs_layout(),
2332 "Relayout dirty level must mark the tree for relayout"
2333 );
2334 }
2335
2336 #[test]
2337 #[cfg(debug_assertions)]
2338 #[should_panic(expected = "not the requested type")]
2339 fn with_widget_mut_wrong_type_panics_in_debug() {
2340 struct Other;
2341 let mut tree = WidgetTree::new();
2342 let id = tree.add(Bumpable { value: 0 });
2343 let mut ctx = EventContext::new();
2344 ctx.with_widget_mut::<Other>(
2345 id,
2346 crate::binding::BindingLevel::RepaintOnly,
2347 |_o: &mut Other| {},
2348 );
2349 tree.collect_from_ctx(ctx, id);
2352 }
2353
2354 #[test]
2355 fn with_widget_mut_closure_may_fire_observed_signals() {
2356 use crate::signal::Signal;
2364 let mut tree = WidgetTree::new();
2365 let id = tree.add(Bumpable { value: 0 });
2366 tree.layout(SizeProposal::exact(100.0, 100.0));
2367
2368 let trigger = Signal::new(0_u64);
2369 let echo = Signal::new(0_u64);
2370 let echo_for_obs = echo.clone();
2371 let _obs = trigger.observe(move |v| echo_for_obs.set(*v));
2372
2373 let trigger_in = trigger.clone();
2374 let mut ctx = EventContext::new();
2375 ctx.with_widget_mut::<Bumpable>(id, crate::binding::BindingLevel::RepaintOnly, move |b| {
2376 b.value = 7;
2377 trigger_in.set(99);
2379 });
2380 tree.collect_from_ctx(ctx, id); assert_eq!(
2383 echo.get(),
2384 99,
2385 "the observer ran during the deferred mutation"
2386 );
2387 let value = tree
2388 .widget_as_any(id)
2389 .and_then(|a| a.downcast_ref::<Bumpable>())
2390 .map(|b| b.value);
2391 assert_eq!(value, Some(7));
2392 }
2393
2394 #[test]
2395 fn request_accessibility_update_forces_rewalk() {
2396 let mut tree = WidgetTree::new();
2397 let id = tree.add(Bumpable { value: 0 });
2398 tree.layout(SizeProposal::exact(100.0, 100.0));
2399 let _ = tree.sync_accessibility(); assert!(
2401 !tree.a11y_dirty,
2402 "sync_accessibility should clear the dirty flag"
2403 );
2404
2405 let mut ctx = EventContext::new();
2406 ctx.request_accessibility_update();
2407 tree.collect_from_ctx(ctx, id);
2408 assert!(
2409 tree.a11y_dirty,
2410 "request_accessibility_update must force an AT re-walk"
2411 );
2412 }
2413
2414 #[test]
2415 fn rebuild_dirties_accessibility_tree() {
2416 let mut tree = WidgetTree::new();
2423 let id = tree.add(Bumpable { value: 0 });
2424 tree.layout(SizeProposal::exact(100.0, 100.0));
2425 let _ = tree.sync_accessibility(); assert!(
2427 !tree.a11y_dirty,
2428 "sync_accessibility should clear the dirty flag"
2429 );
2430
2431 tree.arena_mark_needs_rebuild_for_testing(id);
2434 tree.layout(SizeProposal::exact(100.0, 100.0));
2435 assert!(
2436 tree.a11y_dirty,
2437 "a rebuild must dirty the AT tree so the next sync re-walks"
2438 );
2439 }
2440
2441 #[test]
2442 fn bound_access_label_change_dirties_accessibility_tree() {
2443 use crate::signal::Signal;
2444 use crate::test_widgets::FillWidget;
2445 use crate::widget_builder::WidgetBuilder;
2446
2447 let label = Signal::new("first".to_string());
2453 let mut tree = WidgetTree::new();
2454 let _id = tree.add(FillWidget::new().access_label(label.clone()));
2455 tree.layout(SizeProposal::exact(100.0, 100.0));
2456 let _ = tree.sync_accessibility(); assert!(!tree.a11y_dirty, "sync_accessibility should clear the flag");
2458
2459 label.set("second".to_string());
2460 tree.layout(SizeProposal::exact(100.0, 100.0));
2461 assert!(
2462 tree.a11y_dirty,
2463 "changing a bound access_label must dirty the AT tree"
2464 );
2465 }
2466
2467 #[test]
2468 fn disabled_ancestor_blocks_event_to_descendant() {
2469 use crate::signal::Signal;
2470 use crate::test_widgets::StackWidget;
2471 use std::cell::Cell;
2472 use std::rc::Rc;
2473
2474 let tapped = Rc::new(Cell::new(false));
2475 let flag = tapped.clone();
2476 let enabled = Signal::new(true);
2477
2478 let mut tree = WidgetTree::new();
2479 let child = tree.add(FillWidget::new().on_tap(move |_pos, _ctx| {
2480 flag.set(true);
2481 }));
2482 let parent = tree.add(StackWidget::new().add_child(child));
2483 tree.enabled_when(parent, enabled.clone());
2484 tree.layout(SizeProposal::exact(100.0, 50.0));
2485
2486 enabled.set(false);
2487 tree.click(child);
2488 assert!(
2489 !tapped.get(),
2490 "disabled ancestor should block descendant tap"
2491 );
2492
2493 enabled.set(true);
2494 tree.click(child);
2495 assert!(tapped.get(), "re-enabling should restore dispatch");
2496 }
2497
2498 #[test]
2499 fn pointer_positions_are_widget_local_at_nonzero_origin() {
2500 use crate::event::{Modifiers, PointerButton};
2501 use crate::test_widgets::InsetWidget;
2502 use std::cell::Cell;
2503 use std::rc::Rc;
2504
2505 let tap_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
2507 let down_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
2508 let drag_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
2509 let (tp, dp, gp) = (tap_pos.clone(), down_pos.clone(), drag_pos.clone());
2510
2511 let mut tree = WidgetTree::new();
2512 let child = tree.add(
2513 FillWidget::new()
2514 .on_tap(move |ev, _ctx| tp.set(Some(ev.position)))
2515 .on_pointer_event(move |ev, _ctx| {
2516 if let WidgetEvent::PointerDown { position, .. } = ev {
2517 dp.set(Some(*position));
2518 }
2519 crate::event::EventResponse::Ignored
2520 })
2521 .on_drag(move |phase, _ctx| {
2522 use crate::gesture::DragPhase;
2523 match phase {
2524 DragPhase::Started { position, .. }
2525 | DragPhase::Moved { position, .. }
2526 | DragPhase::Ended { position } => gp.set(Some(position)),
2527 }
2528 }),
2529 );
2530 let inset = tree.add(InsetWidget::new(20.0).set_child(child));
2531 let _ = inset;
2532 tree.layout(SizeProposal::exact(200.0, 200.0));
2533 assert_eq!(tree.bounds(child).origin(), Point::new(20.0, 20.0));
2534
2535 tree.dispatch_event(WidgetEvent::PointerDown {
2537 position: Point::new(50.0, 40.0),
2538 button: PointerButton::Primary,
2539 modifiers: Modifiers::NONE,
2540 });
2541 assert_eq!(
2542 down_pos.get(),
2543 Some(Point::new(30.0, 20.0)),
2544 "on_pointer_event PointerDown must be widget-local"
2545 );
2546 tree.dispatch_event(WidgetEvent::PointerUp {
2547 position: Point::new(50.0, 40.0),
2548 button: PointerButton::Primary,
2549 modifiers: Modifiers::NONE,
2550 });
2551 assert_eq!(
2552 tap_pos.get(),
2553 Some(Point::new(30.0, 20.0)),
2554 "on_tap position must be widget-local"
2555 );
2556
2557 tree.dispatch_event(WidgetEvent::PointerDown {
2560 position: Point::new(50.0, 40.0),
2561 button: PointerButton::Primary,
2562 modifiers: Modifiers::NONE,
2563 });
2564 tree.dispatch_event(WidgetEvent::PointerMove {
2567 position: Point::new(65.0, 55.0),
2568 });
2569 tree.dispatch_event(WidgetEvent::PointerMove {
2570 position: Point::new(90.0, 70.0),
2571 });
2572 assert_eq!(
2573 drag_pos.get(),
2574 Some(Point::new(70.0, 50.0)),
2575 "on_drag position must be widget-local"
2576 );
2577 }
2578
2579 #[test]
2580 fn dormant_widget_not_hit_tested() {
2581 let mut tree = WidgetTree::new();
2582 let widget = tree.add(FillWidget::new());
2583 tree.layout(SizeProposal::exact(100.0, 50.0));
2584
2585 tree.pointer_move(Point::new(50.0, 25.0));
2586 assert_eq!(tree.hovered, Some(widget));
2587
2588 tree.set_dormant(widget);
2589 tree.pointer_move(Point::new(200.0, 200.0));
2590 tree.pointer_move(Point::new(50.0, 25.0));
2591 assert_eq!(tree.hovered, None);
2592 }
2593
2594 #[test]
2595 fn ancestor_pointer_handler_does_not_suppress_descendant_hover() {
2596 use crate::event::EventResponse;
2597 use crate::test_widgets::StackWidget;
2598 use std::cell::Cell;
2599 use std::rc::Rc;
2600
2601 let hovered = Rc::new(Cell::new(false));
2603 let h = hovered.clone();
2604
2605 let mut tree = WidgetTree::new();
2606 let child = tree.add(FillWidget::new().on_hover(move |entered, _ctx| h.set(entered)));
2607 tree.add(
2612 StackWidget::new()
2613 .add_child(child)
2614 .on_pointer_event(|_event, _ctx| EventResponse::Handled),
2615 );
2616 tree.layout(SizeProposal::exact(100.0, 50.0));
2617
2618 tree.pointer_move(Point::new(50.0, 25.0));
2619 assert!(
2620 hovered.get(),
2621 "a greedy ancestor on_pointer_event must NOT swallow the child's PointerEnter"
2622 );
2623
2624 tree.pointer_move(Point::new(500.0, 500.0));
2625 assert!(
2626 !hovered.get(),
2627 "PointerLeave must likewise reach the child despite the ancestor"
2628 );
2629 }
2630
2631 #[test]
2639 fn a_child_hover_handler_does_not_swallow_its_ancestors() {
2640 use crate::test_widgets::StackWidget;
2641 use std::cell::Cell;
2642 use std::rc::Rc;
2643
2644 let (row, button) = (Rc::new(Cell::new(false)), Rc::new(Cell::new(false)));
2645 let (r, b) = (row.clone(), button.clone());
2646
2647 let mut tree = WidgetTree::new();
2648 let child = tree.add(FillWidget::new().on_hover(move |entered, _ctx| b.set(entered)));
2649 tree.add(
2650 StackWidget::new()
2651 .add_child(child)
2652 .on_hover(move |entered, _ctx| r.set(entered)),
2653 );
2654 tree.layout(SizeProposal::exact(100.0, 50.0));
2655
2656 tree.pointer_move(Point::new(50.0, 25.0));
2657 assert!(button.get(), "the child is hovered");
2658 assert!(row.get(), "and so is the row it is inside");
2659
2660 tree.pointer_move(Point::new(500.0, 500.0));
2661 assert!(!button.get(), "the child heard the leave");
2662 assert!(
2663 !row.get(),
2664 "and so did the row — a container is not still hovered because the \
2665 pointer left it through a button"
2666 );
2667 }
2668
2669 #[test]
2670 fn destroy_subtree_clears_dangling_pointer_capture() {
2671 use crate::event::{EventResponse, Modifiers, PointerButton};
2672 use crate::test_widgets::StackWidget;
2673
2674 let mut tree = WidgetTree::new();
2675 let child = tree.add(FillWidget::new().on_pointer_event(|event, ctx| {
2676 if matches!(event, WidgetEvent::PointerDown { .. }) {
2677 ctx.capture_pointer();
2678 }
2679 EventResponse::Ignored
2680 }));
2681 let parent = tree.add(StackWidget::new().add_child(child));
2682 tree.layout(SizeProposal::exact(100.0, 50.0));
2683
2684 tree.dispatch_event(WidgetEvent::PointerDown {
2686 position: Point::new(50.0, 25.0),
2687 button: PointerButton::Primary,
2688 modifiers: Modifiers::NONE,
2689 });
2690 assert_eq!(
2691 tree.pointer_captured_by,
2692 Some(child),
2693 "PointerDown handler should have captured the pointer"
2694 );
2695
2696 tree.destroy_subtree(parent);
2700 assert_eq!(
2701 tree.pointer_captured_by, None,
2702 "destroy_subtree must clear a capture anchored at a destroyed widget"
2703 );
2704 }
2705
2706 #[test]
2713 fn key_preview_consumes_before_focused_on_key() {
2714 use crate::event::EventResponse;
2717 use crate::test_widgets::StackWidget;
2718 use std::cell::Cell;
2719 use std::rc::Rc;
2720
2721 let leaf_fired = Rc::new(Cell::new(false));
2722 let leaf_flag = leaf_fired.clone();
2723 let preview_fired = Rc::new(Cell::new(false));
2724 let preview_flag = preview_fired.clone();
2725
2726 let mut tree = WidgetTree::new();
2727 let leaf = tree.add(FillWidget::new().focusable().on_key(move |event, _c| {
2728 if matches!(event, WidgetEvent::KeyDown { .. }) {
2731 leaf_flag.set(true);
2732 }
2733 EventResponse::Handled
2734 }));
2735 let mid = tree.add(StackWidget::new().add_child(leaf));
2736 let _root =
2737 tree.add(StackWidget::new().add_child(mid).on_key_preview(
2738 move |event, _c| match event {
2739 WidgetEvent::KeyDown {
2740 key: Key::Enter, ..
2741 } => {
2742 preview_flag.set(true);
2743 EventResponse::Handled
2744 }
2745 _ => EventResponse::Ignored,
2746 },
2747 ));
2748
2749 tree.layout(SizeProposal::exact(100.0, 50.0));
2750 tree.focus(leaf);
2751 tree.press_key(Key::Enter, Modifiers::NONE);
2752
2753 assert!(
2754 preview_fired.get(),
2755 "ancestor on_key_preview must fire for KeyDown on a focused descendant"
2756 );
2757 assert!(
2758 !leaf_fired.get(),
2759 "consuming the event in preview must prevent the focused widget's on_key from running"
2760 );
2761 }
2762
2763 #[test]
2764 fn key_preview_falls_through_when_returning_ignored() {
2765 use crate::event::EventResponse;
2768 use crate::test_widgets::StackWidget;
2769 use std::cell::Cell;
2770 use std::rc::Rc;
2771
2772 let leaf_fired = Rc::new(Cell::new(false));
2773 let leaf_flag = leaf_fired.clone();
2774 let preview_fired = Rc::new(Cell::new(false));
2775 let preview_flag = preview_fired.clone();
2776
2777 let mut tree = WidgetTree::new();
2778 let leaf = tree.add(FillWidget::new().focusable().on_key(move |_e, _c| {
2779 leaf_flag.set(true);
2780 EventResponse::Handled
2781 }));
2782 let mid = tree.add(StackWidget::new().add_child(leaf));
2783 let _root = tree.add(StackWidget::new().add_child(mid).on_key_preview(
2784 move |_event, _c| {
2785 preview_flag.set(true);
2786 EventResponse::Ignored
2787 },
2788 ));
2789
2790 tree.layout(SizeProposal::exact(100.0, 50.0));
2791 tree.focus(leaf);
2792 tree.press_key(Key::Enter, Modifiers::NONE);
2793
2794 assert!(preview_fired.get(), "preview must always be invoked");
2795 assert!(
2796 leaf_fired.get(),
2797 "preview returning Ignored must not block the focused widget's on_key"
2798 );
2799 }
2800
2801 #[test]
2802 fn key_preview_excludes_focused_target_itself() {
2803 use crate::event::EventResponse;
2807 use std::cell::Cell;
2808 use std::rc::Rc;
2809
2810 let preview_on_target = Rc::new(Cell::new(false));
2811 let pf = preview_on_target.clone();
2812
2813 let mut tree = WidgetTree::new();
2814 let leaf = tree.add(FillWidget::new().focusable().on_key_preview(move |_e, _c| {
2815 pf.set(true);
2816 EventResponse::Handled
2817 }));
2818 tree.layout(SizeProposal::exact(100.0, 50.0));
2819 tree.focus(leaf);
2820 tree.press_key(Key::Enter, Modifiers::NONE);
2821
2822 assert!(
2823 !preview_on_target.get(),
2824 "the focused widget itself must not see its own on_key_preview"
2825 );
2826 }
2827
2828 #[test]
2829 fn key_preview_root_to_target_order() {
2830 use crate::event::EventResponse;
2834 use crate::test_widgets::StackWidget;
2835 use std::cell::RefCell;
2836 use std::rc::Rc;
2837
2838 let order: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
2839 let outer_log = order.clone();
2840 let inner_log = order.clone();
2841
2842 let mut tree = WidgetTree::new();
2843 let leaf = tree.add(FillWidget::new().focusable());
2844 let inner = tree.add(StackWidget::new().add_child(leaf).on_key_preview(
2845 move |event, _c| {
2846 if matches!(event, WidgetEvent::KeyDown { .. }) {
2847 inner_log.borrow_mut().push("inner");
2848 }
2849 EventResponse::Ignored
2850 },
2851 ));
2852 let _outer = tree.add(StackWidget::new().add_child(inner).on_key_preview(
2853 move |event, _c| {
2854 if matches!(event, WidgetEvent::KeyDown { .. }) {
2855 outer_log.borrow_mut().push("outer");
2856 }
2857 EventResponse::Ignored
2858 },
2859 ));
2860
2861 tree.layout(SizeProposal::exact(100.0, 50.0));
2862 tree.focus(leaf);
2863 tree.dispatch_event(WidgetEvent::KeyDown {
2864 key: Key::Enter,
2865 modifiers: Modifiers::NONE,
2866 text: None,
2867 });
2868
2869 assert_eq!(
2870 *order.borrow(),
2871 vec!["outer", "inner"],
2872 "preview must walk root → parent-of-target"
2873 );
2874 }
2875
2876 #[test]
2877 fn access_action_routes_to_cursored_target_not_focus() {
2878 use crate::signal::Signal;
2882 let a_fired = Signal::new(false);
2883 let b_fired = Signal::new(false);
2884 let a_cb = a_fired.clone();
2885 let b_cb = b_fired.clone();
2886
2887 let mut tree = WidgetTree::new();
2888 let a = tree.add(
2889 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| a_cb.set(true)),
2890 );
2891 let b = tree.add(
2892 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| b_cb.set(true)),
2893 );
2894 tree.layout(SizeProposal::exact(200.0, 100.0));
2895
2896 tree.focus(a);
2897 tree.dispatch_event(WidgetEvent::AccessAction {
2898 action: accesskit::Action::Click,
2899 target: Some(b),
2900 target_node: crate::accessibility::widget_id_to_node_id(b),
2901 data: None,
2902 });
2903
2904 assert!(b_fired.get(), "the cursored target must receive the action");
2905 assert!(
2906 !a_fired.get(),
2907 "the keyboard-focused widget must NOT receive an action targeting another node"
2908 );
2909 }
2910
2911 #[test]
2912 fn access_action_without_target_is_dropped_not_redirected_to_focus() {
2913 use crate::signal::Signal;
2916 let fired = Signal::new(false);
2917 let cb = fired.clone();
2918
2919 let mut tree = WidgetTree::new();
2920 let widget = tree.add(
2921 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| cb.set(true)),
2922 );
2923 tree.layout(SizeProposal::exact(200.0, 100.0));
2924
2925 tree.focus(widget);
2926 tree.dispatch_event(WidgetEvent::AccessAction {
2927 action: accesskit::Action::Click,
2928 target: None,
2929 target_node: crate::accessibility::root_node_id(),
2930 data: None,
2931 });
2932
2933 assert!(
2934 !fired.get(),
2935 "a target-less action must not be redirected to the focused widget"
2936 );
2937 }
2938
2939 #[test]
2946 fn shortcut_fires_matching_action_on_source_widget() {
2947 use crate::action::Action;
2948 use crate::shortcut::{KeyStroke, Shortcut};
2949 use std::cell::Cell;
2950 use std::rc::Rc;
2951
2952 let fired = Rc::new(Cell::new(false));
2953 let fired_flag = fired.clone();
2954
2955 let mut tree = WidgetTree::new();
2956 let widget = tree.add(FillWidget::new().focusable());
2957 tree.push_action(
2958 widget,
2959 Action::new("app.save").on_invoke(move |_intent, _ctx| {
2960 fired_flag.set(true);
2961 }),
2962 );
2963 tree.shortcut_registry_mut().register(
2964 Shortcut::new("app.save")
2965 .primary(KeyStroke::command(Key::S))
2966 .build(),
2967 );
2968
2969 tree.layout(SizeProposal::exact(100.0, 50.0));
2970 tree.focus(widget);
2971
2972 tree.press_key(Key::S, Modifiers::COMMAND);
2973 assert!(fired.get(), "matching action must fire on KeyDown");
2974 }
2975
2976 #[test]
2977 fn global_shortcut_fires_without_focused_widget() {
2978 use crate::action::Action;
2979 use crate::shortcut::{KeyStroke, Shortcut};
2980 use std::cell::Cell;
2981 use std::rc::Rc;
2982
2983 let fired = Rc::new(Cell::new(false));
2987 let fired_flag = fired.clone();
2988
2989 let mut tree = WidgetTree::new();
2990 let root = tree.add(FillWidget::new());
2991 tree.push_action(
2992 root,
2993 Action::new("app.save").on_invoke(move |_intent, _ctx| {
2994 fired_flag.set(true);
2995 }),
2996 );
2997 tree.shortcut_registry_mut().register(
2998 Shortcut::new("app.save")
2999 .primary(KeyStroke::command(Key::S))
3000 .build(),
3001 );
3002
3003 tree.layout(SizeProposal::exact(100.0, 50.0));
3004 tree.press_key(Key::S, Modifiers::COMMAND);
3007 assert!(
3008 fired.get(),
3009 "global shortcut must fire without a focused widget"
3010 );
3011 }
3012
3013 #[test]
3014 fn global_shortcut_fires_after_focused_widget_destroyed() {
3015 use crate::action::Action;
3016 use crate::shortcut::{KeyStroke, Shortcut};
3017 use std::cell::Cell;
3018 use std::rc::Rc;
3019
3020 let fired = Rc::new(Cell::new(false));
3025 let fired_flag = fired.clone();
3026
3027 let mut tree = WidgetTree::new();
3028 let root = tree.add(FillWidget::new());
3029 let focusable = tree.add_child(root, FillWidget::new().focusable());
3030 tree.push_action(
3031 root,
3032 Action::new("app.save").on_invoke(move |_intent, _ctx| {
3033 fired_flag.set(true);
3034 }),
3035 );
3036 tree.shortcut_registry_mut().register(
3037 Shortcut::new("app.save")
3038 .primary(KeyStroke::command(Key::S))
3039 .build(),
3040 );
3041
3042 tree.layout(SizeProposal::exact(100.0, 50.0));
3043 tree.focus(focusable);
3044 assert_eq!(tree.focused(), Some(focusable));
3045
3046 tree.destroy_subtree(focusable);
3049 assert_eq!(tree.focused(), None, "focus must clear when destroyed");
3050
3051 tree.press_key(Key::S, Modifiers::COMMAND);
3052 assert!(
3053 fired.get(),
3054 "global shortcut must still fire after the focused widget is destroyed"
3055 );
3056 }
3057
3058 #[test]
3059 fn scoped_shortcut_matches_only_when_focus_in_scope() {
3060 use crate::action::Action;
3061 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
3062 use std::cell::Cell;
3063 use std::rc::Rc;
3064
3065 let fired = Rc::new(Cell::new(0));
3066 let fired_flag = fired.clone();
3067
3068 let mut tree = WidgetTree::new();
3069 let scope_root = tree.add(FillWidget::new().focusable());
3070 let inside = tree.add_child(scope_root, FillWidget::new().focusable());
3071 let outside = tree.add(FillWidget::new().focusable());
3072
3073 tree.push_action(
3074 scope_root,
3075 Action::new("editor.find").on_invoke(move |_i, _c| {
3076 fired_flag.set(fired_flag.get() + 1);
3077 }),
3078 );
3079 tree.shortcut_registry_mut().register(
3080 Shortcut::new("editor.find")
3081 .primary(KeyStroke::command(Key::F))
3082 .scope(ShortcutScope::Scoped(scope_root))
3083 .build(),
3084 );
3085
3086 tree.layout(SizeProposal::exact(200.0, 100.0));
3087
3088 tree.focus(outside);
3090 tree.press_key(Key::F, Modifiers::COMMAND);
3091 assert_eq!(
3092 fired.get(),
3093 0,
3094 "scoped shortcut must not fire outside scope"
3095 );
3096
3097 tree.focus(inside);
3099 tree.press_key(Key::F, Modifiers::COMMAND);
3100 assert_eq!(
3101 fired.get(),
3102 1,
3103 "scoped shortcut must fire when focus in scope"
3104 );
3105 }
3106
3107 #[test]
3108 fn same_chord_scoped_first_falls_back_to_global_when_focus_outside() {
3109 use crate::action::Action;
3115 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
3116 use std::cell::Cell;
3117 use std::rc::Rc;
3118
3119 let scoped_fired = Rc::new(Cell::new(0));
3120 let global_fired = Rc::new(Cell::new(0));
3121 let sf = scoped_fired.clone();
3122 let gf = global_fired.clone();
3123
3124 let mut tree = WidgetTree::new();
3125 let root = tree.add(FillWidget::new());
3126 let editor = tree.add_child(root, FillWidget::new().focusable());
3127 let _editor_inner = tree.add_child(editor, FillWidget::new().focusable());
3128 let sidebar = tree.add_child(root, FillWidget::new().focusable());
3129
3130 tree.push_action(
3131 editor,
3132 Action::new("editor.saveBlock").on_invoke(move |_i, _c| sf.set(sf.get() + 1)),
3133 );
3134 tree.push_action(
3135 root,
3136 Action::new("zzz.global.save").on_invoke(move |_i, _c| gf.set(gf.get() + 1)),
3137 );
3138 tree.shortcut_registry_mut().register(
3139 Shortcut::new("editor.saveBlock")
3140 .primary(KeyStroke::command(Key::S))
3141 .scope(ShortcutScope::Scoped(editor))
3142 .build(),
3143 );
3144 tree.shortcut_registry_mut().register(
3145 Shortcut::new("zzz.global.save")
3146 .primary(KeyStroke::command(Key::S))
3147 .build(),
3148 );
3149
3150 tree.layout(SizeProposal::exact(200.0, 100.0));
3151 tree.focus(sidebar);
3152 tree.press_key(Key::S, Modifiers::COMMAND);
3153
3154 assert_eq!(global_fired.get(), 1, "applicable global must fire");
3155 assert_eq!(
3156 scoped_fired.get(),
3157 0,
3158 "inapplicable scoped binding must not eat the chord"
3159 );
3160 }
3161
3162 #[test]
3163 fn same_chord_global_first_yields_to_scoped_when_focus_inside() {
3164 use crate::action::Action;
3169 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
3170 use std::cell::Cell;
3171 use std::rc::Rc;
3172
3173 let scoped_fired = Rc::new(Cell::new(0));
3174 let global_fired = Rc::new(Cell::new(0));
3175 let sf = scoped_fired.clone();
3176 let gf = global_fired.clone();
3177
3178 let mut tree = WidgetTree::new();
3179 let root = tree.add(FillWidget::new());
3180 let editor = tree.add_child(root, FillWidget::new().focusable());
3181 let editor_inner = tree.add_child(editor, FillWidget::new().focusable());
3182 let sidebar = tree.add_child(root, FillWidget::new().focusable());
3183
3184 tree.push_action(
3185 editor,
3186 Action::new("editor.saveBlock").on_invoke(move |_i, _c| sf.set(sf.get() + 1)),
3187 );
3188 tree.push_action(
3189 root,
3190 Action::new("app.save").on_invoke(move |_i, _c| gf.set(gf.get() + 1)),
3191 );
3192 tree.shortcut_registry_mut().register(
3193 Shortcut::new("app.save")
3194 .primary(KeyStroke::command(Key::S))
3195 .build(),
3196 );
3197 tree.shortcut_registry_mut().register(
3198 Shortcut::new("editor.saveBlock")
3199 .primary(KeyStroke::command(Key::S))
3200 .scope(ShortcutScope::Scoped(editor))
3201 .build(),
3202 );
3203
3204 tree.layout(SizeProposal::exact(200.0, 100.0));
3205
3206 tree.focus(editor_inner);
3208 tree.press_key(Key::S, Modifiers::COMMAND);
3209 assert_eq!(
3210 scoped_fired.get(),
3211 1,
3212 "in-focus scoped must win over global"
3213 );
3214 assert_eq!(
3215 global_fired.get(),
3216 0,
3217 "global must yield to the scoped binding"
3218 );
3219
3220 tree.focus(sidebar);
3222 tree.press_key(Key::S, Modifiers::COMMAND);
3223 assert_eq!(scoped_fired.get(), 1, "scoped stays put outside its scope");
3224 assert_eq!(
3225 global_fired.get(),
3226 1,
3227 "global fires when focus leaves the scope"
3228 );
3229 }
3230
3231 #[test]
3232 fn propagated_action_lets_ancestor_handle() {
3233 use crate::action::Action;
3234 use crate::intent::IntentResponse;
3235 use crate::shortcut::{KeyStroke, Shortcut};
3236 use std::cell::Cell;
3237 use std::rc::Rc;
3238
3239 let inner_seen = Rc::new(Cell::new(false));
3240 let outer_seen = Rc::new(Cell::new(false));
3241 let inner_flag = inner_seen.clone();
3242 let outer_flag = outer_seen.clone();
3243
3244 let mut tree = WidgetTree::new();
3245 let outer = tree.add(FillWidget::new().focusable());
3246 let inner = tree.add_child(outer, FillWidget::new().focusable());
3247
3248 tree.push_action(
3250 inner,
3251 Action::new("app.save").on_invoke_with_response(move |_i, _c| {
3252 inner_flag.set(true);
3253 IntentResponse::Propagated
3254 }),
3255 );
3256 tree.push_action(
3257 outer,
3258 Action::new("app.save").on_invoke(move |_i, _c| {
3259 outer_flag.set(true);
3260 }),
3261 );
3262 tree.shortcut_registry_mut().register(
3263 Shortcut::new("app.save")
3264 .primary(KeyStroke::command(Key::S))
3265 .build(),
3266 );
3267
3268 tree.layout(SizeProposal::exact(100.0, 50.0));
3269 tree.focus(inner);
3270
3271 tree.press_key(Key::S, Modifiers::COMMAND);
3272 assert!(inner_seen.get(), "inner action observed the intent");
3273 assert!(outer_seen.get(), "outer action reached after Propagated");
3274 }
3275
3276 #[test]
3277 fn handled_action_stops_propagation() {
3278 use crate::action::Action;
3279 use crate::shortcut::{KeyStroke, Shortcut};
3280 use std::cell::Cell;
3281 use std::rc::Rc;
3282
3283 let inner_seen = Rc::new(Cell::new(false));
3284 let outer_seen = Rc::new(Cell::new(false));
3285 let inner_flag = inner_seen.clone();
3286 let outer_flag = outer_seen.clone();
3287
3288 let mut tree = WidgetTree::new();
3289 let outer = tree.add(FillWidget::new().focusable());
3290 let inner = tree.add_child(outer, FillWidget::new().focusable());
3291
3292 tree.push_action(
3293 inner,
3294 Action::new("app.save").on_invoke(move |_i, _c| {
3295 inner_flag.set(true);
3296 }),
3297 );
3298 tree.push_action(
3299 outer,
3300 Action::new("app.save").on_invoke(move |_i, _c| {
3301 outer_flag.set(true);
3302 }),
3303 );
3304 tree.shortcut_registry_mut().register(
3305 Shortcut::new("app.save")
3306 .primary(KeyStroke::command(Key::S))
3307 .build(),
3308 );
3309
3310 tree.layout(SizeProposal::exact(100.0, 50.0));
3311 tree.focus(inner);
3312
3313 tree.press_key(Key::S, Modifiers::COMMAND);
3314 assert!(inner_seen.get());
3315 assert!(!outer_seen.get(), "Handled at inner must stop propagation");
3316 }
3317
3318 #[test]
3319 fn disabled_action_propagates_by_default() {
3320 use crate::action::Action;
3321 use crate::shortcut::{KeyStroke, Shortcut};
3322 use crate::signal::Signal;
3323 use std::cell::Cell;
3324 use std::rc::Rc;
3325
3326 let inner_seen = Rc::new(Cell::new(false));
3327 let outer_seen = Rc::new(Cell::new(false));
3328 let inner_flag = inner_seen.clone();
3329 let outer_flag = outer_seen.clone();
3330
3331 let mut tree = WidgetTree::new();
3332 let outer = tree.add(FillWidget::new().focusable());
3333 let inner = tree.add_child(outer, FillWidget::new().focusable());
3334
3335 let enabled = Signal::new(false);
3336 tree.push_action(
3337 inner,
3338 Action::new("app.save")
3339 .enabled_when(enabled.clone())
3340 .on_invoke(move |_i, _c| {
3341 inner_flag.set(true);
3342 }),
3343 );
3344 tree.push_action(
3345 outer,
3346 Action::new("app.save").on_invoke(move |_i, _c| {
3347 outer_flag.set(true);
3348 }),
3349 );
3350 tree.shortcut_registry_mut().register(
3351 Shortcut::new("app.save")
3352 .primary(KeyStroke::command(Key::S))
3353 .build(),
3354 );
3355
3356 tree.layout(SizeProposal::exact(100.0, 50.0));
3357 tree.focus(inner);
3358
3359 tree.press_key(Key::S, Modifiers::COMMAND);
3360 assert!(!inner_seen.get(), "disabled inner must not run");
3361 assert!(
3362 outer_seen.get(),
3363 "intent must propagate past disabled inner"
3364 );
3365 }
3366
3367 #[test]
3368 fn disabled_action_with_non_propagating_shortcut_consumes() {
3369 use crate::action::Action;
3370 use crate::shortcut::{KeyStroke, Shortcut};
3371 use crate::signal::Signal;
3372 use std::cell::Cell;
3373 use std::rc::Rc;
3374
3375 let inner_seen = Rc::new(Cell::new(false));
3376 let outer_seen = Rc::new(Cell::new(false));
3377 let inner_flag = inner_seen.clone();
3378 let outer_flag = outer_seen.clone();
3379
3380 let mut tree = WidgetTree::new();
3381 let outer = tree.add(FillWidget::new().focusable());
3382 let inner = tree.add_child(outer, FillWidget::new().focusable());
3383
3384 let enabled = Signal::new(false);
3385 tree.push_action(
3386 inner,
3387 Action::new("app.save")
3388 .enabled_when(enabled.clone())
3389 .on_invoke(move |_i, _c| {
3390 inner_flag.set(true);
3391 }),
3392 );
3393 tree.push_action(
3394 outer,
3395 Action::new("app.save").on_invoke(move |_i, _c| {
3396 outer_flag.set(true);
3397 }),
3398 );
3399 tree.shortcut_registry_mut().register(
3400 Shortcut::new("app.save")
3401 .primary(KeyStroke::command(Key::S))
3402 .propagate_when_disabled(false)
3403 .build(),
3404 );
3405
3406 tree.layout(SizeProposal::exact(100.0, 50.0));
3407 tree.focus(inner);
3408
3409 tree.press_key(Key::S, Modifiers::COMMAND);
3410 assert!(!inner_seen.get(), "disabled inner still does not run");
3411 assert!(
3412 !outer_seen.get(),
3413 "intent must NOT propagate when shortcut disallows it"
3414 );
3415 }
3416
3417 #[test]
3418 fn send_intent_from_handler_reaches_ancestor_action() {
3419 use crate::action::Action;
3420 use crate::intent::Intent;
3421 use std::cell::Cell;
3422 use std::rc::Rc;
3423
3424 let save_seen = Rc::new(Cell::new(false));
3425 let save_flag = save_seen.clone();
3426
3427 let mut tree = WidgetTree::new();
3428 let root = tree.add(FillWidget::new());
3429 let button = tree.add_child(
3430 root,
3431 FillWidget::new().on_tap(|_pos, ctx| {
3432 ctx.send_intent(Intent::new("app.save"));
3433 }),
3434 );
3435 tree.push_action(
3436 root,
3437 Action::new("app.save").on_invoke(move |_i, _c| {
3438 save_flag.set(true);
3439 }),
3440 );
3441
3442 tree.layout(SizeProposal::exact(100.0, 50.0));
3443 tree.click(button);
3444 assert!(
3445 save_seen.get(),
3446 "ctx.send_intent must reach ancestor action"
3447 );
3448 }
3449
3450 #[test]
3451 fn widget_type_histogram_counts_distinct_types() {
3452 let mut tree = WidgetTree::new();
3457 let _ = tree.add(FillWidget::new());
3458 let _ = tree.add(FillWidget::new());
3459 let _ = tree.add(FillWidget::new());
3460 tree.layout(SizeProposal::exact(100.0, 100.0));
3461 let histogram = tree.widget_type_histogram();
3462 let total: u32 = histogram.values().sum();
3463 assert!(
3464 total >= 3,
3465 "expected at least 3 active widgets, got {total}: {histogram:?}"
3466 );
3467 let fillwidget_entries: u32 = histogram
3468 .iter()
3469 .filter(|(k, _)| k.contains("FillWidget"))
3470 .map(|(_, v)| *v)
3471 .sum();
3472 assert!(
3473 fillwidget_entries >= 3,
3474 "expected ≥3 FillWidget instances; histogram = {histogram:?}"
3475 );
3476 assert_eq!(tree.active_widget_count() as u32, total);
3477 }
3478
3479 #[test]
3480 fn intent_source_tagged_handler_for_tap_activation() {
3481 use crate::action::Action;
3485 use crate::intent::Intent;
3486 use crate::telemetry::IntentSource;
3487 use std::cell::Cell;
3488 use std::rc::Rc;
3489 let captured = Rc::new(Cell::new(IntentSource::Unknown));
3490 let captured_for_action = captured.clone();
3491
3492 let mut tree = WidgetTree::new();
3493 let root = tree.add(FillWidget::new());
3494 let button = tree.add_child(
3495 root,
3496 FillWidget::new().on_tap(|_pos, ctx| {
3497 ctx.send_intent(Intent::new("app.save"));
3498 }),
3499 );
3500 tree.push_action(
3501 root,
3502 Action::new("app.save").on_invoke(move |intent, _c| {
3503 captured_for_action.set(intent.source);
3504 }),
3505 );
3506
3507 tree.layout(SizeProposal::exact(100.0, 50.0));
3508 tree.click(button);
3509 assert_eq!(
3510 captured.get(),
3511 IntentSource::Handler,
3512 "tap-driven intent must tag IntentSource::Handler"
3513 );
3514 }
3515
3516 #[test]
3517 fn intent_source_programmatic_when_no_handler_active() {
3518 use crate::intent::Intent;
3519 use crate::telemetry::IntentSource;
3520 let intent = Intent::new("app.demo");
3521 assert_eq!(intent.source, IntentSource::Programmatic);
3522
3523 let mut ctx = EventContext::new();
3525 ctx.send_intent(Intent::new("app.demo"));
3526 let queued = ctx.pending_intents.first().expect("intent queued");
3527 assert_eq!(queued.source, IntentSource::Programmatic);
3528 }
3529
3530 #[test]
3531 fn with_intent_source_overrides_for_managed_widgets() {
3532 use crate::intent::Intent;
3533 use crate::telemetry::IntentSource;
3534 let mut ctx = EventContext::new();
3535 ctx.with_intent_source(IntentSource::Menu, |ctx| {
3536 ctx.send_intent(Intent::new("app.demo"));
3537 });
3538 let queued = ctx.pending_intents.first().expect("intent queued");
3539 assert_eq!(
3540 queued.source,
3541 IntentSource::Menu,
3542 "with_intent_source(Menu) must tag the dispatched intent"
3543 );
3544
3545 ctx.send_intent(Intent::new("app.next"));
3549 let next = ctx.pending_intents.last().expect("second intent");
3550 assert_eq!(next.source, IntentSource::Programmatic);
3551 }
3552
3553 #[test]
3554 fn disabled_shortcut_falls_through_to_focused_widget() {
3555 use crate::action::Action;
3556 use crate::shortcut::{KeyStroke, Shortcut};
3557 use crate::signal::Signal;
3558 use std::cell::Cell;
3559 use std::rc::Rc;
3560
3561 let action_fired = Rc::new(Cell::new(false));
3562 let on_key_fired = Rc::new(Cell::new(false));
3563 let af = action_fired.clone();
3564 let kf = on_key_fired.clone();
3565
3566 let enabled = Signal::new(false);
3567
3568 let mut tree = WidgetTree::new();
3569 let widget = tree.add(FillWidget::new().focusable().on_key(move |event, _ctx| {
3570 if matches!(
3571 event,
3572 WidgetEvent::KeyDown {
3573 key: Key::S,
3574 modifiers,
3575 ..
3576 } if modifiers.command()
3577 ) {
3578 kf.set(true);
3579 return EventResponse::Handled;
3580 }
3581 EventResponse::Ignored
3582 }));
3583 tree.push_action(
3584 widget,
3585 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
3586 );
3587 tree.shortcut_registry_mut().register(
3588 Shortcut::new("app.save")
3589 .primary(KeyStroke::command(Key::S))
3590 .enabled_when(enabled.clone())
3591 .build(),
3592 );
3593
3594 tree.layout(SizeProposal::exact(100.0, 50.0));
3595 tree.focus(widget);
3596
3597 tree.press_key(Key::S, Modifiers::COMMAND);
3599 assert!(
3600 !action_fired.get(),
3601 "disabled shortcut must not invoke its action"
3602 );
3603 assert!(
3604 on_key_fired.get(),
3605 "disabled shortcut must let KeyDown reach the focused widget"
3606 );
3607
3608 on_key_fired.set(false);
3610 enabled.set(true);
3611 tree.press_key(Key::S, Modifiers::COMMAND);
3612 assert!(action_fired.get(), "re-enabled shortcut must dispatch");
3613 assert!(
3614 !on_key_fired.get(),
3615 "enabled shortcut must consume the KeyDown"
3616 );
3617 }
3618
3619 #[test]
3620 fn keyboard_capture_bypasses_shortcut() {
3621 use crate::action::Action;
3622 use crate::shortcut::{KeyStroke, Shortcut};
3623 use std::cell::Cell;
3624 use std::rc::Rc;
3625
3626 fn run(capture: bool) -> (bool, bool) {
3632 let action_fired = Rc::new(Cell::new(false));
3633 let on_key_fired = Rc::new(Cell::new(false));
3634 let af = action_fired.clone();
3635 let kf = on_key_fired.clone();
3636
3637 let mut tree = WidgetTree::new();
3638 let widget = tree.add(
3639 FillWidget::new()
3640 .focusable()
3641 .keyboard_capture(capture)
3642 .on_key(move |event, _ctx| {
3643 if matches!(
3644 event,
3645 WidgetEvent::KeyDown { key: Key::S, modifiers, .. } if modifiers.command()
3646 ) {
3647 kf.set(true);
3648 return EventResponse::Handled;
3649 }
3650 EventResponse::Ignored
3651 }),
3652 );
3653 tree.push_action(
3654 widget,
3655 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
3656 );
3657 tree.shortcut_registry_mut().register(
3658 Shortcut::new("app.save")
3659 .primary(KeyStroke::command(Key::S))
3660 .build(),
3661 );
3662
3663 tree.layout(SizeProposal::exact(100.0, 50.0));
3664 tree.focus(widget);
3665 tree.press_key(Key::S, Modifiers::COMMAND);
3666 (action_fired.get(), on_key_fired.get())
3667 }
3668
3669 let (action, on_key) = run(true);
3671 assert!(
3672 !action,
3673 "keyboard_capture must suppress the shortcut action"
3674 );
3675 assert!(on_key, "keyboard_capture must deliver the raw KeyDown");
3676
3677 let (action, on_key) = run(false);
3679 assert!(action, "without capture the shortcut must fire");
3680 assert!(!on_key, "without capture the widget must not see the key");
3681 }
3682
3683 #[test]
3684 fn ctrl_tab_always_escapes_a_keyboard_capture_surface() {
3685 use std::cell::Cell;
3686 use std::rc::Rc;
3687
3688 let saw_key = Rc::new(Cell::new(false));
3694 let sk = saw_key.clone();
3695
3696 let mut tree = WidgetTree::new();
3697 let capture = tree.add(
3698 FillWidget::new()
3699 .focusable()
3700 .keyboard_capture(true)
3701 .on_key(move |_event, _ctx| {
3703 sk.set(true);
3704 EventResponse::Handled
3705 }),
3706 );
3707 let neighbour = tree.add(FillWidget::new().focusable());
3708 tree.layout(SizeProposal::exact(100.0, 50.0));
3709
3710 tree.focus(capture);
3713 tree.press_key(Key::Tab, Modifiers::NONE);
3714 assert!(saw_key.get(), "plain Tab must reach the capture surface");
3715 assert_eq!(
3716 tree.focused(),
3717 Some(capture),
3718 "plain Tab must not move focus off a capture surface"
3719 );
3720
3721 saw_key.set(false);
3723 tree.press_key(Key::Tab, Modifiers::CTRL);
3724 assert!(
3725 !saw_key.get(),
3726 "Ctrl+Tab is reserved and must not reach the capture surface"
3727 );
3728 assert_eq!(
3729 tree.focused(),
3730 Some(neighbour),
3731 "Ctrl+Tab must move focus out of a capture surface"
3732 );
3733
3734 tree.focus(capture);
3736 tree.press_key(Key::Tab, Modifiers::CTRL | Modifiers::SHIFT);
3737 assert_eq!(
3738 tree.focused(),
3739 Some(neighbour),
3740 "Ctrl+Shift+Tab must move focus out of a capture surface"
3741 );
3742 }
3743
3744 #[test]
3745 fn scope_mismatch_does_not_invoke_on_activate() {
3746 use crate::intent::Intent;
3747 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
3748 use std::cell::Cell;
3749 use std::rc::Rc;
3750
3751 let activated = Rc::new(Cell::new(false));
3757 let activated_flag = activated.clone();
3758
3759 let mut tree = WidgetTree::new();
3760 let scope_root = tree.add(FillWidget::new().focusable());
3761 let outside = tree.add(FillWidget::new().focusable());
3762
3763 tree.shortcut_registry_mut().register(
3764 Shortcut::new("editor.find")
3765 .primary(KeyStroke::command(Key::F))
3766 .scope(ShortcutScope::Scoped(scope_root))
3767 .on_activate(move |_ks, _ctx| {
3768 activated_flag.set(true);
3769 Intent::new("editor.find")
3770 })
3771 .build(),
3772 );
3773
3774 tree.layout(SizeProposal::exact(200.0, 100.0));
3775 tree.focus(outside);
3776
3777 tree.press_key(Key::F, Modifiers::COMMAND);
3778 assert!(
3779 !activated.get(),
3780 "on_activate must not run when focus is outside the shortcut's scope"
3781 );
3782 }
3783
3784 #[test]
3785 fn key_capture_runs_callback_and_bypasses_registry() {
3786 use crate::action::Action;
3787 use crate::shortcut::{KeyStroke, Shortcut};
3788 use std::cell::Cell;
3789 use std::rc::Rc;
3790
3791 let action_fired = Rc::new(Cell::new(false));
3792 let af = action_fired.clone();
3793 let captured = Rc::new(Cell::new(None));
3794 let cf = captured.clone();
3795
3796 let mut tree = WidgetTree::new();
3797 let widget = tree.add(FillWidget::new().focusable());
3798 tree.push_action(
3799 widget,
3800 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
3801 );
3802 tree.shortcut_registry_mut().register(
3803 Shortcut::new("app.save")
3804 .primary(KeyStroke::command(Key::S))
3805 .build(),
3806 );
3807
3808 tree.layout(SizeProposal::exact(100.0, 50.0));
3809 tree.focus(widget);
3810
3811 let handle = tree.begin_key_capture(move |ks, _reg, _ctx| cf.set(Some(ks)));
3812 assert!(tree.is_capturing_keys());
3813
3814 tree.press_key(Key::S, Modifiers::COMMAND);
3815 assert_eq!(
3816 captured.get(),
3817 Some(KeyStroke::command(Key::S)),
3818 "capture callback must receive the chord"
3819 );
3820 assert!(
3821 !action_fired.get(),
3822 "shortcut action must not fire while capture is armed"
3823 );
3824 assert!(
3825 !tree.is_capturing_keys(),
3826 "capture is one-shot; next KeyDown flows normally"
3827 );
3828 drop(handle);
3829 }
3830
3831 #[test]
3832 fn key_capture_can_rebind_through_registry() {
3833 use crate::shortcut::{KeyStroke, Shortcut};
3834
3835 let mut tree = WidgetTree::new();
3836 let widget = tree.add(FillWidget::new().focusable());
3837 tree.shortcut_registry_mut().register(
3838 Shortcut::new("app.save")
3839 .primary(KeyStroke::command(Key::S))
3840 .build(),
3841 );
3842
3843 tree.layout(SizeProposal::exact(100.0, 50.0));
3844 tree.focus(widget);
3845
3846 let _h = tree.begin_key_capture(|ks, reg, _ctx| {
3848 reg.rebind_primary("app.save", Some(ks));
3849 });
3850
3851 tree.press_key(Key::B, Modifiers::COMMAND | Modifiers::SHIFT);
3852 assert_eq!(
3853 tree.shortcut_registry()
3854 .effective("app.save")
3855 .unwrap()
3856 .primary,
3857 Some(KeyStroke::command_shift(Key::B))
3858 );
3859 }
3860
3861 #[test]
3862 fn dropping_capture_handle_cancels_capture() {
3863 use crate::shortcut::{KeyStroke, Shortcut};
3864 use std::cell::Cell;
3865 use std::rc::Rc;
3866
3867 let action_fired = Rc::new(Cell::new(false));
3868 let af = action_fired.clone();
3869 let capture_fired = Rc::new(Cell::new(false));
3870 let cf = capture_fired.clone();
3871
3872 let mut tree = WidgetTree::new();
3873 let widget = tree.add(FillWidget::new().focusable());
3874 tree.push_action(
3875 widget,
3876 crate::action::Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
3877 );
3878 tree.shortcut_registry_mut().register(
3879 Shortcut::new("app.save")
3880 .primary(KeyStroke::command(Key::S))
3881 .build(),
3882 );
3883 tree.layout(SizeProposal::exact(100.0, 50.0));
3884 tree.focus(widget);
3885
3886 {
3890 let _h = tree.begin_key_capture(move |_ks, _reg, _ctx| cf.set(true));
3891 assert!(tree.is_capturing_keys());
3892 }
3894 assert!(
3895 !tree.is_capturing_keys(),
3896 "dropping the handle must cancel the capture"
3897 );
3898
3899 tree.press_key(Key::S, Modifiers::COMMAND);
3900 assert!(!capture_fired.get(), "cancelled capture must not fire");
3901 assert!(
3902 action_fired.get(),
3903 "shortcut action runs after capture was cancelled"
3904 );
3905 }
3906
3907 #[test]
3908 fn second_begin_key_capture_does_not_racecancel_first() {
3909 use std::cell::Cell;
3910 use std::rc::Rc;
3911
3912 let first = Rc::new(Cell::new(false));
3913 let second = Rc::new(Cell::new(false));
3914 let f = first.clone();
3915 let s = second.clone();
3916
3917 let mut tree = WidgetTree::new();
3918 let widget = tree.add(FillWidget::new().focusable());
3919 tree.layout(SizeProposal::exact(100.0, 50.0));
3920 tree.focus(widget);
3921
3922 let h1 = tree.begin_key_capture(move |_ks, _reg, _ctx| f.set(true));
3928 let _h2 = tree.begin_key_capture(move |_ks, _reg, _ctx| s.set(true));
3929 drop(h1);
3930
3931 assert!(
3932 tree.is_capturing_keys(),
3933 "dropping the older handle must not cancel the active capture"
3934 );
3935 tree.press_key(Key::K, Modifiers::COMMAND);
3936 assert!(!first.get());
3937 assert!(second.get(), "newest capture wins");
3938 }
3939
3940 #[test]
3941 fn capture_callback_can_send_intent() {
3942 use crate::action::Action;
3943 use crate::intent::Intent;
3944
3945 use std::cell::Cell;
3946 use std::rc::Rc;
3947
3948 let ran = Rc::new(Cell::new(false));
3949 let flag = ran.clone();
3950
3951 let mut tree = WidgetTree::new();
3952 let widget = tree.add(FillWidget::new().focusable());
3953 tree.push_action(
3954 widget,
3955 Action::new("app.save").on_invoke(move |_i, _c| flag.set(true)),
3956 );
3957 tree.layout(SizeProposal::exact(100.0, 50.0));
3958 tree.focus(widget);
3959
3960 let _h = tree.begin_key_capture(|_ks, _reg, ctx| {
3961 ctx.send_intent(Intent::new("app.save"));
3962 });
3963 tree.press_key(Key::X, Modifiers::COMMAND);
3964 assert!(
3965 ran.get(),
3966 "intent queued from capture callback must dispatch"
3967 );
3968 }
3969
3970 #[test]
3971 fn binding_registry_does_not_accumulate_across_rebuilds() {
3972 use crate::binding::BindingLevel;
3973 use crate::signal::Signal;
3974
3975 #[derive(Debug)]
3976 struct BoundLeaf {
3977 tick: Signal<u64>,
3978 }
3979 impl crate::widget::Widget for BoundLeaf {
3980 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
3981 self.tick.bind_to(
3982 ctx.self_id(),
3983 ctx.binding_registry(),
3984 BindingLevel::Relayout,
3985 );
3986 Vec::new()
3987 }
3988 fn layout_response(
3989 &self,
3990 proposal: SizeProposal,
3991 _ctx: &crate::widget::LayoutContext,
3992 ) -> crate::widget::LayoutResponse {
3993 proposal.resolve(10.0, 10.0).into()
3994 }
3995 }
3996
3997 let mut tree = WidgetTree::new();
3998 let tick = Signal::new(0_u64);
3999 let widget = tree.add(BoundLeaf { tick: tick.clone() });
4000 tree.layout(SizeProposal::exact(200.0, 200.0));
4001 let after_first_build = tree.binding_registry().len();
4002 assert!(after_first_build >= 1);
4003
4004 for _ in 0..5 {
4008 tree.arena.mark_needs_rebuild(widget);
4009 tree.layout(SizeProposal::exact(200.0, 200.0));
4010 }
4011 assert_eq!(
4012 tree.binding_registry().len(),
4013 after_first_build,
4014 "bindings must be cleared on rebuild"
4015 );
4016
4017 tree.destroy_subtree(widget);
4018 assert_eq!(
4019 tree.binding_registry().len(),
4020 0,
4021 "bindings must be cleared on destroy"
4022 );
4023 let _ = tick;
4025 }
4026
4027 #[test]
4028 fn ctx_destroy_cancels_animations_and_bindings_via_deferred_path() {
4029 use crate::binding::BindingLevel;
4037 use crate::signal::Signal;
4038 use std::time::{Duration, Instant};
4039 use teksilo_tokens::Easing;
4040
4041 #[derive(Debug)]
4042 struct BoundLeaf {
4043 tick: Signal<u64>,
4044 }
4045 impl crate::widget::Widget for BoundLeaf {
4046 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
4047 self.tick.bind_to(
4048 ctx.self_id(),
4049 ctx.binding_registry(),
4050 BindingLevel::Relayout,
4051 );
4052 Vec::new()
4053 }
4054 fn layout_response(
4055 &self,
4056 proposal: SizeProposal,
4057 _ctx: &crate::widget::LayoutContext,
4058 ) -> crate::widget::LayoutResponse {
4059 proposal.resolve(10.0, 10.0).into()
4060 }
4061 }
4062
4063 let mut tree = WidgetTree::new();
4064 let widget = tree.add(BoundLeaf {
4065 tick: Signal::new(0_u64),
4066 });
4067 tree.layout(SizeProposal::exact(200.0, 200.0));
4068 assert!(tree.binding_registry().len() >= 1);
4069
4070 let anim = Signal::<f32>::new_animated(0.0);
4073 tree.animation_scheduler.animate(
4074 &anim,
4075 widget,
4076 1.0,
4077 Duration::from_secs(10),
4078 Easing::Linear,
4079 Instant::now(),
4080 );
4081 assert_eq!(tree.animation_scheduler.active_count(), 1);
4082
4083 let mut noop = crate::window::NoopWindowOps;
4085 tree.run_with_event_context(&mut noop, |ctx| ctx.destroy(widget));
4086
4087 assert_eq!(
4088 tree.animation_scheduler.active_count(),
4089 0,
4090 "ctx.destroy must cancel animations owned by the destroyed widget"
4091 );
4092 assert_eq!(
4093 tree.binding_registry().len(),
4094 0,
4095 "ctx.destroy must unregister the destroyed widget's bindings"
4096 );
4097 assert!(
4098 tree.arena.get(widget).is_none(),
4099 "node must be removed from the arena"
4100 );
4101 }
4102
4103 #[test]
4104 fn clear_shortcut_override_via_event_context_restores_default() {
4105 use crate::shortcut::{KeyStroke, Shortcut};
4106
4107 let mut tree = WidgetTree::new();
4108 tree.shortcut_registry_mut().register(
4109 Shortcut::new("app.save")
4110 .primary(KeyStroke::command(Key::S))
4111 .build(),
4112 );
4113 tree.shortcut_registry_mut()
4114 .rebind_primary("app.save", Some(KeyStroke::alt(Key::S)));
4115
4116 let source = tree.add(FillWidget::new());
4117 let mut ctx = EventContext::new();
4118 ctx.clear_shortcut_override("app.save");
4119 tree.collect_from_ctx(ctx, source);
4120
4121 assert_eq!(
4122 tree.shortcut_registry()
4123 .effective("app.save")
4124 .unwrap()
4125 .primary,
4126 Some(KeyStroke::command(Key::S))
4127 );
4128 }
4129
4130 #[test]
4131 fn rebind_shortcut_primary_via_event_context() {
4132 use crate::shortcut::{KeyStroke, Shortcut};
4133
4134 let mut tree = WidgetTree::new();
4135 tree.shortcut_registry_mut().register(
4136 Shortcut::new("app.save")
4137 .primary(KeyStroke::command(Key::S))
4138 .build(),
4139 );
4140 let source = tree.add(FillWidget::new());
4141
4142 let mut ctx = EventContext::new();
4143 ctx.rebind_shortcut_primary("app.save", Some(KeyStroke::alt(Key::S)));
4144 tree.collect_from_ctx(ctx, source);
4145
4146 assert_eq!(
4147 tree.shortcut_registry()
4148 .effective("app.save")
4149 .unwrap()
4150 .primary,
4151 Some(KeyStroke::alt(Key::S))
4152 );
4153 }
4154
4155 #[test]
4156 fn unregister_all_for_owner_called_on_destroy() {
4157 use crate::shortcut::{KeyStroke, Shortcut};
4158
4159 let mut tree = WidgetTree::new();
4160 let widget = tree.add(FillWidget::new());
4161 let widget_owner = widget;
4162 tree.shortcut_registry_mut().register_owned(
4163 Shortcut::new("scoped.thing")
4164 .primary(KeyStroke::command(Key::K))
4165 .build(),
4166 widget_owner,
4167 );
4168 assert!(
4169 tree.shortcut_registry()
4170 .get_default("scoped.thing")
4171 .is_some()
4172 );
4173
4174 tree.destroy_subtree(widget);
4175 assert!(
4176 tree.shortcut_registry()
4177 .get_default("scoped.thing")
4178 .is_none(),
4179 "destroying the owner must unregister its shortcut"
4180 );
4181 }
4182
4183 #[test]
4187 fn global_action_reached_from_unrelated_source() {
4188 use crate::action::Action;
4189 use crate::intent::Intent;
4190 use std::cell::Cell;
4191 use std::rc::Rc;
4192
4193 #[derive(Debug)]
4194 struct Registrar(Rc<Cell<bool>>);
4195 impl crate::widget::Widget for Registrar {
4196 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
4197 let flag = self.0.clone();
4198 ctx.register_action_global(
4199 Action::new("test.global").on_invoke(move |_i, _c| flag.set(true)),
4200 );
4201 vec![]
4202 }
4203 fn layout_response(
4204 &self,
4205 _p: teksilo_canvas::SizeProposal,
4206 _c: &crate::widget::LayoutContext,
4207 ) -> crate::widget::LayoutResponse {
4208 teksilo_canvas::Size::new(0.0, 0.0).into()
4209 }
4210 }
4211
4212 let mut tree = WidgetTree::new();
4213 let fired = Rc::new(Cell::new(false));
4214 let registrar = tree.add(Registrar(fired.clone()));
4215 let source = tree.add(FillWidget::new()); let mut ops = crate::window::NoopWindowOps;
4217
4218 tree.dispatch_intent(source, Intent::new("test.global"), true, &mut ops);
4219 assert!(
4220 fired.get(),
4221 "global action must fire from an unrelated source"
4222 );
4223
4224 fired.set(false);
4226 tree.destroy_subtree(registrar);
4227 tree.dispatch_intent(source, Intent::new("test.global"), true, &mut ops);
4228 assert!(
4229 !fired.get(),
4230 "destroying the owner must remove its global action"
4231 );
4232 }
4233
4234 #[test]
4246 fn hit_test_through_translate_scope() {
4247 use crate::test_widgets::StackWidget;
4248 let mut tree = WidgetTree::new();
4249 let child = tree.add(FillWidget::new());
4250 let parent = tree.add(StackWidget::new().add_child(child));
4251 tree.set_transform(parent, teksilo_canvas::Transform2D::translate(100.0, 0.0));
4253 tree.layout(SizeProposal::exact(100.0, 50.0));
4254
4255 assert_eq!(
4259 tree.hit_test(Point::new(50.0, 25.0)),
4260 None,
4261 "pre-transform area is not visually populated and must not hit"
4262 );
4263 assert_eq!(
4265 tree.hit_test(Point::new(150.0, 25.0)),
4266 Some(child),
4267 "visually-rendered area must hit the child"
4268 );
4269 assert_eq!(tree.hit_test(Point::new(250.0, 25.0)), None);
4271 }
4272
4273 #[test]
4274 fn hit_test_through_scale_scope() {
4275 use crate::test_widgets::StackWidget;
4276 let mut tree = WidgetTree::new();
4277 let child = tree.add(FillWidget::new());
4278 let parent = tree.add(StackWidget::new().add_child(child));
4279 tree.set_transform(parent, teksilo_canvas::Transform2D::scale(0.5, 0.5));
4282 tree.layout(SizeProposal::exact(100.0, 50.0));
4283
4284 assert_eq!(tree.hit_test(Point::new(25.0, 12.0)), Some(child));
4286 assert_eq!(
4289 tree.hit_test(Point::new(75.0, 25.0)),
4290 None,
4291 "scaled-out region must not hit"
4292 );
4293 }
4294
4295 #[test]
4296 fn hit_test_through_nested_transforms_compose() {
4297 use crate::test_widgets::StackWidget;
4298 let mut tree = WidgetTree::new();
4299 let leaf = tree.add(FillWidget::new());
4300 let inner = tree.add(StackWidget::new().add_child(leaf));
4301 let outer = tree.add(StackWidget::new().add_child(inner));
4302 tree.set_transform(outer, teksilo_canvas::Transform2D::translate(100.0, 0.0));
4306 tree.set_transform(inner, teksilo_canvas::Transform2D::scale(2.0, 2.0));
4307 tree.layout(SizeProposal::exact(50.0, 25.0));
4308
4309 assert_eq!(tree.hit_test(Point::new(150.0, 25.0)), Some(leaf));
4313 assert_eq!(tree.hit_test(Point::new(50.0, 25.0)), None);
4314 assert_eq!(tree.hit_test(Point::new(250.0, 25.0)), None);
4315 }
4316
4317 #[test]
4318 fn hit_test_identity_transform_unchanged() {
4319 let mut tree = WidgetTree::new();
4323 let widget = tree.add(FillWidget::new());
4324 tree.set_transform(widget, teksilo_canvas::Transform2D::IDENTITY);
4325 tree.layout(SizeProposal::exact(100.0, 50.0));
4326 assert_eq!(tree.hit_test(Point::new(50.0, 25.0)), Some(widget));
4327 }
4328
4329 #[test]
4330 fn arena_effective_transform_composes_ancestors() {
4331 use crate::test_widgets::StackWidget;
4342 let mut tree = WidgetTree::new();
4343 let leaf = tree.add(FillWidget::new());
4344 let inner = tree.add(StackWidget::new().add_child(leaf));
4345 let outer = tree.add(StackWidget::new().add_child(inner));
4346 tree.set_transform(outer, teksilo_canvas::Transform2D::translate(100.0, 0.0));
4347 tree.set_transform(inner, teksilo_canvas::Transform2D::scale(2.0, 2.0));
4348 tree.layout(SizeProposal::exact(50.0, 25.0));
4349
4350 let eff = tree.arena.effective_transform(leaf);
4351 let expected = teksilo_canvas::Transform2D::scale(2.0, 2.0)
4352 .then(&teksilo_canvas::Transform2D::translate(100.0, 0.0));
4353 for (a, b) in eff.m.iter().zip(expected.m.iter()) {
4354 assert!(
4355 (a - b).abs() < 1e-5,
4356 "effective_transform mismatch: got {:?}, want {:?}",
4357 eff.m,
4358 expected.m
4359 );
4360 }
4361
4362 let screen_origin = eff.apply_point(Point::new(0.0, 0.0));
4368 assert!((screen_origin.x - 100.0).abs() < 1e-5);
4369 assert!((screen_origin.y - 0.0).abs() < 1e-5);
4370 let screen_corner = eff.apply_point(Point::new(50.0, 25.0));
4373 assert!((screen_corner.x - 200.0).abs() < 1e-5);
4374 assert!((screen_corner.y - 50.0).abs() < 1e-5);
4375 }
4376
4377 #[derive(Debug)]
4382 struct StubMenu;
4383 impl crate::widget::Widget for StubMenu {
4384 fn layout_response(
4385 &self,
4386 _proposal: SizeProposal,
4387 _ctx: &crate::widget::LayoutContext,
4388 ) -> crate::widget::LayoutResponse {
4389 teksilo_canvas::Size::new(100.0, 40.0).into()
4390 }
4391 }
4392
4393 #[derive(Debug)]
4405 struct NominatingWidget {
4406 row: std::cell::Cell<Option<WidgetId>>,
4407 }
4408
4409 impl crate::widget::Widget for NominatingWidget {
4410 fn layout_response(
4411 &self,
4412 proposal: SizeProposal,
4413 _ctx: &LayoutContext,
4414 ) -> crate::widget::LayoutResponse {
4415 proposal.resolve(50.0, 20.0).into()
4416 }
4417
4418 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
4419 ctx.apply_self_handlers(crate::widget_builder::HandlerSet::new().focusable(true));
4420 Vec::new()
4421 }
4422
4423 fn context_menu_key_target(&self) -> Option<WidgetId> {
4424 self.row.get()
4425 }
4426 }
4427
4428 fn press(tree: &mut WidgetTree, key: Key, modifiers: Modifiers) {
4429 tree.dispatch_event(WidgetEvent::KeyDown {
4430 key,
4431 modifiers,
4432 text: None,
4433 });
4434 }
4435
4436 #[test]
4437 fn the_context_menu_key_opens_the_focused_widget_menu() {
4438 use std::cell::Cell;
4439 use std::rc::Rc;
4440
4441 let opened = Rc::new(Cell::new(false));
4442 let flag = opened.clone();
4443 let mut tree = WidgetTree::new();
4444 let widget = tree.add(
4445 FillWidget::new()
4446 .focusable()
4447 .context_menu(move |_pos, _ctx| {
4448 flag.set(true);
4449 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
4450 }),
4451 );
4452 tree.layout(SizeProposal::exact(200.0, 100.0));
4453 tree.focus(widget);
4454
4455 press(&mut tree, Key::ContextMenu, Modifiers::NONE);
4456 assert!(opened.get(), "the dedicated Menu key must open the menu");
4457 }
4458
4459 #[test]
4462 fn shift_f10_opens_the_focused_widget_menu() {
4463 use std::cell::Cell;
4464 use std::rc::Rc;
4465
4466 let opened = Rc::new(Cell::new(false));
4467 let flag = opened.clone();
4468 let mut tree = WidgetTree::new();
4469 let widget = tree.add(
4470 FillWidget::new()
4471 .focusable()
4472 .context_menu(move |_pos, _ctx| {
4473 flag.set(true);
4474 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
4475 }),
4476 );
4477 tree.layout(SizeProposal::exact(200.0, 100.0));
4478 tree.focus(widget);
4479
4480 press(&mut tree, Key::F10, Modifiers::SHIFT);
4481 assert!(opened.get(), "Shift+F10 must open the menu");
4482 }
4483
4484 #[test]
4487 fn a_near_miss_chord_is_not_a_context_menu_request() {
4488 use std::cell::Cell;
4489 use std::rc::Rc;
4490
4491 let opened = Rc::new(Cell::new(false));
4492 let flag = opened.clone();
4493 let mut tree = WidgetTree::new();
4494 let widget = tree.add(
4495 FillWidget::new()
4496 .focusable()
4497 .context_menu(move |_pos, _ctx| {
4498 flag.set(true);
4499 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
4500 }),
4501 );
4502 tree.layout(SizeProposal::exact(200.0, 100.0));
4503 tree.focus(widget);
4504
4505 press(&mut tree, Key::F10, Modifiers::SHIFT | Modifiers::CTRL);
4506 press(&mut tree, Key::F10, Modifiers::NONE);
4507 assert!(!opened.get(), "only Shift+F10 exactly asks for a menu");
4508 }
4509
4510 #[test]
4514 fn the_keyboard_target_can_be_a_row_rather_than_the_focused_container() {
4515 use std::cell::Cell;
4516 use std::rc::Rc;
4517
4518 let menu_owner = Rc::new(Cell::new(None::<&'static str>));
4519
4520 let row_flag = menu_owner.clone();
4521 let container_flag = menu_owner.clone();
4522
4523 let mut tree = WidgetTree::new();
4524 let row = tree.add(FillWidget::new().context_menu(move |_pos, _ctx| {
4525 row_flag.set(Some("row"));
4526 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
4527 }));
4528 let container = tree.add(
4529 crate::test_widgets::StackWidget::new()
4530 .add_child(row)
4531 .context_menu(move |_pos, _ctx| {
4532 container_flag.set(Some("container"));
4533 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
4534 }),
4535 );
4536 tree.layout(SizeProposal::exact(200.0, 100.0));
4537
4538 let nominator = tree.add(NominatingWidget {
4540 row: std::cell::Cell::new(Some(row)),
4541 });
4542 tree.layout(SizeProposal::exact(200.0, 100.0));
4543 tree.focus(nominator);
4544 let _ = container;
4545
4546 press(&mut tree, Key::ContextMenu, Modifiers::NONE);
4547 assert_eq!(
4548 menu_owner.get(),
4549 Some("row"),
4550 "the nominated row's factory must be the one that runs"
4551 );
4552 }
4553
4554 #[test]
4557 fn the_chord_falls_through_when_there_is_no_menu_to_show() {
4558 use std::cell::Cell;
4559 use std::rc::Rc;
4560
4561 let saw_key = Rc::new(Cell::new(false));
4562 let flag = saw_key.clone();
4563 let mut tree = WidgetTree::new();
4564 let widget = tree.add(FillWidget::new().focusable().on_key(move |ev, _ctx| {
4565 if matches!(ev, WidgetEvent::KeyDown { key: Key::F10, .. }) {
4566 flag.set(true);
4567 }
4568 crate::event::EventResponse::Ignored
4569 }));
4570 tree.layout(SizeProposal::exact(200.0, 100.0));
4571 tree.focus(widget);
4572
4573 press(&mut tree, Key::F10, Modifiers::SHIFT);
4574 assert!(
4575 saw_key.get(),
4576 "with no factory anywhere, the key must reach the widget"
4577 );
4578 }
4579
4580 #[test]
4581 fn context_menu_factory_receives_click_position() {
4582 use crate::event::{Modifiers, PointerButton};
4583 use std::cell::Cell;
4584 use std::rc::Rc;
4585
4586 let captured_position = Rc::new(Cell::new(None::<Point>));
4587 let cap = captured_position.clone();
4588 let mut tree = WidgetTree::new();
4589 let widget = tree.add(FillWidget::new().context_menu(move |pos, _ctx| {
4590 cap.set(Some(pos));
4591 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
4592 }));
4593 tree.layout(SizeProposal::exact(200.0, 100.0));
4594
4595 let click = Point::new(73.0, 42.0);
4596 tree.dispatch_event(WidgetEvent::PointerDown {
4597 position: click,
4598 button: PointerButton::Secondary,
4599 modifiers: Modifiers::NONE,
4600 });
4601
4602 let got = captured_position.get();
4603 assert_eq!(
4604 got,
4605 Some(click),
4606 "factory must receive the click position; got {:?}",
4607 got
4608 );
4609 let _ = widget;
4610 }
4611
4612 #[test]
4613 fn context_menu_factory_returning_none_falls_through_to_parent() {
4614 use crate::event::{Modifiers, PointerButton};
4615 use crate::test_widgets::StackWidget;
4616 use std::cell::Cell;
4617 use std::rc::Rc;
4618
4619 let outer_called = Rc::new(Cell::new(0_u32));
4623 let outer_flag = outer_called.clone();
4624 let mut tree = WidgetTree::new();
4625 let inner = tree.add(FillWidget::new().context_menu(|_pos, _ctx| None));
4626 let _outer = tree.add(StackWidget::new().add_child(inner).context_menu(
4627 move |_pos, _ctx| {
4628 outer_flag.set(outer_flag.get() + 1);
4629 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
4630 },
4631 ));
4632 tree.layout(SizeProposal::exact(200.0, 100.0));
4633
4634 tree.dispatch_event(WidgetEvent::PointerDown {
4635 position: Point::new(50.0, 25.0),
4636 button: PointerButton::Secondary,
4637 modifiers: Modifiers::NONE,
4638 });
4639
4640 assert_eq!(
4641 outer_called.get(),
4642 1,
4643 "inner returning None must fall through to the outer factory"
4644 );
4645 }
4646
4647 #[test]
4648 fn context_menu_factory_none_throughout_chain_does_not_show_overlay() {
4649 use crate::event::{Modifiers, PointerButton};
4650
4651 let mut tree = WidgetTree::new();
4653 tree.add(FillWidget::new().context_menu(|_pos, _ctx| None));
4654 tree.layout(SizeProposal::exact(200.0, 100.0));
4655
4656 let overlay_count_before = tree.overlay_manager.len();
4657 tree.dispatch_event(WidgetEvent::PointerDown {
4658 position: Point::new(50.0, 25.0),
4659 button: PointerButton::Secondary,
4660 modifiers: Modifiers::NONE,
4661 });
4662 let overlay_count_after = tree.overlay_manager.len();
4663 assert_eq!(
4664 overlay_count_before, overlay_count_after,
4665 "a factory returning None must not mount any overlay"
4666 );
4667 }
4668
4669 #[derive(Debug)]
4680 struct FreshChildHost {
4681 preserve: bool,
4682 }
4683 impl Widget for FreshChildHost {
4684 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
4685 vec![ctx.add(FillWidget::new())]
4686 }
4687 fn layout_response(
4688 &self,
4689 p: SizeProposal,
4690 _c: &LayoutContext,
4691 ) -> crate::widget::LayoutResponse {
4692 p.resolve(10.0, 10.0).into()
4693 }
4694 fn preserves_children_on_rebuild(&self) -> bool {
4695 self.preserve
4696 }
4697 }
4698
4699 #[test]
4700 fn reconcile_reaps_dropped_children_no_leak() {
4701 for preserve in [false, true] {
4706 let mut tree = WidgetTree::new();
4707 let host = tree.add(FreshChildHost { preserve });
4708 tree.layout(SizeProposal::exact(100.0, 100.0));
4709 let total0 = tree.arena.len();
4710 let active0 = tree.active_widget_count();
4711 for _ in 0..5 {
4712 tree.arena_mark_needs_rebuild_for_testing(host);
4713 tree.layout(SizeProposal::exact(100.0, 100.0));
4714 }
4715 assert_eq!(
4716 tree.arena.len(),
4717 total0,
4718 "preserve={preserve}: dropped children must be reaped, not leaked"
4719 );
4720 assert_eq!(
4721 tree.active_widget_count(),
4722 active0,
4723 "preserve={preserve}: no stranded still-active orphans"
4724 );
4725 }
4726 }
4727
4728 #[derive(Debug)]
4734 struct DetachedContentHost {
4735 preserve: bool,
4736 }
4737 impl Widget for DetachedContentHost {
4738 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
4739 let popup = ctx.add_detached(FillWidget::new());
4740 ctx.set_dormant(popup);
4741 vec![ctx.add(FillWidget::new())]
4742 }
4743 fn layout_response(
4744 &self,
4745 p: SizeProposal,
4746 _c: &LayoutContext,
4747 ) -> crate::widget::LayoutResponse {
4748 p.resolve(10.0, 10.0).into()
4749 }
4750 fn preserves_children_on_rebuild(&self) -> bool {
4751 self.preserve
4752 }
4753 }
4754
4755 #[test]
4756 fn rebuilding_reaps_detached_content_no_leak() {
4757 for preserve in [false, true] {
4763 let mut tree = WidgetTree::new();
4764 let host = tree.add(DetachedContentHost { preserve });
4765 tree.layout(SizeProposal::exact(100.0, 100.0));
4766 let total0 = tree.arena.len();
4767 for _ in 0..5 {
4768 tree.arena_mark_needs_rebuild_for_testing(host);
4769 tree.layout(SizeProposal::exact(100.0, 100.0));
4770 }
4771 assert_eq!(
4772 tree.arena.len(),
4773 total0,
4774 "preserve={preserve}: the previous build's detached content must be reaped"
4775 );
4776 }
4777 }
4778
4779 #[test]
4780 fn destroying_a_host_reaps_its_detached_content() {
4781 let mut tree = WidgetTree::new();
4782 let outer = tree.add(FillWidget::new());
4783 tree.layout(SizeProposal::exact(100.0, 100.0));
4784 let empty = tree.arena.len();
4785
4786 let host = tree.add_child(outer, DetachedContentHost { preserve: false });
4787 tree.layout(SizeProposal::exact(100.0, 100.0));
4788 assert!(tree.arena.len() > empty);
4789
4790 tree.destroy_subtree(host);
4791 assert_eq!(
4792 tree.arena.len(),
4793 empty,
4794 "the popup must die with the widget that built it"
4795 );
4796 }
4797
4798 #[derive(Debug)]
4800 struct StableChildHost {
4801 child: Option<WidgetId>,
4802 probe: std::rc::Rc<std::cell::Cell<Option<WidgetId>>>,
4803 }
4804 impl Widget for StableChildHost {
4805 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
4806 let id = match self.child {
4807 Some(id) => id,
4808 None => {
4809 let id = ctx.add(FillWidget::new());
4810 self.child = Some(id);
4811 self.probe.set(Some(id));
4812 id
4813 }
4814 };
4815 vec![id]
4816 }
4817 fn layout_response(
4818 &self,
4819 p: SizeProposal,
4820 _c: &LayoutContext,
4821 ) -> crate::widget::LayoutResponse {
4822 p.resolve(10.0, 10.0).into()
4823 }
4824 fn preserves_children_on_rebuild(&self) -> bool {
4825 true
4826 }
4827 }
4828
4829 #[test]
4830 fn reconcile_preserves_reattached_child() {
4831 let probe = std::rc::Rc::new(std::cell::Cell::new(None));
4832 let mut tree = WidgetTree::new();
4833 let host = tree.add(StableChildHost {
4834 child: None,
4835 probe: probe.clone(),
4836 });
4837 tree.layout(SizeProposal::exact(100.0, 100.0));
4838 let child = probe.get().expect("child mounted");
4839 let total0 = tree.arena.len();
4840 for _ in 0..5 {
4841 tree.arena_mark_needs_rebuild_for_testing(host);
4842 tree.layout(SizeProposal::exact(100.0, 100.0));
4843 }
4844 assert!(
4845 tree.arena.is_active(child),
4846 "the re-attached child must survive every rebuild"
4847 );
4848 assert_eq!(tree.arena.len(), total0, "no growth — same child reused");
4849 }
4850
4851 #[derive(Debug)]
4853 struct Wrapper {
4854 child: WidgetId,
4855 }
4856 impl Widget for Wrapper {
4857 fn build(&mut self, _ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
4858 vec![self.child]
4859 }
4860 fn layout_response(
4861 &self,
4862 p: SizeProposal,
4863 _c: &LayoutContext,
4864 ) -> crate::widget::LayoutResponse {
4865 p.resolve(10.0, 10.0).into()
4866 }
4867 }
4868
4869 #[derive(Debug)]
4873 struct ReparentHost {
4874 body: Option<WidgetId>,
4875 probe: std::rc::Rc<std::cell::Cell<Option<WidgetId>>>,
4876 }
4877 impl Widget for ReparentHost {
4878 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
4879 let body = match self.body {
4880 Some(id) => id,
4881 None => {
4882 let id = ctx.add(FillWidget::new());
4883 self.body = Some(id);
4884 self.probe.set(Some(id));
4885 id
4886 }
4887 };
4888 vec![ctx.add(Wrapper { child: body })]
4889 }
4890 fn layout_response(
4891 &self,
4892 p: SizeProposal,
4893 _c: &LayoutContext,
4894 ) -> crate::widget::LayoutResponse {
4895 p.resolve(10.0, 10.0).into()
4896 }
4897 fn preserves_children_on_rebuild(&self) -> bool {
4898 true
4899 }
4900 }
4901
4902 #[test]
4903 fn reconcile_spares_reparented_survivor() {
4904 let probe = std::rc::Rc::new(std::cell::Cell::new(None));
4911 let mut tree = WidgetTree::new();
4912 let host = tree.add(ReparentHost {
4913 body: None,
4914 probe: probe.clone(),
4915 });
4916 tree.layout(SizeProposal::exact(100.0, 100.0));
4917 let body = probe.get().expect("body mounted");
4918 let total0 = tree.arena.len();
4919 for _ in 0..5 {
4920 tree.arena_mark_needs_rebuild_for_testing(host);
4921 tree.layout(SizeProposal::exact(100.0, 100.0));
4922 }
4923 assert!(
4924 tree.arena.is_active(body),
4925 "the re-parented body must survive — it was moved into the new tree, \
4926 not swept with the dropped wrapper"
4927 );
4928 assert_eq!(
4929 tree.arena.len(),
4930 total0,
4931 "dropped wrappers reaped — no per-rebuild leak"
4932 );
4933 }
4934
4935 #[derive(Debug)]
4945 struct BelowContainer {
4946 child: Option<WidgetId>,
4947 offset: f32,
4948 }
4949
4950 impl crate::widget::Widget for BelowContainer {
4951 fn layout_response(
4952 &self,
4953 proposal: SizeProposal,
4954 _ctx: &crate::widget::LayoutContext,
4955 ) -> crate::widget::LayoutResponse {
4956 proposal.resolve(0.0, 0.0).into()
4957 }
4958 fn place_children(
4959 &self,
4960 bounds: Rect,
4961 _proposal: SizeProposal,
4962 children: &mut [crate::widget::WidgetPlacement],
4963 _ctx: &crate::widget::LayoutContext,
4964 ) {
4965 for c in children.iter_mut() {
4966 c.origin = Point::new(bounds.x, bounds.y + self.offset);
4967 c.size = bounds.size();
4968 }
4969 }
4970 fn children(&self) -> Vec<WidgetId> {
4971 self.child.into_iter().collect()
4972 }
4973 }
4974
4975 fn recording_scroll_container(
4978 tree: &mut WidgetTree,
4979 child: WidgetId,
4980 recorded: std::rc::Rc<std::cell::Cell<Option<Rect>>>,
4981 ) -> WidgetId {
4982 use crate::test_widgets::StackWidget;
4983 tree.add(
4984 StackWidget::new()
4985 .add_child(child)
4986 .on_scroll(move |ev, _ctx| match ev {
4987 WidgetEvent::ScrollIntoView { target_bounds, .. } => {
4988 recorded.set(Some(*target_bounds));
4989 EventResponse::Handled
4990 }
4991 _ => EventResponse::Ignored,
4992 })
4993 .clips_children(true),
4994 )
4995 }
4996
4997 #[test]
4998 fn ensure_visible_dispatches_scroll_into_view_to_clipping_ancestor() {
4999 use std::cell::Cell;
5000 use std::rc::Rc;
5001 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
5002 let mut tree = WidgetTree::new();
5003 let actor = tree.add(FillWidget::new());
5004 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
5005 tree.layout(SizeProposal::exact(100.0, 100.0));
5006
5007 let target = Rect::new(10.0, 500.0, 20.0, 15.0);
5010 let mut ctx = EventContext::new();
5011 ctx.ensure_visible(target);
5012 tree.collect_from_ctx(ctx, actor);
5013
5014 assert_eq!(
5015 recorded.get(),
5016 Some(target),
5017 "ensure_visible(rect) must dispatch ScrollIntoView with the exact rect \
5018 to the clips_children ancestor"
5019 );
5020 }
5021
5022 #[test]
5023 fn ensure_visible_is_noop_when_rect_already_visible() {
5024 use std::cell::Cell;
5025 use std::rc::Rc;
5026 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
5027 let mut tree = WidgetTree::new();
5028 let actor = tree.add(FillWidget::new());
5029 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
5030 tree.layout(SizeProposal::exact(100.0, 100.0));
5031
5032 let mut ctx = EventContext::new();
5035 ctx.ensure_visible(Rect::new(10.0, 10.0, 20.0, 15.0));
5036 tree.collect_from_ctx(ctx, actor);
5037
5038 assert_eq!(
5039 recorded.get(),
5040 None,
5041 "a rect already inside the viewport must not trigger a scroll"
5042 );
5043 }
5044
5045 #[test]
5046 fn ensure_visible_margin_forces_scroll_near_edge() {
5047 use std::cell::Cell;
5048 use std::rc::Rc;
5049 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
5050 let mut tree = WidgetTree::new();
5051 let actor = tree.add(FillWidget::new());
5052 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
5053 tree.layout(SizeProposal::exact(100.0, 100.0));
5054
5055 let rect = Rect::new(10.0, 95.0, 20.0, 4.0);
5058 let mut ctx = EventContext::new();
5059 ctx.ensure_visible_with_margin(rect, 10.0);
5060 tree.collect_from_ctx(ctx, actor);
5061
5062 assert_eq!(
5063 recorded.get(),
5064 Some(rect),
5065 "the margin must widen the visibility test so a near-edge rect scrolls"
5066 );
5067 }
5068
5069 fn recording_align_container(
5072 tree: &mut WidgetTree,
5073 child: WidgetId,
5074 recorded: std::rc::Rc<
5075 std::cell::Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>,
5076 >,
5077 ) -> WidgetId {
5078 use crate::test_widgets::StackWidget;
5079 tree.add(
5080 StackWidget::new()
5081 .add_child(child)
5082 .on_scroll(move |ev, _ctx| match ev {
5083 WidgetEvent::ScrollIntoView { align, motion, .. } => {
5084 recorded.set(Some((*align, *motion)));
5085 EventResponse::Handled
5086 }
5087 _ => EventResponse::Ignored,
5088 })
5089 .clips_children(true),
5090 )
5091 }
5092
5093 #[test]
5094 fn ensure_visible_aligned_scrolls_even_when_already_visible() {
5095 use std::cell::Cell;
5096 use std::rc::Rc;
5097 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
5098 let mut tree = WidgetTree::new();
5099 let actor = tree.add(FillWidget::new());
5100 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
5101 tree.layout(SizeProposal::exact(100.0, 100.0));
5102
5103 let target = Rect::new(10.0, 10.0, 20.0, 15.0);
5108 let mut ctx = EventContext::new();
5109 ctx.ensure_visible_aligned(target, 0.5, crate::event::ScrollMotion::Instant);
5110 tree.collect_from_ctx(ctx, actor);
5111
5112 assert_eq!(
5113 recorded.get(),
5114 Some(target),
5115 "an aligned reveal must dispatch even when the rect is already visible"
5116 );
5117 }
5118
5119 #[test]
5120 fn ensure_visible_aligned_forwards_fraction_and_motion() {
5121 use std::cell::Cell;
5122 use std::rc::Rc;
5123 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
5124 Rc::new(Cell::new(None));
5125 let mut tree = WidgetTree::new();
5126 let actor = tree.add(FillWidget::new());
5127 let _container = recording_align_container(&mut tree, actor, recorded.clone());
5128 tree.layout(SizeProposal::exact(100.0, 100.0));
5129
5130 let mut ctx = EventContext::new();
5131 ctx.ensure_visible_aligned(
5132 Rect::new(10.0, 10.0, 20.0, 15.0),
5133 0.25,
5134 crate::event::ScrollMotion::Smooth,
5135 );
5136 tree.collect_from_ctx(ctx, actor);
5137
5138 assert_eq!(
5139 recorded.get(),
5140 Some((
5141 crate::event::ScrollAlign::Fraction(0.25),
5142 crate::event::ScrollMotion::Smooth
5143 )),
5144 "the container must receive the requested fraction and motion verbatim"
5145 );
5146 }
5147
5148 #[test]
5149 fn ensure_visible_aligned_clamps_the_fraction() {
5150 use std::cell::Cell;
5151 use std::rc::Rc;
5152 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
5153 Rc::new(Cell::new(None));
5154 let mut tree = WidgetTree::new();
5155 let actor = tree.add(FillWidget::new());
5156 let _container = recording_align_container(&mut tree, actor, recorded.clone());
5157 tree.layout(SizeProposal::exact(100.0, 100.0));
5158
5159 let mut ctx = EventContext::new();
5160 ctx.ensure_visible_aligned(
5161 Rect::new(10.0, 10.0, 20.0, 15.0),
5162 4.2,
5163 crate::event::ScrollMotion::Instant,
5164 );
5165 tree.collect_from_ctx(ctx, actor);
5166
5167 assert_eq!(
5168 recorded.get().map(|(a, _)| a),
5169 Some(crate::event::ScrollAlign::Fraction(1.0)),
5170 "an out-of-range fraction must clamp rather than aim the pin off-screen"
5171 );
5172 }
5173
5174 #[test]
5175 fn plain_ensure_visible_requests_minimal_alignment() {
5176 use std::cell::Cell;
5177 use std::rc::Rc;
5178 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
5179 Rc::new(Cell::new(None));
5180 let mut tree = WidgetTree::new();
5181 let actor = tree.add(FillWidget::new());
5182 let _container = recording_align_container(&mut tree, actor, recorded.clone());
5183 tree.layout(SizeProposal::exact(100.0, 100.0));
5184
5185 let mut ctx = EventContext::new();
5186 ctx.ensure_visible(Rect::new(10.0, 500.0, 20.0, 15.0));
5187 tree.collect_from_ctx(ctx, actor);
5188
5189 assert_eq!(
5190 recorded.get(),
5191 Some((
5192 crate::event::ScrollAlign::Minimal,
5193 crate::event::ScrollMotion::Instant
5194 )),
5195 "the pre-existing reveal API must keep its exact semantics"
5196 );
5197 }
5198
5199 #[test]
5200 fn only_the_innermost_container_aligns() {
5201 use std::cell::Cell;
5202 use std::rc::Rc;
5203 let inner_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
5204 Rc::new(Cell::new(None));
5205 let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
5206 Rc::new(Cell::new(None));
5207
5208 let mut tree = WidgetTree::new();
5209 let actor = tree.add(FillWidget::new());
5210 let inner = recording_align_container(&mut tree, actor, inner_rec.clone());
5211 let _outer = recording_align_container(&mut tree, inner, outer_rec.clone());
5212 tree.layout(SizeProposal::exact(100.0, 100.0));
5213
5214 let mut ctx = EventContext::new();
5217 ctx.ensure_visible_aligned(
5218 Rect::new(10.0, 500.0, 20.0, 15.0),
5219 0.5,
5220 crate::event::ScrollMotion::Instant,
5221 );
5222 tree.collect_from_ctx(ctx, actor);
5223
5224 assert_eq!(
5225 inner_rec.get().map(|(a, _)| a),
5226 Some(crate::event::ScrollAlign::Fraction(0.5)),
5227 "the innermost clipping ancestor owns the pin"
5228 );
5229 assert_eq!(
5230 outer_rec.get().map(|(a, _)| a),
5231 Some(crate::event::ScrollAlign::Minimal),
5232 "an outer container must only bring the inner viewport into view — a \
5233 fraction names a height in one viewport, not in every ancestor's"
5234 );
5235 }
5236
5237 #[test]
5238 fn ensure_widget_visible_uses_target_arena_bounds() {
5239 use std::cell::Cell;
5240 use std::rc::Rc;
5241 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
5242 let mut tree = WidgetTree::new();
5243 let target = tree.add(FillWidget::new());
5245 let rec = recorded.clone();
5246 let container = tree.add(
5247 BelowContainer {
5248 child: Some(target),
5249 offset: 500.0,
5250 }
5251 .on_scroll(move |ev, _ctx| match ev {
5252 WidgetEvent::ScrollIntoView { target_bounds, .. } => {
5253 rec.set(Some(*target_bounds));
5254 EventResponse::Handled
5255 }
5256 _ => EventResponse::Ignored,
5257 })
5258 .clips_children(true),
5259 );
5260 tree.layout(SizeProposal::exact(100.0, 100.0));
5261
5262 let expected = tree.bounds(target);
5263 assert!(
5264 expected.y > 100.0,
5265 "fixture sanity: the target must sit below the viewport (y={})",
5266 expected.y
5267 );
5268
5269 let mut ctx = EventContext::new();
5272 ctx.ensure_widget_visible(target);
5273 tree.collect_from_ctx(ctx, container);
5274
5275 assert_eq!(
5276 recorded.get(),
5277 Some(expected),
5278 "ensure_widget_visible(id) must dispatch ScrollIntoView with the \
5279 target's current arena bounds"
5280 );
5281 }
5282
5283 #[test]
5284 fn ensure_widget_visible_ignores_missing_widget() {
5285 use std::cell::Cell;
5287 use std::rc::Rc;
5288 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
5289 let mut tree = WidgetTree::new();
5290 let actor = tree.add(FillWidget::new());
5291 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
5292 tree.layout(SizeProposal::exact(100.0, 100.0));
5293
5294 let mut ctx = EventContext::new();
5295 ctx.ensure_widget_visible(WidgetId::default());
5296 tree.collect_from_ctx(ctx, actor); assert_eq!(
5299 recorded.get(),
5300 None,
5301 "an unmounted id must not trigger a scroll"
5302 );
5303 }
5304
5305 #[test]
5306 fn context_menu_inside_a_modal_keeps_the_modal() {
5307 use crate::event::{Modifiers, PointerButton, WidgetEvent};
5311 use crate::overlay::{DismissBehavior, OverlayLayer, OverlayPlacement, OverlayRequest};
5312 use crate::test_widgets::{FillWidget, StackWidget};
5313
5314 let mut tree = WidgetTree::new();
5315 let modal_content = tree.add(StackWidget::new());
5318 let _editor = tree.add_child(
5319 modal_content,
5320 FillWidget::new()
5321 .context_menu(|_pos, _ctx| Some(Box::new(FillWidget::new()) as Box<dyn Widget>)),
5322 );
5323 tree.layout(SizeProposal::exact(200.0, 100.0));
5324
5325 let modal = tree.overlay_manager.show(OverlayRequest {
5326 content_id: modal_content,
5327 anchor: modal_content,
5328 placement: OverlayPlacement::Centered,
5329 dismiss: DismissBehavior::EscapeKey,
5330 layer: OverlayLayer::InTree,
5331 parent_overlay: None,
5332 on_dismiss: None,
5333 fade_duration: None,
5334 });
5335 tree.overlay_manager
5337 .stack
5338 .iter_mut()
5339 .find(|o| o.id == modal)
5340 .unwrap()
5341 .bounds = Rect::new(0.0, 0.0, 200.0, 100.0);
5342 assert_eq!(tree.overlay_manager.len(), 1);
5343
5344 tree.dispatch_event(WidgetEvent::PointerDown {
5346 position: Point::new(50.0, 25.0),
5347 button: PointerButton::Secondary,
5348 modifiers: Modifiers::NONE,
5349 });
5350
5351 assert!(
5352 tree.overlay_manager.active_ids().contains(&modal),
5353 "the modal must survive opening a context menu inside it"
5354 );
5355 assert_eq!(
5356 tree.overlay_manager.len(),
5357 2,
5358 "the context menu should now be open on top of the surviving modal"
5359 );
5360 }
5361}