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 match &event {
407 WidgetEvent::PointerMove { position } => {
408 if let Some(captured) = self.pointer_captured_by {
409 self.dispatch_to_widget(
410 captured,
411 &WidgetEvent::PointerMove {
412 position: *position,
413 },
414 &mut *ops,
415 );
416 if self.active_drag.is_none() {
421 self.advance_drag_observers(
422 &WidgetEvent::PointerMove {
423 position: *position,
424 },
425 &mut *ops,
426 );
427 }
428 } else {
429 self.handle_pointer_move(*position, &mut *ops);
430 }
431 self.update_pointer_leave_overlays(*position, &mut *ops);
432 }
433 WidgetEvent::PointerDown {
434 position, button, ..
435 } => {
436 self.tooltip_pointer_press(Some(*position));
443 if let Some(target) = self.hit_test(*position) {
444 if *button == PointerButton::Secondary
445 && self.show_context_menu_for(target, *position, &mut *ops)
446 {
447 return;
448 }
449 if let Some(focusable) = self.find_focusable_at_or_above(target) {
450 self.focus_with_origin_ops(
451 focusable,
452 crate::focus::FocusOrigin::Pointer,
453 &mut *ops,
454 );
455 }
456 self.dispatch_to_widget(target, &event, &mut *ops);
457 if self.active_drag.is_none()
462 && let Some(captured) = self.pointer_captured_by
463 {
464 self.arm_drag_observers(captured, &event, &mut *ops);
465 }
466 }
467 }
468 WidgetEvent::PointerUp { position, .. } => {
469 self.release_drag_observers(&event, &mut *ops);
476 if let Some(captured) = self.pointer_captured_by {
477 self.dispatch_to_widget(captured, &event, &mut *ops);
478 self.pointer_captured_by = None;
479 } else if let Some(target) = self.hit_test(*position) {
480 self.dispatch_to_widget(target, &event, &mut *ops);
481 }
482 }
483 WidgetEvent::Scroll { .. } => {
484 if let Some(target) = self.hovered.or(self.focused) {
485 self.dispatch_to_widget(target, &event, &mut *ops);
486 }
487 }
488 WidgetEvent::KeyDown { key, modifiers, .. } => {
489 if *key == Key::Tab {
490 let captured_focus = self
505 .focused
506 .is_some_and(|focused| self.is_keyboard_capture(focused));
507 if captured_focus && modifiers.ctrl() {
508 self.cycle_focus(modifiers.shift(), &mut *ops);
509 return;
510 }
511 let handled = self
518 .focused
519 .map(|focused| {
520 self.dispatch_to_widget_returning_handled(focused, &event, &mut *ops)
521 })
522 .unwrap_or(false);
523 if !handled {
524 self.cycle_focus(modifiers.shift(), &mut *ops);
525 }
526 } else if let Some(focused) = self.focused {
527 self.dispatch_to_widget(focused, &event, &mut *ops);
528 }
529 }
530 WidgetEvent::KeyUp { .. }
531 | WidgetEvent::ImeComposition { .. }
532 | WidgetEvent::ImeCommit { .. } => {
533 if let Some(focused) = self.focused {
534 self.dispatch_to_widget(focused, &event, &mut *ops);
535 }
536 }
537 WidgetEvent::AccessAction { target, action, .. } => {
538 if let Some(id) = target.filter(|id| self.arena.is_active(*id)) {
548 if *action == accesskit::Action::Focus {
549 self.focus_with_origin_ops(
550 id,
551 crate::focus::FocusOrigin::Programmatic,
552 &mut *ops,
553 );
554 self.access_action_handled = self.focused == Some(id);
557 } else if *action == accesskit::Action::ShowContextMenu {
558 self.access_action_handled =
574 if self.dispatch_to_widget_returning_handled(id, &event, &mut *ops) {
575 true
576 } else {
577 let position = self.arena.bounds(id).center();
578 self.show_context_menu_for(id, position, &mut *ops)
579 };
580 } else {
581 self.access_action_handled =
582 self.dispatch_to_widget_returning_handled(id, &event, &mut *ops);
583 }
584 }
585 }
586 WidgetEvent::Gesture { .. } => {
587 if let Some(target) = self.hovered.or(self.focused) {
588 self.dispatch_to_widget(target, &event, &mut *ops);
589 }
590 }
591 WidgetEvent::ScrollIntoView { .. }
592 | WidgetEvent::PointerEnter
593 | WidgetEvent::PointerLeave
594 | WidgetEvent::FocusGained { .. }
595 | WidgetEvent::FocusLost => {}
596 }
597 self.drain_pending_intents(&mut *ops);
602 }
603
604 fn show_context_menu_for(
605 &mut self,
606 target: WidgetId,
607 position: Point,
608 ops: &mut dyn crate::window::WindowOps,
609 ) -> bool {
610 let mut ctx = self.make_event_context(&mut *ops);
616 let mut walker = Some(target);
617 let menu_decision: Option<(WidgetId, Box<dyn Widget>)> = loop {
618 let owner_id = {
621 let mut probe = walker;
622 loop {
623 match probe {
624 None => break None,
625 Some(id) => {
626 if self
627 .arena
628 .get(id)
629 .is_some_and(|node| node.context_menu_factory.is_some())
630 {
631 break Some(id);
632 }
633 probe = self.arena.get(id).and_then(|node| node.parent);
634 }
635 }
636 }
637 };
638 let Some(owner_id) = owner_id else {
639 break None;
640 };
641 let outcome: Option<Box<dyn Widget>> = {
648 let node = self
649 .arena
650 .get(owner_id)
651 .expect("owner_id from active arena walk");
652 let factory = node
653 .context_menu_factory
654 .as_ref()
655 .expect("owner_id only set when factory present");
656 factory(position, &mut ctx)
657 };
658 match outcome {
659 Some(menu) => break Some((owner_id, menu)),
660 None => {
661 walker = self.arena.get(owner_id).and_then(|n| n.parent);
663 }
664 }
665 };
666
667 let drain_anchor = menu_decision
671 .as_ref()
672 .map(|(id, _)| *id)
673 .or_else(|| self.arena.roots().first().copied())
674 .unwrap_or(target);
675 self.collect_from_ctx(ctx, drain_anchor);
676
677 let Some((owner_id, menu_widget)) = menu_decision else {
678 return false;
679 };
680
681 let keep: std::collections::HashSet<WidgetId> = self
687 .overlay_manager
688 .stack
689 .iter()
690 .map(|o| o.content_id)
691 .filter(|&content_id| self.is_descendant_of(owner_id, content_id))
692 .collect();
693 let dismissed = self.overlay_manager.dismiss_except(&keep);
694 self.dormant_dismissed_content(&dismissed, &mut *ops);
695
696 let content_id = self.add_boxed(menu_widget);
697 let prev_focus = self.focused;
698 self.overlay_manager.show(crate::overlay::OverlayRequest {
699 content_id,
700 anchor: owner_id,
701 placement: crate::overlay::OverlayPlacement::AtPointer(position),
702 dismiss: crate::overlay::DismissBehavior::EscapeOrClickOutside,
703 layer: crate::overlay::OverlayLayer::InTree,
704 parent_overlay: None,
705 on_dismiss: None,
706 fade_duration: None,
707 });
708 if let Some(focus_id) = prev_focus {
709 self.overlay_manager.set_top_focus_restore(focus_id);
710 }
711 self.focus_ops(content_id, &mut *ops);
712 self.drain_pending_intents(&mut *ops);
717 true
718 }
719
720 fn handle_pointer_move(&mut self, position: Point, ops: &mut dyn crate::window::WindowOps) {
721 self.last_pointer_position = Some(position);
722 let target = self.hit_test(position);
723
724 if target != self.hovered {
725 let previously_hovered = self.hovered;
726 if let Some(old) = self.hovered {
727 self.dispatch_to_widget(old, &WidgetEvent::PointerLeave, &mut *ops);
728 self.tooltip_pointer_leave(old, &mut *ops);
729 }
730 if let Some(new) = target {
731 self.dispatch_to_widget(new, &WidgetEvent::PointerEnter, &mut *ops);
732 self.tooltip_pointer_enter(new);
733 }
734 self.set_hovered(target);
735 self.update_hover_within_signals(previously_hovered, target);
736 } else if let Some(target) = target {
737 self.tooltip_pointer_moved(target, position);
740 }
741
742 if let Some(target) = target {
743 self.dispatch_to_widget(target, &WidgetEvent::PointerMove { position }, &mut *ops);
744 }
745 }
746
747 pub(super) fn dispatch_to_widget(
748 &mut self,
749 target: WidgetId,
750 event: &WidgetEvent,
751 ops: &mut dyn crate::window::WindowOps,
752 ) {
753 self.dispatch_to_widget_returning_handled(target, event, ops);
754 }
755
756 fn widget_has_drag(&self, id: WidgetId) -> bool {
759 self.arena
760 .get(id)
761 .map(|n| n.any_handler(|h| h.on_drag.is_some() || h.on_swipe.is_some()))
762 .unwrap_or(false)
763 }
764
765 fn is_gesture_dead_zone(&self, id: WidgetId) -> bool {
769 self.arena
770 .get(id)
771 .map(|n| n.gesture_dead_zone)
772 .unwrap_or(false)
773 }
774
775 fn is_keyboard_capture(&self, id: WidgetId) -> bool {
779 self.arena
780 .get(id)
781 .map(|n| n.keyboard_capture)
782 .unwrap_or(false)
783 }
784
785 pub(super) fn arm_drag_observers(
796 &mut self,
797 captured: WidgetId,
798 down_event: &WidgetEvent,
799 ops: &mut dyn crate::window::WindowOps,
800 ) {
801 self.drag_observers.clear();
802 if self.widget_has_drag(captured) {
803 return;
804 }
805 if self.is_gesture_dead_zone(captured) {
808 return;
809 }
810 let mut observers = Vec::new();
811 let mut current = self.arena.parent(captured);
812 while let Some(id) = current {
813 if self.is_gesture_dead_zone(id) {
818 break;
819 }
820 if self.widget_has_drag(id) {
821 {
824 let WidgetTree {
825 arena,
826 gesture_owners,
827 ..
828 } = self;
829 if let Some(node) = arena.get_mut(id) {
830 Self::ensure_gesture_arena(node, id, gesture_owners);
831 }
832 }
833 self.observe_drag_on_ancestor(id, down_event, ops);
834 observers.push(id);
835 }
836 current = self.arena.parent(id);
837 }
838 self.drag_observers = observers;
839 }
840
841 pub(super) fn release_drag_observers(
851 &mut self,
852 up_event: &WidgetEvent,
853 ops: &mut dyn crate::window::WindowOps,
854 ) {
855 if self.drag_observers.is_empty() {
856 return;
857 }
858 let observers = std::mem::take(&mut self.drag_observers);
859 for id in &observers {
860 self.observe_drag_on_ancestor(*id, up_event, ops);
864 }
865 }
866
867 pub(super) fn advance_drag_observers(
871 &mut self,
872 move_event: &WidgetEvent,
873 ops: &mut dyn crate::window::WindowOps,
874 ) {
875 if self.drag_observers.is_empty() {
876 return;
877 }
878 let observers = std::mem::take(&mut self.drag_observers);
879 for id in &observers {
880 let recognized = self.observe_drag_on_ancestor(*id, move_event, ops);
881 if recognized || self.active_drag.is_some() {
882 return;
884 }
885 }
886 self.drag_observers = observers;
888 }
889
890 fn observe_drag_on_ancestor(
896 &mut self,
897 id: WidgetId,
898 event: &WidgetEvent,
899 ops: &mut dyn crate::window::WindowOps,
900 ) -> bool {
901 let localized = self.localize_event(id, event);
902 let event = localized.as_ref().unwrap_or(event);
903 let raw = match event {
904 WidgetEvent::PointerDown {
905 position,
906 button,
907 modifiers,
908 } => crate::gesture::RawPointerEvent::Down {
909 position: *position,
910 button: *button,
911 modifiers: *modifiers,
912 },
913 WidgetEvent::PointerMove { position } => crate::gesture::RawPointerEvent::Move {
914 position: *position,
915 },
916 WidgetEvent::PointerUp {
917 position,
918 button,
919 modifiers,
920 } => crate::gesture::RawPointerEvent::Up {
921 position: *position,
922 button: *button,
923 modifiers: *modifiers,
924 },
925 _ => return false,
926 };
927 let mut ctx = self.make_event_context(&mut *ops);
928 let WidgetTree { arena, .. } = self;
929 let recognized = if let Some(node) = arena.get_mut(id) {
930 if let Some(arena_ref) = node.handlers.gesture_arena.as_mut() {
931 if let Some(gesture) = arena_ref.process(&raw) {
932 Self::dispatch_recognized_gesture(node, gesture, &mut ctx);
933 true
934 } else {
935 false
936 }
937 } else {
938 false
939 }
940 } else {
941 false
942 };
943 self.collect_from_ctx(ctx, id);
944 recognized
945 }
946
947 fn localize_event(&self, id: WidgetId, event: &WidgetEvent) -> Option<WidgetEvent> {
959 match event {
960 WidgetEvent::PointerDown {
961 position,
962 button,
963 modifiers,
964 } => Some(WidgetEvent::PointerDown {
965 position: self.arena.local_pointer_position(id, *position),
966 button: *button,
967 modifiers: *modifiers,
968 }),
969 WidgetEvent::PointerUp {
970 position,
971 button,
972 modifiers,
973 } => Some(WidgetEvent::PointerUp {
974 position: self.arena.local_pointer_position(id, *position),
975 button: *button,
976 modifiers: *modifiers,
977 }),
978 WidgetEvent::PointerMove { position } => Some(WidgetEvent::PointerMove {
979 position: self.arena.local_pointer_position(id, *position),
980 }),
981 WidgetEvent::Gesture { gesture } => Some(WidgetEvent::Gesture {
982 gesture: self.localize_gesture(id, gesture),
983 }),
984 _ => None,
985 }
986 }
987
988 fn localize_gesture(&self, id: WidgetId, gesture: &GestureEvent) -> GestureEvent {
995 let loc = |p: teksilo_canvas::Point| self.arena.local_pointer_position(id, p);
996 let tap = |t: &TapEvent| TapEvent::new(loc(t.position), t.button, t.modifiers);
997 match gesture {
998 GestureEvent::Tap(t) => GestureEvent::Tap(tap(t)),
999 GestureEvent::DoubleTap(t) => GestureEvent::DoubleTap(tap(t)),
1000 GestureEvent::TripleTap(t) => GestureEvent::TripleTap(tap(t)),
1001 GestureEvent::LongPress(t) => GestureEvent::LongPress(tap(t)),
1002 GestureEvent::DragStarted { position, button } => GestureEvent::DragStarted {
1003 position: loc(*position),
1004 button: *button,
1005 },
1006 GestureEvent::DragMoved { position, delta } => GestureEvent::DragMoved {
1007 position: loc(*position),
1008 delta: *delta,
1009 },
1010 GestureEvent::DragEnded { position } => GestureEvent::DragEnded {
1011 position: loc(*position),
1012 },
1013 GestureEvent::PinchStarted { center } => GestureEvent::PinchStarted {
1014 center: loc(*center),
1015 },
1016 GestureEvent::PinchChanged {
1017 center,
1018 scale,
1019 rotation,
1020 } => GestureEvent::PinchChanged {
1021 center: loc(*center),
1022 scale: *scale,
1023 rotation: *rotation,
1024 },
1025 GestureEvent::PinchEnded => GestureEvent::PinchEnded,
1026 GestureEvent::Swipe {
1027 direction,
1028 velocity,
1029 } => GestureEvent::Swipe {
1030 direction: *direction,
1031 velocity: *velocity,
1032 },
1033 }
1034 }
1035
1036 pub(super) fn dispatch_to_widget_returning_handled(
1042 &mut self,
1043 target: WidgetId,
1044 event: &WidgetEvent,
1045 ops: &mut dyn crate::window::WindowOps,
1046 ) -> bool {
1047 if !self.arena.is_enabled(target) {
1048 return false;
1049 }
1050
1051 let mut ancestors = Vec::new();
1052 let mut current = self.arena.parent(target);
1053 while let Some(id) = current {
1054 ancestors.push(id);
1055 current = self.arena.parent(id);
1056 }
1057 ancestors.reverse();
1058
1059 let tap_owner: Option<WidgetId> = if matches!(
1077 event,
1078 WidgetEvent::PointerDown { .. } | WidgetEvent::PointerUp { .. }
1079 ) {
1080 let mut owner = None;
1081 let mut cur = Some(target);
1082 while let Some(id) = cur {
1083 if self.arena.get(id).is_some_and(|n| {
1084 n.any_handler(|h| h.on_tap.is_some() || h.on_long_press.is_some())
1085 }) {
1086 owner = Some(id);
1087 break;
1088 }
1089 cur = self.arena.parent(id);
1090 }
1091 owner
1092 } else {
1093 None
1094 };
1095
1096 for &id in &ancestors {
1097 let mut ctx = self.make_event_context(&mut *ops);
1098 ctx.press_claimed_by_interactive_child =
1099 tap_owner.is_some_and(|owner| owner != id && self.is_descendant_of(owner, id));
1100 let localized = self.localize_event(id, event);
1103 let event = localized.as_ref().unwrap_or(event);
1104 let response = if let Some(node) = self.arena.get_mut(id) {
1105 Self::try_handler_preview(node, event, &mut ctx).unwrap_or(EventResponse::Ignored)
1106 } else {
1107 EventResponse::Ignored
1108 };
1109 self.collect_from_ctx(ctx, id);
1110 if response == EventResponse::Handled {
1111 self.arena.mark_needs_paint(id);
1112 return true;
1113 }
1114 }
1115
1116 let needs_layout_on_handle = matches!(
1117 event,
1118 WidgetEvent::Scroll { .. } | WidgetEvent::ScrollIntoView { .. }
1119 );
1120 let mut current = Some(target);
1121 let mut is_target = true;
1122 while let Some(id) = current {
1123 let mut ctx = self.make_event_context(&mut *ops);
1124 ctx.press_claimed_by_interactive_child =
1125 tap_owner.is_some_and(|owner| owner != id && self.is_descendant_of(owner, id));
1126 let localized = self.localize_event(id, event);
1129 let WidgetTree {
1130 arena,
1131 gesture_owners,
1132 ..
1133 } = self;
1134 let event = localized.as_ref().unwrap_or(event);
1135 let response = if let Some(node) = arena.get_mut(id) {
1136 Self::try_handler_bubble(node, event, &mut ctx, is_target, id, gesture_owners)
1137 .unwrap_or(EventResponse::Ignored)
1138 } else {
1139 EventResponse::Ignored
1140 };
1141 self.collect_from_ctx(ctx, id);
1142 if response == EventResponse::Handled {
1143 if needs_layout_on_handle {
1144 self.arena.mark_needs_layout(id);
1145 } else {
1146 self.arena.mark_needs_paint(id);
1147 }
1148 if !matches!(event, WidgetEvent::PointerEnter | WidgetEvent::PointerLeave) {
1163 return true;
1164 }
1165 }
1166 is_target = false;
1167 current = self.arena.parent(id);
1168 }
1169 false
1170 }
1171
1172 pub(super) fn dispatch_to_widget_direct(
1173 &mut self,
1174 target: WidgetId,
1175 event: &WidgetEvent,
1176 ops: &mut dyn crate::window::WindowOps,
1177 ) {
1178 if !self.arena.is_enabled(target) {
1179 return;
1180 }
1181
1182 let mut ctx = self.make_event_context(&mut *ops);
1183 let WidgetTree {
1184 arena,
1185 gesture_owners,
1186 ..
1187 } = self;
1188 let response = if let Some(node) = arena.get_mut(target) {
1189 Self::try_handler_bubble(node, event, &mut ctx, true, target, gesture_owners)
1190 .unwrap_or(EventResponse::Ignored)
1191 } else {
1192 EventResponse::Ignored
1193 };
1194 self.collect_from_ctx(ctx, target);
1195
1196 if response == EventResponse::Handled {
1197 self.arena.mark_needs_paint(target);
1198 }
1199 }
1200
1201 fn try_handler_preview(
1202 node: &mut crate::arena::WidgetNode,
1203 event: &WidgetEvent,
1204 ctx: &mut EventContext,
1205 ) -> Option<EventResponse> {
1206 match event {
1207 WidgetEvent::KeyDown { .. }
1215 | WidgetEvent::KeyUp { .. }
1216 | WidgetEvent::ImeComposition { .. }
1217 | WidgetEvent::ImeCommit { .. } => {
1218 let has = node.external_handlers.on_key_preview.is_some()
1219 || node.handlers.on_key_preview.is_some();
1220 if !has {
1221 return None;
1222 }
1223 Some(fire_event_handler_both(
1224 &mut node.external_handlers.on_key_preview,
1225 &mut node.handlers.on_key_preview,
1226 event,
1227 ctx,
1228 ))
1229 }
1230 WidgetEvent::PointerEnter | WidgetEvent::PointerLeave => None,
1242 _ => {
1243 let has = node.external_handlers.on_pointer_event.is_some()
1244 || node.handlers.on_pointer_event.is_some();
1245 if !has {
1246 return None;
1247 }
1248 Some(fire_event_handler_both(
1249 &mut node.external_handlers.on_pointer_event,
1250 &mut node.handlers.on_pointer_event,
1251 event,
1252 ctx,
1253 ))
1254 }
1255 }
1256 }
1257
1258 fn try_handler_bubble(
1267 node: &mut crate::arena::WidgetNode,
1268 event: &WidgetEvent,
1269 ctx: &mut EventContext,
1270 fire_on_pointer_event: bool,
1271 node_id: WidgetId,
1272 gesture_owners: &mut std::collections::HashSet<WidgetId>,
1273 ) -> Option<EventResponse> {
1274 match event {
1275 WidgetEvent::PointerEnter => {
1276 if let Some(cursor) = node.node_cursor {
1277 ctx.set_cursor(cursor);
1278 }
1279 let mut fired = false;
1280 if let Some(h) = node.external_handlers.on_hover.as_mut() {
1281 h(true, ctx);
1282 fired = true;
1283 }
1284 if let Some(h) = node.handlers.on_hover.as_mut() {
1285 h(true, ctx);
1286 fired = true;
1287 }
1288 if fired {
1289 Some(EventResponse::Handled)
1290 } else {
1291 node.node_cursor.map(|_| EventResponse::Handled)
1292 }
1293 }
1294 WidgetEvent::PointerLeave => {
1295 if node.node_cursor.is_some() {
1296 ctx.set_cursor(crate::widget::CursorIcon::Default);
1297 }
1298 let mut fired = false;
1299 if let Some(h) = node.external_handlers.on_hover.as_mut() {
1300 h(false, ctx);
1301 fired = true;
1302 }
1303 if let Some(h) = node.handlers.on_hover.as_mut() {
1304 h(false, ctx);
1305 fired = true;
1306 }
1307 if fired {
1308 Some(EventResponse::Handled)
1309 } else {
1310 node.node_cursor.map(|_| EventResponse::Handled)
1311 }
1312 }
1313 WidgetEvent::FocusGained { .. } => {
1314 let mut fired = false;
1315 if let Some(h) = node.external_handlers.on_focus.as_mut() {
1316 h(true, ctx);
1317 fired = true;
1318 }
1319 if let Some(h) = node.handlers.on_focus.as_mut() {
1320 h(true, ctx);
1321 fired = true;
1322 }
1323 fired.then_some(EventResponse::Handled)
1324 }
1325 WidgetEvent::FocusLost => {
1326 let mut fired = false;
1327 if let Some(h) = node.external_handlers.on_focus.as_mut() {
1328 h(false, ctx);
1329 fired = true;
1330 }
1331 if let Some(h) = node.handlers.on_focus.as_mut() {
1332 h(false, ctx);
1333 fired = true;
1334 }
1335 fired.then_some(EventResponse::Handled)
1336 }
1337 WidgetEvent::KeyDown { .. }
1338 | WidgetEvent::KeyUp { .. }
1339 | WidgetEvent::ImeComposition { .. }
1340 | WidgetEvent::ImeCommit { .. } => {
1341 if node.external_handlers.on_key.is_some() || node.handlers.on_key.is_some() {
1342 Some(fire_event_handler_both(
1343 &mut node.external_handlers.on_key,
1344 &mut node.handlers.on_key,
1345 event,
1346 ctx,
1347 ))
1348 } else {
1349 None
1350 }
1351 }
1352 WidgetEvent::Scroll { .. } | WidgetEvent::ScrollIntoView { .. } => {
1353 if node.external_handlers.on_scroll.is_some() || node.handlers.on_scroll.is_some() {
1354 Some(fire_event_handler_both(
1355 &mut node.external_handlers.on_scroll,
1356 &mut node.handlers.on_scroll,
1357 event,
1358 ctx,
1359 ))
1360 } else {
1361 None
1362 }
1363 }
1364 WidgetEvent::AccessAction {
1365 action,
1366 target_node,
1367 data,
1368 ..
1369 } => {
1370 let saved_a11y_source = ctx
1380 .current_source
1381 .replace(crate::telemetry::IntentSource::Accessibility);
1382 let request_is_set = node.handlers.on_access_action_request.is_some()
1383 || node.external_handlers.on_access_action_request.is_some();
1384 let user_handled = if request_is_set {
1385 let r1 = node
1386 .external_handlers
1387 .on_access_action_request
1388 .as_mut()
1389 .map(|h| h(*action, *target_node, data.clone(), ctx))
1390 .unwrap_or(EventResponse::Ignored);
1391 let r2 = node
1392 .handlers
1393 .on_access_action_request
1394 .as_mut()
1395 .map(|h| h(*action, *target_node, data.clone(), ctx))
1396 .unwrap_or(EventResponse::Ignored);
1397 Some(
1398 if r1 == EventResponse::Handled || r2 == EventResponse::Handled {
1399 EventResponse::Handled
1400 } else {
1401 EventResponse::Ignored
1402 },
1403 )
1404 } else if node.handlers.on_access_action.is_some()
1405 || node.external_handlers.on_access_action.is_some()
1406 {
1407 let r1 = node
1408 .external_handlers
1409 .on_access_action
1410 .as_mut()
1411 .map(|h| h(*action, ctx))
1412 .unwrap_or(EventResponse::Ignored);
1413 let r2 = node
1414 .handlers
1415 .on_access_action
1416 .as_mut()
1417 .map(|h| h(*action, ctx))
1418 .unwrap_or(EventResponse::Ignored);
1419 Some(
1420 if r1 == EventResponse::Handled || r2 == EventResponse::Handled {
1421 EventResponse::Handled
1422 } else {
1423 EventResponse::Ignored
1424 },
1425 )
1426 } else {
1427 None
1428 };
1429
1430 let mut override_handled = false;
1436 if let Some(ov) = node.access_overrides.as_deref_mut() {
1437 if matches!(action, accesskit::Action::CustomAction) {
1438 if let Some(accesskit::ActionData::CustomAction(idx)) = data
1439 && let Some((_, cb)) = ov.custom_actions.get_mut(*idx as usize)
1440 {
1441 cb(ctx);
1442 override_handled = true;
1443 }
1444 } else {
1445 for (a, cb) in ov.actions.iter_mut() {
1446 if *a == *action {
1447 cb(ctx);
1448 override_handled = true;
1449 }
1450 }
1451 }
1452 }
1453
1454 ctx.current_source = saved_a11y_source;
1455 match (user_handled, override_handled) {
1456 (Some(EventResponse::Handled), _) | (_, true) => Some(EventResponse::Handled),
1457 (Some(EventResponse::Ignored), false) => Some(EventResponse::Ignored),
1458 (None, false) => None,
1459 }
1460 }
1461 WidgetEvent::Gesture { gesture } => {
1462 let matched = matches!(
1466 gesture,
1467 GestureEvent::PinchStarted { .. }
1468 | GestureEvent::PinchChanged { .. }
1469 | GestureEvent::PinchEnded
1470 | GestureEvent::Swipe { .. }
1471 | GestureEvent::DoubleTap { .. }
1472 | GestureEvent::TripleTap { .. }
1473 ) && {
1474 let has_handler = match gesture {
1475 GestureEvent::PinchStarted { .. }
1476 | GestureEvent::PinchChanged { .. }
1477 | GestureEvent::PinchEnded => node.any_handler(|h| h.on_pinch.is_some()),
1478 GestureEvent::Swipe { .. } => node.any_handler(|h| h.on_swipe.is_some()),
1479 GestureEvent::DoubleTap { .. } => {
1480 node.any_handler(|h| h.on_double_tap.is_some())
1481 }
1482 GestureEvent::TripleTap { .. } => {
1483 node.any_handler(|h| h.on_triple_tap.is_some())
1484 }
1485 _ => false,
1486 };
1487 if has_handler {
1488 Self::dispatch_recognized_gesture(node, *gesture, ctx);
1489 }
1490 has_handler
1491 };
1492 if matched {
1493 Some(EventResponse::Handled)
1494 } else {
1495 None
1496 }
1497 }
1498 WidgetEvent::PointerDown {
1499 position,
1500 button,
1501 modifiers,
1502 } => {
1503 if fire_on_pointer_event {
1510 let r = fire_event_handler_both(
1511 &mut node.external_handlers.on_pointer_event,
1512 &mut node.handlers.on_pointer_event,
1513 event,
1514 ctx,
1515 );
1516 if r == EventResponse::Handled {
1517 return Some(EventResponse::Handled);
1518 }
1519 }
1520 Self::ensure_gesture_arena(node, node_id, gesture_owners);
1521 if let Some(arena) = node.handlers.gesture_arena.as_mut() {
1522 ctx.capture_pointer();
1531 let result = arena.process(&RawPointerEvent::Down {
1532 position: *position,
1533 button: *button,
1534 modifiers: *modifiers,
1535 });
1536 if let Some(gesture) = result {
1537 Self::dispatch_recognized_gesture(node, gesture, ctx);
1538 }
1539 return Some(EventResponse::Handled);
1540 }
1541 None
1542 }
1543 WidgetEvent::PointerUp {
1544 position,
1545 button,
1546 modifiers,
1547 } => {
1548 if fire_on_pointer_event {
1549 let r = fire_event_handler_both(
1550 &mut node.external_handlers.on_pointer_event,
1551 &mut node.handlers.on_pointer_event,
1552 event,
1553 ctx,
1554 );
1555 if r == EventResponse::Handled {
1556 return Some(EventResponse::Handled);
1557 }
1558 }
1559 if let Some(arena) = node.handlers.gesture_arena.as_mut() {
1560 let result = arena.process(&RawPointerEvent::Up {
1561 position: *position,
1562 button: *button,
1563 modifiers: *modifiers,
1564 });
1565 if let Some(gesture) = result {
1566 Self::dispatch_recognized_gesture(node, gesture, ctx);
1567 }
1568 return Some(EventResponse::Handled);
1569 }
1570 None
1571 }
1572 WidgetEvent::PointerMove { position } => {
1573 if fire_on_pointer_event {
1574 let r = fire_event_handler_both(
1575 &mut node.external_handlers.on_pointer_event,
1576 &mut node.handlers.on_pointer_event,
1577 event,
1578 ctx,
1579 );
1580 if r == EventResponse::Handled {
1581 return Some(EventResponse::Handled);
1582 }
1583 }
1584 if let Some(arena) = node.handlers.gesture_arena.as_mut() {
1585 let result = arena.process(&RawPointerEvent::Move {
1586 position: *position,
1587 });
1588 if let Some(gesture) = result {
1589 Self::dispatch_recognized_gesture(node, gesture, ctx);
1590 return Some(EventResponse::Handled);
1602 }
1603 return Some(EventResponse::Ignored);
1604 }
1605 None
1606 }
1607 }
1608 }
1609
1610 pub(super) fn collect_from_ctx<'ops>(
1611 &mut self,
1612 mut ctx: EventContext<'ops>,
1613 source_widget: WidgetId,
1614 ) {
1615 let local_ops = ctx.window_ops.take();
1621 let mut noop = crate::window::NoopWindowOps;
1622 let ops: &mut dyn crate::window::WindowOps = match local_ops {
1623 Some(o) => o,
1624 None => &mut noop,
1625 };
1626 if ctx.frame_requested {
1627 self.request_frame();
1628 }
1629 if let Some(cursor) = ctx.cursor_request {
1630 self.current_cursor = cursor;
1631 }
1632 for intent in ctx.pending_intents {
1638 self.enqueue_intent(source_widget, intent, true);
1639 }
1640 if ctx.cancel_key_capture {
1649 self.cancel_key_capture();
1650 }
1651 if let Some(slot) = ctx.pending_key_capture {
1652 self.key_capture = Some(slot);
1653 }
1654 for mutation in ctx.pending_shortcut_mutations {
1656 match mutation {
1657 crate::widget::ShortcutMutation::RebindPrimary { id, keystroke } => {
1658 self.shortcut_registry.rebind_primary(id, keystroke);
1659 }
1660 crate::widget::ShortcutMutation::RebindSecondary { id, keystroke } => {
1661 self.shortcut_registry.rebind_secondary(id, keystroke);
1662 }
1663 crate::widget::ShortcutMutation::ClearOverride { id } => {
1664 self.shortcut_registry.clear_override(&id);
1665 }
1666 }
1667 }
1668 if ctx.close_window_requested {
1669 self.close_window_requested = true;
1670 }
1671 if ctx.force_close_requested {
1672 self.force_close_requested = true;
1673 }
1674 self.pending_modal_requests
1675 .extend(ctx.modal_requests.into_iter().map(|request| {
1676 crate::modal::QueuedModalRequest {
1677 source_widget,
1678 request,
1679 }
1680 }));
1681 if ctx.dismiss_modal && !self.dismiss_modal_for_source(source_widget, &mut *ops) {
1682 self.pending_modal_dismissal = true;
1683 }
1684 for callback in ctx.idle_callbacks {
1685 self.idle_queue.push_boxed(callback);
1686 }
1687 match ctx.dismiss_scope {
1688 Some(crate::widget::DismissScope::All) => {
1689 let dismissed = self.overlay_manager.dismiss_all();
1690 self.dormant_dismissed_content(&dismissed, &mut *ops);
1691 }
1692 Some(crate::widget::DismissScope::AllExceptHosts) => {
1693 self.dismiss_all_overlays_except_hosts(&mut *ops);
1694 }
1695 Some(crate::widget::DismissScope::SelfChain) => {
1696 self.dismiss_self_overlay_chain_for_source(source_widget, &mut *ops);
1697 }
1698 Some(crate::widget::DismissScope::Top) => {
1699 if let Some((_id, content_ids, focus_restore)) = self.overlay_manager.dismiss_top()
1700 {
1701 self.dormant_dismissed_content(&content_ids, &mut *ops);
1702 if let Some(restore_id) = focus_restore
1703 && self.arena.is_active(restore_id)
1704 {
1705 self.focus_ops(restore_id, &mut *ops);
1706 }
1707 }
1708 }
1709 None => {
1710 for id in ctx.overlay_dismissals {
1711 let dismissed = self.overlay_manager.dismiss(id);
1712 self.dormant_dismissed_content(&dismissed, &mut *ops);
1713 }
1714 }
1715 }
1716 for content_id in ctx.overlay_content_dismissals {
1723 if let Some(overlay_id) = self.overlay_manager.find_by_content(content_id) {
1724 let dismissed = self.overlay_manager.dismiss(overlay_id);
1725 self.dormant_dismissed_content(&dismissed, &mut *ops);
1726 }
1727 }
1728 for (id, pause) in ctx.overlay_pause_requests {
1735 if pause {
1736 self.overlay_manager.pause_auto_dismiss(id);
1737 } else {
1738 self.overlay_manager.resume_auto_dismiss(id);
1739 }
1740 }
1741 for preserve_content in ctx.dismiss_descendant_overlays {
1742 self.dismiss_child_overlays_for_source(source_widget, preserve_content, &mut *ops);
1743 }
1744 self.apply_tree_mutations(std::mem::take(&mut ctx.tree_mutations));
1745 if ctx.request_a11y_update {
1746 self.a11y_dirty = true;
1747 }
1748 for mut req in ctx.overlay_requests {
1749 if req.parent_overlay.is_none() {
1750 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
1751 }
1752 if self
1753 .overlay_manager
1754 .find_by_content(req.content_id)
1755 .is_some()
1756 {
1757 continue;
1758 }
1759 let current_focus = self.focused;
1760 self.overlay_manager.show(req);
1761 self.a11y_dirty = true;
1765 if let Some(focus_id) = current_focus {
1766 self.overlay_manager.set_top_focus_restore(focus_id);
1767 }
1768 }
1769 for (mut req, duration) in ctx.timed_overlay_requests {
1770 if req.parent_overlay.is_none() {
1771 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
1772 }
1773 if self
1774 .overlay_manager
1775 .find_by_content(req.content_id)
1776 .is_some()
1777 {
1778 continue;
1779 }
1780 let current_focus = self.focused;
1781 let overlay_id = self.overlay_manager.show_for(req, duration);
1782 self.overlay_manager
1783 .set_shown_at_sim(overlay_id, self.sim_clock);
1784 self.a11y_dirty = true;
1785 if let Some(focus_id) = current_focus {
1786 self.overlay_manager.set_top_focus_restore(focus_id);
1787 }
1788 }
1789 for (mut req, progress, duration) in ctx.reveal_overlay_requests {
1790 if req.parent_overlay.is_none() {
1791 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
1792 }
1793 if self
1794 .overlay_manager
1795 .find_by_content(req.content_id)
1796 .is_some()
1797 {
1798 continue;
1799 }
1800 let content_id = req.content_id;
1801 let current_focus = self.focused;
1802 let overlay_id = self.overlay_manager.show(req);
1803 self.overlay_manager
1804 .set_shown_at_sim(overlay_id, self.sim_clock);
1805 self.a11y_dirty = true;
1806 if let Some(focus_id) = current_focus {
1807 self.overlay_manager.set_top_focus_restore(focus_id);
1808 }
1809 self.register_animated_signal(&progress, content_id);
1815 let _ = progress.try_animate_with_options(crate::animation::AnimationRequest {
1816 target: 1.0,
1817 duration,
1818 easing: teksilo_tokens::Easing::EaseOut,
1819 frame_interval: None,
1820 looping: false,
1821 epsilon: 0.0,
1822 max_duration: None,
1823 });
1824 self.overlay_manager
1825 .attach_fade(overlay_id, progress, duration);
1826 }
1827 if let Some(capture) = ctx.pointer_capture {
1828 if capture {
1829 self.pointer_captured_by = Some(source_widget);
1830 } else {
1831 self.pointer_captured_by = None;
1832 }
1833 }
1834 for (mut request, delay, focus_target) in ctx.delayed_overlay_requests {
1835 if request.parent_overlay.is_none() {
1836 request.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
1837 }
1838 if self
1839 .overlay_manager
1840 .find_by_content(request.content_id)
1841 .is_some()
1842 {
1843 continue;
1844 }
1845 let content_id = request.content_id;
1846 self.pending_delayed_overlays
1847 .retain(|pending| pending.request.content_id != content_id);
1848 self.pending_delayed_overlays.push(PendingDelayedOverlay {
1849 request,
1850 delay,
1851 focus_target,
1852 real_requested_at: std::time::Instant::now(),
1853 sim_requested_at: self.sim_clock,
1854 });
1855 self.arena.mark_needs_paint(source_widget);
1856 }
1857 for content_id in ctx.cancel_delayed_overlays {
1858 self.pending_delayed_overlays
1859 .retain(|pending| pending.request.content_id != content_id);
1860 }
1861 for id in ctx.repaint_requests {
1862 self.arena.mark_needs_paint(id);
1863 }
1864 for id in ctx.synthetic_clicks {
1865 self.synthesise_tap_with_ops(id, &mut *ops);
1870 }
1871 if let Some(&id) = ctx.focus_requests.last() {
1872 let target = self.first_focusable_descendant(id).unwrap_or(id);
1882 self.focus_ops(target, &mut *ops);
1883 }
1884 if let Some(&id) = ctx.focus_into_requests.last() {
1885 if let Some(target) = self.first_focusable_descendant(id) {
1892 self.focus_ops(target, &mut *ops);
1893 }
1894 }
1895
1896 for req in ctx.scroll_into_view_requests {
1905 self.scroll_rect_into_view(
1906 req.from.unwrap_or(source_widget),
1909 req.rect,
1910 req.margin,
1911 req.align,
1912 req.motion,
1913 &mut *ops,
1914 );
1915 }
1916 for (id, margin) in ctx.scroll_widget_into_view_requests {
1923 if self.arena.get(id).is_some() {
1924 let bounds = self.arena.bounds(id);
1925 self.scroll_rect_into_view(
1926 id,
1927 bounds,
1928 margin,
1929 crate::event::ScrollAlign::Minimal,
1930 crate::event::ScrollMotion::Instant,
1931 &mut *ops,
1932 );
1933 }
1934 }
1935
1936 if let Some(&id) = ctx.highlight_tooltip_requests.last() {
1941 self.show_highlight_tooltip(id, &mut *ops);
1942 }
1943
1944 if let Some((source_widget, payload, preview_widget)) = ctx.drag_start_request {
1946 let (preview_content_id, preview_overlay_id) = if let Some(preview) = preview_widget {
1947 let content_id = self.add_boxed(preview);
1954 let overlay_id = self.overlay_manager.show(crate::overlay::OverlayRequest {
1955 content_id,
1956 anchor: source_widget,
1957 placement: crate::overlay::OverlayPlacement::AtPointer(
1958 teksilo_canvas::Point::ZERO,
1959 ),
1960 dismiss: crate::overlay::DismissBehavior::Manual,
1961 layer: crate::overlay::OverlayLayer::InTree,
1962 parent_overlay: None,
1963 on_dismiss: None,
1964 fade_duration: None,
1965 });
1966 self.arena.mark_needs_layout(content_id);
1970 (Some(content_id), Some(overlay_id))
1971 } else {
1972 (None, None)
1973 };
1974 self.active_drag = Some(crate::drag_state::DragSession {
1975 payload,
1976 source_widget: Some(source_widget),
1977 is_external: false,
1978 current_position: teksilo_canvas::Point::ZERO,
1979 current_target: None,
1980 feedback: crate::drag_state::DropFeedback::NoFeedback,
1981 preview_content_id,
1982 preview_overlay_id,
1983 });
1984 self.pointer_captured_by = Some(source_widget);
1985 self.current_cursor = crate::widget::CursorIcon::Grabbing;
1988 }
1989 if ctx.cancel_drag {
1990 self.cancel_active_drag(&mut *ops);
1991 }
1992
1993 if let Some(theme) = ctx.theme_request {
1995 self.pending_theme_request = Some(theme);
2001 }
2002 if ctx.follow_system_request {
2003 self.pending_follow_system_request = true;
2007 }
2008 if let Some(locale) = ctx.locale_request {
2009 self.pending_locale_request = Some(locale);
2015 }
2016 if let Some(scale) = ctx.text_scale_request {
2017 self.pending_text_scale_request = Some(scale);
2022 }
2023 }
2024
2025 fn apply_tree_mutations(&mut self, mutations: Vec<crate::widget::TreeMutation>) {
2026 use crate::binding::BindingLevel;
2027 use crate::widget::TreeMutation;
2028
2029 for mutation in mutations {
2030 match mutation {
2031 TreeMutation::SetDormant(id) => self.arena.set_dormant(id),
2032 TreeMutation::Activate(id) => self.arena.activate(id),
2033 TreeMutation::Destroy(id) => {
2034 if let Some(overlay_id) = self.overlay_manager().find_by_content(id) {
2047 self.dismiss_overlay(overlay_id);
2048 }
2049 self.destroy_subtree(id);
2050 }
2051 TreeMutation::MaterializeNow(id) => {
2055 if self.arena.get(id).is_some() {
2056 self.rebuild_single_widget(id);
2057 }
2058 }
2059 TreeMutation::WithWidgetMut { id, dirty, apply } => {
2060 let existed = if let Some(any) =
2065 self.arena.get_mut(id).and_then(|n| n.widget.as_any_mut())
2066 {
2067 apply(any);
2068 true
2069 } else {
2070 false
2071 };
2072 if existed {
2073 match dirty {
2074 BindingLevel::RepaintOnly => self.arena.mark_needs_paint(id),
2075 BindingLevel::SubtreeRepaint => self.arena.mark_subtree_needs_paint(id),
2076 BindingLevel::Relayout => {
2077 self.arena.mark_needs_layout(id);
2078 self.arena.mark_ancestors_need_layout(id);
2079 }
2080 BindingLevel::Rebuild => {
2081 self.arena.mark_needs_rebuild(id);
2082 self.arena.mark_ancestors_need_layout(id);
2083 }
2084 BindingLevel::AccessibilityOnly => self.a11y_dirty = true,
2085 }
2086 }
2087 }
2088 }
2089 }
2090 }
2091
2092 pub fn hit_test(&self, point: Point) -> Option<WidgetId> {
2093 self.hit_test_excluding_overlay_and_widget(point, None, None)
2094 }
2095
2096 pub fn hit_test_excluding_overlay_and_widget(
2100 &self,
2101 point: Point,
2102 exclude_overlay: Option<crate::overlay::OverlayId>,
2103 exclude_widget: Option<WidgetId>,
2104 ) -> Option<WidgetId> {
2105 if let Some(overlay_id) = self.overlay_manager.hit_test(point) {
2106 if Some(overlay_id) == exclude_overlay {
2107 } else if let Some(overlay) = self.overlay_manager.overlay(overlay_id) {
2109 return self.arena.hit_test_in_subtree_excluding(
2110 overlay.content_id,
2111 point,
2112 exclude_widget,
2113 );
2114 }
2115 }
2116
2117 if self.overlay_manager.topmost_centered().is_some() {
2118 return None;
2119 }
2120
2121 self.arena.hit_test_at(point, exclude_widget)
2124 }
2125}
2126
2127#[cfg(test)]
2128mod tests {
2129 use super::*;
2130 use crate::test_widgets::FillWidget;
2131 use crate::widget::CursorIcon;
2132 use crate::widget_builder::WidgetBuilder;
2133
2134 #[test]
2135 fn pointer_enter_leave_synthesized() {
2136 let mut tree = WidgetTree::new();
2137 let widget = tree.add(FillWidget::new());
2138 tree.layout(SizeProposal::exact(100.0, 50.0));
2139 tree.pointer_move(Point::new(50.0, 25.0));
2140 assert_eq!(tree.hovered, Some(widget));
2141 tree.pointer_move(Point::new(200.0, 200.0));
2142 assert_eq!(tree.hovered, None);
2143 }
2144
2145 #[test]
2146 fn pointer_hover_updates_current_cursor() {
2147 let mut tree = WidgetTree::new();
2148 tree.add(FillWidget::new().cursor(CursorIcon::ColResize));
2149 tree.layout(SizeProposal::exact(100.0, 50.0));
2150
2151 tree.pointer_move(Point::new(50.0, 25.0));
2152 assert_eq!(tree.current_cursor(), CursorIcon::ColResize);
2153
2154 tree.pointer_move(Point::new(200.0, 200.0));
2155 assert_eq!(tree.current_cursor(), CursorIcon::Default);
2156 }
2157
2158 #[derive(Debug)]
2161 struct Bumpable {
2162 value: i32,
2163 }
2164
2165 impl crate::widget::Widget for Bumpable {
2166 fn layout_response(
2167 &self,
2168 proposal: SizeProposal,
2169 _ctx: &crate::widget::LayoutContext,
2170 ) -> crate::widget::LayoutResponse {
2171 proposal.resolve(10.0, 10.0).into()
2172 }
2173 fn as_any(&self) -> Option<&dyn std::any::Any> {
2174 Some(self)
2175 }
2176 fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
2177 Some(self)
2178 }
2179 }
2180
2181 #[test]
2182 fn with_widget_mut_applies_and_dirty_marks() {
2183 let mut tree = WidgetTree::new();
2184 let id = tree.add(Bumpable { value: 0 });
2185 tree.layout(SizeProposal::exact(100.0, 100.0));
2186
2187 let mut ctx = EventContext::new();
2188 ctx.with_widget_mut::<Bumpable>(id, crate::binding::BindingLevel::Relayout, |b| {
2189 b.value = 42;
2190 });
2191 tree.collect_from_ctx(ctx, id);
2192
2193 let value = tree
2194 .widget_as_any(id)
2195 .and_then(|a| a.downcast_ref::<Bumpable>())
2196 .map(|b| b.value);
2197 assert_eq!(
2198 value,
2199 Some(42),
2200 "the deferred closure must mutate the live widget"
2201 );
2202 assert!(
2203 tree.needs_layout(),
2204 "Relayout dirty level must mark the tree for relayout"
2205 );
2206 }
2207
2208 #[test]
2209 #[cfg(debug_assertions)]
2210 #[should_panic(expected = "not the requested type")]
2211 fn with_widget_mut_wrong_type_panics_in_debug() {
2212 struct Other;
2213 let mut tree = WidgetTree::new();
2214 let id = tree.add(Bumpable { value: 0 });
2215 let mut ctx = EventContext::new();
2216 ctx.with_widget_mut::<Other>(
2217 id,
2218 crate::binding::BindingLevel::RepaintOnly,
2219 |_o: &mut Other| {},
2220 );
2221 tree.collect_from_ctx(ctx, id);
2224 }
2225
2226 #[test]
2227 fn with_widget_mut_closure_may_fire_observed_signals() {
2228 use crate::signal::Signal;
2236 let mut tree = WidgetTree::new();
2237 let id = tree.add(Bumpable { value: 0 });
2238 tree.layout(SizeProposal::exact(100.0, 100.0));
2239
2240 let trigger = Signal::new(0_u64);
2241 let echo = Signal::new(0_u64);
2242 let echo_for_obs = echo.clone();
2243 let _obs = trigger.observe(move |v| echo_for_obs.set(*v));
2244
2245 let trigger_in = trigger.clone();
2246 let mut ctx = EventContext::new();
2247 ctx.with_widget_mut::<Bumpable>(id, crate::binding::BindingLevel::RepaintOnly, move |b| {
2248 b.value = 7;
2249 trigger_in.set(99);
2251 });
2252 tree.collect_from_ctx(ctx, id); assert_eq!(
2255 echo.get(),
2256 99,
2257 "the observer ran during the deferred mutation"
2258 );
2259 let value = tree
2260 .widget_as_any(id)
2261 .and_then(|a| a.downcast_ref::<Bumpable>())
2262 .map(|b| b.value);
2263 assert_eq!(value, Some(7));
2264 }
2265
2266 #[test]
2267 fn request_accessibility_update_forces_rewalk() {
2268 let mut tree = WidgetTree::new();
2269 let id = tree.add(Bumpable { value: 0 });
2270 tree.layout(SizeProposal::exact(100.0, 100.0));
2271 let _ = tree.sync_accessibility(); assert!(
2273 !tree.a11y_dirty,
2274 "sync_accessibility should clear the dirty flag"
2275 );
2276
2277 let mut ctx = EventContext::new();
2278 ctx.request_accessibility_update();
2279 tree.collect_from_ctx(ctx, id);
2280 assert!(
2281 tree.a11y_dirty,
2282 "request_accessibility_update must force an AT re-walk"
2283 );
2284 }
2285
2286 #[test]
2287 fn rebuild_dirties_accessibility_tree() {
2288 let mut tree = WidgetTree::new();
2295 let id = tree.add(Bumpable { value: 0 });
2296 tree.layout(SizeProposal::exact(100.0, 100.0));
2297 let _ = tree.sync_accessibility(); assert!(
2299 !tree.a11y_dirty,
2300 "sync_accessibility should clear the dirty flag"
2301 );
2302
2303 tree.arena_mark_needs_rebuild_for_testing(id);
2306 tree.layout(SizeProposal::exact(100.0, 100.0));
2307 assert!(
2308 tree.a11y_dirty,
2309 "a rebuild must dirty the AT tree so the next sync re-walks"
2310 );
2311 }
2312
2313 #[test]
2314 fn bound_access_label_change_dirties_accessibility_tree() {
2315 use crate::signal::Signal;
2316 use crate::test_widgets::FillWidget;
2317 use crate::widget_builder::WidgetBuilder;
2318
2319 let label = Signal::new("first".to_string());
2325 let mut tree = WidgetTree::new();
2326 let _id = tree.add(FillWidget::new().access_label(label.clone()));
2327 tree.layout(SizeProposal::exact(100.0, 100.0));
2328 let _ = tree.sync_accessibility(); assert!(!tree.a11y_dirty, "sync_accessibility should clear the flag");
2330
2331 label.set("second".to_string());
2332 tree.layout(SizeProposal::exact(100.0, 100.0));
2333 assert!(
2334 tree.a11y_dirty,
2335 "changing a bound access_label must dirty the AT tree"
2336 );
2337 }
2338
2339 #[test]
2340 fn disabled_ancestor_blocks_event_to_descendant() {
2341 use crate::signal::Signal;
2342 use crate::test_widgets::StackWidget;
2343 use std::cell::Cell;
2344 use std::rc::Rc;
2345
2346 let tapped = Rc::new(Cell::new(false));
2347 let flag = tapped.clone();
2348 let enabled = Signal::new(true);
2349
2350 let mut tree = WidgetTree::new();
2351 let child = tree.add(FillWidget::new().on_tap(move |_pos, _ctx| {
2352 flag.set(true);
2353 }));
2354 let parent = tree.add(StackWidget::new().add_child(child));
2355 tree.enabled_when(parent, enabled.clone());
2356 tree.layout(SizeProposal::exact(100.0, 50.0));
2357
2358 enabled.set(false);
2359 tree.click(child);
2360 assert!(
2361 !tapped.get(),
2362 "disabled ancestor should block descendant tap"
2363 );
2364
2365 enabled.set(true);
2366 tree.click(child);
2367 assert!(tapped.get(), "re-enabling should restore dispatch");
2368 }
2369
2370 #[test]
2371 fn pointer_positions_are_widget_local_at_nonzero_origin() {
2372 use crate::event::{Modifiers, PointerButton};
2373 use crate::test_widgets::InsetWidget;
2374 use std::cell::Cell;
2375 use std::rc::Rc;
2376
2377 let tap_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
2379 let down_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
2380 let drag_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
2381 let (tp, dp, gp) = (tap_pos.clone(), down_pos.clone(), drag_pos.clone());
2382
2383 let mut tree = WidgetTree::new();
2384 let child = tree.add(
2385 FillWidget::new()
2386 .on_tap(move |ev, _ctx| tp.set(Some(ev.position)))
2387 .on_pointer_event(move |ev, _ctx| {
2388 if let WidgetEvent::PointerDown { position, .. } = ev {
2389 dp.set(Some(*position));
2390 }
2391 crate::event::EventResponse::Ignored
2392 })
2393 .on_drag(move |phase, _ctx| {
2394 use crate::gesture::DragPhase;
2395 match phase {
2396 DragPhase::Started { position, .. }
2397 | DragPhase::Moved { position, .. }
2398 | DragPhase::Ended { position } => gp.set(Some(position)),
2399 }
2400 }),
2401 );
2402 let inset = tree.add(InsetWidget::new(20.0).set_child(child));
2403 let _ = inset;
2404 tree.layout(SizeProposal::exact(200.0, 200.0));
2405 assert_eq!(tree.bounds(child).origin(), Point::new(20.0, 20.0));
2406
2407 tree.dispatch_event(WidgetEvent::PointerDown {
2409 position: Point::new(50.0, 40.0),
2410 button: PointerButton::Primary,
2411 modifiers: Modifiers::NONE,
2412 });
2413 assert_eq!(
2414 down_pos.get(),
2415 Some(Point::new(30.0, 20.0)),
2416 "on_pointer_event PointerDown must be widget-local"
2417 );
2418 tree.dispatch_event(WidgetEvent::PointerUp {
2419 position: Point::new(50.0, 40.0),
2420 button: PointerButton::Primary,
2421 modifiers: Modifiers::NONE,
2422 });
2423 assert_eq!(
2424 tap_pos.get(),
2425 Some(Point::new(30.0, 20.0)),
2426 "on_tap position must be widget-local"
2427 );
2428
2429 tree.dispatch_event(WidgetEvent::PointerDown {
2432 position: Point::new(50.0, 40.0),
2433 button: PointerButton::Primary,
2434 modifiers: Modifiers::NONE,
2435 });
2436 tree.dispatch_event(WidgetEvent::PointerMove {
2439 position: Point::new(65.0, 55.0),
2440 });
2441 tree.dispatch_event(WidgetEvent::PointerMove {
2442 position: Point::new(90.0, 70.0),
2443 });
2444 assert_eq!(
2445 drag_pos.get(),
2446 Some(Point::new(70.0, 50.0)),
2447 "on_drag position must be widget-local"
2448 );
2449 }
2450
2451 #[test]
2452 fn dormant_widget_not_hit_tested() {
2453 let mut tree = WidgetTree::new();
2454 let widget = tree.add(FillWidget::new());
2455 tree.layout(SizeProposal::exact(100.0, 50.0));
2456
2457 tree.pointer_move(Point::new(50.0, 25.0));
2458 assert_eq!(tree.hovered, Some(widget));
2459
2460 tree.set_dormant(widget);
2461 tree.pointer_move(Point::new(200.0, 200.0));
2462 tree.pointer_move(Point::new(50.0, 25.0));
2463 assert_eq!(tree.hovered, None);
2464 }
2465
2466 #[test]
2467 fn ancestor_pointer_handler_does_not_suppress_descendant_hover() {
2468 use crate::event::EventResponse;
2469 use crate::test_widgets::StackWidget;
2470 use std::cell::Cell;
2471 use std::rc::Rc;
2472
2473 let hovered = Rc::new(Cell::new(false));
2475 let h = hovered.clone();
2476
2477 let mut tree = WidgetTree::new();
2478 let child = tree.add(FillWidget::new().on_hover(move |entered, _ctx| h.set(entered)));
2479 tree.add(
2484 StackWidget::new()
2485 .add_child(child)
2486 .on_pointer_event(|_event, _ctx| EventResponse::Handled),
2487 );
2488 tree.layout(SizeProposal::exact(100.0, 50.0));
2489
2490 tree.pointer_move(Point::new(50.0, 25.0));
2491 assert!(
2492 hovered.get(),
2493 "a greedy ancestor on_pointer_event must NOT swallow the child's PointerEnter"
2494 );
2495
2496 tree.pointer_move(Point::new(500.0, 500.0));
2497 assert!(
2498 !hovered.get(),
2499 "PointerLeave must likewise reach the child despite the ancestor"
2500 );
2501 }
2502
2503 #[test]
2511 fn a_child_hover_handler_does_not_swallow_its_ancestors() {
2512 use crate::test_widgets::StackWidget;
2513 use std::cell::Cell;
2514 use std::rc::Rc;
2515
2516 let (row, button) = (Rc::new(Cell::new(false)), Rc::new(Cell::new(false)));
2517 let (r, b) = (row.clone(), button.clone());
2518
2519 let mut tree = WidgetTree::new();
2520 let child = tree.add(FillWidget::new().on_hover(move |entered, _ctx| b.set(entered)));
2521 tree.add(
2522 StackWidget::new()
2523 .add_child(child)
2524 .on_hover(move |entered, _ctx| r.set(entered)),
2525 );
2526 tree.layout(SizeProposal::exact(100.0, 50.0));
2527
2528 tree.pointer_move(Point::new(50.0, 25.0));
2529 assert!(button.get(), "the child is hovered");
2530 assert!(row.get(), "and so is the row it is inside");
2531
2532 tree.pointer_move(Point::new(500.0, 500.0));
2533 assert!(!button.get(), "the child heard the leave");
2534 assert!(
2535 !row.get(),
2536 "and so did the row — a container is not still hovered because the \
2537 pointer left it through a button"
2538 );
2539 }
2540
2541 #[test]
2542 fn destroy_subtree_clears_dangling_pointer_capture() {
2543 use crate::event::{EventResponse, Modifiers, PointerButton};
2544 use crate::test_widgets::StackWidget;
2545
2546 let mut tree = WidgetTree::new();
2547 let child = tree.add(FillWidget::new().on_pointer_event(|event, ctx| {
2548 if matches!(event, WidgetEvent::PointerDown { .. }) {
2549 ctx.capture_pointer();
2550 }
2551 EventResponse::Ignored
2552 }));
2553 let parent = tree.add(StackWidget::new().add_child(child));
2554 tree.layout(SizeProposal::exact(100.0, 50.0));
2555
2556 tree.dispatch_event(WidgetEvent::PointerDown {
2558 position: Point::new(50.0, 25.0),
2559 button: PointerButton::Primary,
2560 modifiers: Modifiers::NONE,
2561 });
2562 assert_eq!(
2563 tree.pointer_captured_by,
2564 Some(child),
2565 "PointerDown handler should have captured the pointer"
2566 );
2567
2568 tree.destroy_subtree(parent);
2572 assert_eq!(
2573 tree.pointer_captured_by, None,
2574 "destroy_subtree must clear a capture anchored at a destroyed widget"
2575 );
2576 }
2577
2578 #[test]
2585 fn key_preview_consumes_before_focused_on_key() {
2586 use crate::event::EventResponse;
2589 use crate::test_widgets::StackWidget;
2590 use std::cell::Cell;
2591 use std::rc::Rc;
2592
2593 let leaf_fired = Rc::new(Cell::new(false));
2594 let leaf_flag = leaf_fired.clone();
2595 let preview_fired = Rc::new(Cell::new(false));
2596 let preview_flag = preview_fired.clone();
2597
2598 let mut tree = WidgetTree::new();
2599 let leaf = tree.add(FillWidget::new().focusable().on_key(move |event, _c| {
2600 if matches!(event, WidgetEvent::KeyDown { .. }) {
2603 leaf_flag.set(true);
2604 }
2605 EventResponse::Handled
2606 }));
2607 let mid = tree.add(StackWidget::new().add_child(leaf));
2608 let _root =
2609 tree.add(StackWidget::new().add_child(mid).on_key_preview(
2610 move |event, _c| match event {
2611 WidgetEvent::KeyDown {
2612 key: Key::Enter, ..
2613 } => {
2614 preview_flag.set(true);
2615 EventResponse::Handled
2616 }
2617 _ => EventResponse::Ignored,
2618 },
2619 ));
2620
2621 tree.layout(SizeProposal::exact(100.0, 50.0));
2622 tree.focus(leaf);
2623 tree.press_key(Key::Enter, Modifiers::NONE);
2624
2625 assert!(
2626 preview_fired.get(),
2627 "ancestor on_key_preview must fire for KeyDown on a focused descendant"
2628 );
2629 assert!(
2630 !leaf_fired.get(),
2631 "consuming the event in preview must prevent the focused widget's on_key from running"
2632 );
2633 }
2634
2635 #[test]
2636 fn key_preview_falls_through_when_returning_ignored() {
2637 use crate::event::EventResponse;
2640 use crate::test_widgets::StackWidget;
2641 use std::cell::Cell;
2642 use std::rc::Rc;
2643
2644 let leaf_fired = Rc::new(Cell::new(false));
2645 let leaf_flag = leaf_fired.clone();
2646 let preview_fired = Rc::new(Cell::new(false));
2647 let preview_flag = preview_fired.clone();
2648
2649 let mut tree = WidgetTree::new();
2650 let leaf = tree.add(FillWidget::new().focusable().on_key(move |_e, _c| {
2651 leaf_flag.set(true);
2652 EventResponse::Handled
2653 }));
2654 let mid = tree.add(StackWidget::new().add_child(leaf));
2655 let _root = tree.add(StackWidget::new().add_child(mid).on_key_preview(
2656 move |_event, _c| {
2657 preview_flag.set(true);
2658 EventResponse::Ignored
2659 },
2660 ));
2661
2662 tree.layout(SizeProposal::exact(100.0, 50.0));
2663 tree.focus(leaf);
2664 tree.press_key(Key::Enter, Modifiers::NONE);
2665
2666 assert!(preview_fired.get(), "preview must always be invoked");
2667 assert!(
2668 leaf_fired.get(),
2669 "preview returning Ignored must not block the focused widget's on_key"
2670 );
2671 }
2672
2673 #[test]
2674 fn key_preview_excludes_focused_target_itself() {
2675 use crate::event::EventResponse;
2679 use std::cell::Cell;
2680 use std::rc::Rc;
2681
2682 let preview_on_target = Rc::new(Cell::new(false));
2683 let pf = preview_on_target.clone();
2684
2685 let mut tree = WidgetTree::new();
2686 let leaf = tree.add(FillWidget::new().focusable().on_key_preview(move |_e, _c| {
2687 pf.set(true);
2688 EventResponse::Handled
2689 }));
2690 tree.layout(SizeProposal::exact(100.0, 50.0));
2691 tree.focus(leaf);
2692 tree.press_key(Key::Enter, Modifiers::NONE);
2693
2694 assert!(
2695 !preview_on_target.get(),
2696 "the focused widget itself must not see its own on_key_preview"
2697 );
2698 }
2699
2700 #[test]
2701 fn key_preview_root_to_target_order() {
2702 use crate::event::EventResponse;
2706 use crate::test_widgets::StackWidget;
2707 use std::cell::RefCell;
2708 use std::rc::Rc;
2709
2710 let order: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
2711 let outer_log = order.clone();
2712 let inner_log = order.clone();
2713
2714 let mut tree = WidgetTree::new();
2715 let leaf = tree.add(FillWidget::new().focusable());
2716 let inner = tree.add(StackWidget::new().add_child(leaf).on_key_preview(
2717 move |event, _c| {
2718 if matches!(event, WidgetEvent::KeyDown { .. }) {
2719 inner_log.borrow_mut().push("inner");
2720 }
2721 EventResponse::Ignored
2722 },
2723 ));
2724 let _outer = tree.add(StackWidget::new().add_child(inner).on_key_preview(
2725 move |event, _c| {
2726 if matches!(event, WidgetEvent::KeyDown { .. }) {
2727 outer_log.borrow_mut().push("outer");
2728 }
2729 EventResponse::Ignored
2730 },
2731 ));
2732
2733 tree.layout(SizeProposal::exact(100.0, 50.0));
2734 tree.focus(leaf);
2735 tree.dispatch_event(WidgetEvent::KeyDown {
2736 key: Key::Enter,
2737 modifiers: Modifiers::NONE,
2738 text: None,
2739 });
2740
2741 assert_eq!(
2742 *order.borrow(),
2743 vec!["outer", "inner"],
2744 "preview must walk root → parent-of-target"
2745 );
2746 }
2747
2748 #[test]
2749 fn access_action_routes_to_cursored_target_not_focus() {
2750 use crate::signal::Signal;
2754 let a_fired = Signal::new(false);
2755 let b_fired = Signal::new(false);
2756 let a_cb = a_fired.clone();
2757 let b_cb = b_fired.clone();
2758
2759 let mut tree = WidgetTree::new();
2760 let a = tree.add(
2761 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| a_cb.set(true)),
2762 );
2763 let b = tree.add(
2764 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| b_cb.set(true)),
2765 );
2766 tree.layout(SizeProposal::exact(200.0, 100.0));
2767
2768 tree.focus(a);
2769 tree.dispatch_event(WidgetEvent::AccessAction {
2770 action: accesskit::Action::Click,
2771 target: Some(b),
2772 target_node: crate::accessibility::widget_id_to_node_id(b),
2773 data: None,
2774 });
2775
2776 assert!(b_fired.get(), "the cursored target must receive the action");
2777 assert!(
2778 !a_fired.get(),
2779 "the keyboard-focused widget must NOT receive an action targeting another node"
2780 );
2781 }
2782
2783 #[test]
2784 fn access_action_without_target_is_dropped_not_redirected_to_focus() {
2785 use crate::signal::Signal;
2788 let fired = Signal::new(false);
2789 let cb = fired.clone();
2790
2791 let mut tree = WidgetTree::new();
2792 let widget = tree.add(
2793 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| cb.set(true)),
2794 );
2795 tree.layout(SizeProposal::exact(200.0, 100.0));
2796
2797 tree.focus(widget);
2798 tree.dispatch_event(WidgetEvent::AccessAction {
2799 action: accesskit::Action::Click,
2800 target: None,
2801 target_node: crate::accessibility::root_node_id(),
2802 data: None,
2803 });
2804
2805 assert!(
2806 !fired.get(),
2807 "a target-less action must not be redirected to the focused widget"
2808 );
2809 }
2810
2811 #[test]
2818 fn shortcut_fires_matching_action_on_source_widget() {
2819 use crate::action::Action;
2820 use crate::shortcut::{KeyStroke, Shortcut};
2821 use std::cell::Cell;
2822 use std::rc::Rc;
2823
2824 let fired = Rc::new(Cell::new(false));
2825 let fired_flag = fired.clone();
2826
2827 let mut tree = WidgetTree::new();
2828 let widget = tree.add(FillWidget::new().focusable());
2829 tree.push_action(
2830 widget,
2831 Action::new("app.save").on_invoke(move |_intent, _ctx| {
2832 fired_flag.set(true);
2833 }),
2834 );
2835 tree.shortcut_registry_mut().register(
2836 Shortcut::new("app.save")
2837 .primary(KeyStroke::command(Key::S))
2838 .build(),
2839 );
2840
2841 tree.layout(SizeProposal::exact(100.0, 50.0));
2842 tree.focus(widget);
2843
2844 tree.press_key(Key::S, Modifiers::COMMAND);
2845 assert!(fired.get(), "matching action must fire on KeyDown");
2846 }
2847
2848 #[test]
2849 fn global_shortcut_fires_without_focused_widget() {
2850 use crate::action::Action;
2851 use crate::shortcut::{KeyStroke, Shortcut};
2852 use std::cell::Cell;
2853 use std::rc::Rc;
2854
2855 let fired = Rc::new(Cell::new(false));
2859 let fired_flag = fired.clone();
2860
2861 let mut tree = WidgetTree::new();
2862 let root = tree.add(FillWidget::new());
2863 tree.push_action(
2864 root,
2865 Action::new("app.save").on_invoke(move |_intent, _ctx| {
2866 fired_flag.set(true);
2867 }),
2868 );
2869 tree.shortcut_registry_mut().register(
2870 Shortcut::new("app.save")
2871 .primary(KeyStroke::command(Key::S))
2872 .build(),
2873 );
2874
2875 tree.layout(SizeProposal::exact(100.0, 50.0));
2876 tree.press_key(Key::S, Modifiers::COMMAND);
2879 assert!(
2880 fired.get(),
2881 "global shortcut must fire without a focused widget"
2882 );
2883 }
2884
2885 #[test]
2886 fn global_shortcut_fires_after_focused_widget_destroyed() {
2887 use crate::action::Action;
2888 use crate::shortcut::{KeyStroke, Shortcut};
2889 use std::cell::Cell;
2890 use std::rc::Rc;
2891
2892 let fired = Rc::new(Cell::new(false));
2897 let fired_flag = fired.clone();
2898
2899 let mut tree = WidgetTree::new();
2900 let root = tree.add(FillWidget::new());
2901 let focusable = tree.add_child(root, FillWidget::new().focusable());
2902 tree.push_action(
2903 root,
2904 Action::new("app.save").on_invoke(move |_intent, _ctx| {
2905 fired_flag.set(true);
2906 }),
2907 );
2908 tree.shortcut_registry_mut().register(
2909 Shortcut::new("app.save")
2910 .primary(KeyStroke::command(Key::S))
2911 .build(),
2912 );
2913
2914 tree.layout(SizeProposal::exact(100.0, 50.0));
2915 tree.focus(focusable);
2916 assert_eq!(tree.focused(), Some(focusable));
2917
2918 tree.destroy_subtree(focusable);
2921 assert_eq!(tree.focused(), None, "focus must clear when destroyed");
2922
2923 tree.press_key(Key::S, Modifiers::COMMAND);
2924 assert!(
2925 fired.get(),
2926 "global shortcut must still fire after the focused widget is destroyed"
2927 );
2928 }
2929
2930 #[test]
2931 fn scoped_shortcut_matches_only_when_focus_in_scope() {
2932 use crate::action::Action;
2933 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
2934 use std::cell::Cell;
2935 use std::rc::Rc;
2936
2937 let fired = Rc::new(Cell::new(0));
2938 let fired_flag = fired.clone();
2939
2940 let mut tree = WidgetTree::new();
2941 let scope_root = tree.add(FillWidget::new().focusable());
2942 let inside = tree.add_child(scope_root, FillWidget::new().focusable());
2943 let outside = tree.add(FillWidget::new().focusable());
2944
2945 tree.push_action(
2946 scope_root,
2947 Action::new("editor.find").on_invoke(move |_i, _c| {
2948 fired_flag.set(fired_flag.get() + 1);
2949 }),
2950 );
2951 tree.shortcut_registry_mut().register(
2952 Shortcut::new("editor.find")
2953 .primary(KeyStroke::command(Key::F))
2954 .scope(ShortcutScope::Scoped(scope_root))
2955 .build(),
2956 );
2957
2958 tree.layout(SizeProposal::exact(200.0, 100.0));
2959
2960 tree.focus(outside);
2962 tree.press_key(Key::F, Modifiers::COMMAND);
2963 assert_eq!(
2964 fired.get(),
2965 0,
2966 "scoped shortcut must not fire outside scope"
2967 );
2968
2969 tree.focus(inside);
2971 tree.press_key(Key::F, Modifiers::COMMAND);
2972 assert_eq!(
2973 fired.get(),
2974 1,
2975 "scoped shortcut must fire when focus in scope"
2976 );
2977 }
2978
2979 #[test]
2980 fn same_chord_scoped_first_falls_back_to_global_when_focus_outside() {
2981 use crate::action::Action;
2987 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
2988 use std::cell::Cell;
2989 use std::rc::Rc;
2990
2991 let scoped_fired = Rc::new(Cell::new(0));
2992 let global_fired = Rc::new(Cell::new(0));
2993 let sf = scoped_fired.clone();
2994 let gf = global_fired.clone();
2995
2996 let mut tree = WidgetTree::new();
2997 let root = tree.add(FillWidget::new());
2998 let editor = tree.add_child(root, FillWidget::new().focusable());
2999 let _editor_inner = tree.add_child(editor, FillWidget::new().focusable());
3000 let sidebar = tree.add_child(root, FillWidget::new().focusable());
3001
3002 tree.push_action(
3003 editor,
3004 Action::new("editor.saveBlock").on_invoke(move |_i, _c| sf.set(sf.get() + 1)),
3005 );
3006 tree.push_action(
3007 root,
3008 Action::new("zzz.global.save").on_invoke(move |_i, _c| gf.set(gf.get() + 1)),
3009 );
3010 tree.shortcut_registry_mut().register(
3011 Shortcut::new("editor.saveBlock")
3012 .primary(KeyStroke::command(Key::S))
3013 .scope(ShortcutScope::Scoped(editor))
3014 .build(),
3015 );
3016 tree.shortcut_registry_mut().register(
3017 Shortcut::new("zzz.global.save")
3018 .primary(KeyStroke::command(Key::S))
3019 .build(),
3020 );
3021
3022 tree.layout(SizeProposal::exact(200.0, 100.0));
3023 tree.focus(sidebar);
3024 tree.press_key(Key::S, Modifiers::COMMAND);
3025
3026 assert_eq!(global_fired.get(), 1, "applicable global must fire");
3027 assert_eq!(
3028 scoped_fired.get(),
3029 0,
3030 "inapplicable scoped binding must not eat the chord"
3031 );
3032 }
3033
3034 #[test]
3035 fn same_chord_global_first_yields_to_scoped_when_focus_inside() {
3036 use crate::action::Action;
3041 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
3042 use std::cell::Cell;
3043 use std::rc::Rc;
3044
3045 let scoped_fired = Rc::new(Cell::new(0));
3046 let global_fired = Rc::new(Cell::new(0));
3047 let sf = scoped_fired.clone();
3048 let gf = global_fired.clone();
3049
3050 let mut tree = WidgetTree::new();
3051 let root = tree.add(FillWidget::new());
3052 let editor = tree.add_child(root, FillWidget::new().focusable());
3053 let editor_inner = tree.add_child(editor, FillWidget::new().focusable());
3054 let sidebar = tree.add_child(root, FillWidget::new().focusable());
3055
3056 tree.push_action(
3057 editor,
3058 Action::new("editor.saveBlock").on_invoke(move |_i, _c| sf.set(sf.get() + 1)),
3059 );
3060 tree.push_action(
3061 root,
3062 Action::new("app.save").on_invoke(move |_i, _c| gf.set(gf.get() + 1)),
3063 );
3064 tree.shortcut_registry_mut().register(
3065 Shortcut::new("app.save")
3066 .primary(KeyStroke::command(Key::S))
3067 .build(),
3068 );
3069 tree.shortcut_registry_mut().register(
3070 Shortcut::new("editor.saveBlock")
3071 .primary(KeyStroke::command(Key::S))
3072 .scope(ShortcutScope::Scoped(editor))
3073 .build(),
3074 );
3075
3076 tree.layout(SizeProposal::exact(200.0, 100.0));
3077
3078 tree.focus(editor_inner);
3080 tree.press_key(Key::S, Modifiers::COMMAND);
3081 assert_eq!(
3082 scoped_fired.get(),
3083 1,
3084 "in-focus scoped must win over global"
3085 );
3086 assert_eq!(
3087 global_fired.get(),
3088 0,
3089 "global must yield to the scoped binding"
3090 );
3091
3092 tree.focus(sidebar);
3094 tree.press_key(Key::S, Modifiers::COMMAND);
3095 assert_eq!(scoped_fired.get(), 1, "scoped stays put outside its scope");
3096 assert_eq!(
3097 global_fired.get(),
3098 1,
3099 "global fires when focus leaves the scope"
3100 );
3101 }
3102
3103 #[test]
3104 fn propagated_action_lets_ancestor_handle() {
3105 use crate::action::Action;
3106 use crate::intent::IntentResponse;
3107 use crate::shortcut::{KeyStroke, Shortcut};
3108 use std::cell::Cell;
3109 use std::rc::Rc;
3110
3111 let inner_seen = Rc::new(Cell::new(false));
3112 let outer_seen = Rc::new(Cell::new(false));
3113 let inner_flag = inner_seen.clone();
3114 let outer_flag = outer_seen.clone();
3115
3116 let mut tree = WidgetTree::new();
3117 let outer = tree.add(FillWidget::new().focusable());
3118 let inner = tree.add_child(outer, FillWidget::new().focusable());
3119
3120 tree.push_action(
3122 inner,
3123 Action::new("app.save").on_invoke_with_response(move |_i, _c| {
3124 inner_flag.set(true);
3125 IntentResponse::Propagated
3126 }),
3127 );
3128 tree.push_action(
3129 outer,
3130 Action::new("app.save").on_invoke(move |_i, _c| {
3131 outer_flag.set(true);
3132 }),
3133 );
3134 tree.shortcut_registry_mut().register(
3135 Shortcut::new("app.save")
3136 .primary(KeyStroke::command(Key::S))
3137 .build(),
3138 );
3139
3140 tree.layout(SizeProposal::exact(100.0, 50.0));
3141 tree.focus(inner);
3142
3143 tree.press_key(Key::S, Modifiers::COMMAND);
3144 assert!(inner_seen.get(), "inner action observed the intent");
3145 assert!(outer_seen.get(), "outer action reached after Propagated");
3146 }
3147
3148 #[test]
3149 fn handled_action_stops_propagation() {
3150 use crate::action::Action;
3151 use crate::shortcut::{KeyStroke, Shortcut};
3152 use std::cell::Cell;
3153 use std::rc::Rc;
3154
3155 let inner_seen = Rc::new(Cell::new(false));
3156 let outer_seen = Rc::new(Cell::new(false));
3157 let inner_flag = inner_seen.clone();
3158 let outer_flag = outer_seen.clone();
3159
3160 let mut tree = WidgetTree::new();
3161 let outer = tree.add(FillWidget::new().focusable());
3162 let inner = tree.add_child(outer, FillWidget::new().focusable());
3163
3164 tree.push_action(
3165 inner,
3166 Action::new("app.save").on_invoke(move |_i, _c| {
3167 inner_flag.set(true);
3168 }),
3169 );
3170 tree.push_action(
3171 outer,
3172 Action::new("app.save").on_invoke(move |_i, _c| {
3173 outer_flag.set(true);
3174 }),
3175 );
3176 tree.shortcut_registry_mut().register(
3177 Shortcut::new("app.save")
3178 .primary(KeyStroke::command(Key::S))
3179 .build(),
3180 );
3181
3182 tree.layout(SizeProposal::exact(100.0, 50.0));
3183 tree.focus(inner);
3184
3185 tree.press_key(Key::S, Modifiers::COMMAND);
3186 assert!(inner_seen.get());
3187 assert!(!outer_seen.get(), "Handled at inner must stop propagation");
3188 }
3189
3190 #[test]
3191 fn disabled_action_propagates_by_default() {
3192 use crate::action::Action;
3193 use crate::shortcut::{KeyStroke, Shortcut};
3194 use crate::signal::Signal;
3195 use std::cell::Cell;
3196 use std::rc::Rc;
3197
3198 let inner_seen = Rc::new(Cell::new(false));
3199 let outer_seen = Rc::new(Cell::new(false));
3200 let inner_flag = inner_seen.clone();
3201 let outer_flag = outer_seen.clone();
3202
3203 let mut tree = WidgetTree::new();
3204 let outer = tree.add(FillWidget::new().focusable());
3205 let inner = tree.add_child(outer, FillWidget::new().focusable());
3206
3207 let enabled = Signal::new(false);
3208 tree.push_action(
3209 inner,
3210 Action::new("app.save")
3211 .enabled_when(enabled.clone())
3212 .on_invoke(move |_i, _c| {
3213 inner_flag.set(true);
3214 }),
3215 );
3216 tree.push_action(
3217 outer,
3218 Action::new("app.save").on_invoke(move |_i, _c| {
3219 outer_flag.set(true);
3220 }),
3221 );
3222 tree.shortcut_registry_mut().register(
3223 Shortcut::new("app.save")
3224 .primary(KeyStroke::command(Key::S))
3225 .build(),
3226 );
3227
3228 tree.layout(SizeProposal::exact(100.0, 50.0));
3229 tree.focus(inner);
3230
3231 tree.press_key(Key::S, Modifiers::COMMAND);
3232 assert!(!inner_seen.get(), "disabled inner must not run");
3233 assert!(
3234 outer_seen.get(),
3235 "intent must propagate past disabled inner"
3236 );
3237 }
3238
3239 #[test]
3240 fn disabled_action_with_non_propagating_shortcut_consumes() {
3241 use crate::action::Action;
3242 use crate::shortcut::{KeyStroke, Shortcut};
3243 use crate::signal::Signal;
3244 use std::cell::Cell;
3245 use std::rc::Rc;
3246
3247 let inner_seen = Rc::new(Cell::new(false));
3248 let outer_seen = Rc::new(Cell::new(false));
3249 let inner_flag = inner_seen.clone();
3250 let outer_flag = outer_seen.clone();
3251
3252 let mut tree = WidgetTree::new();
3253 let outer = tree.add(FillWidget::new().focusable());
3254 let inner = tree.add_child(outer, FillWidget::new().focusable());
3255
3256 let enabled = Signal::new(false);
3257 tree.push_action(
3258 inner,
3259 Action::new("app.save")
3260 .enabled_when(enabled.clone())
3261 .on_invoke(move |_i, _c| {
3262 inner_flag.set(true);
3263 }),
3264 );
3265 tree.push_action(
3266 outer,
3267 Action::new("app.save").on_invoke(move |_i, _c| {
3268 outer_flag.set(true);
3269 }),
3270 );
3271 tree.shortcut_registry_mut().register(
3272 Shortcut::new("app.save")
3273 .primary(KeyStroke::command(Key::S))
3274 .propagate_when_disabled(false)
3275 .build(),
3276 );
3277
3278 tree.layout(SizeProposal::exact(100.0, 50.0));
3279 tree.focus(inner);
3280
3281 tree.press_key(Key::S, Modifiers::COMMAND);
3282 assert!(!inner_seen.get(), "disabled inner still does not run");
3283 assert!(
3284 !outer_seen.get(),
3285 "intent must NOT propagate when shortcut disallows it"
3286 );
3287 }
3288
3289 #[test]
3290 fn send_intent_from_handler_reaches_ancestor_action() {
3291 use crate::action::Action;
3292 use crate::intent::Intent;
3293 use std::cell::Cell;
3294 use std::rc::Rc;
3295
3296 let save_seen = Rc::new(Cell::new(false));
3297 let save_flag = save_seen.clone();
3298
3299 let mut tree = WidgetTree::new();
3300 let root = tree.add(FillWidget::new());
3301 let button = tree.add_child(
3302 root,
3303 FillWidget::new().on_tap(|_pos, ctx| {
3304 ctx.send_intent(Intent::new("app.save"));
3305 }),
3306 );
3307 tree.push_action(
3308 root,
3309 Action::new("app.save").on_invoke(move |_i, _c| {
3310 save_flag.set(true);
3311 }),
3312 );
3313
3314 tree.layout(SizeProposal::exact(100.0, 50.0));
3315 tree.click(button);
3316 assert!(
3317 save_seen.get(),
3318 "ctx.send_intent must reach ancestor action"
3319 );
3320 }
3321
3322 #[test]
3323 fn widget_type_histogram_counts_distinct_types() {
3324 let mut tree = WidgetTree::new();
3329 let _ = tree.add(FillWidget::new());
3330 let _ = tree.add(FillWidget::new());
3331 let _ = tree.add(FillWidget::new());
3332 tree.layout(SizeProposal::exact(100.0, 100.0));
3333 let histogram = tree.widget_type_histogram();
3334 let total: u32 = histogram.values().sum();
3335 assert!(
3336 total >= 3,
3337 "expected at least 3 active widgets, got {total}: {histogram:?}"
3338 );
3339 let fillwidget_entries: u32 = histogram
3340 .iter()
3341 .filter(|(k, _)| k.contains("FillWidget"))
3342 .map(|(_, v)| *v)
3343 .sum();
3344 assert!(
3345 fillwidget_entries >= 3,
3346 "expected ≥3 FillWidget instances; histogram = {histogram:?}"
3347 );
3348 assert_eq!(tree.active_widget_count() as u32, total);
3349 }
3350
3351 #[test]
3352 fn intent_source_tagged_handler_for_tap_activation() {
3353 use crate::action::Action;
3357 use crate::intent::Intent;
3358 use crate::telemetry::IntentSource;
3359 use std::cell::Cell;
3360 use std::rc::Rc;
3361 let captured = Rc::new(Cell::new(IntentSource::Unknown));
3362 let captured_for_action = captured.clone();
3363
3364 let mut tree = WidgetTree::new();
3365 let root = tree.add(FillWidget::new());
3366 let button = tree.add_child(
3367 root,
3368 FillWidget::new().on_tap(|_pos, ctx| {
3369 ctx.send_intent(Intent::new("app.save"));
3370 }),
3371 );
3372 tree.push_action(
3373 root,
3374 Action::new("app.save").on_invoke(move |intent, _c| {
3375 captured_for_action.set(intent.source);
3376 }),
3377 );
3378
3379 tree.layout(SizeProposal::exact(100.0, 50.0));
3380 tree.click(button);
3381 assert_eq!(
3382 captured.get(),
3383 IntentSource::Handler,
3384 "tap-driven intent must tag IntentSource::Handler"
3385 );
3386 }
3387
3388 #[test]
3389 fn intent_source_programmatic_when_no_handler_active() {
3390 use crate::intent::Intent;
3391 use crate::telemetry::IntentSource;
3392 let intent = Intent::new("app.demo");
3393 assert_eq!(intent.source, IntentSource::Programmatic);
3394
3395 let mut ctx = EventContext::new();
3397 ctx.send_intent(Intent::new("app.demo"));
3398 let queued = ctx.pending_intents.first().expect("intent queued");
3399 assert_eq!(queued.source, IntentSource::Programmatic);
3400 }
3401
3402 #[test]
3403 fn with_intent_source_overrides_for_managed_widgets() {
3404 use crate::intent::Intent;
3405 use crate::telemetry::IntentSource;
3406 let mut ctx = EventContext::new();
3407 ctx.with_intent_source(IntentSource::Menu, |ctx| {
3408 ctx.send_intent(Intent::new("app.demo"));
3409 });
3410 let queued = ctx.pending_intents.first().expect("intent queued");
3411 assert_eq!(
3412 queued.source,
3413 IntentSource::Menu,
3414 "with_intent_source(Menu) must tag the dispatched intent"
3415 );
3416
3417 ctx.send_intent(Intent::new("app.next"));
3421 let next = ctx.pending_intents.last().expect("second intent");
3422 assert_eq!(next.source, IntentSource::Programmatic);
3423 }
3424
3425 #[test]
3426 fn disabled_shortcut_falls_through_to_focused_widget() {
3427 use crate::action::Action;
3428 use crate::shortcut::{KeyStroke, Shortcut};
3429 use crate::signal::Signal;
3430 use std::cell::Cell;
3431 use std::rc::Rc;
3432
3433 let action_fired = Rc::new(Cell::new(false));
3434 let on_key_fired = Rc::new(Cell::new(false));
3435 let af = action_fired.clone();
3436 let kf = on_key_fired.clone();
3437
3438 let enabled = Signal::new(false);
3439
3440 let mut tree = WidgetTree::new();
3441 let widget = tree.add(FillWidget::new().focusable().on_key(move |event, _ctx| {
3442 if matches!(
3443 event,
3444 WidgetEvent::KeyDown {
3445 key: Key::S,
3446 modifiers,
3447 ..
3448 } if modifiers.ctrl()
3449 ) {
3450 kf.set(true);
3451 return EventResponse::Handled;
3452 }
3453 EventResponse::Ignored
3454 }));
3455 tree.push_action(
3456 widget,
3457 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
3458 );
3459 tree.shortcut_registry_mut().register(
3460 Shortcut::new("app.save")
3461 .primary(KeyStroke::command(Key::S))
3462 .enabled_when(enabled.clone())
3463 .build(),
3464 );
3465
3466 tree.layout(SizeProposal::exact(100.0, 50.0));
3467 tree.focus(widget);
3468
3469 tree.press_key(Key::S, Modifiers::COMMAND);
3471 assert!(
3472 !action_fired.get(),
3473 "disabled shortcut must not invoke its action"
3474 );
3475 assert!(
3476 on_key_fired.get(),
3477 "disabled shortcut must let KeyDown reach the focused widget"
3478 );
3479
3480 on_key_fired.set(false);
3482 enabled.set(true);
3483 tree.press_key(Key::S, Modifiers::COMMAND);
3484 assert!(action_fired.get(), "re-enabled shortcut must dispatch");
3485 assert!(
3486 !on_key_fired.get(),
3487 "enabled shortcut must consume the KeyDown"
3488 );
3489 }
3490
3491 #[test]
3492 fn keyboard_capture_bypasses_shortcut() {
3493 use crate::action::Action;
3494 use crate::shortcut::{KeyStroke, Shortcut};
3495 use std::cell::Cell;
3496 use std::rc::Rc;
3497
3498 fn run(capture: bool) -> (bool, bool) {
3503 let action_fired = Rc::new(Cell::new(false));
3504 let on_key_fired = Rc::new(Cell::new(false));
3505 let af = action_fired.clone();
3506 let kf = on_key_fired.clone();
3507
3508 let mut tree = WidgetTree::new();
3509 let widget = tree.add(
3510 FillWidget::new()
3511 .focusable()
3512 .keyboard_capture(capture)
3513 .on_key(move |event, _ctx| {
3514 if matches!(
3515 event,
3516 WidgetEvent::KeyDown { key: Key::S, modifiers, .. } if modifiers.ctrl()
3517 ) {
3518 kf.set(true);
3519 return EventResponse::Handled;
3520 }
3521 EventResponse::Ignored
3522 }),
3523 );
3524 tree.push_action(
3525 widget,
3526 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
3527 );
3528 tree.shortcut_registry_mut().register(
3529 Shortcut::new("app.save")
3530 .primary(KeyStroke::command(Key::S))
3531 .build(),
3532 );
3533
3534 tree.layout(SizeProposal::exact(100.0, 50.0));
3535 tree.focus(widget);
3536 tree.press_key(Key::S, Modifiers::COMMAND);
3537 (action_fired.get(), on_key_fired.get())
3538 }
3539
3540 let (action, on_key) = run(true);
3542 assert!(
3543 !action,
3544 "keyboard_capture must suppress the shortcut action"
3545 );
3546 assert!(on_key, "keyboard_capture must deliver the raw KeyDown");
3547
3548 let (action, on_key) = run(false);
3550 assert!(action, "without capture the shortcut must fire");
3551 assert!(!on_key, "without capture the widget must not see the key");
3552 }
3553
3554 #[test]
3555 fn ctrl_tab_always_escapes_a_keyboard_capture_surface() {
3556 use std::cell::Cell;
3557 use std::rc::Rc;
3558
3559 let saw_key = Rc::new(Cell::new(false));
3565 let sk = saw_key.clone();
3566
3567 let mut tree = WidgetTree::new();
3568 let capture = tree.add(
3569 FillWidget::new()
3570 .focusable()
3571 .keyboard_capture(true)
3572 .on_key(move |_event, _ctx| {
3574 sk.set(true);
3575 EventResponse::Handled
3576 }),
3577 );
3578 let neighbour = tree.add(FillWidget::new().focusable());
3579 tree.layout(SizeProposal::exact(100.0, 50.0));
3580
3581 tree.focus(capture);
3584 tree.press_key(Key::Tab, Modifiers::NONE);
3585 assert!(saw_key.get(), "plain Tab must reach the capture surface");
3586 assert_eq!(
3587 tree.focused(),
3588 Some(capture),
3589 "plain Tab must not move focus off a capture surface"
3590 );
3591
3592 saw_key.set(false);
3594 tree.press_key(Key::Tab, Modifiers::CTRL);
3595 assert!(
3596 !saw_key.get(),
3597 "Ctrl+Tab is reserved and must not reach the capture surface"
3598 );
3599 assert_eq!(
3600 tree.focused(),
3601 Some(neighbour),
3602 "Ctrl+Tab must move focus out of a capture surface"
3603 );
3604
3605 tree.focus(capture);
3607 tree.press_key(Key::Tab, Modifiers::CTRL | Modifiers::SHIFT);
3608 assert_eq!(
3609 tree.focused(),
3610 Some(neighbour),
3611 "Ctrl+Shift+Tab must move focus out of a capture surface"
3612 );
3613 }
3614
3615 #[test]
3616 fn scope_mismatch_does_not_invoke_on_activate() {
3617 use crate::intent::Intent;
3618 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
3619 use std::cell::Cell;
3620 use std::rc::Rc;
3621
3622 let activated = Rc::new(Cell::new(false));
3628 let activated_flag = activated.clone();
3629
3630 let mut tree = WidgetTree::new();
3631 let scope_root = tree.add(FillWidget::new().focusable());
3632 let outside = tree.add(FillWidget::new().focusable());
3633
3634 tree.shortcut_registry_mut().register(
3635 Shortcut::new("editor.find")
3636 .primary(KeyStroke::command(Key::F))
3637 .scope(ShortcutScope::Scoped(scope_root))
3638 .on_activate(move |_ks, _ctx| {
3639 activated_flag.set(true);
3640 Intent::new("editor.find")
3641 })
3642 .build(),
3643 );
3644
3645 tree.layout(SizeProposal::exact(200.0, 100.0));
3646 tree.focus(outside);
3647
3648 tree.press_key(Key::F, Modifiers::COMMAND);
3649 assert!(
3650 !activated.get(),
3651 "on_activate must not run when focus is outside the shortcut's scope"
3652 );
3653 }
3654
3655 #[test]
3656 fn key_capture_runs_callback_and_bypasses_registry() {
3657 use crate::action::Action;
3658 use crate::shortcut::{KeyStroke, Shortcut};
3659 use std::cell::Cell;
3660 use std::rc::Rc;
3661
3662 let action_fired = Rc::new(Cell::new(false));
3663 let af = action_fired.clone();
3664 let captured = Rc::new(Cell::new(None));
3665 let cf = captured.clone();
3666
3667 let mut tree = WidgetTree::new();
3668 let widget = tree.add(FillWidget::new().focusable());
3669 tree.push_action(
3670 widget,
3671 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
3672 );
3673 tree.shortcut_registry_mut().register(
3674 Shortcut::new("app.save")
3675 .primary(KeyStroke::command(Key::S))
3676 .build(),
3677 );
3678
3679 tree.layout(SizeProposal::exact(100.0, 50.0));
3680 tree.focus(widget);
3681
3682 let handle = tree.begin_key_capture(move |ks, _reg, _ctx| cf.set(Some(ks)));
3683 assert!(tree.is_capturing_keys());
3684
3685 tree.press_key(Key::S, Modifiers::COMMAND);
3686 assert_eq!(
3687 captured.get(),
3688 Some(KeyStroke::command(Key::S)),
3689 "capture callback must receive the chord"
3690 );
3691 assert!(
3692 !action_fired.get(),
3693 "shortcut action must not fire while capture is armed"
3694 );
3695 assert!(
3696 !tree.is_capturing_keys(),
3697 "capture is one-shot; next KeyDown flows normally"
3698 );
3699 drop(handle);
3700 }
3701
3702 #[test]
3703 fn key_capture_can_rebind_through_registry() {
3704 use crate::shortcut::{KeyStroke, Shortcut};
3705
3706 let mut tree = WidgetTree::new();
3707 let widget = tree.add(FillWidget::new().focusable());
3708 tree.shortcut_registry_mut().register(
3709 Shortcut::new("app.save")
3710 .primary(KeyStroke::command(Key::S))
3711 .build(),
3712 );
3713
3714 tree.layout(SizeProposal::exact(100.0, 50.0));
3715 tree.focus(widget);
3716
3717 let _h = tree.begin_key_capture(|ks, reg, _ctx| {
3719 reg.rebind_primary("app.save", Some(ks));
3720 });
3721
3722 tree.press_key(Key::B, Modifiers::COMMAND | Modifiers::SHIFT);
3723 assert_eq!(
3724 tree.shortcut_registry()
3725 .effective("app.save")
3726 .unwrap()
3727 .primary,
3728 Some(KeyStroke::command_shift(Key::B))
3729 );
3730 }
3731
3732 #[test]
3733 fn dropping_capture_handle_cancels_capture() {
3734 use crate::shortcut::{KeyStroke, Shortcut};
3735 use std::cell::Cell;
3736 use std::rc::Rc;
3737
3738 let action_fired = Rc::new(Cell::new(false));
3739 let af = action_fired.clone();
3740 let capture_fired = Rc::new(Cell::new(false));
3741 let cf = capture_fired.clone();
3742
3743 let mut tree = WidgetTree::new();
3744 let widget = tree.add(FillWidget::new().focusable());
3745 tree.push_action(
3746 widget,
3747 crate::action::Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
3748 );
3749 tree.shortcut_registry_mut().register(
3750 Shortcut::new("app.save")
3751 .primary(KeyStroke::command(Key::S))
3752 .build(),
3753 );
3754 tree.layout(SizeProposal::exact(100.0, 50.0));
3755 tree.focus(widget);
3756
3757 {
3761 let _h = tree.begin_key_capture(move |_ks, _reg, _ctx| cf.set(true));
3762 assert!(tree.is_capturing_keys());
3763 }
3765 assert!(
3766 !tree.is_capturing_keys(),
3767 "dropping the handle must cancel the capture"
3768 );
3769
3770 tree.press_key(Key::S, Modifiers::COMMAND);
3771 assert!(!capture_fired.get(), "cancelled capture must not fire");
3772 assert!(
3773 action_fired.get(),
3774 "shortcut action runs after capture was cancelled"
3775 );
3776 }
3777
3778 #[test]
3779 fn second_begin_key_capture_does_not_racecancel_first() {
3780 use std::cell::Cell;
3781 use std::rc::Rc;
3782
3783 let first = Rc::new(Cell::new(false));
3784 let second = Rc::new(Cell::new(false));
3785 let f = first.clone();
3786 let s = second.clone();
3787
3788 let mut tree = WidgetTree::new();
3789 let widget = tree.add(FillWidget::new().focusable());
3790 tree.layout(SizeProposal::exact(100.0, 50.0));
3791 tree.focus(widget);
3792
3793 let h1 = tree.begin_key_capture(move |_ks, _reg, _ctx| f.set(true));
3799 let _h2 = tree.begin_key_capture(move |_ks, _reg, _ctx| s.set(true));
3800 drop(h1);
3801
3802 assert!(
3803 tree.is_capturing_keys(),
3804 "dropping the older handle must not cancel the active capture"
3805 );
3806 tree.press_key(Key::K, Modifiers::COMMAND);
3807 assert!(!first.get());
3808 assert!(second.get(), "newest capture wins");
3809 }
3810
3811 #[test]
3812 fn capture_callback_can_send_intent() {
3813 use crate::action::Action;
3814 use crate::intent::Intent;
3815
3816 use std::cell::Cell;
3817 use std::rc::Rc;
3818
3819 let ran = Rc::new(Cell::new(false));
3820 let flag = ran.clone();
3821
3822 let mut tree = WidgetTree::new();
3823 let widget = tree.add(FillWidget::new().focusable());
3824 tree.push_action(
3825 widget,
3826 Action::new("app.save").on_invoke(move |_i, _c| flag.set(true)),
3827 );
3828 tree.layout(SizeProposal::exact(100.0, 50.0));
3829 tree.focus(widget);
3830
3831 let _h = tree.begin_key_capture(|_ks, _reg, ctx| {
3832 ctx.send_intent(Intent::new("app.save"));
3833 });
3834 tree.press_key(Key::X, Modifiers::COMMAND);
3835 assert!(
3836 ran.get(),
3837 "intent queued from capture callback must dispatch"
3838 );
3839 }
3840
3841 #[test]
3842 fn binding_registry_does_not_accumulate_across_rebuilds() {
3843 use crate::binding::BindingLevel;
3844 use crate::signal::Signal;
3845
3846 #[derive(Debug)]
3847 struct BoundLeaf {
3848 tick: Signal<u64>,
3849 }
3850 impl crate::widget::Widget for BoundLeaf {
3851 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
3852 self.tick.bind_to(
3853 ctx.self_id(),
3854 ctx.binding_registry(),
3855 BindingLevel::Relayout,
3856 );
3857 Vec::new()
3858 }
3859 fn layout_response(
3860 &self,
3861 proposal: SizeProposal,
3862 _ctx: &crate::widget::LayoutContext,
3863 ) -> crate::widget::LayoutResponse {
3864 proposal.resolve(10.0, 10.0).into()
3865 }
3866 }
3867
3868 let mut tree = WidgetTree::new();
3869 let tick = Signal::new(0_u64);
3870 let widget = tree.add(BoundLeaf { tick: tick.clone() });
3871 tree.layout(SizeProposal::exact(200.0, 200.0));
3872 let after_first_build = tree.binding_registry().len();
3873 assert!(after_first_build >= 1);
3874
3875 for _ in 0..5 {
3879 tree.arena.mark_needs_rebuild(widget);
3880 tree.layout(SizeProposal::exact(200.0, 200.0));
3881 }
3882 assert_eq!(
3883 tree.binding_registry().len(),
3884 after_first_build,
3885 "bindings must be cleared on rebuild"
3886 );
3887
3888 tree.destroy_subtree(widget);
3889 assert_eq!(
3890 tree.binding_registry().len(),
3891 0,
3892 "bindings must be cleared on destroy"
3893 );
3894 let _ = tick;
3896 }
3897
3898 #[test]
3899 fn ctx_destroy_cancels_animations_and_bindings_via_deferred_path() {
3900 use crate::binding::BindingLevel;
3908 use crate::signal::Signal;
3909 use std::time::{Duration, Instant};
3910 use teksilo_tokens::Easing;
3911
3912 #[derive(Debug)]
3913 struct BoundLeaf {
3914 tick: Signal<u64>,
3915 }
3916 impl crate::widget::Widget for BoundLeaf {
3917 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
3918 self.tick.bind_to(
3919 ctx.self_id(),
3920 ctx.binding_registry(),
3921 BindingLevel::Relayout,
3922 );
3923 Vec::new()
3924 }
3925 fn layout_response(
3926 &self,
3927 proposal: SizeProposal,
3928 _ctx: &crate::widget::LayoutContext,
3929 ) -> crate::widget::LayoutResponse {
3930 proposal.resolve(10.0, 10.0).into()
3931 }
3932 }
3933
3934 let mut tree = WidgetTree::new();
3935 let widget = tree.add(BoundLeaf {
3936 tick: Signal::new(0_u64),
3937 });
3938 tree.layout(SizeProposal::exact(200.0, 200.0));
3939 assert!(tree.binding_registry().len() >= 1);
3940
3941 let anim = Signal::<f32>::new_animated(0.0);
3944 tree.animation_scheduler.animate(
3945 &anim,
3946 widget,
3947 1.0,
3948 Duration::from_secs(10),
3949 Easing::Linear,
3950 Instant::now(),
3951 );
3952 assert_eq!(tree.animation_scheduler.active_count(), 1);
3953
3954 let mut noop = crate::window::NoopWindowOps;
3956 tree.run_with_event_context(&mut noop, |ctx| ctx.destroy(widget));
3957
3958 assert_eq!(
3959 tree.animation_scheduler.active_count(),
3960 0,
3961 "ctx.destroy must cancel animations owned by the destroyed widget"
3962 );
3963 assert_eq!(
3964 tree.binding_registry().len(),
3965 0,
3966 "ctx.destroy must unregister the destroyed widget's bindings"
3967 );
3968 assert!(
3969 tree.arena.get(widget).is_none(),
3970 "node must be removed from the arena"
3971 );
3972 }
3973
3974 #[test]
3975 fn clear_shortcut_override_via_event_context_restores_default() {
3976 use crate::shortcut::{KeyStroke, Shortcut};
3977
3978 let mut tree = WidgetTree::new();
3979 tree.shortcut_registry_mut().register(
3980 Shortcut::new("app.save")
3981 .primary(KeyStroke::command(Key::S))
3982 .build(),
3983 );
3984 tree.shortcut_registry_mut()
3985 .rebind_primary("app.save", Some(KeyStroke::alt(Key::S)));
3986
3987 let source = tree.add(FillWidget::new());
3988 let mut ctx = EventContext::new();
3989 ctx.clear_shortcut_override("app.save");
3990 tree.collect_from_ctx(ctx, source);
3991
3992 assert_eq!(
3993 tree.shortcut_registry()
3994 .effective("app.save")
3995 .unwrap()
3996 .primary,
3997 Some(KeyStroke::command(Key::S))
3998 );
3999 }
4000
4001 #[test]
4002 fn rebind_shortcut_primary_via_event_context() {
4003 use crate::shortcut::{KeyStroke, Shortcut};
4004
4005 let mut tree = WidgetTree::new();
4006 tree.shortcut_registry_mut().register(
4007 Shortcut::new("app.save")
4008 .primary(KeyStroke::command(Key::S))
4009 .build(),
4010 );
4011 let source = tree.add(FillWidget::new());
4012
4013 let mut ctx = EventContext::new();
4014 ctx.rebind_shortcut_primary("app.save", Some(KeyStroke::alt(Key::S)));
4015 tree.collect_from_ctx(ctx, source);
4016
4017 assert_eq!(
4018 tree.shortcut_registry()
4019 .effective("app.save")
4020 .unwrap()
4021 .primary,
4022 Some(KeyStroke::alt(Key::S))
4023 );
4024 }
4025
4026 #[test]
4027 fn unregister_all_for_owner_called_on_destroy() {
4028 use crate::shortcut::{KeyStroke, Shortcut};
4029
4030 let mut tree = WidgetTree::new();
4031 let widget = tree.add(FillWidget::new());
4032 let widget_owner = widget;
4033 tree.shortcut_registry_mut().register_owned(
4034 Shortcut::new("scoped.thing")
4035 .primary(KeyStroke::command(Key::K))
4036 .build(),
4037 widget_owner,
4038 );
4039 assert!(
4040 tree.shortcut_registry()
4041 .get_default("scoped.thing")
4042 .is_some()
4043 );
4044
4045 tree.destroy_subtree(widget);
4046 assert!(
4047 tree.shortcut_registry()
4048 .get_default("scoped.thing")
4049 .is_none(),
4050 "destroying the owner must unregister its shortcut"
4051 );
4052 }
4053
4054 #[test]
4058 fn global_action_reached_from_unrelated_source() {
4059 use crate::action::Action;
4060 use crate::intent::Intent;
4061 use std::cell::Cell;
4062 use std::rc::Rc;
4063
4064 #[derive(Debug)]
4065 struct Registrar(Rc<Cell<bool>>);
4066 impl crate::widget::Widget for Registrar {
4067 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
4068 let flag = self.0.clone();
4069 ctx.register_action_global(
4070 Action::new("test.global").on_invoke(move |_i, _c| flag.set(true)),
4071 );
4072 vec![]
4073 }
4074 fn layout_response(
4075 &self,
4076 _p: teksilo_canvas::SizeProposal,
4077 _c: &crate::widget::LayoutContext,
4078 ) -> crate::widget::LayoutResponse {
4079 teksilo_canvas::Size::new(0.0, 0.0).into()
4080 }
4081 }
4082
4083 let mut tree = WidgetTree::new();
4084 let fired = Rc::new(Cell::new(false));
4085 let registrar = tree.add(Registrar(fired.clone()));
4086 let source = tree.add(FillWidget::new()); let mut ops = crate::window::NoopWindowOps;
4088
4089 tree.dispatch_intent(source, Intent::new("test.global"), true, &mut ops);
4090 assert!(
4091 fired.get(),
4092 "global action must fire from an unrelated source"
4093 );
4094
4095 fired.set(false);
4097 tree.destroy_subtree(registrar);
4098 tree.dispatch_intent(source, Intent::new("test.global"), true, &mut ops);
4099 assert!(
4100 !fired.get(),
4101 "destroying the owner must remove its global action"
4102 );
4103 }
4104
4105 #[test]
4117 fn hit_test_through_translate_scope() {
4118 use crate::test_widgets::StackWidget;
4119 let mut tree = WidgetTree::new();
4120 let child = tree.add(FillWidget::new());
4121 let parent = tree.add(StackWidget::new().add_child(child));
4122 tree.set_transform(parent, teksilo_canvas::Transform2D::translate(100.0, 0.0));
4124 tree.layout(SizeProposal::exact(100.0, 50.0));
4125
4126 assert_eq!(
4130 tree.hit_test(Point::new(50.0, 25.0)),
4131 None,
4132 "pre-transform area is not visually populated and must not hit"
4133 );
4134 assert_eq!(
4136 tree.hit_test(Point::new(150.0, 25.0)),
4137 Some(child),
4138 "visually-rendered area must hit the child"
4139 );
4140 assert_eq!(tree.hit_test(Point::new(250.0, 25.0)), None);
4142 }
4143
4144 #[test]
4145 fn hit_test_through_scale_scope() {
4146 use crate::test_widgets::StackWidget;
4147 let mut tree = WidgetTree::new();
4148 let child = tree.add(FillWidget::new());
4149 let parent = tree.add(StackWidget::new().add_child(child));
4150 tree.set_transform(parent, teksilo_canvas::Transform2D::scale(0.5, 0.5));
4153 tree.layout(SizeProposal::exact(100.0, 50.0));
4154
4155 assert_eq!(tree.hit_test(Point::new(25.0, 12.0)), Some(child));
4157 assert_eq!(
4160 tree.hit_test(Point::new(75.0, 25.0)),
4161 None,
4162 "scaled-out region must not hit"
4163 );
4164 }
4165
4166 #[test]
4167 fn hit_test_through_nested_transforms_compose() {
4168 use crate::test_widgets::StackWidget;
4169 let mut tree = WidgetTree::new();
4170 let leaf = tree.add(FillWidget::new());
4171 let inner = tree.add(StackWidget::new().add_child(leaf));
4172 let outer = tree.add(StackWidget::new().add_child(inner));
4173 tree.set_transform(outer, teksilo_canvas::Transform2D::translate(100.0, 0.0));
4177 tree.set_transform(inner, teksilo_canvas::Transform2D::scale(2.0, 2.0));
4178 tree.layout(SizeProposal::exact(50.0, 25.0));
4179
4180 assert_eq!(tree.hit_test(Point::new(150.0, 25.0)), Some(leaf));
4184 assert_eq!(tree.hit_test(Point::new(50.0, 25.0)), None);
4185 assert_eq!(tree.hit_test(Point::new(250.0, 25.0)), None);
4186 }
4187
4188 #[test]
4189 fn hit_test_identity_transform_unchanged() {
4190 let mut tree = WidgetTree::new();
4194 let widget = tree.add(FillWidget::new());
4195 tree.set_transform(widget, teksilo_canvas::Transform2D::IDENTITY);
4196 tree.layout(SizeProposal::exact(100.0, 50.0));
4197 assert_eq!(tree.hit_test(Point::new(50.0, 25.0)), Some(widget));
4198 }
4199
4200 #[test]
4201 fn arena_effective_transform_composes_ancestors() {
4202 use crate::test_widgets::StackWidget;
4213 let mut tree = WidgetTree::new();
4214 let leaf = tree.add(FillWidget::new());
4215 let inner = tree.add(StackWidget::new().add_child(leaf));
4216 let outer = tree.add(StackWidget::new().add_child(inner));
4217 tree.set_transform(outer, teksilo_canvas::Transform2D::translate(100.0, 0.0));
4218 tree.set_transform(inner, teksilo_canvas::Transform2D::scale(2.0, 2.0));
4219 tree.layout(SizeProposal::exact(50.0, 25.0));
4220
4221 let eff = tree.arena.effective_transform(leaf);
4222 let expected = teksilo_canvas::Transform2D::scale(2.0, 2.0)
4223 .then(&teksilo_canvas::Transform2D::translate(100.0, 0.0));
4224 for (a, b) in eff.m.iter().zip(expected.m.iter()) {
4225 assert!(
4226 (a - b).abs() < 1e-5,
4227 "effective_transform mismatch: got {:?}, want {:?}",
4228 eff.m,
4229 expected.m
4230 );
4231 }
4232
4233 let screen_origin = eff.apply_point(Point::new(0.0, 0.0));
4239 assert!((screen_origin.x - 100.0).abs() < 1e-5);
4240 assert!((screen_origin.y - 0.0).abs() < 1e-5);
4241 let screen_corner = eff.apply_point(Point::new(50.0, 25.0));
4244 assert!((screen_corner.x - 200.0).abs() < 1e-5);
4245 assert!((screen_corner.y - 50.0).abs() < 1e-5);
4246 }
4247
4248 #[derive(Debug)]
4253 struct StubMenu;
4254 impl crate::widget::Widget for StubMenu {
4255 fn layout_response(
4256 &self,
4257 _proposal: SizeProposal,
4258 _ctx: &crate::widget::LayoutContext,
4259 ) -> crate::widget::LayoutResponse {
4260 teksilo_canvas::Size::new(100.0, 40.0).into()
4261 }
4262 }
4263
4264 #[test]
4265 fn context_menu_factory_receives_click_position() {
4266 use crate::event::{Modifiers, PointerButton};
4267 use std::cell::Cell;
4268 use std::rc::Rc;
4269
4270 let captured_position = Rc::new(Cell::new(None::<Point>));
4271 let cap = captured_position.clone();
4272 let mut tree = WidgetTree::new();
4273 let widget = tree.add(FillWidget::new().context_menu(move |pos, _ctx| {
4274 cap.set(Some(pos));
4275 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
4276 }));
4277 tree.layout(SizeProposal::exact(200.0, 100.0));
4278
4279 let click = Point::new(73.0, 42.0);
4280 tree.dispatch_event(WidgetEvent::PointerDown {
4281 position: click,
4282 button: PointerButton::Secondary,
4283 modifiers: Modifiers::NONE,
4284 });
4285
4286 let got = captured_position.get();
4287 assert_eq!(
4288 got,
4289 Some(click),
4290 "factory must receive the click position; got {:?}",
4291 got
4292 );
4293 let _ = widget;
4294 }
4295
4296 #[test]
4297 fn context_menu_factory_returning_none_falls_through_to_parent() {
4298 use crate::event::{Modifiers, PointerButton};
4299 use crate::test_widgets::StackWidget;
4300 use std::cell::Cell;
4301 use std::rc::Rc;
4302
4303 let outer_called = Rc::new(Cell::new(0_u32));
4307 let outer_flag = outer_called.clone();
4308 let mut tree = WidgetTree::new();
4309 let inner = tree.add(FillWidget::new().context_menu(|_pos, _ctx| None));
4310 let _outer = tree.add(StackWidget::new().add_child(inner).context_menu(
4311 move |_pos, _ctx| {
4312 outer_flag.set(outer_flag.get() + 1);
4313 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
4314 },
4315 ));
4316 tree.layout(SizeProposal::exact(200.0, 100.0));
4317
4318 tree.dispatch_event(WidgetEvent::PointerDown {
4319 position: Point::new(50.0, 25.0),
4320 button: PointerButton::Secondary,
4321 modifiers: Modifiers::NONE,
4322 });
4323
4324 assert_eq!(
4325 outer_called.get(),
4326 1,
4327 "inner returning None must fall through to the outer factory"
4328 );
4329 }
4330
4331 #[test]
4332 fn context_menu_factory_none_throughout_chain_does_not_show_overlay() {
4333 use crate::event::{Modifiers, PointerButton};
4334
4335 let mut tree = WidgetTree::new();
4337 tree.add(FillWidget::new().context_menu(|_pos, _ctx| None));
4338 tree.layout(SizeProposal::exact(200.0, 100.0));
4339
4340 let overlay_count_before = tree.overlay_manager.len();
4341 tree.dispatch_event(WidgetEvent::PointerDown {
4342 position: Point::new(50.0, 25.0),
4343 button: PointerButton::Secondary,
4344 modifiers: Modifiers::NONE,
4345 });
4346 let overlay_count_after = tree.overlay_manager.len();
4347 assert_eq!(
4348 overlay_count_before, overlay_count_after,
4349 "a factory returning None must not mount any overlay"
4350 );
4351 }
4352
4353 #[derive(Debug)]
4364 struct FreshChildHost {
4365 preserve: bool,
4366 }
4367 impl Widget for FreshChildHost {
4368 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
4369 vec![ctx.add(FillWidget::new())]
4370 }
4371 fn layout_response(
4372 &self,
4373 p: SizeProposal,
4374 _c: &LayoutContext,
4375 ) -> crate::widget::LayoutResponse {
4376 p.resolve(10.0, 10.0).into()
4377 }
4378 fn preserves_children_on_rebuild(&self) -> bool {
4379 self.preserve
4380 }
4381 }
4382
4383 #[test]
4384 fn reconcile_reaps_dropped_children_no_leak() {
4385 for preserve in [false, true] {
4390 let mut tree = WidgetTree::new();
4391 let host = tree.add(FreshChildHost { preserve });
4392 tree.layout(SizeProposal::exact(100.0, 100.0));
4393 let total0 = tree.arena.len();
4394 let active0 = tree.active_widget_count();
4395 for _ in 0..5 {
4396 tree.arena_mark_needs_rebuild_for_testing(host);
4397 tree.layout(SizeProposal::exact(100.0, 100.0));
4398 }
4399 assert_eq!(
4400 tree.arena.len(),
4401 total0,
4402 "preserve={preserve}: dropped children must be reaped, not leaked"
4403 );
4404 assert_eq!(
4405 tree.active_widget_count(),
4406 active0,
4407 "preserve={preserve}: no stranded still-active orphans"
4408 );
4409 }
4410 }
4411
4412 #[derive(Debug)]
4418 struct DetachedContentHost {
4419 preserve: bool,
4420 }
4421 impl Widget for DetachedContentHost {
4422 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
4423 let popup = ctx.add_detached(FillWidget::new());
4424 ctx.set_dormant(popup);
4425 vec![ctx.add(FillWidget::new())]
4426 }
4427 fn layout_response(
4428 &self,
4429 p: SizeProposal,
4430 _c: &LayoutContext,
4431 ) -> crate::widget::LayoutResponse {
4432 p.resolve(10.0, 10.0).into()
4433 }
4434 fn preserves_children_on_rebuild(&self) -> bool {
4435 self.preserve
4436 }
4437 }
4438
4439 #[test]
4440 fn rebuilding_reaps_detached_content_no_leak() {
4441 for preserve in [false, true] {
4447 let mut tree = WidgetTree::new();
4448 let host = tree.add(DetachedContentHost { preserve });
4449 tree.layout(SizeProposal::exact(100.0, 100.0));
4450 let total0 = tree.arena.len();
4451 for _ in 0..5 {
4452 tree.arena_mark_needs_rebuild_for_testing(host);
4453 tree.layout(SizeProposal::exact(100.0, 100.0));
4454 }
4455 assert_eq!(
4456 tree.arena.len(),
4457 total0,
4458 "preserve={preserve}: the previous build's detached content must be reaped"
4459 );
4460 }
4461 }
4462
4463 #[test]
4464 fn destroying_a_host_reaps_its_detached_content() {
4465 let mut tree = WidgetTree::new();
4466 let outer = tree.add(FillWidget::new());
4467 tree.layout(SizeProposal::exact(100.0, 100.0));
4468 let empty = tree.arena.len();
4469
4470 let host = tree.add_child(outer, DetachedContentHost { preserve: false });
4471 tree.layout(SizeProposal::exact(100.0, 100.0));
4472 assert!(tree.arena.len() > empty);
4473
4474 tree.destroy_subtree(host);
4475 assert_eq!(
4476 tree.arena.len(),
4477 empty,
4478 "the popup must die with the widget that built it"
4479 );
4480 }
4481
4482 #[derive(Debug)]
4484 struct StableChildHost {
4485 child: Option<WidgetId>,
4486 probe: std::rc::Rc<std::cell::Cell<Option<WidgetId>>>,
4487 }
4488 impl Widget for StableChildHost {
4489 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
4490 let id = match self.child {
4491 Some(id) => id,
4492 None => {
4493 let id = ctx.add(FillWidget::new());
4494 self.child = Some(id);
4495 self.probe.set(Some(id));
4496 id
4497 }
4498 };
4499 vec![id]
4500 }
4501 fn layout_response(
4502 &self,
4503 p: SizeProposal,
4504 _c: &LayoutContext,
4505 ) -> crate::widget::LayoutResponse {
4506 p.resolve(10.0, 10.0).into()
4507 }
4508 fn preserves_children_on_rebuild(&self) -> bool {
4509 true
4510 }
4511 }
4512
4513 #[test]
4514 fn reconcile_preserves_reattached_child() {
4515 let probe = std::rc::Rc::new(std::cell::Cell::new(None));
4516 let mut tree = WidgetTree::new();
4517 let host = tree.add(StableChildHost {
4518 child: None,
4519 probe: probe.clone(),
4520 });
4521 tree.layout(SizeProposal::exact(100.0, 100.0));
4522 let child = probe.get().expect("child mounted");
4523 let total0 = tree.arena.len();
4524 for _ in 0..5 {
4525 tree.arena_mark_needs_rebuild_for_testing(host);
4526 tree.layout(SizeProposal::exact(100.0, 100.0));
4527 }
4528 assert!(
4529 tree.arena.is_active(child),
4530 "the re-attached child must survive every rebuild"
4531 );
4532 assert_eq!(tree.arena.len(), total0, "no growth — same child reused");
4533 }
4534
4535 #[derive(Debug)]
4537 struct Wrapper {
4538 child: WidgetId,
4539 }
4540 impl Widget for Wrapper {
4541 fn build(&mut self, _ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
4542 vec![self.child]
4543 }
4544 fn layout_response(
4545 &self,
4546 p: SizeProposal,
4547 _c: &LayoutContext,
4548 ) -> crate::widget::LayoutResponse {
4549 p.resolve(10.0, 10.0).into()
4550 }
4551 }
4552
4553 #[derive(Debug)]
4557 struct ReparentHost {
4558 body: Option<WidgetId>,
4559 probe: std::rc::Rc<std::cell::Cell<Option<WidgetId>>>,
4560 }
4561 impl Widget for ReparentHost {
4562 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
4563 let body = match self.body {
4564 Some(id) => id,
4565 None => {
4566 let id = ctx.add(FillWidget::new());
4567 self.body = Some(id);
4568 self.probe.set(Some(id));
4569 id
4570 }
4571 };
4572 vec![ctx.add(Wrapper { child: body })]
4573 }
4574 fn layout_response(
4575 &self,
4576 p: SizeProposal,
4577 _c: &LayoutContext,
4578 ) -> crate::widget::LayoutResponse {
4579 p.resolve(10.0, 10.0).into()
4580 }
4581 fn preserves_children_on_rebuild(&self) -> bool {
4582 true
4583 }
4584 }
4585
4586 #[test]
4587 fn reconcile_spares_reparented_survivor() {
4588 let probe = std::rc::Rc::new(std::cell::Cell::new(None));
4595 let mut tree = WidgetTree::new();
4596 let host = tree.add(ReparentHost {
4597 body: None,
4598 probe: probe.clone(),
4599 });
4600 tree.layout(SizeProposal::exact(100.0, 100.0));
4601 let body = probe.get().expect("body mounted");
4602 let total0 = tree.arena.len();
4603 for _ in 0..5 {
4604 tree.arena_mark_needs_rebuild_for_testing(host);
4605 tree.layout(SizeProposal::exact(100.0, 100.0));
4606 }
4607 assert!(
4608 tree.arena.is_active(body),
4609 "the re-parented body must survive — it was moved into the new tree, \
4610 not swept with the dropped wrapper"
4611 );
4612 assert_eq!(
4613 tree.arena.len(),
4614 total0,
4615 "dropped wrappers reaped — no per-rebuild leak"
4616 );
4617 }
4618
4619 #[derive(Debug)]
4629 struct BelowContainer {
4630 child: Option<WidgetId>,
4631 offset: f32,
4632 }
4633
4634 impl crate::widget::Widget for BelowContainer {
4635 fn layout_response(
4636 &self,
4637 proposal: SizeProposal,
4638 _ctx: &crate::widget::LayoutContext,
4639 ) -> crate::widget::LayoutResponse {
4640 proposal.resolve(0.0, 0.0).into()
4641 }
4642 fn place_children(
4643 &self,
4644 bounds: Rect,
4645 _proposal: SizeProposal,
4646 children: &mut [crate::widget::WidgetPlacement],
4647 _ctx: &crate::widget::LayoutContext,
4648 ) {
4649 for c in children.iter_mut() {
4650 c.origin = Point::new(bounds.x, bounds.y + self.offset);
4651 c.size = bounds.size();
4652 }
4653 }
4654 fn children(&self) -> Vec<WidgetId> {
4655 self.child.into_iter().collect()
4656 }
4657 }
4658
4659 fn recording_scroll_container(
4662 tree: &mut WidgetTree,
4663 child: WidgetId,
4664 recorded: std::rc::Rc<std::cell::Cell<Option<Rect>>>,
4665 ) -> WidgetId {
4666 use crate::test_widgets::StackWidget;
4667 tree.add(
4668 StackWidget::new()
4669 .add_child(child)
4670 .on_scroll(move |ev, _ctx| match ev {
4671 WidgetEvent::ScrollIntoView { target_bounds, .. } => {
4672 recorded.set(Some(*target_bounds));
4673 EventResponse::Handled
4674 }
4675 _ => EventResponse::Ignored,
4676 })
4677 .clips_children(true),
4678 )
4679 }
4680
4681 #[test]
4682 fn ensure_visible_dispatches_scroll_into_view_to_clipping_ancestor() {
4683 use std::cell::Cell;
4684 use std::rc::Rc;
4685 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
4686 let mut tree = WidgetTree::new();
4687 let actor = tree.add(FillWidget::new());
4688 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
4689 tree.layout(SizeProposal::exact(100.0, 100.0));
4690
4691 let target = Rect::new(10.0, 500.0, 20.0, 15.0);
4694 let mut ctx = EventContext::new();
4695 ctx.ensure_visible(target);
4696 tree.collect_from_ctx(ctx, actor);
4697
4698 assert_eq!(
4699 recorded.get(),
4700 Some(target),
4701 "ensure_visible(rect) must dispatch ScrollIntoView with the exact rect \
4702 to the clips_children ancestor"
4703 );
4704 }
4705
4706 #[test]
4707 fn ensure_visible_is_noop_when_rect_already_visible() {
4708 use std::cell::Cell;
4709 use std::rc::Rc;
4710 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
4711 let mut tree = WidgetTree::new();
4712 let actor = tree.add(FillWidget::new());
4713 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
4714 tree.layout(SizeProposal::exact(100.0, 100.0));
4715
4716 let mut ctx = EventContext::new();
4719 ctx.ensure_visible(Rect::new(10.0, 10.0, 20.0, 15.0));
4720 tree.collect_from_ctx(ctx, actor);
4721
4722 assert_eq!(
4723 recorded.get(),
4724 None,
4725 "a rect already inside the viewport must not trigger a scroll"
4726 );
4727 }
4728
4729 #[test]
4730 fn ensure_visible_margin_forces_scroll_near_edge() {
4731 use std::cell::Cell;
4732 use std::rc::Rc;
4733 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
4734 let mut tree = WidgetTree::new();
4735 let actor = tree.add(FillWidget::new());
4736 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
4737 tree.layout(SizeProposal::exact(100.0, 100.0));
4738
4739 let rect = Rect::new(10.0, 95.0, 20.0, 4.0);
4742 let mut ctx = EventContext::new();
4743 ctx.ensure_visible_with_margin(rect, 10.0);
4744 tree.collect_from_ctx(ctx, actor);
4745
4746 assert_eq!(
4747 recorded.get(),
4748 Some(rect),
4749 "the margin must widen the visibility test so a near-edge rect scrolls"
4750 );
4751 }
4752
4753 fn recording_align_container(
4756 tree: &mut WidgetTree,
4757 child: WidgetId,
4758 recorded: std::rc::Rc<
4759 std::cell::Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>,
4760 >,
4761 ) -> WidgetId {
4762 use crate::test_widgets::StackWidget;
4763 tree.add(
4764 StackWidget::new()
4765 .add_child(child)
4766 .on_scroll(move |ev, _ctx| match ev {
4767 WidgetEvent::ScrollIntoView { align, motion, .. } => {
4768 recorded.set(Some((*align, *motion)));
4769 EventResponse::Handled
4770 }
4771 _ => EventResponse::Ignored,
4772 })
4773 .clips_children(true),
4774 )
4775 }
4776
4777 #[test]
4778 fn ensure_visible_aligned_scrolls_even_when_already_visible() {
4779 use std::cell::Cell;
4780 use std::rc::Rc;
4781 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
4782 let mut tree = WidgetTree::new();
4783 let actor = tree.add(FillWidget::new());
4784 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
4785 tree.layout(SizeProposal::exact(100.0, 100.0));
4786
4787 let target = Rect::new(10.0, 10.0, 20.0, 15.0);
4792 let mut ctx = EventContext::new();
4793 ctx.ensure_visible_aligned(target, 0.5, crate::event::ScrollMotion::Instant);
4794 tree.collect_from_ctx(ctx, actor);
4795
4796 assert_eq!(
4797 recorded.get(),
4798 Some(target),
4799 "an aligned reveal must dispatch even when the rect is already visible"
4800 );
4801 }
4802
4803 #[test]
4804 fn ensure_visible_aligned_forwards_fraction_and_motion() {
4805 use std::cell::Cell;
4806 use std::rc::Rc;
4807 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
4808 Rc::new(Cell::new(None));
4809 let mut tree = WidgetTree::new();
4810 let actor = tree.add(FillWidget::new());
4811 let _container = recording_align_container(&mut tree, actor, recorded.clone());
4812 tree.layout(SizeProposal::exact(100.0, 100.0));
4813
4814 let mut ctx = EventContext::new();
4815 ctx.ensure_visible_aligned(
4816 Rect::new(10.0, 10.0, 20.0, 15.0),
4817 0.25,
4818 crate::event::ScrollMotion::Smooth,
4819 );
4820 tree.collect_from_ctx(ctx, actor);
4821
4822 assert_eq!(
4823 recorded.get(),
4824 Some((
4825 crate::event::ScrollAlign::Fraction(0.25),
4826 crate::event::ScrollMotion::Smooth
4827 )),
4828 "the container must receive the requested fraction and motion verbatim"
4829 );
4830 }
4831
4832 #[test]
4833 fn ensure_visible_aligned_clamps_the_fraction() {
4834 use std::cell::Cell;
4835 use std::rc::Rc;
4836 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
4837 Rc::new(Cell::new(None));
4838 let mut tree = WidgetTree::new();
4839 let actor = tree.add(FillWidget::new());
4840 let _container = recording_align_container(&mut tree, actor, recorded.clone());
4841 tree.layout(SizeProposal::exact(100.0, 100.0));
4842
4843 let mut ctx = EventContext::new();
4844 ctx.ensure_visible_aligned(
4845 Rect::new(10.0, 10.0, 20.0, 15.0),
4846 4.2,
4847 crate::event::ScrollMotion::Instant,
4848 );
4849 tree.collect_from_ctx(ctx, actor);
4850
4851 assert_eq!(
4852 recorded.get().map(|(a, _)| a),
4853 Some(crate::event::ScrollAlign::Fraction(1.0)),
4854 "an out-of-range fraction must clamp rather than aim the pin off-screen"
4855 );
4856 }
4857
4858 #[test]
4859 fn plain_ensure_visible_requests_minimal_alignment() {
4860 use std::cell::Cell;
4861 use std::rc::Rc;
4862 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
4863 Rc::new(Cell::new(None));
4864 let mut tree = WidgetTree::new();
4865 let actor = tree.add(FillWidget::new());
4866 let _container = recording_align_container(&mut tree, actor, recorded.clone());
4867 tree.layout(SizeProposal::exact(100.0, 100.0));
4868
4869 let mut ctx = EventContext::new();
4870 ctx.ensure_visible(Rect::new(10.0, 500.0, 20.0, 15.0));
4871 tree.collect_from_ctx(ctx, actor);
4872
4873 assert_eq!(
4874 recorded.get(),
4875 Some((
4876 crate::event::ScrollAlign::Minimal,
4877 crate::event::ScrollMotion::Instant
4878 )),
4879 "the pre-existing reveal API must keep its exact semantics"
4880 );
4881 }
4882
4883 #[test]
4884 fn only_the_innermost_container_aligns() {
4885 use std::cell::Cell;
4886 use std::rc::Rc;
4887 let inner_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
4888 Rc::new(Cell::new(None));
4889 let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
4890 Rc::new(Cell::new(None));
4891
4892 let mut tree = WidgetTree::new();
4893 let actor = tree.add(FillWidget::new());
4894 let inner = recording_align_container(&mut tree, actor, inner_rec.clone());
4895 let _outer = recording_align_container(&mut tree, inner, outer_rec.clone());
4896 tree.layout(SizeProposal::exact(100.0, 100.0));
4897
4898 let mut ctx = EventContext::new();
4901 ctx.ensure_visible_aligned(
4902 Rect::new(10.0, 500.0, 20.0, 15.0),
4903 0.5,
4904 crate::event::ScrollMotion::Instant,
4905 );
4906 tree.collect_from_ctx(ctx, actor);
4907
4908 assert_eq!(
4909 inner_rec.get().map(|(a, _)| a),
4910 Some(crate::event::ScrollAlign::Fraction(0.5)),
4911 "the innermost clipping ancestor owns the pin"
4912 );
4913 assert_eq!(
4914 outer_rec.get().map(|(a, _)| a),
4915 Some(crate::event::ScrollAlign::Minimal),
4916 "an outer container must only bring the inner viewport into view — a \
4917 fraction names a height in one viewport, not in every ancestor's"
4918 );
4919 }
4920
4921 #[test]
4922 fn ensure_widget_visible_uses_target_arena_bounds() {
4923 use std::cell::Cell;
4924 use std::rc::Rc;
4925 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
4926 let mut tree = WidgetTree::new();
4927 let target = tree.add(FillWidget::new());
4929 let rec = recorded.clone();
4930 let container = tree.add(
4931 BelowContainer {
4932 child: Some(target),
4933 offset: 500.0,
4934 }
4935 .on_scroll(move |ev, _ctx| match ev {
4936 WidgetEvent::ScrollIntoView { target_bounds, .. } => {
4937 rec.set(Some(*target_bounds));
4938 EventResponse::Handled
4939 }
4940 _ => EventResponse::Ignored,
4941 })
4942 .clips_children(true),
4943 );
4944 tree.layout(SizeProposal::exact(100.0, 100.0));
4945
4946 let expected = tree.bounds(target);
4947 assert!(
4948 expected.y > 100.0,
4949 "fixture sanity: the target must sit below the viewport (y={})",
4950 expected.y
4951 );
4952
4953 let mut ctx = EventContext::new();
4956 ctx.ensure_widget_visible(target);
4957 tree.collect_from_ctx(ctx, container);
4958
4959 assert_eq!(
4960 recorded.get(),
4961 Some(expected),
4962 "ensure_widget_visible(id) must dispatch ScrollIntoView with the \
4963 target's current arena bounds"
4964 );
4965 }
4966
4967 #[test]
4968 fn ensure_widget_visible_ignores_missing_widget() {
4969 use std::cell::Cell;
4971 use std::rc::Rc;
4972 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
4973 let mut tree = WidgetTree::new();
4974 let actor = tree.add(FillWidget::new());
4975 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
4976 tree.layout(SizeProposal::exact(100.0, 100.0));
4977
4978 let mut ctx = EventContext::new();
4979 ctx.ensure_widget_visible(WidgetId::default());
4980 tree.collect_from_ctx(ctx, actor); assert_eq!(
4983 recorded.get(),
4984 None,
4985 "an unmounted id must not trigger a scroll"
4986 );
4987 }
4988
4989 #[test]
4990 fn context_menu_inside_a_modal_keeps_the_modal() {
4991 use crate::event::{Modifiers, PointerButton, WidgetEvent};
4995 use crate::overlay::{DismissBehavior, OverlayLayer, OverlayPlacement, OverlayRequest};
4996 use crate::test_widgets::{FillWidget, StackWidget};
4997
4998 let mut tree = WidgetTree::new();
4999 let modal_content = tree.add(StackWidget::new());
5002 let _editor = tree.add_child(
5003 modal_content,
5004 FillWidget::new()
5005 .context_menu(|_pos, _ctx| Some(Box::new(FillWidget::new()) as Box<dyn Widget>)),
5006 );
5007 tree.layout(SizeProposal::exact(200.0, 100.0));
5008
5009 let modal = tree.overlay_manager.show(OverlayRequest {
5010 content_id: modal_content,
5011 anchor: modal_content,
5012 placement: OverlayPlacement::Centered,
5013 dismiss: DismissBehavior::EscapeKey,
5014 layer: OverlayLayer::InTree,
5015 parent_overlay: None,
5016 on_dismiss: None,
5017 fade_duration: None,
5018 });
5019 tree.overlay_manager
5021 .stack
5022 .iter_mut()
5023 .find(|o| o.id == modal)
5024 .unwrap()
5025 .bounds = Rect::new(0.0, 0.0, 200.0, 100.0);
5026 assert_eq!(tree.overlay_manager.len(), 1);
5027
5028 tree.dispatch_event(WidgetEvent::PointerDown {
5030 position: Point::new(50.0, 25.0),
5031 button: PointerButton::Secondary,
5032 modifiers: Modifiers::NONE,
5033 });
5034
5035 assert!(
5036 tree.overlay_manager.active_ids().contains(&modal),
5037 "the modal must survive opening a context menu inside it"
5038 );
5039 assert_eq!(
5040 tree.overlay_manager.len(),
5041 2,
5042 "the context menu should now be open on top of the surviving modal"
5043 );
5044 }
5045}