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::RowSpaceActivate { row, fallback } => {
2114 self.keyboard_toggle_in(row).unwrap_or(fallback)();
2122 }
2123 TreeMutation::WithWidgetMut { id, dirty, apply } => {
2124 let existed = if let Some(any) =
2129 self.arena.get_mut(id).and_then(|n| n.widget.as_any_mut())
2130 {
2131 apply(any);
2132 true
2133 } else {
2134 false
2135 };
2136 if existed {
2137 match dirty {
2138 BindingLevel::RepaintOnly => self.arena.mark_needs_paint(id),
2139 BindingLevel::SubtreeRepaint => self.arena.mark_subtree_needs_paint(id),
2140 BindingLevel::Relayout => {
2141 self.arena.mark_needs_layout(id);
2142 self.arena.mark_ancestors_need_layout(id);
2143 }
2144 BindingLevel::Rebuild => {
2145 self.arena.mark_needs_rebuild(id);
2146 self.arena.mark_ancestors_need_layout(id);
2147 }
2148 BindingLevel::AccessibilityOnly => self.a11y_dirty = true,
2149 }
2150 }
2151 }
2152 }
2153 }
2154 }
2155
2156 pub fn hit_test(&self, point: Point) -> Option<WidgetId> {
2157 self.hit_test_excluding_overlay_and_widget(point, None, None)
2158 }
2159
2160 pub fn hit_test_excluding_overlay_and_widget(
2164 &self,
2165 point: Point,
2166 exclude_overlay: Option<crate::overlay::OverlayId>,
2167 exclude_widget: Option<WidgetId>,
2168 ) -> Option<WidgetId> {
2169 if let Some(overlay_id) = self.overlay_manager.hit_test(point) {
2170 if Some(overlay_id) == exclude_overlay {
2171 } else if let Some(overlay) = self.overlay_manager.overlay(overlay_id) {
2173 return self.arena.hit_test_in_subtree_excluding(
2174 overlay.content_id,
2175 point,
2176 exclude_widget,
2177 );
2178 }
2179 }
2180
2181 if self.overlay_manager.topmost_centered().is_some() {
2182 return None;
2183 }
2184
2185 self.arena.hit_test_at(point, exclude_widget)
2188 }
2189}
2190
2191fn is_context_menu_chord(key: Key, modifiers: Modifiers) -> bool {
2209 match key {
2210 Key::ContextMenu => modifiers == Modifiers::NONE,
2211 Key::F10 => modifiers == Modifiers::SHIFT,
2212 #[cfg(target_os = "macos")]
2213 Key::M => modifiers == Modifiers::CTRL | Modifiers::SHIFT,
2214 _ => false,
2215 }
2216}
2217
2218#[cfg(test)]
2219mod tests {
2220 use super::*;
2221 use crate::test_widgets::FillWidget;
2222 use crate::widget::CursorIcon;
2223 use crate::widget_builder::WidgetBuilder;
2224
2225 #[test]
2226 fn pointer_enter_leave_synthesized() {
2227 let mut tree = WidgetTree::new();
2228 let widget = tree.add(FillWidget::new());
2229 tree.layout(SizeProposal::exact(100.0, 50.0));
2230 tree.pointer_move(Point::new(50.0, 25.0));
2231 assert_eq!(tree.hovered, Some(widget));
2232 tree.pointer_move(Point::new(200.0, 200.0));
2233 assert_eq!(tree.hovered, None);
2234 }
2235
2236 #[test]
2237 fn pointer_hover_updates_current_cursor() {
2238 let mut tree = WidgetTree::new();
2239 tree.add(FillWidget::new().cursor(CursorIcon::ColResize));
2240 tree.layout(SizeProposal::exact(100.0, 50.0));
2241
2242 tree.pointer_move(Point::new(50.0, 25.0));
2243 assert_eq!(tree.current_cursor(), CursorIcon::ColResize);
2244
2245 tree.pointer_move(Point::new(200.0, 200.0));
2246 assert_eq!(tree.current_cursor(), CursorIcon::Default);
2247 }
2248
2249 #[derive(Debug)]
2252 struct Bumpable {
2253 value: i32,
2254 }
2255
2256 impl crate::widget::Widget for Bumpable {
2257 fn layout_response(
2258 &self,
2259 proposal: SizeProposal,
2260 _ctx: &crate::widget::LayoutContext,
2261 ) -> crate::widget::LayoutResponse {
2262 proposal.resolve(10.0, 10.0).into()
2263 }
2264 fn as_any(&self) -> Option<&dyn std::any::Any> {
2265 Some(self)
2266 }
2267 fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
2268 Some(self)
2269 }
2270 }
2271
2272 #[test]
2273 fn with_widget_mut_applies_and_dirty_marks() {
2274 let mut tree = WidgetTree::new();
2275 let id = tree.add(Bumpable { value: 0 });
2276 tree.layout(SizeProposal::exact(100.0, 100.0));
2277
2278 let mut ctx = EventContext::new();
2279 ctx.with_widget_mut::<Bumpable>(id, crate::binding::BindingLevel::Relayout, |b| {
2280 b.value = 42;
2281 });
2282 tree.collect_from_ctx(ctx, id);
2283
2284 let value = tree
2285 .widget_as_any(id)
2286 .and_then(|a| a.downcast_ref::<Bumpable>())
2287 .map(|b| b.value);
2288 assert_eq!(
2289 value,
2290 Some(42),
2291 "the deferred closure must mutate the live widget"
2292 );
2293 assert!(
2294 tree.needs_layout(),
2295 "Relayout dirty level must mark the tree for relayout"
2296 );
2297 }
2298
2299 #[test]
2300 #[cfg(debug_assertions)]
2301 #[should_panic(expected = "not the requested type")]
2302 fn with_widget_mut_wrong_type_panics_in_debug() {
2303 struct Other;
2304 let mut tree = WidgetTree::new();
2305 let id = tree.add(Bumpable { value: 0 });
2306 let mut ctx = EventContext::new();
2307 ctx.with_widget_mut::<Other>(
2308 id,
2309 crate::binding::BindingLevel::RepaintOnly,
2310 |_o: &mut Other| {},
2311 );
2312 tree.collect_from_ctx(ctx, id);
2315 }
2316
2317 #[test]
2318 fn with_widget_mut_closure_may_fire_observed_signals() {
2319 use crate::signal::Signal;
2327 let mut tree = WidgetTree::new();
2328 let id = tree.add(Bumpable { value: 0 });
2329 tree.layout(SizeProposal::exact(100.0, 100.0));
2330
2331 let trigger = Signal::new(0_u64);
2332 let echo = Signal::new(0_u64);
2333 let echo_for_obs = echo.clone();
2334 let _obs = trigger.observe(move |v| echo_for_obs.set(*v));
2335
2336 let trigger_in = trigger.clone();
2337 let mut ctx = EventContext::new();
2338 ctx.with_widget_mut::<Bumpable>(id, crate::binding::BindingLevel::RepaintOnly, move |b| {
2339 b.value = 7;
2340 trigger_in.set(99);
2342 });
2343 tree.collect_from_ctx(ctx, id); assert_eq!(
2346 echo.get(),
2347 99,
2348 "the observer ran during the deferred mutation"
2349 );
2350 let value = tree
2351 .widget_as_any(id)
2352 .and_then(|a| a.downcast_ref::<Bumpable>())
2353 .map(|b| b.value);
2354 assert_eq!(value, Some(7));
2355 }
2356
2357 #[test]
2358 fn request_accessibility_update_forces_rewalk() {
2359 let mut tree = WidgetTree::new();
2360 let id = tree.add(Bumpable { value: 0 });
2361 tree.layout(SizeProposal::exact(100.0, 100.0));
2362 let _ = tree.sync_accessibility(); assert!(
2364 !tree.a11y_dirty,
2365 "sync_accessibility should clear the dirty flag"
2366 );
2367
2368 let mut ctx = EventContext::new();
2369 ctx.request_accessibility_update();
2370 tree.collect_from_ctx(ctx, id);
2371 assert!(
2372 tree.a11y_dirty,
2373 "request_accessibility_update must force an AT re-walk"
2374 );
2375 }
2376
2377 #[test]
2378 fn rebuild_dirties_accessibility_tree() {
2379 let mut tree = WidgetTree::new();
2386 let id = tree.add(Bumpable { value: 0 });
2387 tree.layout(SizeProposal::exact(100.0, 100.0));
2388 let _ = tree.sync_accessibility(); assert!(
2390 !tree.a11y_dirty,
2391 "sync_accessibility should clear the dirty flag"
2392 );
2393
2394 tree.arena_mark_needs_rebuild_for_testing(id);
2397 tree.layout(SizeProposal::exact(100.0, 100.0));
2398 assert!(
2399 tree.a11y_dirty,
2400 "a rebuild must dirty the AT tree so the next sync re-walks"
2401 );
2402 }
2403
2404 #[test]
2405 fn bound_access_label_change_dirties_accessibility_tree() {
2406 use crate::signal::Signal;
2407 use crate::test_widgets::FillWidget;
2408 use crate::widget_builder::WidgetBuilder;
2409
2410 let label = Signal::new("first".to_string());
2416 let mut tree = WidgetTree::new();
2417 let _id = tree.add(FillWidget::new().access_label(label.clone()));
2418 tree.layout(SizeProposal::exact(100.0, 100.0));
2419 let _ = tree.sync_accessibility(); assert!(!tree.a11y_dirty, "sync_accessibility should clear the flag");
2421
2422 label.set("second".to_string());
2423 tree.layout(SizeProposal::exact(100.0, 100.0));
2424 assert!(
2425 tree.a11y_dirty,
2426 "changing a bound access_label must dirty the AT tree"
2427 );
2428 }
2429
2430 #[test]
2431 fn disabled_ancestor_blocks_event_to_descendant() {
2432 use crate::signal::Signal;
2433 use crate::test_widgets::StackWidget;
2434 use std::cell::Cell;
2435 use std::rc::Rc;
2436
2437 let tapped = Rc::new(Cell::new(false));
2438 let flag = tapped.clone();
2439 let enabled = Signal::new(true);
2440
2441 let mut tree = WidgetTree::new();
2442 let child = tree.add(FillWidget::new().on_tap(move |_pos, _ctx| {
2443 flag.set(true);
2444 }));
2445 let parent = tree.add(StackWidget::new().add_child(child));
2446 tree.enabled_when(parent, enabled.clone());
2447 tree.layout(SizeProposal::exact(100.0, 50.0));
2448
2449 enabled.set(false);
2450 tree.click(child);
2451 assert!(
2452 !tapped.get(),
2453 "disabled ancestor should block descendant tap"
2454 );
2455
2456 enabled.set(true);
2457 tree.click(child);
2458 assert!(tapped.get(), "re-enabling should restore dispatch");
2459 }
2460
2461 #[test]
2462 fn pointer_positions_are_widget_local_at_nonzero_origin() {
2463 use crate::event::{Modifiers, PointerButton};
2464 use crate::test_widgets::InsetWidget;
2465 use std::cell::Cell;
2466 use std::rc::Rc;
2467
2468 let tap_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
2470 let down_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
2471 let drag_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
2472 let (tp, dp, gp) = (tap_pos.clone(), down_pos.clone(), drag_pos.clone());
2473
2474 let mut tree = WidgetTree::new();
2475 let child = tree.add(
2476 FillWidget::new()
2477 .on_tap(move |ev, _ctx| tp.set(Some(ev.position)))
2478 .on_pointer_event(move |ev, _ctx| {
2479 if let WidgetEvent::PointerDown { position, .. } = ev {
2480 dp.set(Some(*position));
2481 }
2482 crate::event::EventResponse::Ignored
2483 })
2484 .on_drag(move |phase, _ctx| {
2485 use crate::gesture::DragPhase;
2486 match phase {
2487 DragPhase::Started { position, .. }
2488 | DragPhase::Moved { position, .. }
2489 | DragPhase::Ended { position } => gp.set(Some(position)),
2490 }
2491 }),
2492 );
2493 let inset = tree.add(InsetWidget::new(20.0).set_child(child));
2494 let _ = inset;
2495 tree.layout(SizeProposal::exact(200.0, 200.0));
2496 assert_eq!(tree.bounds(child).origin(), Point::new(20.0, 20.0));
2497
2498 tree.dispatch_event(WidgetEvent::PointerDown {
2500 position: Point::new(50.0, 40.0),
2501 button: PointerButton::Primary,
2502 modifiers: Modifiers::NONE,
2503 });
2504 assert_eq!(
2505 down_pos.get(),
2506 Some(Point::new(30.0, 20.0)),
2507 "on_pointer_event PointerDown must be widget-local"
2508 );
2509 tree.dispatch_event(WidgetEvent::PointerUp {
2510 position: Point::new(50.0, 40.0),
2511 button: PointerButton::Primary,
2512 modifiers: Modifiers::NONE,
2513 });
2514 assert_eq!(
2515 tap_pos.get(),
2516 Some(Point::new(30.0, 20.0)),
2517 "on_tap position must be widget-local"
2518 );
2519
2520 tree.dispatch_event(WidgetEvent::PointerDown {
2523 position: Point::new(50.0, 40.0),
2524 button: PointerButton::Primary,
2525 modifiers: Modifiers::NONE,
2526 });
2527 tree.dispatch_event(WidgetEvent::PointerMove {
2530 position: Point::new(65.0, 55.0),
2531 });
2532 tree.dispatch_event(WidgetEvent::PointerMove {
2533 position: Point::new(90.0, 70.0),
2534 });
2535 assert_eq!(
2536 drag_pos.get(),
2537 Some(Point::new(70.0, 50.0)),
2538 "on_drag position must be widget-local"
2539 );
2540 }
2541
2542 #[test]
2543 fn dormant_widget_not_hit_tested() {
2544 let mut tree = WidgetTree::new();
2545 let widget = tree.add(FillWidget::new());
2546 tree.layout(SizeProposal::exact(100.0, 50.0));
2547
2548 tree.pointer_move(Point::new(50.0, 25.0));
2549 assert_eq!(tree.hovered, Some(widget));
2550
2551 tree.set_dormant(widget);
2552 tree.pointer_move(Point::new(200.0, 200.0));
2553 tree.pointer_move(Point::new(50.0, 25.0));
2554 assert_eq!(tree.hovered, None);
2555 }
2556
2557 #[test]
2558 fn ancestor_pointer_handler_does_not_suppress_descendant_hover() {
2559 use crate::event::EventResponse;
2560 use crate::test_widgets::StackWidget;
2561 use std::cell::Cell;
2562 use std::rc::Rc;
2563
2564 let hovered = Rc::new(Cell::new(false));
2566 let h = hovered.clone();
2567
2568 let mut tree = WidgetTree::new();
2569 let child = tree.add(FillWidget::new().on_hover(move |entered, _ctx| h.set(entered)));
2570 tree.add(
2575 StackWidget::new()
2576 .add_child(child)
2577 .on_pointer_event(|_event, _ctx| EventResponse::Handled),
2578 );
2579 tree.layout(SizeProposal::exact(100.0, 50.0));
2580
2581 tree.pointer_move(Point::new(50.0, 25.0));
2582 assert!(
2583 hovered.get(),
2584 "a greedy ancestor on_pointer_event must NOT swallow the child's PointerEnter"
2585 );
2586
2587 tree.pointer_move(Point::new(500.0, 500.0));
2588 assert!(
2589 !hovered.get(),
2590 "PointerLeave must likewise reach the child despite the ancestor"
2591 );
2592 }
2593
2594 #[test]
2602 fn a_child_hover_handler_does_not_swallow_its_ancestors() {
2603 use crate::test_widgets::StackWidget;
2604 use std::cell::Cell;
2605 use std::rc::Rc;
2606
2607 let (row, button) = (Rc::new(Cell::new(false)), Rc::new(Cell::new(false)));
2608 let (r, b) = (row.clone(), button.clone());
2609
2610 let mut tree = WidgetTree::new();
2611 let child = tree.add(FillWidget::new().on_hover(move |entered, _ctx| b.set(entered)));
2612 tree.add(
2613 StackWidget::new()
2614 .add_child(child)
2615 .on_hover(move |entered, _ctx| r.set(entered)),
2616 );
2617 tree.layout(SizeProposal::exact(100.0, 50.0));
2618
2619 tree.pointer_move(Point::new(50.0, 25.0));
2620 assert!(button.get(), "the child is hovered");
2621 assert!(row.get(), "and so is the row it is inside");
2622
2623 tree.pointer_move(Point::new(500.0, 500.0));
2624 assert!(!button.get(), "the child heard the leave");
2625 assert!(
2626 !row.get(),
2627 "and so did the row — a container is not still hovered because the \
2628 pointer left it through a button"
2629 );
2630 }
2631
2632 #[test]
2633 fn destroy_subtree_clears_dangling_pointer_capture() {
2634 use crate::event::{EventResponse, Modifiers, PointerButton};
2635 use crate::test_widgets::StackWidget;
2636
2637 let mut tree = WidgetTree::new();
2638 let child = tree.add(FillWidget::new().on_pointer_event(|event, ctx| {
2639 if matches!(event, WidgetEvent::PointerDown { .. }) {
2640 ctx.capture_pointer();
2641 }
2642 EventResponse::Ignored
2643 }));
2644 let parent = tree.add(StackWidget::new().add_child(child));
2645 tree.layout(SizeProposal::exact(100.0, 50.0));
2646
2647 tree.dispatch_event(WidgetEvent::PointerDown {
2649 position: Point::new(50.0, 25.0),
2650 button: PointerButton::Primary,
2651 modifiers: Modifiers::NONE,
2652 });
2653 assert_eq!(
2654 tree.pointer_captured_by,
2655 Some(child),
2656 "PointerDown handler should have captured the pointer"
2657 );
2658
2659 tree.destroy_subtree(parent);
2663 assert_eq!(
2664 tree.pointer_captured_by, None,
2665 "destroy_subtree must clear a capture anchored at a destroyed widget"
2666 );
2667 }
2668
2669 #[test]
2676 fn key_preview_consumes_before_focused_on_key() {
2677 use crate::event::EventResponse;
2680 use crate::test_widgets::StackWidget;
2681 use std::cell::Cell;
2682 use std::rc::Rc;
2683
2684 let leaf_fired = Rc::new(Cell::new(false));
2685 let leaf_flag = leaf_fired.clone();
2686 let preview_fired = Rc::new(Cell::new(false));
2687 let preview_flag = preview_fired.clone();
2688
2689 let mut tree = WidgetTree::new();
2690 let leaf = tree.add(FillWidget::new().focusable().on_key(move |event, _c| {
2691 if matches!(event, WidgetEvent::KeyDown { .. }) {
2694 leaf_flag.set(true);
2695 }
2696 EventResponse::Handled
2697 }));
2698 let mid = tree.add(StackWidget::new().add_child(leaf));
2699 let _root =
2700 tree.add(StackWidget::new().add_child(mid).on_key_preview(
2701 move |event, _c| match event {
2702 WidgetEvent::KeyDown {
2703 key: Key::Enter, ..
2704 } => {
2705 preview_flag.set(true);
2706 EventResponse::Handled
2707 }
2708 _ => EventResponse::Ignored,
2709 },
2710 ));
2711
2712 tree.layout(SizeProposal::exact(100.0, 50.0));
2713 tree.focus(leaf);
2714 tree.press_key(Key::Enter, Modifiers::NONE);
2715
2716 assert!(
2717 preview_fired.get(),
2718 "ancestor on_key_preview must fire for KeyDown on a focused descendant"
2719 );
2720 assert!(
2721 !leaf_fired.get(),
2722 "consuming the event in preview must prevent the focused widget's on_key from running"
2723 );
2724 }
2725
2726 #[test]
2727 fn key_preview_falls_through_when_returning_ignored() {
2728 use crate::event::EventResponse;
2731 use crate::test_widgets::StackWidget;
2732 use std::cell::Cell;
2733 use std::rc::Rc;
2734
2735 let leaf_fired = Rc::new(Cell::new(false));
2736 let leaf_flag = leaf_fired.clone();
2737 let preview_fired = Rc::new(Cell::new(false));
2738 let preview_flag = preview_fired.clone();
2739
2740 let mut tree = WidgetTree::new();
2741 let leaf = tree.add(FillWidget::new().focusable().on_key(move |_e, _c| {
2742 leaf_flag.set(true);
2743 EventResponse::Handled
2744 }));
2745 let mid = tree.add(StackWidget::new().add_child(leaf));
2746 let _root = tree.add(StackWidget::new().add_child(mid).on_key_preview(
2747 move |_event, _c| {
2748 preview_flag.set(true);
2749 EventResponse::Ignored
2750 },
2751 ));
2752
2753 tree.layout(SizeProposal::exact(100.0, 50.0));
2754 tree.focus(leaf);
2755 tree.press_key(Key::Enter, Modifiers::NONE);
2756
2757 assert!(preview_fired.get(), "preview must always be invoked");
2758 assert!(
2759 leaf_fired.get(),
2760 "preview returning Ignored must not block the focused widget's on_key"
2761 );
2762 }
2763
2764 #[test]
2765 fn key_preview_excludes_focused_target_itself() {
2766 use crate::event::EventResponse;
2770 use std::cell::Cell;
2771 use std::rc::Rc;
2772
2773 let preview_on_target = Rc::new(Cell::new(false));
2774 let pf = preview_on_target.clone();
2775
2776 let mut tree = WidgetTree::new();
2777 let leaf = tree.add(FillWidget::new().focusable().on_key_preview(move |_e, _c| {
2778 pf.set(true);
2779 EventResponse::Handled
2780 }));
2781 tree.layout(SizeProposal::exact(100.0, 50.0));
2782 tree.focus(leaf);
2783 tree.press_key(Key::Enter, Modifiers::NONE);
2784
2785 assert!(
2786 !preview_on_target.get(),
2787 "the focused widget itself must not see its own on_key_preview"
2788 );
2789 }
2790
2791 #[test]
2792 fn key_preview_root_to_target_order() {
2793 use crate::event::EventResponse;
2797 use crate::test_widgets::StackWidget;
2798 use std::cell::RefCell;
2799 use std::rc::Rc;
2800
2801 let order: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
2802 let outer_log = order.clone();
2803 let inner_log = order.clone();
2804
2805 let mut tree = WidgetTree::new();
2806 let leaf = tree.add(FillWidget::new().focusable());
2807 let inner = tree.add(StackWidget::new().add_child(leaf).on_key_preview(
2808 move |event, _c| {
2809 if matches!(event, WidgetEvent::KeyDown { .. }) {
2810 inner_log.borrow_mut().push("inner");
2811 }
2812 EventResponse::Ignored
2813 },
2814 ));
2815 let _outer = tree.add(StackWidget::new().add_child(inner).on_key_preview(
2816 move |event, _c| {
2817 if matches!(event, WidgetEvent::KeyDown { .. }) {
2818 outer_log.borrow_mut().push("outer");
2819 }
2820 EventResponse::Ignored
2821 },
2822 ));
2823
2824 tree.layout(SizeProposal::exact(100.0, 50.0));
2825 tree.focus(leaf);
2826 tree.dispatch_event(WidgetEvent::KeyDown {
2827 key: Key::Enter,
2828 modifiers: Modifiers::NONE,
2829 text: None,
2830 });
2831
2832 assert_eq!(
2833 *order.borrow(),
2834 vec!["outer", "inner"],
2835 "preview must walk root → parent-of-target"
2836 );
2837 }
2838
2839 #[test]
2840 fn access_action_routes_to_cursored_target_not_focus() {
2841 use crate::signal::Signal;
2845 let a_fired = Signal::new(false);
2846 let b_fired = Signal::new(false);
2847 let a_cb = a_fired.clone();
2848 let b_cb = b_fired.clone();
2849
2850 let mut tree = WidgetTree::new();
2851 let a = tree.add(
2852 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| a_cb.set(true)),
2853 );
2854 let b = tree.add(
2855 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| b_cb.set(true)),
2856 );
2857 tree.layout(SizeProposal::exact(200.0, 100.0));
2858
2859 tree.focus(a);
2860 tree.dispatch_event(WidgetEvent::AccessAction {
2861 action: accesskit::Action::Click,
2862 target: Some(b),
2863 target_node: crate::accessibility::widget_id_to_node_id(b),
2864 data: None,
2865 });
2866
2867 assert!(b_fired.get(), "the cursored target must receive the action");
2868 assert!(
2869 !a_fired.get(),
2870 "the keyboard-focused widget must NOT receive an action targeting another node"
2871 );
2872 }
2873
2874 #[test]
2875 fn access_action_without_target_is_dropped_not_redirected_to_focus() {
2876 use crate::signal::Signal;
2879 let fired = Signal::new(false);
2880 let cb = fired.clone();
2881
2882 let mut tree = WidgetTree::new();
2883 let widget = tree.add(
2884 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| cb.set(true)),
2885 );
2886 tree.layout(SizeProposal::exact(200.0, 100.0));
2887
2888 tree.focus(widget);
2889 tree.dispatch_event(WidgetEvent::AccessAction {
2890 action: accesskit::Action::Click,
2891 target: None,
2892 target_node: crate::accessibility::root_node_id(),
2893 data: None,
2894 });
2895
2896 assert!(
2897 !fired.get(),
2898 "a target-less action must not be redirected to the focused widget"
2899 );
2900 }
2901
2902 #[test]
2909 fn shortcut_fires_matching_action_on_source_widget() {
2910 use crate::action::Action;
2911 use crate::shortcut::{KeyStroke, Shortcut};
2912 use std::cell::Cell;
2913 use std::rc::Rc;
2914
2915 let fired = Rc::new(Cell::new(false));
2916 let fired_flag = fired.clone();
2917
2918 let mut tree = WidgetTree::new();
2919 let widget = tree.add(FillWidget::new().focusable());
2920 tree.push_action(
2921 widget,
2922 Action::new("app.save").on_invoke(move |_intent, _ctx| {
2923 fired_flag.set(true);
2924 }),
2925 );
2926 tree.shortcut_registry_mut().register(
2927 Shortcut::new("app.save")
2928 .primary(KeyStroke::command(Key::S))
2929 .build(),
2930 );
2931
2932 tree.layout(SizeProposal::exact(100.0, 50.0));
2933 tree.focus(widget);
2934
2935 tree.press_key(Key::S, Modifiers::COMMAND);
2936 assert!(fired.get(), "matching action must fire on KeyDown");
2937 }
2938
2939 #[test]
2940 fn global_shortcut_fires_without_focused_widget() {
2941 use crate::action::Action;
2942 use crate::shortcut::{KeyStroke, Shortcut};
2943 use std::cell::Cell;
2944 use std::rc::Rc;
2945
2946 let fired = Rc::new(Cell::new(false));
2950 let fired_flag = fired.clone();
2951
2952 let mut tree = WidgetTree::new();
2953 let root = tree.add(FillWidget::new());
2954 tree.push_action(
2955 root,
2956 Action::new("app.save").on_invoke(move |_intent, _ctx| {
2957 fired_flag.set(true);
2958 }),
2959 );
2960 tree.shortcut_registry_mut().register(
2961 Shortcut::new("app.save")
2962 .primary(KeyStroke::command(Key::S))
2963 .build(),
2964 );
2965
2966 tree.layout(SizeProposal::exact(100.0, 50.0));
2967 tree.press_key(Key::S, Modifiers::COMMAND);
2970 assert!(
2971 fired.get(),
2972 "global shortcut must fire without a focused widget"
2973 );
2974 }
2975
2976 #[test]
2977 fn global_shortcut_fires_after_focused_widget_destroyed() {
2978 use crate::action::Action;
2979 use crate::shortcut::{KeyStroke, Shortcut};
2980 use std::cell::Cell;
2981 use std::rc::Rc;
2982
2983 let fired = Rc::new(Cell::new(false));
2988 let fired_flag = fired.clone();
2989
2990 let mut tree = WidgetTree::new();
2991 let root = tree.add(FillWidget::new());
2992 let focusable = tree.add_child(root, FillWidget::new().focusable());
2993 tree.push_action(
2994 root,
2995 Action::new("app.save").on_invoke(move |_intent, _ctx| {
2996 fired_flag.set(true);
2997 }),
2998 );
2999 tree.shortcut_registry_mut().register(
3000 Shortcut::new("app.save")
3001 .primary(KeyStroke::command(Key::S))
3002 .build(),
3003 );
3004
3005 tree.layout(SizeProposal::exact(100.0, 50.0));
3006 tree.focus(focusable);
3007 assert_eq!(tree.focused(), Some(focusable));
3008
3009 tree.destroy_subtree(focusable);
3012 assert_eq!(tree.focused(), None, "focus must clear when destroyed");
3013
3014 tree.press_key(Key::S, Modifiers::COMMAND);
3015 assert!(
3016 fired.get(),
3017 "global shortcut must still fire after the focused widget is destroyed"
3018 );
3019 }
3020
3021 #[test]
3022 fn scoped_shortcut_matches_only_when_focus_in_scope() {
3023 use crate::action::Action;
3024 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
3025 use std::cell::Cell;
3026 use std::rc::Rc;
3027
3028 let fired = Rc::new(Cell::new(0));
3029 let fired_flag = fired.clone();
3030
3031 let mut tree = WidgetTree::new();
3032 let scope_root = tree.add(FillWidget::new().focusable());
3033 let inside = tree.add_child(scope_root, FillWidget::new().focusable());
3034 let outside = tree.add(FillWidget::new().focusable());
3035
3036 tree.push_action(
3037 scope_root,
3038 Action::new("editor.find").on_invoke(move |_i, _c| {
3039 fired_flag.set(fired_flag.get() + 1);
3040 }),
3041 );
3042 tree.shortcut_registry_mut().register(
3043 Shortcut::new("editor.find")
3044 .primary(KeyStroke::command(Key::F))
3045 .scope(ShortcutScope::Scoped(scope_root))
3046 .build(),
3047 );
3048
3049 tree.layout(SizeProposal::exact(200.0, 100.0));
3050
3051 tree.focus(outside);
3053 tree.press_key(Key::F, Modifiers::COMMAND);
3054 assert_eq!(
3055 fired.get(),
3056 0,
3057 "scoped shortcut must not fire outside scope"
3058 );
3059
3060 tree.focus(inside);
3062 tree.press_key(Key::F, Modifiers::COMMAND);
3063 assert_eq!(
3064 fired.get(),
3065 1,
3066 "scoped shortcut must fire when focus in scope"
3067 );
3068 }
3069
3070 #[test]
3071 fn same_chord_scoped_first_falls_back_to_global_when_focus_outside() {
3072 use crate::action::Action;
3078 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
3079 use std::cell::Cell;
3080 use std::rc::Rc;
3081
3082 let scoped_fired = Rc::new(Cell::new(0));
3083 let global_fired = Rc::new(Cell::new(0));
3084 let sf = scoped_fired.clone();
3085 let gf = global_fired.clone();
3086
3087 let mut tree = WidgetTree::new();
3088 let root = tree.add(FillWidget::new());
3089 let editor = tree.add_child(root, FillWidget::new().focusable());
3090 let _editor_inner = tree.add_child(editor, FillWidget::new().focusable());
3091 let sidebar = tree.add_child(root, FillWidget::new().focusable());
3092
3093 tree.push_action(
3094 editor,
3095 Action::new("editor.saveBlock").on_invoke(move |_i, _c| sf.set(sf.get() + 1)),
3096 );
3097 tree.push_action(
3098 root,
3099 Action::new("zzz.global.save").on_invoke(move |_i, _c| gf.set(gf.get() + 1)),
3100 );
3101 tree.shortcut_registry_mut().register(
3102 Shortcut::new("editor.saveBlock")
3103 .primary(KeyStroke::command(Key::S))
3104 .scope(ShortcutScope::Scoped(editor))
3105 .build(),
3106 );
3107 tree.shortcut_registry_mut().register(
3108 Shortcut::new("zzz.global.save")
3109 .primary(KeyStroke::command(Key::S))
3110 .build(),
3111 );
3112
3113 tree.layout(SizeProposal::exact(200.0, 100.0));
3114 tree.focus(sidebar);
3115 tree.press_key(Key::S, Modifiers::COMMAND);
3116
3117 assert_eq!(global_fired.get(), 1, "applicable global must fire");
3118 assert_eq!(
3119 scoped_fired.get(),
3120 0,
3121 "inapplicable scoped binding must not eat the chord"
3122 );
3123 }
3124
3125 #[test]
3126 fn same_chord_global_first_yields_to_scoped_when_focus_inside() {
3127 use crate::action::Action;
3132 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
3133 use std::cell::Cell;
3134 use std::rc::Rc;
3135
3136 let scoped_fired = Rc::new(Cell::new(0));
3137 let global_fired = Rc::new(Cell::new(0));
3138 let sf = scoped_fired.clone();
3139 let gf = global_fired.clone();
3140
3141 let mut tree = WidgetTree::new();
3142 let root = tree.add(FillWidget::new());
3143 let editor = tree.add_child(root, FillWidget::new().focusable());
3144 let editor_inner = tree.add_child(editor, FillWidget::new().focusable());
3145 let sidebar = tree.add_child(root, FillWidget::new().focusable());
3146
3147 tree.push_action(
3148 editor,
3149 Action::new("editor.saveBlock").on_invoke(move |_i, _c| sf.set(sf.get() + 1)),
3150 );
3151 tree.push_action(
3152 root,
3153 Action::new("app.save").on_invoke(move |_i, _c| gf.set(gf.get() + 1)),
3154 );
3155 tree.shortcut_registry_mut().register(
3156 Shortcut::new("app.save")
3157 .primary(KeyStroke::command(Key::S))
3158 .build(),
3159 );
3160 tree.shortcut_registry_mut().register(
3161 Shortcut::new("editor.saveBlock")
3162 .primary(KeyStroke::command(Key::S))
3163 .scope(ShortcutScope::Scoped(editor))
3164 .build(),
3165 );
3166
3167 tree.layout(SizeProposal::exact(200.0, 100.0));
3168
3169 tree.focus(editor_inner);
3171 tree.press_key(Key::S, Modifiers::COMMAND);
3172 assert_eq!(
3173 scoped_fired.get(),
3174 1,
3175 "in-focus scoped must win over global"
3176 );
3177 assert_eq!(
3178 global_fired.get(),
3179 0,
3180 "global must yield to the scoped binding"
3181 );
3182
3183 tree.focus(sidebar);
3185 tree.press_key(Key::S, Modifiers::COMMAND);
3186 assert_eq!(scoped_fired.get(), 1, "scoped stays put outside its scope");
3187 assert_eq!(
3188 global_fired.get(),
3189 1,
3190 "global fires when focus leaves the scope"
3191 );
3192 }
3193
3194 #[test]
3195 fn propagated_action_lets_ancestor_handle() {
3196 use crate::action::Action;
3197 use crate::intent::IntentResponse;
3198 use crate::shortcut::{KeyStroke, Shortcut};
3199 use std::cell::Cell;
3200 use std::rc::Rc;
3201
3202 let inner_seen = Rc::new(Cell::new(false));
3203 let outer_seen = Rc::new(Cell::new(false));
3204 let inner_flag = inner_seen.clone();
3205 let outer_flag = outer_seen.clone();
3206
3207 let mut tree = WidgetTree::new();
3208 let outer = tree.add(FillWidget::new().focusable());
3209 let inner = tree.add_child(outer, FillWidget::new().focusable());
3210
3211 tree.push_action(
3213 inner,
3214 Action::new("app.save").on_invoke_with_response(move |_i, _c| {
3215 inner_flag.set(true);
3216 IntentResponse::Propagated
3217 }),
3218 );
3219 tree.push_action(
3220 outer,
3221 Action::new("app.save").on_invoke(move |_i, _c| {
3222 outer_flag.set(true);
3223 }),
3224 );
3225 tree.shortcut_registry_mut().register(
3226 Shortcut::new("app.save")
3227 .primary(KeyStroke::command(Key::S))
3228 .build(),
3229 );
3230
3231 tree.layout(SizeProposal::exact(100.0, 50.0));
3232 tree.focus(inner);
3233
3234 tree.press_key(Key::S, Modifiers::COMMAND);
3235 assert!(inner_seen.get(), "inner action observed the intent");
3236 assert!(outer_seen.get(), "outer action reached after Propagated");
3237 }
3238
3239 #[test]
3240 fn handled_action_stops_propagation() {
3241 use crate::action::Action;
3242 use crate::shortcut::{KeyStroke, Shortcut};
3243 use std::cell::Cell;
3244 use std::rc::Rc;
3245
3246 let inner_seen = Rc::new(Cell::new(false));
3247 let outer_seen = Rc::new(Cell::new(false));
3248 let inner_flag = inner_seen.clone();
3249 let outer_flag = outer_seen.clone();
3250
3251 let mut tree = WidgetTree::new();
3252 let outer = tree.add(FillWidget::new().focusable());
3253 let inner = tree.add_child(outer, FillWidget::new().focusable());
3254
3255 tree.push_action(
3256 inner,
3257 Action::new("app.save").on_invoke(move |_i, _c| {
3258 inner_flag.set(true);
3259 }),
3260 );
3261 tree.push_action(
3262 outer,
3263 Action::new("app.save").on_invoke(move |_i, _c| {
3264 outer_flag.set(true);
3265 }),
3266 );
3267 tree.shortcut_registry_mut().register(
3268 Shortcut::new("app.save")
3269 .primary(KeyStroke::command(Key::S))
3270 .build(),
3271 );
3272
3273 tree.layout(SizeProposal::exact(100.0, 50.0));
3274 tree.focus(inner);
3275
3276 tree.press_key(Key::S, Modifiers::COMMAND);
3277 assert!(inner_seen.get());
3278 assert!(!outer_seen.get(), "Handled at inner must stop propagation");
3279 }
3280
3281 #[test]
3282 fn disabled_action_propagates_by_default() {
3283 use crate::action::Action;
3284 use crate::shortcut::{KeyStroke, Shortcut};
3285 use crate::signal::Signal;
3286 use std::cell::Cell;
3287 use std::rc::Rc;
3288
3289 let inner_seen = Rc::new(Cell::new(false));
3290 let outer_seen = Rc::new(Cell::new(false));
3291 let inner_flag = inner_seen.clone();
3292 let outer_flag = outer_seen.clone();
3293
3294 let mut tree = WidgetTree::new();
3295 let outer = tree.add(FillWidget::new().focusable());
3296 let inner = tree.add_child(outer, FillWidget::new().focusable());
3297
3298 let enabled = Signal::new(false);
3299 tree.push_action(
3300 inner,
3301 Action::new("app.save")
3302 .enabled_when(enabled.clone())
3303 .on_invoke(move |_i, _c| {
3304 inner_flag.set(true);
3305 }),
3306 );
3307 tree.push_action(
3308 outer,
3309 Action::new("app.save").on_invoke(move |_i, _c| {
3310 outer_flag.set(true);
3311 }),
3312 );
3313 tree.shortcut_registry_mut().register(
3314 Shortcut::new("app.save")
3315 .primary(KeyStroke::command(Key::S))
3316 .build(),
3317 );
3318
3319 tree.layout(SizeProposal::exact(100.0, 50.0));
3320 tree.focus(inner);
3321
3322 tree.press_key(Key::S, Modifiers::COMMAND);
3323 assert!(!inner_seen.get(), "disabled inner must not run");
3324 assert!(
3325 outer_seen.get(),
3326 "intent must propagate past disabled inner"
3327 );
3328 }
3329
3330 #[test]
3331 fn disabled_action_with_non_propagating_shortcut_consumes() {
3332 use crate::action::Action;
3333 use crate::shortcut::{KeyStroke, Shortcut};
3334 use crate::signal::Signal;
3335 use std::cell::Cell;
3336 use std::rc::Rc;
3337
3338 let inner_seen = Rc::new(Cell::new(false));
3339 let outer_seen = Rc::new(Cell::new(false));
3340 let inner_flag = inner_seen.clone();
3341 let outer_flag = outer_seen.clone();
3342
3343 let mut tree = WidgetTree::new();
3344 let outer = tree.add(FillWidget::new().focusable());
3345 let inner = tree.add_child(outer, FillWidget::new().focusable());
3346
3347 let enabled = Signal::new(false);
3348 tree.push_action(
3349 inner,
3350 Action::new("app.save")
3351 .enabled_when(enabled.clone())
3352 .on_invoke(move |_i, _c| {
3353 inner_flag.set(true);
3354 }),
3355 );
3356 tree.push_action(
3357 outer,
3358 Action::new("app.save").on_invoke(move |_i, _c| {
3359 outer_flag.set(true);
3360 }),
3361 );
3362 tree.shortcut_registry_mut().register(
3363 Shortcut::new("app.save")
3364 .primary(KeyStroke::command(Key::S))
3365 .propagate_when_disabled(false)
3366 .build(),
3367 );
3368
3369 tree.layout(SizeProposal::exact(100.0, 50.0));
3370 tree.focus(inner);
3371
3372 tree.press_key(Key::S, Modifiers::COMMAND);
3373 assert!(!inner_seen.get(), "disabled inner still does not run");
3374 assert!(
3375 !outer_seen.get(),
3376 "intent must NOT propagate when shortcut disallows it"
3377 );
3378 }
3379
3380 #[test]
3381 fn send_intent_from_handler_reaches_ancestor_action() {
3382 use crate::action::Action;
3383 use crate::intent::Intent;
3384 use std::cell::Cell;
3385 use std::rc::Rc;
3386
3387 let save_seen = Rc::new(Cell::new(false));
3388 let save_flag = save_seen.clone();
3389
3390 let mut tree = WidgetTree::new();
3391 let root = tree.add(FillWidget::new());
3392 let button = tree.add_child(
3393 root,
3394 FillWidget::new().on_tap(|_pos, ctx| {
3395 ctx.send_intent(Intent::new("app.save"));
3396 }),
3397 );
3398 tree.push_action(
3399 root,
3400 Action::new("app.save").on_invoke(move |_i, _c| {
3401 save_flag.set(true);
3402 }),
3403 );
3404
3405 tree.layout(SizeProposal::exact(100.0, 50.0));
3406 tree.click(button);
3407 assert!(
3408 save_seen.get(),
3409 "ctx.send_intent must reach ancestor action"
3410 );
3411 }
3412
3413 #[test]
3414 fn widget_type_histogram_counts_distinct_types() {
3415 let mut tree = WidgetTree::new();
3420 let _ = tree.add(FillWidget::new());
3421 let _ = tree.add(FillWidget::new());
3422 let _ = tree.add(FillWidget::new());
3423 tree.layout(SizeProposal::exact(100.0, 100.0));
3424 let histogram = tree.widget_type_histogram();
3425 let total: u32 = histogram.values().sum();
3426 assert!(
3427 total >= 3,
3428 "expected at least 3 active widgets, got {total}: {histogram:?}"
3429 );
3430 let fillwidget_entries: u32 = histogram
3431 .iter()
3432 .filter(|(k, _)| k.contains("FillWidget"))
3433 .map(|(_, v)| *v)
3434 .sum();
3435 assert!(
3436 fillwidget_entries >= 3,
3437 "expected ≥3 FillWidget instances; histogram = {histogram:?}"
3438 );
3439 assert_eq!(tree.active_widget_count() as u32, total);
3440 }
3441
3442 #[test]
3443 fn intent_source_tagged_handler_for_tap_activation() {
3444 use crate::action::Action;
3448 use crate::intent::Intent;
3449 use crate::telemetry::IntentSource;
3450 use std::cell::Cell;
3451 use std::rc::Rc;
3452 let captured = Rc::new(Cell::new(IntentSource::Unknown));
3453 let captured_for_action = captured.clone();
3454
3455 let mut tree = WidgetTree::new();
3456 let root = tree.add(FillWidget::new());
3457 let button = tree.add_child(
3458 root,
3459 FillWidget::new().on_tap(|_pos, ctx| {
3460 ctx.send_intent(Intent::new("app.save"));
3461 }),
3462 );
3463 tree.push_action(
3464 root,
3465 Action::new("app.save").on_invoke(move |intent, _c| {
3466 captured_for_action.set(intent.source);
3467 }),
3468 );
3469
3470 tree.layout(SizeProposal::exact(100.0, 50.0));
3471 tree.click(button);
3472 assert_eq!(
3473 captured.get(),
3474 IntentSource::Handler,
3475 "tap-driven intent must tag IntentSource::Handler"
3476 );
3477 }
3478
3479 #[test]
3480 fn intent_source_programmatic_when_no_handler_active() {
3481 use crate::intent::Intent;
3482 use crate::telemetry::IntentSource;
3483 let intent = Intent::new("app.demo");
3484 assert_eq!(intent.source, IntentSource::Programmatic);
3485
3486 let mut ctx = EventContext::new();
3488 ctx.send_intent(Intent::new("app.demo"));
3489 let queued = ctx.pending_intents.first().expect("intent queued");
3490 assert_eq!(queued.source, IntentSource::Programmatic);
3491 }
3492
3493 #[test]
3494 fn with_intent_source_overrides_for_managed_widgets() {
3495 use crate::intent::Intent;
3496 use crate::telemetry::IntentSource;
3497 let mut ctx = EventContext::new();
3498 ctx.with_intent_source(IntentSource::Menu, |ctx| {
3499 ctx.send_intent(Intent::new("app.demo"));
3500 });
3501 let queued = ctx.pending_intents.first().expect("intent queued");
3502 assert_eq!(
3503 queued.source,
3504 IntentSource::Menu,
3505 "with_intent_source(Menu) must tag the dispatched intent"
3506 );
3507
3508 ctx.send_intent(Intent::new("app.next"));
3512 let next = ctx.pending_intents.last().expect("second intent");
3513 assert_eq!(next.source, IntentSource::Programmatic);
3514 }
3515
3516 #[test]
3517 fn disabled_shortcut_falls_through_to_focused_widget() {
3518 use crate::action::Action;
3519 use crate::shortcut::{KeyStroke, Shortcut};
3520 use crate::signal::Signal;
3521 use std::cell::Cell;
3522 use std::rc::Rc;
3523
3524 let action_fired = Rc::new(Cell::new(false));
3525 let on_key_fired = Rc::new(Cell::new(false));
3526 let af = action_fired.clone();
3527 let kf = on_key_fired.clone();
3528
3529 let enabled = Signal::new(false);
3530
3531 let mut tree = WidgetTree::new();
3532 let widget = tree.add(FillWidget::new().focusable().on_key(move |event, _ctx| {
3533 if matches!(
3534 event,
3535 WidgetEvent::KeyDown {
3536 key: Key::S,
3537 modifiers,
3538 ..
3539 } if modifiers.command()
3540 ) {
3541 kf.set(true);
3542 return EventResponse::Handled;
3543 }
3544 EventResponse::Ignored
3545 }));
3546 tree.push_action(
3547 widget,
3548 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
3549 );
3550 tree.shortcut_registry_mut().register(
3551 Shortcut::new("app.save")
3552 .primary(KeyStroke::command(Key::S))
3553 .enabled_when(enabled.clone())
3554 .build(),
3555 );
3556
3557 tree.layout(SizeProposal::exact(100.0, 50.0));
3558 tree.focus(widget);
3559
3560 tree.press_key(Key::S, Modifiers::COMMAND);
3562 assert!(
3563 !action_fired.get(),
3564 "disabled shortcut must not invoke its action"
3565 );
3566 assert!(
3567 on_key_fired.get(),
3568 "disabled shortcut must let KeyDown reach the focused widget"
3569 );
3570
3571 on_key_fired.set(false);
3573 enabled.set(true);
3574 tree.press_key(Key::S, Modifiers::COMMAND);
3575 assert!(action_fired.get(), "re-enabled shortcut must dispatch");
3576 assert!(
3577 !on_key_fired.get(),
3578 "enabled shortcut must consume the KeyDown"
3579 );
3580 }
3581
3582 #[test]
3583 fn keyboard_capture_bypasses_shortcut() {
3584 use crate::action::Action;
3585 use crate::shortcut::{KeyStroke, Shortcut};
3586 use std::cell::Cell;
3587 use std::rc::Rc;
3588
3589 fn run(capture: bool) -> (bool, bool) {
3595 let action_fired = Rc::new(Cell::new(false));
3596 let on_key_fired = Rc::new(Cell::new(false));
3597 let af = action_fired.clone();
3598 let kf = on_key_fired.clone();
3599
3600 let mut tree = WidgetTree::new();
3601 let widget = tree.add(
3602 FillWidget::new()
3603 .focusable()
3604 .keyboard_capture(capture)
3605 .on_key(move |event, _ctx| {
3606 if matches!(
3607 event,
3608 WidgetEvent::KeyDown { key: Key::S, modifiers, .. } if modifiers.command()
3609 ) {
3610 kf.set(true);
3611 return EventResponse::Handled;
3612 }
3613 EventResponse::Ignored
3614 }),
3615 );
3616 tree.push_action(
3617 widget,
3618 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
3619 );
3620 tree.shortcut_registry_mut().register(
3621 Shortcut::new("app.save")
3622 .primary(KeyStroke::command(Key::S))
3623 .build(),
3624 );
3625
3626 tree.layout(SizeProposal::exact(100.0, 50.0));
3627 tree.focus(widget);
3628 tree.press_key(Key::S, Modifiers::COMMAND);
3629 (action_fired.get(), on_key_fired.get())
3630 }
3631
3632 let (action, on_key) = run(true);
3634 assert!(
3635 !action,
3636 "keyboard_capture must suppress the shortcut action"
3637 );
3638 assert!(on_key, "keyboard_capture must deliver the raw KeyDown");
3639
3640 let (action, on_key) = run(false);
3642 assert!(action, "without capture the shortcut must fire");
3643 assert!(!on_key, "without capture the widget must not see the key");
3644 }
3645
3646 #[test]
3647 fn ctrl_tab_always_escapes_a_keyboard_capture_surface() {
3648 use std::cell::Cell;
3649 use std::rc::Rc;
3650
3651 let saw_key = Rc::new(Cell::new(false));
3657 let sk = saw_key.clone();
3658
3659 let mut tree = WidgetTree::new();
3660 let capture = tree.add(
3661 FillWidget::new()
3662 .focusable()
3663 .keyboard_capture(true)
3664 .on_key(move |_event, _ctx| {
3666 sk.set(true);
3667 EventResponse::Handled
3668 }),
3669 );
3670 let neighbour = tree.add(FillWidget::new().focusable());
3671 tree.layout(SizeProposal::exact(100.0, 50.0));
3672
3673 tree.focus(capture);
3676 tree.press_key(Key::Tab, Modifiers::NONE);
3677 assert!(saw_key.get(), "plain Tab must reach the capture surface");
3678 assert_eq!(
3679 tree.focused(),
3680 Some(capture),
3681 "plain Tab must not move focus off a capture surface"
3682 );
3683
3684 saw_key.set(false);
3686 tree.press_key(Key::Tab, Modifiers::CTRL);
3687 assert!(
3688 !saw_key.get(),
3689 "Ctrl+Tab is reserved and must not reach the capture surface"
3690 );
3691 assert_eq!(
3692 tree.focused(),
3693 Some(neighbour),
3694 "Ctrl+Tab must move focus out of a capture surface"
3695 );
3696
3697 tree.focus(capture);
3699 tree.press_key(Key::Tab, Modifiers::CTRL | Modifiers::SHIFT);
3700 assert_eq!(
3701 tree.focused(),
3702 Some(neighbour),
3703 "Ctrl+Shift+Tab must move focus out of a capture surface"
3704 );
3705 }
3706
3707 #[test]
3708 fn scope_mismatch_does_not_invoke_on_activate() {
3709 use crate::intent::Intent;
3710 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
3711 use std::cell::Cell;
3712 use std::rc::Rc;
3713
3714 let activated = Rc::new(Cell::new(false));
3720 let activated_flag = activated.clone();
3721
3722 let mut tree = WidgetTree::new();
3723 let scope_root = tree.add(FillWidget::new().focusable());
3724 let outside = tree.add(FillWidget::new().focusable());
3725
3726 tree.shortcut_registry_mut().register(
3727 Shortcut::new("editor.find")
3728 .primary(KeyStroke::command(Key::F))
3729 .scope(ShortcutScope::Scoped(scope_root))
3730 .on_activate(move |_ks, _ctx| {
3731 activated_flag.set(true);
3732 Intent::new("editor.find")
3733 })
3734 .build(),
3735 );
3736
3737 tree.layout(SizeProposal::exact(200.0, 100.0));
3738 tree.focus(outside);
3739
3740 tree.press_key(Key::F, Modifiers::COMMAND);
3741 assert!(
3742 !activated.get(),
3743 "on_activate must not run when focus is outside the shortcut's scope"
3744 );
3745 }
3746
3747 #[test]
3748 fn key_capture_runs_callback_and_bypasses_registry() {
3749 use crate::action::Action;
3750 use crate::shortcut::{KeyStroke, Shortcut};
3751 use std::cell::Cell;
3752 use std::rc::Rc;
3753
3754 let action_fired = Rc::new(Cell::new(false));
3755 let af = action_fired.clone();
3756 let captured = Rc::new(Cell::new(None));
3757 let cf = captured.clone();
3758
3759 let mut tree = WidgetTree::new();
3760 let widget = tree.add(FillWidget::new().focusable());
3761 tree.push_action(
3762 widget,
3763 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
3764 );
3765 tree.shortcut_registry_mut().register(
3766 Shortcut::new("app.save")
3767 .primary(KeyStroke::command(Key::S))
3768 .build(),
3769 );
3770
3771 tree.layout(SizeProposal::exact(100.0, 50.0));
3772 tree.focus(widget);
3773
3774 let handle = tree.begin_key_capture(move |ks, _reg, _ctx| cf.set(Some(ks)));
3775 assert!(tree.is_capturing_keys());
3776
3777 tree.press_key(Key::S, Modifiers::COMMAND);
3778 assert_eq!(
3779 captured.get(),
3780 Some(KeyStroke::command(Key::S)),
3781 "capture callback must receive the chord"
3782 );
3783 assert!(
3784 !action_fired.get(),
3785 "shortcut action must not fire while capture is armed"
3786 );
3787 assert!(
3788 !tree.is_capturing_keys(),
3789 "capture is one-shot; next KeyDown flows normally"
3790 );
3791 drop(handle);
3792 }
3793
3794 #[test]
3795 fn key_capture_can_rebind_through_registry() {
3796 use crate::shortcut::{KeyStroke, Shortcut};
3797
3798 let mut tree = WidgetTree::new();
3799 let widget = tree.add(FillWidget::new().focusable());
3800 tree.shortcut_registry_mut().register(
3801 Shortcut::new("app.save")
3802 .primary(KeyStroke::command(Key::S))
3803 .build(),
3804 );
3805
3806 tree.layout(SizeProposal::exact(100.0, 50.0));
3807 tree.focus(widget);
3808
3809 let _h = tree.begin_key_capture(|ks, reg, _ctx| {
3811 reg.rebind_primary("app.save", Some(ks));
3812 });
3813
3814 tree.press_key(Key::B, Modifiers::COMMAND | Modifiers::SHIFT);
3815 assert_eq!(
3816 tree.shortcut_registry()
3817 .effective("app.save")
3818 .unwrap()
3819 .primary,
3820 Some(KeyStroke::command_shift(Key::B))
3821 );
3822 }
3823
3824 #[test]
3825 fn dropping_capture_handle_cancels_capture() {
3826 use crate::shortcut::{KeyStroke, Shortcut};
3827 use std::cell::Cell;
3828 use std::rc::Rc;
3829
3830 let action_fired = Rc::new(Cell::new(false));
3831 let af = action_fired.clone();
3832 let capture_fired = Rc::new(Cell::new(false));
3833 let cf = capture_fired.clone();
3834
3835 let mut tree = WidgetTree::new();
3836 let widget = tree.add(FillWidget::new().focusable());
3837 tree.push_action(
3838 widget,
3839 crate::action::Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
3840 );
3841 tree.shortcut_registry_mut().register(
3842 Shortcut::new("app.save")
3843 .primary(KeyStroke::command(Key::S))
3844 .build(),
3845 );
3846 tree.layout(SizeProposal::exact(100.0, 50.0));
3847 tree.focus(widget);
3848
3849 {
3853 let _h = tree.begin_key_capture(move |_ks, _reg, _ctx| cf.set(true));
3854 assert!(tree.is_capturing_keys());
3855 }
3857 assert!(
3858 !tree.is_capturing_keys(),
3859 "dropping the handle must cancel the capture"
3860 );
3861
3862 tree.press_key(Key::S, Modifiers::COMMAND);
3863 assert!(!capture_fired.get(), "cancelled capture must not fire");
3864 assert!(
3865 action_fired.get(),
3866 "shortcut action runs after capture was cancelled"
3867 );
3868 }
3869
3870 #[test]
3871 fn second_begin_key_capture_does_not_racecancel_first() {
3872 use std::cell::Cell;
3873 use std::rc::Rc;
3874
3875 let first = Rc::new(Cell::new(false));
3876 let second = Rc::new(Cell::new(false));
3877 let f = first.clone();
3878 let s = second.clone();
3879
3880 let mut tree = WidgetTree::new();
3881 let widget = tree.add(FillWidget::new().focusable());
3882 tree.layout(SizeProposal::exact(100.0, 50.0));
3883 tree.focus(widget);
3884
3885 let h1 = tree.begin_key_capture(move |_ks, _reg, _ctx| f.set(true));
3891 let _h2 = tree.begin_key_capture(move |_ks, _reg, _ctx| s.set(true));
3892 drop(h1);
3893
3894 assert!(
3895 tree.is_capturing_keys(),
3896 "dropping the older handle must not cancel the active capture"
3897 );
3898 tree.press_key(Key::K, Modifiers::COMMAND);
3899 assert!(!first.get());
3900 assert!(second.get(), "newest capture wins");
3901 }
3902
3903 #[test]
3904 fn capture_callback_can_send_intent() {
3905 use crate::action::Action;
3906 use crate::intent::Intent;
3907
3908 use std::cell::Cell;
3909 use std::rc::Rc;
3910
3911 let ran = Rc::new(Cell::new(false));
3912 let flag = ran.clone();
3913
3914 let mut tree = WidgetTree::new();
3915 let widget = tree.add(FillWidget::new().focusable());
3916 tree.push_action(
3917 widget,
3918 Action::new("app.save").on_invoke(move |_i, _c| flag.set(true)),
3919 );
3920 tree.layout(SizeProposal::exact(100.0, 50.0));
3921 tree.focus(widget);
3922
3923 let _h = tree.begin_key_capture(|_ks, _reg, ctx| {
3924 ctx.send_intent(Intent::new("app.save"));
3925 });
3926 tree.press_key(Key::X, Modifiers::COMMAND);
3927 assert!(
3928 ran.get(),
3929 "intent queued from capture callback must dispatch"
3930 );
3931 }
3932
3933 #[test]
3934 fn binding_registry_does_not_accumulate_across_rebuilds() {
3935 use crate::binding::BindingLevel;
3936 use crate::signal::Signal;
3937
3938 #[derive(Debug)]
3939 struct BoundLeaf {
3940 tick: Signal<u64>,
3941 }
3942 impl crate::widget::Widget for BoundLeaf {
3943 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
3944 self.tick.bind_to(
3945 ctx.self_id(),
3946 ctx.binding_registry(),
3947 BindingLevel::Relayout,
3948 );
3949 Vec::new()
3950 }
3951 fn layout_response(
3952 &self,
3953 proposal: SizeProposal,
3954 _ctx: &crate::widget::LayoutContext,
3955 ) -> crate::widget::LayoutResponse {
3956 proposal.resolve(10.0, 10.0).into()
3957 }
3958 }
3959
3960 let mut tree = WidgetTree::new();
3961 let tick = Signal::new(0_u64);
3962 let widget = tree.add(BoundLeaf { tick: tick.clone() });
3963 tree.layout(SizeProposal::exact(200.0, 200.0));
3964 let after_first_build = tree.binding_registry().len();
3965 assert!(after_first_build >= 1);
3966
3967 for _ in 0..5 {
3971 tree.arena.mark_needs_rebuild(widget);
3972 tree.layout(SizeProposal::exact(200.0, 200.0));
3973 }
3974 assert_eq!(
3975 tree.binding_registry().len(),
3976 after_first_build,
3977 "bindings must be cleared on rebuild"
3978 );
3979
3980 tree.destroy_subtree(widget);
3981 assert_eq!(
3982 tree.binding_registry().len(),
3983 0,
3984 "bindings must be cleared on destroy"
3985 );
3986 let _ = tick;
3988 }
3989
3990 #[test]
3991 fn ctx_destroy_cancels_animations_and_bindings_via_deferred_path() {
3992 use crate::binding::BindingLevel;
4000 use crate::signal::Signal;
4001 use std::time::{Duration, Instant};
4002 use teksilo_tokens::Easing;
4003
4004 #[derive(Debug)]
4005 struct BoundLeaf {
4006 tick: Signal<u64>,
4007 }
4008 impl crate::widget::Widget for BoundLeaf {
4009 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
4010 self.tick.bind_to(
4011 ctx.self_id(),
4012 ctx.binding_registry(),
4013 BindingLevel::Relayout,
4014 );
4015 Vec::new()
4016 }
4017 fn layout_response(
4018 &self,
4019 proposal: SizeProposal,
4020 _ctx: &crate::widget::LayoutContext,
4021 ) -> crate::widget::LayoutResponse {
4022 proposal.resolve(10.0, 10.0).into()
4023 }
4024 }
4025
4026 let mut tree = WidgetTree::new();
4027 let widget = tree.add(BoundLeaf {
4028 tick: Signal::new(0_u64),
4029 });
4030 tree.layout(SizeProposal::exact(200.0, 200.0));
4031 assert!(tree.binding_registry().len() >= 1);
4032
4033 let anim = Signal::<f32>::new_animated(0.0);
4036 tree.animation_scheduler.animate(
4037 &anim,
4038 widget,
4039 1.0,
4040 Duration::from_secs(10),
4041 Easing::Linear,
4042 Instant::now(),
4043 );
4044 assert_eq!(tree.animation_scheduler.active_count(), 1);
4045
4046 let mut noop = crate::window::NoopWindowOps;
4048 tree.run_with_event_context(&mut noop, |ctx| ctx.destroy(widget));
4049
4050 assert_eq!(
4051 tree.animation_scheduler.active_count(),
4052 0,
4053 "ctx.destroy must cancel animations owned by the destroyed widget"
4054 );
4055 assert_eq!(
4056 tree.binding_registry().len(),
4057 0,
4058 "ctx.destroy must unregister the destroyed widget's bindings"
4059 );
4060 assert!(
4061 tree.arena.get(widget).is_none(),
4062 "node must be removed from the arena"
4063 );
4064 }
4065
4066 #[test]
4067 fn clear_shortcut_override_via_event_context_restores_default() {
4068 use crate::shortcut::{KeyStroke, Shortcut};
4069
4070 let mut tree = WidgetTree::new();
4071 tree.shortcut_registry_mut().register(
4072 Shortcut::new("app.save")
4073 .primary(KeyStroke::command(Key::S))
4074 .build(),
4075 );
4076 tree.shortcut_registry_mut()
4077 .rebind_primary("app.save", Some(KeyStroke::alt(Key::S)));
4078
4079 let source = tree.add(FillWidget::new());
4080 let mut ctx = EventContext::new();
4081 ctx.clear_shortcut_override("app.save");
4082 tree.collect_from_ctx(ctx, source);
4083
4084 assert_eq!(
4085 tree.shortcut_registry()
4086 .effective("app.save")
4087 .unwrap()
4088 .primary,
4089 Some(KeyStroke::command(Key::S))
4090 );
4091 }
4092
4093 #[test]
4094 fn rebind_shortcut_primary_via_event_context() {
4095 use crate::shortcut::{KeyStroke, Shortcut};
4096
4097 let mut tree = WidgetTree::new();
4098 tree.shortcut_registry_mut().register(
4099 Shortcut::new("app.save")
4100 .primary(KeyStroke::command(Key::S))
4101 .build(),
4102 );
4103 let source = tree.add(FillWidget::new());
4104
4105 let mut ctx = EventContext::new();
4106 ctx.rebind_shortcut_primary("app.save", Some(KeyStroke::alt(Key::S)));
4107 tree.collect_from_ctx(ctx, source);
4108
4109 assert_eq!(
4110 tree.shortcut_registry()
4111 .effective("app.save")
4112 .unwrap()
4113 .primary,
4114 Some(KeyStroke::alt(Key::S))
4115 );
4116 }
4117
4118 #[test]
4119 fn unregister_all_for_owner_called_on_destroy() {
4120 use crate::shortcut::{KeyStroke, Shortcut};
4121
4122 let mut tree = WidgetTree::new();
4123 let widget = tree.add(FillWidget::new());
4124 let widget_owner = widget;
4125 tree.shortcut_registry_mut().register_owned(
4126 Shortcut::new("scoped.thing")
4127 .primary(KeyStroke::command(Key::K))
4128 .build(),
4129 widget_owner,
4130 );
4131 assert!(
4132 tree.shortcut_registry()
4133 .get_default("scoped.thing")
4134 .is_some()
4135 );
4136
4137 tree.destroy_subtree(widget);
4138 assert!(
4139 tree.shortcut_registry()
4140 .get_default("scoped.thing")
4141 .is_none(),
4142 "destroying the owner must unregister its shortcut"
4143 );
4144 }
4145
4146 #[test]
4150 fn global_action_reached_from_unrelated_source() {
4151 use crate::action::Action;
4152 use crate::intent::Intent;
4153 use std::cell::Cell;
4154 use std::rc::Rc;
4155
4156 #[derive(Debug)]
4157 struct Registrar(Rc<Cell<bool>>);
4158 impl crate::widget::Widget for Registrar {
4159 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
4160 let flag = self.0.clone();
4161 ctx.register_action_global(
4162 Action::new("test.global").on_invoke(move |_i, _c| flag.set(true)),
4163 );
4164 vec![]
4165 }
4166 fn layout_response(
4167 &self,
4168 _p: teksilo_canvas::SizeProposal,
4169 _c: &crate::widget::LayoutContext,
4170 ) -> crate::widget::LayoutResponse {
4171 teksilo_canvas::Size::new(0.0, 0.0).into()
4172 }
4173 }
4174
4175 let mut tree = WidgetTree::new();
4176 let fired = Rc::new(Cell::new(false));
4177 let registrar = tree.add(Registrar(fired.clone()));
4178 let source = tree.add(FillWidget::new()); let mut ops = crate::window::NoopWindowOps;
4180
4181 tree.dispatch_intent(source, Intent::new("test.global"), true, &mut ops);
4182 assert!(
4183 fired.get(),
4184 "global action must fire from an unrelated source"
4185 );
4186
4187 fired.set(false);
4189 tree.destroy_subtree(registrar);
4190 tree.dispatch_intent(source, Intent::new("test.global"), true, &mut ops);
4191 assert!(
4192 !fired.get(),
4193 "destroying the owner must remove its global action"
4194 );
4195 }
4196
4197 #[test]
4209 fn hit_test_through_translate_scope() {
4210 use crate::test_widgets::StackWidget;
4211 let mut tree = WidgetTree::new();
4212 let child = tree.add(FillWidget::new());
4213 let parent = tree.add(StackWidget::new().add_child(child));
4214 tree.set_transform(parent, teksilo_canvas::Transform2D::translate(100.0, 0.0));
4216 tree.layout(SizeProposal::exact(100.0, 50.0));
4217
4218 assert_eq!(
4222 tree.hit_test(Point::new(50.0, 25.0)),
4223 None,
4224 "pre-transform area is not visually populated and must not hit"
4225 );
4226 assert_eq!(
4228 tree.hit_test(Point::new(150.0, 25.0)),
4229 Some(child),
4230 "visually-rendered area must hit the child"
4231 );
4232 assert_eq!(tree.hit_test(Point::new(250.0, 25.0)), None);
4234 }
4235
4236 #[test]
4237 fn hit_test_through_scale_scope() {
4238 use crate::test_widgets::StackWidget;
4239 let mut tree = WidgetTree::new();
4240 let child = tree.add(FillWidget::new());
4241 let parent = tree.add(StackWidget::new().add_child(child));
4242 tree.set_transform(parent, teksilo_canvas::Transform2D::scale(0.5, 0.5));
4245 tree.layout(SizeProposal::exact(100.0, 50.0));
4246
4247 assert_eq!(tree.hit_test(Point::new(25.0, 12.0)), Some(child));
4249 assert_eq!(
4252 tree.hit_test(Point::new(75.0, 25.0)),
4253 None,
4254 "scaled-out region must not hit"
4255 );
4256 }
4257
4258 #[test]
4259 fn hit_test_through_nested_transforms_compose() {
4260 use crate::test_widgets::StackWidget;
4261 let mut tree = WidgetTree::new();
4262 let leaf = tree.add(FillWidget::new());
4263 let inner = tree.add(StackWidget::new().add_child(leaf));
4264 let outer = tree.add(StackWidget::new().add_child(inner));
4265 tree.set_transform(outer, teksilo_canvas::Transform2D::translate(100.0, 0.0));
4269 tree.set_transform(inner, teksilo_canvas::Transform2D::scale(2.0, 2.0));
4270 tree.layout(SizeProposal::exact(50.0, 25.0));
4271
4272 assert_eq!(tree.hit_test(Point::new(150.0, 25.0)), Some(leaf));
4276 assert_eq!(tree.hit_test(Point::new(50.0, 25.0)), None);
4277 assert_eq!(tree.hit_test(Point::new(250.0, 25.0)), None);
4278 }
4279
4280 #[test]
4281 fn hit_test_identity_transform_unchanged() {
4282 let mut tree = WidgetTree::new();
4286 let widget = tree.add(FillWidget::new());
4287 tree.set_transform(widget, teksilo_canvas::Transform2D::IDENTITY);
4288 tree.layout(SizeProposal::exact(100.0, 50.0));
4289 assert_eq!(tree.hit_test(Point::new(50.0, 25.0)), Some(widget));
4290 }
4291
4292 #[test]
4293 fn arena_effective_transform_composes_ancestors() {
4294 use crate::test_widgets::StackWidget;
4305 let mut tree = WidgetTree::new();
4306 let leaf = tree.add(FillWidget::new());
4307 let inner = tree.add(StackWidget::new().add_child(leaf));
4308 let outer = tree.add(StackWidget::new().add_child(inner));
4309 tree.set_transform(outer, teksilo_canvas::Transform2D::translate(100.0, 0.0));
4310 tree.set_transform(inner, teksilo_canvas::Transform2D::scale(2.0, 2.0));
4311 tree.layout(SizeProposal::exact(50.0, 25.0));
4312
4313 let eff = tree.arena.effective_transform(leaf);
4314 let expected = teksilo_canvas::Transform2D::scale(2.0, 2.0)
4315 .then(&teksilo_canvas::Transform2D::translate(100.0, 0.0));
4316 for (a, b) in eff.m.iter().zip(expected.m.iter()) {
4317 assert!(
4318 (a - b).abs() < 1e-5,
4319 "effective_transform mismatch: got {:?}, want {:?}",
4320 eff.m,
4321 expected.m
4322 );
4323 }
4324
4325 let screen_origin = eff.apply_point(Point::new(0.0, 0.0));
4331 assert!((screen_origin.x - 100.0).abs() < 1e-5);
4332 assert!((screen_origin.y - 0.0).abs() < 1e-5);
4333 let screen_corner = eff.apply_point(Point::new(50.0, 25.0));
4336 assert!((screen_corner.x - 200.0).abs() < 1e-5);
4337 assert!((screen_corner.y - 50.0).abs() < 1e-5);
4338 }
4339
4340 #[derive(Debug)]
4345 struct StubMenu;
4346 impl crate::widget::Widget for StubMenu {
4347 fn layout_response(
4348 &self,
4349 _proposal: SizeProposal,
4350 _ctx: &crate::widget::LayoutContext,
4351 ) -> crate::widget::LayoutResponse {
4352 teksilo_canvas::Size::new(100.0, 40.0).into()
4353 }
4354 }
4355
4356 #[derive(Debug)]
4368 struct NominatingWidget {
4369 row: std::cell::Cell<Option<WidgetId>>,
4370 }
4371
4372 impl crate::widget::Widget for NominatingWidget {
4373 fn layout_response(
4374 &self,
4375 proposal: SizeProposal,
4376 _ctx: &LayoutContext,
4377 ) -> crate::widget::LayoutResponse {
4378 proposal.resolve(50.0, 20.0).into()
4379 }
4380
4381 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
4382 ctx.apply_self_handlers(crate::widget_builder::HandlerSet::new().focusable(true));
4383 Vec::new()
4384 }
4385
4386 fn context_menu_key_target(&self) -> Option<WidgetId> {
4387 self.row.get()
4388 }
4389 }
4390
4391 fn press(tree: &mut WidgetTree, key: Key, modifiers: Modifiers) {
4392 tree.dispatch_event(WidgetEvent::KeyDown {
4393 key,
4394 modifiers,
4395 text: None,
4396 });
4397 }
4398
4399 #[test]
4400 fn the_context_menu_key_opens_the_focused_widget_menu() {
4401 use std::cell::Cell;
4402 use std::rc::Rc;
4403
4404 let opened = Rc::new(Cell::new(false));
4405 let flag = opened.clone();
4406 let mut tree = WidgetTree::new();
4407 let widget = tree.add(
4408 FillWidget::new()
4409 .focusable()
4410 .context_menu(move |_pos, _ctx| {
4411 flag.set(true);
4412 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
4413 }),
4414 );
4415 tree.layout(SizeProposal::exact(200.0, 100.0));
4416 tree.focus(widget);
4417
4418 press(&mut tree, Key::ContextMenu, Modifiers::NONE);
4419 assert!(opened.get(), "the dedicated Menu key must open the menu");
4420 }
4421
4422 #[test]
4425 fn shift_f10_opens_the_focused_widget_menu() {
4426 use std::cell::Cell;
4427 use std::rc::Rc;
4428
4429 let opened = Rc::new(Cell::new(false));
4430 let flag = opened.clone();
4431 let mut tree = WidgetTree::new();
4432 let widget = tree.add(
4433 FillWidget::new()
4434 .focusable()
4435 .context_menu(move |_pos, _ctx| {
4436 flag.set(true);
4437 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
4438 }),
4439 );
4440 tree.layout(SizeProposal::exact(200.0, 100.0));
4441 tree.focus(widget);
4442
4443 press(&mut tree, Key::F10, Modifiers::SHIFT);
4444 assert!(opened.get(), "Shift+F10 must open the menu");
4445 }
4446
4447 #[test]
4450 fn a_near_miss_chord_is_not_a_context_menu_request() {
4451 use std::cell::Cell;
4452 use std::rc::Rc;
4453
4454 let opened = Rc::new(Cell::new(false));
4455 let flag = opened.clone();
4456 let mut tree = WidgetTree::new();
4457 let widget = tree.add(
4458 FillWidget::new()
4459 .focusable()
4460 .context_menu(move |_pos, _ctx| {
4461 flag.set(true);
4462 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
4463 }),
4464 );
4465 tree.layout(SizeProposal::exact(200.0, 100.0));
4466 tree.focus(widget);
4467
4468 press(&mut tree, Key::F10, Modifiers::SHIFT | Modifiers::CTRL);
4469 press(&mut tree, Key::F10, Modifiers::NONE);
4470 assert!(!opened.get(), "only Shift+F10 exactly asks for a menu");
4471 }
4472
4473 #[test]
4477 fn the_keyboard_target_can_be_a_row_rather_than_the_focused_container() {
4478 use std::cell::Cell;
4479 use std::rc::Rc;
4480
4481 let menu_owner = Rc::new(Cell::new(None::<&'static str>));
4482
4483 let row_flag = menu_owner.clone();
4484 let container_flag = menu_owner.clone();
4485
4486 let mut tree = WidgetTree::new();
4487 let row = tree.add(FillWidget::new().context_menu(move |_pos, _ctx| {
4488 row_flag.set(Some("row"));
4489 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
4490 }));
4491 let container = tree.add(
4492 crate::test_widgets::StackWidget::new()
4493 .add_child(row)
4494 .context_menu(move |_pos, _ctx| {
4495 container_flag.set(Some("container"));
4496 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
4497 }),
4498 );
4499 tree.layout(SizeProposal::exact(200.0, 100.0));
4500
4501 let nominator = tree.add(NominatingWidget {
4503 row: std::cell::Cell::new(Some(row)),
4504 });
4505 tree.layout(SizeProposal::exact(200.0, 100.0));
4506 tree.focus(nominator);
4507 let _ = container;
4508
4509 press(&mut tree, Key::ContextMenu, Modifiers::NONE);
4510 assert_eq!(
4511 menu_owner.get(),
4512 Some("row"),
4513 "the nominated row's factory must be the one that runs"
4514 );
4515 }
4516
4517 #[test]
4520 fn the_chord_falls_through_when_there_is_no_menu_to_show() {
4521 use std::cell::Cell;
4522 use std::rc::Rc;
4523
4524 let saw_key = Rc::new(Cell::new(false));
4525 let flag = saw_key.clone();
4526 let mut tree = WidgetTree::new();
4527 let widget = tree.add(FillWidget::new().focusable().on_key(move |ev, _ctx| {
4528 if matches!(ev, WidgetEvent::KeyDown { key: Key::F10, .. }) {
4529 flag.set(true);
4530 }
4531 crate::event::EventResponse::Ignored
4532 }));
4533 tree.layout(SizeProposal::exact(200.0, 100.0));
4534 tree.focus(widget);
4535
4536 press(&mut tree, Key::F10, Modifiers::SHIFT);
4537 assert!(
4538 saw_key.get(),
4539 "with no factory anywhere, the key must reach the widget"
4540 );
4541 }
4542
4543 #[test]
4544 fn context_menu_factory_receives_click_position() {
4545 use crate::event::{Modifiers, PointerButton};
4546 use std::cell::Cell;
4547 use std::rc::Rc;
4548
4549 let captured_position = Rc::new(Cell::new(None::<Point>));
4550 let cap = captured_position.clone();
4551 let mut tree = WidgetTree::new();
4552 let widget = tree.add(FillWidget::new().context_menu(move |pos, _ctx| {
4553 cap.set(Some(pos));
4554 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
4555 }));
4556 tree.layout(SizeProposal::exact(200.0, 100.0));
4557
4558 let click = Point::new(73.0, 42.0);
4559 tree.dispatch_event(WidgetEvent::PointerDown {
4560 position: click,
4561 button: PointerButton::Secondary,
4562 modifiers: Modifiers::NONE,
4563 });
4564
4565 let got = captured_position.get();
4566 assert_eq!(
4567 got,
4568 Some(click),
4569 "factory must receive the click position; got {:?}",
4570 got
4571 );
4572 let _ = widget;
4573 }
4574
4575 #[test]
4576 fn context_menu_factory_returning_none_falls_through_to_parent() {
4577 use crate::event::{Modifiers, PointerButton};
4578 use crate::test_widgets::StackWidget;
4579 use std::cell::Cell;
4580 use std::rc::Rc;
4581
4582 let outer_called = Rc::new(Cell::new(0_u32));
4586 let outer_flag = outer_called.clone();
4587 let mut tree = WidgetTree::new();
4588 let inner = tree.add(FillWidget::new().context_menu(|_pos, _ctx| None));
4589 let _outer = tree.add(StackWidget::new().add_child(inner).context_menu(
4590 move |_pos, _ctx| {
4591 outer_flag.set(outer_flag.get() + 1);
4592 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
4593 },
4594 ));
4595 tree.layout(SizeProposal::exact(200.0, 100.0));
4596
4597 tree.dispatch_event(WidgetEvent::PointerDown {
4598 position: Point::new(50.0, 25.0),
4599 button: PointerButton::Secondary,
4600 modifiers: Modifiers::NONE,
4601 });
4602
4603 assert_eq!(
4604 outer_called.get(),
4605 1,
4606 "inner returning None must fall through to the outer factory"
4607 );
4608 }
4609
4610 #[test]
4611 fn context_menu_factory_none_throughout_chain_does_not_show_overlay() {
4612 use crate::event::{Modifiers, PointerButton};
4613
4614 let mut tree = WidgetTree::new();
4616 tree.add(FillWidget::new().context_menu(|_pos, _ctx| None));
4617 tree.layout(SizeProposal::exact(200.0, 100.0));
4618
4619 let overlay_count_before = tree.overlay_manager.len();
4620 tree.dispatch_event(WidgetEvent::PointerDown {
4621 position: Point::new(50.0, 25.0),
4622 button: PointerButton::Secondary,
4623 modifiers: Modifiers::NONE,
4624 });
4625 let overlay_count_after = tree.overlay_manager.len();
4626 assert_eq!(
4627 overlay_count_before, overlay_count_after,
4628 "a factory returning None must not mount any overlay"
4629 );
4630 }
4631
4632 #[derive(Debug)]
4643 struct FreshChildHost {
4644 preserve: bool,
4645 }
4646 impl Widget for FreshChildHost {
4647 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
4648 vec![ctx.add(FillWidget::new())]
4649 }
4650 fn layout_response(
4651 &self,
4652 p: SizeProposal,
4653 _c: &LayoutContext,
4654 ) -> crate::widget::LayoutResponse {
4655 p.resolve(10.0, 10.0).into()
4656 }
4657 fn preserves_children_on_rebuild(&self) -> bool {
4658 self.preserve
4659 }
4660 }
4661
4662 #[test]
4663 fn reconcile_reaps_dropped_children_no_leak() {
4664 for preserve in [false, true] {
4669 let mut tree = WidgetTree::new();
4670 let host = tree.add(FreshChildHost { preserve });
4671 tree.layout(SizeProposal::exact(100.0, 100.0));
4672 let total0 = tree.arena.len();
4673 let active0 = tree.active_widget_count();
4674 for _ in 0..5 {
4675 tree.arena_mark_needs_rebuild_for_testing(host);
4676 tree.layout(SizeProposal::exact(100.0, 100.0));
4677 }
4678 assert_eq!(
4679 tree.arena.len(),
4680 total0,
4681 "preserve={preserve}: dropped children must be reaped, not leaked"
4682 );
4683 assert_eq!(
4684 tree.active_widget_count(),
4685 active0,
4686 "preserve={preserve}: no stranded still-active orphans"
4687 );
4688 }
4689 }
4690
4691 #[derive(Debug)]
4697 struct DetachedContentHost {
4698 preserve: bool,
4699 }
4700 impl Widget for DetachedContentHost {
4701 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
4702 let popup = ctx.add_detached(FillWidget::new());
4703 ctx.set_dormant(popup);
4704 vec![ctx.add(FillWidget::new())]
4705 }
4706 fn layout_response(
4707 &self,
4708 p: SizeProposal,
4709 _c: &LayoutContext,
4710 ) -> crate::widget::LayoutResponse {
4711 p.resolve(10.0, 10.0).into()
4712 }
4713 fn preserves_children_on_rebuild(&self) -> bool {
4714 self.preserve
4715 }
4716 }
4717
4718 #[test]
4719 fn rebuilding_reaps_detached_content_no_leak() {
4720 for preserve in [false, true] {
4726 let mut tree = WidgetTree::new();
4727 let host = tree.add(DetachedContentHost { preserve });
4728 tree.layout(SizeProposal::exact(100.0, 100.0));
4729 let total0 = tree.arena.len();
4730 for _ in 0..5 {
4731 tree.arena_mark_needs_rebuild_for_testing(host);
4732 tree.layout(SizeProposal::exact(100.0, 100.0));
4733 }
4734 assert_eq!(
4735 tree.arena.len(),
4736 total0,
4737 "preserve={preserve}: the previous build's detached content must be reaped"
4738 );
4739 }
4740 }
4741
4742 #[test]
4743 fn destroying_a_host_reaps_its_detached_content() {
4744 let mut tree = WidgetTree::new();
4745 let outer = tree.add(FillWidget::new());
4746 tree.layout(SizeProposal::exact(100.0, 100.0));
4747 let empty = tree.arena.len();
4748
4749 let host = tree.add_child(outer, DetachedContentHost { preserve: false });
4750 tree.layout(SizeProposal::exact(100.0, 100.0));
4751 assert!(tree.arena.len() > empty);
4752
4753 tree.destroy_subtree(host);
4754 assert_eq!(
4755 tree.arena.len(),
4756 empty,
4757 "the popup must die with the widget that built it"
4758 );
4759 }
4760
4761 #[derive(Debug)]
4763 struct StableChildHost {
4764 child: Option<WidgetId>,
4765 probe: std::rc::Rc<std::cell::Cell<Option<WidgetId>>>,
4766 }
4767 impl Widget for StableChildHost {
4768 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
4769 let id = match self.child {
4770 Some(id) => id,
4771 None => {
4772 let id = ctx.add(FillWidget::new());
4773 self.child = Some(id);
4774 self.probe.set(Some(id));
4775 id
4776 }
4777 };
4778 vec![id]
4779 }
4780 fn layout_response(
4781 &self,
4782 p: SizeProposal,
4783 _c: &LayoutContext,
4784 ) -> crate::widget::LayoutResponse {
4785 p.resolve(10.0, 10.0).into()
4786 }
4787 fn preserves_children_on_rebuild(&self) -> bool {
4788 true
4789 }
4790 }
4791
4792 #[test]
4793 fn reconcile_preserves_reattached_child() {
4794 let probe = std::rc::Rc::new(std::cell::Cell::new(None));
4795 let mut tree = WidgetTree::new();
4796 let host = tree.add(StableChildHost {
4797 child: None,
4798 probe: probe.clone(),
4799 });
4800 tree.layout(SizeProposal::exact(100.0, 100.0));
4801 let child = probe.get().expect("child mounted");
4802 let total0 = tree.arena.len();
4803 for _ in 0..5 {
4804 tree.arena_mark_needs_rebuild_for_testing(host);
4805 tree.layout(SizeProposal::exact(100.0, 100.0));
4806 }
4807 assert!(
4808 tree.arena.is_active(child),
4809 "the re-attached child must survive every rebuild"
4810 );
4811 assert_eq!(tree.arena.len(), total0, "no growth — same child reused");
4812 }
4813
4814 #[derive(Debug)]
4816 struct Wrapper {
4817 child: WidgetId,
4818 }
4819 impl Widget for Wrapper {
4820 fn build(&mut self, _ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
4821 vec![self.child]
4822 }
4823 fn layout_response(
4824 &self,
4825 p: SizeProposal,
4826 _c: &LayoutContext,
4827 ) -> crate::widget::LayoutResponse {
4828 p.resolve(10.0, 10.0).into()
4829 }
4830 }
4831
4832 #[derive(Debug)]
4836 struct ReparentHost {
4837 body: Option<WidgetId>,
4838 probe: std::rc::Rc<std::cell::Cell<Option<WidgetId>>>,
4839 }
4840 impl Widget for ReparentHost {
4841 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
4842 let body = match self.body {
4843 Some(id) => id,
4844 None => {
4845 let id = ctx.add(FillWidget::new());
4846 self.body = Some(id);
4847 self.probe.set(Some(id));
4848 id
4849 }
4850 };
4851 vec![ctx.add(Wrapper { child: body })]
4852 }
4853 fn layout_response(
4854 &self,
4855 p: SizeProposal,
4856 _c: &LayoutContext,
4857 ) -> crate::widget::LayoutResponse {
4858 p.resolve(10.0, 10.0).into()
4859 }
4860 fn preserves_children_on_rebuild(&self) -> bool {
4861 true
4862 }
4863 }
4864
4865 #[test]
4866 fn reconcile_spares_reparented_survivor() {
4867 let probe = std::rc::Rc::new(std::cell::Cell::new(None));
4874 let mut tree = WidgetTree::new();
4875 let host = tree.add(ReparentHost {
4876 body: None,
4877 probe: probe.clone(),
4878 });
4879 tree.layout(SizeProposal::exact(100.0, 100.0));
4880 let body = probe.get().expect("body mounted");
4881 let total0 = tree.arena.len();
4882 for _ in 0..5 {
4883 tree.arena_mark_needs_rebuild_for_testing(host);
4884 tree.layout(SizeProposal::exact(100.0, 100.0));
4885 }
4886 assert!(
4887 tree.arena.is_active(body),
4888 "the re-parented body must survive — it was moved into the new tree, \
4889 not swept with the dropped wrapper"
4890 );
4891 assert_eq!(
4892 tree.arena.len(),
4893 total0,
4894 "dropped wrappers reaped — no per-rebuild leak"
4895 );
4896 }
4897
4898 #[derive(Debug)]
4908 struct BelowContainer {
4909 child: Option<WidgetId>,
4910 offset: f32,
4911 }
4912
4913 impl crate::widget::Widget for BelowContainer {
4914 fn layout_response(
4915 &self,
4916 proposal: SizeProposal,
4917 _ctx: &crate::widget::LayoutContext,
4918 ) -> crate::widget::LayoutResponse {
4919 proposal.resolve(0.0, 0.0).into()
4920 }
4921 fn place_children(
4922 &self,
4923 bounds: Rect,
4924 _proposal: SizeProposal,
4925 children: &mut [crate::widget::WidgetPlacement],
4926 _ctx: &crate::widget::LayoutContext,
4927 ) {
4928 for c in children.iter_mut() {
4929 c.origin = Point::new(bounds.x, bounds.y + self.offset);
4930 c.size = bounds.size();
4931 }
4932 }
4933 fn children(&self) -> Vec<WidgetId> {
4934 self.child.into_iter().collect()
4935 }
4936 }
4937
4938 fn recording_scroll_container(
4941 tree: &mut WidgetTree,
4942 child: WidgetId,
4943 recorded: std::rc::Rc<std::cell::Cell<Option<Rect>>>,
4944 ) -> WidgetId {
4945 use crate::test_widgets::StackWidget;
4946 tree.add(
4947 StackWidget::new()
4948 .add_child(child)
4949 .on_scroll(move |ev, _ctx| match ev {
4950 WidgetEvent::ScrollIntoView { target_bounds, .. } => {
4951 recorded.set(Some(*target_bounds));
4952 EventResponse::Handled
4953 }
4954 _ => EventResponse::Ignored,
4955 })
4956 .clips_children(true),
4957 )
4958 }
4959
4960 #[test]
4961 fn ensure_visible_dispatches_scroll_into_view_to_clipping_ancestor() {
4962 use std::cell::Cell;
4963 use std::rc::Rc;
4964 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
4965 let mut tree = WidgetTree::new();
4966 let actor = tree.add(FillWidget::new());
4967 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
4968 tree.layout(SizeProposal::exact(100.0, 100.0));
4969
4970 let target = Rect::new(10.0, 500.0, 20.0, 15.0);
4973 let mut ctx = EventContext::new();
4974 ctx.ensure_visible(target);
4975 tree.collect_from_ctx(ctx, actor);
4976
4977 assert_eq!(
4978 recorded.get(),
4979 Some(target),
4980 "ensure_visible(rect) must dispatch ScrollIntoView with the exact rect \
4981 to the clips_children ancestor"
4982 );
4983 }
4984
4985 #[test]
4986 fn ensure_visible_is_noop_when_rect_already_visible() {
4987 use std::cell::Cell;
4988 use std::rc::Rc;
4989 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
4990 let mut tree = WidgetTree::new();
4991 let actor = tree.add(FillWidget::new());
4992 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
4993 tree.layout(SizeProposal::exact(100.0, 100.0));
4994
4995 let mut ctx = EventContext::new();
4998 ctx.ensure_visible(Rect::new(10.0, 10.0, 20.0, 15.0));
4999 tree.collect_from_ctx(ctx, actor);
5000
5001 assert_eq!(
5002 recorded.get(),
5003 None,
5004 "a rect already inside the viewport must not trigger a scroll"
5005 );
5006 }
5007
5008 #[test]
5009 fn ensure_visible_margin_forces_scroll_near_edge() {
5010 use std::cell::Cell;
5011 use std::rc::Rc;
5012 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
5013 let mut tree = WidgetTree::new();
5014 let actor = tree.add(FillWidget::new());
5015 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
5016 tree.layout(SizeProposal::exact(100.0, 100.0));
5017
5018 let rect = Rect::new(10.0, 95.0, 20.0, 4.0);
5021 let mut ctx = EventContext::new();
5022 ctx.ensure_visible_with_margin(rect, 10.0);
5023 tree.collect_from_ctx(ctx, actor);
5024
5025 assert_eq!(
5026 recorded.get(),
5027 Some(rect),
5028 "the margin must widen the visibility test so a near-edge rect scrolls"
5029 );
5030 }
5031
5032 fn recording_align_container(
5035 tree: &mut WidgetTree,
5036 child: WidgetId,
5037 recorded: std::rc::Rc<
5038 std::cell::Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>,
5039 >,
5040 ) -> WidgetId {
5041 use crate::test_widgets::StackWidget;
5042 tree.add(
5043 StackWidget::new()
5044 .add_child(child)
5045 .on_scroll(move |ev, _ctx| match ev {
5046 WidgetEvent::ScrollIntoView { align, motion, .. } => {
5047 recorded.set(Some((*align, *motion)));
5048 EventResponse::Handled
5049 }
5050 _ => EventResponse::Ignored,
5051 })
5052 .clips_children(true),
5053 )
5054 }
5055
5056 #[test]
5057 fn ensure_visible_aligned_scrolls_even_when_already_visible() {
5058 use std::cell::Cell;
5059 use std::rc::Rc;
5060 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
5061 let mut tree = WidgetTree::new();
5062 let actor = tree.add(FillWidget::new());
5063 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
5064 tree.layout(SizeProposal::exact(100.0, 100.0));
5065
5066 let target = Rect::new(10.0, 10.0, 20.0, 15.0);
5071 let mut ctx = EventContext::new();
5072 ctx.ensure_visible_aligned(target, 0.5, crate::event::ScrollMotion::Instant);
5073 tree.collect_from_ctx(ctx, actor);
5074
5075 assert_eq!(
5076 recorded.get(),
5077 Some(target),
5078 "an aligned reveal must dispatch even when the rect is already visible"
5079 );
5080 }
5081
5082 #[test]
5083 fn ensure_visible_aligned_forwards_fraction_and_motion() {
5084 use std::cell::Cell;
5085 use std::rc::Rc;
5086 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
5087 Rc::new(Cell::new(None));
5088 let mut tree = WidgetTree::new();
5089 let actor = tree.add(FillWidget::new());
5090 let _container = recording_align_container(&mut tree, actor, recorded.clone());
5091 tree.layout(SizeProposal::exact(100.0, 100.0));
5092
5093 let mut ctx = EventContext::new();
5094 ctx.ensure_visible_aligned(
5095 Rect::new(10.0, 10.0, 20.0, 15.0),
5096 0.25,
5097 crate::event::ScrollMotion::Smooth,
5098 );
5099 tree.collect_from_ctx(ctx, actor);
5100
5101 assert_eq!(
5102 recorded.get(),
5103 Some((
5104 crate::event::ScrollAlign::Fraction(0.25),
5105 crate::event::ScrollMotion::Smooth
5106 )),
5107 "the container must receive the requested fraction and motion verbatim"
5108 );
5109 }
5110
5111 #[test]
5112 fn ensure_visible_aligned_clamps_the_fraction() {
5113 use std::cell::Cell;
5114 use std::rc::Rc;
5115 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
5116 Rc::new(Cell::new(None));
5117 let mut tree = WidgetTree::new();
5118 let actor = tree.add(FillWidget::new());
5119 let _container = recording_align_container(&mut tree, actor, recorded.clone());
5120 tree.layout(SizeProposal::exact(100.0, 100.0));
5121
5122 let mut ctx = EventContext::new();
5123 ctx.ensure_visible_aligned(
5124 Rect::new(10.0, 10.0, 20.0, 15.0),
5125 4.2,
5126 crate::event::ScrollMotion::Instant,
5127 );
5128 tree.collect_from_ctx(ctx, actor);
5129
5130 assert_eq!(
5131 recorded.get().map(|(a, _)| a),
5132 Some(crate::event::ScrollAlign::Fraction(1.0)),
5133 "an out-of-range fraction must clamp rather than aim the pin off-screen"
5134 );
5135 }
5136
5137 #[test]
5138 fn plain_ensure_visible_requests_minimal_alignment() {
5139 use std::cell::Cell;
5140 use std::rc::Rc;
5141 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
5142 Rc::new(Cell::new(None));
5143 let mut tree = WidgetTree::new();
5144 let actor = tree.add(FillWidget::new());
5145 let _container = recording_align_container(&mut tree, actor, recorded.clone());
5146 tree.layout(SizeProposal::exact(100.0, 100.0));
5147
5148 let mut ctx = EventContext::new();
5149 ctx.ensure_visible(Rect::new(10.0, 500.0, 20.0, 15.0));
5150 tree.collect_from_ctx(ctx, actor);
5151
5152 assert_eq!(
5153 recorded.get(),
5154 Some((
5155 crate::event::ScrollAlign::Minimal,
5156 crate::event::ScrollMotion::Instant
5157 )),
5158 "the pre-existing reveal API must keep its exact semantics"
5159 );
5160 }
5161
5162 #[test]
5163 fn only_the_innermost_container_aligns() {
5164 use std::cell::Cell;
5165 use std::rc::Rc;
5166 let inner_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
5167 Rc::new(Cell::new(None));
5168 let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
5169 Rc::new(Cell::new(None));
5170
5171 let mut tree = WidgetTree::new();
5172 let actor = tree.add(FillWidget::new());
5173 let inner = recording_align_container(&mut tree, actor, inner_rec.clone());
5174 let _outer = recording_align_container(&mut tree, inner, outer_rec.clone());
5175 tree.layout(SizeProposal::exact(100.0, 100.0));
5176
5177 let mut ctx = EventContext::new();
5180 ctx.ensure_visible_aligned(
5181 Rect::new(10.0, 500.0, 20.0, 15.0),
5182 0.5,
5183 crate::event::ScrollMotion::Instant,
5184 );
5185 tree.collect_from_ctx(ctx, actor);
5186
5187 assert_eq!(
5188 inner_rec.get().map(|(a, _)| a),
5189 Some(crate::event::ScrollAlign::Fraction(0.5)),
5190 "the innermost clipping ancestor owns the pin"
5191 );
5192 assert_eq!(
5193 outer_rec.get().map(|(a, _)| a),
5194 Some(crate::event::ScrollAlign::Minimal),
5195 "an outer container must only bring the inner viewport into view — a \
5196 fraction names a height in one viewport, not in every ancestor's"
5197 );
5198 }
5199
5200 #[test]
5201 fn ensure_widget_visible_uses_target_arena_bounds() {
5202 use std::cell::Cell;
5203 use std::rc::Rc;
5204 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
5205 let mut tree = WidgetTree::new();
5206 let target = tree.add(FillWidget::new());
5208 let rec = recorded.clone();
5209 let container = tree.add(
5210 BelowContainer {
5211 child: Some(target),
5212 offset: 500.0,
5213 }
5214 .on_scroll(move |ev, _ctx| match ev {
5215 WidgetEvent::ScrollIntoView { target_bounds, .. } => {
5216 rec.set(Some(*target_bounds));
5217 EventResponse::Handled
5218 }
5219 _ => EventResponse::Ignored,
5220 })
5221 .clips_children(true),
5222 );
5223 tree.layout(SizeProposal::exact(100.0, 100.0));
5224
5225 let expected = tree.bounds(target);
5226 assert!(
5227 expected.y > 100.0,
5228 "fixture sanity: the target must sit below the viewport (y={})",
5229 expected.y
5230 );
5231
5232 let mut ctx = EventContext::new();
5235 ctx.ensure_widget_visible(target);
5236 tree.collect_from_ctx(ctx, container);
5237
5238 assert_eq!(
5239 recorded.get(),
5240 Some(expected),
5241 "ensure_widget_visible(id) must dispatch ScrollIntoView with the \
5242 target's current arena bounds"
5243 );
5244 }
5245
5246 #[test]
5247 fn ensure_widget_visible_ignores_missing_widget() {
5248 use std::cell::Cell;
5250 use std::rc::Rc;
5251 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
5252 let mut tree = WidgetTree::new();
5253 let actor = tree.add(FillWidget::new());
5254 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
5255 tree.layout(SizeProposal::exact(100.0, 100.0));
5256
5257 let mut ctx = EventContext::new();
5258 ctx.ensure_widget_visible(WidgetId::default());
5259 tree.collect_from_ctx(ctx, actor); assert_eq!(
5262 recorded.get(),
5263 None,
5264 "an unmounted id must not trigger a scroll"
5265 );
5266 }
5267
5268 #[test]
5269 fn context_menu_inside_a_modal_keeps_the_modal() {
5270 use crate::event::{Modifiers, PointerButton, WidgetEvent};
5274 use crate::overlay::{DismissBehavior, OverlayLayer, OverlayPlacement, OverlayRequest};
5275 use crate::test_widgets::{FillWidget, StackWidget};
5276
5277 let mut tree = WidgetTree::new();
5278 let modal_content = tree.add(StackWidget::new());
5281 let _editor = tree.add_child(
5282 modal_content,
5283 FillWidget::new()
5284 .context_menu(|_pos, _ctx| Some(Box::new(FillWidget::new()) as Box<dyn Widget>)),
5285 );
5286 tree.layout(SizeProposal::exact(200.0, 100.0));
5287
5288 let modal = tree.overlay_manager.show(OverlayRequest {
5289 content_id: modal_content,
5290 anchor: modal_content,
5291 placement: OverlayPlacement::Centered,
5292 dismiss: DismissBehavior::EscapeKey,
5293 layer: OverlayLayer::InTree,
5294 parent_overlay: None,
5295 on_dismiss: None,
5296 fade_duration: None,
5297 });
5298 tree.overlay_manager
5300 .stack
5301 .iter_mut()
5302 .find(|o| o.id == modal)
5303 .unwrap()
5304 .bounds = Rect::new(0.0, 0.0, 200.0, 100.0);
5305 assert_eq!(tree.overlay_manager.len(), 1);
5306
5307 tree.dispatch_event(WidgetEvent::PointerDown {
5309 position: Point::new(50.0, 25.0),
5310 button: PointerButton::Secondary,
5311 modifiers: Modifiers::NONE,
5312 });
5313
5314 assert!(
5315 tree.overlay_manager.active_ids().contains(&modal),
5316 "the modal must survive opening a context menu inside it"
5317 );
5318 assert_eq!(
5319 tree.overlay_manager.len(),
5320 2,
5321 "the context menu should now be open on top of the surviving modal"
5322 );
5323 }
5324}