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 self.focus_with_origin_ops(
565 id,
566 crate::focus::FocusOrigin::Programmatic,
567 &mut *ops,
568 );
569 self.access_action_handled = self.focused == Some(id);
572 } else if *action == accesskit::Action::ShowContextMenu {
573 self.access_action_handled =
589 if self.dispatch_to_widget_returning_handled(id, &event, &mut *ops) {
590 true
591 } else {
592 let position = self.arena.bounds(id).center();
593 self.show_context_menu_for(id, position, &mut *ops)
594 };
595 } else {
596 self.access_action_handled =
597 self.dispatch_to_widget_returning_handled(id, &event, &mut *ops);
598 }
599 }
600 }
601 WidgetEvent::Gesture { .. } => {
602 if let Some(target) = self.hovered.or(self.focused) {
603 self.dispatch_to_widget(target, &event, &mut *ops);
604 }
605 }
606 WidgetEvent::ScrollIntoView { .. }
607 | WidgetEvent::PointerEnter
608 | WidgetEvent::PointerLeave
609 | WidgetEvent::FocusGained { .. }
610 | WidgetEvent::FocusLost => {}
611 }
612 self.drain_pending_intents(&mut *ops);
617 }
618
619 fn open_context_menu_from_keyboard(&mut self, ops: &mut dyn crate::window::WindowOps) -> bool {
632 let Some(focused) = self.focused else {
633 return false;
634 };
635 let target = self
636 .arena
637 .get(focused)
638 .and_then(|node| node.widget.context_menu_key_target())
639 .filter(|id| self.arena.is_active(*id))
640 .unwrap_or(focused);
641
642 let bounds = self.bounds(target);
646 let anchor = Point {
647 x: bounds.x + bounds.width / 2.0,
648 y: bounds.y + bounds.height / 2.0,
649 };
650 self.show_context_menu_for(target, anchor, ops)
651 }
652
653 fn show_context_menu_for(
654 &mut self,
655 target: WidgetId,
656 position: Point,
657 ops: &mut dyn crate::window::WindowOps,
658 ) -> bool {
659 let mut ctx = self.make_event_context(&mut *ops);
665 let mut walker = Some(target);
666 let menu_decision: Option<(WidgetId, Box<dyn Widget>)> = loop {
667 let owner_id = {
670 let mut probe = walker;
671 loop {
672 match probe {
673 None => break None,
674 Some(id) => {
675 if self
676 .arena
677 .get(id)
678 .is_some_and(|node| node.context_menu_factory.is_some())
679 {
680 break Some(id);
681 }
682 probe = self.arena.get(id).and_then(|node| node.parent);
683 }
684 }
685 }
686 };
687 let Some(owner_id) = owner_id else {
688 break None;
689 };
690 let outcome: Option<Box<dyn Widget>> = {
697 let node = self
698 .arena
699 .get(owner_id)
700 .expect("owner_id from active arena walk");
701 let factory = node
702 .context_menu_factory
703 .as_ref()
704 .expect("owner_id only set when factory present");
705 factory(position, &mut ctx)
706 };
707 match outcome {
708 Some(menu) => break Some((owner_id, menu)),
709 None => {
710 walker = self.arena.get(owner_id).and_then(|n| n.parent);
712 }
713 }
714 };
715
716 let drain_anchor = menu_decision
720 .as_ref()
721 .map(|(id, _)| *id)
722 .or_else(|| self.arena.roots().first().copied())
723 .unwrap_or(target);
724 self.collect_from_ctx(ctx, drain_anchor);
725
726 let Some((owner_id, menu_widget)) = menu_decision else {
727 return false;
728 };
729
730 let keep: std::collections::HashSet<WidgetId> = self
736 .overlay_manager
737 .stack
738 .iter()
739 .map(|o| o.content_id)
740 .filter(|&content_id| self.is_descendant_of(owner_id, content_id))
741 .collect();
742 let dismissed = self.overlay_manager.dismiss_except(&keep);
743 self.dormant_dismissed_content(&dismissed, &mut *ops);
744
745 let content_id = self.add_boxed(menu_widget);
746 let prev_focus = self.focused;
747 self.overlay_manager.show(crate::overlay::OverlayRequest {
748 content_id,
749 anchor: owner_id,
750 placement: crate::overlay::OverlayPlacement::AtPointer(position),
751 dismiss: crate::overlay::DismissBehavior::EscapeOrClickOutside,
752 layer: crate::overlay::OverlayLayer::InTree,
753 parent_overlay: None,
754 on_dismiss: None,
755 fade_duration: None,
756 });
757 if let Some(focus_id) = prev_focus {
758 self.overlay_manager.set_top_focus_restore(focus_id);
759 }
760 self.focus_ops(content_id, &mut *ops);
761 self.drain_pending_intents(&mut *ops);
766 true
767 }
768
769 fn handle_pointer_move(&mut self, position: Point, ops: &mut dyn crate::window::WindowOps) {
770 self.last_pointer_position = Some(position);
771 let target = self.hit_test(position);
772
773 if target != self.hovered {
774 let previously_hovered = self.hovered;
775 if let Some(old) = self.hovered {
776 self.dispatch_to_widget(old, &WidgetEvent::PointerLeave, &mut *ops);
777 self.tooltip_pointer_leave(old, &mut *ops);
778 }
779 if let Some(new) = target {
780 self.dispatch_to_widget(new, &WidgetEvent::PointerEnter, &mut *ops);
781 self.tooltip_pointer_enter(new);
782 }
783 self.set_hovered(target);
784 self.update_hover_within_signals(previously_hovered, target);
785 } else if let Some(target) = target {
786 self.tooltip_pointer_moved(target, position);
789 }
790
791 if let Some(target) = target {
792 self.dispatch_to_widget(target, &WidgetEvent::PointerMove { position }, &mut *ops);
793 }
794 }
795
796 pub(super) fn dispatch_to_widget(
797 &mut self,
798 target: WidgetId,
799 event: &WidgetEvent,
800 ops: &mut dyn crate::window::WindowOps,
801 ) {
802 self.dispatch_to_widget_returning_handled(target, event, ops);
803 }
804
805 fn widget_has_drag(&self, id: WidgetId) -> bool {
808 self.arena
809 .get(id)
810 .map(|n| n.any_handler(|h| h.on_drag.is_some() || h.on_swipe.is_some()))
811 .unwrap_or(false)
812 }
813
814 fn is_gesture_dead_zone(&self, id: WidgetId) -> bool {
818 self.arena
819 .get(id)
820 .map(|n| n.gesture_dead_zone)
821 .unwrap_or(false)
822 }
823
824 fn is_keyboard_capture(&self, id: WidgetId) -> bool {
828 self.arena
829 .get(id)
830 .map(|n| n.keyboard_capture)
831 .unwrap_or(false)
832 }
833
834 pub(super) fn arm_drag_observers(
845 &mut self,
846 captured: WidgetId,
847 down_event: &WidgetEvent,
848 ops: &mut dyn crate::window::WindowOps,
849 ) {
850 self.drag_observers.clear();
851 if self.widget_has_drag(captured) {
852 return;
853 }
854 if self.is_gesture_dead_zone(captured) {
857 return;
858 }
859 let mut observers = Vec::new();
860 let mut current = self.arena.parent(captured);
861 while let Some(id) = current {
862 if self.is_gesture_dead_zone(id) {
867 break;
868 }
869 if self.widget_has_drag(id) {
870 {
873 let WidgetTree {
874 arena,
875 gesture_owners,
876 ..
877 } = self;
878 if let Some(node) = arena.get_mut(id) {
879 Self::ensure_gesture_arena(node, id, gesture_owners);
880 }
881 }
882 self.observe_drag_on_ancestor(id, down_event, ops);
883 observers.push(id);
884 }
885 current = self.arena.parent(id);
886 }
887 self.drag_observers = observers;
888 }
889
890 pub(super) fn release_drag_observers(
900 &mut self,
901 up_event: &WidgetEvent,
902 ops: &mut dyn crate::window::WindowOps,
903 ) {
904 if self.drag_observers.is_empty() {
905 return;
906 }
907 let observers = std::mem::take(&mut self.drag_observers);
908 for id in &observers {
909 self.observe_drag_on_ancestor(*id, up_event, ops);
913 }
914 }
915
916 pub(super) fn advance_drag_observers(
920 &mut self,
921 move_event: &WidgetEvent,
922 ops: &mut dyn crate::window::WindowOps,
923 ) {
924 if self.drag_observers.is_empty() {
925 return;
926 }
927 let observers = std::mem::take(&mut self.drag_observers);
928 for id in &observers {
929 let recognized = self.observe_drag_on_ancestor(*id, move_event, ops);
930 if recognized || self.active_drag.is_some() {
931 return;
933 }
934 }
935 self.drag_observers = observers;
937 }
938
939 fn observe_drag_on_ancestor(
945 &mut self,
946 id: WidgetId,
947 event: &WidgetEvent,
948 ops: &mut dyn crate::window::WindowOps,
949 ) -> bool {
950 let localized = self.localize_event(id, event);
951 let event = localized.as_ref().unwrap_or(event);
952 let raw = match event {
953 WidgetEvent::PointerDown {
954 position,
955 button,
956 modifiers,
957 } => crate::gesture::RawPointerEvent::Down {
958 position: *position,
959 button: *button,
960 modifiers: *modifiers,
961 },
962 WidgetEvent::PointerMove { position } => crate::gesture::RawPointerEvent::Move {
963 position: *position,
964 },
965 WidgetEvent::PointerUp {
966 position,
967 button,
968 modifiers,
969 } => crate::gesture::RawPointerEvent::Up {
970 position: *position,
971 button: *button,
972 modifiers: *modifiers,
973 },
974 _ => return false,
975 };
976 let mut ctx = self.make_event_context(&mut *ops);
977 let WidgetTree { arena, .. } = self;
978 let recognized = if let Some(node) = arena.get_mut(id) {
979 if let Some(arena_ref) = node.handlers.gesture_arena.as_mut() {
980 if let Some(gesture) = arena_ref.process(&raw) {
981 Self::dispatch_recognized_gesture(node, gesture, &mut ctx);
982 true
983 } else {
984 false
985 }
986 } else {
987 false
988 }
989 } else {
990 false
991 };
992 self.collect_from_ctx(ctx, id);
993 recognized
994 }
995
996 fn localize_event(&self, id: WidgetId, event: &WidgetEvent) -> Option<WidgetEvent> {
1008 match event {
1009 WidgetEvent::PointerDown {
1010 position,
1011 button,
1012 modifiers,
1013 } => Some(WidgetEvent::PointerDown {
1014 position: self.arena.local_pointer_position(id, *position),
1015 button: *button,
1016 modifiers: *modifiers,
1017 }),
1018 WidgetEvent::PointerUp {
1019 position,
1020 button,
1021 modifiers,
1022 } => Some(WidgetEvent::PointerUp {
1023 position: self.arena.local_pointer_position(id, *position),
1024 button: *button,
1025 modifiers: *modifiers,
1026 }),
1027 WidgetEvent::PointerMove { position } => Some(WidgetEvent::PointerMove {
1028 position: self.arena.local_pointer_position(id, *position),
1029 }),
1030 WidgetEvent::Gesture { gesture } => Some(WidgetEvent::Gesture {
1031 gesture: self.localize_gesture(id, gesture),
1032 }),
1033 _ => None,
1034 }
1035 }
1036
1037 fn localize_gesture(&self, id: WidgetId, gesture: &GestureEvent) -> GestureEvent {
1044 let loc = |p: teksilo_canvas::Point| self.arena.local_pointer_position(id, p);
1045 let tap = |t: &TapEvent| TapEvent::new(loc(t.position), t.button, t.modifiers);
1046 match gesture {
1047 GestureEvent::Tap(t) => GestureEvent::Tap(tap(t)),
1048 GestureEvent::DoubleTap(t) => GestureEvent::DoubleTap(tap(t)),
1049 GestureEvent::TripleTap(t) => GestureEvent::TripleTap(tap(t)),
1050 GestureEvent::LongPress(t) => GestureEvent::LongPress(tap(t)),
1051 GestureEvent::DragStarted { position, button } => GestureEvent::DragStarted {
1052 position: loc(*position),
1053 button: *button,
1054 },
1055 GestureEvent::DragMoved { position, delta } => GestureEvent::DragMoved {
1056 position: loc(*position),
1057 delta: *delta,
1058 },
1059 GestureEvent::DragEnded { position } => GestureEvent::DragEnded {
1060 position: loc(*position),
1061 },
1062 GestureEvent::PinchStarted { center } => GestureEvent::PinchStarted {
1063 center: loc(*center),
1064 },
1065 GestureEvent::PinchChanged {
1066 center,
1067 scale,
1068 rotation,
1069 } => GestureEvent::PinchChanged {
1070 center: loc(*center),
1071 scale: *scale,
1072 rotation: *rotation,
1073 },
1074 GestureEvent::PinchEnded => GestureEvent::PinchEnded,
1075 GestureEvent::Swipe {
1076 direction,
1077 velocity,
1078 } => GestureEvent::Swipe {
1079 direction: *direction,
1080 velocity: *velocity,
1081 },
1082 }
1083 }
1084
1085 pub(super) fn dispatch_to_widget_returning_handled(
1091 &mut self,
1092 target: WidgetId,
1093 event: &WidgetEvent,
1094 ops: &mut dyn crate::window::WindowOps,
1095 ) -> bool {
1096 if !self.arena.is_enabled(target) {
1097 return false;
1098 }
1099
1100 let mut ancestors = Vec::new();
1101 let mut current = self.arena.parent(target);
1102 while let Some(id) = current {
1103 ancestors.push(id);
1104 current = self.arena.parent(id);
1105 }
1106 ancestors.reverse();
1107
1108 let tap_owner: Option<WidgetId> = if matches!(
1126 event,
1127 WidgetEvent::PointerDown { .. } | WidgetEvent::PointerUp { .. }
1128 ) {
1129 let mut owner = None;
1130 let mut cur = Some(target);
1131 while let Some(id) = cur {
1132 if self.arena.get(id).is_some_and(|n| {
1133 n.any_handler(|h| h.on_tap.is_some() || h.on_long_press.is_some())
1134 }) {
1135 owner = Some(id);
1136 break;
1137 }
1138 cur = self.arena.parent(id);
1139 }
1140 owner
1141 } else {
1142 None
1143 };
1144
1145 for &id in &ancestors {
1146 let mut ctx = self.make_event_context(&mut *ops);
1147 ctx.press_claimed_by_interactive_child =
1148 tap_owner.is_some_and(|owner| owner != id && self.is_descendant_of(owner, id));
1149 let localized = self.localize_event(id, event);
1152 let event = localized.as_ref().unwrap_or(event);
1153 let response = if let Some(node) = self.arena.get_mut(id) {
1154 Self::try_handler_preview(node, event, &mut ctx).unwrap_or(EventResponse::Ignored)
1155 } else {
1156 EventResponse::Ignored
1157 };
1158 self.collect_from_ctx(ctx, id);
1159 if response == EventResponse::Handled {
1160 self.arena.mark_needs_paint(id);
1161 return true;
1162 }
1163 }
1164
1165 let needs_layout_on_handle = matches!(
1166 event,
1167 WidgetEvent::Scroll { .. } | WidgetEvent::ScrollIntoView { .. }
1168 );
1169 let mut current = Some(target);
1170 let mut is_target = true;
1171 while let Some(id) = current {
1172 let mut ctx = self.make_event_context(&mut *ops);
1173 ctx.press_claimed_by_interactive_child =
1174 tap_owner.is_some_and(|owner| owner != id && self.is_descendant_of(owner, id));
1175 let localized = self.localize_event(id, event);
1178 let WidgetTree {
1179 arena,
1180 gesture_owners,
1181 ..
1182 } = self;
1183 let event = localized.as_ref().unwrap_or(event);
1184 let response = if let Some(node) = arena.get_mut(id) {
1185 Self::try_handler_bubble(node, event, &mut ctx, is_target, id, gesture_owners)
1186 .unwrap_or(EventResponse::Ignored)
1187 } else {
1188 EventResponse::Ignored
1189 };
1190 self.collect_from_ctx(ctx, id);
1191 if response == EventResponse::Handled {
1192 if needs_layout_on_handle {
1193 self.arena.mark_needs_layout(id);
1194 } else {
1195 self.arena.mark_needs_paint(id);
1196 }
1197 if !matches!(event, WidgetEvent::PointerEnter | WidgetEvent::PointerLeave) {
1212 return true;
1213 }
1214 }
1215 is_target = false;
1216 current = self.arena.parent(id);
1217 }
1218 false
1219 }
1220
1221 pub(super) fn dispatch_to_widget_direct(
1222 &mut self,
1223 target: WidgetId,
1224 event: &WidgetEvent,
1225 ops: &mut dyn crate::window::WindowOps,
1226 ) {
1227 if !self.arena.is_enabled(target) {
1228 return;
1229 }
1230
1231 let mut ctx = self.make_event_context(&mut *ops);
1232 let WidgetTree {
1233 arena,
1234 gesture_owners,
1235 ..
1236 } = self;
1237 let response = if let Some(node) = arena.get_mut(target) {
1238 Self::try_handler_bubble(node, event, &mut ctx, true, target, gesture_owners)
1239 .unwrap_or(EventResponse::Ignored)
1240 } else {
1241 EventResponse::Ignored
1242 };
1243 self.collect_from_ctx(ctx, target);
1244
1245 if response == EventResponse::Handled {
1246 self.arena.mark_needs_paint(target);
1247 }
1248 }
1249
1250 fn try_handler_preview(
1251 node: &mut crate::arena::WidgetNode,
1252 event: &WidgetEvent,
1253 ctx: &mut EventContext,
1254 ) -> Option<EventResponse> {
1255 match event {
1256 WidgetEvent::KeyDown { .. }
1264 | WidgetEvent::KeyUp { .. }
1265 | WidgetEvent::ImeComposition { .. }
1266 | WidgetEvent::ImeCommit { .. } => {
1267 let has = node.external_handlers.on_key_preview.is_some()
1268 || node.handlers.on_key_preview.is_some();
1269 if !has {
1270 return None;
1271 }
1272 Some(fire_event_handler_both(
1273 &mut node.external_handlers.on_key_preview,
1274 &mut node.handlers.on_key_preview,
1275 event,
1276 ctx,
1277 ))
1278 }
1279 WidgetEvent::PointerEnter | WidgetEvent::PointerLeave => None,
1291 _ => {
1292 let has = node.external_handlers.on_pointer_event.is_some()
1293 || node.handlers.on_pointer_event.is_some();
1294 if !has {
1295 return None;
1296 }
1297 Some(fire_event_handler_both(
1298 &mut node.external_handlers.on_pointer_event,
1299 &mut node.handlers.on_pointer_event,
1300 event,
1301 ctx,
1302 ))
1303 }
1304 }
1305 }
1306
1307 fn try_handler_bubble(
1316 node: &mut crate::arena::WidgetNode,
1317 event: &WidgetEvent,
1318 ctx: &mut EventContext,
1319 fire_on_pointer_event: bool,
1320 node_id: WidgetId,
1321 gesture_owners: &mut std::collections::HashSet<WidgetId>,
1322 ) -> Option<EventResponse> {
1323 match event {
1324 WidgetEvent::PointerEnter => {
1325 if let Some(cursor) = node.node_cursor {
1326 ctx.set_cursor(cursor);
1327 }
1328 let mut fired = false;
1329 if let Some(h) = node.external_handlers.on_hover.as_mut() {
1330 h(true, ctx);
1331 fired = true;
1332 }
1333 if let Some(h) = node.handlers.on_hover.as_mut() {
1334 h(true, ctx);
1335 fired = true;
1336 }
1337 if fired {
1338 Some(EventResponse::Handled)
1339 } else {
1340 node.node_cursor.map(|_| EventResponse::Handled)
1341 }
1342 }
1343 WidgetEvent::PointerLeave => {
1344 if node.node_cursor.is_some() {
1345 ctx.set_cursor(crate::widget::CursorIcon::Default);
1346 }
1347 let mut fired = false;
1348 if let Some(h) = node.external_handlers.on_hover.as_mut() {
1349 h(false, ctx);
1350 fired = true;
1351 }
1352 if let Some(h) = node.handlers.on_hover.as_mut() {
1353 h(false, ctx);
1354 fired = true;
1355 }
1356 if fired {
1357 Some(EventResponse::Handled)
1358 } else {
1359 node.node_cursor.map(|_| EventResponse::Handled)
1360 }
1361 }
1362 WidgetEvent::FocusGained { .. } => {
1363 let mut fired = false;
1364 if let Some(h) = node.external_handlers.on_focus.as_mut() {
1365 h(true, ctx);
1366 fired = true;
1367 }
1368 if let Some(h) = node.handlers.on_focus.as_mut() {
1369 h(true, ctx);
1370 fired = true;
1371 }
1372 fired.then_some(EventResponse::Handled)
1373 }
1374 WidgetEvent::FocusLost => {
1375 let mut fired = false;
1376 if let Some(h) = node.external_handlers.on_focus.as_mut() {
1377 h(false, ctx);
1378 fired = true;
1379 }
1380 if let Some(h) = node.handlers.on_focus.as_mut() {
1381 h(false, ctx);
1382 fired = true;
1383 }
1384 fired.then_some(EventResponse::Handled)
1385 }
1386 WidgetEvent::KeyDown { .. }
1387 | WidgetEvent::KeyUp { .. }
1388 | WidgetEvent::ImeComposition { .. }
1389 | WidgetEvent::ImeCommit { .. } => {
1390 if node.external_handlers.on_key.is_some() || node.handlers.on_key.is_some() {
1391 Some(fire_event_handler_both(
1392 &mut node.external_handlers.on_key,
1393 &mut node.handlers.on_key,
1394 event,
1395 ctx,
1396 ))
1397 } else {
1398 None
1399 }
1400 }
1401 WidgetEvent::Scroll { .. } | WidgetEvent::ScrollIntoView { .. } => {
1402 if node.external_handlers.on_scroll.is_some() || node.handlers.on_scroll.is_some() {
1403 Some(fire_event_handler_both(
1404 &mut node.external_handlers.on_scroll,
1405 &mut node.handlers.on_scroll,
1406 event,
1407 ctx,
1408 ))
1409 } else {
1410 None
1411 }
1412 }
1413 WidgetEvent::AccessAction {
1414 action,
1415 target_node,
1416 data,
1417 ..
1418 } => {
1419 let saved_a11y_source = ctx
1429 .current_source
1430 .replace(crate::telemetry::IntentSource::Accessibility);
1431 let request_is_set = node.handlers.on_access_action_request.is_some()
1432 || node.external_handlers.on_access_action_request.is_some();
1433 let user_handled = if request_is_set {
1434 let r1 = node
1435 .external_handlers
1436 .on_access_action_request
1437 .as_mut()
1438 .map(|h| h(*action, *target_node, data.clone(), ctx))
1439 .unwrap_or(EventResponse::Ignored);
1440 let r2 = node
1441 .handlers
1442 .on_access_action_request
1443 .as_mut()
1444 .map(|h| h(*action, *target_node, data.clone(), ctx))
1445 .unwrap_or(EventResponse::Ignored);
1446 Some(
1447 if r1 == EventResponse::Handled || r2 == EventResponse::Handled {
1448 EventResponse::Handled
1449 } else {
1450 EventResponse::Ignored
1451 },
1452 )
1453 } else if node.handlers.on_access_action.is_some()
1454 || node.external_handlers.on_access_action.is_some()
1455 {
1456 let r1 = node
1457 .external_handlers
1458 .on_access_action
1459 .as_mut()
1460 .map(|h| h(*action, ctx))
1461 .unwrap_or(EventResponse::Ignored);
1462 let r2 = node
1463 .handlers
1464 .on_access_action
1465 .as_mut()
1466 .map(|h| h(*action, ctx))
1467 .unwrap_or(EventResponse::Ignored);
1468 Some(
1469 if r1 == EventResponse::Handled || r2 == EventResponse::Handled {
1470 EventResponse::Handled
1471 } else {
1472 EventResponse::Ignored
1473 },
1474 )
1475 } else {
1476 None
1477 };
1478
1479 let mut override_handled = false;
1485 if let Some(ov) = node.access_overrides.as_deref_mut() {
1486 if matches!(action, accesskit::Action::CustomAction) {
1487 if let Some(accesskit::ActionData::CustomAction(idx)) = data
1488 && let Some((_, cb)) = ov.custom_actions.get_mut(*idx as usize)
1489 {
1490 cb(ctx);
1491 override_handled = true;
1492 }
1493 } else {
1494 for (a, cb) in ov.actions.iter_mut() {
1495 if *a == *action {
1496 cb(ctx);
1497 override_handled = true;
1498 }
1499 }
1500 }
1501 }
1502
1503 ctx.current_source = saved_a11y_source;
1504 match (user_handled, override_handled) {
1505 (Some(EventResponse::Handled), _) | (_, true) => Some(EventResponse::Handled),
1506 (Some(EventResponse::Ignored), false) => Some(EventResponse::Ignored),
1507 (None, false) => None,
1508 }
1509 }
1510 WidgetEvent::Gesture { gesture } => {
1511 let matched = matches!(
1515 gesture,
1516 GestureEvent::PinchStarted { .. }
1517 | GestureEvent::PinchChanged { .. }
1518 | GestureEvent::PinchEnded
1519 | GestureEvent::Swipe { .. }
1520 | GestureEvent::DoubleTap { .. }
1521 | GestureEvent::TripleTap { .. }
1522 ) && {
1523 let has_handler = match gesture {
1524 GestureEvent::PinchStarted { .. }
1525 | GestureEvent::PinchChanged { .. }
1526 | GestureEvent::PinchEnded => node.any_handler(|h| h.on_pinch.is_some()),
1527 GestureEvent::Swipe { .. } => node.any_handler(|h| h.on_swipe.is_some()),
1528 GestureEvent::DoubleTap { .. } => {
1529 node.any_handler(|h| h.on_double_tap.is_some())
1530 }
1531 GestureEvent::TripleTap { .. } => {
1532 node.any_handler(|h| h.on_triple_tap.is_some())
1533 }
1534 _ => false,
1535 };
1536 if has_handler {
1537 Self::dispatch_recognized_gesture(node, *gesture, ctx);
1538 }
1539 has_handler
1540 };
1541 if matched {
1542 Some(EventResponse::Handled)
1543 } else {
1544 None
1545 }
1546 }
1547 WidgetEvent::PointerDown {
1548 position,
1549 button,
1550 modifiers,
1551 } => {
1552 if fire_on_pointer_event {
1559 let r = fire_event_handler_both(
1560 &mut node.external_handlers.on_pointer_event,
1561 &mut node.handlers.on_pointer_event,
1562 event,
1563 ctx,
1564 );
1565 if r == EventResponse::Handled {
1566 return Some(EventResponse::Handled);
1567 }
1568 }
1569 Self::ensure_gesture_arena(node, node_id, gesture_owners);
1570 if let Some(arena) = node.handlers.gesture_arena.as_mut() {
1571 ctx.capture_pointer();
1580 let result = arena.process(&RawPointerEvent::Down {
1581 position: *position,
1582 button: *button,
1583 modifiers: *modifiers,
1584 });
1585 if let Some(gesture) = result {
1586 Self::dispatch_recognized_gesture(node, gesture, ctx);
1587 }
1588 return Some(EventResponse::Handled);
1589 }
1590 None
1591 }
1592 WidgetEvent::PointerUp {
1593 position,
1594 button,
1595 modifiers,
1596 } => {
1597 if fire_on_pointer_event {
1598 let r = fire_event_handler_both(
1599 &mut node.external_handlers.on_pointer_event,
1600 &mut node.handlers.on_pointer_event,
1601 event,
1602 ctx,
1603 );
1604 if r == EventResponse::Handled {
1605 return Some(EventResponse::Handled);
1606 }
1607 }
1608 if let Some(arena) = node.handlers.gesture_arena.as_mut() {
1609 let result = arena.process(&RawPointerEvent::Up {
1610 position: *position,
1611 button: *button,
1612 modifiers: *modifiers,
1613 });
1614 if let Some(gesture) = result {
1615 Self::dispatch_recognized_gesture(node, gesture, ctx);
1616 }
1617 return Some(EventResponse::Handled);
1618 }
1619 None
1620 }
1621 WidgetEvent::PointerMove { position } => {
1622 if fire_on_pointer_event {
1623 let r = fire_event_handler_both(
1624 &mut node.external_handlers.on_pointer_event,
1625 &mut node.handlers.on_pointer_event,
1626 event,
1627 ctx,
1628 );
1629 if r == EventResponse::Handled {
1630 return Some(EventResponse::Handled);
1631 }
1632 }
1633 if let Some(arena) = node.handlers.gesture_arena.as_mut() {
1634 let result = arena.process(&RawPointerEvent::Move {
1635 position: *position,
1636 });
1637 if let Some(gesture) = result {
1638 Self::dispatch_recognized_gesture(node, gesture, ctx);
1639 return Some(EventResponse::Handled);
1651 }
1652 return Some(EventResponse::Ignored);
1653 }
1654 None
1655 }
1656 }
1657 }
1658
1659 pub(super) fn collect_from_ctx<'ops>(
1660 &mut self,
1661 mut ctx: EventContext<'ops>,
1662 source_widget: WidgetId,
1663 ) {
1664 let local_ops = ctx.window_ops.take();
1670 let mut noop = crate::window::NoopWindowOps;
1671 let ops: &mut dyn crate::window::WindowOps = match local_ops {
1672 Some(o) => o,
1673 None => &mut noop,
1674 };
1675 if ctx.frame_requested {
1676 self.request_frame();
1677 }
1678 if let Some(cursor) = ctx.cursor_request {
1679 self.current_cursor = cursor;
1680 }
1681 for intent in ctx.pending_intents {
1687 self.enqueue_intent(source_widget, intent, true);
1688 }
1689 if ctx.cancel_key_capture {
1698 self.cancel_key_capture();
1699 }
1700 if let Some(slot) = ctx.pending_key_capture {
1701 self.key_capture = Some(slot);
1702 }
1703 for mutation in ctx.pending_shortcut_mutations {
1705 match mutation {
1706 crate::widget::ShortcutMutation::RebindPrimary { id, keystroke } => {
1707 self.shortcut_registry.rebind_primary(id, keystroke);
1708 }
1709 crate::widget::ShortcutMutation::RebindSecondary { id, keystroke } => {
1710 self.shortcut_registry.rebind_secondary(id, keystroke);
1711 }
1712 crate::widget::ShortcutMutation::ClearOverride { id } => {
1713 self.shortcut_registry.clear_override(&id);
1714 }
1715 }
1716 }
1717 if ctx.close_window_requested {
1718 self.close_window_requested = true;
1719 }
1720 if ctx.force_close_requested {
1721 self.force_close_requested = true;
1722 }
1723 self.pending_modal_requests
1724 .extend(ctx.modal_requests.into_iter().map(|request| {
1725 crate::modal::QueuedModalRequest {
1726 source_widget,
1727 request,
1728 }
1729 }));
1730 if ctx.dismiss_modal && !self.dismiss_modal_for_source(source_widget, &mut *ops) {
1731 self.pending_modal_dismissal = true;
1732 }
1733 for callback in ctx.idle_callbacks {
1734 self.idle_queue.push_boxed(callback);
1735 }
1736 match ctx.dismiss_scope {
1737 Some(crate::widget::DismissScope::All) => {
1738 let dismissed = self.overlay_manager.dismiss_all();
1739 self.dormant_dismissed_content(&dismissed, &mut *ops);
1740 }
1741 Some(crate::widget::DismissScope::AllExceptHosts) => {
1742 self.dismiss_all_overlays_except_hosts(&mut *ops);
1743 }
1744 Some(crate::widget::DismissScope::SelfChain) => {
1745 self.dismiss_self_overlay_chain_for_source(source_widget, &mut *ops);
1746 }
1747 Some(crate::widget::DismissScope::Top) => {
1748 if let Some((_id, content_ids, focus_restore)) = self.overlay_manager.dismiss_top()
1749 {
1750 self.dormant_dismissed_content(&content_ids, &mut *ops);
1751 if let Some(restore_id) = focus_restore
1752 && self.arena.is_active(restore_id)
1753 {
1754 self.focus_ops(restore_id, &mut *ops);
1755 }
1756 }
1757 }
1758 None => {
1759 for id in ctx.overlay_dismissals {
1760 let dismissed = self.overlay_manager.dismiss(id);
1761 self.dormant_dismissed_content(&dismissed, &mut *ops);
1762 }
1763 }
1764 }
1765 for content_id in ctx.overlay_content_dismissals {
1772 if let Some(overlay_id) = self.overlay_manager.find_by_content(content_id) {
1773 let dismissed = self.overlay_manager.dismiss(overlay_id);
1774 self.dormant_dismissed_content(&dismissed, &mut *ops);
1775 }
1776 }
1777 for (id, pause) in ctx.overlay_pause_requests {
1784 if pause {
1785 self.overlay_manager.pause_auto_dismiss(id);
1786 } else {
1787 self.overlay_manager.resume_auto_dismiss(id);
1788 }
1789 }
1790 for preserve_content in ctx.dismiss_descendant_overlays {
1791 self.dismiss_child_overlays_for_source(source_widget, preserve_content, &mut *ops);
1792 }
1793 self.apply_tree_mutations(std::mem::take(&mut ctx.tree_mutations));
1794 if ctx.request_a11y_update {
1795 self.a11y_dirty = true;
1796 }
1797 for (message, politeness) in std::mem::take(&mut ctx.announcements) {
1800 self.announce_with(message, politeness);
1801 }
1802 for mut req in ctx.overlay_requests {
1803 if req.parent_overlay.is_none() {
1804 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
1805 }
1806 if self
1807 .overlay_manager
1808 .find_by_content(req.content_id)
1809 .is_some()
1810 {
1811 continue;
1812 }
1813 let current_focus = self.focused;
1814 self.overlay_manager.show(req);
1815 self.a11y_dirty = true;
1819 if let Some(focus_id) = current_focus {
1820 self.overlay_manager.set_top_focus_restore(focus_id);
1821 }
1822 }
1823 for (mut req, duration) in ctx.timed_overlay_requests {
1824 if req.parent_overlay.is_none() {
1825 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
1826 }
1827 if self
1828 .overlay_manager
1829 .find_by_content(req.content_id)
1830 .is_some()
1831 {
1832 continue;
1833 }
1834 let current_focus = self.focused;
1835 let overlay_id = self.overlay_manager.show_for(req, duration);
1836 self.overlay_manager
1837 .set_shown_at_sim(overlay_id, self.sim_clock);
1838 self.a11y_dirty = true;
1839 if let Some(focus_id) = current_focus {
1840 self.overlay_manager.set_top_focus_restore(focus_id);
1841 }
1842 }
1843 for (mut req, progress, duration) in ctx.reveal_overlay_requests {
1844 if req.parent_overlay.is_none() {
1845 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
1846 }
1847 if self
1848 .overlay_manager
1849 .find_by_content(req.content_id)
1850 .is_some()
1851 {
1852 continue;
1853 }
1854 let content_id = req.content_id;
1855 let current_focus = self.focused;
1856 let overlay_id = self.overlay_manager.show(req);
1857 self.overlay_manager
1858 .set_shown_at_sim(overlay_id, self.sim_clock);
1859 self.a11y_dirty = true;
1860 if let Some(focus_id) = current_focus {
1861 self.overlay_manager.set_top_focus_restore(focus_id);
1862 }
1863 self.register_animated_signal(&progress, content_id);
1869 let _ = progress.try_animate_with_options(crate::animation::AnimationRequest {
1870 target: 1.0,
1871 duration,
1872 easing: teksilo_tokens::Easing::EaseOut,
1873 frame_interval: None,
1874 looping: false,
1875 epsilon: 0.0,
1876 max_duration: None,
1877 });
1878 self.overlay_manager
1879 .attach_fade(overlay_id, progress, duration);
1880 }
1881 if let Some(capture) = ctx.pointer_capture {
1882 if capture {
1883 self.pointer_captured_by = Some(source_widget);
1884 } else {
1885 self.pointer_captured_by = None;
1886 }
1887 }
1888 for (mut request, delay, focus_target) in ctx.delayed_overlay_requests {
1889 if request.parent_overlay.is_none() {
1890 request.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
1891 }
1892 if self
1893 .overlay_manager
1894 .find_by_content(request.content_id)
1895 .is_some()
1896 {
1897 continue;
1898 }
1899 let content_id = request.content_id;
1900 self.pending_delayed_overlays
1901 .retain(|pending| pending.request.content_id != content_id);
1902 self.pending_delayed_overlays.push(PendingDelayedOverlay {
1903 request,
1904 delay,
1905 focus_target,
1906 real_requested_at: std::time::Instant::now(),
1907 sim_requested_at: self.sim_clock,
1908 });
1909 self.arena.mark_needs_paint(source_widget);
1910 }
1911 for content_id in ctx.cancel_delayed_overlays {
1912 self.pending_delayed_overlays
1913 .retain(|pending| pending.request.content_id != content_id);
1914 }
1915 for id in ctx.repaint_requests {
1916 self.arena.mark_needs_paint(id);
1917 }
1918 for id in ctx.synthetic_clicks {
1919 self.synthesise_tap_with_ops(id, &mut *ops);
1924 }
1925 if let Some(&id) = ctx.focus_requests.last() {
1926 let target = self.first_focusable_descendant(id).unwrap_or(id);
1936 self.focus_ops(target, &mut *ops);
1937 }
1938 if let Some(&id) = ctx.focus_into_requests.last() {
1939 if let Some(target) = self.first_focusable_descendant(id) {
1946 self.focus_ops(target, &mut *ops);
1947 }
1948 }
1949
1950 for req in ctx.scroll_into_view_requests {
1959 self.scroll_rect_into_view(
1960 req.from.unwrap_or(source_widget),
1963 req.rect,
1964 req.margin,
1965 req.align,
1966 req.motion,
1967 &mut *ops,
1968 );
1969 }
1970 for (id, margin) in ctx.scroll_widget_into_view_requests {
1977 if self.arena.get(id).is_some() {
1978 let bounds = self.arena.bounds(id);
1979 self.scroll_rect_into_view(
1980 id,
1981 bounds,
1982 margin,
1983 crate::event::ScrollAlign::Minimal,
1984 crate::event::ScrollMotion::Instant,
1985 &mut *ops,
1986 );
1987 }
1988 }
1989
1990 if let Some(&id) = ctx.highlight_tooltip_requests.last() {
1995 self.show_highlight_tooltip(id, &mut *ops);
1996 }
1997
1998 if let Some((source_widget, payload, preview_widget)) = ctx.drag_start_request {
2000 let (preview_content_id, preview_overlay_id) = if let Some(preview) = preview_widget {
2001 let content_id = self.add_boxed(preview);
2008 let overlay_id = self.overlay_manager.show(crate::overlay::OverlayRequest {
2009 content_id,
2010 anchor: source_widget,
2011 placement: crate::overlay::OverlayPlacement::AtPointer(
2012 teksilo_canvas::Point::ZERO,
2013 ),
2014 dismiss: crate::overlay::DismissBehavior::Manual,
2015 layer: crate::overlay::OverlayLayer::InTree,
2016 parent_overlay: None,
2017 on_dismiss: None,
2018 fade_duration: None,
2019 });
2020 self.arena.mark_needs_layout(content_id);
2024 (Some(content_id), Some(overlay_id))
2025 } else {
2026 (None, None)
2027 };
2028 self.active_drag = Some(crate::drag_state::DragSession {
2029 payload,
2030 source_widget: Some(source_widget),
2031 is_external: false,
2032 current_position: teksilo_canvas::Point::ZERO,
2033 current_target: None,
2034 feedback: crate::drag_state::DropFeedback::NoFeedback,
2035 preview_content_id,
2036 preview_overlay_id,
2037 });
2038 self.pointer_captured_by = Some(source_widget);
2039 self.current_cursor = crate::widget::CursorIcon::Grabbing;
2042 }
2043 if ctx.cancel_drag {
2044 self.cancel_active_drag(&mut *ops);
2045 }
2046
2047 if let Some(theme) = ctx.theme_request {
2049 self.pending_theme_request = Some(theme);
2055 }
2056 if ctx.follow_system_request {
2057 self.pending_follow_system_request = true;
2061 }
2062 if let Some(locale) = ctx.locale_request {
2063 self.pending_locale_request = Some(locale);
2069 }
2070 if let Some(scale) = ctx.text_scale_request {
2071 self.pending_text_scale_request = Some(scale);
2076 }
2077 }
2078
2079 fn apply_tree_mutations(&mut self, mutations: Vec<crate::widget::TreeMutation>) {
2080 use crate::binding::BindingLevel;
2081 use crate::widget::TreeMutation;
2082
2083 for mutation in mutations {
2084 match mutation {
2085 TreeMutation::SetDormant(id) => self.arena.set_dormant(id),
2086 TreeMutation::Activate(id) => self.arena.activate(id),
2087 TreeMutation::Destroy(id) => {
2088 if let Some(overlay_id) = self.overlay_manager().find_by_content(id) {
2101 self.dismiss_overlay(overlay_id);
2102 }
2103 self.destroy_subtree(id);
2104 }
2105 TreeMutation::MaterializeNow(id) => {
2109 if self.arena.get(id).is_some() {
2110 self.rebuild_single_widget(id);
2111 }
2112 }
2113 TreeMutation::WithWidgetMut { id, dirty, apply } => {
2114 let existed = if let Some(any) =
2119 self.arena.get_mut(id).and_then(|n| n.widget.as_any_mut())
2120 {
2121 apply(any);
2122 true
2123 } else {
2124 false
2125 };
2126 if existed {
2127 match dirty {
2128 BindingLevel::RepaintOnly => self.arena.mark_needs_paint(id),
2129 BindingLevel::SubtreeRepaint => self.arena.mark_subtree_needs_paint(id),
2130 BindingLevel::Relayout => {
2131 self.arena.mark_needs_layout(id);
2132 self.arena.mark_ancestors_need_layout(id);
2133 }
2134 BindingLevel::Rebuild => {
2135 self.arena.mark_needs_rebuild(id);
2136 self.arena.mark_ancestors_need_layout(id);
2137 }
2138 BindingLevel::AccessibilityOnly => self.a11y_dirty = true,
2139 }
2140 }
2141 }
2142 }
2143 }
2144 }
2145
2146 pub fn hit_test(&self, point: Point) -> Option<WidgetId> {
2147 self.hit_test_excluding_overlay_and_widget(point, None, None)
2148 }
2149
2150 pub fn hit_test_excluding_overlay_and_widget(
2154 &self,
2155 point: Point,
2156 exclude_overlay: Option<crate::overlay::OverlayId>,
2157 exclude_widget: Option<WidgetId>,
2158 ) -> Option<WidgetId> {
2159 if let Some(overlay_id) = self.overlay_manager.hit_test(point) {
2160 if Some(overlay_id) == exclude_overlay {
2161 } else if let Some(overlay) = self.overlay_manager.overlay(overlay_id) {
2163 return self.arena.hit_test_in_subtree_excluding(
2164 overlay.content_id,
2165 point,
2166 exclude_widget,
2167 );
2168 }
2169 }
2170
2171 if self.overlay_manager.topmost_centered().is_some() {
2172 return None;
2173 }
2174
2175 self.arena.hit_test_at(point, exclude_widget)
2178 }
2179}
2180
2181fn is_context_menu_chord(key: Key, modifiers: Modifiers) -> bool {
2199 match key {
2200 Key::ContextMenu => modifiers == Modifiers::NONE,
2201 Key::F10 => modifiers == Modifiers::SHIFT,
2202 #[cfg(target_os = "macos")]
2203 Key::M => modifiers == Modifiers::CTRL | Modifiers::SHIFT,
2204 _ => false,
2205 }
2206}
2207
2208#[cfg(test)]
2209mod tests {
2210 use super::*;
2211 use crate::test_widgets::FillWidget;
2212 use crate::widget::CursorIcon;
2213 use crate::widget_builder::WidgetBuilder;
2214
2215 #[test]
2216 fn pointer_enter_leave_synthesized() {
2217 let mut tree = WidgetTree::new();
2218 let widget = tree.add(FillWidget::new());
2219 tree.layout(SizeProposal::exact(100.0, 50.0));
2220 tree.pointer_move(Point::new(50.0, 25.0));
2221 assert_eq!(tree.hovered, Some(widget));
2222 tree.pointer_move(Point::new(200.0, 200.0));
2223 assert_eq!(tree.hovered, None);
2224 }
2225
2226 #[test]
2227 fn pointer_hover_updates_current_cursor() {
2228 let mut tree = WidgetTree::new();
2229 tree.add(FillWidget::new().cursor(CursorIcon::ColResize));
2230 tree.layout(SizeProposal::exact(100.0, 50.0));
2231
2232 tree.pointer_move(Point::new(50.0, 25.0));
2233 assert_eq!(tree.current_cursor(), CursorIcon::ColResize);
2234
2235 tree.pointer_move(Point::new(200.0, 200.0));
2236 assert_eq!(tree.current_cursor(), CursorIcon::Default);
2237 }
2238
2239 #[derive(Debug)]
2242 struct Bumpable {
2243 value: i32,
2244 }
2245
2246 impl crate::widget::Widget for Bumpable {
2247 fn layout_response(
2248 &self,
2249 proposal: SizeProposal,
2250 _ctx: &crate::widget::LayoutContext,
2251 ) -> crate::widget::LayoutResponse {
2252 proposal.resolve(10.0, 10.0).into()
2253 }
2254 fn as_any(&self) -> Option<&dyn std::any::Any> {
2255 Some(self)
2256 }
2257 fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
2258 Some(self)
2259 }
2260 }
2261
2262 #[test]
2263 fn with_widget_mut_applies_and_dirty_marks() {
2264 let mut tree = WidgetTree::new();
2265 let id = tree.add(Bumpable { value: 0 });
2266 tree.layout(SizeProposal::exact(100.0, 100.0));
2267
2268 let mut ctx = EventContext::new();
2269 ctx.with_widget_mut::<Bumpable>(id, crate::binding::BindingLevel::Relayout, |b| {
2270 b.value = 42;
2271 });
2272 tree.collect_from_ctx(ctx, id);
2273
2274 let value = tree
2275 .widget_as_any(id)
2276 .and_then(|a| a.downcast_ref::<Bumpable>())
2277 .map(|b| b.value);
2278 assert_eq!(
2279 value,
2280 Some(42),
2281 "the deferred closure must mutate the live widget"
2282 );
2283 assert!(
2284 tree.needs_layout(),
2285 "Relayout dirty level must mark the tree for relayout"
2286 );
2287 }
2288
2289 #[test]
2290 #[cfg(debug_assertions)]
2291 #[should_panic(expected = "not the requested type")]
2292 fn with_widget_mut_wrong_type_panics_in_debug() {
2293 struct Other;
2294 let mut tree = WidgetTree::new();
2295 let id = tree.add(Bumpable { value: 0 });
2296 let mut ctx = EventContext::new();
2297 ctx.with_widget_mut::<Other>(
2298 id,
2299 crate::binding::BindingLevel::RepaintOnly,
2300 |_o: &mut Other| {},
2301 );
2302 tree.collect_from_ctx(ctx, id);
2305 }
2306
2307 #[test]
2308 fn with_widget_mut_closure_may_fire_observed_signals() {
2309 use crate::signal::Signal;
2317 let mut tree = WidgetTree::new();
2318 let id = tree.add(Bumpable { value: 0 });
2319 tree.layout(SizeProposal::exact(100.0, 100.0));
2320
2321 let trigger = Signal::new(0_u64);
2322 let echo = Signal::new(0_u64);
2323 let echo_for_obs = echo.clone();
2324 let _obs = trigger.observe(move |v| echo_for_obs.set(*v));
2325
2326 let trigger_in = trigger.clone();
2327 let mut ctx = EventContext::new();
2328 ctx.with_widget_mut::<Bumpable>(id, crate::binding::BindingLevel::RepaintOnly, move |b| {
2329 b.value = 7;
2330 trigger_in.set(99);
2332 });
2333 tree.collect_from_ctx(ctx, id); assert_eq!(
2336 echo.get(),
2337 99,
2338 "the observer ran during the deferred mutation"
2339 );
2340 let value = tree
2341 .widget_as_any(id)
2342 .and_then(|a| a.downcast_ref::<Bumpable>())
2343 .map(|b| b.value);
2344 assert_eq!(value, Some(7));
2345 }
2346
2347 #[test]
2348 fn request_accessibility_update_forces_rewalk() {
2349 let mut tree = WidgetTree::new();
2350 let id = tree.add(Bumpable { value: 0 });
2351 tree.layout(SizeProposal::exact(100.0, 100.0));
2352 let _ = tree.sync_accessibility(); assert!(
2354 !tree.a11y_dirty,
2355 "sync_accessibility should clear the dirty flag"
2356 );
2357
2358 let mut ctx = EventContext::new();
2359 ctx.request_accessibility_update();
2360 tree.collect_from_ctx(ctx, id);
2361 assert!(
2362 tree.a11y_dirty,
2363 "request_accessibility_update must force an AT re-walk"
2364 );
2365 }
2366
2367 #[test]
2368 fn rebuild_dirties_accessibility_tree() {
2369 let mut tree = WidgetTree::new();
2376 let id = tree.add(Bumpable { value: 0 });
2377 tree.layout(SizeProposal::exact(100.0, 100.0));
2378 let _ = tree.sync_accessibility(); assert!(
2380 !tree.a11y_dirty,
2381 "sync_accessibility should clear the dirty flag"
2382 );
2383
2384 tree.arena_mark_needs_rebuild_for_testing(id);
2387 tree.layout(SizeProposal::exact(100.0, 100.0));
2388 assert!(
2389 tree.a11y_dirty,
2390 "a rebuild must dirty the AT tree so the next sync re-walks"
2391 );
2392 }
2393
2394 #[test]
2395 fn bound_access_label_change_dirties_accessibility_tree() {
2396 use crate::signal::Signal;
2397 use crate::test_widgets::FillWidget;
2398 use crate::widget_builder::WidgetBuilder;
2399
2400 let label = Signal::new("first".to_string());
2406 let mut tree = WidgetTree::new();
2407 let _id = tree.add(FillWidget::new().access_label(label.clone()));
2408 tree.layout(SizeProposal::exact(100.0, 100.0));
2409 let _ = tree.sync_accessibility(); assert!(!tree.a11y_dirty, "sync_accessibility should clear the flag");
2411
2412 label.set("second".to_string());
2413 tree.layout(SizeProposal::exact(100.0, 100.0));
2414 assert!(
2415 tree.a11y_dirty,
2416 "changing a bound access_label must dirty the AT tree"
2417 );
2418 }
2419
2420 #[test]
2421 fn disabled_ancestor_blocks_event_to_descendant() {
2422 use crate::signal::Signal;
2423 use crate::test_widgets::StackWidget;
2424 use std::cell::Cell;
2425 use std::rc::Rc;
2426
2427 let tapped = Rc::new(Cell::new(false));
2428 let flag = tapped.clone();
2429 let enabled = Signal::new(true);
2430
2431 let mut tree = WidgetTree::new();
2432 let child = tree.add(FillWidget::new().on_tap(move |_pos, _ctx| {
2433 flag.set(true);
2434 }));
2435 let parent = tree.add(StackWidget::new().add_child(child));
2436 tree.enabled_when(parent, enabled.clone());
2437 tree.layout(SizeProposal::exact(100.0, 50.0));
2438
2439 enabled.set(false);
2440 tree.click(child);
2441 assert!(
2442 !tapped.get(),
2443 "disabled ancestor should block descendant tap"
2444 );
2445
2446 enabled.set(true);
2447 tree.click(child);
2448 assert!(tapped.get(), "re-enabling should restore dispatch");
2449 }
2450
2451 #[test]
2452 fn pointer_positions_are_widget_local_at_nonzero_origin() {
2453 use crate::event::{Modifiers, PointerButton};
2454 use crate::test_widgets::InsetWidget;
2455 use std::cell::Cell;
2456 use std::rc::Rc;
2457
2458 let tap_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
2460 let down_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
2461 let drag_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
2462 let (tp, dp, gp) = (tap_pos.clone(), down_pos.clone(), drag_pos.clone());
2463
2464 let mut tree = WidgetTree::new();
2465 let child = tree.add(
2466 FillWidget::new()
2467 .on_tap(move |ev, _ctx| tp.set(Some(ev.position)))
2468 .on_pointer_event(move |ev, _ctx| {
2469 if let WidgetEvent::PointerDown { position, .. } = ev {
2470 dp.set(Some(*position));
2471 }
2472 crate::event::EventResponse::Ignored
2473 })
2474 .on_drag(move |phase, _ctx| {
2475 use crate::gesture::DragPhase;
2476 match phase {
2477 DragPhase::Started { position, .. }
2478 | DragPhase::Moved { position, .. }
2479 | DragPhase::Ended { position } => gp.set(Some(position)),
2480 }
2481 }),
2482 );
2483 let inset = tree.add(InsetWidget::new(20.0).set_child(child));
2484 let _ = inset;
2485 tree.layout(SizeProposal::exact(200.0, 200.0));
2486 assert_eq!(tree.bounds(child).origin(), Point::new(20.0, 20.0));
2487
2488 tree.dispatch_event(WidgetEvent::PointerDown {
2490 position: Point::new(50.0, 40.0),
2491 button: PointerButton::Primary,
2492 modifiers: Modifiers::NONE,
2493 });
2494 assert_eq!(
2495 down_pos.get(),
2496 Some(Point::new(30.0, 20.0)),
2497 "on_pointer_event PointerDown must be widget-local"
2498 );
2499 tree.dispatch_event(WidgetEvent::PointerUp {
2500 position: Point::new(50.0, 40.0),
2501 button: PointerButton::Primary,
2502 modifiers: Modifiers::NONE,
2503 });
2504 assert_eq!(
2505 tap_pos.get(),
2506 Some(Point::new(30.0, 20.0)),
2507 "on_tap position must be widget-local"
2508 );
2509
2510 tree.dispatch_event(WidgetEvent::PointerDown {
2513 position: Point::new(50.0, 40.0),
2514 button: PointerButton::Primary,
2515 modifiers: Modifiers::NONE,
2516 });
2517 tree.dispatch_event(WidgetEvent::PointerMove {
2520 position: Point::new(65.0, 55.0),
2521 });
2522 tree.dispatch_event(WidgetEvent::PointerMove {
2523 position: Point::new(90.0, 70.0),
2524 });
2525 assert_eq!(
2526 drag_pos.get(),
2527 Some(Point::new(70.0, 50.0)),
2528 "on_drag position must be widget-local"
2529 );
2530 }
2531
2532 #[test]
2533 fn dormant_widget_not_hit_tested() {
2534 let mut tree = WidgetTree::new();
2535 let widget = tree.add(FillWidget::new());
2536 tree.layout(SizeProposal::exact(100.0, 50.0));
2537
2538 tree.pointer_move(Point::new(50.0, 25.0));
2539 assert_eq!(tree.hovered, Some(widget));
2540
2541 tree.set_dormant(widget);
2542 tree.pointer_move(Point::new(200.0, 200.0));
2543 tree.pointer_move(Point::new(50.0, 25.0));
2544 assert_eq!(tree.hovered, None);
2545 }
2546
2547 #[test]
2548 fn ancestor_pointer_handler_does_not_suppress_descendant_hover() {
2549 use crate::event::EventResponse;
2550 use crate::test_widgets::StackWidget;
2551 use std::cell::Cell;
2552 use std::rc::Rc;
2553
2554 let hovered = Rc::new(Cell::new(false));
2556 let h = hovered.clone();
2557
2558 let mut tree = WidgetTree::new();
2559 let child = tree.add(FillWidget::new().on_hover(move |entered, _ctx| h.set(entered)));
2560 tree.add(
2565 StackWidget::new()
2566 .add_child(child)
2567 .on_pointer_event(|_event, _ctx| EventResponse::Handled),
2568 );
2569 tree.layout(SizeProposal::exact(100.0, 50.0));
2570
2571 tree.pointer_move(Point::new(50.0, 25.0));
2572 assert!(
2573 hovered.get(),
2574 "a greedy ancestor on_pointer_event must NOT swallow the child's PointerEnter"
2575 );
2576
2577 tree.pointer_move(Point::new(500.0, 500.0));
2578 assert!(
2579 !hovered.get(),
2580 "PointerLeave must likewise reach the child despite the ancestor"
2581 );
2582 }
2583
2584 #[test]
2592 fn a_child_hover_handler_does_not_swallow_its_ancestors() {
2593 use crate::test_widgets::StackWidget;
2594 use std::cell::Cell;
2595 use std::rc::Rc;
2596
2597 let (row, button) = (Rc::new(Cell::new(false)), Rc::new(Cell::new(false)));
2598 let (r, b) = (row.clone(), button.clone());
2599
2600 let mut tree = WidgetTree::new();
2601 let child = tree.add(FillWidget::new().on_hover(move |entered, _ctx| b.set(entered)));
2602 tree.add(
2603 StackWidget::new()
2604 .add_child(child)
2605 .on_hover(move |entered, _ctx| r.set(entered)),
2606 );
2607 tree.layout(SizeProposal::exact(100.0, 50.0));
2608
2609 tree.pointer_move(Point::new(50.0, 25.0));
2610 assert!(button.get(), "the child is hovered");
2611 assert!(row.get(), "and so is the row it is inside");
2612
2613 tree.pointer_move(Point::new(500.0, 500.0));
2614 assert!(!button.get(), "the child heard the leave");
2615 assert!(
2616 !row.get(),
2617 "and so did the row — a container is not still hovered because the \
2618 pointer left it through a button"
2619 );
2620 }
2621
2622 #[test]
2623 fn destroy_subtree_clears_dangling_pointer_capture() {
2624 use crate::event::{EventResponse, Modifiers, PointerButton};
2625 use crate::test_widgets::StackWidget;
2626
2627 let mut tree = WidgetTree::new();
2628 let child = tree.add(FillWidget::new().on_pointer_event(|event, ctx| {
2629 if matches!(event, WidgetEvent::PointerDown { .. }) {
2630 ctx.capture_pointer();
2631 }
2632 EventResponse::Ignored
2633 }));
2634 let parent = tree.add(StackWidget::new().add_child(child));
2635 tree.layout(SizeProposal::exact(100.0, 50.0));
2636
2637 tree.dispatch_event(WidgetEvent::PointerDown {
2639 position: Point::new(50.0, 25.0),
2640 button: PointerButton::Primary,
2641 modifiers: Modifiers::NONE,
2642 });
2643 assert_eq!(
2644 tree.pointer_captured_by,
2645 Some(child),
2646 "PointerDown handler should have captured the pointer"
2647 );
2648
2649 tree.destroy_subtree(parent);
2653 assert_eq!(
2654 tree.pointer_captured_by, None,
2655 "destroy_subtree must clear a capture anchored at a destroyed widget"
2656 );
2657 }
2658
2659 #[test]
2666 fn key_preview_consumes_before_focused_on_key() {
2667 use crate::event::EventResponse;
2670 use crate::test_widgets::StackWidget;
2671 use std::cell::Cell;
2672 use std::rc::Rc;
2673
2674 let leaf_fired = Rc::new(Cell::new(false));
2675 let leaf_flag = leaf_fired.clone();
2676 let preview_fired = Rc::new(Cell::new(false));
2677 let preview_flag = preview_fired.clone();
2678
2679 let mut tree = WidgetTree::new();
2680 let leaf = tree.add(FillWidget::new().focusable().on_key(move |event, _c| {
2681 if matches!(event, WidgetEvent::KeyDown { .. }) {
2684 leaf_flag.set(true);
2685 }
2686 EventResponse::Handled
2687 }));
2688 let mid = tree.add(StackWidget::new().add_child(leaf));
2689 let _root =
2690 tree.add(StackWidget::new().add_child(mid).on_key_preview(
2691 move |event, _c| match event {
2692 WidgetEvent::KeyDown {
2693 key: Key::Enter, ..
2694 } => {
2695 preview_flag.set(true);
2696 EventResponse::Handled
2697 }
2698 _ => EventResponse::Ignored,
2699 },
2700 ));
2701
2702 tree.layout(SizeProposal::exact(100.0, 50.0));
2703 tree.focus(leaf);
2704 tree.press_key(Key::Enter, Modifiers::NONE);
2705
2706 assert!(
2707 preview_fired.get(),
2708 "ancestor on_key_preview must fire for KeyDown on a focused descendant"
2709 );
2710 assert!(
2711 !leaf_fired.get(),
2712 "consuming the event in preview must prevent the focused widget's on_key from running"
2713 );
2714 }
2715
2716 #[test]
2717 fn key_preview_falls_through_when_returning_ignored() {
2718 use crate::event::EventResponse;
2721 use crate::test_widgets::StackWidget;
2722 use std::cell::Cell;
2723 use std::rc::Rc;
2724
2725 let leaf_fired = Rc::new(Cell::new(false));
2726 let leaf_flag = leaf_fired.clone();
2727 let preview_fired = Rc::new(Cell::new(false));
2728 let preview_flag = preview_fired.clone();
2729
2730 let mut tree = WidgetTree::new();
2731 let leaf = tree.add(FillWidget::new().focusable().on_key(move |_e, _c| {
2732 leaf_flag.set(true);
2733 EventResponse::Handled
2734 }));
2735 let mid = tree.add(StackWidget::new().add_child(leaf));
2736 let _root = tree.add(StackWidget::new().add_child(mid).on_key_preview(
2737 move |_event, _c| {
2738 preview_flag.set(true);
2739 EventResponse::Ignored
2740 },
2741 ));
2742
2743 tree.layout(SizeProposal::exact(100.0, 50.0));
2744 tree.focus(leaf);
2745 tree.press_key(Key::Enter, Modifiers::NONE);
2746
2747 assert!(preview_fired.get(), "preview must always be invoked");
2748 assert!(
2749 leaf_fired.get(),
2750 "preview returning Ignored must not block the focused widget's on_key"
2751 );
2752 }
2753
2754 #[test]
2755 fn key_preview_excludes_focused_target_itself() {
2756 use crate::event::EventResponse;
2760 use std::cell::Cell;
2761 use std::rc::Rc;
2762
2763 let preview_on_target = Rc::new(Cell::new(false));
2764 let pf = preview_on_target.clone();
2765
2766 let mut tree = WidgetTree::new();
2767 let leaf = tree.add(FillWidget::new().focusable().on_key_preview(move |_e, _c| {
2768 pf.set(true);
2769 EventResponse::Handled
2770 }));
2771 tree.layout(SizeProposal::exact(100.0, 50.0));
2772 tree.focus(leaf);
2773 tree.press_key(Key::Enter, Modifiers::NONE);
2774
2775 assert!(
2776 !preview_on_target.get(),
2777 "the focused widget itself must not see its own on_key_preview"
2778 );
2779 }
2780
2781 #[test]
2782 fn key_preview_root_to_target_order() {
2783 use crate::event::EventResponse;
2787 use crate::test_widgets::StackWidget;
2788 use std::cell::RefCell;
2789 use std::rc::Rc;
2790
2791 let order: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
2792 let outer_log = order.clone();
2793 let inner_log = order.clone();
2794
2795 let mut tree = WidgetTree::new();
2796 let leaf = tree.add(FillWidget::new().focusable());
2797 let inner = tree.add(StackWidget::new().add_child(leaf).on_key_preview(
2798 move |event, _c| {
2799 if matches!(event, WidgetEvent::KeyDown { .. }) {
2800 inner_log.borrow_mut().push("inner");
2801 }
2802 EventResponse::Ignored
2803 },
2804 ));
2805 let _outer = tree.add(StackWidget::new().add_child(inner).on_key_preview(
2806 move |event, _c| {
2807 if matches!(event, WidgetEvent::KeyDown { .. }) {
2808 outer_log.borrow_mut().push("outer");
2809 }
2810 EventResponse::Ignored
2811 },
2812 ));
2813
2814 tree.layout(SizeProposal::exact(100.0, 50.0));
2815 tree.focus(leaf);
2816 tree.dispatch_event(WidgetEvent::KeyDown {
2817 key: Key::Enter,
2818 modifiers: Modifiers::NONE,
2819 text: None,
2820 });
2821
2822 assert_eq!(
2823 *order.borrow(),
2824 vec!["outer", "inner"],
2825 "preview must walk root → parent-of-target"
2826 );
2827 }
2828
2829 #[test]
2830 fn access_action_routes_to_cursored_target_not_focus() {
2831 use crate::signal::Signal;
2835 let a_fired = Signal::new(false);
2836 let b_fired = Signal::new(false);
2837 let a_cb = a_fired.clone();
2838 let b_cb = b_fired.clone();
2839
2840 let mut tree = WidgetTree::new();
2841 let a = tree.add(
2842 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| a_cb.set(true)),
2843 );
2844 let b = tree.add(
2845 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| b_cb.set(true)),
2846 );
2847 tree.layout(SizeProposal::exact(200.0, 100.0));
2848
2849 tree.focus(a);
2850 tree.dispatch_event(WidgetEvent::AccessAction {
2851 action: accesskit::Action::Click,
2852 target: Some(b),
2853 target_node: crate::accessibility::widget_id_to_node_id(b),
2854 data: None,
2855 });
2856
2857 assert!(b_fired.get(), "the cursored target must receive the action");
2858 assert!(
2859 !a_fired.get(),
2860 "the keyboard-focused widget must NOT receive an action targeting another node"
2861 );
2862 }
2863
2864 #[test]
2865 fn access_action_without_target_is_dropped_not_redirected_to_focus() {
2866 use crate::signal::Signal;
2869 let fired = Signal::new(false);
2870 let cb = fired.clone();
2871
2872 let mut tree = WidgetTree::new();
2873 let widget = tree.add(
2874 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| cb.set(true)),
2875 );
2876 tree.layout(SizeProposal::exact(200.0, 100.0));
2877
2878 tree.focus(widget);
2879 tree.dispatch_event(WidgetEvent::AccessAction {
2880 action: accesskit::Action::Click,
2881 target: None,
2882 target_node: crate::accessibility::root_node_id(),
2883 data: None,
2884 });
2885
2886 assert!(
2887 !fired.get(),
2888 "a target-less action must not be redirected to the focused widget"
2889 );
2890 }
2891
2892 #[test]
2899 fn shortcut_fires_matching_action_on_source_widget() {
2900 use crate::action::Action;
2901 use crate::shortcut::{KeyStroke, Shortcut};
2902 use std::cell::Cell;
2903 use std::rc::Rc;
2904
2905 let fired = Rc::new(Cell::new(false));
2906 let fired_flag = fired.clone();
2907
2908 let mut tree = WidgetTree::new();
2909 let widget = tree.add(FillWidget::new().focusable());
2910 tree.push_action(
2911 widget,
2912 Action::new("app.save").on_invoke(move |_intent, _ctx| {
2913 fired_flag.set(true);
2914 }),
2915 );
2916 tree.shortcut_registry_mut().register(
2917 Shortcut::new("app.save")
2918 .primary(KeyStroke::command(Key::S))
2919 .build(),
2920 );
2921
2922 tree.layout(SizeProposal::exact(100.0, 50.0));
2923 tree.focus(widget);
2924
2925 tree.press_key(Key::S, Modifiers::COMMAND);
2926 assert!(fired.get(), "matching action must fire on KeyDown");
2927 }
2928
2929 #[test]
2930 fn global_shortcut_fires_without_focused_widget() {
2931 use crate::action::Action;
2932 use crate::shortcut::{KeyStroke, Shortcut};
2933 use std::cell::Cell;
2934 use std::rc::Rc;
2935
2936 let fired = Rc::new(Cell::new(false));
2940 let fired_flag = fired.clone();
2941
2942 let mut tree = WidgetTree::new();
2943 let root = tree.add(FillWidget::new());
2944 tree.push_action(
2945 root,
2946 Action::new("app.save").on_invoke(move |_intent, _ctx| {
2947 fired_flag.set(true);
2948 }),
2949 );
2950 tree.shortcut_registry_mut().register(
2951 Shortcut::new("app.save")
2952 .primary(KeyStroke::command(Key::S))
2953 .build(),
2954 );
2955
2956 tree.layout(SizeProposal::exact(100.0, 50.0));
2957 tree.press_key(Key::S, Modifiers::COMMAND);
2960 assert!(
2961 fired.get(),
2962 "global shortcut must fire without a focused widget"
2963 );
2964 }
2965
2966 #[test]
2967 fn global_shortcut_fires_after_focused_widget_destroyed() {
2968 use crate::action::Action;
2969 use crate::shortcut::{KeyStroke, Shortcut};
2970 use std::cell::Cell;
2971 use std::rc::Rc;
2972
2973 let fired = Rc::new(Cell::new(false));
2978 let fired_flag = fired.clone();
2979
2980 let mut tree = WidgetTree::new();
2981 let root = tree.add(FillWidget::new());
2982 let focusable = tree.add_child(root, FillWidget::new().focusable());
2983 tree.push_action(
2984 root,
2985 Action::new("app.save").on_invoke(move |_intent, _ctx| {
2986 fired_flag.set(true);
2987 }),
2988 );
2989 tree.shortcut_registry_mut().register(
2990 Shortcut::new("app.save")
2991 .primary(KeyStroke::command(Key::S))
2992 .build(),
2993 );
2994
2995 tree.layout(SizeProposal::exact(100.0, 50.0));
2996 tree.focus(focusable);
2997 assert_eq!(tree.focused(), Some(focusable));
2998
2999 tree.destroy_subtree(focusable);
3002 assert_eq!(tree.focused(), None, "focus must clear when destroyed");
3003
3004 tree.press_key(Key::S, Modifiers::COMMAND);
3005 assert!(
3006 fired.get(),
3007 "global shortcut must still fire after the focused widget is destroyed"
3008 );
3009 }
3010
3011 #[test]
3012 fn scoped_shortcut_matches_only_when_focus_in_scope() {
3013 use crate::action::Action;
3014 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
3015 use std::cell::Cell;
3016 use std::rc::Rc;
3017
3018 let fired = Rc::new(Cell::new(0));
3019 let fired_flag = fired.clone();
3020
3021 let mut tree = WidgetTree::new();
3022 let scope_root = tree.add(FillWidget::new().focusable());
3023 let inside = tree.add_child(scope_root, FillWidget::new().focusable());
3024 let outside = tree.add(FillWidget::new().focusable());
3025
3026 tree.push_action(
3027 scope_root,
3028 Action::new("editor.find").on_invoke(move |_i, _c| {
3029 fired_flag.set(fired_flag.get() + 1);
3030 }),
3031 );
3032 tree.shortcut_registry_mut().register(
3033 Shortcut::new("editor.find")
3034 .primary(KeyStroke::command(Key::F))
3035 .scope(ShortcutScope::Scoped(scope_root))
3036 .build(),
3037 );
3038
3039 tree.layout(SizeProposal::exact(200.0, 100.0));
3040
3041 tree.focus(outside);
3043 tree.press_key(Key::F, Modifiers::COMMAND);
3044 assert_eq!(
3045 fired.get(),
3046 0,
3047 "scoped shortcut must not fire outside scope"
3048 );
3049
3050 tree.focus(inside);
3052 tree.press_key(Key::F, Modifiers::COMMAND);
3053 assert_eq!(
3054 fired.get(),
3055 1,
3056 "scoped shortcut must fire when focus in scope"
3057 );
3058 }
3059
3060 #[test]
3061 fn same_chord_scoped_first_falls_back_to_global_when_focus_outside() {
3062 use crate::action::Action;
3068 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
3069 use std::cell::Cell;
3070 use std::rc::Rc;
3071
3072 let scoped_fired = Rc::new(Cell::new(0));
3073 let global_fired = Rc::new(Cell::new(0));
3074 let sf = scoped_fired.clone();
3075 let gf = global_fired.clone();
3076
3077 let mut tree = WidgetTree::new();
3078 let root = tree.add(FillWidget::new());
3079 let editor = tree.add_child(root, FillWidget::new().focusable());
3080 let _editor_inner = tree.add_child(editor, FillWidget::new().focusable());
3081 let sidebar = tree.add_child(root, FillWidget::new().focusable());
3082
3083 tree.push_action(
3084 editor,
3085 Action::new("editor.saveBlock").on_invoke(move |_i, _c| sf.set(sf.get() + 1)),
3086 );
3087 tree.push_action(
3088 root,
3089 Action::new("zzz.global.save").on_invoke(move |_i, _c| gf.set(gf.get() + 1)),
3090 );
3091 tree.shortcut_registry_mut().register(
3092 Shortcut::new("editor.saveBlock")
3093 .primary(KeyStroke::command(Key::S))
3094 .scope(ShortcutScope::Scoped(editor))
3095 .build(),
3096 );
3097 tree.shortcut_registry_mut().register(
3098 Shortcut::new("zzz.global.save")
3099 .primary(KeyStroke::command(Key::S))
3100 .build(),
3101 );
3102
3103 tree.layout(SizeProposal::exact(200.0, 100.0));
3104 tree.focus(sidebar);
3105 tree.press_key(Key::S, Modifiers::COMMAND);
3106
3107 assert_eq!(global_fired.get(), 1, "applicable global must fire");
3108 assert_eq!(
3109 scoped_fired.get(),
3110 0,
3111 "inapplicable scoped binding must not eat the chord"
3112 );
3113 }
3114
3115 #[test]
3116 fn same_chord_global_first_yields_to_scoped_when_focus_inside() {
3117 use crate::action::Action;
3122 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
3123 use std::cell::Cell;
3124 use std::rc::Rc;
3125
3126 let scoped_fired = Rc::new(Cell::new(0));
3127 let global_fired = Rc::new(Cell::new(0));
3128 let sf = scoped_fired.clone();
3129 let gf = global_fired.clone();
3130
3131 let mut tree = WidgetTree::new();
3132 let root = tree.add(FillWidget::new());
3133 let editor = tree.add_child(root, FillWidget::new().focusable());
3134 let editor_inner = tree.add_child(editor, FillWidget::new().focusable());
3135 let sidebar = tree.add_child(root, FillWidget::new().focusable());
3136
3137 tree.push_action(
3138 editor,
3139 Action::new("editor.saveBlock").on_invoke(move |_i, _c| sf.set(sf.get() + 1)),
3140 );
3141 tree.push_action(
3142 root,
3143 Action::new("app.save").on_invoke(move |_i, _c| gf.set(gf.get() + 1)),
3144 );
3145 tree.shortcut_registry_mut().register(
3146 Shortcut::new("app.save")
3147 .primary(KeyStroke::command(Key::S))
3148 .build(),
3149 );
3150 tree.shortcut_registry_mut().register(
3151 Shortcut::new("editor.saveBlock")
3152 .primary(KeyStroke::command(Key::S))
3153 .scope(ShortcutScope::Scoped(editor))
3154 .build(),
3155 );
3156
3157 tree.layout(SizeProposal::exact(200.0, 100.0));
3158
3159 tree.focus(editor_inner);
3161 tree.press_key(Key::S, Modifiers::COMMAND);
3162 assert_eq!(
3163 scoped_fired.get(),
3164 1,
3165 "in-focus scoped must win over global"
3166 );
3167 assert_eq!(
3168 global_fired.get(),
3169 0,
3170 "global must yield to the scoped binding"
3171 );
3172
3173 tree.focus(sidebar);
3175 tree.press_key(Key::S, Modifiers::COMMAND);
3176 assert_eq!(scoped_fired.get(), 1, "scoped stays put outside its scope");
3177 assert_eq!(
3178 global_fired.get(),
3179 1,
3180 "global fires when focus leaves the scope"
3181 );
3182 }
3183
3184 #[test]
3185 fn propagated_action_lets_ancestor_handle() {
3186 use crate::action::Action;
3187 use crate::intent::IntentResponse;
3188 use crate::shortcut::{KeyStroke, Shortcut};
3189 use std::cell::Cell;
3190 use std::rc::Rc;
3191
3192 let inner_seen = Rc::new(Cell::new(false));
3193 let outer_seen = Rc::new(Cell::new(false));
3194 let inner_flag = inner_seen.clone();
3195 let outer_flag = outer_seen.clone();
3196
3197 let mut tree = WidgetTree::new();
3198 let outer = tree.add(FillWidget::new().focusable());
3199 let inner = tree.add_child(outer, FillWidget::new().focusable());
3200
3201 tree.push_action(
3203 inner,
3204 Action::new("app.save").on_invoke_with_response(move |_i, _c| {
3205 inner_flag.set(true);
3206 IntentResponse::Propagated
3207 }),
3208 );
3209 tree.push_action(
3210 outer,
3211 Action::new("app.save").on_invoke(move |_i, _c| {
3212 outer_flag.set(true);
3213 }),
3214 );
3215 tree.shortcut_registry_mut().register(
3216 Shortcut::new("app.save")
3217 .primary(KeyStroke::command(Key::S))
3218 .build(),
3219 );
3220
3221 tree.layout(SizeProposal::exact(100.0, 50.0));
3222 tree.focus(inner);
3223
3224 tree.press_key(Key::S, Modifiers::COMMAND);
3225 assert!(inner_seen.get(), "inner action observed the intent");
3226 assert!(outer_seen.get(), "outer action reached after Propagated");
3227 }
3228
3229 #[test]
3230 fn handled_action_stops_propagation() {
3231 use crate::action::Action;
3232 use crate::shortcut::{KeyStroke, Shortcut};
3233 use std::cell::Cell;
3234 use std::rc::Rc;
3235
3236 let inner_seen = Rc::new(Cell::new(false));
3237 let outer_seen = Rc::new(Cell::new(false));
3238 let inner_flag = inner_seen.clone();
3239 let outer_flag = outer_seen.clone();
3240
3241 let mut tree = WidgetTree::new();
3242 let outer = tree.add(FillWidget::new().focusable());
3243 let inner = tree.add_child(outer, FillWidget::new().focusable());
3244
3245 tree.push_action(
3246 inner,
3247 Action::new("app.save").on_invoke(move |_i, _c| {
3248 inner_flag.set(true);
3249 }),
3250 );
3251 tree.push_action(
3252 outer,
3253 Action::new("app.save").on_invoke(move |_i, _c| {
3254 outer_flag.set(true);
3255 }),
3256 );
3257 tree.shortcut_registry_mut().register(
3258 Shortcut::new("app.save")
3259 .primary(KeyStroke::command(Key::S))
3260 .build(),
3261 );
3262
3263 tree.layout(SizeProposal::exact(100.0, 50.0));
3264 tree.focus(inner);
3265
3266 tree.press_key(Key::S, Modifiers::COMMAND);
3267 assert!(inner_seen.get());
3268 assert!(!outer_seen.get(), "Handled at inner must stop propagation");
3269 }
3270
3271 #[test]
3272 fn disabled_action_propagates_by_default() {
3273 use crate::action::Action;
3274 use crate::shortcut::{KeyStroke, Shortcut};
3275 use crate::signal::Signal;
3276 use std::cell::Cell;
3277 use std::rc::Rc;
3278
3279 let inner_seen = Rc::new(Cell::new(false));
3280 let outer_seen = Rc::new(Cell::new(false));
3281 let inner_flag = inner_seen.clone();
3282 let outer_flag = outer_seen.clone();
3283
3284 let mut tree = WidgetTree::new();
3285 let outer = tree.add(FillWidget::new().focusable());
3286 let inner = tree.add_child(outer, FillWidget::new().focusable());
3287
3288 let enabled = Signal::new(false);
3289 tree.push_action(
3290 inner,
3291 Action::new("app.save")
3292 .enabled_when(enabled.clone())
3293 .on_invoke(move |_i, _c| {
3294 inner_flag.set(true);
3295 }),
3296 );
3297 tree.push_action(
3298 outer,
3299 Action::new("app.save").on_invoke(move |_i, _c| {
3300 outer_flag.set(true);
3301 }),
3302 );
3303 tree.shortcut_registry_mut().register(
3304 Shortcut::new("app.save")
3305 .primary(KeyStroke::command(Key::S))
3306 .build(),
3307 );
3308
3309 tree.layout(SizeProposal::exact(100.0, 50.0));
3310 tree.focus(inner);
3311
3312 tree.press_key(Key::S, Modifiers::COMMAND);
3313 assert!(!inner_seen.get(), "disabled inner must not run");
3314 assert!(
3315 outer_seen.get(),
3316 "intent must propagate past disabled inner"
3317 );
3318 }
3319
3320 #[test]
3321 fn disabled_action_with_non_propagating_shortcut_consumes() {
3322 use crate::action::Action;
3323 use crate::shortcut::{KeyStroke, Shortcut};
3324 use crate::signal::Signal;
3325 use std::cell::Cell;
3326 use std::rc::Rc;
3327
3328 let inner_seen = Rc::new(Cell::new(false));
3329 let outer_seen = Rc::new(Cell::new(false));
3330 let inner_flag = inner_seen.clone();
3331 let outer_flag = outer_seen.clone();
3332
3333 let mut tree = WidgetTree::new();
3334 let outer = tree.add(FillWidget::new().focusable());
3335 let inner = tree.add_child(outer, FillWidget::new().focusable());
3336
3337 let enabled = Signal::new(false);
3338 tree.push_action(
3339 inner,
3340 Action::new("app.save")
3341 .enabled_when(enabled.clone())
3342 .on_invoke(move |_i, _c| {
3343 inner_flag.set(true);
3344 }),
3345 );
3346 tree.push_action(
3347 outer,
3348 Action::new("app.save").on_invoke(move |_i, _c| {
3349 outer_flag.set(true);
3350 }),
3351 );
3352 tree.shortcut_registry_mut().register(
3353 Shortcut::new("app.save")
3354 .primary(KeyStroke::command(Key::S))
3355 .propagate_when_disabled(false)
3356 .build(),
3357 );
3358
3359 tree.layout(SizeProposal::exact(100.0, 50.0));
3360 tree.focus(inner);
3361
3362 tree.press_key(Key::S, Modifiers::COMMAND);
3363 assert!(!inner_seen.get(), "disabled inner still does not run");
3364 assert!(
3365 !outer_seen.get(),
3366 "intent must NOT propagate when shortcut disallows it"
3367 );
3368 }
3369
3370 #[test]
3371 fn send_intent_from_handler_reaches_ancestor_action() {
3372 use crate::action::Action;
3373 use crate::intent::Intent;
3374 use std::cell::Cell;
3375 use std::rc::Rc;
3376
3377 let save_seen = Rc::new(Cell::new(false));
3378 let save_flag = save_seen.clone();
3379
3380 let mut tree = WidgetTree::new();
3381 let root = tree.add(FillWidget::new());
3382 let button = tree.add_child(
3383 root,
3384 FillWidget::new().on_tap(|_pos, ctx| {
3385 ctx.send_intent(Intent::new("app.save"));
3386 }),
3387 );
3388 tree.push_action(
3389 root,
3390 Action::new("app.save").on_invoke(move |_i, _c| {
3391 save_flag.set(true);
3392 }),
3393 );
3394
3395 tree.layout(SizeProposal::exact(100.0, 50.0));
3396 tree.click(button);
3397 assert!(
3398 save_seen.get(),
3399 "ctx.send_intent must reach ancestor action"
3400 );
3401 }
3402
3403 #[test]
3404 fn widget_type_histogram_counts_distinct_types() {
3405 let mut tree = WidgetTree::new();
3410 let _ = tree.add(FillWidget::new());
3411 let _ = tree.add(FillWidget::new());
3412 let _ = tree.add(FillWidget::new());
3413 tree.layout(SizeProposal::exact(100.0, 100.0));
3414 let histogram = tree.widget_type_histogram();
3415 let total: u32 = histogram.values().sum();
3416 assert!(
3417 total >= 3,
3418 "expected at least 3 active widgets, got {total}: {histogram:?}"
3419 );
3420 let fillwidget_entries: u32 = histogram
3421 .iter()
3422 .filter(|(k, _)| k.contains("FillWidget"))
3423 .map(|(_, v)| *v)
3424 .sum();
3425 assert!(
3426 fillwidget_entries >= 3,
3427 "expected ≥3 FillWidget instances; histogram = {histogram:?}"
3428 );
3429 assert_eq!(tree.active_widget_count() as u32, total);
3430 }
3431
3432 #[test]
3433 fn intent_source_tagged_handler_for_tap_activation() {
3434 use crate::action::Action;
3438 use crate::intent::Intent;
3439 use crate::telemetry::IntentSource;
3440 use std::cell::Cell;
3441 use std::rc::Rc;
3442 let captured = Rc::new(Cell::new(IntentSource::Unknown));
3443 let captured_for_action = captured.clone();
3444
3445 let mut tree = WidgetTree::new();
3446 let root = tree.add(FillWidget::new());
3447 let button = tree.add_child(
3448 root,
3449 FillWidget::new().on_tap(|_pos, ctx| {
3450 ctx.send_intent(Intent::new("app.save"));
3451 }),
3452 );
3453 tree.push_action(
3454 root,
3455 Action::new("app.save").on_invoke(move |intent, _c| {
3456 captured_for_action.set(intent.source);
3457 }),
3458 );
3459
3460 tree.layout(SizeProposal::exact(100.0, 50.0));
3461 tree.click(button);
3462 assert_eq!(
3463 captured.get(),
3464 IntentSource::Handler,
3465 "tap-driven intent must tag IntentSource::Handler"
3466 );
3467 }
3468
3469 #[test]
3470 fn intent_source_programmatic_when_no_handler_active() {
3471 use crate::intent::Intent;
3472 use crate::telemetry::IntentSource;
3473 let intent = Intent::new("app.demo");
3474 assert_eq!(intent.source, IntentSource::Programmatic);
3475
3476 let mut ctx = EventContext::new();
3478 ctx.send_intent(Intent::new("app.demo"));
3479 let queued = ctx.pending_intents.first().expect("intent queued");
3480 assert_eq!(queued.source, IntentSource::Programmatic);
3481 }
3482
3483 #[test]
3484 fn with_intent_source_overrides_for_managed_widgets() {
3485 use crate::intent::Intent;
3486 use crate::telemetry::IntentSource;
3487 let mut ctx = EventContext::new();
3488 ctx.with_intent_source(IntentSource::Menu, |ctx| {
3489 ctx.send_intent(Intent::new("app.demo"));
3490 });
3491 let queued = ctx.pending_intents.first().expect("intent queued");
3492 assert_eq!(
3493 queued.source,
3494 IntentSource::Menu,
3495 "with_intent_source(Menu) must tag the dispatched intent"
3496 );
3497
3498 ctx.send_intent(Intent::new("app.next"));
3502 let next = ctx.pending_intents.last().expect("second intent");
3503 assert_eq!(next.source, IntentSource::Programmatic);
3504 }
3505
3506 #[test]
3507 fn disabled_shortcut_falls_through_to_focused_widget() {
3508 use crate::action::Action;
3509 use crate::shortcut::{KeyStroke, Shortcut};
3510 use crate::signal::Signal;
3511 use std::cell::Cell;
3512 use std::rc::Rc;
3513
3514 let action_fired = Rc::new(Cell::new(false));
3515 let on_key_fired = Rc::new(Cell::new(false));
3516 let af = action_fired.clone();
3517 let kf = on_key_fired.clone();
3518
3519 let enabled = Signal::new(false);
3520
3521 let mut tree = WidgetTree::new();
3522 let widget = tree.add(FillWidget::new().focusable().on_key(move |event, _ctx| {
3523 if matches!(
3524 event,
3525 WidgetEvent::KeyDown {
3526 key: Key::S,
3527 modifiers,
3528 ..
3529 } if modifiers.command()
3530 ) {
3531 kf.set(true);
3532 return EventResponse::Handled;
3533 }
3534 EventResponse::Ignored
3535 }));
3536 tree.push_action(
3537 widget,
3538 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
3539 );
3540 tree.shortcut_registry_mut().register(
3541 Shortcut::new("app.save")
3542 .primary(KeyStroke::command(Key::S))
3543 .enabled_when(enabled.clone())
3544 .build(),
3545 );
3546
3547 tree.layout(SizeProposal::exact(100.0, 50.0));
3548 tree.focus(widget);
3549
3550 tree.press_key(Key::S, Modifiers::COMMAND);
3552 assert!(
3553 !action_fired.get(),
3554 "disabled shortcut must not invoke its action"
3555 );
3556 assert!(
3557 on_key_fired.get(),
3558 "disabled shortcut must let KeyDown reach the focused widget"
3559 );
3560
3561 on_key_fired.set(false);
3563 enabled.set(true);
3564 tree.press_key(Key::S, Modifiers::COMMAND);
3565 assert!(action_fired.get(), "re-enabled shortcut must dispatch");
3566 assert!(
3567 !on_key_fired.get(),
3568 "enabled shortcut must consume the KeyDown"
3569 );
3570 }
3571
3572 #[test]
3573 fn keyboard_capture_bypasses_shortcut() {
3574 use crate::action::Action;
3575 use crate::shortcut::{KeyStroke, Shortcut};
3576 use std::cell::Cell;
3577 use std::rc::Rc;
3578
3579 fn run(capture: bool) -> (bool, bool) {
3585 let action_fired = Rc::new(Cell::new(false));
3586 let on_key_fired = Rc::new(Cell::new(false));
3587 let af = action_fired.clone();
3588 let kf = on_key_fired.clone();
3589
3590 let mut tree = WidgetTree::new();
3591 let widget = tree.add(
3592 FillWidget::new()
3593 .focusable()
3594 .keyboard_capture(capture)
3595 .on_key(move |event, _ctx| {
3596 if matches!(
3597 event,
3598 WidgetEvent::KeyDown { key: Key::S, modifiers, .. } if modifiers.command()
3599 ) {
3600 kf.set(true);
3601 return EventResponse::Handled;
3602 }
3603 EventResponse::Ignored
3604 }),
3605 );
3606 tree.push_action(
3607 widget,
3608 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
3609 );
3610 tree.shortcut_registry_mut().register(
3611 Shortcut::new("app.save")
3612 .primary(KeyStroke::command(Key::S))
3613 .build(),
3614 );
3615
3616 tree.layout(SizeProposal::exact(100.0, 50.0));
3617 tree.focus(widget);
3618 tree.press_key(Key::S, Modifiers::COMMAND);
3619 (action_fired.get(), on_key_fired.get())
3620 }
3621
3622 let (action, on_key) = run(true);
3624 assert!(
3625 !action,
3626 "keyboard_capture must suppress the shortcut action"
3627 );
3628 assert!(on_key, "keyboard_capture must deliver the raw KeyDown");
3629
3630 let (action, on_key) = run(false);
3632 assert!(action, "without capture the shortcut must fire");
3633 assert!(!on_key, "without capture the widget must not see the key");
3634 }
3635
3636 #[test]
3637 fn ctrl_tab_always_escapes_a_keyboard_capture_surface() {
3638 use std::cell::Cell;
3639 use std::rc::Rc;
3640
3641 let saw_key = Rc::new(Cell::new(false));
3647 let sk = saw_key.clone();
3648
3649 let mut tree = WidgetTree::new();
3650 let capture = tree.add(
3651 FillWidget::new()
3652 .focusable()
3653 .keyboard_capture(true)
3654 .on_key(move |_event, _ctx| {
3656 sk.set(true);
3657 EventResponse::Handled
3658 }),
3659 );
3660 let neighbour = tree.add(FillWidget::new().focusable());
3661 tree.layout(SizeProposal::exact(100.0, 50.0));
3662
3663 tree.focus(capture);
3666 tree.press_key(Key::Tab, Modifiers::NONE);
3667 assert!(saw_key.get(), "plain Tab must reach the capture surface");
3668 assert_eq!(
3669 tree.focused(),
3670 Some(capture),
3671 "plain Tab must not move focus off a capture surface"
3672 );
3673
3674 saw_key.set(false);
3676 tree.press_key(Key::Tab, Modifiers::CTRL);
3677 assert!(
3678 !saw_key.get(),
3679 "Ctrl+Tab is reserved and must not reach the capture surface"
3680 );
3681 assert_eq!(
3682 tree.focused(),
3683 Some(neighbour),
3684 "Ctrl+Tab must move focus out of a capture surface"
3685 );
3686
3687 tree.focus(capture);
3689 tree.press_key(Key::Tab, Modifiers::CTRL | Modifiers::SHIFT);
3690 assert_eq!(
3691 tree.focused(),
3692 Some(neighbour),
3693 "Ctrl+Shift+Tab must move focus out of a capture surface"
3694 );
3695 }
3696
3697 #[test]
3698 fn scope_mismatch_does_not_invoke_on_activate() {
3699 use crate::intent::Intent;
3700 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
3701 use std::cell::Cell;
3702 use std::rc::Rc;
3703
3704 let activated = Rc::new(Cell::new(false));
3710 let activated_flag = activated.clone();
3711
3712 let mut tree = WidgetTree::new();
3713 let scope_root = tree.add(FillWidget::new().focusable());
3714 let outside = tree.add(FillWidget::new().focusable());
3715
3716 tree.shortcut_registry_mut().register(
3717 Shortcut::new("editor.find")
3718 .primary(KeyStroke::command(Key::F))
3719 .scope(ShortcutScope::Scoped(scope_root))
3720 .on_activate(move |_ks, _ctx| {
3721 activated_flag.set(true);
3722 Intent::new("editor.find")
3723 })
3724 .build(),
3725 );
3726
3727 tree.layout(SizeProposal::exact(200.0, 100.0));
3728 tree.focus(outside);
3729
3730 tree.press_key(Key::F, Modifiers::COMMAND);
3731 assert!(
3732 !activated.get(),
3733 "on_activate must not run when focus is outside the shortcut's scope"
3734 );
3735 }
3736
3737 #[test]
3738 fn key_capture_runs_callback_and_bypasses_registry() {
3739 use crate::action::Action;
3740 use crate::shortcut::{KeyStroke, Shortcut};
3741 use std::cell::Cell;
3742 use std::rc::Rc;
3743
3744 let action_fired = Rc::new(Cell::new(false));
3745 let af = action_fired.clone();
3746 let captured = Rc::new(Cell::new(None));
3747 let cf = captured.clone();
3748
3749 let mut tree = WidgetTree::new();
3750 let widget = tree.add(FillWidget::new().focusable());
3751 tree.push_action(
3752 widget,
3753 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
3754 );
3755 tree.shortcut_registry_mut().register(
3756 Shortcut::new("app.save")
3757 .primary(KeyStroke::command(Key::S))
3758 .build(),
3759 );
3760
3761 tree.layout(SizeProposal::exact(100.0, 50.0));
3762 tree.focus(widget);
3763
3764 let handle = tree.begin_key_capture(move |ks, _reg, _ctx| cf.set(Some(ks)));
3765 assert!(tree.is_capturing_keys());
3766
3767 tree.press_key(Key::S, Modifiers::COMMAND);
3768 assert_eq!(
3769 captured.get(),
3770 Some(KeyStroke::command(Key::S)),
3771 "capture callback must receive the chord"
3772 );
3773 assert!(
3774 !action_fired.get(),
3775 "shortcut action must not fire while capture is armed"
3776 );
3777 assert!(
3778 !tree.is_capturing_keys(),
3779 "capture is one-shot; next KeyDown flows normally"
3780 );
3781 drop(handle);
3782 }
3783
3784 #[test]
3785 fn key_capture_can_rebind_through_registry() {
3786 use crate::shortcut::{KeyStroke, Shortcut};
3787
3788 let mut tree = WidgetTree::new();
3789 let widget = tree.add(FillWidget::new().focusable());
3790 tree.shortcut_registry_mut().register(
3791 Shortcut::new("app.save")
3792 .primary(KeyStroke::command(Key::S))
3793 .build(),
3794 );
3795
3796 tree.layout(SizeProposal::exact(100.0, 50.0));
3797 tree.focus(widget);
3798
3799 let _h = tree.begin_key_capture(|ks, reg, _ctx| {
3801 reg.rebind_primary("app.save", Some(ks));
3802 });
3803
3804 tree.press_key(Key::B, Modifiers::COMMAND | Modifiers::SHIFT);
3805 assert_eq!(
3806 tree.shortcut_registry()
3807 .effective("app.save")
3808 .unwrap()
3809 .primary,
3810 Some(KeyStroke::command_shift(Key::B))
3811 );
3812 }
3813
3814 #[test]
3815 fn dropping_capture_handle_cancels_capture() {
3816 use crate::shortcut::{KeyStroke, Shortcut};
3817 use std::cell::Cell;
3818 use std::rc::Rc;
3819
3820 let action_fired = Rc::new(Cell::new(false));
3821 let af = action_fired.clone();
3822 let capture_fired = Rc::new(Cell::new(false));
3823 let cf = capture_fired.clone();
3824
3825 let mut tree = WidgetTree::new();
3826 let widget = tree.add(FillWidget::new().focusable());
3827 tree.push_action(
3828 widget,
3829 crate::action::Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
3830 );
3831 tree.shortcut_registry_mut().register(
3832 Shortcut::new("app.save")
3833 .primary(KeyStroke::command(Key::S))
3834 .build(),
3835 );
3836 tree.layout(SizeProposal::exact(100.0, 50.0));
3837 tree.focus(widget);
3838
3839 {
3843 let _h = tree.begin_key_capture(move |_ks, _reg, _ctx| cf.set(true));
3844 assert!(tree.is_capturing_keys());
3845 }
3847 assert!(
3848 !tree.is_capturing_keys(),
3849 "dropping the handle must cancel the capture"
3850 );
3851
3852 tree.press_key(Key::S, Modifiers::COMMAND);
3853 assert!(!capture_fired.get(), "cancelled capture must not fire");
3854 assert!(
3855 action_fired.get(),
3856 "shortcut action runs after capture was cancelled"
3857 );
3858 }
3859
3860 #[test]
3861 fn second_begin_key_capture_does_not_racecancel_first() {
3862 use std::cell::Cell;
3863 use std::rc::Rc;
3864
3865 let first = Rc::new(Cell::new(false));
3866 let second = Rc::new(Cell::new(false));
3867 let f = first.clone();
3868 let s = second.clone();
3869
3870 let mut tree = WidgetTree::new();
3871 let widget = tree.add(FillWidget::new().focusable());
3872 tree.layout(SizeProposal::exact(100.0, 50.0));
3873 tree.focus(widget);
3874
3875 let h1 = tree.begin_key_capture(move |_ks, _reg, _ctx| f.set(true));
3881 let _h2 = tree.begin_key_capture(move |_ks, _reg, _ctx| s.set(true));
3882 drop(h1);
3883
3884 assert!(
3885 tree.is_capturing_keys(),
3886 "dropping the older handle must not cancel the active capture"
3887 );
3888 tree.press_key(Key::K, Modifiers::COMMAND);
3889 assert!(!first.get());
3890 assert!(second.get(), "newest capture wins");
3891 }
3892
3893 #[test]
3894 fn capture_callback_can_send_intent() {
3895 use crate::action::Action;
3896 use crate::intent::Intent;
3897
3898 use std::cell::Cell;
3899 use std::rc::Rc;
3900
3901 let ran = Rc::new(Cell::new(false));
3902 let flag = ran.clone();
3903
3904 let mut tree = WidgetTree::new();
3905 let widget = tree.add(FillWidget::new().focusable());
3906 tree.push_action(
3907 widget,
3908 Action::new("app.save").on_invoke(move |_i, _c| flag.set(true)),
3909 );
3910 tree.layout(SizeProposal::exact(100.0, 50.0));
3911 tree.focus(widget);
3912
3913 let _h = tree.begin_key_capture(|_ks, _reg, ctx| {
3914 ctx.send_intent(Intent::new("app.save"));
3915 });
3916 tree.press_key(Key::X, Modifiers::COMMAND);
3917 assert!(
3918 ran.get(),
3919 "intent queued from capture callback must dispatch"
3920 );
3921 }
3922
3923 #[test]
3924 fn binding_registry_does_not_accumulate_across_rebuilds() {
3925 use crate::binding::BindingLevel;
3926 use crate::signal::Signal;
3927
3928 #[derive(Debug)]
3929 struct BoundLeaf {
3930 tick: Signal<u64>,
3931 }
3932 impl crate::widget::Widget for BoundLeaf {
3933 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
3934 self.tick.bind_to(
3935 ctx.self_id(),
3936 ctx.binding_registry(),
3937 BindingLevel::Relayout,
3938 );
3939 Vec::new()
3940 }
3941 fn layout_response(
3942 &self,
3943 proposal: SizeProposal,
3944 _ctx: &crate::widget::LayoutContext,
3945 ) -> crate::widget::LayoutResponse {
3946 proposal.resolve(10.0, 10.0).into()
3947 }
3948 }
3949
3950 let mut tree = WidgetTree::new();
3951 let tick = Signal::new(0_u64);
3952 let widget = tree.add(BoundLeaf { tick: tick.clone() });
3953 tree.layout(SizeProposal::exact(200.0, 200.0));
3954 let after_first_build = tree.binding_registry().len();
3955 assert!(after_first_build >= 1);
3956
3957 for _ in 0..5 {
3961 tree.arena.mark_needs_rebuild(widget);
3962 tree.layout(SizeProposal::exact(200.0, 200.0));
3963 }
3964 assert_eq!(
3965 tree.binding_registry().len(),
3966 after_first_build,
3967 "bindings must be cleared on rebuild"
3968 );
3969
3970 tree.destroy_subtree(widget);
3971 assert_eq!(
3972 tree.binding_registry().len(),
3973 0,
3974 "bindings must be cleared on destroy"
3975 );
3976 let _ = tick;
3978 }
3979
3980 #[test]
3981 fn ctx_destroy_cancels_animations_and_bindings_via_deferred_path() {
3982 use crate::binding::BindingLevel;
3990 use crate::signal::Signal;
3991 use std::time::{Duration, Instant};
3992 use teksilo_tokens::Easing;
3993
3994 #[derive(Debug)]
3995 struct BoundLeaf {
3996 tick: Signal<u64>,
3997 }
3998 impl crate::widget::Widget for BoundLeaf {
3999 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
4000 self.tick.bind_to(
4001 ctx.self_id(),
4002 ctx.binding_registry(),
4003 BindingLevel::Relayout,
4004 );
4005 Vec::new()
4006 }
4007 fn layout_response(
4008 &self,
4009 proposal: SizeProposal,
4010 _ctx: &crate::widget::LayoutContext,
4011 ) -> crate::widget::LayoutResponse {
4012 proposal.resolve(10.0, 10.0).into()
4013 }
4014 }
4015
4016 let mut tree = WidgetTree::new();
4017 let widget = tree.add(BoundLeaf {
4018 tick: Signal::new(0_u64),
4019 });
4020 tree.layout(SizeProposal::exact(200.0, 200.0));
4021 assert!(tree.binding_registry().len() >= 1);
4022
4023 let anim = Signal::<f32>::new_animated(0.0);
4026 tree.animation_scheduler.animate(
4027 &anim,
4028 widget,
4029 1.0,
4030 Duration::from_secs(10),
4031 Easing::Linear,
4032 Instant::now(),
4033 );
4034 assert_eq!(tree.animation_scheduler.active_count(), 1);
4035
4036 let mut noop = crate::window::NoopWindowOps;
4038 tree.run_with_event_context(&mut noop, |ctx| ctx.destroy(widget));
4039
4040 assert_eq!(
4041 tree.animation_scheduler.active_count(),
4042 0,
4043 "ctx.destroy must cancel animations owned by the destroyed widget"
4044 );
4045 assert_eq!(
4046 tree.binding_registry().len(),
4047 0,
4048 "ctx.destroy must unregister the destroyed widget's bindings"
4049 );
4050 assert!(
4051 tree.arena.get(widget).is_none(),
4052 "node must be removed from the arena"
4053 );
4054 }
4055
4056 #[test]
4057 fn clear_shortcut_override_via_event_context_restores_default() {
4058 use crate::shortcut::{KeyStroke, Shortcut};
4059
4060 let mut tree = WidgetTree::new();
4061 tree.shortcut_registry_mut().register(
4062 Shortcut::new("app.save")
4063 .primary(KeyStroke::command(Key::S))
4064 .build(),
4065 );
4066 tree.shortcut_registry_mut()
4067 .rebind_primary("app.save", Some(KeyStroke::alt(Key::S)));
4068
4069 let source = tree.add(FillWidget::new());
4070 let mut ctx = EventContext::new();
4071 ctx.clear_shortcut_override("app.save");
4072 tree.collect_from_ctx(ctx, source);
4073
4074 assert_eq!(
4075 tree.shortcut_registry()
4076 .effective("app.save")
4077 .unwrap()
4078 .primary,
4079 Some(KeyStroke::command(Key::S))
4080 );
4081 }
4082
4083 #[test]
4084 fn rebind_shortcut_primary_via_event_context() {
4085 use crate::shortcut::{KeyStroke, Shortcut};
4086
4087 let mut tree = WidgetTree::new();
4088 tree.shortcut_registry_mut().register(
4089 Shortcut::new("app.save")
4090 .primary(KeyStroke::command(Key::S))
4091 .build(),
4092 );
4093 let source = tree.add(FillWidget::new());
4094
4095 let mut ctx = EventContext::new();
4096 ctx.rebind_shortcut_primary("app.save", Some(KeyStroke::alt(Key::S)));
4097 tree.collect_from_ctx(ctx, source);
4098
4099 assert_eq!(
4100 tree.shortcut_registry()
4101 .effective("app.save")
4102 .unwrap()
4103 .primary,
4104 Some(KeyStroke::alt(Key::S))
4105 );
4106 }
4107
4108 #[test]
4109 fn unregister_all_for_owner_called_on_destroy() {
4110 use crate::shortcut::{KeyStroke, Shortcut};
4111
4112 let mut tree = WidgetTree::new();
4113 let widget = tree.add(FillWidget::new());
4114 let widget_owner = widget;
4115 tree.shortcut_registry_mut().register_owned(
4116 Shortcut::new("scoped.thing")
4117 .primary(KeyStroke::command(Key::K))
4118 .build(),
4119 widget_owner,
4120 );
4121 assert!(
4122 tree.shortcut_registry()
4123 .get_default("scoped.thing")
4124 .is_some()
4125 );
4126
4127 tree.destroy_subtree(widget);
4128 assert!(
4129 tree.shortcut_registry()
4130 .get_default("scoped.thing")
4131 .is_none(),
4132 "destroying the owner must unregister its shortcut"
4133 );
4134 }
4135
4136 #[test]
4140 fn global_action_reached_from_unrelated_source() {
4141 use crate::action::Action;
4142 use crate::intent::Intent;
4143 use std::cell::Cell;
4144 use std::rc::Rc;
4145
4146 #[derive(Debug)]
4147 struct Registrar(Rc<Cell<bool>>);
4148 impl crate::widget::Widget for Registrar {
4149 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
4150 let flag = self.0.clone();
4151 ctx.register_action_global(
4152 Action::new("test.global").on_invoke(move |_i, _c| flag.set(true)),
4153 );
4154 vec![]
4155 }
4156 fn layout_response(
4157 &self,
4158 _p: teksilo_canvas::SizeProposal,
4159 _c: &crate::widget::LayoutContext,
4160 ) -> crate::widget::LayoutResponse {
4161 teksilo_canvas::Size::new(0.0, 0.0).into()
4162 }
4163 }
4164
4165 let mut tree = WidgetTree::new();
4166 let fired = Rc::new(Cell::new(false));
4167 let registrar = tree.add(Registrar(fired.clone()));
4168 let source = tree.add(FillWidget::new()); let mut ops = crate::window::NoopWindowOps;
4170
4171 tree.dispatch_intent(source, Intent::new("test.global"), true, &mut ops);
4172 assert!(
4173 fired.get(),
4174 "global action must fire from an unrelated source"
4175 );
4176
4177 fired.set(false);
4179 tree.destroy_subtree(registrar);
4180 tree.dispatch_intent(source, Intent::new("test.global"), true, &mut ops);
4181 assert!(
4182 !fired.get(),
4183 "destroying the owner must remove its global action"
4184 );
4185 }
4186
4187 #[test]
4199 fn hit_test_through_translate_scope() {
4200 use crate::test_widgets::StackWidget;
4201 let mut tree = WidgetTree::new();
4202 let child = tree.add(FillWidget::new());
4203 let parent = tree.add(StackWidget::new().add_child(child));
4204 tree.set_transform(parent, teksilo_canvas::Transform2D::translate(100.0, 0.0));
4206 tree.layout(SizeProposal::exact(100.0, 50.0));
4207
4208 assert_eq!(
4212 tree.hit_test(Point::new(50.0, 25.0)),
4213 None,
4214 "pre-transform area is not visually populated and must not hit"
4215 );
4216 assert_eq!(
4218 tree.hit_test(Point::new(150.0, 25.0)),
4219 Some(child),
4220 "visually-rendered area must hit the child"
4221 );
4222 assert_eq!(tree.hit_test(Point::new(250.0, 25.0)), None);
4224 }
4225
4226 #[test]
4227 fn hit_test_through_scale_scope() {
4228 use crate::test_widgets::StackWidget;
4229 let mut tree = WidgetTree::new();
4230 let child = tree.add(FillWidget::new());
4231 let parent = tree.add(StackWidget::new().add_child(child));
4232 tree.set_transform(parent, teksilo_canvas::Transform2D::scale(0.5, 0.5));
4235 tree.layout(SizeProposal::exact(100.0, 50.0));
4236
4237 assert_eq!(tree.hit_test(Point::new(25.0, 12.0)), Some(child));
4239 assert_eq!(
4242 tree.hit_test(Point::new(75.0, 25.0)),
4243 None,
4244 "scaled-out region must not hit"
4245 );
4246 }
4247
4248 #[test]
4249 fn hit_test_through_nested_transforms_compose() {
4250 use crate::test_widgets::StackWidget;
4251 let mut tree = WidgetTree::new();
4252 let leaf = tree.add(FillWidget::new());
4253 let inner = tree.add(StackWidget::new().add_child(leaf));
4254 let outer = tree.add(StackWidget::new().add_child(inner));
4255 tree.set_transform(outer, teksilo_canvas::Transform2D::translate(100.0, 0.0));
4259 tree.set_transform(inner, teksilo_canvas::Transform2D::scale(2.0, 2.0));
4260 tree.layout(SizeProposal::exact(50.0, 25.0));
4261
4262 assert_eq!(tree.hit_test(Point::new(150.0, 25.0)), Some(leaf));
4266 assert_eq!(tree.hit_test(Point::new(50.0, 25.0)), None);
4267 assert_eq!(tree.hit_test(Point::new(250.0, 25.0)), None);
4268 }
4269
4270 #[test]
4271 fn hit_test_identity_transform_unchanged() {
4272 let mut tree = WidgetTree::new();
4276 let widget = tree.add(FillWidget::new());
4277 tree.set_transform(widget, teksilo_canvas::Transform2D::IDENTITY);
4278 tree.layout(SizeProposal::exact(100.0, 50.0));
4279 assert_eq!(tree.hit_test(Point::new(50.0, 25.0)), Some(widget));
4280 }
4281
4282 #[test]
4283 fn arena_effective_transform_composes_ancestors() {
4284 use crate::test_widgets::StackWidget;
4295 let mut tree = WidgetTree::new();
4296 let leaf = tree.add(FillWidget::new());
4297 let inner = tree.add(StackWidget::new().add_child(leaf));
4298 let outer = tree.add(StackWidget::new().add_child(inner));
4299 tree.set_transform(outer, teksilo_canvas::Transform2D::translate(100.0, 0.0));
4300 tree.set_transform(inner, teksilo_canvas::Transform2D::scale(2.0, 2.0));
4301 tree.layout(SizeProposal::exact(50.0, 25.0));
4302
4303 let eff = tree.arena.effective_transform(leaf);
4304 let expected = teksilo_canvas::Transform2D::scale(2.0, 2.0)
4305 .then(&teksilo_canvas::Transform2D::translate(100.0, 0.0));
4306 for (a, b) in eff.m.iter().zip(expected.m.iter()) {
4307 assert!(
4308 (a - b).abs() < 1e-5,
4309 "effective_transform mismatch: got {:?}, want {:?}",
4310 eff.m,
4311 expected.m
4312 );
4313 }
4314
4315 let screen_origin = eff.apply_point(Point::new(0.0, 0.0));
4321 assert!((screen_origin.x - 100.0).abs() < 1e-5);
4322 assert!((screen_origin.y - 0.0).abs() < 1e-5);
4323 let screen_corner = eff.apply_point(Point::new(50.0, 25.0));
4326 assert!((screen_corner.x - 200.0).abs() < 1e-5);
4327 assert!((screen_corner.y - 50.0).abs() < 1e-5);
4328 }
4329
4330 #[derive(Debug)]
4335 struct StubMenu;
4336 impl crate::widget::Widget for StubMenu {
4337 fn layout_response(
4338 &self,
4339 _proposal: SizeProposal,
4340 _ctx: &crate::widget::LayoutContext,
4341 ) -> crate::widget::LayoutResponse {
4342 teksilo_canvas::Size::new(100.0, 40.0).into()
4343 }
4344 }
4345
4346 #[derive(Debug)]
4358 struct NominatingWidget {
4359 row: std::cell::Cell<Option<WidgetId>>,
4360 }
4361
4362 impl crate::widget::Widget for NominatingWidget {
4363 fn layout_response(
4364 &self,
4365 proposal: SizeProposal,
4366 _ctx: &LayoutContext,
4367 ) -> crate::widget::LayoutResponse {
4368 proposal.resolve(50.0, 20.0).into()
4369 }
4370
4371 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
4372 ctx.apply_self_handlers(crate::widget_builder::HandlerSet::new().focusable(true));
4373 Vec::new()
4374 }
4375
4376 fn context_menu_key_target(&self) -> Option<WidgetId> {
4377 self.row.get()
4378 }
4379 }
4380
4381 fn press(tree: &mut WidgetTree, key: Key, modifiers: Modifiers) {
4382 tree.dispatch_event(WidgetEvent::KeyDown {
4383 key,
4384 modifiers,
4385 text: None,
4386 });
4387 }
4388
4389 #[test]
4390 fn the_context_menu_key_opens_the_focused_widget_menu() {
4391 use std::cell::Cell;
4392 use std::rc::Rc;
4393
4394 let opened = Rc::new(Cell::new(false));
4395 let flag = opened.clone();
4396 let mut tree = WidgetTree::new();
4397 let widget = tree.add(
4398 FillWidget::new()
4399 .focusable()
4400 .context_menu(move |_pos, _ctx| {
4401 flag.set(true);
4402 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
4403 }),
4404 );
4405 tree.layout(SizeProposal::exact(200.0, 100.0));
4406 tree.focus(widget);
4407
4408 press(&mut tree, Key::ContextMenu, Modifiers::NONE);
4409 assert!(opened.get(), "the dedicated Menu key must open the menu");
4410 }
4411
4412 #[test]
4415 fn shift_f10_opens_the_focused_widget_menu() {
4416 use std::cell::Cell;
4417 use std::rc::Rc;
4418
4419 let opened = Rc::new(Cell::new(false));
4420 let flag = opened.clone();
4421 let mut tree = WidgetTree::new();
4422 let widget = tree.add(
4423 FillWidget::new()
4424 .focusable()
4425 .context_menu(move |_pos, _ctx| {
4426 flag.set(true);
4427 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
4428 }),
4429 );
4430 tree.layout(SizeProposal::exact(200.0, 100.0));
4431 tree.focus(widget);
4432
4433 press(&mut tree, Key::F10, Modifiers::SHIFT);
4434 assert!(opened.get(), "Shift+F10 must open the menu");
4435 }
4436
4437 #[test]
4440 fn a_near_miss_chord_is_not_a_context_menu_request() {
4441 use std::cell::Cell;
4442 use std::rc::Rc;
4443
4444 let opened = Rc::new(Cell::new(false));
4445 let flag = opened.clone();
4446 let mut tree = WidgetTree::new();
4447 let widget = tree.add(
4448 FillWidget::new()
4449 .focusable()
4450 .context_menu(move |_pos, _ctx| {
4451 flag.set(true);
4452 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
4453 }),
4454 );
4455 tree.layout(SizeProposal::exact(200.0, 100.0));
4456 tree.focus(widget);
4457
4458 press(&mut tree, Key::F10, Modifiers::SHIFT | Modifiers::CTRL);
4459 press(&mut tree, Key::F10, Modifiers::NONE);
4460 assert!(!opened.get(), "only Shift+F10 exactly asks for a menu");
4461 }
4462
4463 #[test]
4467 fn the_keyboard_target_can_be_a_row_rather_than_the_focused_container() {
4468 use std::cell::Cell;
4469 use std::rc::Rc;
4470
4471 let menu_owner = Rc::new(Cell::new(None::<&'static str>));
4472
4473 let row_flag = menu_owner.clone();
4474 let container_flag = menu_owner.clone();
4475
4476 let mut tree = WidgetTree::new();
4477 let row = tree.add(FillWidget::new().context_menu(move |_pos, _ctx| {
4478 row_flag.set(Some("row"));
4479 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
4480 }));
4481 let container = tree.add(
4482 crate::test_widgets::StackWidget::new()
4483 .add_child(row)
4484 .context_menu(move |_pos, _ctx| {
4485 container_flag.set(Some("container"));
4486 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
4487 }),
4488 );
4489 tree.layout(SizeProposal::exact(200.0, 100.0));
4490
4491 let nominator = tree.add(NominatingWidget {
4493 row: std::cell::Cell::new(Some(row)),
4494 });
4495 tree.layout(SizeProposal::exact(200.0, 100.0));
4496 tree.focus(nominator);
4497 let _ = container;
4498
4499 press(&mut tree, Key::ContextMenu, Modifiers::NONE);
4500 assert_eq!(
4501 menu_owner.get(),
4502 Some("row"),
4503 "the nominated row's factory must be the one that runs"
4504 );
4505 }
4506
4507 #[test]
4510 fn the_chord_falls_through_when_there_is_no_menu_to_show() {
4511 use std::cell::Cell;
4512 use std::rc::Rc;
4513
4514 let saw_key = Rc::new(Cell::new(false));
4515 let flag = saw_key.clone();
4516 let mut tree = WidgetTree::new();
4517 let widget = tree.add(FillWidget::new().focusable().on_key(move |ev, _ctx| {
4518 if matches!(ev, WidgetEvent::KeyDown { key: Key::F10, .. }) {
4519 flag.set(true);
4520 }
4521 crate::event::EventResponse::Ignored
4522 }));
4523 tree.layout(SizeProposal::exact(200.0, 100.0));
4524 tree.focus(widget);
4525
4526 press(&mut tree, Key::F10, Modifiers::SHIFT);
4527 assert!(
4528 saw_key.get(),
4529 "with no factory anywhere, the key must reach the widget"
4530 );
4531 }
4532
4533 #[test]
4534 fn context_menu_factory_receives_click_position() {
4535 use crate::event::{Modifiers, PointerButton};
4536 use std::cell::Cell;
4537 use std::rc::Rc;
4538
4539 let captured_position = Rc::new(Cell::new(None::<Point>));
4540 let cap = captured_position.clone();
4541 let mut tree = WidgetTree::new();
4542 let widget = tree.add(FillWidget::new().context_menu(move |pos, _ctx| {
4543 cap.set(Some(pos));
4544 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
4545 }));
4546 tree.layout(SizeProposal::exact(200.0, 100.0));
4547
4548 let click = Point::new(73.0, 42.0);
4549 tree.dispatch_event(WidgetEvent::PointerDown {
4550 position: click,
4551 button: PointerButton::Secondary,
4552 modifiers: Modifiers::NONE,
4553 });
4554
4555 let got = captured_position.get();
4556 assert_eq!(
4557 got,
4558 Some(click),
4559 "factory must receive the click position; got {:?}",
4560 got
4561 );
4562 let _ = widget;
4563 }
4564
4565 #[test]
4566 fn context_menu_factory_returning_none_falls_through_to_parent() {
4567 use crate::event::{Modifiers, PointerButton};
4568 use crate::test_widgets::StackWidget;
4569 use std::cell::Cell;
4570 use std::rc::Rc;
4571
4572 let outer_called = Rc::new(Cell::new(0_u32));
4576 let outer_flag = outer_called.clone();
4577 let mut tree = WidgetTree::new();
4578 let inner = tree.add(FillWidget::new().context_menu(|_pos, _ctx| None));
4579 let _outer = tree.add(StackWidget::new().add_child(inner).context_menu(
4580 move |_pos, _ctx| {
4581 outer_flag.set(outer_flag.get() + 1);
4582 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
4583 },
4584 ));
4585 tree.layout(SizeProposal::exact(200.0, 100.0));
4586
4587 tree.dispatch_event(WidgetEvent::PointerDown {
4588 position: Point::new(50.0, 25.0),
4589 button: PointerButton::Secondary,
4590 modifiers: Modifiers::NONE,
4591 });
4592
4593 assert_eq!(
4594 outer_called.get(),
4595 1,
4596 "inner returning None must fall through to the outer factory"
4597 );
4598 }
4599
4600 #[test]
4601 fn context_menu_factory_none_throughout_chain_does_not_show_overlay() {
4602 use crate::event::{Modifiers, PointerButton};
4603
4604 let mut tree = WidgetTree::new();
4606 tree.add(FillWidget::new().context_menu(|_pos, _ctx| None));
4607 tree.layout(SizeProposal::exact(200.0, 100.0));
4608
4609 let overlay_count_before = tree.overlay_manager.len();
4610 tree.dispatch_event(WidgetEvent::PointerDown {
4611 position: Point::new(50.0, 25.0),
4612 button: PointerButton::Secondary,
4613 modifiers: Modifiers::NONE,
4614 });
4615 let overlay_count_after = tree.overlay_manager.len();
4616 assert_eq!(
4617 overlay_count_before, overlay_count_after,
4618 "a factory returning None must not mount any overlay"
4619 );
4620 }
4621
4622 #[derive(Debug)]
4633 struct FreshChildHost {
4634 preserve: bool,
4635 }
4636 impl Widget for FreshChildHost {
4637 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
4638 vec![ctx.add(FillWidget::new())]
4639 }
4640 fn layout_response(
4641 &self,
4642 p: SizeProposal,
4643 _c: &LayoutContext,
4644 ) -> crate::widget::LayoutResponse {
4645 p.resolve(10.0, 10.0).into()
4646 }
4647 fn preserves_children_on_rebuild(&self) -> bool {
4648 self.preserve
4649 }
4650 }
4651
4652 #[test]
4653 fn reconcile_reaps_dropped_children_no_leak() {
4654 for preserve in [false, true] {
4659 let mut tree = WidgetTree::new();
4660 let host = tree.add(FreshChildHost { preserve });
4661 tree.layout(SizeProposal::exact(100.0, 100.0));
4662 let total0 = tree.arena.len();
4663 let active0 = tree.active_widget_count();
4664 for _ in 0..5 {
4665 tree.arena_mark_needs_rebuild_for_testing(host);
4666 tree.layout(SizeProposal::exact(100.0, 100.0));
4667 }
4668 assert_eq!(
4669 tree.arena.len(),
4670 total0,
4671 "preserve={preserve}: dropped children must be reaped, not leaked"
4672 );
4673 assert_eq!(
4674 tree.active_widget_count(),
4675 active0,
4676 "preserve={preserve}: no stranded still-active orphans"
4677 );
4678 }
4679 }
4680
4681 #[derive(Debug)]
4687 struct DetachedContentHost {
4688 preserve: bool,
4689 }
4690 impl Widget for DetachedContentHost {
4691 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
4692 let popup = ctx.add_detached(FillWidget::new());
4693 ctx.set_dormant(popup);
4694 vec![ctx.add(FillWidget::new())]
4695 }
4696 fn layout_response(
4697 &self,
4698 p: SizeProposal,
4699 _c: &LayoutContext,
4700 ) -> crate::widget::LayoutResponse {
4701 p.resolve(10.0, 10.0).into()
4702 }
4703 fn preserves_children_on_rebuild(&self) -> bool {
4704 self.preserve
4705 }
4706 }
4707
4708 #[test]
4709 fn rebuilding_reaps_detached_content_no_leak() {
4710 for preserve in [false, true] {
4716 let mut tree = WidgetTree::new();
4717 let host = tree.add(DetachedContentHost { preserve });
4718 tree.layout(SizeProposal::exact(100.0, 100.0));
4719 let total0 = tree.arena.len();
4720 for _ in 0..5 {
4721 tree.arena_mark_needs_rebuild_for_testing(host);
4722 tree.layout(SizeProposal::exact(100.0, 100.0));
4723 }
4724 assert_eq!(
4725 tree.arena.len(),
4726 total0,
4727 "preserve={preserve}: the previous build's detached content must be reaped"
4728 );
4729 }
4730 }
4731
4732 #[test]
4733 fn destroying_a_host_reaps_its_detached_content() {
4734 let mut tree = WidgetTree::new();
4735 let outer = tree.add(FillWidget::new());
4736 tree.layout(SizeProposal::exact(100.0, 100.0));
4737 let empty = tree.arena.len();
4738
4739 let host = tree.add_child(outer, DetachedContentHost { preserve: false });
4740 tree.layout(SizeProposal::exact(100.0, 100.0));
4741 assert!(tree.arena.len() > empty);
4742
4743 tree.destroy_subtree(host);
4744 assert_eq!(
4745 tree.arena.len(),
4746 empty,
4747 "the popup must die with the widget that built it"
4748 );
4749 }
4750
4751 #[derive(Debug)]
4753 struct StableChildHost {
4754 child: Option<WidgetId>,
4755 probe: std::rc::Rc<std::cell::Cell<Option<WidgetId>>>,
4756 }
4757 impl Widget for StableChildHost {
4758 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
4759 let id = match self.child {
4760 Some(id) => id,
4761 None => {
4762 let id = ctx.add(FillWidget::new());
4763 self.child = Some(id);
4764 self.probe.set(Some(id));
4765 id
4766 }
4767 };
4768 vec![id]
4769 }
4770 fn layout_response(
4771 &self,
4772 p: SizeProposal,
4773 _c: &LayoutContext,
4774 ) -> crate::widget::LayoutResponse {
4775 p.resolve(10.0, 10.0).into()
4776 }
4777 fn preserves_children_on_rebuild(&self) -> bool {
4778 true
4779 }
4780 }
4781
4782 #[test]
4783 fn reconcile_preserves_reattached_child() {
4784 let probe = std::rc::Rc::new(std::cell::Cell::new(None));
4785 let mut tree = WidgetTree::new();
4786 let host = tree.add(StableChildHost {
4787 child: None,
4788 probe: probe.clone(),
4789 });
4790 tree.layout(SizeProposal::exact(100.0, 100.0));
4791 let child = probe.get().expect("child mounted");
4792 let total0 = tree.arena.len();
4793 for _ in 0..5 {
4794 tree.arena_mark_needs_rebuild_for_testing(host);
4795 tree.layout(SizeProposal::exact(100.0, 100.0));
4796 }
4797 assert!(
4798 tree.arena.is_active(child),
4799 "the re-attached child must survive every rebuild"
4800 );
4801 assert_eq!(tree.arena.len(), total0, "no growth — same child reused");
4802 }
4803
4804 #[derive(Debug)]
4806 struct Wrapper {
4807 child: WidgetId,
4808 }
4809 impl Widget for Wrapper {
4810 fn build(&mut self, _ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
4811 vec![self.child]
4812 }
4813 fn layout_response(
4814 &self,
4815 p: SizeProposal,
4816 _c: &LayoutContext,
4817 ) -> crate::widget::LayoutResponse {
4818 p.resolve(10.0, 10.0).into()
4819 }
4820 }
4821
4822 #[derive(Debug)]
4826 struct ReparentHost {
4827 body: Option<WidgetId>,
4828 probe: std::rc::Rc<std::cell::Cell<Option<WidgetId>>>,
4829 }
4830 impl Widget for ReparentHost {
4831 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
4832 let body = match self.body {
4833 Some(id) => id,
4834 None => {
4835 let id = ctx.add(FillWidget::new());
4836 self.body = Some(id);
4837 self.probe.set(Some(id));
4838 id
4839 }
4840 };
4841 vec![ctx.add(Wrapper { child: body })]
4842 }
4843 fn layout_response(
4844 &self,
4845 p: SizeProposal,
4846 _c: &LayoutContext,
4847 ) -> crate::widget::LayoutResponse {
4848 p.resolve(10.0, 10.0).into()
4849 }
4850 fn preserves_children_on_rebuild(&self) -> bool {
4851 true
4852 }
4853 }
4854
4855 #[test]
4856 fn reconcile_spares_reparented_survivor() {
4857 let probe = std::rc::Rc::new(std::cell::Cell::new(None));
4864 let mut tree = WidgetTree::new();
4865 let host = tree.add(ReparentHost {
4866 body: None,
4867 probe: probe.clone(),
4868 });
4869 tree.layout(SizeProposal::exact(100.0, 100.0));
4870 let body = probe.get().expect("body mounted");
4871 let total0 = tree.arena.len();
4872 for _ in 0..5 {
4873 tree.arena_mark_needs_rebuild_for_testing(host);
4874 tree.layout(SizeProposal::exact(100.0, 100.0));
4875 }
4876 assert!(
4877 tree.arena.is_active(body),
4878 "the re-parented body must survive — it was moved into the new tree, \
4879 not swept with the dropped wrapper"
4880 );
4881 assert_eq!(
4882 tree.arena.len(),
4883 total0,
4884 "dropped wrappers reaped — no per-rebuild leak"
4885 );
4886 }
4887
4888 #[derive(Debug)]
4898 struct BelowContainer {
4899 child: Option<WidgetId>,
4900 offset: f32,
4901 }
4902
4903 impl crate::widget::Widget for BelowContainer {
4904 fn layout_response(
4905 &self,
4906 proposal: SizeProposal,
4907 _ctx: &crate::widget::LayoutContext,
4908 ) -> crate::widget::LayoutResponse {
4909 proposal.resolve(0.0, 0.0).into()
4910 }
4911 fn place_children(
4912 &self,
4913 bounds: Rect,
4914 _proposal: SizeProposal,
4915 children: &mut [crate::widget::WidgetPlacement],
4916 _ctx: &crate::widget::LayoutContext,
4917 ) {
4918 for c in children.iter_mut() {
4919 c.origin = Point::new(bounds.x, bounds.y + self.offset);
4920 c.size = bounds.size();
4921 }
4922 }
4923 fn children(&self) -> Vec<WidgetId> {
4924 self.child.into_iter().collect()
4925 }
4926 }
4927
4928 fn recording_scroll_container(
4931 tree: &mut WidgetTree,
4932 child: WidgetId,
4933 recorded: std::rc::Rc<std::cell::Cell<Option<Rect>>>,
4934 ) -> WidgetId {
4935 use crate::test_widgets::StackWidget;
4936 tree.add(
4937 StackWidget::new()
4938 .add_child(child)
4939 .on_scroll(move |ev, _ctx| match ev {
4940 WidgetEvent::ScrollIntoView { target_bounds, .. } => {
4941 recorded.set(Some(*target_bounds));
4942 EventResponse::Handled
4943 }
4944 _ => EventResponse::Ignored,
4945 })
4946 .clips_children(true),
4947 )
4948 }
4949
4950 #[test]
4951 fn ensure_visible_dispatches_scroll_into_view_to_clipping_ancestor() {
4952 use std::cell::Cell;
4953 use std::rc::Rc;
4954 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
4955 let mut tree = WidgetTree::new();
4956 let actor = tree.add(FillWidget::new());
4957 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
4958 tree.layout(SizeProposal::exact(100.0, 100.0));
4959
4960 let target = Rect::new(10.0, 500.0, 20.0, 15.0);
4963 let mut ctx = EventContext::new();
4964 ctx.ensure_visible(target);
4965 tree.collect_from_ctx(ctx, actor);
4966
4967 assert_eq!(
4968 recorded.get(),
4969 Some(target),
4970 "ensure_visible(rect) must dispatch ScrollIntoView with the exact rect \
4971 to the clips_children ancestor"
4972 );
4973 }
4974
4975 #[test]
4976 fn ensure_visible_is_noop_when_rect_already_visible() {
4977 use std::cell::Cell;
4978 use std::rc::Rc;
4979 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
4980 let mut tree = WidgetTree::new();
4981 let actor = tree.add(FillWidget::new());
4982 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
4983 tree.layout(SizeProposal::exact(100.0, 100.0));
4984
4985 let mut ctx = EventContext::new();
4988 ctx.ensure_visible(Rect::new(10.0, 10.0, 20.0, 15.0));
4989 tree.collect_from_ctx(ctx, actor);
4990
4991 assert_eq!(
4992 recorded.get(),
4993 None,
4994 "a rect already inside the viewport must not trigger a scroll"
4995 );
4996 }
4997
4998 #[test]
4999 fn ensure_visible_margin_forces_scroll_near_edge() {
5000 use std::cell::Cell;
5001 use std::rc::Rc;
5002 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
5003 let mut tree = WidgetTree::new();
5004 let actor = tree.add(FillWidget::new());
5005 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
5006 tree.layout(SizeProposal::exact(100.0, 100.0));
5007
5008 let rect = Rect::new(10.0, 95.0, 20.0, 4.0);
5011 let mut ctx = EventContext::new();
5012 ctx.ensure_visible_with_margin(rect, 10.0);
5013 tree.collect_from_ctx(ctx, actor);
5014
5015 assert_eq!(
5016 recorded.get(),
5017 Some(rect),
5018 "the margin must widen the visibility test so a near-edge rect scrolls"
5019 );
5020 }
5021
5022 fn recording_align_container(
5025 tree: &mut WidgetTree,
5026 child: WidgetId,
5027 recorded: std::rc::Rc<
5028 std::cell::Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>,
5029 >,
5030 ) -> WidgetId {
5031 use crate::test_widgets::StackWidget;
5032 tree.add(
5033 StackWidget::new()
5034 .add_child(child)
5035 .on_scroll(move |ev, _ctx| match ev {
5036 WidgetEvent::ScrollIntoView { align, motion, .. } => {
5037 recorded.set(Some((*align, *motion)));
5038 EventResponse::Handled
5039 }
5040 _ => EventResponse::Ignored,
5041 })
5042 .clips_children(true),
5043 )
5044 }
5045
5046 #[test]
5047 fn ensure_visible_aligned_scrolls_even_when_already_visible() {
5048 use std::cell::Cell;
5049 use std::rc::Rc;
5050 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
5051 let mut tree = WidgetTree::new();
5052 let actor = tree.add(FillWidget::new());
5053 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
5054 tree.layout(SizeProposal::exact(100.0, 100.0));
5055
5056 let target = Rect::new(10.0, 10.0, 20.0, 15.0);
5061 let mut ctx = EventContext::new();
5062 ctx.ensure_visible_aligned(target, 0.5, crate::event::ScrollMotion::Instant);
5063 tree.collect_from_ctx(ctx, actor);
5064
5065 assert_eq!(
5066 recorded.get(),
5067 Some(target),
5068 "an aligned reveal must dispatch even when the rect is already visible"
5069 );
5070 }
5071
5072 #[test]
5073 fn ensure_visible_aligned_forwards_fraction_and_motion() {
5074 use std::cell::Cell;
5075 use std::rc::Rc;
5076 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
5077 Rc::new(Cell::new(None));
5078 let mut tree = WidgetTree::new();
5079 let actor = tree.add(FillWidget::new());
5080 let _container = recording_align_container(&mut tree, actor, recorded.clone());
5081 tree.layout(SizeProposal::exact(100.0, 100.0));
5082
5083 let mut ctx = EventContext::new();
5084 ctx.ensure_visible_aligned(
5085 Rect::new(10.0, 10.0, 20.0, 15.0),
5086 0.25,
5087 crate::event::ScrollMotion::Smooth,
5088 );
5089 tree.collect_from_ctx(ctx, actor);
5090
5091 assert_eq!(
5092 recorded.get(),
5093 Some((
5094 crate::event::ScrollAlign::Fraction(0.25),
5095 crate::event::ScrollMotion::Smooth
5096 )),
5097 "the container must receive the requested fraction and motion verbatim"
5098 );
5099 }
5100
5101 #[test]
5102 fn ensure_visible_aligned_clamps_the_fraction() {
5103 use std::cell::Cell;
5104 use std::rc::Rc;
5105 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
5106 Rc::new(Cell::new(None));
5107 let mut tree = WidgetTree::new();
5108 let actor = tree.add(FillWidget::new());
5109 let _container = recording_align_container(&mut tree, actor, recorded.clone());
5110 tree.layout(SizeProposal::exact(100.0, 100.0));
5111
5112 let mut ctx = EventContext::new();
5113 ctx.ensure_visible_aligned(
5114 Rect::new(10.0, 10.0, 20.0, 15.0),
5115 4.2,
5116 crate::event::ScrollMotion::Instant,
5117 );
5118 tree.collect_from_ctx(ctx, actor);
5119
5120 assert_eq!(
5121 recorded.get().map(|(a, _)| a),
5122 Some(crate::event::ScrollAlign::Fraction(1.0)),
5123 "an out-of-range fraction must clamp rather than aim the pin off-screen"
5124 );
5125 }
5126
5127 #[test]
5128 fn plain_ensure_visible_requests_minimal_alignment() {
5129 use std::cell::Cell;
5130 use std::rc::Rc;
5131 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
5132 Rc::new(Cell::new(None));
5133 let mut tree = WidgetTree::new();
5134 let actor = tree.add(FillWidget::new());
5135 let _container = recording_align_container(&mut tree, actor, recorded.clone());
5136 tree.layout(SizeProposal::exact(100.0, 100.0));
5137
5138 let mut ctx = EventContext::new();
5139 ctx.ensure_visible(Rect::new(10.0, 500.0, 20.0, 15.0));
5140 tree.collect_from_ctx(ctx, actor);
5141
5142 assert_eq!(
5143 recorded.get(),
5144 Some((
5145 crate::event::ScrollAlign::Minimal,
5146 crate::event::ScrollMotion::Instant
5147 )),
5148 "the pre-existing reveal API must keep its exact semantics"
5149 );
5150 }
5151
5152 #[test]
5153 fn only_the_innermost_container_aligns() {
5154 use std::cell::Cell;
5155 use std::rc::Rc;
5156 let inner_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
5157 Rc::new(Cell::new(None));
5158 let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
5159 Rc::new(Cell::new(None));
5160
5161 let mut tree = WidgetTree::new();
5162 let actor = tree.add(FillWidget::new());
5163 let inner = recording_align_container(&mut tree, actor, inner_rec.clone());
5164 let _outer = recording_align_container(&mut tree, inner, outer_rec.clone());
5165 tree.layout(SizeProposal::exact(100.0, 100.0));
5166
5167 let mut ctx = EventContext::new();
5170 ctx.ensure_visible_aligned(
5171 Rect::new(10.0, 500.0, 20.0, 15.0),
5172 0.5,
5173 crate::event::ScrollMotion::Instant,
5174 );
5175 tree.collect_from_ctx(ctx, actor);
5176
5177 assert_eq!(
5178 inner_rec.get().map(|(a, _)| a),
5179 Some(crate::event::ScrollAlign::Fraction(0.5)),
5180 "the innermost clipping ancestor owns the pin"
5181 );
5182 assert_eq!(
5183 outer_rec.get().map(|(a, _)| a),
5184 Some(crate::event::ScrollAlign::Minimal),
5185 "an outer container must only bring the inner viewport into view — a \
5186 fraction names a height in one viewport, not in every ancestor's"
5187 );
5188 }
5189
5190 #[test]
5191 fn ensure_widget_visible_uses_target_arena_bounds() {
5192 use std::cell::Cell;
5193 use std::rc::Rc;
5194 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
5195 let mut tree = WidgetTree::new();
5196 let target = tree.add(FillWidget::new());
5198 let rec = recorded.clone();
5199 let container = tree.add(
5200 BelowContainer {
5201 child: Some(target),
5202 offset: 500.0,
5203 }
5204 .on_scroll(move |ev, _ctx| match ev {
5205 WidgetEvent::ScrollIntoView { target_bounds, .. } => {
5206 rec.set(Some(*target_bounds));
5207 EventResponse::Handled
5208 }
5209 _ => EventResponse::Ignored,
5210 })
5211 .clips_children(true),
5212 );
5213 tree.layout(SizeProposal::exact(100.0, 100.0));
5214
5215 let expected = tree.bounds(target);
5216 assert!(
5217 expected.y > 100.0,
5218 "fixture sanity: the target must sit below the viewport (y={})",
5219 expected.y
5220 );
5221
5222 let mut ctx = EventContext::new();
5225 ctx.ensure_widget_visible(target);
5226 tree.collect_from_ctx(ctx, container);
5227
5228 assert_eq!(
5229 recorded.get(),
5230 Some(expected),
5231 "ensure_widget_visible(id) must dispatch ScrollIntoView with the \
5232 target's current arena bounds"
5233 );
5234 }
5235
5236 #[test]
5237 fn ensure_widget_visible_ignores_missing_widget() {
5238 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 actor = tree.add(FillWidget::new());
5244 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
5245 tree.layout(SizeProposal::exact(100.0, 100.0));
5246
5247 let mut ctx = EventContext::new();
5248 ctx.ensure_widget_visible(WidgetId::default());
5249 tree.collect_from_ctx(ctx, actor); assert_eq!(
5252 recorded.get(),
5253 None,
5254 "an unmounted id must not trigger a scroll"
5255 );
5256 }
5257
5258 #[test]
5259 fn context_menu_inside_a_modal_keeps_the_modal() {
5260 use crate::event::{Modifiers, PointerButton, WidgetEvent};
5264 use crate::overlay::{DismissBehavior, OverlayLayer, OverlayPlacement, OverlayRequest};
5265 use crate::test_widgets::{FillWidget, StackWidget};
5266
5267 let mut tree = WidgetTree::new();
5268 let modal_content = tree.add(StackWidget::new());
5271 let _editor = tree.add_child(
5272 modal_content,
5273 FillWidget::new()
5274 .context_menu(|_pos, _ctx| Some(Box::new(FillWidget::new()) as Box<dyn Widget>)),
5275 );
5276 tree.layout(SizeProposal::exact(200.0, 100.0));
5277
5278 let modal = tree.overlay_manager.show(OverlayRequest {
5279 content_id: modal_content,
5280 anchor: modal_content,
5281 placement: OverlayPlacement::Centered,
5282 dismiss: DismissBehavior::EscapeKey,
5283 layer: OverlayLayer::InTree,
5284 parent_overlay: None,
5285 on_dismiss: None,
5286 fade_duration: None,
5287 });
5288 tree.overlay_manager
5290 .stack
5291 .iter_mut()
5292 .find(|o| o.id == modal)
5293 .unwrap()
5294 .bounds = Rect::new(0.0, 0.0, 200.0, 100.0);
5295 assert_eq!(tree.overlay_manager.len(), 1);
5296
5297 tree.dispatch_event(WidgetEvent::PointerDown {
5299 position: Point::new(50.0, 25.0),
5300 button: PointerButton::Secondary,
5301 modifiers: Modifiers::NONE,
5302 });
5303
5304 assert!(
5305 tree.overlay_manager.active_ids().contains(&modal),
5306 "the modal must survive opening a context menu inside it"
5307 );
5308 assert_eq!(
5309 tree.overlay_manager.len(),
5310 2,
5311 "the context menu should now be open on top of the surviving modal"
5312 );
5313 }
5314}