1use super::*;
9
10use crate::event::ButtonMask;
11use crate::gesture::{GestureEvent, RawPointerEvent, TapEvent};
12
13fn fire_event_handler_both(
17 external: &mut Option<Box<dyn FnMut(&WidgetEvent, &mut EventContext) -> EventResponse>>,
18 own: &mut Option<Box<dyn FnMut(&WidgetEvent, &mut EventContext) -> EventResponse>>,
19 event: &WidgetEvent,
20 ctx: &mut EventContext,
21) -> EventResponse {
22 let r1 = external
23 .as_mut()
24 .map(|h| h(event, ctx))
25 .unwrap_or(EventResponse::Ignored);
26 let r2 = own
27 .as_mut()
28 .map(|h| h(event, ctx))
29 .unwrap_or(EventResponse::Ignored);
30 if r1 == EventResponse::Handled || r2 == EventResponse::Handled {
31 EventResponse::Handled
32 } else {
33 EventResponse::Ignored
34 }
35}
36
37pub(super) enum QueuedDispatch {
44 Event {
51 event: WidgetEvent,
52 snapshot: Box<crate::pointer::InputSnapshot>,
53 },
54 Cancel {
65 pointer: crate::pointer::PointerId,
66 reason: crate::pointer::CancelReason,
67 recipient: Option<WidgetId>,
75 },
76}
77
78fn pointer_event_position(event: &WidgetEvent) -> Option<Point> {
84 match event {
85 WidgetEvent::PointerDown { position, .. }
86 | WidgetEvent::PointerUp { position, .. }
87 | WidgetEvent::PointerMove { position, .. } => Some(*position),
88 _ => None,
92 }
93}
94
95#[derive(Copy, Clone)]
100struct BubbleGates {
101 fire_on_pointer_event: bool,
109 arena_blocked: bool,
114}
115
116impl WidgetTree {
117 fn scope_distance_from_focus(&self, focus: WidgetId, scope_id: WidgetId) -> Option<usize> {
121 let mut hops = 0usize;
122 let mut current = Some(focus);
123 while let Some(c) = current {
124 if c == scope_id {
125 return Some(hops);
126 }
127 current = self.arena.parent(c);
128 hops += 1;
129 }
130 None
131 }
132
133 fn select_shortcut_for_focus(
143 &self,
144 candidates: &[(&'static str, crate::shortcut::ShortcutScope, bool)],
145 ) -> Option<(&'static str, crate::shortcut::ShortcutScope, bool)> {
146 use crate::shortcut::ShortcutScope;
147 let mut best: Option<(usize, (&'static str, ShortcutScope, bool))> = None;
148 for &(id, scope, propagate) in candidates {
149 let score = match scope {
154 ShortcutScope::Global => Some(0usize),
155 ShortcutScope::Scoped(scope_id) => self
156 .focused
157 .and_then(|f| self.scope_distance_from_focus(f, scope_id))
158 .map(|hops| usize::MAX - hops),
159 };
160 let Some(score) = score else { continue };
161 if best.as_ref().is_none_or(|(b, _)| score > *b) {
164 best = Some((score, (id, scope, propagate)));
165 }
166 }
167 best.map(|(_, c)| c)
168 }
169
170 pub fn dispatch_event_with_ops(
184 &mut self,
185 event: WidgetEvent,
186 ops: &mut dyn crate::window::WindowOps,
187 ) {
188 let mut snapshot = crate::pointer::InputSnapshot::from_event(&event);
191 if snapshot.pointer.time == crate::pointer::EventTime::ZERO {
196 snapshot.pointer.time = self.input_now();
197 }
198 self.dispatch_with_input_snapshot(event, snapshot, ops)
199 }
200
201 pub fn dispatch_event(&mut self, event: WidgetEvent) {
207 let mut noop = crate::window::NoopWindowOps;
208 self.dispatch_event_with_ops(event, &mut noop);
209 }
210
211 pub fn dispatch_pointer(&mut self, sample: crate::pointer::PointerSample) {
233 let mut noop = crate::window::NoopWindowOps;
234 self.dispatch_pointer_with_ops(sample, &mut noop);
235 }
236
237 pub fn dispatch_pointer_with_ops(
241 &mut self,
242 sample: crate::pointer::PointerSample,
243 ops: &mut dyn crate::window::WindowOps,
244 ) {
245 use crate::pointer::PointerPhase;
246
247 crate::trace_input!(
248 Samples,
249 "{:?} {:?} at {:?} buttons={:?} t={:?}",
250 sample.phase,
251 sample.pointer.id,
252 sample.position,
253 sample.pointer.buttons,
254 sample.pointer.time
255 );
256
257 let button = sample
261 .button
262 .unwrap_or(crate::event::PointerButton::Primary);
263 let event = match sample.phase {
264 PointerPhase::Down => WidgetEvent::PointerDown {
265 position: sample.position,
266 button,
267 modifiers: sample.modifiers,
268 pointer: sample.pointer,
269 },
270 PointerPhase::Move => WidgetEvent::PointerMove {
271 position: sample.position,
272 modifiers: sample.modifiers,
273 pointer: sample.pointer,
274 },
275 PointerPhase::Up => WidgetEvent::PointerUp {
276 position: sample.position,
277 button,
278 modifiers: sample.modifiers,
279 pointer: sample.pointer,
280 },
281 PointerPhase::Cancel => {
287 self.overlay_manager.abort_dismiss(sample.pointer.id);
292 self.cancel_pointer(
293 sample.pointer.id,
294 crate::pointer::CancelReason::Platform,
295 ops,
296 );
297 if !sample.pointer.kind.hovers() {
304 self.pointers.end(sample.pointer.id);
305 }
306 return;
307 }
308 };
309
310 if !self.pointers.would_admit(&sample.pointer) {
316 return;
317 }
318 let ends_pointer = matches!(sample.phase, PointerPhase::Up | PointerPhase::Cancel)
322 && !sample.pointer.kind.hovers();
323 let pointer_id = sample.pointer.id;
324
325 let snapshot = crate::pointer::InputSnapshot::from_pointer_sample(&sample);
326 self.dispatch_with_input_snapshot(event, snapshot, ops);
327
328 if ends_pointer {
329 self.pointers.end(pointer_id);
330 }
331 }
332
333 pub fn dispatch_scroll(&mut self, sample: crate::pointer::ScrollSample) {
341 let mut noop = crate::window::NoopWindowOps;
342 self.dispatch_scroll_with_ops(sample, &mut noop);
343 }
344
345 pub fn dispatch_scroll_with_ops(
348 &mut self,
349 sample: crate::pointer::ScrollSample,
350 ops: &mut dyn crate::window::WindowOps,
351 ) {
352 crate::trace_input!(
353 Samples,
354 "scroll {:?} {:?}/{:?} at {:?}",
355 sample.delta,
356 sample.phase,
357 sample.source,
358 sample.position
359 );
360
361 let event = WidgetEvent::Scroll {
362 delta: sample.delta,
363 modifiers: sample.modifiers,
364 window_position: sample.position,
365 phase: sample.phase,
366 pointer: sample.pointer,
367 };
368 let snapshot = crate::pointer::InputSnapshot::from_scroll_sample(&sample);
369 self.dispatch_with_input_snapshot(event, snapshot, ops);
370 }
371
372 pub fn pointer_left_window(&mut self, ops: &mut dyn crate::window::WindowOps) {
397 if self
401 .pointers
402 .hover_owner_id()
403 .and_then(|id| self.captured_by(id))
404 .is_some()
405 {
406 return;
407 }
408 let Some(hovered) = self.hovered_id() else {
409 return;
410 };
411 let leave = WidgetEvent::PointerLeave {
414 pointer: self.hover_transition_pointer(),
415 };
416 self.dispatch_to_widget(hovered, &leave, &mut *ops);
417 self.tooltip_pointer_leave(hovered, &mut *ops);
418 self.set_hovered(None);
419 self.update_hover_within_signals(Some(hovered), None);
420 }
421
422 pub(super) fn hover_transition_pointer(&self) -> crate::pointer::PointerInfo {
439 self.pointers
440 .hover_owner()
441 .map(|entry| entry.info)
442 .unwrap_or_else(|| crate::pointer::PointerInfo::mouse(self.input_now()))
443 }
444
445 fn dispatch_with_input_snapshot(
452 &mut self,
453 event: WidgetEvent,
454 snapshot: crate::pointer::InputSnapshot,
455 ops: &mut dyn crate::window::WindowOps,
456 ) {
457 self.pending_dispatch.push_back(QueuedDispatch::Event {
467 event,
468 snapshot: Box::new(snapshot),
469 });
470 if self.dispatch_depth > 0 {
471 return;
472 }
473 self.drain_pending_dispatch(&mut *ops);
474 }
475
476 pub(super) fn drain_pending_dispatch(&mut self, ops: &mut dyn crate::window::WindowOps) {
482 while let Some(queued) = self.pending_dispatch.pop_front() {
483 match queued {
484 QueuedDispatch::Event { event, snapshot } => {
485 self.run_one_dispatch(event, *snapshot, &mut *ops);
486 }
487 QueuedDispatch::Cancel {
488 pointer,
489 reason,
490 recipient,
491 } => {
492 self.run_one_cancel(pointer, reason, recipient, &mut *ops);
493 }
494 }
495 }
496 }
497
498 fn run_one_dispatch(
505 &mut self,
506 event: WidgetEvent,
507 snapshot: crate::pointer::InputSnapshot,
508 ops: &mut dyn crate::window::WindowOps,
509 ) {
510 let previous = std::mem::replace(&mut self.current_input, snapshot);
511 self.dispatch_depth += 1;
512 self.dispatch_event_impl(event, ops);
513 self.dispatch_depth -= 1;
514 self.current_input = previous;
515 }
516
517 fn dispatch_event_impl(&mut self, event: WidgetEvent, ops: &mut dyn crate::window::WindowOps) {
518 if let Some(position) = pointer_event_position(&event) {
524 let is_down = matches!(event, WidgetEvent::PointerDown { .. });
525 let is_move = matches!(event, WidgetEvent::PointerMove { .. });
526 if !self.admit_current_pointer(position, is_down, is_move) {
527 return;
528 }
529 if self.current_input.pointer.kind.hovers() {
533 self.claim_hover_owner_for_current(&mut *ops);
534 }
535 }
536
537 match &event {
541 WidgetEvent::KeyDown { .. } if !self.focus_visible.get() => {
542 self.focus_visible.set(true);
543 }
544 WidgetEvent::PointerDown { .. } if self.focus_visible.get() => {
545 self.focus_visible.set(false);
546 }
547 _ => {}
548 }
549
550 let overlay_back_key = match self.layout_direction {
556 crate::environment::LayoutDirection::RightToLeft => Key::ArrowRight,
557 crate::environment::LayoutDirection::LeftToRight => Key::ArrowLeft,
558 };
559 if let WidgetEvent::KeyDown { key, .. } = &event
560 && *key == overlay_back_key
561 {
562 let nested_menu_overlays = {
592 let ids: Vec<_> = self
593 .overlay_manager
594 .stack
595 .iter()
596 .filter(|o| o.band == crate::overlay::OverlayBand::Standard && !o.inert)
597 .map(|o| o.id)
598 .collect();
599 ids.into_iter()
600 .filter(|&id| !self.overlay_is_host_surface(id))
601 .count()
602 };
603 let top_id = self
610 .overlay_manager
611 .stack
612 .iter()
613 .rev()
614 .find(|o| !o.inert)
615 .map(|o| o.id);
616 let top_is_back_navigable = top_id.is_some_and(|id| !self.overlay_is_host_surface(id));
617 if nested_menu_overlays > 1
618 && top_is_back_navigable
619 && let Some(top_id) = top_id
620 {
621 let (content_ids, focus_restore) =
622 self.overlay_manager.dismiss_with_focus_restore_because(
623 top_id,
624 crate::overlay::DismissReason::Escape,
625 );
626 self.dormant_dismissed_content(&content_ids, &mut *ops);
627 if let Some(restore_id) = focus_restore
628 && self.arena.is_active(restore_id)
629 {
630 self.focus_ops(restore_id, &mut *ops);
631 }
632 return;
633 }
634 }
635
636 if let WidgetEvent::KeyDown {
642 key: Key::Escape, ..
643 } = &event
644 {
645 self.tooltip_escape_pressed();
646 self.inert_overlays_escape_pressed(&mut *ops);
649 }
650
651 if let WidgetEvent::KeyDown {
652 key: Key::Escape, ..
653 } = &event
654 && !self.overlay_manager.is_empty()
655 && let Some((_id, content_ids, focus_restore)) =
656 self.overlay_manager.try_dismiss_top_on_escape()
657 {
658 self.dormant_dismissed_content(&content_ids, &mut *ops);
659 if let Some(restore_id) = focus_restore
660 && self.arena.is_active(restore_id)
661 {
662 self.focus_ops(restore_id, &mut *ops);
663 }
664 return;
665 }
666
667 match &event {
672 WidgetEvent::PointerDown {
673 position, button, ..
674 } => {
675 if self.arm_outside_press_dismissal(*position, *button, &mut *ops) {
676 return;
677 }
678 }
679 WidgetEvent::PointerUp { position, .. } => {
680 if self.commit_outside_press_dismissal(*position, &mut *ops) {
681 return;
682 }
683 }
684 WidgetEvent::PointerCancel { pointer, .. } => {
685 self.overlay_manager.abort_dismiss(pointer.id);
689 }
690 _ => {}
691 }
692
693 if let WidgetEvent::KeyDown { key, modifiers, .. } = &event
704 && let Some(callback) = self.take_key_capture()
705 {
706 let keystroke = crate::shortcut::KeyStroke::new(*key, *modifiers);
707 let mut cap_ctx = self.make_event_context(&mut *ops);
708 callback(keystroke, self.shortcut_registry_mut(), &mut cap_ctx);
709 let anchor = self.focused.or_else(|| self.arena.roots().first().copied());
712 if let Some(anchor_id) = anchor {
713 self.collect_from_ctx(cap_ctx, anchor_id);
714 self.drain_pending_intents(&mut *ops);
715 }
716 return;
717 }
718
719 let focus_captures_keys = matches!(&event, WidgetEvent::KeyDown { .. })
727 && self.focused.is_some_and(|f| self.is_keyboard_capture(f));
728
729 if !focus_captures_keys && let WidgetEvent::KeyDown { key, modifiers, .. } = &event {
744 let keystroke = crate::shortcut::KeyStroke::new(*key, *modifiers);
745 let candidates: Vec<(&'static str, crate::shortcut::ShortcutScope, bool)> = self
754 .shortcut_registry
755 .matches_by_keystroke(keystroke)
756 .map(|eff| {
757 (
758 eff.shortcut.id,
759 eff.shortcut.scope,
760 eff.shortcut.propagate_when_disabled,
761 )
762 })
763 .collect();
764 let lookup = self.select_shortcut_for_focus(&candidates);
765 if let Some((id, scope, propagate_when_disabled)) = lookup {
766 let anchor = match scope {
767 crate::shortcut::ShortcutScope::Global => {
772 self.focused.or_else(|| self.arena.roots().first().copied())
773 }
774 crate::shortcut::ShortcutScope::Scoped(scope_id) => {
775 self.focused.filter(|f| self.is_descendant_of(*f, scope_id))
776 }
777 };
778 if let Some(anchor_id) = anchor {
779 let mut act_ctx = self.make_event_context(&mut *ops);
780 if let Some(intent) =
781 self.shortcut_registry
782 .invoke_on_activate(id, keystroke, &mut act_ctx)
783 {
784 self.collect_from_ctx(act_ctx, anchor_id);
785 self.drain_pending_intents(&mut *ops);
789 let intent = intent.with_source(crate::telemetry::IntentSource::Shortcut);
793 let consumed = self.dispatch_intent(
794 anchor_id,
795 intent,
796 propagate_when_disabled,
797 &mut *ops,
798 );
799 self.drain_pending_intents(&mut *ops);
800 if consumed {
801 return;
802 }
803 }
810 }
811 }
816 }
819
820 if self.outbound_drag_source.is_some()
826 && let WidgetEvent::KeyDown {
827 key: Key::Escape, ..
828 } = &event
829 {
830 ops.cancel_os_drag();
831 }
836
837 if self.active_drag.is_some() {
839 match &event {
840 WidgetEvent::PointerMove { position, .. } => {
841 self.handle_drag_move(*position, &mut *ops);
842 return;
843 }
844 WidgetEvent::PointerUp { position, .. } => {
845 self.handle_drag_drop(*position, &mut *ops);
846 return;
847 }
848 WidgetEvent::KeyDown {
849 key: Key::Escape, ..
850 } => {
851 self.cancel_active_drag(&mut *ops);
852 return;
853 }
854 WidgetEvent::Scroll { .. } => {
855 let target_and_pos = self
861 .active_drag
862 .as_ref()
863 .and_then(|d| d.current_target.map(|t| (t, d.current_position)));
864 if let Some((target, _pos)) = target_and_pos {
865 self.dispatch_to_widget(target, &event, &mut *ops);
866 }
867 if let Some((_, pos)) = target_and_pos
868 && self.active_drag.is_some()
869 {
870 self.handle_drag_move(pos, &mut *ops);
871 }
872 return;
873 }
874 _ => {}
875 }
876 }
877
878 if let WidgetEvent::KeyDown { key, modifiers, .. } = &event
887 && is_context_menu_chord(*key, *modifiers)
888 && self.open_context_menu_from_keyboard(&mut *ops)
889 {
890 return;
891 }
892
893 match &event {
894 WidgetEvent::PointerMove { position, .. } => {
895 self.note_sequence_position(*position);
899 self.revalidate_sequence(&mut *ops);
900 self.tick_sequence_timers();
903 self.note_palm_sample(*position);
908 self.feed_pinch(super::pan_arbiter::PinchFeed::Move, *position, &mut *ops);
909 if let Some(captured) = self.current_pointer_capture() {
910 self.dispatch_to_widget(captured, &event, &mut *ops);
911 if self.active_drag.is_none() {
916 self.advance_sequence(&event, &mut *ops);
917 }
918 } else {
919 self.handle_pointer_move(&event, *position, &mut *ops);
920 if self.active_drag.is_none() {
925 self.advance_sequence(&event, &mut *ops);
926 }
927 }
928 self.advance_pan(*position, &mut *ops);
931 self.update_press(*position);
934 self.touch_route_moved(self.current_pointer_id());
937 if self.pointers.hover_owner_id() == Some(self.current_pointer_id()) {
947 self.update_pointer_leave_overlays(*position, &mut *ops);
948 }
949 }
950 WidgetEvent::PointerDown {
951 position, button, ..
952 } => {
953 let pressed = self.current_pointer_id();
956 self.cancelled_pointers.retain(|p| *p != pressed);
957 self.tooltip_pointer_press(Some(*position));
964 let pressing = self.current_input.pointer;
968 if let Some(target) = self.hit_test_for(*position, &pressing) {
969 if *button == PointerButton::Secondary
970 && self.show_context_menu_for(
971 target,
972 *position,
973 crate::widget_builder::ContextMenuTrigger::Pointer,
974 &mut *ops,
975 )
976 {
977 return;
978 }
979 self.begin_sequence(target, *position);
985 let modifiers = match &event {
990 WidgetEvent::PointerDown { modifiers, .. } => *modifiers,
991 _ => crate::event::Modifiers::NONE,
992 };
993 self.begin_pan(target, *position, modifiers);
994 self.begin_palm_watch(*position);
995 self.feed_pinch(super::pan_arbiter::PinchFeed::Down, *position, &mut *ops);
996 let focusable = self.find_focusable_at_or_above(target);
1000 self.begin_press(focusable);
1001 if !pressing.kind.is_direct()
1008 && let Some(focusable) = focusable
1009 {
1010 self.focus_with_origin_ops(
1011 focusable,
1012 crate::focus::FocusOrigin::Pointer(pressing.kind),
1013 &mut *ops,
1014 );
1015 }
1016 self.dispatch_to_widget(target, &event, &mut *ops);
1017 if self.active_drag.is_none() {
1022 self.enrol_sequence_members(&event, &mut *ops);
1023 }
1024 self.adopt_press_owner(*button);
1028 self.arm_touch_route(target, *position);
1033 }
1034 }
1035 WidgetEvent::PointerUp { position, .. } => {
1036 let released = self.current_pointer_id();
1042 self.cancel_touch_route(released);
1046 if let Some(index) = self.cancelled_pointers.iter().position(|p| *p == released) {
1047 self.cancelled_pointers.swap_remove(index);
1048 crate::trace_input!(
1049 Samples,
1050 "swallowing the Up for {released:?}: its press was cancelled"
1051 );
1052 return;
1053 }
1054 self.feed_pinch(super::pan_arbiter::PinchFeed::Up, *position, &mut *ops);
1061 if self.take_palm_verdict(released) {
1062 crate::trace_input!(
1063 Samples,
1064 "{released:?} released as a palm: large, and it never moved"
1065 );
1066 self.cancel_pointer(
1067 released,
1068 crate::pointer::CancelReason::PalmRejected,
1069 &mut *ops,
1070 );
1071 return;
1072 }
1073 self.end_pan(*position, &mut *ops);
1076 self.note_sequence_position(*position);
1083 self.end_sequence(&event, &mut *ops);
1084 self.focus_on_release(*position, &mut *ops);
1088 if let Some(captured) = self.current_pointer_capture() {
1089 self.dispatch_to_widget(captured, &event, &mut *ops);
1090 self.set_current_pointer_capture(None);
1093 } else {
1094 let releasing = self.current_input.pointer;
1095 if let Some(target) = self.hit_test_for(*position, &releasing) {
1096 self.dispatch_to_widget(target, &event, &mut *ops);
1097 }
1098 }
1099 let released = self.current_pointer_id();
1102 self.release_arenas_following(released);
1103 self.end_press(released);
1107 }
1108 WidgetEvent::PointerCancel {
1109 reason, pointer, ..
1110 } => {
1111 let (reason, pointer) = (*reason, pointer.id);
1116 self.cancel_pointer(pointer, reason, &mut *ops);
1117 }
1118 WidgetEvent::Scroll {
1119 window_position: position,
1120 ..
1121 } => {
1122 use super::pan_arbiter::ScrollDelivery;
1123 match ScrollDelivery::for_source(self.current_input.scroll_source) {
1124 ScrollDelivery::ClaimantChain => {
1129 let position = *position;
1130 self.route_scroll_along_chain(&event, position, &mut *ops);
1131 }
1132 ScrollDelivery::Bubble => {
1138 let scrolling = self.current_input.pointer;
1139 let target = match position {
1140 Some(p) => self.hit_test_for(*p, &scrolling),
1141 None => self.hovered_id().or(self.focused),
1142 };
1143 if let Some(target) = target {
1144 self.dispatch_to_widget(target, &event, &mut *ops);
1145 }
1146 }
1147 }
1148 }
1149 WidgetEvent::KeyDown { key, modifiers, .. } => {
1150 if *key == Key::Tab {
1151 let captured_focus = self
1166 .focused
1167 .is_some_and(|focused| self.is_keyboard_capture(focused));
1168 if captured_focus && modifiers.ctrl() {
1169 self.cycle_focus(modifiers.shift(), &mut *ops);
1170 return;
1171 }
1172 let handled = self
1179 .focused
1180 .map(|focused| {
1181 self.dispatch_to_widget_returning_handled(focused, &event, &mut *ops)
1182 })
1183 .unwrap_or(false);
1184 if !handled {
1185 self.cycle_focus(modifiers.shift(), &mut *ops);
1186 }
1187 } else if let Some(focused) = self.focused {
1188 self.dispatch_to_widget(focused, &event, &mut *ops);
1189 }
1190 }
1191 WidgetEvent::KeyUp { .. }
1192 | WidgetEvent::ImeComposition { .. }
1193 | WidgetEvent::ImeCommit { .. } => {
1194 if let Some(focused) = self.focused {
1195 self.dispatch_to_widget(focused, &event, &mut *ops);
1196 }
1197 }
1198 WidgetEvent::AccessAction { target, action, .. } => {
1199 if let Some(id) =
1217 target.filter(|id| self.arena.is_active(*id) && !self.is_behind_modal(*id))
1218 {
1219 if *action == accesskit::Action::Focus {
1220 if self.advertises_focus_action(id) {
1249 let target = self
1250 .focusable_composite_behind(id)
1251 .or_else(|| self.first_focusable_descendant(id))
1252 .unwrap_or(id);
1253 self.focus_with_origin_ops(
1259 target,
1260 crate::focus::FocusOrigin::Accessibility,
1261 &mut *ops,
1262 );
1263 self.access_action_handled = self.focused == Some(target);
1266 } else {
1267 self.access_action_handled = false;
1268 }
1269 } else if *action == accesskit::Action::ShowContextMenu {
1270 self.access_action_handled =
1286 if self.dispatch_to_widget_returning_handled(id, &event, &mut *ops) {
1287 true
1288 } else {
1289 let position = self.arena.bounds(id).center();
1290 self.show_context_menu_for(
1291 id,
1292 position,
1293 crate::widget_builder::ContextMenuTrigger::Accessibility,
1294 &mut *ops,
1295 )
1296 };
1297 } else {
1298 self.access_action_handled =
1299 self.dispatch_to_widget_returning_handled(id, &event, &mut *ops);
1300 }
1301 }
1302 }
1303 WidgetEvent::Gesture { .. } => {
1304 if let Some(target) = self.hovered_id().or(self.focused) {
1305 self.dispatch_to_widget(target, &event, &mut *ops);
1306 }
1307 }
1308 WidgetEvent::ScrollIntoView { .. }
1309 | WidgetEvent::PointerEnter { .. }
1310 | WidgetEvent::PointerLeave { .. }
1311 | WidgetEvent::FocusGained { .. }
1312 | WidgetEvent::FocusLost => {}
1313 }
1314 self.drain_pending_intents(&mut *ops);
1319 }
1320
1321 fn open_context_menu_from_keyboard(&mut self, ops: &mut dyn crate::window::WindowOps) -> bool {
1334 let Some(focused) = self.focused else {
1335 return false;
1336 };
1337 let target = self
1338 .arena
1339 .get(focused)
1340 .and_then(|node| node.widget.context_menu_key_target())
1341 .filter(|id| self.arena.is_active(*id))
1342 .unwrap_or(focused);
1343
1344 let bounds = self.bounds(target);
1348 let anchor = Point {
1349 x: bounds.x + bounds.width / 2.0,
1350 y: bounds.y + bounds.height / 2.0,
1351 };
1352 self.show_context_menu_for(
1353 target,
1354 anchor,
1355 crate::widget_builder::ContextMenuTrigger::Keyboard,
1356 ops,
1357 )
1358 }
1359
1360 pub(super) fn show_context_menu_for(
1369 &mut self,
1370 target: WidgetId,
1371 position: Point,
1372 trigger: crate::widget_builder::ContextMenuTrigger,
1373 ops: &mut dyn crate::window::WindowOps,
1374 ) -> bool {
1375 let mut ctx = self.make_event_context(&mut *ops);
1381 ctx.context_menu_trigger = Some(trigger);
1382 let mut walker = Some(target);
1383 let menu_decision: Option<(WidgetId, Box<dyn Widget>)> = loop {
1384 let owner_id = {
1387 let mut probe = walker;
1388 loop {
1389 match probe {
1390 None => break None,
1391 Some(id) => {
1392 if self
1393 .arena
1394 .get(id)
1395 .is_some_and(|node| node.context_menu_factory.is_some())
1396 {
1397 break Some(id);
1398 }
1399 probe = self.arena.get(id).and_then(|node| node.parent);
1400 }
1401 }
1402 }
1403 };
1404 let Some(owner_id) = owner_id else {
1405 break None;
1406 };
1407 let outcome: Option<Box<dyn Widget>> = {
1414 let node = self
1415 .arena
1416 .get(owner_id)
1417 .expect("owner_id from active arena walk");
1418 let factory = node
1419 .context_menu_factory
1420 .as_ref()
1421 .expect("owner_id only set when factory present");
1422 factory(position, &mut ctx)
1423 };
1424 match outcome {
1425 Some(menu) => break Some((owner_id, menu)),
1426 None => {
1427 walker = self.arena.get(owner_id).and_then(|n| n.parent);
1429 }
1430 }
1431 };
1432
1433 let drain_anchor = menu_decision
1437 .as_ref()
1438 .map(|(id, _)| *id)
1439 .or_else(|| self.arena.roots().first().copied())
1440 .unwrap_or(target);
1441 self.collect_from_ctx(ctx, drain_anchor);
1442
1443 let Some((owner_id, menu_widget)) = menu_decision else {
1444 return false;
1445 };
1446
1447 let keep: std::collections::HashSet<WidgetId> = self
1453 .overlay_manager
1454 .stack
1455 .iter()
1456 .map(|o| o.content_id)
1457 .filter(|&content_id| self.is_descendant_of(owner_id, content_id))
1458 .collect();
1459 let dismissed = self.overlay_manager.dismiss_except(&keep);
1460 self.dormant_dismissed_content(&dismissed, &mut *ops);
1461
1462 let content_id = self.add_boxed(menu_widget);
1463 let prev_focus = self.focused;
1464 let placement =
1471 crate::overlay::OverlayPlacement::at_pointer_for(position, &self.current_input.pointer);
1472 self.overlay_manager
1473 .show(crate::overlay::OverlayRequest::new(
1474 content_id,
1475 owner_id,
1476 placement,
1477 crate::overlay::DismissBehavior::EscapeOrClickOutside,
1478 ));
1479 if let Some(focus_id) = prev_focus {
1480 self.overlay_manager.set_top_focus_restore(focus_id);
1481 }
1482 self.focus_ops(content_id, &mut *ops);
1483 self.drain_pending_intents(&mut *ops);
1488 true
1489 }
1490
1491 fn arm_outside_press_dismissal(
1512 &mut self,
1513 position: Point,
1514 button: PointerButton,
1515 ops: &mut dyn crate::window::WindowOps,
1516 ) -> bool {
1517 self.prune_stale_dismiss_arms();
1518 let pressing = self.current_input.pointer;
1519 let busy = self.busy_press_points(pressing.id);
1520
1521 if pressing.kind.is_direct() {
1522 return self
1523 .overlay_manager
1524 .arm_dismiss(pressing.id, position, &busy)
1525 .suppress_beneath;
1526 }
1527
1528 let (dismissed, focus_restore, toggle_anchors) =
1529 self.overlay_manager.dismiss_outside_press(position, &busy);
1530 if dismissed.is_empty() {
1531 return false;
1532 }
1533 self.dormant_dismissed_content(&dismissed, &mut *ops);
1534 if let Some(restore_id) = focus_restore
1535 && self.arena.is_active(restore_id)
1536 {
1537 self.focus_ops(restore_id, &mut *ops);
1538 }
1539 toggle_anchors.iter().any(|&anchor| {
1552 self.arena.is_active(anchor)
1553 && self.arena.bounds(anchor).contains(position)
1554 && (button == PointerButton::Primary
1555 || self
1556 .anchor_tap_buttons(anchor, position, &pressing)
1557 .contains(button))
1558 })
1559 }
1560
1561 fn anchor_tap_buttons(
1571 &self,
1572 anchor: WidgetId,
1573 position: Point,
1574 pointer: &crate::pointer::PointerInfo,
1575 ) -> ButtonMask {
1576 let mut mask = ButtonMask::NONE;
1577 let mut node = self.hit_test_for(position, pointer);
1578 while let Some(id) = node {
1579 mask = mask.union(self.tap_buttons(id));
1580 if id == anchor {
1581 return mask;
1582 }
1583 node = self.arena.parent(id);
1584 }
1585 ButtonMask::NONE
1586 }
1587
1588 fn commit_outside_press_dismissal(
1595 &mut self,
1596 position: Point,
1597 ops: &mut dyn crate::window::WindowOps,
1598 ) -> bool {
1599 let pointer = self.current_pointer_id();
1600 if !self.overlay_manager.has_armed_dismiss(pointer) {
1601 return false;
1602 }
1603 let (dismissed, focus_restore, _anchors) =
1604 self.overlay_manager.commit_dismiss(pointer, position);
1605 if !dismissed.is_empty() {
1606 self.dormant_dismissed_content(&dismissed, &mut *ops);
1607 if let Some(restore_id) = focus_restore
1608 && self.arena.is_active(restore_id)
1609 {
1610 self.focus_ops(restore_id, &mut *ops);
1611 }
1612 }
1613 true
1614 }
1615
1616 fn busy_press_points(&self, exclude: crate::pointer::PointerId) -> Vec<Point> {
1633 self.pointers
1634 .iter()
1635 .filter(|entry| entry.info.id != exclude)
1636 .filter(|entry| self.press_is_revocable(entry.info.id))
1637 .map(|entry| entry.down_position)
1638 .collect()
1639 }
1640
1641 fn prune_stale_dismiss_arms(&mut self) {
1648 let stale: Vec<crate::pointer::PointerId> = self
1649 .overlay_manager
1650 .armed_pointers()
1651 .into_iter()
1652 .filter(|id| self.pointers.get(*id).is_none())
1653 .collect();
1654 for id in stale {
1655 self.overlay_manager.abort_dismiss(id);
1656 }
1657 }
1658
1659 fn begin_press(&mut self, focusable: Option<WidgetId>) {
1680 let pointer = self.current_pointer_id();
1681 let now = self.sequence_now();
1682 let delay = self
1683 .pan_session_open(pointer)
1684 .then(|| self.current_profile().press_feedback_delay)
1685 .filter(|d| !d.is_zero());
1686 self.presses.press(pointer, focusable, now, delay);
1687 }
1688
1689 fn adopt_press_owner(&mut self, button: PointerButton) {
1712 let pointer = self.current_pointer_id();
1713 let Some(owner) = self.current_sequence().and_then(|s| s.pressed_owner()) else {
1714 return;
1715 };
1716 if !self.press_buttons(owner).contains(button) {
1717 crate::trace_input!(
1718 Samples,
1719 "no press visual for {owner:?}: {button:?} is not one of its accepted buttons"
1720 );
1721 return;
1722 }
1723 self.presses.set_owner(pointer, owner);
1724 self.publish_pressed(owner);
1725 }
1726
1727 fn update_press(&mut self, position: Point) {
1740 let pointer = self.current_pointer_id();
1741 if self.presses.get(pointer).is_none() {
1742 return;
1743 }
1744 let claimed_elsewhere = self.current_sequence().is_some_and(|sequence| {
1747 sequence
1748 .winner()
1749 .is_some_and(|winner| Some(winner) != sequence.pressed_owner())
1750 });
1751 if claimed_elsewhere {
1752 self.end_press(pointer);
1753 return;
1754 }
1755 let profile = self.current_profile();
1756 let origin = self.current_sequence().map(|s| s.press_origin());
1757 let owner = self.presses.get(pointer).and_then(|p| p.owner);
1758 let inside = match (origin, owner) {
1759 (Some(origin), Some(owner)) => {
1760 let bounds = self
1761 .arena
1762 .is_active(owner)
1763 .then(|| self.arena.bounds(owner));
1764 !crate::gesture::TapBoundary::for_pointer(&self.current_input.pointer, &profile)
1765 .left(origin, position, bounds, &profile)
1766 }
1767 _ => true,
1770 };
1771 let Some(press) = self.presses.get_mut(pointer) else {
1772 return;
1773 };
1774 if press.inside == inside {
1775 return;
1776 }
1777 press.inside = inside;
1778 if let Some(owner) = owner {
1779 self.publish_pressed(owner);
1780 }
1781 }
1782
1783 pub(crate) fn end_press(&mut self, pointer: crate::pointer::PointerId) {
1789 if let Some(owner) = self.presses.release(pointer) {
1790 self.publish_pressed(owner);
1791 }
1792 }
1793
1794 pub(crate) fn resolve_press_delays(&mut self, now: crate::pointer::EventTime) {
1797 if self.presses.is_empty() {
1798 return;
1799 }
1800 for id in self.presses.resolve_delays(now) {
1801 self.publish_pressed(id);
1802 self.arena.mark_needs_paint(id);
1803 }
1804 }
1805
1806 pub(crate) fn next_press_deadline(&self) -> Option<std::time::Instant> {
1809 self.presses.next_deadline().map(|t| self.instant_for(t))
1810 }
1811
1812 fn focus_on_release(&mut self, position: Point, ops: &mut dyn crate::window::WindowOps) {
1823 let releasing = self.current_input.pointer;
1824 if !releasing.kind.is_direct() {
1825 return;
1826 }
1827 let pointer = self.current_pointer_id();
1828 let Some(pressed) = self.presses.get(pointer).and_then(|p| p.focusable) else {
1829 return;
1830 };
1831 let released_on = self
1832 .hit_test_for(position, &releasing)
1833 .and_then(|target| self.find_focusable_at_or_above(target));
1834 if released_on == Some(pressed) {
1835 self.focus_with_origin_ops(
1836 pressed,
1837 crate::focus::FocusOrigin::Pointer(releasing.kind),
1838 &mut *ops,
1839 );
1840 }
1841 }
1842
1843 fn handle_pointer_move(
1863 &mut self,
1864 event: &WidgetEvent,
1865 position: Point,
1866 ops: &mut dyn crate::window::WindowOps,
1867 ) {
1868 let moving = self.current_input.pointer;
1873 if self.pointers.hover_owner_id() != Some(self.current_pointer_id()) {
1874 if let Some(target) = self.hit_test_for(position, &moving) {
1875 self.dispatch_to_widget(target, event, &mut *ops);
1876 }
1877 return;
1878 }
1879 let target = self.hit_test_for(position, &moving);
1880
1881 if target != self.hovered_id() {
1882 let hover = self.hover_transition_pointer();
1884 let previously_hovered = self.hovered_id();
1885 if let Some(old) = previously_hovered {
1886 self.dispatch_to_widget(
1887 old,
1888 &WidgetEvent::PointerLeave { pointer: hover },
1889 &mut *ops,
1890 );
1891 self.tooltip_pointer_leave(old, &mut *ops);
1892 }
1893 if let Some(new) = target {
1894 self.dispatch_to_widget(
1895 new,
1896 &WidgetEvent::PointerEnter { pointer: hover },
1897 &mut *ops,
1898 );
1899 self.tooltip_pointer_enter(new);
1900 }
1901 self.set_hovered(target);
1902 self.update_hover_within_signals(previously_hovered, target);
1903 } else if let Some(target) = target {
1904 self.tooltip_pointer_moved(target, position);
1907 }
1908
1909 if let Some(target) = target {
1910 self.dispatch_to_widget(target, event, &mut *ops);
1911 }
1912 }
1913
1914 pub(super) fn dispatch_to_widget(
1915 &mut self,
1916 target: WidgetId,
1917 event: &WidgetEvent,
1918 ops: &mut dyn crate::window::WindowOps,
1919 ) {
1920 self.dispatch_to_widget_returning_handled(target, event, ops);
1921 }
1922
1923 pub(super) fn localize_event(
1939 frame: &crate::arena::LocalFrame,
1940 event: &WidgetEvent,
1941 ) -> Option<WidgetEvent> {
1942 match event {
1943 WidgetEvent::PointerDown {
1944 position,
1945 button,
1946 modifiers,
1947 pointer,
1948 } => Some(WidgetEvent::PointerDown {
1949 position: frame.to_local(*position),
1950 button: *button,
1951 modifiers: *modifiers,
1952 pointer: *pointer,
1953 }),
1954 WidgetEvent::PointerUp {
1955 position,
1956 button,
1957 modifiers,
1958 pointer,
1959 } => Some(WidgetEvent::PointerUp {
1960 position: frame.to_local(*position),
1961 button: *button,
1962 modifiers: *modifiers,
1963 pointer: *pointer,
1964 }),
1965 WidgetEvent::PointerMove {
1966 position,
1967 modifiers,
1968 pointer,
1969 } => Some(WidgetEvent::PointerMove {
1970 position: frame.to_local(*position),
1971 modifiers: *modifiers,
1972 pointer: *pointer,
1973 }),
1974 WidgetEvent::Gesture { gesture } => Some(WidgetEvent::Gesture {
1975 gesture: Self::localize_gesture(frame, gesture),
1976 }),
1977 _ => None,
1983 }
1984 }
1985
1986 fn localize_gesture(frame: &crate::arena::LocalFrame, gesture: &GestureEvent) -> GestureEvent {
1993 let loc = |p: teksilo_canvas::Point| frame.to_local(p);
1994 let tap = |t: &TapEvent| {
1995 TapEvent::new(loc(t.position), t.button, t.modifiers).with_pointer(t.pointer)
1996 };
1997 match gesture {
1998 GestureEvent::Tap(t) => GestureEvent::Tap(tap(t)),
1999 GestureEvent::DoubleTap(t) => GestureEvent::DoubleTap(tap(t)),
2000 GestureEvent::TripleTap(t) => GestureEvent::TripleTap(tap(t)),
2001 GestureEvent::LongPress(t) => GestureEvent::LongPress(tap(t)),
2002 GestureEvent::DragStarted {
2003 position,
2004 button,
2005 pointer,
2006 } => GestureEvent::DragStarted {
2007 position: loc(*position),
2008 button: *button,
2009 pointer: *pointer,
2010 },
2011 GestureEvent::DragMoved {
2012 position,
2013 delta,
2014 pointer,
2015 } => GestureEvent::DragMoved {
2016 position: loc(*position),
2017 delta: *delta,
2018 pointer: *pointer,
2019 },
2020 GestureEvent::DragEnded { position, pointer } => GestureEvent::DragEnded {
2021 position: loc(*position),
2022 pointer: *pointer,
2023 },
2024 GestureEvent::DragCancelled {
2025 position,
2026 pointer,
2027 reason,
2028 } => GestureEvent::DragCancelled {
2029 position: loc(*position),
2030 pointer: *pointer,
2031 reason: *reason,
2032 },
2033 GestureEvent::PinchStarted { center } => GestureEvent::PinchStarted {
2034 center: loc(*center),
2035 },
2036 GestureEvent::PinchChanged {
2037 center,
2038 scale,
2039 rotation,
2040 } => GestureEvent::PinchChanged {
2041 center: loc(*center),
2042 scale: *scale,
2043 rotation: *rotation,
2044 },
2045 GestureEvent::PinchEnded => GestureEvent::PinchEnded,
2046 GestureEvent::PinchCancelled { reason } => {
2047 GestureEvent::PinchCancelled { reason: *reason }
2048 }
2049 GestureEvent::Swipe {
2050 direction,
2051 velocity,
2052 } => GestureEvent::Swipe {
2053 direction: *direction,
2054 velocity: *velocity,
2055 },
2056 }
2057 }
2058
2059 pub(super) fn dispatch_to_widget_returning_handled(
2065 &mut self,
2066 target: WidgetId,
2067 event: &WidgetEvent,
2068 ops: &mut dyn crate::window::WindowOps,
2069 ) -> bool {
2070 if !self.arena.is_enabled(target) {
2071 return false;
2072 }
2073
2074 let mut ancestors = Vec::new();
2075 let mut current = self.arena.parent(target);
2076 while let Some(id) = current {
2077 ancestors.push(id);
2078 current = self.arena.parent(id);
2079 }
2080 ancestors.reverse();
2081
2082 let tap_owner: Option<WidgetId> = if matches!(
2100 event,
2101 WidgetEvent::PointerDown { .. } | WidgetEvent::PointerUp { .. }
2102 ) {
2103 let mut owner = None;
2104 let mut cur = Some(target);
2105 while let Some(id) = cur {
2106 if self.arena.get(id).is_some_and(|n| {
2107 n.any_handler(|h| h.on_tap.is_some() || h.on_long_press.is_some())
2108 }) {
2109 owner = Some(id);
2110 break;
2111 }
2112 cur = self.arena.parent(id);
2113 }
2114 owner
2115 } else {
2116 None
2117 };
2118
2119 for &id in &ancestors {
2120 let frame = self.arena.local_frame(id);
2121 let mut ctx = self
2122 .make_event_context(&mut *ops)
2123 .with_dispatch_node(id)
2124 .with_dispatch_target(target)
2125 .with_local_frame(frame);
2126 ctx.press_claimed_by_interactive_child =
2127 tap_owner.is_some_and(|owner| owner != id && self.is_descendant_of(owner, id));
2128 let localized = Self::localize_event(&frame, event);
2131 let event = localized.as_ref().unwrap_or(event);
2132 let response = if let Some(node) = self.arena.get_mut(id) {
2133 Self::try_handler_preview(node, event, &mut ctx).unwrap_or(EventResponse::Ignored)
2134 } else {
2135 EventResponse::Ignored
2136 };
2137 self.collect_from_ctx(ctx, id);
2138 if response == EventResponse::Handled {
2139 self.arena.mark_needs_paint(id);
2140 if matches!(event, WidgetEvent::PointerDown { .. }) {
2148 self.note_preview_claim(id);
2149 }
2150 return true;
2151 }
2152 }
2153
2154 let needs_layout_on_handle = matches!(
2155 event,
2156 WidgetEvent::Scroll { .. } | WidgetEvent::ScrollIntoView { .. }
2157 );
2158 let mut current = Some(target);
2159 let mut is_target = true;
2160 while let Some(id) = current {
2161 let frame = self.arena.local_frame(id);
2162 let mut ctx = self
2163 .make_event_context(&mut *ops)
2164 .with_dispatch_node(id)
2165 .with_dispatch_target(target)
2166 .with_local_frame(frame);
2167 ctx.press_claimed_by_interactive_child =
2168 tap_owner.is_some_and(|owner| owner != id && self.is_descendant_of(owner, id));
2169 let localized = Self::localize_event(&frame, event);
2172 let gesture_cx = self.recognizer_context(id);
2173 let arena_blocked = self.sequence_blocks_arena(id);
2176 let WidgetTree {
2177 arena,
2178 gesture_owners,
2179 ..
2180 } = self;
2181 let event = localized.as_ref().unwrap_or(event);
2182 let response = if let Some(node) = arena.get_mut(id) {
2183 Self::try_handler_bubble(
2184 node,
2185 event,
2186 &mut ctx,
2187 BubbleGates {
2188 fire_on_pointer_event: is_target,
2189 arena_blocked,
2190 },
2191 id,
2192 gesture_owners,
2193 gesture_cx,
2194 )
2195 .unwrap_or(EventResponse::Ignored)
2196 } else {
2197 EventResponse::Ignored
2198 };
2199 self.collect_from_ctx(ctx, id);
2200 if response == EventResponse::Handled {
2201 if needs_layout_on_handle {
2202 self.arena.mark_needs_layout(id);
2203 } else {
2204 self.arena.mark_needs_paint(id);
2205 }
2206 self.note_pointer_acceptance(id, event);
2207 if !matches!(
2222 event,
2223 WidgetEvent::PointerEnter { .. } | WidgetEvent::PointerLeave { .. }
2224 ) {
2225 return true;
2226 }
2227 }
2228 is_target = false;
2229 current = self.arena.parent(id);
2230 }
2231 false
2232 }
2233
2234 pub(super) fn dispatch_to_widget_direct(
2235 &mut self,
2236 target: WidgetId,
2237 event: &WidgetEvent,
2238 ops: &mut dyn crate::window::WindowOps,
2239 ) {
2240 self.dispatch_to_widget_direct_returning_handled(target, event, ops);
2241 }
2242
2243 pub(super) fn dispatch_to_widget_direct_returning_handled(
2253 &mut self,
2254 target: WidgetId,
2255 event: &WidgetEvent,
2256 ops: &mut dyn crate::window::WindowOps,
2257 ) -> bool {
2258 if !self.arena.is_enabled(target) {
2259 return false;
2260 }
2261
2262 let frame = self.arena.local_frame(target);
2263 let mut ctx = self
2264 .make_event_context(&mut *ops)
2265 .with_dispatch_node(target)
2266 .with_dispatch_target(target)
2267 .with_local_frame(frame);
2268 let gesture_cx = self.recognizer_context(target);
2269 let arena_blocked = self.sequence_blocks_arena(target);
2270 let WidgetTree {
2271 arena,
2272 gesture_owners,
2273 ..
2274 } = self;
2275 let response = if let Some(node) = arena.get_mut(target) {
2276 Self::try_handler_bubble(
2277 node,
2278 event,
2279 &mut ctx,
2280 BubbleGates {
2281 fire_on_pointer_event: true,
2282 arena_blocked,
2283 },
2284 target,
2285 gesture_owners,
2286 gesture_cx,
2287 )
2288 .unwrap_or(EventResponse::Ignored)
2289 } else {
2290 EventResponse::Ignored
2291 };
2292 self.collect_from_ctx(ctx, target);
2293
2294 if response == EventResponse::Handled {
2295 if matches!(
2298 event,
2299 WidgetEvent::Scroll { .. } | WidgetEvent::ScrollIntoView { .. }
2300 ) {
2301 self.arena.mark_needs_layout(target);
2302 } else {
2303 self.arena.mark_needs_paint(target);
2304 }
2305 self.note_pointer_acceptance(target, event);
2306 }
2307 response == EventResponse::Handled
2308 }
2309
2310 pub(super) fn note_pointer_acceptance(&mut self, target: WidgetId, event: &WidgetEvent) {
2319 if !matches!(
2320 event,
2321 WidgetEvent::PointerDown { .. }
2322 | WidgetEvent::PointerMove { .. }
2323 | WidgetEvent::PointerUp { .. }
2324 ) {
2325 return;
2326 }
2327 let pointer = self.current_pointer_id();
2328 if let Some(entry) = self.pointers.get_mut(pointer) {
2329 entry.last_accepted = Some(target);
2330 }
2331 }
2332
2333 fn try_handler_preview(
2334 node: &mut crate::arena::WidgetNode,
2335 event: &WidgetEvent,
2336 ctx: &mut EventContext,
2337 ) -> Option<EventResponse> {
2338 match event {
2339 WidgetEvent::KeyDown { .. }
2347 | WidgetEvent::KeyUp { .. }
2348 | WidgetEvent::ImeComposition { .. }
2349 | WidgetEvent::ImeCommit { .. } => {
2350 let has = node.external_handlers.on_key_preview.is_some()
2351 || node.handlers.on_key_preview.is_some();
2352 if !has {
2353 return None;
2354 }
2355 Some(fire_event_handler_both(
2356 &mut node.external_handlers.on_key_preview,
2357 &mut node.handlers.on_key_preview,
2358 event,
2359 ctx,
2360 ))
2361 }
2362 WidgetEvent::PointerEnter { .. } | WidgetEvent::PointerLeave { .. } => None,
2374 _ => {
2375 let has = node.external_handlers.on_pointer_event.is_some()
2376 || node.handlers.on_pointer_event.is_some();
2377 if !has {
2378 return None;
2379 }
2380 Some(fire_event_handler_both(
2381 &mut node.external_handlers.on_pointer_event,
2382 &mut node.handlers.on_pointer_event,
2383 event,
2384 ctx,
2385 ))
2386 }
2387 }
2388 }
2389
2390 fn try_handler_bubble(
2391 node: &mut crate::arena::WidgetNode,
2392 event: &WidgetEvent,
2393 ctx: &mut EventContext,
2394 gates: BubbleGates,
2395 node_id: WidgetId,
2396 gesture_owners: &mut std::collections::HashSet<WidgetId>,
2397 gesture_cx: crate::gesture::RecognizerContext<'_>,
2398 ) -> Option<EventResponse> {
2399 let BubbleGates {
2400 fire_on_pointer_event,
2401 arena_blocked,
2402 } = gates;
2403 match event {
2404 WidgetEvent::PointerEnter { .. } => {
2405 if let Some(cursor) = node.node_cursor {
2406 ctx.declared_cursor_request = Some(cursor);
2411 }
2412 let mut fired = false;
2413 if let Some(h) = node.external_handlers.on_hover.as_mut() {
2414 h(true, ctx);
2415 fired = true;
2416 }
2417 if let Some(h) = node.handlers.on_hover.as_mut() {
2418 h(true, ctx);
2419 fired = true;
2420 }
2421 if fired {
2422 Some(EventResponse::Handled)
2423 } else {
2424 node.node_cursor.map(|_| EventResponse::Handled)
2425 }
2426 }
2427 WidgetEvent::PointerLeave { .. } => {
2428 if node.node_cursor.is_some() {
2429 ctx.declared_cursor_request = Some(crate::widget::CursorIcon::Default);
2430 }
2431 let mut fired = false;
2432 if let Some(h) = node.external_handlers.on_hover.as_mut() {
2433 h(false, ctx);
2434 fired = true;
2435 }
2436 if let Some(h) = node.handlers.on_hover.as_mut() {
2437 h(false, ctx);
2438 fired = true;
2439 }
2440 if fired {
2441 Some(EventResponse::Handled)
2442 } else {
2443 node.node_cursor.map(|_| EventResponse::Handled)
2444 }
2445 }
2446 WidgetEvent::FocusGained { .. } => {
2447 let mut fired = false;
2448 if let Some(h) = node.external_handlers.on_focus.as_mut() {
2449 h(true, ctx);
2450 fired = true;
2451 }
2452 if let Some(h) = node.handlers.on_focus.as_mut() {
2453 h(true, ctx);
2454 fired = true;
2455 }
2456 fired.then_some(EventResponse::Handled)
2457 }
2458 WidgetEvent::FocusLost => {
2459 let mut fired = false;
2460 if let Some(h) = node.external_handlers.on_focus.as_mut() {
2461 h(false, ctx);
2462 fired = true;
2463 }
2464 if let Some(h) = node.handlers.on_focus.as_mut() {
2465 h(false, ctx);
2466 fired = true;
2467 }
2468 fired.then_some(EventResponse::Handled)
2469 }
2470 WidgetEvent::KeyDown { .. }
2471 | WidgetEvent::KeyUp { .. }
2472 | WidgetEvent::ImeComposition { .. }
2473 | WidgetEvent::ImeCommit { .. } => {
2474 if node.external_handlers.on_key.is_some() || node.handlers.on_key.is_some() {
2475 Some(fire_event_handler_both(
2476 &mut node.external_handlers.on_key,
2477 &mut node.handlers.on_key,
2478 event,
2479 ctx,
2480 ))
2481 } else {
2482 None
2483 }
2484 }
2485 WidgetEvent::Scroll { .. } | WidgetEvent::ScrollIntoView { .. } => {
2486 if node.external_handlers.on_scroll.is_some() || node.handlers.on_scroll.is_some() {
2487 Some(fire_event_handler_both(
2488 &mut node.external_handlers.on_scroll,
2489 &mut node.handlers.on_scroll,
2490 event,
2491 ctx,
2492 ))
2493 } else {
2494 None
2495 }
2496 }
2497 WidgetEvent::AccessAction {
2498 action,
2499 target_node,
2500 data,
2501 ..
2502 } => {
2503 let saved_a11y_source = ctx
2523 .current_source
2524 .replace(crate::telemetry::IntentSource::Accessibility);
2525 let mut any_slot = false;
2526 let mut any_handled = false;
2527 if let Some(h) = node.external_handlers.on_access_action_request.as_mut() {
2528 any_slot = true;
2529 any_handled |=
2530 h(*action, *target_node, data.clone(), ctx) == EventResponse::Handled;
2531 }
2532 if let Some(h) = node.handlers.on_access_action_request.as_mut() {
2533 any_slot = true;
2534 any_handled |=
2535 h(*action, *target_node, data.clone(), ctx) == EventResponse::Handled;
2536 }
2537 if let Some(h) = node.external_handlers.on_access_action.as_mut() {
2538 any_slot = true;
2539 any_handled |= h(*action, ctx) == EventResponse::Handled;
2540 }
2541 if let Some(h) = node.handlers.on_access_action.as_mut() {
2542 any_slot = true;
2543 any_handled |= h(*action, ctx) == EventResponse::Handled;
2544 }
2545 let user_handled = any_slot.then_some(if any_handled {
2546 EventResponse::Handled
2547 } else {
2548 EventResponse::Ignored
2549 });
2550
2551 let mut override_handled = false;
2557 if let Some(ov) = node.access_overrides.as_deref_mut() {
2558 if matches!(action, accesskit::Action::CustomAction) {
2559 if let Some(accesskit::ActionData::CustomAction(idx)) = data
2560 && let Some((_, cb)) = ov.custom_actions.get_mut(*idx as usize)
2561 {
2562 cb(ctx);
2563 override_handled = true;
2564 }
2565 } else {
2566 for (a, cb) in ov.actions.iter_mut() {
2567 if *a == *action {
2568 cb(ctx);
2569 override_handled = true;
2570 }
2571 }
2572 }
2573 }
2574
2575 ctx.current_source = saved_a11y_source;
2576 match (user_handled, override_handled) {
2577 (Some(EventResponse::Handled), _) | (_, true) => Some(EventResponse::Handled),
2578 (Some(EventResponse::Ignored), false) => Some(EventResponse::Ignored),
2579 (None, false) => None,
2580 }
2581 }
2582 WidgetEvent::Gesture { gesture } => {
2583 let matched = matches!(
2587 gesture,
2588 GestureEvent::PinchStarted { .. }
2589 | GestureEvent::PinchChanged { .. }
2590 | GestureEvent::PinchEnded
2591 | GestureEvent::Swipe { .. }
2592 | GestureEvent::DoubleTap { .. }
2593 | GestureEvent::TripleTap { .. }
2594 ) && {
2595 let has_handler = match gesture {
2596 GestureEvent::PinchStarted { .. }
2597 | GestureEvent::PinchChanged { .. }
2598 | GestureEvent::PinchEnded => node.any_handler(|h| h.on_pinch.is_some()),
2599 GestureEvent::Swipe { .. } => node.any_handler(|h| h.on_swipe.is_some()),
2600 GestureEvent::DoubleTap { .. } => {
2601 node.any_handler(|h| h.on_double_tap.is_some())
2602 }
2603 GestureEvent::TripleTap { .. } => {
2604 node.any_handler(|h| h.on_triple_tap.is_some())
2605 }
2606 _ => false,
2607 };
2608 if has_handler {
2609 Self::dispatch_recognized_gesture(node, *gesture, ctx);
2610 }
2611 has_handler
2612 };
2613 if matched {
2614 Some(EventResponse::Handled)
2615 } else {
2616 None
2617 }
2618 }
2619 WidgetEvent::PointerDown {
2620 position,
2621 button,
2622 modifiers,
2623 ..
2624 } => {
2625 if fire_on_pointer_event {
2632 let r = fire_event_handler_both(
2633 &mut node.external_handlers.on_pointer_event,
2634 &mut node.handlers.on_pointer_event,
2635 event,
2636 ctx,
2637 );
2638 if r == EventResponse::Handled {
2639 return Some(EventResponse::Handled);
2640 }
2641 }
2642 if arena_blocked {
2643 return None;
2647 }
2648 Self::ensure_gesture_arena(node, node_id, gesture_owners);
2649 if let Some(arena) = node.handlers.gesture_arena.as_mut() {
2650 let cx = gesture_cx;
2651 ctx.capture_pointer_implicit();
2666 let result = arena.process(
2667 &RawPointerEvent::Down {
2668 position: *position,
2669 button: *button,
2670 modifiers: *modifiers,
2671 pointer: cx.pointer,
2672 time: cx.now,
2673 },
2674 &cx,
2675 );
2676 if let Some(gesture) = result {
2677 Self::dispatch_recognized_gesture(node, gesture, ctx);
2678 }
2679 return Some(EventResponse::Handled);
2680 }
2681 None
2682 }
2683 WidgetEvent::PointerUp {
2684 position,
2685 button,
2686 modifiers,
2687 ..
2688 } => {
2689 if fire_on_pointer_event {
2690 let r = fire_event_handler_both(
2691 &mut node.external_handlers.on_pointer_event,
2692 &mut node.handlers.on_pointer_event,
2693 event,
2694 ctx,
2695 );
2696 if r == EventResponse::Handled {
2697 return Some(EventResponse::Handled);
2698 }
2699 }
2700 if arena_blocked {
2701 return None;
2702 }
2703 if let Some(arena) = node.handlers.gesture_arena.as_mut() {
2704 let cx = gesture_cx;
2705 let result = arena.process(
2706 &RawPointerEvent::Up {
2707 position: *position,
2708 button: *button,
2709 modifiers: *modifiers,
2710 pointer: cx.pointer,
2711 time: cx.now,
2712 },
2713 &cx,
2714 );
2715 if let Some(gesture) = result {
2716 Self::dispatch_recognized_gesture(node, gesture, ctx);
2717 }
2718 return Some(EventResponse::Handled);
2719 }
2720 None
2721 }
2722 WidgetEvent::PointerMove { position, .. } => {
2723 if fire_on_pointer_event {
2724 let r = fire_event_handler_both(
2725 &mut node.external_handlers.on_pointer_event,
2726 &mut node.handlers.on_pointer_event,
2727 event,
2728 ctx,
2729 );
2730 if r == EventResponse::Handled {
2731 return Some(EventResponse::Handled);
2732 }
2733 }
2734 if arena_blocked {
2735 return None;
2736 }
2737 if let Some(arena) = node.handlers.gesture_arena.as_mut() {
2738 let cx = gesture_cx;
2739 let result = arena.process(
2740 &RawPointerEvent::Move {
2741 position: *position,
2742 pointer: cx.pointer,
2743 time: cx.now,
2744 },
2745 &cx,
2746 );
2747 if let Some(gesture) = result {
2748 Self::dispatch_recognized_gesture(node, gesture, ctx);
2749 return Some(EventResponse::Handled);
2761 }
2762 return Some(EventResponse::Ignored);
2763 }
2764 None
2765 }
2766 WidgetEvent::PointerCancel {
2767 reason, pointer, ..
2768 } => {
2769 for slot in [
2777 &mut node.external_handlers.on_pointer_cancel,
2778 &mut node.handlers.on_pointer_cancel,
2779 ] {
2780 if let Some(handler) = slot.as_mut() {
2781 handler(pointer, *reason, ctx);
2782 }
2783 }
2784 if fire_on_pointer_event {
2785 let r = fire_event_handler_both(
2786 &mut node.external_handlers.on_pointer_event,
2787 &mut node.handlers.on_pointer_event,
2788 event,
2789 ctx,
2790 );
2791 if r == EventResponse::Handled {
2792 return Some(EventResponse::Handled);
2793 }
2794 }
2795 None
2796 }
2797 }
2798 }
2799
2800 pub(super) fn collect_from_ctx<'ops>(
2801 &mut self,
2802 mut ctx: EventContext<'ops>,
2803 source_widget: WidgetId,
2804 ) {
2805 let local_ops = ctx.window_ops.take();
2811 let mut noop = crate::window::NoopWindowOps;
2812 let ops: &mut dyn crate::window::WindowOps = match local_ops {
2813 Some(o) => o,
2814 None => &mut noop,
2815 };
2816 if ctx.frame_requested {
2817 self.request_frame();
2818 }
2819 if let Some(declared) = ctx.declared_cursor_request {
2823 self.node_declared_cursor = declared;
2824 self.current_cursor = declared;
2825 }
2826 match ctx.cursor_request {
2827 Some(crate::widget::CursorRequest::Set(cursor)) => {
2828 self.current_cursor = cursor;
2829 }
2830 Some(crate::widget::CursorRequest::Release) => {
2834 self.current_cursor = self.node_declared_cursor;
2835 }
2836 None => {}
2837 }
2838 for intent in ctx.pending_intents {
2844 self.enqueue_intent(source_widget, intent, true);
2845 }
2846 if ctx.cancel_key_capture {
2855 self.cancel_key_capture();
2856 }
2857 if let Some(slot) = ctx.pending_key_capture {
2858 self.key_capture = Some(slot);
2859 }
2860 for mutation in ctx.pending_shortcut_mutations {
2862 match mutation {
2863 crate::widget::ShortcutMutation::RebindPrimary { id, keystroke } => {
2864 self.shortcut_registry.rebind_primary(id, keystroke);
2865 }
2866 crate::widget::ShortcutMutation::RebindSecondary { id, keystroke } => {
2867 self.shortcut_registry.rebind_secondary(id, keystroke);
2868 }
2869 crate::widget::ShortcutMutation::ClearOverride { id } => {
2870 self.shortcut_registry.clear_override(&id);
2871 }
2872 }
2873 }
2874 if ctx.close_window_requested {
2875 self.close_window_requested = true;
2876 }
2877 if ctx.force_close_requested {
2878 self.force_close_requested = true;
2879 }
2880 self.pending_modal_requests
2881 .extend(ctx.modal_requests.into_iter().map(|request| {
2882 crate::modal::QueuedModalRequest {
2883 source_widget,
2884 request,
2885 }
2886 }));
2887 if ctx.dismiss_modal && !self.dismiss_modal_for_source(source_widget, &mut *ops) {
2888 self.pending_modal_dismissal = true;
2889 }
2890 for callback in ctx.idle_callbacks {
2891 self.idle_queue.push_boxed(callback);
2892 }
2893 match ctx.dismiss_scope {
2894 Some(crate::widget::DismissScope::All) => {
2895 let dismissed = self.overlay_manager.dismiss_all();
2896 self.dormant_dismissed_content(&dismissed, &mut *ops);
2897 }
2898 Some(crate::widget::DismissScope::AllExceptHosts) => {
2899 self.dismiss_all_overlays_except_hosts(&mut *ops);
2900 }
2901 Some(crate::widget::DismissScope::SelfChain) => {
2902 self.dismiss_self_overlay_chain_for_source(source_widget, &mut *ops);
2903 }
2904 Some(crate::widget::DismissScope::Top) => {
2905 if let Some((_id, content_ids, focus_restore)) = self.overlay_manager.dismiss_top()
2906 {
2907 self.dormant_dismissed_content(&content_ids, &mut *ops);
2908 if let Some(restore_id) = focus_restore
2909 && self.arena.is_active(restore_id)
2910 {
2911 self.focus_ops(restore_id, &mut *ops);
2912 }
2913 }
2914 }
2915 None => {
2916 for id in ctx.overlay_dismissals {
2917 let dismissed = self.overlay_manager.dismiss(id);
2918 self.dormant_dismissed_content(&dismissed, &mut *ops);
2919 }
2920 }
2921 }
2922 for content_id in ctx.overlay_content_dismissals {
2929 if let Some(overlay_id) = self.overlay_manager.find_by_content(content_id) {
2930 let dismissed = self.overlay_manager.dismiss(overlay_id);
2931 self.dormant_dismissed_content(&dismissed, &mut *ops);
2932 }
2933 }
2934 for (id, pause) in ctx.overlay_pause_requests {
2941 if pause {
2942 self.overlay_manager.pause_auto_dismiss(id);
2943 } else {
2944 self.overlay_manager.resume_auto_dismiss(id);
2945 }
2946 }
2947 for preserve_content in ctx.dismiss_descendant_overlays {
2948 self.dismiss_child_overlays_for_source(source_widget, preserve_content, &mut *ops);
2949 }
2950 let deferred_row_activations =
2951 self.apply_tree_mutations(std::mem::take(&mut ctx.tree_mutations));
2952 if ctx.request_a11y_update {
2953 self.a11y_dirty = true;
2954 }
2955 if let Some(visible) = ctx.soft_keyboard_request.take() {
2956 self.request_soft_keyboard(visible);
2957 }
2958 for (message, politeness) in std::mem::take(&mut ctx.announcements) {
2961 self.announce_with(message, politeness);
2962 }
2963 for mut req in ctx.overlay_requests {
2964 if req.parent_overlay.is_none() {
2965 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
2966 }
2967 if self
2968 .overlay_manager
2969 .find_by_content(req.content_id)
2970 .is_some()
2971 {
2972 continue;
2973 }
2974 let current_focus = self.focused;
2975 self.overlay_manager.show(req);
2976 self.a11y_dirty = true;
2980 if let Some(focus_id) = current_focus {
2981 self.overlay_manager.set_top_focus_restore(focus_id);
2982 }
2983 }
2984 for (mut req, band) in ctx.overlay_band_requests {
2985 if req.parent_overlay.is_none() {
2986 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
2987 }
2988 if self
2989 .overlay_manager
2990 .find_by_content(req.content_id)
2991 .is_some()
2992 {
2993 continue;
2994 }
2995 let content_id = req.content_id;
2996 self.overlay_manager.show_in_band(req, band);
2997 self.arena.activate(content_id);
2998 self.a11y_dirty = true;
2999 }
3003 for (content_id, placement) in ctx.overlay_placement_updates {
3006 if let Some(overlay_id) = self.overlay_manager.find_by_content(content_id) {
3007 self.overlay_manager.update_placement(overlay_id, placement);
3008 }
3009 }
3010 for (content_id, source) in ctx.overlay_placement_sources {
3011 if let Some(overlay_id) = self.overlay_manager.find_by_content(content_id) {
3012 self.overlay_manager
3013 .set_placement_source(overlay_id, source);
3014 }
3015 }
3016 for (mut req, duration) in ctx.timed_overlay_requests {
3017 if req.parent_overlay.is_none() {
3018 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3019 }
3020 if self
3021 .overlay_manager
3022 .find_by_content(req.content_id)
3023 .is_some()
3024 {
3025 continue;
3026 }
3027 let current_focus = self.focused;
3028 let overlay_id = self.overlay_manager.show_for(req, duration);
3029 self.overlay_manager
3030 .set_shown_at_sim(overlay_id, self.sim_clock);
3031 self.a11y_dirty = true;
3032 if let Some(focus_id) = current_focus {
3033 self.overlay_manager.set_top_focus_restore(focus_id);
3034 }
3035 }
3036 for (mut req, progress, duration) in ctx.reveal_overlay_requests {
3037 if req.parent_overlay.is_none() {
3038 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3039 }
3040 if self
3041 .overlay_manager
3042 .find_by_content(req.content_id)
3043 .is_some()
3044 {
3045 continue;
3046 }
3047 let content_id = req.content_id;
3048 let current_focus = self.focused;
3049 let overlay_id = self.overlay_manager.show(req);
3050 self.overlay_manager
3051 .set_shown_at_sim(overlay_id, self.sim_clock);
3052 self.a11y_dirty = true;
3053 if let Some(focus_id) = current_focus {
3054 self.overlay_manager.set_top_focus_restore(focus_id);
3055 }
3056 self.register_animated_signal(&progress, content_id);
3062 let _ = progress.try_animate_with_options(crate::animation::AnimationRequest {
3063 target: 1.0,
3064 duration,
3065 easing: teksilo_tokens::Easing::EaseOut,
3066 frame_interval: None,
3067 looping: false,
3068 epsilon: 0.0,
3069 max_duration: None,
3070 });
3071 self.overlay_manager
3072 .attach_fade(overlay_id, progress, duration);
3073 }
3074 if let Some((pointer, capture)) = ctx.pointer_capture {
3075 let named = pointer;
3079 let pointer = pointer.unwrap_or_else(|| self.current_pointer_id());
3080 self.set_pointer_capture(pointer, capture.then_some(source_widget));
3081 if capture && ctx.explicit_capture && named.is_none() {
3085 self.note_explicit_capture(source_widget);
3086 }
3087 }
3088 if let Some(activation) = ctx.drag_activation_override.take() {
3089 self.note_drag_activation_override(source_widget, activation);
3093 }
3094 if ctx.recognized_owning_gesture {
3095 self.note_gesture_recognized(source_widget);
3098 }
3099 if !ctx.gesture_acts.is_empty() {
3100 let acts = std::mem::take(&mut ctx.gesture_acts);
3101 self.apply_gesture_acts(&acts, source_widget);
3102 }
3103 if let Some(reason) = ctx.cancel_pointer_request {
3104 let pointer = self.current_pointer_id();
3105 self.cancel_pointer(pointer, reason, &mut *ops);
3106 }
3107 for (mut request, delay, focus_target, replace_siblings) in ctx.delayed_overlay_requests {
3108 if request.parent_overlay.is_none() {
3109 request.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3110 }
3111 if self
3112 .overlay_manager
3113 .find_by_content(request.content_id)
3114 .is_some()
3115 {
3116 continue;
3117 }
3118 let content_id = request.content_id;
3119 self.pending_delayed_overlays
3120 .retain(|pending| pending.request.content_id != content_id);
3121 self.pending_delayed_overlays.push(PendingDelayedOverlay {
3122 request,
3123 delay,
3124 focus_target,
3125 replace_siblings,
3126 real_requested_at: std::time::Instant::now(),
3127 sim_requested_at: self.sim_clock,
3128 });
3129 self.arena.mark_needs_paint(source_widget);
3130 }
3131 for content_id in ctx.cancel_delayed_overlays {
3132 self.pending_delayed_overlays
3133 .retain(|pending| pending.request.content_id != content_id);
3134 }
3135 for content_id in ctx.safe_region_arm_requests {
3142 if let Some(apex) = self
3143 .previous_pointer_position
3144 .or_else(|| self.hover_owner_position())
3145 {
3146 self.overlay_manager.arm_safe_region(
3147 content_id,
3148 apex,
3149 std::time::Instant::now(),
3150 self.sim_clock,
3151 );
3152 }
3153 }
3154 for id in ctx.repaint_requests {
3155 self.arena.mark_needs_paint(id);
3156 }
3157 for id in ctx.synthetic_clicks {
3158 self.synthesise_tap_with_ops(id, &mut *ops);
3163 }
3164 if ctx.clear_focus_requested {
3167 self.clear_focus_ops(&mut *ops);
3168 }
3169 if let Some(&id) = ctx.focus_requests.last() {
3174 self.request_focus_with_ops(id, super::FocusRequestMode::SelfOrDescendant, &mut *ops);
3184 }
3185 if let Some(&id) = ctx.focus_into_requests.last() {
3186 self.request_focus_with_ops(id, super::FocusRequestMode::Into, &mut *ops);
3193 }
3194
3195 for req in ctx.scroll_into_view_requests {
3204 self.scroll_rect_into_view(
3205 req.from.unwrap_or(source_widget),
3208 req.rect,
3209 req.margin,
3210 req.align,
3211 req.motion,
3212 &mut *ops,
3213 );
3214 }
3215 for (id, margin) in ctx.scroll_widget_into_view_requests {
3222 if self.arena.get(id).is_some() {
3223 let bounds = self.arena.bounds(id);
3224 self.scroll_rect_into_view(
3225 id,
3226 bounds,
3227 margin,
3228 crate::event::ScrollAlign::Minimal,
3229 crate::event::ScrollMotion::Instant,
3230 &mut *ops,
3231 );
3232 }
3233 }
3234
3235 if let Some(&id) = ctx.highlight_tooltip_requests.last() {
3240 self.show_highlight_tooltip(id, &mut *ops);
3241 }
3242
3243 if let Some((source_widget, payload, preview_widget)) = ctx.drag_start_request {
3245 let (preview_content_id, preview_overlay_id) = if let Some(preview) = preview_widget {
3246 let content_id = self.add_boxed(preview);
3253 let overlay_id = self
3254 .overlay_manager
3255 .show(crate::overlay::OverlayRequest::new(
3256 content_id,
3257 source_widget,
3258 crate::overlay::OverlayPlacement::AtPointer(teksilo_canvas::Point::ZERO),
3259 crate::overlay::DismissBehavior::Manual,
3260 ));
3261 self.arena.mark_needs_layout(content_id);
3265 (Some(content_id), Some(overlay_id))
3266 } else {
3267 (None, None)
3268 };
3269 self.active_drag = Some(crate::drag_state::DragSession {
3270 payload,
3271 pointer: self.current_input.pointer,
3277 source_widget: Some(source_widget),
3278 is_external: false,
3279 current_position: teksilo_canvas::Point::ZERO,
3280 current_target: None,
3281 feedback: crate::drag_state::DropFeedback::NoFeedback,
3282 preview_content_id,
3283 preview_overlay_id,
3284 });
3285 self.set_current_pointer_capture(Some(source_widget));
3286 self.current_cursor = crate::widget::CursorIcon::Grabbing;
3289 }
3290 if ctx.cancel_drag {
3291 self.cancel_active_drag(&mut *ops);
3292 }
3293
3294 if let Some(theme) = ctx.theme_request {
3296 self.pending_theme_request = Some(theme);
3302 }
3303 if ctx.follow_system_request {
3304 self.pending_follow_system_request = true;
3308 }
3309 if let Some(locale) = ctx.locale_request {
3310 self.pending_locale_request = Some(locale);
3316 }
3317 if let Some(scale) = ctx.text_scale_request {
3318 self.pending_text_scale_request = Some(scale);
3323 }
3324 if !deferred_row_activations.is_empty() {
3331 self.run_with_event_context(&mut *ops, move |ctx| {
3332 for action in deferred_row_activations {
3333 action(ctx);
3334 }
3335 });
3336 }
3337 }
3338
3339 #[must_use]
3345 fn apply_tree_mutations(
3346 &mut self,
3347 mutations: Vec<crate::widget::TreeMutation>,
3348 ) -> Vec<std::rc::Rc<dyn Fn(&mut crate::widget::EventContext)>> {
3349 let mut deferred_row_activations = Vec::new();
3350 use crate::binding::BindingLevel;
3351 use crate::widget::TreeMutation;
3352
3353 for mutation in mutations {
3354 match mutation {
3355 TreeMutation::SetDormant(id) => {
3356 self.park_subtree(id);
3357 }
3358 TreeMutation::Activate(id) => self.arena.activate(id),
3359 TreeMutation::Destroy(id) => {
3360 if let Some(overlay_id) = self.overlay_manager().find_by_content(id) {
3373 self.dismiss_overlay(overlay_id);
3374 }
3375 self.destroy_subtree(id);
3376 }
3377 TreeMutation::MaterializeNow(id) => {
3381 if self.arena.get(id).is_some() {
3382 self.rebuild_single_widget(id);
3383 }
3384 }
3385 TreeMutation::RowSpaceActivate { row, fallback } => {
3386 deferred_row_activations.push(self.keyboard_toggle_in(row).unwrap_or(fallback));
3396 }
3397 TreeMutation::WithWidgetMut { id, dirty, apply } => {
3398 let existed = if let Some(any) =
3403 self.arena.get_mut(id).and_then(|n| n.widget.as_any_mut())
3404 {
3405 apply(any);
3406 true
3407 } else {
3408 false
3409 };
3410 if existed {
3411 match dirty {
3412 BindingLevel::RepaintOnly => self.arena.mark_needs_paint(id),
3413 BindingLevel::SubtreeRepaint => self.arena.mark_subtree_needs_paint(id),
3414 BindingLevel::Relayout => {
3415 self.arena.mark_needs_layout(id);
3416 self.arena.mark_ancestors_need_layout(id);
3417 }
3418 BindingLevel::Rebuild => {
3419 self.arena.mark_needs_rebuild(id);
3420 self.arena.mark_ancestors_need_layout(id);
3421 }
3422 BindingLevel::AccessibilityOnly => self.a11y_dirty = true,
3423 }
3424 }
3425 }
3426 }
3427 }
3428 deferred_row_activations
3429 }
3430
3431 pub fn hit_test(&self, point: Point) -> Option<WidgetId> {
3440 self.hit_test_excluding_overlay_and_widget(point, None, None)
3441 }
3442
3443 pub fn hit_test_for(
3454 &self,
3455 point: Point,
3456 pointer: &crate::pointer::PointerInfo,
3457 ) -> Option<WidgetId> {
3458 self.hit_test_for_excluding(point, pointer, None, None)
3459 }
3460
3461 pub fn hit_test_for_excluding(
3464 &self,
3465 point: Point,
3466 pointer: &crate::pointer::PointerInfo,
3467 exclude_overlay: Option<crate::overlay::OverlayId>,
3468 exclude_widget: Option<WidgetId>,
3469 ) -> Option<WidgetId> {
3470 let surfaces = self.text_surfaces();
3471 let read_only = |id: WidgetId| surfaces.is_read_only(id);
3472 let hit =
3473 crate::pointer::hit_slop::HitContext::new(pointer.kind, &self.effective_theme.input)
3474 .direction(self.layout_direction)
3475 .read_only_probe(&read_only);
3476 self.hit_test_with(point, exclude_overlay, exclude_widget, &hit)
3477 }
3478
3479 pub fn hit_test_excluding_overlay_and_widget(
3486 &self,
3487 point: Point,
3488 exclude_overlay: Option<crate::overlay::OverlayId>,
3489 exclude_widget: Option<WidgetId>,
3490 ) -> Option<WidgetId> {
3491 self.hit_test_with(
3492 point,
3493 exclude_overlay,
3494 exclude_widget,
3495 &crate::pointer::hit_slop::HitContext::mouse(),
3496 )
3497 }
3498
3499 pub(crate) fn hit_test_with(
3502 &self,
3503 point: Point,
3504 exclude_overlay: Option<crate::overlay::OverlayId>,
3505 exclude_widget: Option<WidgetId>,
3506 hit: &crate::pointer::hit_slop::HitContext<'_>,
3507 ) -> Option<WidgetId> {
3508 if let Some(overlay_id) = self.overlay_manager.hit_test(point) {
3509 if Some(overlay_id) == exclude_overlay {
3510 } else if let Some(overlay) = self.overlay_manager.overlay(overlay_id) {
3512 let content_id = overlay.content_id;
3516 if let Some(found) =
3517 self.arena
3518 .hit_test_in_subtree_with_slop(content_id, point, exclude_widget, hit)
3519 {
3520 return Some(found);
3521 }
3522 if !self
3540 .arena
3541 .get(content_id)
3542 .is_some_and(|node| node.event_pass_through)
3543 {
3544 return None;
3545 }
3546 }
3547 }
3548
3549 if self.overlay_manager.topmost_centered().is_some() {
3550 return None;
3551 }
3552
3553 let mut roots = self.arena.roots();
3559 let inert: Vec<WidgetId> = self
3560 .overlay_manager
3561 .stack
3562 .iter()
3563 .filter(|overlay| overlay.inert)
3564 .map(|overlay| overlay.content_id)
3565 .collect();
3566 if !inert.is_empty() {
3567 roots.retain(|root| !inert.contains(root));
3568 }
3569 self.arena
3570 .hit_test_roots_with_slop(roots, point, exclude_widget, hit)
3571 }
3572}
3573
3574fn is_context_menu_chord(key: Key, modifiers: Modifiers) -> bool {
3592 match key {
3593 Key::ContextMenu => modifiers == Modifiers::NONE,
3594 Key::F10 => modifiers == Modifiers::SHIFT,
3595 #[cfg(target_os = "macos")]
3596 Key::M => modifiers == Modifiers::CTRL | Modifiers::SHIFT,
3597 _ => false,
3598 }
3599}
3600
3601#[cfg(test)]
3602mod tests {
3603 use super::*;
3604 use crate::test_widgets::FillWidget;
3605 use crate::widget::CursorIcon;
3606 use crate::widget_builder::WidgetBuilder;
3607
3608 #[test]
3609 fn pointer_enter_leave_synthesized() {
3610 let mut tree = WidgetTree::new();
3611 let widget = tree.add(FillWidget::new());
3612 tree.layout(SizeProposal::exact(100.0, 50.0));
3613 tree.pointer_move(Point::new(50.0, 25.0));
3614 assert_eq!(tree.hovered(), Some(widget));
3615 tree.pointer_move(Point::new(200.0, 200.0));
3616 assert_eq!(tree.hovered(), None);
3617 }
3618
3619 #[test]
3620 fn pointer_hover_updates_current_cursor() {
3621 let mut tree = WidgetTree::new();
3622 tree.add(FillWidget::new().cursor(CursorIcon::ColResize));
3623 tree.layout(SizeProposal::exact(100.0, 50.0));
3624
3625 tree.pointer_move(Point::new(50.0, 25.0));
3626 assert_eq!(tree.current_cursor(), CursorIcon::ColResize);
3627
3628 tree.pointer_move(Point::new(200.0, 200.0));
3629 assert_eq!(tree.current_cursor(), CursorIcon::Default);
3630 }
3631
3632 #[derive(Debug)]
3635 struct SelfDeclared(CursorIcon);
3636
3637 impl crate::widget::Widget for SelfDeclared {
3638 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
3639 ctx.apply_self_handlers(crate::widget_builder::HandlerSet::new().cursor(self.0));
3640 Vec::new()
3641 }
3642
3643 fn layout_response(
3644 &self,
3645 proposal: SizeProposal,
3646 _ctx: &crate::widget::LayoutContext,
3647 ) -> crate::widget::LayoutResponse {
3648 proposal.resolve(100.0, 50.0).into()
3649 }
3650 }
3651
3652 #[test]
3658 fn declared_cursor_reads_what_a_node_asks_for_and_current_cursor_follows_the_hover() {
3659 let over = Point::new(50.0, 25.0);
3660 let away = Point::new(200.0, 200.0);
3661
3662 let mut tree = WidgetTree::new();
3664 let built = tree.add(FillWidget::new().cursor(CursorIcon::ColResize));
3665 tree.layout(SizeProposal::exact(100.0, 50.0));
3666 assert_eq!(tree.declared_cursor(built), Some(CursorIcon::ColResize));
3667 assert_eq!(
3668 tree.current_cursor(),
3669 CursorIcon::Default,
3670 "not hovered yet"
3671 );
3672 tree.pointer_move(over);
3673 assert_eq!(tree.current_cursor(), CursorIcon::ColResize);
3674 assert_eq!(tree.declared_cursor(built), Some(CursorIcon::ColResize));
3675 tree.pointer_move(away);
3676 assert_eq!(tree.current_cursor(), CursorIcon::Default);
3677 assert_eq!(
3678 tree.declared_cursor(built),
3679 Some(CursorIcon::ColResize),
3680 "the declaration does not depend on the pointer",
3681 );
3682
3683 let mut tree = WidgetTree::new();
3685 let own = tree.add(SelfDeclared(CursorIcon::Crosshair));
3686 tree.layout(SizeProposal::exact(100.0, 50.0));
3687 assert_eq!(tree.declared_cursor(own), Some(CursorIcon::Crosshair));
3688 tree.pointer_move(over);
3689 assert_eq!(tree.current_cursor(), CursorIcon::Crosshair);
3690 tree.pointer_move(away);
3691 assert_eq!(tree.current_cursor(), CursorIcon::Default);
3692
3693 let mut tree = WidgetTree::new();
3695 let plain = tree.add(FillWidget::new());
3696 tree.layout(SizeProposal::exact(100.0, 50.0));
3697 assert_eq!(tree.declared_cursor(plain), None);
3698 let mut tree = WidgetTree::new();
3699 let explicit = tree.add(FillWidget::new().cursor(CursorIcon::Default));
3700 tree.layout(SizeProposal::exact(100.0, 50.0));
3701 assert_eq!(
3702 tree.declared_cursor(explicit),
3703 Some(CursorIcon::Default),
3704 "declaring the default cursor is not declaring nothing",
3705 );
3706 }
3707
3708 #[test]
3718 fn release_cursor_hands_the_cursor_back_to_the_node_that_declared_it() {
3719 let mut tree = WidgetTree::new();
3723 tree.add(
3724 FillWidget::new()
3725 .cursor(CursorIcon::ColResize)
3726 .on_pointer_event(|event, ctx| {
3727 if let WidgetEvent::PointerMove { position, .. } = event {
3728 if position.x < 30.0 {
3729 ctx.set_cursor(CursorIcon::Crosshair);
3730 } else {
3731 ctx.release_cursor();
3732 }
3733 }
3734 EventResponse::Ignored
3735 }),
3736 );
3737 tree.layout(SizeProposal::exact(100.0, 50.0));
3738
3739 tree.pointer_move(Point::new(10.0, 25.0));
3740 assert_eq!(
3741 tree.current_cursor(),
3742 CursorIcon::Crosshair,
3743 "the handler outranks the node it is attached to",
3744 );
3745 tree.pointer_move(Point::new(60.0, 25.0));
3746 assert_eq!(
3747 tree.current_cursor(),
3748 CursorIcon::ColResize,
3749 "and hands it back to the node, not to Default — no enter/leave \
3750 fires on a move within one node, so nothing else could",
3751 );
3752 tree.pointer_move(Point::new(10.0, 25.0));
3753 assert_eq!(
3754 tree.current_cursor(),
3755 CursorIcon::Crosshair,
3756 "and can take it again"
3757 );
3758 tree.pointer_move(Point::new(200.0, 200.0));
3759 assert_eq!(
3760 tree.current_cursor(),
3761 CursorIcon::Default,
3762 "leaving the node clears both the override and the declaration",
3763 );
3764 }
3765
3766 #[test]
3770 fn release_cursor_is_a_no_op_with_nothing_to_undo() {
3771 let mut tree = WidgetTree::new();
3772 tree.add(
3773 FillWidget::new()
3774 .cursor(CursorIcon::ColResize)
3775 .on_pointer_event(|event, ctx| {
3776 if matches!(event, WidgetEvent::PointerMove { .. }) {
3777 ctx.release_cursor();
3778 }
3779 EventResponse::Ignored
3780 }),
3781 );
3782 tree.layout(SizeProposal::exact(100.0, 50.0));
3783 tree.pointer_move(Point::new(50.0, 25.0));
3784 assert_eq!(tree.current_cursor(), CursorIcon::ColResize);
3785
3786 let mut tree = WidgetTree::new();
3788 tree.add(FillWidget::new().on_pointer_event(|event, ctx| {
3789 if let WidgetEvent::PointerMove { position, .. } = event {
3790 if position.x < 30.0 {
3791 ctx.set_cursor(CursorIcon::Crosshair);
3792 } else {
3793 ctx.release_cursor();
3794 }
3795 }
3796 EventResponse::Ignored
3797 }));
3798 tree.layout(SizeProposal::exact(100.0, 50.0));
3799 tree.pointer_move(Point::new(10.0, 25.0));
3800 assert_eq!(tree.current_cursor(), CursorIcon::Crosshair);
3801 tree.pointer_move(Point::new(60.0, 25.0));
3802 assert_eq!(
3803 tree.current_cursor(),
3804 CursorIcon::Default,
3805 "nothing declared a cursor for this chain, so the hand-back \
3806 resolves to Default",
3807 );
3808 }
3809
3810 #[test]
3818 fn an_on_hover_override_does_not_erase_the_declaration_it_outranks() {
3819 let mut tree = WidgetTree::new();
3820 tree.add(
3821 FillWidget::new()
3822 .cursor(CursorIcon::ColResize)
3823 .on_hover(|entered, ctx| {
3824 if entered {
3825 ctx.set_cursor(CursorIcon::Crosshair);
3826 }
3827 })
3828 .on_pointer_event(|event, ctx| {
3829 if let WidgetEvent::PointerMove { position, .. } = event
3830 && position.x >= 30.0
3831 {
3832 ctx.release_cursor();
3833 }
3834 EventResponse::Ignored
3835 }),
3836 );
3837 tree.layout(SizeProposal::exact(100.0, 50.0));
3838
3839 tree.pointer_move(Point::new(10.0, 25.0));
3840 assert_eq!(
3841 tree.current_cursor(),
3842 CursorIcon::Crosshair,
3843 "on_hover runs after the node cursor is applied, so it wins",
3844 );
3845 tree.pointer_move(Point::new(60.0, 25.0));
3846 assert_eq!(
3847 tree.current_cursor(),
3848 CursorIcon::ColResize,
3849 "and the node's declaration survived being overridden",
3850 );
3851 }
3852
3853 #[derive(Debug)]
3856 struct Bumpable {
3857 value: i32,
3858 }
3859
3860 impl crate::widget::Widget for Bumpable {
3861 fn layout_response(
3862 &self,
3863 proposal: SizeProposal,
3864 _ctx: &crate::widget::LayoutContext,
3865 ) -> crate::widget::LayoutResponse {
3866 proposal.resolve(10.0, 10.0).into()
3867 }
3868 fn as_any(&self) -> Option<&dyn std::any::Any> {
3869 Some(self)
3870 }
3871 fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
3872 Some(self)
3873 }
3874 }
3875
3876 #[test]
3877 fn with_widget_mut_applies_and_dirty_marks() {
3878 let mut tree = WidgetTree::new();
3879 let id = tree.add(Bumpable { value: 0 });
3880 tree.layout(SizeProposal::exact(100.0, 100.0));
3881
3882 let mut ctx = EventContext::new();
3883 ctx.with_widget_mut::<Bumpable>(id, crate::binding::BindingLevel::Relayout, |b| {
3884 b.value = 42;
3885 });
3886 tree.collect_from_ctx(ctx, id);
3887
3888 let value = tree
3889 .widget_as_any(id)
3890 .and_then(|a| a.downcast_ref::<Bumpable>())
3891 .map(|b| b.value);
3892 assert_eq!(
3893 value,
3894 Some(42),
3895 "the deferred closure must mutate the live widget"
3896 );
3897 assert!(
3898 tree.needs_layout(),
3899 "Relayout dirty level must mark the tree for relayout"
3900 );
3901 }
3902
3903 #[test]
3904 #[cfg(debug_assertions)]
3905 #[should_panic(expected = "not the requested type")]
3906 fn with_widget_mut_wrong_type_panics_in_debug() {
3907 struct Other;
3908 let mut tree = WidgetTree::new();
3909 let id = tree.add(Bumpable { value: 0 });
3910 let mut ctx = EventContext::new();
3911 ctx.with_widget_mut::<Other>(
3912 id,
3913 crate::binding::BindingLevel::RepaintOnly,
3914 |_o: &mut Other| {},
3915 );
3916 tree.collect_from_ctx(ctx, id);
3919 }
3920
3921 #[test]
3922 fn with_widget_mut_closure_may_fire_observed_signals() {
3923 use crate::signal::Signal;
3931 let mut tree = WidgetTree::new();
3932 let id = tree.add(Bumpable { value: 0 });
3933 tree.layout(SizeProposal::exact(100.0, 100.0));
3934
3935 let trigger = Signal::new(0_u64);
3936 let echo = Signal::new(0_u64);
3937 let echo_for_obs = echo.clone();
3938 let _obs = trigger.observe(move |v| echo_for_obs.set(*v));
3939
3940 let trigger_in = trigger.clone();
3941 let mut ctx = EventContext::new();
3942 ctx.with_widget_mut::<Bumpable>(id, crate::binding::BindingLevel::RepaintOnly, move |b| {
3943 b.value = 7;
3944 trigger_in.set(99);
3946 });
3947 tree.collect_from_ctx(ctx, id); assert_eq!(
3950 echo.get(),
3951 99,
3952 "the observer ran during the deferred mutation"
3953 );
3954 let value = tree
3955 .widget_as_any(id)
3956 .and_then(|a| a.downcast_ref::<Bumpable>())
3957 .map(|b| b.value);
3958 assert_eq!(value, Some(7));
3959 }
3960
3961 #[test]
3962 fn request_accessibility_update_forces_rewalk() {
3963 let mut tree = WidgetTree::new();
3964 let id = tree.add(Bumpable { value: 0 });
3965 tree.layout(SizeProposal::exact(100.0, 100.0));
3966 let _ = tree.sync_accessibility(); assert!(
3968 !tree.a11y_dirty,
3969 "sync_accessibility should clear the dirty flag"
3970 );
3971
3972 let mut ctx = EventContext::new();
3973 ctx.request_accessibility_update();
3974 tree.collect_from_ctx(ctx, id);
3975 assert!(
3976 tree.a11y_dirty,
3977 "request_accessibility_update must force an AT re-walk"
3978 );
3979 }
3980
3981 #[test]
3982 fn rebuild_dirties_accessibility_tree() {
3983 let mut tree = WidgetTree::new();
3990 let id = tree.add(Bumpable { value: 0 });
3991 tree.layout(SizeProposal::exact(100.0, 100.0));
3992 let _ = tree.sync_accessibility(); assert!(
3994 !tree.a11y_dirty,
3995 "sync_accessibility should clear the dirty flag"
3996 );
3997
3998 tree.arena_mark_needs_rebuild_for_testing(id);
4001 tree.layout(SizeProposal::exact(100.0, 100.0));
4002 assert!(
4003 tree.a11y_dirty,
4004 "a rebuild must dirty the AT tree so the next sync re-walks"
4005 );
4006 }
4007
4008 #[test]
4009 fn bound_access_label_change_dirties_accessibility_tree() {
4010 use crate::signal::Signal;
4011 use crate::test_widgets::FillWidget;
4012 use crate::widget_builder::WidgetBuilder;
4013
4014 let label = Signal::new("first".to_string());
4020 let mut tree = WidgetTree::new();
4021 let _id = tree.add(FillWidget::new().access_label(label.clone()));
4022 tree.layout(SizeProposal::exact(100.0, 100.0));
4023 let _ = tree.sync_accessibility(); assert!(!tree.a11y_dirty, "sync_accessibility should clear the flag");
4025
4026 label.set("second".to_string());
4027 tree.layout(SizeProposal::exact(100.0, 100.0));
4028 assert!(
4029 tree.a11y_dirty,
4030 "changing a bound access_label must dirty the AT tree"
4031 );
4032 }
4033
4034 #[test]
4035 fn disabled_ancestor_blocks_event_to_descendant() {
4036 use crate::signal::Signal;
4037 use crate::test_widgets::StackWidget;
4038 use std::cell::Cell;
4039 use std::rc::Rc;
4040
4041 let tapped = Rc::new(Cell::new(false));
4042 let flag = tapped.clone();
4043 let enabled = Signal::new(true);
4044
4045 let mut tree = WidgetTree::new();
4046 let child = tree.add(FillWidget::new().on_tap(move |_pos, _ctx| {
4047 flag.set(true);
4048 }));
4049 let parent = tree.add(StackWidget::new().child(child));
4050 tree.enabled_when(parent, enabled.clone());
4051 tree.layout(SizeProposal::exact(100.0, 50.0));
4052
4053 enabled.set(false);
4054 tree.click(child);
4055 assert!(
4056 !tapped.get(),
4057 "disabled ancestor should block descendant tap"
4058 );
4059
4060 enabled.set(true);
4061 tree.click(child);
4062 assert!(tapped.get(), "re-enabling should restore dispatch");
4063 }
4064
4065 #[test]
4066 fn pointer_positions_are_widget_local_at_nonzero_origin() {
4067 use crate::event::{Modifiers, PointerButton};
4068 use crate::test_widgets::InsetWidget;
4069 use std::cell::Cell;
4070 use std::rc::Rc;
4071
4072 let tap_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
4074 let down_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
4075 let drag_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
4076 let (tp, dp, gp) = (tap_pos.clone(), down_pos.clone(), drag_pos.clone());
4077
4078 let mut tree = WidgetTree::new();
4079 let child = tree.add(
4080 FillWidget::new()
4081 .on_tap(move |ev, _ctx| tp.set(Some(ev.position)))
4082 .on_pointer_event(move |ev, _ctx| {
4083 if let WidgetEvent::PointerDown { position, .. } = ev {
4084 dp.set(Some(*position));
4085 }
4086 crate::event::EventResponse::Ignored
4087 })
4088 .on_drag(move |phase, _ctx| {
4089 use crate::gesture::DragPhase;
4090 match phase {
4091 DragPhase::Started { position, .. }
4092 | DragPhase::Moved { position, .. }
4093 | DragPhase::Ended { position, .. } => gp.set(Some(position)),
4094 _ => {}
4095 }
4096 }),
4097 );
4098 let inset = tree.add(InsetWidget::new(20.0).set_child(child));
4099 let _ = inset;
4100 tree.layout(SizeProposal::exact(200.0, 200.0));
4101 assert_eq!(tree.bounds(child).origin(), Point::new(20.0, 20.0));
4102
4103 tree.dispatch_event(WidgetEvent::pointer_down(
4105 Point::new(50.0, 40.0),
4106 PointerButton::Primary,
4107 Modifiers::NONE,
4108 ));
4109 assert_eq!(
4110 down_pos.get(),
4111 Some(Point::new(30.0, 20.0)),
4112 "on_pointer_event PointerDown must be widget-local"
4113 );
4114 tree.dispatch_event(WidgetEvent::pointer_up(
4115 Point::new(50.0, 40.0),
4116 PointerButton::Primary,
4117 Modifiers::NONE,
4118 ));
4119 assert_eq!(
4120 tap_pos.get(),
4121 Some(Point::new(30.0, 20.0)),
4122 "on_tap position must be widget-local"
4123 );
4124
4125 tree.dispatch_event(WidgetEvent::pointer_down(
4128 Point::new(50.0, 40.0),
4129 PointerButton::Primary,
4130 Modifiers::NONE,
4131 ));
4132 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(65.0, 55.0)));
4135 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(90.0, 70.0)));
4136 assert_eq!(
4137 drag_pos.get(),
4138 Some(Point::new(70.0, 50.0)),
4139 "on_drag position must be widget-local"
4140 );
4141 }
4142
4143 #[test]
4144 fn dormant_widget_not_hit_tested() {
4145 let mut tree = WidgetTree::new();
4146 let widget = tree.add(FillWidget::new());
4147 tree.layout(SizeProposal::exact(100.0, 50.0));
4148
4149 tree.pointer_move(Point::new(50.0, 25.0));
4150 assert_eq!(tree.hovered(), Some(widget));
4151
4152 tree.set_dormant(widget);
4153 tree.pointer_move(Point::new(200.0, 200.0));
4154 tree.pointer_move(Point::new(50.0, 25.0));
4155 assert_eq!(tree.hovered(), None);
4156 }
4157
4158 #[test]
4159 fn ancestor_pointer_handler_does_not_suppress_descendant_hover() {
4160 use crate::event::EventResponse;
4161 use crate::test_widgets::StackWidget;
4162 use std::cell::Cell;
4163 use std::rc::Rc;
4164
4165 let hovered = Rc::new(Cell::new(false));
4167 let h = hovered.clone();
4168
4169 let mut tree = WidgetTree::new();
4170 let child = tree.add(FillWidget::new().on_hover(move |entered, _ctx| h.set(entered)));
4171 tree.add(
4176 StackWidget::new()
4177 .child(child)
4178 .on_pointer_event(|_event, _ctx| EventResponse::Handled),
4179 );
4180 tree.layout(SizeProposal::exact(100.0, 50.0));
4181
4182 tree.pointer_move(Point::new(50.0, 25.0));
4183 assert!(
4184 hovered.get(),
4185 "a greedy ancestor on_pointer_event must NOT swallow the child's PointerEnter"
4186 );
4187
4188 tree.pointer_move(Point::new(500.0, 500.0));
4189 assert!(
4190 !hovered.get(),
4191 "PointerLeave must likewise reach the child despite the ancestor"
4192 );
4193 }
4194
4195 #[test]
4203 fn a_child_hover_handler_does_not_swallow_its_ancestors() {
4204 use crate::test_widgets::StackWidget;
4205 use std::cell::Cell;
4206 use std::rc::Rc;
4207
4208 let (row, button) = (Rc::new(Cell::new(false)), Rc::new(Cell::new(false)));
4209 let (r, b) = (row.clone(), button.clone());
4210
4211 let mut tree = WidgetTree::new();
4212 let child = tree.add(FillWidget::new().on_hover(move |entered, _ctx| b.set(entered)));
4213 tree.add(
4214 StackWidget::new()
4215 .child(child)
4216 .on_hover(move |entered, _ctx| r.set(entered)),
4217 );
4218 tree.layout(SizeProposal::exact(100.0, 50.0));
4219
4220 tree.pointer_move(Point::new(50.0, 25.0));
4221 assert!(button.get(), "the child is hovered");
4222 assert!(row.get(), "and so is the row it is inside");
4223
4224 tree.pointer_move(Point::new(500.0, 500.0));
4225 assert!(!button.get(), "the child heard the leave");
4226 assert!(
4227 !row.get(),
4228 "and so did the row — a container is not still hovered because the \
4229 pointer left it through a button"
4230 );
4231 }
4232
4233 #[test]
4240 fn key_preview_consumes_before_focused_on_key() {
4241 use crate::event::EventResponse;
4244 use crate::test_widgets::StackWidget;
4245 use std::cell::Cell;
4246 use std::rc::Rc;
4247
4248 let leaf_fired = Rc::new(Cell::new(false));
4249 let leaf_flag = leaf_fired.clone();
4250 let preview_fired = Rc::new(Cell::new(false));
4251 let preview_flag = preview_fired.clone();
4252
4253 let mut tree = WidgetTree::new();
4254 let leaf = tree.add(FillWidget::new().focusable().on_key(move |event, _c| {
4255 if matches!(event, WidgetEvent::KeyDown { .. }) {
4258 leaf_flag.set(true);
4259 }
4260 EventResponse::Handled
4261 }));
4262 let mid = tree.add(StackWidget::new().child(leaf));
4263 let _root = tree.add(
4264 StackWidget::new()
4265 .child(mid)
4266 .on_key_preview(move |event, _c| match event {
4267 WidgetEvent::KeyDown {
4268 key: Key::Enter, ..
4269 } => {
4270 preview_flag.set(true);
4271 EventResponse::Handled
4272 }
4273 _ => EventResponse::Ignored,
4274 }),
4275 );
4276
4277 tree.layout(SizeProposal::exact(100.0, 50.0));
4278 tree.focus(leaf);
4279 tree.press_key(Key::Enter, Modifiers::NONE);
4280
4281 assert!(
4282 preview_fired.get(),
4283 "ancestor on_key_preview must fire for KeyDown on a focused descendant"
4284 );
4285 assert!(
4286 !leaf_fired.get(),
4287 "consuming the event in preview must prevent the focused widget's on_key from running"
4288 );
4289 }
4290
4291 #[test]
4292 fn key_preview_falls_through_when_returning_ignored() {
4293 use crate::event::EventResponse;
4296 use crate::test_widgets::StackWidget;
4297 use std::cell::Cell;
4298 use std::rc::Rc;
4299
4300 let leaf_fired = Rc::new(Cell::new(false));
4301 let leaf_flag = leaf_fired.clone();
4302 let preview_fired = Rc::new(Cell::new(false));
4303 let preview_flag = preview_fired.clone();
4304
4305 let mut tree = WidgetTree::new();
4306 let leaf = tree.add(FillWidget::new().focusable().on_key(move |_e, _c| {
4307 leaf_flag.set(true);
4308 EventResponse::Handled
4309 }));
4310 let mid = tree.add(StackWidget::new().child(leaf));
4311 let _root = tree.add(
4312 StackWidget::new()
4313 .child(mid)
4314 .on_key_preview(move |_event, _c| {
4315 preview_flag.set(true);
4316 EventResponse::Ignored
4317 }),
4318 );
4319
4320 tree.layout(SizeProposal::exact(100.0, 50.0));
4321 tree.focus(leaf);
4322 tree.press_key(Key::Enter, Modifiers::NONE);
4323
4324 assert!(preview_fired.get(), "preview must always be invoked");
4325 assert!(
4326 leaf_fired.get(),
4327 "preview returning Ignored must not block the focused widget's on_key"
4328 );
4329 }
4330
4331 #[test]
4332 fn key_preview_excludes_focused_target_itself() {
4333 use crate::event::EventResponse;
4337 use std::cell::Cell;
4338 use std::rc::Rc;
4339
4340 let preview_on_target = Rc::new(Cell::new(false));
4341 let pf = preview_on_target.clone();
4342
4343 let mut tree = WidgetTree::new();
4344 let leaf = tree.add(FillWidget::new().focusable().on_key_preview(move |_e, _c| {
4345 pf.set(true);
4346 EventResponse::Handled
4347 }));
4348 tree.layout(SizeProposal::exact(100.0, 50.0));
4349 tree.focus(leaf);
4350 tree.press_key(Key::Enter, Modifiers::NONE);
4351
4352 assert!(
4353 !preview_on_target.get(),
4354 "the focused widget itself must not see its own on_key_preview"
4355 );
4356 }
4357
4358 #[test]
4359 fn key_preview_root_to_target_order() {
4360 use crate::event::EventResponse;
4364 use crate::test_widgets::StackWidget;
4365 use std::cell::RefCell;
4366 use std::rc::Rc;
4367
4368 let order: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
4369 let outer_log = order.clone();
4370 let inner_log = order.clone();
4371
4372 let mut tree = WidgetTree::new();
4373 let leaf = tree.add(FillWidget::new().focusable());
4374 let inner = tree.add(
4375 StackWidget::new()
4376 .child(leaf)
4377 .on_key_preview(move |event, _c| {
4378 if matches!(event, WidgetEvent::KeyDown { .. }) {
4379 inner_log.borrow_mut().push("inner");
4380 }
4381 EventResponse::Ignored
4382 }),
4383 );
4384 let _outer = tree.add(
4385 StackWidget::new()
4386 .child(inner)
4387 .on_key_preview(move |event, _c| {
4388 if matches!(event, WidgetEvent::KeyDown { .. }) {
4389 outer_log.borrow_mut().push("outer");
4390 }
4391 EventResponse::Ignored
4392 }),
4393 );
4394
4395 tree.layout(SizeProposal::exact(100.0, 50.0));
4396 tree.focus(leaf);
4397 tree.dispatch_event(WidgetEvent::KeyDown {
4398 key: Key::Enter,
4399 modifiers: Modifiers::NONE,
4400 text: None,
4401 });
4402
4403 assert_eq!(
4404 *order.borrow(),
4405 vec!["outer", "inner"],
4406 "preview must walk root → parent-of-target"
4407 );
4408 }
4409
4410 #[test]
4411 fn access_action_routes_to_cursored_target_not_focus() {
4412 use crate::signal::Signal;
4416 let a_fired = Signal::new(false);
4417 let b_fired = Signal::new(false);
4418 let a_cb = a_fired.clone();
4419 let b_cb = b_fired.clone();
4420
4421 let mut tree = WidgetTree::new();
4422 let a = tree.add(
4423 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| a_cb.set(true)),
4424 );
4425 let b = tree.add(
4426 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| b_cb.set(true)),
4427 );
4428 tree.layout(SizeProposal::exact(200.0, 100.0));
4429
4430 tree.focus(a);
4431 tree.dispatch_event(WidgetEvent::AccessAction {
4432 action: accesskit::Action::Click,
4433 target: Some(b),
4434 target_node: crate::accessibility::widget_id_to_node_id(b),
4435 data: None,
4436 });
4437
4438 assert!(b_fired.get(), "the cursored target must receive the action");
4439 assert!(
4440 !a_fired.get(),
4441 "the keyboard-focused widget must NOT receive an action targeting another node"
4442 );
4443 }
4444
4445 #[test]
4446 fn access_action_without_target_is_dropped_not_redirected_to_focus() {
4447 use crate::signal::Signal;
4450 let fired = Signal::new(false);
4451 let cb = fired.clone();
4452
4453 let mut tree = WidgetTree::new();
4454 let widget = tree.add(
4455 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| cb.set(true)),
4456 );
4457 tree.layout(SizeProposal::exact(200.0, 100.0));
4458
4459 tree.focus(widget);
4460 tree.dispatch_event(WidgetEvent::AccessAction {
4461 action: accesskit::Action::Click,
4462 target: None,
4463 target_node: crate::accessibility::root_node_id(),
4464 data: None,
4465 });
4466
4467 assert!(
4468 !fired.get(),
4469 "a target-less action must not be redirected to the focused widget"
4470 );
4471 }
4472
4473 #[test]
4480 fn shortcut_fires_matching_action_on_source_widget() {
4481 use crate::action::Action;
4482 use crate::shortcut::{KeyStroke, Shortcut};
4483 use std::cell::Cell;
4484 use std::rc::Rc;
4485
4486 let fired = Rc::new(Cell::new(false));
4487 let fired_flag = fired.clone();
4488
4489 let mut tree = WidgetTree::new();
4490 let widget = tree.add(FillWidget::new().focusable());
4491 tree.push_action(
4492 widget,
4493 Action::new("app.save").on_invoke(move |_intent, _ctx| {
4494 fired_flag.set(true);
4495 }),
4496 );
4497 tree.shortcut_registry_mut().register(
4498 Shortcut::new("app.save")
4499 .primary(KeyStroke::command(Key::S))
4500 .build(),
4501 );
4502
4503 tree.layout(SizeProposal::exact(100.0, 50.0));
4504 tree.focus(widget);
4505
4506 tree.press_key(Key::S, Modifiers::COMMAND);
4507 assert!(fired.get(), "matching action must fire on KeyDown");
4508 }
4509
4510 #[test]
4511 fn global_shortcut_fires_without_focused_widget() {
4512 use crate::action::Action;
4513 use crate::shortcut::{KeyStroke, Shortcut};
4514 use std::cell::Cell;
4515 use std::rc::Rc;
4516
4517 let fired = Rc::new(Cell::new(false));
4521 let fired_flag = fired.clone();
4522
4523 let mut tree = WidgetTree::new();
4524 let root = tree.add(FillWidget::new());
4525 tree.push_action(
4526 root,
4527 Action::new("app.save").on_invoke(move |_intent, _ctx| {
4528 fired_flag.set(true);
4529 }),
4530 );
4531 tree.shortcut_registry_mut().register(
4532 Shortcut::new("app.save")
4533 .primary(KeyStroke::command(Key::S))
4534 .build(),
4535 );
4536
4537 tree.layout(SizeProposal::exact(100.0, 50.0));
4538 tree.press_key(Key::S, Modifiers::COMMAND);
4541 assert!(
4542 fired.get(),
4543 "global shortcut must fire without a focused widget"
4544 );
4545 }
4546
4547 #[test]
4548 fn global_shortcut_fires_after_focused_widget_destroyed() {
4549 use crate::action::Action;
4550 use crate::shortcut::{KeyStroke, Shortcut};
4551 use std::cell::Cell;
4552 use std::rc::Rc;
4553
4554 let fired = Rc::new(Cell::new(false));
4559 let fired_flag = fired.clone();
4560
4561 let mut tree = WidgetTree::new();
4562 let root = tree.add(FillWidget::new());
4563 let focusable = tree.add_child(root, FillWidget::new().focusable());
4564 tree.push_action(
4565 root,
4566 Action::new("app.save").on_invoke(move |_intent, _ctx| {
4567 fired_flag.set(true);
4568 }),
4569 );
4570 tree.shortcut_registry_mut().register(
4571 Shortcut::new("app.save")
4572 .primary(KeyStroke::command(Key::S))
4573 .build(),
4574 );
4575
4576 tree.layout(SizeProposal::exact(100.0, 50.0));
4577 tree.focus(focusable);
4578 assert_eq!(tree.focused(), Some(focusable));
4579
4580 tree.destroy_subtree(focusable);
4583 assert_eq!(tree.focused(), None, "focus must clear when destroyed");
4584
4585 tree.press_key(Key::S, Modifiers::COMMAND);
4586 assert!(
4587 fired.get(),
4588 "global shortcut must still fire after the focused widget is destroyed"
4589 );
4590 }
4591
4592 #[test]
4593 fn scoped_shortcut_matches_only_when_focus_in_scope() {
4594 use crate::action::Action;
4595 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
4596 use std::cell::Cell;
4597 use std::rc::Rc;
4598
4599 let fired = Rc::new(Cell::new(0));
4600 let fired_flag = fired.clone();
4601
4602 let mut tree = WidgetTree::new();
4603 let scope_root = tree.add(FillWidget::new().focusable());
4604 let inside = tree.add_child(scope_root, FillWidget::new().focusable());
4605 let outside = tree.add(FillWidget::new().focusable());
4606
4607 tree.push_action(
4608 scope_root,
4609 Action::new("editor.find").on_invoke(move |_i, _c| {
4610 fired_flag.set(fired_flag.get() + 1);
4611 }),
4612 );
4613 tree.shortcut_registry_mut().register(
4614 Shortcut::new("editor.find")
4615 .primary(KeyStroke::command(Key::F))
4616 .scope(ShortcutScope::Scoped(scope_root))
4617 .build(),
4618 );
4619
4620 tree.layout(SizeProposal::exact(200.0, 100.0));
4621
4622 tree.focus(outside);
4624 tree.press_key(Key::F, Modifiers::COMMAND);
4625 assert_eq!(
4626 fired.get(),
4627 0,
4628 "scoped shortcut must not fire outside scope"
4629 );
4630
4631 tree.focus(inside);
4633 tree.press_key(Key::F, Modifiers::COMMAND);
4634 assert_eq!(
4635 fired.get(),
4636 1,
4637 "scoped shortcut must fire when focus in scope"
4638 );
4639 }
4640
4641 #[test]
4642 fn same_chord_scoped_first_falls_back_to_global_when_focus_outside() {
4643 use crate::action::Action;
4649 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
4650 use std::cell::Cell;
4651 use std::rc::Rc;
4652
4653 let scoped_fired = Rc::new(Cell::new(0));
4654 let global_fired = Rc::new(Cell::new(0));
4655 let sf = scoped_fired.clone();
4656 let gf = global_fired.clone();
4657
4658 let mut tree = WidgetTree::new();
4659 let root = tree.add(FillWidget::new());
4660 let editor = tree.add_child(root, FillWidget::new().focusable());
4661 let _editor_inner = tree.add_child(editor, FillWidget::new().focusable());
4662 let sidebar = tree.add_child(root, FillWidget::new().focusable());
4663
4664 tree.push_action(
4665 editor,
4666 Action::new("editor.saveBlock").on_invoke(move |_i, _c| sf.set(sf.get() + 1)),
4667 );
4668 tree.push_action(
4669 root,
4670 Action::new("zzz.global.save").on_invoke(move |_i, _c| gf.set(gf.get() + 1)),
4671 );
4672 tree.shortcut_registry_mut().register(
4673 Shortcut::new("editor.saveBlock")
4674 .primary(KeyStroke::command(Key::S))
4675 .scope(ShortcutScope::Scoped(editor))
4676 .build(),
4677 );
4678 tree.shortcut_registry_mut().register(
4679 Shortcut::new("zzz.global.save")
4680 .primary(KeyStroke::command(Key::S))
4681 .build(),
4682 );
4683
4684 tree.layout(SizeProposal::exact(200.0, 100.0));
4685 tree.focus(sidebar);
4686 tree.press_key(Key::S, Modifiers::COMMAND);
4687
4688 assert_eq!(global_fired.get(), 1, "applicable global must fire");
4689 assert_eq!(
4690 scoped_fired.get(),
4691 0,
4692 "inapplicable scoped binding must not eat the chord"
4693 );
4694 }
4695
4696 #[test]
4697 fn same_chord_global_first_yields_to_scoped_when_focus_inside() {
4698 use crate::action::Action;
4703 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
4704 use std::cell::Cell;
4705 use std::rc::Rc;
4706
4707 let scoped_fired = Rc::new(Cell::new(0));
4708 let global_fired = Rc::new(Cell::new(0));
4709 let sf = scoped_fired.clone();
4710 let gf = global_fired.clone();
4711
4712 let mut tree = WidgetTree::new();
4713 let root = tree.add(FillWidget::new());
4714 let editor = tree.add_child(root, FillWidget::new().focusable());
4715 let editor_inner = tree.add_child(editor, FillWidget::new().focusable());
4716 let sidebar = tree.add_child(root, FillWidget::new().focusable());
4717
4718 tree.push_action(
4719 editor,
4720 Action::new("editor.saveBlock").on_invoke(move |_i, _c| sf.set(sf.get() + 1)),
4721 );
4722 tree.push_action(
4723 root,
4724 Action::new("app.save").on_invoke(move |_i, _c| gf.set(gf.get() + 1)),
4725 );
4726 tree.shortcut_registry_mut().register(
4727 Shortcut::new("app.save")
4728 .primary(KeyStroke::command(Key::S))
4729 .build(),
4730 );
4731 tree.shortcut_registry_mut().register(
4732 Shortcut::new("editor.saveBlock")
4733 .primary(KeyStroke::command(Key::S))
4734 .scope(ShortcutScope::Scoped(editor))
4735 .build(),
4736 );
4737
4738 tree.layout(SizeProposal::exact(200.0, 100.0));
4739
4740 tree.focus(editor_inner);
4742 tree.press_key(Key::S, Modifiers::COMMAND);
4743 assert_eq!(
4744 scoped_fired.get(),
4745 1,
4746 "in-focus scoped must win over global"
4747 );
4748 assert_eq!(
4749 global_fired.get(),
4750 0,
4751 "global must yield to the scoped binding"
4752 );
4753
4754 tree.focus(sidebar);
4756 tree.press_key(Key::S, Modifiers::COMMAND);
4757 assert_eq!(scoped_fired.get(), 1, "scoped stays put outside its scope");
4758 assert_eq!(
4759 global_fired.get(),
4760 1,
4761 "global fires when focus leaves the scope"
4762 );
4763 }
4764
4765 #[test]
4766 fn propagated_action_lets_ancestor_handle() {
4767 use crate::action::Action;
4768 use crate::intent::IntentResponse;
4769 use crate::shortcut::{KeyStroke, Shortcut};
4770 use std::cell::Cell;
4771 use std::rc::Rc;
4772
4773 let inner_seen = Rc::new(Cell::new(false));
4774 let outer_seen = Rc::new(Cell::new(false));
4775 let inner_flag = inner_seen.clone();
4776 let outer_flag = outer_seen.clone();
4777
4778 let mut tree = WidgetTree::new();
4779 let outer = tree.add(FillWidget::new().focusable());
4780 let inner = tree.add_child(outer, FillWidget::new().focusable());
4781
4782 tree.push_action(
4784 inner,
4785 Action::new("app.save").on_invoke_with_response(move |_i, _c| {
4786 inner_flag.set(true);
4787 IntentResponse::Propagated
4788 }),
4789 );
4790 tree.push_action(
4791 outer,
4792 Action::new("app.save").on_invoke(move |_i, _c| {
4793 outer_flag.set(true);
4794 }),
4795 );
4796 tree.shortcut_registry_mut().register(
4797 Shortcut::new("app.save")
4798 .primary(KeyStroke::command(Key::S))
4799 .build(),
4800 );
4801
4802 tree.layout(SizeProposal::exact(100.0, 50.0));
4803 tree.focus(inner);
4804
4805 tree.press_key(Key::S, Modifiers::COMMAND);
4806 assert!(inner_seen.get(), "inner action observed the intent");
4807 assert!(outer_seen.get(), "outer action reached after Propagated");
4808 }
4809
4810 #[test]
4811 fn handled_action_stops_propagation() {
4812 use crate::action::Action;
4813 use crate::shortcut::{KeyStroke, Shortcut};
4814 use std::cell::Cell;
4815 use std::rc::Rc;
4816
4817 let inner_seen = Rc::new(Cell::new(false));
4818 let outer_seen = Rc::new(Cell::new(false));
4819 let inner_flag = inner_seen.clone();
4820 let outer_flag = outer_seen.clone();
4821
4822 let mut tree = WidgetTree::new();
4823 let outer = tree.add(FillWidget::new().focusable());
4824 let inner = tree.add_child(outer, FillWidget::new().focusable());
4825
4826 tree.push_action(
4827 inner,
4828 Action::new("app.save").on_invoke(move |_i, _c| {
4829 inner_flag.set(true);
4830 }),
4831 );
4832 tree.push_action(
4833 outer,
4834 Action::new("app.save").on_invoke(move |_i, _c| {
4835 outer_flag.set(true);
4836 }),
4837 );
4838 tree.shortcut_registry_mut().register(
4839 Shortcut::new("app.save")
4840 .primary(KeyStroke::command(Key::S))
4841 .build(),
4842 );
4843
4844 tree.layout(SizeProposal::exact(100.0, 50.0));
4845 tree.focus(inner);
4846
4847 tree.press_key(Key::S, Modifiers::COMMAND);
4848 assert!(inner_seen.get());
4849 assert!(!outer_seen.get(), "Handled at inner must stop propagation");
4850 }
4851
4852 #[test]
4853 fn disabled_action_propagates_by_default() {
4854 use crate::action::Action;
4855 use crate::shortcut::{KeyStroke, Shortcut};
4856 use crate::signal::Signal;
4857 use std::cell::Cell;
4858 use std::rc::Rc;
4859
4860 let inner_seen = Rc::new(Cell::new(false));
4861 let outer_seen = Rc::new(Cell::new(false));
4862 let inner_flag = inner_seen.clone();
4863 let outer_flag = outer_seen.clone();
4864
4865 let mut tree = WidgetTree::new();
4866 let outer = tree.add(FillWidget::new().focusable());
4867 let inner = tree.add_child(outer, FillWidget::new().focusable());
4868
4869 let enabled = Signal::new(false);
4870 tree.push_action(
4871 inner,
4872 Action::new("app.save")
4873 .enabled_when(enabled.clone())
4874 .on_invoke(move |_i, _c| {
4875 inner_flag.set(true);
4876 }),
4877 );
4878 tree.push_action(
4879 outer,
4880 Action::new("app.save").on_invoke(move |_i, _c| {
4881 outer_flag.set(true);
4882 }),
4883 );
4884 tree.shortcut_registry_mut().register(
4885 Shortcut::new("app.save")
4886 .primary(KeyStroke::command(Key::S))
4887 .build(),
4888 );
4889
4890 tree.layout(SizeProposal::exact(100.0, 50.0));
4891 tree.focus(inner);
4892
4893 tree.press_key(Key::S, Modifiers::COMMAND);
4894 assert!(!inner_seen.get(), "disabled inner must not run");
4895 assert!(
4896 outer_seen.get(),
4897 "intent must propagate past disabled inner"
4898 );
4899 }
4900
4901 #[test]
4902 fn disabled_action_with_non_propagating_shortcut_consumes() {
4903 use crate::action::Action;
4904 use crate::shortcut::{KeyStroke, Shortcut};
4905 use crate::signal::Signal;
4906 use std::cell::Cell;
4907 use std::rc::Rc;
4908
4909 let inner_seen = Rc::new(Cell::new(false));
4910 let outer_seen = Rc::new(Cell::new(false));
4911 let inner_flag = inner_seen.clone();
4912 let outer_flag = outer_seen.clone();
4913
4914 let mut tree = WidgetTree::new();
4915 let outer = tree.add(FillWidget::new().focusable());
4916 let inner = tree.add_child(outer, FillWidget::new().focusable());
4917
4918 let enabled = Signal::new(false);
4919 tree.push_action(
4920 inner,
4921 Action::new("app.save")
4922 .enabled_when(enabled.clone())
4923 .on_invoke(move |_i, _c| {
4924 inner_flag.set(true);
4925 }),
4926 );
4927 tree.push_action(
4928 outer,
4929 Action::new("app.save").on_invoke(move |_i, _c| {
4930 outer_flag.set(true);
4931 }),
4932 );
4933 tree.shortcut_registry_mut().register(
4934 Shortcut::new("app.save")
4935 .primary(KeyStroke::command(Key::S))
4936 .propagate_when_disabled(false)
4937 .build(),
4938 );
4939
4940 tree.layout(SizeProposal::exact(100.0, 50.0));
4941 tree.focus(inner);
4942
4943 tree.press_key(Key::S, Modifiers::COMMAND);
4944 assert!(!inner_seen.get(), "disabled inner still does not run");
4945 assert!(
4946 !outer_seen.get(),
4947 "intent must NOT propagate when shortcut disallows it"
4948 );
4949 }
4950
4951 #[test]
4952 fn send_intent_from_handler_reaches_ancestor_action() {
4953 use crate::action::Action;
4954 use crate::intent::Intent;
4955 use std::cell::Cell;
4956 use std::rc::Rc;
4957
4958 let save_seen = Rc::new(Cell::new(false));
4959 let save_flag = save_seen.clone();
4960
4961 let mut tree = WidgetTree::new();
4962 let root = tree.add(FillWidget::new());
4963 let button = tree.add_child(
4964 root,
4965 FillWidget::new().on_tap(|_pos, ctx| {
4966 ctx.send_intent(Intent::new("app.save"));
4967 }),
4968 );
4969 tree.push_action(
4970 root,
4971 Action::new("app.save").on_invoke(move |_i, _c| {
4972 save_flag.set(true);
4973 }),
4974 );
4975
4976 tree.layout(SizeProposal::exact(100.0, 50.0));
4977 tree.click(button);
4978 assert!(
4979 save_seen.get(),
4980 "ctx.send_intent must reach ancestor action"
4981 );
4982 }
4983
4984 #[test]
4985 fn widget_type_histogram_counts_distinct_types() {
4986 let mut tree = WidgetTree::new();
4991 let _ = tree.add(FillWidget::new());
4992 let _ = tree.add(FillWidget::new());
4993 let _ = tree.add(FillWidget::new());
4994 tree.layout(SizeProposal::exact(100.0, 100.0));
4995 let histogram = tree.widget_type_histogram();
4996 let total: u32 = histogram.values().sum();
4997 assert!(
4998 total >= 3,
4999 "expected at least 3 active widgets, got {total}: {histogram:?}"
5000 );
5001 let fillwidget_entries: u32 = histogram
5002 .iter()
5003 .filter(|(k, _)| k.contains("FillWidget"))
5004 .map(|(_, v)| *v)
5005 .sum();
5006 assert!(
5007 fillwidget_entries >= 3,
5008 "expected ≥3 FillWidget instances; histogram = {histogram:?}"
5009 );
5010 assert_eq!(tree.active_widget_count() as u32, total);
5011 }
5012
5013 #[test]
5014 fn intent_source_tagged_handler_for_tap_activation() {
5015 use crate::action::Action;
5019 use crate::intent::Intent;
5020 use crate::telemetry::IntentSource;
5021 use std::cell::Cell;
5022 use std::rc::Rc;
5023 let captured = Rc::new(Cell::new(IntentSource::Unknown));
5024 let captured_for_action = captured.clone();
5025
5026 let mut tree = WidgetTree::new();
5027 let root = tree.add(FillWidget::new());
5028 let button = tree.add_child(
5029 root,
5030 FillWidget::new().on_tap(|_pos, ctx| {
5031 ctx.send_intent(Intent::new("app.save"));
5032 }),
5033 );
5034 tree.push_action(
5035 root,
5036 Action::new("app.save").on_invoke(move |intent, _c| {
5037 captured_for_action.set(intent.source);
5038 }),
5039 );
5040
5041 tree.layout(SizeProposal::exact(100.0, 50.0));
5042 tree.click(button);
5043 assert_eq!(
5044 captured.get(),
5045 IntentSource::Handler,
5046 "tap-driven intent must tag IntentSource::Handler"
5047 );
5048 }
5049
5050 #[test]
5051 fn intent_source_programmatic_when_no_handler_active() {
5052 use crate::intent::Intent;
5053 use crate::telemetry::IntentSource;
5054 let intent = Intent::new("app.demo");
5055 assert_eq!(intent.source, IntentSource::Programmatic);
5056
5057 let mut ctx = EventContext::new();
5059 ctx.send_intent(Intent::new("app.demo"));
5060 let queued = ctx.pending_intents.first().expect("intent queued");
5061 assert_eq!(queued.source, IntentSource::Programmatic);
5062 }
5063
5064 #[test]
5065 fn with_intent_source_overrides_for_managed_widgets() {
5066 use crate::intent::Intent;
5067 use crate::telemetry::IntentSource;
5068 let mut ctx = EventContext::new();
5069 ctx.with_intent_source(IntentSource::Menu, |ctx| {
5070 ctx.send_intent(Intent::new("app.demo"));
5071 });
5072 let queued = ctx.pending_intents.first().expect("intent queued");
5073 assert_eq!(
5074 queued.source,
5075 IntentSource::Menu,
5076 "with_intent_source(Menu) must tag the dispatched intent"
5077 );
5078
5079 ctx.send_intent(Intent::new("app.next"));
5083 let next = ctx.pending_intents.last().expect("second intent");
5084 assert_eq!(next.source, IntentSource::Programmatic);
5085 }
5086
5087 #[test]
5088 fn disabled_shortcut_falls_through_to_focused_widget() {
5089 use crate::action::Action;
5090 use crate::shortcut::{KeyStroke, Shortcut};
5091 use crate::signal::Signal;
5092 use std::cell::Cell;
5093 use std::rc::Rc;
5094
5095 let action_fired = Rc::new(Cell::new(false));
5096 let on_key_fired = Rc::new(Cell::new(false));
5097 let af = action_fired.clone();
5098 let kf = on_key_fired.clone();
5099
5100 let enabled = Signal::new(false);
5101
5102 let mut tree = WidgetTree::new();
5103 let widget = tree.add(FillWidget::new().focusable().on_key(move |event, _ctx| {
5104 if matches!(
5105 event,
5106 WidgetEvent::KeyDown {
5107 key: Key::S,
5108 modifiers,
5109 ..
5110 } if modifiers.command()
5111 ) {
5112 kf.set(true);
5113 return EventResponse::Handled;
5114 }
5115 EventResponse::Ignored
5116 }));
5117 tree.push_action(
5118 widget,
5119 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
5120 );
5121 tree.shortcut_registry_mut().register(
5122 Shortcut::new("app.save")
5123 .primary(KeyStroke::command(Key::S))
5124 .enabled_when(enabled.clone())
5125 .build(),
5126 );
5127
5128 tree.layout(SizeProposal::exact(100.0, 50.0));
5129 tree.focus(widget);
5130
5131 tree.press_key(Key::S, Modifiers::COMMAND);
5133 assert!(
5134 !action_fired.get(),
5135 "disabled shortcut must not invoke its action"
5136 );
5137 assert!(
5138 on_key_fired.get(),
5139 "disabled shortcut must let KeyDown reach the focused widget"
5140 );
5141
5142 on_key_fired.set(false);
5144 enabled.set(true);
5145 tree.press_key(Key::S, Modifiers::COMMAND);
5146 assert!(action_fired.get(), "re-enabled shortcut must dispatch");
5147 assert!(
5148 !on_key_fired.get(),
5149 "enabled shortcut must consume the KeyDown"
5150 );
5151 }
5152
5153 #[test]
5154 fn keyboard_capture_bypasses_shortcut() {
5155 use crate::action::Action;
5156 use crate::shortcut::{KeyStroke, Shortcut};
5157 use std::cell::Cell;
5158 use std::rc::Rc;
5159
5160 fn run(capture: bool) -> (bool, bool) {
5166 let action_fired = Rc::new(Cell::new(false));
5167 let on_key_fired = Rc::new(Cell::new(false));
5168 let af = action_fired.clone();
5169 let kf = on_key_fired.clone();
5170
5171 let mut tree = WidgetTree::new();
5172 let widget = tree.add(
5173 FillWidget::new()
5174 .focusable()
5175 .keyboard_capture(capture)
5176 .on_key(move |event, _ctx| {
5177 if matches!(
5178 event,
5179 WidgetEvent::KeyDown { key: Key::S, modifiers, .. } if modifiers.command()
5180 ) {
5181 kf.set(true);
5182 return EventResponse::Handled;
5183 }
5184 EventResponse::Ignored
5185 }),
5186 );
5187 tree.push_action(
5188 widget,
5189 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
5190 );
5191 tree.shortcut_registry_mut().register(
5192 Shortcut::new("app.save")
5193 .primary(KeyStroke::command(Key::S))
5194 .build(),
5195 );
5196
5197 tree.layout(SizeProposal::exact(100.0, 50.0));
5198 tree.focus(widget);
5199 tree.press_key(Key::S, Modifiers::COMMAND);
5200 (action_fired.get(), on_key_fired.get())
5201 }
5202
5203 let (action, on_key) = run(true);
5205 assert!(
5206 !action,
5207 "keyboard_capture must suppress the shortcut action"
5208 );
5209 assert!(on_key, "keyboard_capture must deliver the raw KeyDown");
5210
5211 let (action, on_key) = run(false);
5213 assert!(action, "without capture the shortcut must fire");
5214 assert!(!on_key, "without capture the widget must not see the key");
5215 }
5216
5217 fn key_recorder(
5219 tree: &mut WidgetTree,
5220 ) -> (WidgetId, std::rc::Rc<std::cell::RefCell<Vec<Key>>>) {
5221 let seen = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
5222 let log = seen.clone();
5223 let widget = tree.add(FillWidget::new().focusable().on_key(move |event, _ctx| {
5224 if let WidgetEvent::KeyDown { key, .. } = event {
5225 log.borrow_mut().push(*key);
5226 }
5227 EventResponse::Ignored
5228 }));
5229 tree.layout(SizeProposal::exact(100.0, 50.0));
5230 tree.focus(widget);
5231 (widget, seen)
5232 }
5233
5234 #[test]
5235 fn a_shortcut_no_action_takes_leaves_its_key_to_the_focused_widget() {
5236 use crate::action::Action;
5237 use crate::intent::IntentResponse;
5238 use crate::shortcut::{KeyStroke, Shortcut};
5239
5240 let mut tree = WidgetTree::new();
5241 let (widget, seen) = key_recorder(&mut tree);
5242 tree.shortcut_registry_mut().register(
5243 Shortcut::new("app.save")
5244 .primary(KeyStroke::command(Key::S))
5245 .build(),
5246 );
5247
5248 tree.press_key(Key::S, Modifiers::COMMAND);
5250 assert_eq!(
5251 *seen.borrow(),
5252 vec![Key::S],
5253 "an unhandled intent must not eat the key"
5254 );
5255
5256 tree.push_action(
5258 widget,
5259 Action::new("app.save").on_invoke_with_response(|_i, _c| IntentResponse::Propagated),
5260 );
5261 tree.press_key(Key::S, Modifiers::COMMAND);
5262 assert_eq!(
5263 seen.borrow().len(),
5264 2,
5265 "a propagated intent must leave the key too"
5266 );
5267
5268 let mut tree = WidgetTree::new();
5271 let (widget, seen) = key_recorder(&mut tree);
5272 tree.shortcut_registry_mut().register(
5273 Shortcut::new("app.save")
5274 .primary(KeyStroke::command(Key::S))
5275 .build(),
5276 );
5277 tree.push_action(widget, Action::new("app.save").on_invoke(|_i, _c| {}));
5278 tree.press_key(Key::S, Modifiers::COMMAND);
5279 assert!(
5280 seen.borrow().is_empty(),
5281 "a handled shortcut consumes its key"
5282 );
5283 }
5284
5285 #[test]
5286 fn standard_edit_chords_follow_a_rebind() {
5287 use crate::action::Action;
5288 use crate::shortcut::{KeyStroke, StandardEdit};
5289 use std::cell::Cell;
5290 use std::rc::Rc;
5291
5292 let mut tree = WidgetTree::new();
5293 let (widget, seen) = key_recorder(&mut tree);
5294 let copies = Rc::new(Cell::new(0));
5295 let count = copies.clone();
5296 tree.push_action(
5297 widget,
5298 Action::new(StandardEdit::Copy.id())
5299 .on_invoke(move |_i, _c| count.set(count.get() + 1)),
5300 );
5301
5302 tree.press_key(Key::C, Modifiers::COMMAND);
5304 assert_eq!(copies.get(), 1, "Ctrl+C reaches the edit.copy action");
5305 assert!(seen.borrow().is_empty());
5306
5307 let elsewhere = KeyStroke::new(Key::Character('à'), Modifiers::CTRL);
5310 tree.shortcut_registry_mut()
5311 .rebind_primary(StandardEdit::Copy.id(), Some(elsewhere));
5312 tree.press_key(Key::C, Modifiers::COMMAND);
5313 assert_eq!(copies.get(), 1, "the replaced chord must not copy");
5314 assert_eq!(*seen.borrow(), vec![Key::C]);
5315 tree.press_key(Key::Character('à'), Modifiers::CTRL);
5316 assert_eq!(copies.get(), 2, "the new chord copies");
5317 }
5318
5319 #[test]
5320 fn ctrl_tab_always_escapes_a_keyboard_capture_surface() {
5321 use std::cell::Cell;
5322 use std::rc::Rc;
5323
5324 let saw_key = Rc::new(Cell::new(false));
5330 let sk = saw_key.clone();
5331
5332 let mut tree = WidgetTree::new();
5333 let capture = tree.add(
5334 FillWidget::new()
5335 .focusable()
5336 .keyboard_capture(true)
5337 .on_key(move |_event, _ctx| {
5339 sk.set(true);
5340 EventResponse::Handled
5341 }),
5342 );
5343 let neighbour = tree.add(FillWidget::new().focusable());
5344 tree.layout(SizeProposal::exact(100.0, 50.0));
5345
5346 tree.focus(capture);
5349 tree.press_key(Key::Tab, Modifiers::NONE);
5350 assert!(saw_key.get(), "plain Tab must reach the capture surface");
5351 assert_eq!(
5352 tree.focused(),
5353 Some(capture),
5354 "plain Tab must not move focus off a capture surface"
5355 );
5356
5357 saw_key.set(false);
5359 tree.press_key(Key::Tab, Modifiers::CTRL);
5360 assert!(
5361 !saw_key.get(),
5362 "Ctrl+Tab is reserved and must not reach the capture surface"
5363 );
5364 assert_eq!(
5365 tree.focused(),
5366 Some(neighbour),
5367 "Ctrl+Tab must move focus out of a capture surface"
5368 );
5369
5370 tree.focus(capture);
5372 tree.press_key(Key::Tab, Modifiers::CTRL | Modifiers::SHIFT);
5373 assert_eq!(
5374 tree.focused(),
5375 Some(neighbour),
5376 "Ctrl+Shift+Tab must move focus out of a capture surface"
5377 );
5378 }
5379
5380 #[test]
5381 fn scope_mismatch_does_not_invoke_on_activate() {
5382 use crate::intent::Intent;
5383 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
5384 use std::cell::Cell;
5385 use std::rc::Rc;
5386
5387 let activated = Rc::new(Cell::new(false));
5393 let activated_flag = activated.clone();
5394
5395 let mut tree = WidgetTree::new();
5396 let scope_root = tree.add(FillWidget::new().focusable());
5397 let outside = tree.add(FillWidget::new().focusable());
5398
5399 tree.shortcut_registry_mut().register(
5400 Shortcut::new("editor.find")
5401 .primary(KeyStroke::command(Key::F))
5402 .scope(ShortcutScope::Scoped(scope_root))
5403 .on_activate(move |_ks, _ctx| {
5404 activated_flag.set(true);
5405 Intent::new("editor.find")
5406 })
5407 .build(),
5408 );
5409
5410 tree.layout(SizeProposal::exact(200.0, 100.0));
5411 tree.focus(outside);
5412
5413 tree.press_key(Key::F, Modifiers::COMMAND);
5414 assert!(
5415 !activated.get(),
5416 "on_activate must not run when focus is outside the shortcut's scope"
5417 );
5418 }
5419
5420 #[test]
5421 fn key_capture_runs_callback_and_bypasses_registry() {
5422 use crate::action::Action;
5423 use crate::shortcut::{KeyStroke, Shortcut};
5424 use std::cell::Cell;
5425 use std::rc::Rc;
5426
5427 let action_fired = Rc::new(Cell::new(false));
5428 let af = action_fired.clone();
5429 let captured = Rc::new(Cell::new(None));
5430 let cf = captured.clone();
5431
5432 let mut tree = WidgetTree::new();
5433 let widget = tree.add(FillWidget::new().focusable());
5434 tree.push_action(
5435 widget,
5436 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
5437 );
5438 tree.shortcut_registry_mut().register(
5439 Shortcut::new("app.save")
5440 .primary(KeyStroke::command(Key::S))
5441 .build(),
5442 );
5443
5444 tree.layout(SizeProposal::exact(100.0, 50.0));
5445 tree.focus(widget);
5446
5447 let handle = tree.begin_key_capture(move |ks, _reg, _ctx| cf.set(Some(ks)));
5448 assert!(tree.is_capturing_keys());
5449
5450 tree.press_key(Key::S, Modifiers::COMMAND);
5451 assert_eq!(
5452 captured.get(),
5453 Some(KeyStroke::command(Key::S)),
5454 "capture callback must receive the chord"
5455 );
5456 assert!(
5457 !action_fired.get(),
5458 "shortcut action must not fire while capture is armed"
5459 );
5460 assert!(
5461 !tree.is_capturing_keys(),
5462 "capture is one-shot; next KeyDown flows normally"
5463 );
5464 drop(handle);
5465 }
5466
5467 #[test]
5468 fn key_capture_can_rebind_through_registry() {
5469 use crate::shortcut::{KeyStroke, Shortcut};
5470
5471 let mut tree = WidgetTree::new();
5472 let widget = tree.add(FillWidget::new().focusable());
5473 tree.shortcut_registry_mut().register(
5474 Shortcut::new("app.save")
5475 .primary(KeyStroke::command(Key::S))
5476 .build(),
5477 );
5478
5479 tree.layout(SizeProposal::exact(100.0, 50.0));
5480 tree.focus(widget);
5481
5482 let _h = tree.begin_key_capture(|ks, reg, _ctx| {
5484 reg.rebind_primary("app.save", Some(ks));
5485 });
5486
5487 tree.press_key(Key::B, Modifiers::COMMAND | Modifiers::SHIFT);
5488 assert_eq!(
5489 tree.shortcut_registry()
5490 .effective("app.save")
5491 .unwrap()
5492 .primary,
5493 Some(KeyStroke::command_shift(Key::B))
5494 );
5495 }
5496
5497 #[test]
5498 fn dropping_capture_handle_cancels_capture() {
5499 use crate::shortcut::{KeyStroke, Shortcut};
5500 use std::cell::Cell;
5501 use std::rc::Rc;
5502
5503 let action_fired = Rc::new(Cell::new(false));
5504 let af = action_fired.clone();
5505 let capture_fired = Rc::new(Cell::new(false));
5506 let cf = capture_fired.clone();
5507
5508 let mut tree = WidgetTree::new();
5509 let widget = tree.add(FillWidget::new().focusable());
5510 tree.push_action(
5511 widget,
5512 crate::action::Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
5513 );
5514 tree.shortcut_registry_mut().register(
5515 Shortcut::new("app.save")
5516 .primary(KeyStroke::command(Key::S))
5517 .build(),
5518 );
5519 tree.layout(SizeProposal::exact(100.0, 50.0));
5520 tree.focus(widget);
5521
5522 {
5526 let _h = tree.begin_key_capture(move |_ks, _reg, _ctx| cf.set(true));
5527 assert!(tree.is_capturing_keys());
5528 }
5530 assert!(
5531 !tree.is_capturing_keys(),
5532 "dropping the handle must cancel the capture"
5533 );
5534
5535 tree.press_key(Key::S, Modifiers::COMMAND);
5536 assert!(!capture_fired.get(), "cancelled capture must not fire");
5537 assert!(
5538 action_fired.get(),
5539 "shortcut action runs after capture was cancelled"
5540 );
5541 }
5542
5543 #[test]
5544 fn second_begin_key_capture_does_not_racecancel_first() {
5545 use std::cell::Cell;
5546 use std::rc::Rc;
5547
5548 let first = Rc::new(Cell::new(false));
5549 let second = Rc::new(Cell::new(false));
5550 let f = first.clone();
5551 let s = second.clone();
5552
5553 let mut tree = WidgetTree::new();
5554 let widget = tree.add(FillWidget::new().focusable());
5555 tree.layout(SizeProposal::exact(100.0, 50.0));
5556 tree.focus(widget);
5557
5558 let h1 = tree.begin_key_capture(move |_ks, _reg, _ctx| f.set(true));
5564 let _h2 = tree.begin_key_capture(move |_ks, _reg, _ctx| s.set(true));
5565 drop(h1);
5566
5567 assert!(
5568 tree.is_capturing_keys(),
5569 "dropping the older handle must not cancel the active capture"
5570 );
5571 tree.press_key(Key::K, Modifiers::COMMAND);
5572 assert!(!first.get());
5573 assert!(second.get(), "newest capture wins");
5574 }
5575
5576 #[test]
5577 fn capture_callback_can_send_intent() {
5578 use crate::action::Action;
5579 use crate::intent::Intent;
5580
5581 use std::cell::Cell;
5582 use std::rc::Rc;
5583
5584 let ran = Rc::new(Cell::new(false));
5585 let flag = ran.clone();
5586
5587 let mut tree = WidgetTree::new();
5588 let widget = tree.add(FillWidget::new().focusable());
5589 tree.push_action(
5590 widget,
5591 Action::new("app.save").on_invoke(move |_i, _c| flag.set(true)),
5592 );
5593 tree.layout(SizeProposal::exact(100.0, 50.0));
5594 tree.focus(widget);
5595
5596 let _h = tree.begin_key_capture(|_ks, _reg, ctx| {
5597 ctx.send_intent(Intent::new("app.save"));
5598 });
5599 tree.press_key(Key::X, Modifiers::COMMAND);
5600 assert!(
5601 ran.get(),
5602 "intent queued from capture callback must dispatch"
5603 );
5604 }
5605
5606 #[test]
5607 fn binding_registry_does_not_accumulate_across_rebuilds() {
5608 use crate::binding::BindingLevel;
5609 use crate::signal::Signal;
5610
5611 #[derive(Debug)]
5612 struct BoundLeaf {
5613 tick: Signal<u64>,
5614 }
5615 impl crate::widget::Widget for BoundLeaf {
5616 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
5617 self.tick.bind_to(
5618 ctx.self_id(),
5619 ctx.binding_registry(),
5620 BindingLevel::Relayout,
5621 );
5622 Vec::new()
5623 }
5624 fn layout_response(
5625 &self,
5626 proposal: SizeProposal,
5627 _ctx: &crate::widget::LayoutContext,
5628 ) -> crate::widget::LayoutResponse {
5629 proposal.resolve(10.0, 10.0).into()
5630 }
5631 }
5632
5633 let mut tree = WidgetTree::new();
5634 let tick = Signal::new(0_u64);
5635 let widget = tree.add(BoundLeaf { tick: tick.clone() });
5636 tree.layout(SizeProposal::exact(200.0, 200.0));
5637 let after_first_build = tree.binding_registry().len();
5638 assert!(after_first_build >= 1);
5639
5640 for _ in 0..5 {
5644 tree.arena.mark_needs_rebuild(widget);
5645 tree.layout(SizeProposal::exact(200.0, 200.0));
5646 }
5647 assert_eq!(
5648 tree.binding_registry().len(),
5649 after_first_build,
5650 "bindings must be cleared on rebuild"
5651 );
5652
5653 tree.destroy_subtree(widget);
5654 assert_eq!(
5655 tree.binding_registry().len(),
5656 0,
5657 "bindings must be cleared on destroy"
5658 );
5659 let _ = tick;
5661 }
5662
5663 #[test]
5664 fn ctx_destroy_cancels_animations_and_bindings_via_deferred_path() {
5665 use crate::binding::BindingLevel;
5673 use crate::signal::Signal;
5674 use std::time::{Duration, Instant};
5675 use teksilo_tokens::Easing;
5676
5677 #[derive(Debug)]
5678 struct BoundLeaf {
5679 tick: Signal<u64>,
5680 }
5681 impl crate::widget::Widget for BoundLeaf {
5682 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
5683 self.tick.bind_to(
5684 ctx.self_id(),
5685 ctx.binding_registry(),
5686 BindingLevel::Relayout,
5687 );
5688 Vec::new()
5689 }
5690 fn layout_response(
5691 &self,
5692 proposal: SizeProposal,
5693 _ctx: &crate::widget::LayoutContext,
5694 ) -> crate::widget::LayoutResponse {
5695 proposal.resolve(10.0, 10.0).into()
5696 }
5697 }
5698
5699 let mut tree = WidgetTree::new();
5700 let widget = tree.add(BoundLeaf {
5701 tick: Signal::new(0_u64),
5702 });
5703 tree.layout(SizeProposal::exact(200.0, 200.0));
5704 assert!(tree.binding_registry().len() >= 1);
5705
5706 let anim = Signal::<f32>::new_animated(0.0);
5709 tree.animation_scheduler.animate(
5710 &anim,
5711 widget,
5712 1.0,
5713 Duration::from_secs(10),
5714 Easing::Linear,
5715 Instant::now(),
5716 );
5717 assert_eq!(tree.animation_scheduler.active_count(), 1);
5718
5719 let mut noop = crate::window::NoopWindowOps;
5721 tree.run_with_event_context(&mut noop, |ctx| ctx.destroy(widget));
5722
5723 assert_eq!(
5724 tree.animation_scheduler.active_count(),
5725 0,
5726 "ctx.destroy must cancel animations owned by the destroyed widget"
5727 );
5728 assert_eq!(
5729 tree.binding_registry().len(),
5730 0,
5731 "ctx.destroy must unregister the destroyed widget's bindings"
5732 );
5733 assert!(
5734 tree.arena.get(widget).is_none(),
5735 "node must be removed from the arena"
5736 );
5737 }
5738
5739 #[test]
5740 fn clear_shortcut_override_via_event_context_restores_default() {
5741 use crate::shortcut::{KeyStroke, Shortcut};
5742
5743 let mut tree = WidgetTree::new();
5744 tree.shortcut_registry_mut().register(
5745 Shortcut::new("app.save")
5746 .primary(KeyStroke::command(Key::S))
5747 .build(),
5748 );
5749 tree.shortcut_registry_mut()
5750 .rebind_primary("app.save", Some(KeyStroke::alt(Key::S)));
5751
5752 let source = tree.add(FillWidget::new());
5753 let mut ctx = EventContext::new();
5754 ctx.clear_shortcut_override("app.save");
5755 tree.collect_from_ctx(ctx, source);
5756
5757 assert_eq!(
5758 tree.shortcut_registry()
5759 .effective("app.save")
5760 .unwrap()
5761 .primary,
5762 Some(KeyStroke::command(Key::S))
5763 );
5764 }
5765
5766 #[test]
5767 fn rebind_shortcut_primary_via_event_context() {
5768 use crate::shortcut::{KeyStroke, Shortcut};
5769
5770 let mut tree = WidgetTree::new();
5771 tree.shortcut_registry_mut().register(
5772 Shortcut::new("app.save")
5773 .primary(KeyStroke::command(Key::S))
5774 .build(),
5775 );
5776 let source = tree.add(FillWidget::new());
5777
5778 let mut ctx = EventContext::new();
5779 ctx.rebind_shortcut_primary("app.save", Some(KeyStroke::alt(Key::S)));
5780 tree.collect_from_ctx(ctx, source);
5781
5782 assert_eq!(
5783 tree.shortcut_registry()
5784 .effective("app.save")
5785 .unwrap()
5786 .primary,
5787 Some(KeyStroke::alt(Key::S))
5788 );
5789 }
5790
5791 #[test]
5792 fn unregister_all_for_owner_called_on_destroy() {
5793 use crate::shortcut::{KeyStroke, Shortcut};
5794
5795 let mut tree = WidgetTree::new();
5796 let widget = tree.add(FillWidget::new());
5797 let widget_owner = widget;
5798 tree.shortcut_registry_mut().register_owned(
5799 Shortcut::new("scoped.thing")
5800 .primary(KeyStroke::command(Key::K))
5801 .build(),
5802 widget_owner,
5803 );
5804 assert!(
5805 tree.shortcut_registry()
5806 .get_default("scoped.thing")
5807 .is_some()
5808 );
5809
5810 tree.destroy_subtree(widget);
5811 assert!(
5812 tree.shortcut_registry()
5813 .get_default("scoped.thing")
5814 .is_none(),
5815 "destroying the owner must unregister its shortcut"
5816 );
5817 }
5818
5819 #[test]
5823 fn global_action_reached_from_unrelated_source() {
5824 use crate::action::Action;
5825 use crate::intent::Intent;
5826 use std::cell::Cell;
5827 use std::rc::Rc;
5828
5829 #[derive(Debug)]
5830 struct Registrar(Rc<Cell<bool>>);
5831 impl crate::widget::Widget for Registrar {
5832 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
5833 let flag = self.0.clone();
5834 ctx.register_action_global(
5835 Action::new("test.global").on_invoke(move |_i, _c| flag.set(true)),
5836 );
5837 vec![]
5838 }
5839 fn layout_response(
5840 &self,
5841 _p: teksilo_canvas::SizeProposal,
5842 _c: &crate::widget::LayoutContext,
5843 ) -> crate::widget::LayoutResponse {
5844 teksilo_canvas::Size::new(0.0, 0.0).into()
5845 }
5846 }
5847
5848 let mut tree = WidgetTree::new();
5849 let fired = Rc::new(Cell::new(false));
5850 let registrar = tree.add(Registrar(fired.clone()));
5851 let source = tree.add(FillWidget::new()); let mut ops = crate::window::NoopWindowOps;
5853
5854 tree.dispatch_intent(source, Intent::new("test.global"), true, &mut ops);
5855 assert!(
5856 fired.get(),
5857 "global action must fire from an unrelated source"
5858 );
5859
5860 fired.set(false);
5862 tree.destroy_subtree(registrar);
5863 tree.dispatch_intent(source, Intent::new("test.global"), true, &mut ops);
5864 assert!(
5865 !fired.get(),
5866 "destroying the owner must remove its global action"
5867 );
5868 }
5869
5870 #[test]
5882 fn hit_test_through_translate_scope() {
5883 use crate::test_widgets::StackWidget;
5884 let mut tree = WidgetTree::new();
5885 let child = tree.add(FillWidget::new());
5886 let parent = tree.add(StackWidget::new().child(child));
5887 tree.set_transform(parent, teksilo_canvas::Transform2D::translate(100.0, 0.0));
5889 tree.layout(SizeProposal::exact(100.0, 50.0));
5890
5891 assert_eq!(
5895 tree.hit_test(Point::new(50.0, 25.0)),
5896 None,
5897 "pre-transform area is not visually populated and must not hit"
5898 );
5899 assert_eq!(
5901 tree.hit_test(Point::new(150.0, 25.0)),
5902 Some(child),
5903 "visually-rendered area must hit the child"
5904 );
5905 assert_eq!(tree.hit_test(Point::new(250.0, 25.0)), None);
5907 }
5908
5909 #[test]
5910 fn hit_test_through_scale_scope() {
5911 use crate::test_widgets::StackWidget;
5912 let mut tree = WidgetTree::new();
5913 let child = tree.add(FillWidget::new());
5914 let parent = tree.add(StackWidget::new().child(child));
5915 tree.set_transform(parent, teksilo_canvas::Transform2D::scale(0.5, 0.5));
5918 tree.layout(SizeProposal::exact(100.0, 50.0));
5919
5920 assert_eq!(tree.hit_test(Point::new(25.0, 12.0)), Some(child));
5922 assert_eq!(
5925 tree.hit_test(Point::new(75.0, 25.0)),
5926 None,
5927 "scaled-out region must not hit"
5928 );
5929 }
5930
5931 #[test]
5932 fn hit_test_through_nested_transforms_compose() {
5933 use crate::test_widgets::StackWidget;
5934 let mut tree = WidgetTree::new();
5935 let leaf = tree.add(FillWidget::new());
5936 let inner = tree.add(StackWidget::new().child(leaf));
5937 let outer = tree.add(StackWidget::new().child(inner));
5938 tree.set_transform(outer, teksilo_canvas::Transform2D::translate(100.0, 0.0));
5942 tree.set_transform(inner, teksilo_canvas::Transform2D::scale(2.0, 2.0));
5943 tree.layout(SizeProposal::exact(50.0, 25.0));
5944
5945 assert_eq!(tree.hit_test(Point::new(150.0, 25.0)), Some(leaf));
5949 assert_eq!(tree.hit_test(Point::new(50.0, 25.0)), None);
5950 assert_eq!(tree.hit_test(Point::new(250.0, 25.0)), None);
5951 }
5952
5953 #[test]
5954 fn hit_test_identity_transform_unchanged() {
5955 let mut tree = WidgetTree::new();
5959 let widget = tree.add(FillWidget::new());
5960 tree.set_transform(widget, teksilo_canvas::Transform2D::IDENTITY);
5961 tree.layout(SizeProposal::exact(100.0, 50.0));
5962 assert_eq!(tree.hit_test(Point::new(50.0, 25.0)), Some(widget));
5963 }
5964
5965 #[test]
5966 fn arena_effective_transform_composes_ancestors() {
5967 use crate::test_widgets::StackWidget;
5978 let mut tree = WidgetTree::new();
5979 let leaf = tree.add(FillWidget::new());
5980 let inner = tree.add(StackWidget::new().child(leaf));
5981 let outer = tree.add(StackWidget::new().child(inner));
5982 tree.set_transform(outer, teksilo_canvas::Transform2D::translate(100.0, 0.0));
5983 tree.set_transform(inner, teksilo_canvas::Transform2D::scale(2.0, 2.0));
5984 tree.layout(SizeProposal::exact(50.0, 25.0));
5985
5986 let eff = tree.arena.effective_transform(leaf);
5987 let expected = teksilo_canvas::Transform2D::scale(2.0, 2.0)
5988 .then(&teksilo_canvas::Transform2D::translate(100.0, 0.0));
5989 for (a, b) in eff.m.iter().zip(expected.m.iter()) {
5990 assert!(
5991 (a - b).abs() < 1e-5,
5992 "effective_transform mismatch: got {:?}, want {:?}",
5993 eff.m,
5994 expected.m
5995 );
5996 }
5997
5998 let screen_origin = eff.apply_point(Point::new(0.0, 0.0));
6004 assert!((screen_origin.x - 100.0).abs() < 1e-5);
6005 assert!((screen_origin.y - 0.0).abs() < 1e-5);
6006 let screen_corner = eff.apply_point(Point::new(50.0, 25.0));
6009 assert!((screen_corner.x - 200.0).abs() < 1e-5);
6010 assert!((screen_corner.y - 50.0).abs() < 1e-5);
6011 }
6012
6013 #[derive(Debug)]
6018 struct StubMenu;
6019 impl crate::widget::Widget for StubMenu {
6020 fn layout_response(
6021 &self,
6022 _proposal: SizeProposal,
6023 _ctx: &crate::widget::LayoutContext,
6024 ) -> crate::widget::LayoutResponse {
6025 teksilo_canvas::Size::new(100.0, 40.0).into()
6026 }
6027 }
6028
6029 #[derive(Debug)]
6041 struct NominatingWidget {
6042 row: std::cell::Cell<Option<WidgetId>>,
6043 }
6044
6045 impl crate::widget::Widget for NominatingWidget {
6046 fn layout_response(
6047 &self,
6048 proposal: SizeProposal,
6049 _ctx: &LayoutContext,
6050 ) -> crate::widget::LayoutResponse {
6051 proposal.resolve(50.0, 20.0).into()
6052 }
6053
6054 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6055 ctx.apply_self_handlers(crate::widget_builder::HandlerSet::new().focusable(true));
6056 Vec::new()
6057 }
6058
6059 fn context_menu_key_target(&self) -> Option<WidgetId> {
6060 self.row.get()
6061 }
6062 }
6063
6064 fn press(tree: &mut WidgetTree, key: Key, modifiers: Modifiers) {
6065 tree.dispatch_event(WidgetEvent::KeyDown {
6066 key,
6067 modifiers,
6068 text: None,
6069 });
6070 }
6071
6072 #[test]
6073 fn the_context_menu_key_opens_the_focused_widget_menu() {
6074 use std::cell::Cell;
6075 use std::rc::Rc;
6076
6077 let opened = Rc::new(Cell::new(false));
6078 let flag = opened.clone();
6079 let mut tree = WidgetTree::new();
6080 let widget = tree.add(
6081 FillWidget::new()
6082 .focusable()
6083 .context_menu(move |_pos, _ctx| {
6084 flag.set(true);
6085 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6086 }),
6087 );
6088 tree.layout(SizeProposal::exact(200.0, 100.0));
6089 tree.focus(widget);
6090
6091 press(&mut tree, Key::ContextMenu, Modifiers::NONE);
6092 assert!(opened.get(), "the dedicated Menu key must open the menu");
6093 }
6094
6095 #[test]
6098 fn shift_f10_opens_the_focused_widget_menu() {
6099 use std::cell::Cell;
6100 use std::rc::Rc;
6101
6102 let opened = Rc::new(Cell::new(false));
6103 let flag = opened.clone();
6104 let mut tree = WidgetTree::new();
6105 let widget = tree.add(
6106 FillWidget::new()
6107 .focusable()
6108 .context_menu(move |_pos, _ctx| {
6109 flag.set(true);
6110 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6111 }),
6112 );
6113 tree.layout(SizeProposal::exact(200.0, 100.0));
6114 tree.focus(widget);
6115
6116 press(&mut tree, Key::F10, Modifiers::SHIFT);
6117 assert!(opened.get(), "Shift+F10 must open the menu");
6118 }
6119
6120 #[test]
6123 fn a_near_miss_chord_is_not_a_context_menu_request() {
6124 use std::cell::Cell;
6125 use std::rc::Rc;
6126
6127 let opened = Rc::new(Cell::new(false));
6128 let flag = opened.clone();
6129 let mut tree = WidgetTree::new();
6130 let widget = tree.add(
6131 FillWidget::new()
6132 .focusable()
6133 .context_menu(move |_pos, _ctx| {
6134 flag.set(true);
6135 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6136 }),
6137 );
6138 tree.layout(SizeProposal::exact(200.0, 100.0));
6139 tree.focus(widget);
6140
6141 press(&mut tree, Key::F10, Modifiers::SHIFT | Modifiers::CTRL);
6142 press(&mut tree, Key::F10, Modifiers::NONE);
6143 assert!(!opened.get(), "only Shift+F10 exactly asks for a menu");
6144 }
6145
6146 #[test]
6150 fn the_keyboard_target_can_be_a_row_rather_than_the_focused_container() {
6151 use std::cell::Cell;
6152 use std::rc::Rc;
6153
6154 let menu_owner = Rc::new(Cell::new(None::<&'static str>));
6155
6156 let row_flag = menu_owner.clone();
6157 let container_flag = menu_owner.clone();
6158
6159 let mut tree = WidgetTree::new();
6160 let row = tree.add(FillWidget::new().context_menu(move |_pos, _ctx| {
6161 row_flag.set(Some("row"));
6162 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6163 }));
6164 let container = tree.add(
6165 crate::test_widgets::StackWidget::new()
6166 .child(row)
6167 .context_menu(move |_pos, _ctx| {
6168 container_flag.set(Some("container"));
6169 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6170 }),
6171 );
6172 tree.layout(SizeProposal::exact(200.0, 100.0));
6173
6174 let nominator = tree.add(NominatingWidget {
6176 row: std::cell::Cell::new(Some(row)),
6177 });
6178 tree.layout(SizeProposal::exact(200.0, 100.0));
6179 tree.focus(nominator);
6180 let _ = container;
6181
6182 press(&mut tree, Key::ContextMenu, Modifiers::NONE);
6183 assert_eq!(
6184 menu_owner.get(),
6185 Some("row"),
6186 "the nominated row's factory must be the one that runs"
6187 );
6188 }
6189
6190 #[test]
6193 fn the_chord_falls_through_when_there_is_no_menu_to_show() {
6194 use std::cell::Cell;
6195 use std::rc::Rc;
6196
6197 let saw_key = Rc::new(Cell::new(false));
6198 let flag = saw_key.clone();
6199 let mut tree = WidgetTree::new();
6200 let widget = tree.add(FillWidget::new().focusable().on_key(move |ev, _ctx| {
6201 if matches!(ev, WidgetEvent::KeyDown { key: Key::F10, .. }) {
6202 flag.set(true);
6203 }
6204 crate::event::EventResponse::Ignored
6205 }));
6206 tree.layout(SizeProposal::exact(200.0, 100.0));
6207 tree.focus(widget);
6208
6209 press(&mut tree, Key::F10, Modifiers::SHIFT);
6210 assert!(
6211 saw_key.get(),
6212 "with no factory anywhere, the key must reach the widget"
6213 );
6214 }
6215
6216 #[test]
6222 fn each_route_to_a_menu_tells_the_factory_what_asked_for_it() {
6223 use crate::widget_builder::ContextMenuTrigger;
6224 use std::cell::RefCell;
6225 use std::rc::Rc;
6226
6227 let seen = Rc::new(RefCell::new(Vec::new()));
6228 let log = seen.clone();
6229 let mut tree = WidgetTree::new();
6230 let widget = tree.add(
6231 FillWidget::new()
6232 .focusable()
6233 .context_menu(move |_pos, ctx| {
6234 log.borrow_mut().push(ctx.context_menu_trigger());
6235 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6236 }),
6237 );
6238 tree.layout(SizeProposal::exact(200.0, 100.0));
6239 tree.focus(widget);
6240 let inside = tree.bounds(widget).center();
6241
6242 press(&mut tree, Key::F10, Modifiers::SHIFT);
6243 press(&mut tree, Key::Escape, Modifiers::NONE);
6244 press(&mut tree, Key::ContextMenu, Modifiers::NONE);
6245 press(&mut tree, Key::Escape, Modifiers::NONE);
6246 tree.dispatch_event(WidgetEvent::pointer_down(
6247 inside,
6248 crate::event::PointerButton::Secondary,
6249 Modifiers::NONE,
6250 ));
6251 tree.dispatch_event(WidgetEvent::pointer_up(
6252 inside,
6253 crate::event::PointerButton::Secondary,
6254 Modifiers::NONE,
6255 ));
6256 press(&mut tree, Key::Escape, Modifiers::NONE);
6257 tree.dispatch_event(WidgetEvent::AccessAction {
6258 action: accesskit::Action::ShowContextMenu,
6259 target: Some(widget),
6260 target_node: crate::accessibility::widget_id_to_node_id(widget),
6261 data: None,
6262 });
6263 press(&mut tree, Key::Escape, Modifiers::NONE);
6264 tree.long_press_at(teksilo_tokens::PointerKind::Touch, inside);
6265
6266 assert_eq!(
6267 *seen.borrow(),
6268 vec![
6269 Some(ContextMenuTrigger::Keyboard),
6270 Some(ContextMenuTrigger::Keyboard),
6271 Some(ContextMenuTrigger::Pointer),
6272 Some(ContextMenuTrigger::Accessibility),
6273 Some(ContextMenuTrigger::Pointer),
6274 ],
6275 "Shift+F10, the Menu key, a right-click, the assistive action and a \
6276 hold, in that order"
6277 );
6278 }
6279
6280 #[test]
6281 fn context_menu_factory_receives_click_position() {
6282 use crate::event::{Modifiers, PointerButton};
6283 use std::cell::Cell;
6284 use std::rc::Rc;
6285
6286 let captured_position = Rc::new(Cell::new(None::<Point>));
6287 let cap = captured_position.clone();
6288 let mut tree = WidgetTree::new();
6289 let widget = tree.add(FillWidget::new().context_menu(move |pos, _ctx| {
6290 cap.set(Some(pos));
6291 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6292 }));
6293 tree.layout(SizeProposal::exact(200.0, 100.0));
6294
6295 let click = Point::new(73.0, 42.0);
6296 tree.dispatch_event(WidgetEvent::pointer_down(
6297 click,
6298 PointerButton::Secondary,
6299 Modifiers::NONE,
6300 ));
6301
6302 let got = captured_position.get();
6303 assert_eq!(
6304 got,
6305 Some(click),
6306 "factory must receive the click position; got {:?}",
6307 got
6308 );
6309 let _ = widget;
6310 }
6311
6312 #[test]
6313 fn context_menu_factory_returning_none_falls_through_to_parent() {
6314 use crate::event::{Modifiers, PointerButton};
6315 use crate::test_widgets::StackWidget;
6316 use std::cell::Cell;
6317 use std::rc::Rc;
6318
6319 let outer_called = Rc::new(Cell::new(0_u32));
6323 let outer_flag = outer_called.clone();
6324 let mut tree = WidgetTree::new();
6325 let inner = tree.add(FillWidget::new().context_menu(|_pos, _ctx| None));
6326 let _outer = tree.add(
6327 StackWidget::new()
6328 .child(inner)
6329 .context_menu(move |_pos, _ctx| {
6330 outer_flag.set(outer_flag.get() + 1);
6331 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6332 }),
6333 );
6334 tree.layout(SizeProposal::exact(200.0, 100.0));
6335
6336 tree.dispatch_event(WidgetEvent::pointer_down(
6337 Point::new(50.0, 25.0),
6338 PointerButton::Secondary,
6339 Modifiers::NONE,
6340 ));
6341
6342 assert_eq!(
6343 outer_called.get(),
6344 1,
6345 "inner returning None must fall through to the outer factory"
6346 );
6347 }
6348
6349 #[test]
6350 fn context_menu_factory_none_throughout_chain_does_not_show_overlay() {
6351 use crate::event::{Modifiers, PointerButton};
6352
6353 let mut tree = WidgetTree::new();
6355 tree.add(FillWidget::new().context_menu(|_pos, _ctx| None));
6356 tree.layout(SizeProposal::exact(200.0, 100.0));
6357
6358 let overlay_count_before = tree.overlay_manager.len();
6359 tree.dispatch_event(WidgetEvent::pointer_down(
6360 Point::new(50.0, 25.0),
6361 PointerButton::Secondary,
6362 Modifiers::NONE,
6363 ));
6364 let overlay_count_after = tree.overlay_manager.len();
6365 assert_eq!(
6366 overlay_count_before, overlay_count_after,
6367 "a factory returning None must not mount any overlay"
6368 );
6369 }
6370
6371 #[derive(Debug)]
6382 struct FreshChildHost {
6383 preserve: bool,
6384 }
6385 impl Widget for FreshChildHost {
6386 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6387 vec![ctx.add(FillWidget::new())]
6388 }
6389 fn layout_response(
6390 &self,
6391 p: SizeProposal,
6392 _c: &LayoutContext,
6393 ) -> crate::widget::LayoutResponse {
6394 p.resolve(10.0, 10.0).into()
6395 }
6396 fn preserves_children_on_rebuild(&self) -> bool {
6397 self.preserve
6398 }
6399 }
6400
6401 #[test]
6402 fn reconcile_reaps_dropped_children_no_leak() {
6403 for preserve in [false, true] {
6408 let mut tree = WidgetTree::new();
6409 let host = tree.add(FreshChildHost { preserve });
6410 tree.layout(SizeProposal::exact(100.0, 100.0));
6411 let total0 = tree.arena.len();
6412 let active0 = tree.active_widget_count();
6413 for _ in 0..5 {
6414 tree.arena_mark_needs_rebuild_for_testing(host);
6415 tree.layout(SizeProposal::exact(100.0, 100.0));
6416 }
6417 assert_eq!(
6418 tree.arena.len(),
6419 total0,
6420 "preserve={preserve}: dropped children must be reaped, not leaked"
6421 );
6422 assert_eq!(
6423 tree.active_widget_count(),
6424 active0,
6425 "preserve={preserve}: no stranded still-active orphans"
6426 );
6427 }
6428 }
6429
6430 #[derive(Debug)]
6436 struct DetachedContentHost {
6437 preserve: bool,
6438 }
6439 impl Widget for DetachedContentHost {
6440 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6441 let popup = ctx.add_detached(FillWidget::new());
6442 ctx.set_dormant(popup);
6443 vec![ctx.add(FillWidget::new())]
6444 }
6445 fn layout_response(
6446 &self,
6447 p: SizeProposal,
6448 _c: &LayoutContext,
6449 ) -> crate::widget::LayoutResponse {
6450 p.resolve(10.0, 10.0).into()
6451 }
6452 fn preserves_children_on_rebuild(&self) -> bool {
6453 self.preserve
6454 }
6455 }
6456
6457 #[test]
6458 fn rebuilding_reaps_detached_content_no_leak() {
6459 for preserve in [false, true] {
6465 let mut tree = WidgetTree::new();
6466 let host = tree.add(DetachedContentHost { preserve });
6467 tree.layout(SizeProposal::exact(100.0, 100.0));
6468 let total0 = tree.arena.len();
6469 for _ in 0..5 {
6470 tree.arena_mark_needs_rebuild_for_testing(host);
6471 tree.layout(SizeProposal::exact(100.0, 100.0));
6472 }
6473 assert_eq!(
6474 tree.arena.len(),
6475 total0,
6476 "preserve={preserve}: the previous build's detached content must be reaped"
6477 );
6478 }
6479 }
6480
6481 #[test]
6482 fn destroying_a_host_reaps_its_detached_content() {
6483 let mut tree = WidgetTree::new();
6484 let outer = tree.add(FillWidget::new());
6485 tree.layout(SizeProposal::exact(100.0, 100.0));
6486 let empty = tree.arena.len();
6487
6488 let host = tree.add_child(outer, DetachedContentHost { preserve: false });
6489 tree.layout(SizeProposal::exact(100.0, 100.0));
6490 assert!(tree.arena.len() > empty);
6491
6492 tree.destroy_subtree(host);
6493 assert_eq!(
6494 tree.arena.len(),
6495 empty,
6496 "the popup must die with the widget that built it"
6497 );
6498 }
6499
6500 #[derive(Debug)]
6502 struct StableChildHost {
6503 child: Option<WidgetId>,
6504 probe: std::rc::Rc<std::cell::Cell<Option<WidgetId>>>,
6505 }
6506 impl Widget for StableChildHost {
6507 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6508 let id = match self.child {
6509 Some(id) => id,
6510 None => {
6511 let id = ctx.add(FillWidget::new());
6512 self.child = Some(id);
6513 self.probe.set(Some(id));
6514 id
6515 }
6516 };
6517 vec![id]
6518 }
6519 fn layout_response(
6520 &self,
6521 p: SizeProposal,
6522 _c: &LayoutContext,
6523 ) -> crate::widget::LayoutResponse {
6524 p.resolve(10.0, 10.0).into()
6525 }
6526 fn preserves_children_on_rebuild(&self) -> bool {
6527 true
6528 }
6529 }
6530
6531 #[test]
6532 fn reconcile_preserves_reattached_child() {
6533 let probe = std::rc::Rc::new(std::cell::Cell::new(None));
6534 let mut tree = WidgetTree::new();
6535 let host = tree.add(StableChildHost {
6536 child: None,
6537 probe: probe.clone(),
6538 });
6539 tree.layout(SizeProposal::exact(100.0, 100.0));
6540 let child = probe.get().expect("child mounted");
6541 let total0 = tree.arena.len();
6542 for _ in 0..5 {
6543 tree.arena_mark_needs_rebuild_for_testing(host);
6544 tree.layout(SizeProposal::exact(100.0, 100.0));
6545 }
6546 assert!(
6547 tree.arena.is_active(child),
6548 "the re-attached child must survive every rebuild"
6549 );
6550 assert_eq!(tree.arena.len(), total0, "no growth — same child reused");
6551 }
6552
6553 #[derive(Debug)]
6555 struct Wrapper {
6556 child: WidgetId,
6557 }
6558 impl Widget for Wrapper {
6559 fn build(&mut self, _ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6560 vec![self.child]
6561 }
6562 fn layout_response(
6563 &self,
6564 p: SizeProposal,
6565 _c: &LayoutContext,
6566 ) -> crate::widget::LayoutResponse {
6567 p.resolve(10.0, 10.0).into()
6568 }
6569 }
6570
6571 #[derive(Debug)]
6575 struct ReparentHost {
6576 body: Option<WidgetId>,
6577 probe: std::rc::Rc<std::cell::Cell<Option<WidgetId>>>,
6578 }
6579 impl Widget for ReparentHost {
6580 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6581 let body = match self.body {
6582 Some(id) => id,
6583 None => {
6584 let id = ctx.add(FillWidget::new());
6585 self.body = Some(id);
6586 self.probe.set(Some(id));
6587 id
6588 }
6589 };
6590 vec![ctx.add(Wrapper { child: body })]
6591 }
6592 fn layout_response(
6593 &self,
6594 p: SizeProposal,
6595 _c: &LayoutContext,
6596 ) -> crate::widget::LayoutResponse {
6597 p.resolve(10.0, 10.0).into()
6598 }
6599 fn preserves_children_on_rebuild(&self) -> bool {
6600 true
6601 }
6602 }
6603
6604 #[test]
6605 fn reconcile_spares_reparented_survivor() {
6606 let probe = std::rc::Rc::new(std::cell::Cell::new(None));
6613 let mut tree = WidgetTree::new();
6614 let host = tree.add(ReparentHost {
6615 body: None,
6616 probe: probe.clone(),
6617 });
6618 tree.layout(SizeProposal::exact(100.0, 100.0));
6619 let body = probe.get().expect("body mounted");
6620 let total0 = tree.arena.len();
6621 for _ in 0..5 {
6622 tree.arena_mark_needs_rebuild_for_testing(host);
6623 tree.layout(SizeProposal::exact(100.0, 100.0));
6624 }
6625 assert!(
6626 tree.arena.is_active(body),
6627 "the re-parented body must survive — it was moved into the new tree, \
6628 not swept with the dropped wrapper"
6629 );
6630 assert_eq!(
6631 tree.arena.len(),
6632 total0,
6633 "dropped wrappers reaped — no per-rebuild leak"
6634 );
6635 }
6636
6637 #[derive(Debug)]
6647 struct BelowContainer {
6648 child: Option<WidgetId>,
6649 offset: f32,
6650 }
6651
6652 impl crate::widget::Widget for BelowContainer {
6653 fn layout_response(
6654 &self,
6655 proposal: SizeProposal,
6656 _ctx: &crate::widget::LayoutContext,
6657 ) -> crate::widget::LayoutResponse {
6658 proposal.resolve(0.0, 0.0).into()
6659 }
6660 fn place_children(
6661 &self,
6662 bounds: Rect,
6663 _proposal: SizeProposal,
6664 children: &mut [crate::widget::WidgetPlacement],
6665 _ctx: &crate::widget::LayoutContext,
6666 ) {
6667 for c in children.iter_mut() {
6668 c.origin = Point::new(bounds.x, bounds.y + self.offset);
6669 c.size = bounds.size();
6670 }
6671 }
6672 fn children(&self) -> Vec<WidgetId> {
6673 self.child.into_iter().collect()
6674 }
6675 }
6676
6677 fn recording_scroll_container(
6680 tree: &mut WidgetTree,
6681 child: WidgetId,
6682 recorded: std::rc::Rc<std::cell::Cell<Option<Rect>>>,
6683 ) -> WidgetId {
6684 use crate::test_widgets::StackWidget;
6685 tree.add(
6686 StackWidget::new()
6687 .child(child)
6688 .on_scroll(move |ev, _ctx| match ev {
6689 WidgetEvent::ScrollIntoView { target_bounds, .. } => {
6690 recorded.set(Some(*target_bounds));
6691 EventResponse::Handled
6692 }
6693 _ => EventResponse::Ignored,
6694 })
6695 .clips_children(true),
6696 )
6697 }
6698
6699 #[test]
6700 fn ensure_visible_dispatches_scroll_into_view_to_clipping_ancestor() {
6701 use std::cell::Cell;
6702 use std::rc::Rc;
6703 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6704 let mut tree = WidgetTree::new();
6705 let actor = tree.add(FillWidget::new());
6706 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
6707 tree.layout(SizeProposal::exact(100.0, 100.0));
6708
6709 let target = Rect::new(10.0, 500.0, 20.0, 15.0);
6712 let mut ctx = EventContext::new();
6713 ctx.ensure_visible(target);
6714 tree.collect_from_ctx(ctx, actor);
6715
6716 assert_eq!(
6717 recorded.get(),
6718 Some(target),
6719 "ensure_visible(rect) must dispatch ScrollIntoView with the exact rect \
6720 to the clips_children ancestor"
6721 );
6722 }
6723
6724 #[test]
6725 fn ensure_visible_is_noop_when_rect_already_visible() {
6726 use std::cell::Cell;
6727 use std::rc::Rc;
6728 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6729 let mut tree = WidgetTree::new();
6730 let actor = tree.add(FillWidget::new());
6731 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
6732 tree.layout(SizeProposal::exact(100.0, 100.0));
6733
6734 let mut ctx = EventContext::new();
6737 ctx.ensure_visible(Rect::new(10.0, 10.0, 20.0, 15.0));
6738 tree.collect_from_ctx(ctx, actor);
6739
6740 assert_eq!(
6741 recorded.get(),
6742 None,
6743 "a rect already inside the viewport must not trigger a scroll"
6744 );
6745 }
6746
6747 #[test]
6748 fn ensure_visible_margin_forces_scroll_near_edge() {
6749 use std::cell::Cell;
6750 use std::rc::Rc;
6751 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6752 let mut tree = WidgetTree::new();
6753 let actor = tree.add(FillWidget::new());
6754 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
6755 tree.layout(SizeProposal::exact(100.0, 100.0));
6756
6757 let rect = Rect::new(10.0, 95.0, 20.0, 4.0);
6760 let mut ctx = EventContext::new();
6761 ctx.ensure_visible_with_margin(rect, 10.0);
6762 tree.collect_from_ctx(ctx, actor);
6763
6764 assert_eq!(
6765 recorded.get(),
6766 Some(rect),
6767 "the margin must widen the visibility test so a near-edge rect scrolls"
6768 );
6769 }
6770
6771 fn recording_align_container(
6774 tree: &mut WidgetTree,
6775 child: WidgetId,
6776 recorded: std::rc::Rc<
6777 std::cell::Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>,
6778 >,
6779 ) -> WidgetId {
6780 use crate::test_widgets::StackWidget;
6781 tree.add(
6782 StackWidget::new()
6783 .child(child)
6784 .on_scroll(move |ev, _ctx| match ev {
6785 WidgetEvent::ScrollIntoView { align, motion, .. } => {
6786 recorded.set(Some((*align, *motion)));
6787 EventResponse::Handled
6788 }
6789 _ => EventResponse::Ignored,
6790 })
6791 .clips_children(true),
6792 )
6793 }
6794
6795 #[test]
6796 fn ensure_visible_aligned_scrolls_even_when_already_visible() {
6797 use std::cell::Cell;
6798 use std::rc::Rc;
6799 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6800 let mut tree = WidgetTree::new();
6801 let actor = tree.add(FillWidget::new());
6802 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
6803 tree.layout(SizeProposal::exact(100.0, 100.0));
6804
6805 let target = Rect::new(10.0, 10.0, 20.0, 15.0);
6810 let mut ctx = EventContext::new();
6811 ctx.ensure_visible_aligned(target, 0.5, crate::event::ScrollMotion::Instant);
6812 tree.collect_from_ctx(ctx, actor);
6813
6814 assert_eq!(
6815 recorded.get(),
6816 Some(target),
6817 "an aligned reveal must dispatch even when the rect is already visible"
6818 );
6819 }
6820
6821 #[test]
6822 fn ensure_visible_aligned_forwards_fraction_and_motion() {
6823 use std::cell::Cell;
6824 use std::rc::Rc;
6825 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6826 Rc::new(Cell::new(None));
6827 let mut tree = WidgetTree::new();
6828 let actor = tree.add(FillWidget::new());
6829 let _container = recording_align_container(&mut tree, actor, recorded.clone());
6830 tree.layout(SizeProposal::exact(100.0, 100.0));
6831
6832 let mut ctx = EventContext::new();
6833 ctx.ensure_visible_aligned(
6834 Rect::new(10.0, 10.0, 20.0, 15.0),
6835 0.25,
6836 crate::event::ScrollMotion::Smooth,
6837 );
6838 tree.collect_from_ctx(ctx, actor);
6839
6840 assert_eq!(
6841 recorded.get(),
6842 Some((
6843 crate::event::ScrollAlign::Fraction(0.25),
6844 crate::event::ScrollMotion::Smooth
6845 )),
6846 "the container must receive the requested fraction and motion verbatim"
6847 );
6848 }
6849
6850 #[test]
6851 fn ensure_visible_aligned_clamps_the_fraction() {
6852 use std::cell::Cell;
6853 use std::rc::Rc;
6854 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6855 Rc::new(Cell::new(None));
6856 let mut tree = WidgetTree::new();
6857 let actor = tree.add(FillWidget::new());
6858 let _container = recording_align_container(&mut tree, actor, recorded.clone());
6859 tree.layout(SizeProposal::exact(100.0, 100.0));
6860
6861 let mut ctx = EventContext::new();
6862 ctx.ensure_visible_aligned(
6863 Rect::new(10.0, 10.0, 20.0, 15.0),
6864 4.2,
6865 crate::event::ScrollMotion::Instant,
6866 );
6867 tree.collect_from_ctx(ctx, actor);
6868
6869 assert_eq!(
6870 recorded.get().map(|(a, _)| a),
6871 Some(crate::event::ScrollAlign::Fraction(1.0)),
6872 "an out-of-range fraction must clamp rather than aim the pin off-screen"
6873 );
6874 }
6875
6876 #[test]
6877 fn plain_ensure_visible_requests_minimal_alignment() {
6878 use std::cell::Cell;
6879 use std::rc::Rc;
6880 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6881 Rc::new(Cell::new(None));
6882 let mut tree = WidgetTree::new();
6883 let actor = tree.add(FillWidget::new());
6884 let _container = recording_align_container(&mut tree, actor, recorded.clone());
6885 tree.layout(SizeProposal::exact(100.0, 100.0));
6886
6887 let mut ctx = EventContext::new();
6888 ctx.ensure_visible(Rect::new(10.0, 500.0, 20.0, 15.0));
6889 tree.collect_from_ctx(ctx, actor);
6890
6891 assert_eq!(
6892 recorded.get(),
6893 Some((
6894 crate::event::ScrollAlign::Minimal,
6895 crate::event::ScrollMotion::Instant
6896 )),
6897 "the pre-existing reveal API must keep its exact semantics"
6898 );
6899 }
6900
6901 #[test]
6902 fn only_the_innermost_container_aligns() {
6903 use std::cell::Cell;
6904 use std::rc::Rc;
6905 let inner_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6906 Rc::new(Cell::new(None));
6907 let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6908 Rc::new(Cell::new(None));
6909
6910 let mut tree = WidgetTree::new();
6911 let actor = tree.add(FillWidget::new());
6912 let inner = recording_align_container(&mut tree, actor, inner_rec.clone());
6913 let _outer = recording_align_container(&mut tree, inner, outer_rec.clone());
6914 tree.layout(SizeProposal::exact(100.0, 100.0));
6915
6916 let mut ctx = EventContext::new();
6919 ctx.ensure_visible_aligned(
6920 Rect::new(10.0, 500.0, 20.0, 15.0),
6921 0.5,
6922 crate::event::ScrollMotion::Instant,
6923 );
6924 tree.collect_from_ctx(ctx, actor);
6925
6926 assert_eq!(
6927 inner_rec.get().map(|(a, _)| a),
6928 Some(crate::event::ScrollAlign::Fraction(0.5)),
6929 "the innermost clipping ancestor owns the pin"
6930 );
6931 assert_eq!(
6932 outer_rec.get().map(|(a, _)| a),
6933 Some(crate::event::ScrollAlign::Minimal),
6934 "an outer container must only bring the inner viewport into view — a \
6935 fraction names a height in one viewport, not in every ancestor's"
6936 );
6937 }
6938
6939 #[test]
6940 fn a_clipper_that_does_not_scroll_passes_the_pin_on() {
6941 use crate::test_widgets::StackWidget;
6948 use std::cell::Cell;
6949 use std::rc::Rc;
6950 let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6951 Rc::new(Cell::new(None));
6952
6953 let mut tree = WidgetTree::new();
6954 let actor = tree.add(FillWidget::new());
6955 let clipper = tree.add(StackWidget::new().child(actor).clips_children_on(true));
6956 let _scroller = recording_align_container(&mut tree, clipper, outer_rec.clone());
6957 tree.layout(SizeProposal::exact(100.0, 100.0));
6958
6959 let mut ctx = EventContext::new();
6962 ctx.ensure_visible_aligned(
6963 Rect::new(10.0, 10.0, 20.0, 15.0),
6964 0.5,
6965 crate::event::ScrollMotion::Instant,
6966 );
6967 tree.collect_from_ctx(ctx, actor);
6968
6969 assert_eq!(
6970 outer_rec.get().map(|(a, _)| a),
6971 Some(crate::event::ScrollAlign::Fraction(0.5)),
6972 "a clipping wrapper with no scroll handler must pass the pin through \
6973 to the scroll container above it"
6974 );
6975 }
6976
6977 #[test]
6978 fn a_scroller_that_declines_to_move_still_holds_the_pin() {
6979 use crate::test_widgets::StackWidget;
6984 use std::cell::Cell;
6985 use std::rc::Rc;
6986 let inner_rec: Rc<Cell<Option<crate::event::ScrollAlign>>> = Rc::new(Cell::new(None));
6987 let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6988 Rc::new(Cell::new(None));
6989
6990 let mut tree = WidgetTree::new();
6991 let actor = tree.add(FillWidget::new());
6992 let rec = inner_rec.clone();
6993 let inner = tree.add(
6994 StackWidget::new()
6995 .child(actor)
6996 .on_scroll(move |ev, _ctx| match ev {
6997 WidgetEvent::ScrollIntoView { align, .. } => {
6998 rec.set(Some(*align));
6999 EventResponse::Ignored
7000 }
7001 _ => EventResponse::Ignored,
7002 })
7003 .clips_children(true),
7004 );
7005 let _outer = recording_align_container(&mut tree, inner, outer_rec.clone());
7006 tree.layout(SizeProposal::exact(100.0, 100.0));
7007
7008 let mut ctx = EventContext::new();
7010 ctx.ensure_visible_aligned(
7011 Rect::new(10.0, 500.0, 20.0, 15.0),
7012 0.5,
7013 crate::event::ScrollMotion::Instant,
7014 );
7015 tree.collect_from_ctx(ctx, actor);
7016
7017 assert_eq!(
7018 inner_rec.get(),
7019 Some(crate::event::ScrollAlign::Fraction(0.5)),
7020 "the innermost scroller is offered the pin"
7021 );
7022 assert_eq!(
7023 outer_rec.get().map(|(a, _)| a),
7024 Some(crate::event::ScrollAlign::Minimal),
7025 "declining to move must not pass the pin outward"
7026 );
7027 }
7028
7029 #[test]
7030 fn ensure_widget_visible_uses_target_arena_bounds() {
7031 use std::cell::Cell;
7032 use std::rc::Rc;
7033 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
7034 let mut tree = WidgetTree::new();
7035 let target = tree.add(FillWidget::new());
7037 let rec = recorded.clone();
7038 let container = tree.add(
7039 BelowContainer {
7040 child: Some(target),
7041 offset: 500.0,
7042 }
7043 .on_scroll(move |ev, _ctx| match ev {
7044 WidgetEvent::ScrollIntoView { target_bounds, .. } => {
7045 rec.set(Some(*target_bounds));
7046 EventResponse::Handled
7047 }
7048 _ => EventResponse::Ignored,
7049 })
7050 .clips_children(true),
7051 );
7052 tree.layout(SizeProposal::exact(100.0, 100.0));
7053
7054 let expected = tree.bounds(target);
7055 assert!(
7056 expected.y > 100.0,
7057 "fixture sanity: the target must sit below the viewport (y={})",
7058 expected.y
7059 );
7060
7061 let mut ctx = EventContext::new();
7064 ctx.ensure_widget_visible(target);
7065 tree.collect_from_ctx(ctx, container);
7066
7067 assert_eq!(
7068 recorded.get(),
7069 Some(expected),
7070 "ensure_widget_visible(id) must dispatch ScrollIntoView with the \
7071 target's current arena bounds"
7072 );
7073 }
7074
7075 #[test]
7076 fn ensure_widget_visible_ignores_missing_widget() {
7077 use std::cell::Cell;
7079 use std::rc::Rc;
7080 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
7081 let mut tree = WidgetTree::new();
7082 let actor = tree.add(FillWidget::new());
7083 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
7084 tree.layout(SizeProposal::exact(100.0, 100.0));
7085
7086 let mut ctx = EventContext::new();
7087 ctx.ensure_widget_visible(WidgetId::default());
7088 tree.collect_from_ctx(ctx, actor); assert_eq!(
7091 recorded.get(),
7092 None,
7093 "an unmounted id must not trigger a scroll"
7094 );
7095 }
7096
7097 #[test]
7098 fn context_menu_inside_a_modal_keeps_the_modal() {
7099 use crate::event::{Modifiers, PointerButton, WidgetEvent};
7103 use crate::overlay::{DismissBehavior, OverlayPlacement, OverlayRequest};
7104 use crate::test_widgets::{FillWidget, StackWidget};
7105
7106 let mut tree = WidgetTree::new();
7107 let modal_content = tree.add(StackWidget::new());
7110 let _editor = tree.add_child(
7111 modal_content,
7112 FillWidget::new()
7113 .context_menu(|_pos, _ctx| Some(Box::new(FillWidget::new()) as Box<dyn Widget>)),
7114 );
7115 tree.layout(SizeProposal::exact(200.0, 100.0));
7116
7117 let modal = tree.overlay_manager.show(OverlayRequest::new(
7118 modal_content,
7119 modal_content,
7120 OverlayPlacement::Centered,
7121 DismissBehavior::EscapeKey,
7122 ));
7123 tree.overlay_manager
7125 .stack
7126 .iter_mut()
7127 .find(|o| o.id == modal)
7128 .unwrap()
7129 .bounds = Rect::new(0.0, 0.0, 200.0, 100.0);
7130 assert_eq!(tree.overlay_manager.len(), 1);
7131
7132 tree.dispatch_event(WidgetEvent::pointer_down(
7134 Point::new(50.0, 25.0),
7135 PointerButton::Secondary,
7136 Modifiers::NONE,
7137 ));
7138
7139 assert!(
7140 tree.overlay_manager.active_ids().contains(&modal),
7141 "the modal must survive opening a context menu inside it"
7142 );
7143 assert_eq!(
7144 tree.overlay_manager.len(),
7145 2,
7146 "the context menu should now be open on top of the surviving modal"
7147 );
7148 }
7149}
7150
7151#[cfg(test)]
7154mod input_ingress_tests {
7155 use super::*;
7156 use crate::event::{EventResponse, Modifiers, PointerButton, ScrollDelta};
7157 use crate::pointer::{
7158 EventTime, PointerId, PointerInfo, PointerPhase, PointerSample, ScrollPhase, ScrollSample,
7159 ScrollSource,
7160 };
7161 use crate::test_widgets::FillWidget;
7162 use crate::widget::{LayoutContext, WidgetPlacement};
7163 use crate::widget_builder::WidgetBuilder;
7164 use std::cell::RefCell;
7165 use std::rc::Rc;
7166
7167 #[derive(Debug)]
7174 struct RowWidget {
7175 children: Vec<WidgetId>,
7176 }
7177
7178 impl crate::widget::Widget for RowWidget {
7179 fn layout_response(
7180 &self,
7181 proposal: SizeProposal,
7182 _ctx: &LayoutContext,
7183 ) -> crate::widget::LayoutResponse {
7184 proposal.resolve(0.0, 0.0).into()
7185 }
7186
7187 fn place_children(
7188 &self,
7189 bounds: Rect,
7190 _proposal: SizeProposal,
7191 children: &mut [WidgetPlacement],
7192 _ctx: &LayoutContext,
7193 ) {
7194 let n = children.len().max(1) as f32;
7195 let w = bounds.width / n;
7196 for (i, child) in children.iter_mut().enumerate() {
7197 child.origin = Point::new(bounds.x + w * i as f32, bounds.y);
7198 child.size = teksilo_canvas::Size::new(w, bounds.height);
7199 }
7200 }
7201
7202 fn children(&self) -> Vec<WidgetId> {
7203 self.children.clone()
7204 }
7205 }
7206
7207 fn record(drive: impl FnOnce(&mut WidgetTree)) -> Vec<String> {
7210 let log: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(Vec::new()));
7211
7212 let mut tree = WidgetTree::new();
7213 let pointer_log = log.clone();
7214 let hover_log = log.clone();
7215 tree.add(
7216 FillWidget::new()
7217 .on_pointer_event(move |event, _ctx| {
7218 pointer_log.borrow_mut().push(format!("{event:?}"));
7219 EventResponse::Ignored
7220 })
7221 .on_hover(move |entered, _ctx| {
7222 hover_log.borrow_mut().push(format!("hover({entered})"));
7223 }),
7224 );
7225 tree.layout(SizeProposal::exact(100.0, 100.0));
7226
7227 drive(&mut tree);
7228 log.borrow().clone()
7229 }
7230
7231 #[test]
7237 fn a_mouse_sample_reproduces_todays_event_stream() {
7238 let inside = Point::new(40.0, 40.0);
7239 let moved = Point::new(60.0, 55.0);
7240 let outside = Point::new(400.0, 400.0);
7241
7242 let legacy = record(|tree| {
7243 tree.dispatch_event(WidgetEvent::pointer_move(inside));
7244 tree.dispatch_event(WidgetEvent::pointer_down(
7245 inside,
7246 PointerButton::Primary,
7247 Modifiers::NONE,
7248 ));
7249 tree.dispatch_event(WidgetEvent::pointer_move(moved));
7250 tree.dispatch_event(WidgetEvent::pointer_up(
7251 moved,
7252 PointerButton::Primary,
7253 Modifiers::NONE,
7254 ));
7255 tree.dispatch_event(WidgetEvent::pointer_move(outside));
7256 });
7257
7258 let sampled = record(|tree| {
7259 let t = EventTime::ZERO;
7260 tree.dispatch_pointer(PointerSample::mouse(PointerPhase::Move, inside, t));
7261 tree.dispatch_pointer(
7262 PointerSample::mouse(PointerPhase::Down, inside, t)
7263 .with_button(PointerButton::Primary),
7264 );
7265 tree.dispatch_pointer(PointerSample::mouse(PointerPhase::Move, moved, t));
7266 tree.dispatch_pointer(
7267 PointerSample::mouse(PointerPhase::Up, moved, t)
7268 .with_button(PointerButton::Primary),
7269 );
7270 tree.dispatch_pointer(PointerSample::mouse(PointerPhase::Move, outside, t));
7271 });
7272
7273 assert_eq!(legacy, sampled);
7274 assert!(
7275 legacy.contains(&"hover(true)".to_string())
7276 && legacy.contains(&"hover(false)".to_string()),
7277 "the fixture must actually exercise enter and leave: {legacy:?}"
7278 );
7279 }
7280
7281 #[test]
7285 fn a_buttonless_press_lowers_to_primary() {
7286 let events = record(|tree| {
7287 tree.dispatch_pointer(PointerSample::mouse(
7288 PointerPhase::Down,
7289 Point::new(20.0, 20.0),
7290 EventTime::ZERO,
7291 ));
7292 });
7293 assert!(
7294 events.iter().any(|e| e.contains("button: Primary")),
7295 "{events:?}"
7296 );
7297 }
7298
7299 #[test]
7305 fn a_sampled_press_and_release_form_a_tap() {
7306 let taps = Rc::new(std::cell::Cell::new(0));
7307 let seen = taps.clone();
7308 let mut tree = WidgetTree::new();
7309 tree.add(FillWidget::new().on_tap(move |_event, _ctx| seen.set(seen.get() + 1)));
7310 tree.layout(SizeProposal::exact(100.0, 100.0));
7311
7312 let at = Point::new(50.0, 50.0);
7313 tree.dispatch_pointer(PointerSample::mouse(
7314 PointerPhase::Down,
7315 at,
7316 EventTime::from_millis(0),
7317 ));
7318 tree.dispatch_pointer(PointerSample::mouse(
7319 PointerPhase::Up,
7320 at,
7321 EventTime::from_millis(5),
7322 ));
7323
7324 assert_eq!(taps.get(), 1);
7325 }
7326
7327 fn scroll_fixture() -> (WidgetTree, Rc<RefCell<Vec<&'static str>>>) {
7332 let hits: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
7333 let mut tree = WidgetTree::new();
7334
7335 let leading_hits = hits.clone();
7336 let trailing_hits = hits.clone();
7337 let leading = tree.add(FillWidget::new().on_scroll(move |_event, _ctx| {
7338 leading_hits.borrow_mut().push("leading");
7339 EventResponse::Handled
7340 }));
7341 let trailing = tree.add(FillWidget::new().on_scroll(move |_event, _ctx| {
7342 trailing_hits.borrow_mut().push("trailing");
7343 EventResponse::Handled
7344 }));
7345 tree.add(RowWidget {
7346 children: vec![leading, trailing],
7347 });
7348 tree.layout(SizeProposal::exact(200.0, 100.0));
7349 (tree, hits)
7350 }
7351
7352 fn notch() -> ScrollDelta {
7353 ScrollDelta::Lines { x: 0.0, y: -1.0 }
7354 }
7355
7356 #[test]
7359 fn a_positionless_scroll_routes_by_hover() {
7360 let (mut tree, hits) = scroll_fixture();
7361
7362 tree.pointer_move(Point::new(150.0, 50.0)); tree.dispatch_event(WidgetEvent::scroll(notch(), Modifiers::NONE));
7364 assert_eq!(*hits.borrow(), vec!["trailing"]);
7365
7366 tree.pointer_move(Point::new(50.0, 50.0)); tree.dispatch_scroll(ScrollSample::wheel(
7368 notch(),
7369 Modifiers::NONE,
7370 EventTime::ZERO,
7371 ));
7372 assert_eq!(*hits.borrow(), vec!["trailing", "leading"]);
7373 }
7374
7375 #[test]
7380 fn a_positioned_scroll_routes_by_hit_test() {
7381 let (mut tree, hits) = scroll_fixture();
7382 tree.pointer_move(Point::new(150.0, 50.0)); tree.dispatch_event(WidgetEvent::scroll_at(
7385 notch(),
7386 Modifiers::NONE,
7387 Point::new(50.0, 50.0), ));
7389 assert_eq!(*hits.borrow(), vec!["leading"]);
7390
7391 tree.dispatch_scroll(
7392 ScrollSample::wheel(notch(), Modifiers::NONE, EventTime::ZERO)
7393 .at(Point::new(150.0, 50.0)),
7394 );
7395 assert_eq!(*hits.borrow(), vec!["leading", "trailing"]);
7396 }
7397
7398 #[test]
7402 fn a_positionless_scroll_with_no_hover_goes_nowhere() {
7403 let (mut tree, hits) = scroll_fixture();
7404 tree.dispatch_event(WidgetEvent::scroll(notch(), Modifiers::NONE));
7405 assert!(hits.borrow().is_empty());
7406 }
7407
7408 #[test]
7413 fn a_handler_sees_the_sample_it_is_serving() {
7414 let seen: Rc<RefCell<Vec<(ScrollPhase, ScrollSource, Option<Point>)>>> =
7415 Rc::new(RefCell::new(Vec::new()));
7416 let sink = seen.clone();
7417
7418 let mut tree = WidgetTree::new();
7419 tree.add(
7426 FillWidget::new()
7427 .scroll_container(crate::pointer::touch_action::PanAxes::BOTH)
7428 .on_scroll(move |_event, ctx| {
7429 sink.borrow_mut().push((
7430 ctx.scroll_phase(),
7431 ctx.scroll_source(),
7432 ctx.pointer_position(),
7433 ));
7434 EventResponse::Handled
7435 }),
7436 );
7437 tree.layout(SizeProposal::exact(100.0, 100.0));
7438
7439 tree.dispatch_scroll(ScrollSample {
7440 delta: notch(),
7441 position: Some(Point::new(50.0, 50.0)),
7442 phase: ScrollPhase::Momentum,
7443 source: ScrollSource::TouchPan,
7444 pointer: PointerInfo::mouse(EventTime::from_millis(12)),
7445 modifiers: Modifiers::NONE,
7446 });
7447
7448 assert_eq!(
7449 *seen.borrow(),
7450 vec![(
7451 ScrollPhase::Momentum,
7452 ScrollSource::TouchPan,
7453 Some(Point::new(50.0, 50.0))
7454 )]
7455 );
7456 }
7457
7458 #[test]
7461 fn a_legacy_event_reports_the_mouse_at_the_epoch() {
7462 let seen: Rc<RefCell<Option<(PointerId, ScrollPhase)>>> = Rc::new(RefCell::new(None));
7463 let sink = seen.clone();
7464
7465 let mut tree = WidgetTree::new();
7466 tree.add(FillWidget::new().on_pointer_event(move |_event, ctx| {
7467 *sink.borrow_mut() = Some((ctx.pointer().id, ctx.scroll_phase()));
7468 EventResponse::Ignored
7469 }));
7470 tree.layout(SizeProposal::exact(100.0, 100.0));
7471 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(50.0, 50.0)));
7472
7473 assert_eq!(
7474 *seen.borrow(),
7475 Some((PointerId::MOUSE, ScrollPhase::Discrete))
7476 );
7477 }
7478
7479 #[test]
7483 fn the_snapshot_is_restored_after_a_dispatch() {
7484 let mut tree = WidgetTree::new();
7485 tree.add(FillWidget::new());
7486 tree.layout(SizeProposal::exact(100.0, 100.0));
7487
7488 let before = tree.current_input.clone();
7489 tree.dispatch_scroll(
7490 ScrollSample::wheel(notch(), Modifiers::NONE, EventTime::ZERO)
7491 .at(Point::new(10.0, 10.0)),
7492 );
7493 assert_eq!(tree.current_input, before);
7494 }
7495}
7496
7497#[cfg(test)]
7498mod press_and_focus_tests {
7499 use std::cell::{Cell, RefCell};
7504 use std::rc::Rc;
7505 use teksilo_canvas::{Point, SizeProposal};
7506
7507 use crate::WidgetId;
7508 use crate::event::{EventResponse, Key, Modifiers, PointerButton, WidgetEvent};
7509 use crate::focus::FocusOrigin;
7510 use crate::pointer::clock::ManualClock;
7511 use crate::pointer::touch_action::PanClaim;
7512 use crate::pointer::{
7513 BackendDeviceKey, EventTime, PointerId, PointerIdAllocator, PointerInfo, PointerPhase,
7514 PointerSample,
7515 };
7516 use crate::test_widgets::{FillWidget, StackWidget};
7517 use crate::widget_builder::WidgetBuilder;
7518 use crate::widget_tree::WidgetTree;
7519
7520 fn contact_id() -> PointerId {
7526 use std::sync::atomic::{AtomicU64, Ordering};
7527 static NEXT: AtomicU64 = AtomicU64::new(1);
7528 PointerIdAllocator::global().begin(
7529 BackendDeviceKey::new(0x0B11),
7530 NEXT.fetch_add(1, Ordering::Relaxed),
7531 )
7532 }
7533
7534 fn contact(id: PointerId, phase: PointerPhase, at: Point, t: EventTime) -> PointerSample {
7535 PointerSample {
7536 pointer: PointerInfo::touch(id, t),
7537 phase,
7538 position: at,
7539 button: None,
7540 modifiers: Modifiers::NONE,
7541 coalesced: Vec::new(),
7542 }
7543 }
7544
7545 fn tree_on_a_clock() -> (WidgetTree, Rc<ManualClock>) {
7548 let mut tree = WidgetTree::new();
7549 let clock = Rc::new(ManualClock::new(EventTime::ZERO));
7550 tree.set_input_clock(clock.clone());
7551 (tree, clock)
7552 }
7553
7554 fn tappable(tree: &mut WidgetTree) -> (WidgetId, crate::signal::Signal<bool>) {
7561 let id = tree.add(FillWidget::new().focusable().on_tap(|_e, _c| {}));
7562 let signal = tree.pressed_signal(id);
7563 (id, signal)
7564 }
7565
7566 #[test]
7573 fn a_pointer_origin_names_its_device() {
7574 use teksilo_tokens::{PenKind, PointerKind};
7575
7576 assert!(FocusOrigin::Pointer(PointerKind::Touch).is_pointer());
7577 assert!(FocusOrigin::Pointer(PointerKind::Pen(PenKind::Pen)).is_pointer());
7578 assert!(FocusOrigin::POINTER.is_pointer());
7579 assert!(!FocusOrigin::Keyboard.is_pointer());
7580 assert!(!FocusOrigin::Programmatic.is_pointer());
7581 assert!(!FocusOrigin::Accessibility.is_pointer());
7582
7583 assert_eq!(
7584 FocusOrigin::Pointer(PointerKind::Touch).pointer_kind(),
7585 Some(PointerKind::Touch),
7586 );
7587 assert_eq!(FocusOrigin::Keyboard.pointer_kind(), None);
7588 assert_eq!(
7589 FocusOrigin::POINTER.pointer_kind(),
7590 Some(PointerKind::Unknown),
7591 "a widget deriving its own origin says so rather than naming a device it never saw",
7592 );
7593 }
7594
7595 #[test]
7599 fn only_a_real_navigation_declares_a_modality() {
7600 use teksilo_tokens::PointerKind;
7601
7602 assert_eq!(FocusOrigin::Keyboard.focus_visible(), Some(true));
7603 assert_eq!(FocusOrigin::Accessibility.focus_visible(), Some(true));
7604 assert_eq!(
7605 FocusOrigin::Pointer(PointerKind::Touch).focus_visible(),
7606 Some(false),
7607 );
7608 assert_eq!(FocusOrigin::Programmatic.focus_visible(), None);
7609 }
7610
7611 #[test]
7614 fn the_tier_three_configs_are_unchanged() {
7615 let origin: crate::signal::Signal<Option<FocusOrigin>> =
7616 crate::signal::Signal::new(Some(FocusOrigin::Keyboard));
7617 let slider: crate::signal::Signal<Option<FocusOrigin>> = origin.clone();
7618 let splitter: crate::signal::Signal<Option<FocusOrigin>> = origin.clone();
7619 let segmented: crate::signal::Signal<Option<FocusOrigin>> = origin.clone();
7620 for field in [slider, splitter, segmented] {
7621 assert_eq!(field.get(), Some(FocusOrigin::Keyboard));
7622 }
7623 origin.set(Some(FocusOrigin::POINTER));
7624 assert_ne!(
7625 origin.get(),
7626 Some(FocusOrigin::Keyboard),
7627 "the `== Some(Keyboard)` test every consumer makes still discriminates",
7628 );
7629 }
7630
7631 #[test]
7642 fn a_touch_tap_after_keyboard_focus_leaves_no_ring() {
7643 let (mut tree, _clock) = tree_on_a_clock();
7644 let a = tree.add(FillWidget::new().focusable());
7645 let b = tree.add(FillWidget::new().focusable());
7646 let root = tree.add(SideBySide {
7647 children: vec![a, b],
7648 });
7649 tree.layout(SizeProposal::exact(200.0, 100.0));
7650 let _ = root;
7651
7652 let visible = tree.focus_visible_signal();
7653 tree.press_key(Key::Tab, Modifiers::NONE);
7654 assert_eq!(tree.focused(), Some(a));
7655 assert!(visible.get(), "keyboard navigation reveals the ring");
7656
7657 let id = contact_id();
7658 let at = tree.bounds(b).center();
7659 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
7660 tree.dispatch_pointer(contact(
7661 id,
7662 PointerPhase::Up,
7663 at,
7664 EventTime::from_millis(30),
7665 ));
7666
7667 assert_eq!(tree.focused(), Some(b), "the release moved focus");
7668 assert!(!visible.get(), "and the ring did not come with it");
7669 assert_eq!(
7670 tree.focus_origin(),
7671 Some(FocusOrigin::Pointer(teksilo_tokens::PointerKind::Touch)),
7672 );
7673 }
7674
7675 #[test]
7680 fn an_assistive_focus_reveals_the_ring() {
7681 let mut tree = WidgetTree::new();
7682 let a = tree.add(FillWidget::new().focusable());
7683 let b = tree.add(FillWidget::new().focusable());
7684 let root = tree.add(SideBySide {
7685 children: vec![a, b],
7686 });
7687 tree.layout(SizeProposal::exact(200.0, 50.0));
7688 let _ = root;
7689 let visible = tree.focus_visible_signal();
7690
7691 tree.click(a);
7692 assert_eq!(tree.focused(), Some(a));
7693 assert!(!visible.get(), "the click hid it");
7694
7695 tree.dispatch_event(WidgetEvent::AccessAction {
7696 target: Some(b),
7697 action: accesskit::Action::Focus,
7698 target_node: crate::accessibility::widget_id_to_node_id(b),
7699 data: None,
7700 });
7701 assert_eq!(tree.focused(), Some(b));
7702 assert!(visible.get(), "an assistive move is a navigation");
7703 assert_eq!(tree.focus_origin(), Some(FocusOrigin::Accessibility));
7704 }
7705
7706 #[test]
7711 fn a_programmatic_focus_leaves_the_modality_alone() {
7712 let mut tree = WidgetTree::new();
7713 let a = tree.add(FillWidget::new().focusable());
7714 let b = tree.add(FillWidget::new().focusable());
7715 let root = tree.add(SideBySide {
7716 children: vec![a, b],
7717 });
7718 tree.layout(SizeProposal::exact(200.0, 100.0));
7719 let _ = root;
7720 let visible = tree.focus_visible_signal();
7721
7722 tree.press_key(Key::Tab, Modifiers::NONE);
7723 assert_eq!(tree.focused(), Some(a));
7724 assert!(visible.get());
7725 tree.focus(b);
7726 assert!(
7727 visible.get(),
7728 "a scripted focus does not hide a keyboard ring"
7729 );
7730
7731 tree.click(a);
7732 assert!(!visible.get());
7733 tree.focus(b);
7734 assert!(
7735 !visible.get(),
7736 "and does not reveal one after a click either",
7737 );
7738 }
7739
7740 #[test]
7746 fn a_mouse_still_focuses_on_press() {
7747 let mut tree = WidgetTree::new();
7748 let w = tree.add(FillWidget::new().focusable());
7749 tree.layout(SizeProposal::exact(100.0, 50.0));
7750 let center = tree.bounds(w).center();
7751
7752 tree.dispatch_event(WidgetEvent::pointer_down(
7753 center,
7754 PointerButton::Primary,
7755 Modifiers::NONE,
7756 ));
7757 assert_eq!(
7758 tree.focused(),
7759 Some(w),
7760 "focus lands on the press for an indirect pointer",
7761 );
7762 }
7763
7764 #[test]
7766 fn a_finger_focuses_on_release() {
7767 let (mut tree, _clock) = tree_on_a_clock();
7768 let w = tree.add(FillWidget::new().focusable());
7769 tree.layout(SizeProposal::exact(100.0, 50.0));
7770 let center = tree.bounds(w).center();
7771
7772 let id = contact_id();
7773 tree.dispatch_pointer(contact(id, PointerPhase::Down, center, EventTime::ZERO));
7774 assert_eq!(
7775 tree.focused(),
7776 None,
7777 "a finger that has only landed has chosen nothing yet",
7778 );
7779
7780 tree.dispatch_pointer(contact(
7781 id,
7782 PointerPhase::Up,
7783 center,
7784 EventTime::from_millis(40),
7785 ));
7786 assert_eq!(tree.focused(), Some(w));
7787 assert_eq!(
7788 tree.focus_origin().and_then(FocusOrigin::pointer_kind),
7789 Some(teksilo_tokens::PointerKind::Touch),
7790 "the origin names the device that delivered it",
7791 );
7792 }
7793
7794 #[test]
7798 fn a_release_on_a_different_focusable_moves_no_focus() {
7799 let (mut tree, _clock) = tree_on_a_clock();
7800 let a = tree.add(FillWidget::new().focusable());
7801 let b = tree.add(FillWidget::new().focusable());
7802 let root = tree.add(SideBySide {
7803 children: vec![a, b],
7804 });
7805 tree.layout(SizeProposal::exact(200.0, 50.0));
7806 let _ = root;
7807
7808 let on_a = tree.bounds(a).center();
7809 let on_b = tree.bounds(b).center();
7810 let id = contact_id();
7811 tree.dispatch_pointer(contact(id, PointerPhase::Down, on_a, EventTime::ZERO));
7812 tree.dispatch_pointer(contact(
7813 id,
7814 PointerPhase::Move,
7815 on_b,
7816 EventTime::from_millis(20),
7817 ));
7818 tree.dispatch_pointer(contact(
7819 id,
7820 PointerPhase::Up,
7821 on_b,
7822 EventTime::from_millis(40),
7823 ));
7824
7825 assert_eq!(
7826 tree.focused(),
7827 None,
7828 "the guard is `the release landed on the same focusable as the press`",
7829 );
7830 }
7831
7832 #[test]
7839 fn a_mouse_press_visual_is_unchanged() {
7840 let mut tree = WidgetTree::new();
7841 let (w, pressed) = tappable(&mut tree);
7842 tree.layout(SizeProposal::exact(100.0, 50.0));
7843 let center = tree.bounds(w).center();
7844
7845 assert!(!pressed.get(), "not pressed at rest");
7846 tree.dispatch_event(WidgetEvent::pointer_down(
7847 center,
7848 PointerButton::Primary,
7849 Modifiers::NONE,
7850 ));
7851 assert!(pressed.get(), "an indirect pointer lights up on the press");
7852 assert!(
7853 !tree.press_pending(w),
7854 "and never waits: a mouse opens no pan session, so there is no \
7855 ambiguity to wait out",
7856 );
7857 assert_eq!(tree.pressed_by(w), Some(PointerId::MOUSE));
7858
7859 tree.dispatch_event(WidgetEvent::pointer_up(
7860 center,
7861 PointerButton::Primary,
7862 Modifiers::NONE,
7863 ));
7864 assert!(!pressed.get(), "the release clears it");
7865 assert_eq!(tree.pressed_by(w), None);
7866 }
7867
7868 #[test]
7872 fn a_slide_off_clears_the_visual_and_re_entry_restores_it() {
7873 let (mut tree, _clock) = tree_on_a_clock();
7874 let (w, pressed) = tappable(&mut tree);
7875 tree.layout(SizeProposal::exact(100.0, 200.0));
7876 let inside = tree.bounds(w).center();
7877 let outside = Point::new(inside.x, inside.y + 400.0);
7878
7879 let id = contact_id();
7880 tree.dispatch_pointer(contact(id, PointerPhase::Down, inside, EventTime::ZERO));
7881 assert!(
7882 pressed.get(),
7883 "nothing here claims a pan, so no delay applies"
7884 );
7885
7886 tree.dispatch_pointer(contact(
7887 id,
7888 PointerPhase::Move,
7889 outside,
7890 EventTime::from_millis(20),
7891 ));
7892 assert!(!pressed.get(), "the press has left its target");
7893 assert!(!tree.press_is_inside(w));
7894 assert_eq!(
7895 tree.pressed_by(w),
7896 Some(id),
7897 "the contact still holds the press — it is the *visual* that is off",
7898 );
7899
7900 tree.dispatch_pointer(contact(
7901 id,
7902 PointerPhase::Move,
7903 inside,
7904 EventTime::from_millis(40),
7905 ));
7906 assert!(pressed.get(), "sliding back on restores it");
7907
7908 tree.dispatch_pointer(contact(
7909 id,
7910 PointerPhase::Up,
7911 inside,
7912 EventTime::from_millis(60),
7913 ));
7914 assert!(!pressed.get());
7915 }
7916
7917 #[test]
7920 fn a_second_contact_cannot_clear_the_first_visual() {
7921 let (mut tree, _clock) = tree_on_a_clock();
7922 let (w, pressed) = tappable(&mut tree);
7923 tree.layout(SizeProposal::exact(100.0, 50.0));
7924 let bounds = tree.bounds(w);
7925 let first_at = Point::new(bounds.x + 20.0, bounds.y + 25.0);
7926 let second_at = Point::new(bounds.x + 70.0, bounds.y + 25.0);
7927
7928 let first = contact_id();
7929 let second = contact_id();
7930 tree.dispatch_pointer(contact(
7931 first,
7932 PointerPhase::Down,
7933 first_at,
7934 EventTime::ZERO,
7935 ));
7936 assert!(pressed.get());
7937 assert_eq!(tree.pressed_by(w), Some(first));
7938
7939 tree.dispatch_pointer(contact(
7940 second,
7941 PointerPhase::Down,
7942 second_at,
7943 EventTime::from_millis(10),
7944 ));
7945 assert_eq!(
7946 tree.pressed_by(w),
7947 Some(first),
7948 "under `MultiContact::First` the second contact is terminated before \
7949 it reaches the arena at all",
7950 );
7951
7952 tree.dispatch_pointer(contact(
7953 second,
7954 PointerPhase::Up,
7955 second_at,
7956 EventTime::from_millis(20),
7957 ));
7958 assert!(
7959 pressed.get(),
7960 "so the second contact's release cannot clear a visual it never owned",
7961 );
7962 assert_eq!(tree.pressed_by(w), Some(first));
7963
7964 tree.dispatch_pointer(contact(
7965 first,
7966 PointerPhase::Up,
7967 first_at,
7968 EventTime::from_millis(30),
7969 ));
7970 assert!(!pressed.get(), "the owner's release does clear it");
7971 }
7972
7973 #[test]
7976 fn a_cancel_clears_the_visual() {
7977 let (mut tree, _clock) = tree_on_a_clock();
7978 let (w, pressed) = tappable(&mut tree);
7979 tree.layout(SizeProposal::exact(100.0, 50.0));
7980 let center = tree.bounds(w).center();
7981
7982 let id = contact_id();
7983 tree.dispatch_pointer(contact(id, PointerPhase::Down, center, EventTime::ZERO));
7984 assert!(pressed.get());
7985
7986 tree.dispatch_pointer(contact(
7987 id,
7988 PointerPhase::Cancel,
7989 center,
7990 EventTime::from_millis(20),
7991 ));
7992 assert!(!pressed.get(), "a press that was taken away is not painted");
7993 assert_eq!(tree.pressed_by(w), None);
7994 }
7995
7996 #[test]
7999 fn a_pan_claim_clears_the_visual() {
8000 let (mut tree, clock) = tree_on_a_clock();
8001 let (row, pressed) = tappable(&mut tree);
8002 let list = tree.add(
8003 StackWidget::new()
8004 .child(row)
8005 .pan_claim(PanClaim::vertical())
8006 .on_scroll(|_e, _c| EventResponse::Handled),
8007 );
8008 tree.layout(SizeProposal::exact(200.0, 400.0));
8009 let _ = list;
8010 let start = Point::new(100.0, 200.0);
8011
8012 let id = contact_id();
8013 tree.dispatch_pointer(contact(id, PointerPhase::Down, start, EventTime::ZERO));
8014 assert!(tree.press_pending(row));
8016 clock.set(EventTime::from_millis(100));
8017 tree.tick_gestures(std::time::Instant::now());
8018 assert!(pressed.get(), "…and appears once the delay has elapsed");
8019
8020 tree.dispatch_pointer(contact(
8022 id,
8023 PointerPhase::Move,
8024 Point::new(100.0, 260.0),
8025 EventTime::from_millis(120),
8026 ));
8027 assert!(
8028 !pressed.get(),
8029 "the row lost the press to the list and must stop advertising it",
8030 );
8031 assert_eq!(tree.pressed_by(row), None);
8032 }
8033
8034 #[test]
8043 fn a_button_the_control_cannot_act_on_raises_no_visual() {
8044 let taps = Rc::new(Cell::new(0u32));
8045 let count = taps.clone();
8046 let mut tree = WidgetTree::new();
8047 let w = tree.add(FillWidget::new().focusable().on_tap(move |_e, _c| {
8048 count.set(count.get() + 1);
8049 }));
8050 let pressed = tree.pressed_signal(w);
8051 tree.layout(SizeProposal::exact(100.0, 50.0));
8052 let center = tree.bounds(w).center();
8053
8054 for button in [
8055 PointerButton::Middle,
8056 PointerButton::Back,
8057 PointerButton::Forward,
8058 ] {
8059 tree.dispatch_event(WidgetEvent::pointer_down(center, button, Modifiers::NONE));
8060 assert!(
8061 !pressed.get(),
8062 "{button:?} cannot activate an `on_tap` node, so it must not light one up",
8063 );
8064 assert_eq!(tree.pressed_by(w), None, "and owns no visual to clear");
8065 tree.dispatch_event(WidgetEvent::pointer_up(center, button, Modifiers::NONE));
8066 assert_eq!(
8067 taps.get(),
8068 0,
8069 "the tap recognizer refuses {button:?} too — that is the point",
8070 );
8071 }
8072
8073 tree.dispatch_event(WidgetEvent::pointer_down(
8075 center,
8076 PointerButton::Primary,
8077 Modifiers::NONE,
8078 ));
8079 assert!(pressed.get(), "a primary press lights up as it always has");
8080 assert_eq!(tree.pressed_by(w), Some(PointerId::MOUSE));
8081 tree.dispatch_event(WidgetEvent::pointer_up(
8082 center,
8083 PointerButton::Primary,
8084 Modifiers::NONE,
8085 ));
8086 assert!(!pressed.get());
8087 assert_eq!(taps.get(), 1);
8088 }
8089
8090 #[test]
8094 fn a_secondary_press_with_no_context_menu_raises_no_visual() {
8095 let mut tree = WidgetTree::new();
8096 let (w, pressed) = tappable(&mut tree);
8097 tree.layout(SizeProposal::exact(100.0, 50.0));
8098 let center = tree.bounds(w).center();
8099
8100 tree.dispatch_event(WidgetEvent::pointer_down(
8101 center,
8102 PointerButton::Secondary,
8103 Modifiers::NONE,
8104 ));
8105 assert!(
8106 !pressed.get(),
8107 "nothing opened, and nothing may look pressed either",
8108 );
8109 assert_eq!(tree.pressed_by(w), None);
8110 assert!(!tree.press_is_inside(w));
8111 }
8112
8113 #[test]
8117 fn a_widened_mask_widens_the_visual_with_it() {
8118 use crate::event::ButtonMask;
8119
8120 let mut tree = WidgetTree::new();
8121 let w = tree.add(
8122 FillWidget::new()
8123 .on_tap(|_e, _c| {})
8124 .accept_tap_buttons(ButtonMask::PRIMARY | ButtonMask::MIDDLE),
8125 );
8126 let pressed = tree.pressed_signal(w);
8127 tree.layout(SizeProposal::exact(100.0, 50.0));
8128 let center = tree.bounds(w).center();
8129
8130 tree.dispatch_event(WidgetEvent::pointer_down(
8131 center,
8132 PointerButton::Middle,
8133 Modifiers::NONE,
8134 ));
8135 assert!(pressed.get(), "this node really does act on a middle-click");
8136
8137 tree.dispatch_event(WidgetEvent::pointer_up(
8138 center,
8139 PointerButton::Middle,
8140 Modifiers::NONE,
8141 ));
8142 assert!(!pressed.get());
8143 }
8144
8145 #[test]
8151 fn a_press_that_raises_no_visual_still_defers_its_focus() {
8152 use teksilo_tokens::{PenKind, PointerKind};
8153
8154 let (mut tree, _clock) = tree_on_a_clock();
8155 let (w, pressed) = tappable(&mut tree);
8156 tree.layout(SizeProposal::exact(100.0, 50.0));
8157 let center = tree.bounds(w).center();
8158
8159 let id = contact_id();
8160 let barrel = |phase, t| PointerSample {
8161 pointer: PointerInfo {
8162 kind: PointerKind::Pen(PenKind::Pen),
8163 ..PointerInfo::touch(id, t)
8164 },
8165 phase,
8166 position: center,
8167 button: Some(PointerButton::Secondary),
8168 modifiers: Modifiers::NONE,
8169 coalesced: Vec::new(),
8170 };
8171
8172 tree.dispatch_pointer(barrel(PointerPhase::Down, EventTime::ZERO));
8173 assert!(!pressed.get(), "the barrel button activates nothing here");
8174 assert_eq!(tree.pressed_by(w), None);
8175 assert_eq!(
8176 tree.focused(),
8177 None,
8178 "a direct pointer has chosen nothing until it lifts",
8179 );
8180
8181 tree.dispatch_pointer(barrel(PointerPhase::Up, EventTime::from_millis(40)));
8182 assert_eq!(
8183 tree.focused(),
8184 Some(w),
8185 "the deferral is not button-gated: the release landed where the press did",
8186 );
8187 assert_eq!(
8188 tree.focus_origin().and_then(FocusOrigin::pointer_kind),
8189 Some(PointerKind::Pen(PenKind::Pen)),
8190 );
8191 }
8192
8193 #[test]
8200 fn the_feedback_delay_applies_only_inside_a_claimant() {
8201 let (mut tree, _clock) = tree_on_a_clock();
8203 let (w, pressed) = tappable(&mut tree);
8204 tree.layout(SizeProposal::exact(200.0, 400.0));
8205 let id = contact_id();
8206 let at = tree.bounds(w).center();
8207 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8208 assert!(
8209 !tree.press_pending(w),
8210 "no claimant above it, so nothing to rule out",
8211 );
8212 assert!(pressed.get());
8213
8214 let (mut tree, clock) = tree_on_a_clock();
8216 let (row, pressed) = tappable(&mut tree);
8217 let list = tree.add(
8218 StackWidget::new()
8219 .child(row)
8220 .pan_claim(PanClaim::vertical())
8221 .on_scroll(|_e, _c| EventResponse::Handled),
8222 );
8223 tree.layout(SizeProposal::exact(200.0, 400.0));
8224 let _ = list;
8225 let id = contact_id();
8226 let at = Point::new(100.0, 200.0);
8227 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8228 assert!(tree.press_pending(row), "a finger might be about to scroll");
8229 assert!(!pressed.get(), "so the row does not flash");
8230 assert!(
8231 tree.press_is_inside(row),
8232 "the press is real; only its visual is being withheld",
8233 );
8234
8235 clock.set(EventTime::from_millis(99));
8236 tree.tick_gestures(std::time::Instant::now());
8237 assert!(!pressed.get(), "99 ms is under the 100 ms delay");
8238
8239 clock.set(EventTime::from_millis(100));
8240 tree.tick_gestures(std::time::Instant::now());
8241 assert!(pressed.get(), "and 100 ms is it");
8242 assert!(!tree.press_pending(row));
8243 }
8244
8245 #[test]
8248 fn a_mouse_inside_a_claimant_never_waits() {
8249 let mut tree = WidgetTree::new();
8250 let (row, pressed) = tappable(&mut tree);
8251 let list = tree.add(
8252 StackWidget::new()
8253 .child(row)
8254 .pan_claim(PanClaim::vertical())
8255 .on_scroll(|_e, _c| EventResponse::Handled),
8256 );
8257 tree.layout(SizeProposal::exact(200.0, 400.0));
8258 let _ = list;
8259
8260 tree.dispatch_event(WidgetEvent::pointer_down(
8261 Point::new(100.0, 200.0),
8262 PointerButton::Primary,
8263 Modifiers::NONE,
8264 ));
8265 assert!(!tree.press_pending(row));
8266 assert!(pressed.get(), "Compact with a mouse is exactly as it was");
8267 }
8268
8269 #[test]
8276 fn a_handler_can_read_the_press_it_is_inside() {
8277 let (mut tree, _clock) = tree_on_a_clock();
8278 let seen: Rc<RefCell<Vec<(bool, bool, bool)>>> = Rc::new(RefCell::new(Vec::new()));
8279 let log = seen.clone();
8280 let row = tree.add(FillWidget::new().on_tap(|_e, _c| {}).on_pointer_event(
8281 move |event, ctx| {
8282 if matches!(event, WidgetEvent::PointerUp { .. }) {
8283 log.borrow_mut().push((
8284 ctx.is_pressed(),
8285 ctx.press_is_inside(),
8286 ctx.press_pending(),
8287 ));
8288 }
8289 EventResponse::Ignored
8290 },
8291 ));
8292 let list = tree.add(
8293 StackWidget::new()
8294 .child(row)
8295 .pan_claim(PanClaim::vertical())
8296 .on_scroll(|_e, _c| EventResponse::Handled),
8297 );
8298 tree.layout(SizeProposal::exact(200.0, 400.0));
8299 let _ = list;
8300
8301 let id = contact_id();
8302 let at = Point::new(100.0, 200.0);
8303 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8304 tree.dispatch_pointer(contact(
8305 id,
8306 PointerPhase::Up,
8307 at,
8308 EventTime::from_millis(30),
8309 ));
8310
8311 assert_eq!(
8312 seen.borrow().as_slice(),
8313 &[(false, true, true)],
8314 "released inside the claimant before the delay elapsed: inside, \
8315 pending, and therefore not yet showing",
8316 );
8317 }
8318
8319 #[test]
8322 fn a_handler_outside_a_press_reads_nothing() {
8323 let mut tree = WidgetTree::new();
8324 let asked = Rc::new(Cell::new(false));
8325 let flag = asked.clone();
8326 let w = tree.add(FillWidget::new().focusable().on_key(move |_e, ctx| {
8327 flag.set(ctx.is_pressed() || ctx.press_is_inside() || ctx.press_pending());
8328 EventResponse::Ignored
8329 }));
8330 tree.layout(SizeProposal::exact(100.0, 50.0));
8331 tree.focus(w);
8332 tree.press_key(Key::ArrowDown, Modifiers::NONE);
8333 assert!(!asked.get());
8334 }
8335
8336 #[test]
8340 fn a_completed_press_leaves_the_detector_clean() {
8341 let (mut tree, _clock) = tree_on_a_clock();
8342 let (w, pressed) = tappable(&mut tree);
8343 tree.layout(SizeProposal::exact(100.0, 50.0));
8344 let at = tree.bounds(w).center();
8345
8346 let id = contact_id();
8347 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8348 assert!(pressed.get());
8349 tree.dispatch_pointer(contact(
8350 id,
8351 PointerPhase::Up,
8352 at,
8353 EventTime::from_millis(30),
8354 ));
8355 tree.assert_no_leaked_pointer_state();
8356 }
8357
8358 #[test]
8362 fn the_detector_reports_a_press_still_held() {
8363 let (mut tree, _clock) = tree_on_a_clock();
8364 let (w, _pressed) = tappable(&mut tree);
8365 tree.layout(SizeProposal::exact(100.0, 50.0));
8366 let at = tree.bounds(w).center();
8367
8368 let id = contact_id();
8369 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8370 let held = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
8371 tree.assert_no_leaked_pointer_state()
8372 }));
8373 let message = *held
8374 .expect_err("a live press is leaked state")
8375 .downcast::<String>()
8376 .expect("the detector panics with a message");
8377 assert!(
8378 message.contains("is still pressed by"),
8379 "the press is named in the report, got: {message}",
8380 );
8381 }
8382
8383 #[derive(Debug)]
8387 struct SideBySide {
8388 children: Vec<WidgetId>,
8389 }
8390
8391 impl crate::widget::Widget for SideBySide {
8392 fn layout_response(
8393 &self,
8394 proposal: SizeProposal,
8395 _ctx: &crate::widget::LayoutContext,
8396 ) -> crate::widget::LayoutResponse {
8397 proposal.resolve(0.0, 0.0).into()
8398 }
8399
8400 fn place_children(
8401 &self,
8402 bounds: teksilo_canvas::Rect,
8403 _proposal: SizeProposal,
8404 children: &mut [crate::widget::WidgetPlacement],
8405 _ctx: &crate::widget::LayoutContext,
8406 ) {
8407 let n = children.len().max(1) as f32;
8408 let w = bounds.width / n;
8409 for (i, child) in children.iter_mut().enumerate() {
8410 child.origin = Point::new(bounds.x + w * i as f32, bounds.y);
8411 child.size = teksilo_canvas::Size::new(w, bounds.height);
8412 }
8413 }
8414
8415 fn children(&self) -> Vec<WidgetId> {
8416 self.children.clone()
8417 }
8418 }
8419}
8420
8421#[cfg(test)]
8422mod overlay_release_dismissal_tests {
8423 use std::cell::Cell;
8433 use std::rc::Rc;
8434
8435 use teksilo_canvas::{Point, Rect, Size, SizeProposal};
8436
8437 use crate::WidgetId;
8438 use crate::event::{Modifiers, PointerButton, WidgetEvent};
8439 use crate::overlay::{DismissBehavior, OverlayPlacement, OverlayRequest};
8440 use crate::pointer::{
8441 BackendDeviceKey, EventTime, PointerId, PointerIdAllocator, PointerInfo, PointerPhase,
8442 PointerSample,
8443 };
8444 use crate::test_widgets::FillWidget;
8445 use crate::widget::{LayoutContext, LayoutResponse, Widget};
8446 use crate::widget_builder::WidgetBuilder;
8447 use crate::widget_tree::WidgetTree;
8448
8449 #[derive(Debug)]
8456 struct Panel(Size);
8457
8458 impl Widget for Panel {
8459 fn layout_response(&self, _proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
8460 self.0.into()
8461 }
8462 }
8463
8464 #[derive(Debug)]
8472 struct PageAndTrigger {
8473 children: Vec<WidgetId>,
8474 }
8475
8476 impl Widget for PageAndTrigger {
8477 fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
8478 proposal.resolve(0.0, 0.0).into()
8479 }
8480
8481 fn place_children(
8482 &self,
8483 bounds: Rect,
8484 _proposal: SizeProposal,
8485 children: &mut [crate::widget::WidgetPlacement],
8486 _ctx: &LayoutContext,
8487 ) {
8488 if let Some(page) = children.get_mut(0) {
8489 page.origin = bounds.origin();
8490 page.size = bounds.size();
8491 }
8492 if let Some(trigger) = children.get_mut(1) {
8493 trigger.origin = bounds.origin();
8494 trigger.size = Size::new(10.0, 10.0);
8495 }
8496 }
8497
8498 fn children(&self) -> Vec<WidgetId> {
8499 self.children.clone()
8500 }
8501 }
8502
8503 fn contact_id(n: u64) -> PointerId {
8504 PointerIdAllocator::global().begin(BackendDeviceKey::new(0x0FA2), n)
8505 }
8506
8507 fn touch(id: PointerId, phase: PointerPhase, at: Point) -> PointerSample {
8508 PointerSample {
8509 pointer: PointerInfo::touch(id, EventTime::ZERO),
8510 phase,
8511 position: at,
8512 button: None,
8513 modifiers: Modifiers::NONE,
8514 coalesced: Vec::new(),
8515 }
8516 }
8517
8518 fn page_with_a_menu() -> (WidgetTree, WidgetId, Rc<Cell<u32>>) {
8525 let mut tree = WidgetTree::new();
8526 let taps = Rc::new(Cell::new(0u32));
8527 let counter = taps.clone();
8528 let page = tree.add(
8529 FillWidget::new()
8530 .focusable()
8531 .on_tap(move |_e, _c| counter.set(counter.get() + 1)),
8532 );
8533 let trigger = tree.add(Panel(Size::new(10.0, 10.0)));
8536 let _root = tree.add(PageAndTrigger {
8537 children: vec![page, trigger],
8538 });
8539 let menu = tree.add(Panel(Size::new(200.0, 200.0)));
8540 tree.show_overlay(OverlayRequest::new(
8541 menu,
8542 trigger,
8543 OverlayPlacement::AtPointer(Point::new(100.0, 100.0)),
8544 DismissBehavior::ClickOutside,
8545 ));
8546 tree.layout(SizeProposal::exact(800.0, 600.0));
8547 assert_eq!(tree.active_overlays().len(), 1, "the menu is open");
8548 (tree, page, taps)
8549 }
8550
8551 fn outside() -> Point {
8552 Point::new(600.0, 500.0)
8553 }
8554
8555 fn inside_menu() -> Point {
8556 Point::new(150.0, 150.0)
8557 }
8558
8559 #[test]
8566 fn the_suppressed_down_leaves_nothing_pressed_or_captured_beneath() {
8567 let (mut tree, page, taps) = page_with_a_menu();
8568 let finger = contact_id(1);
8569 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8570
8571 assert_eq!(
8572 tree.active_overlays().len(),
8573 1,
8574 "the press does not close the menu; the release does"
8575 );
8576 assert_eq!(tree.pressed_by(page), None, "nothing beneath is pressed");
8577 let entry = tree.pointers.get(finger).expect("the contact is live");
8578 assert_eq!(entry.captured_by, None, "nothing beneath captured it");
8579 assert!(entry.sequence.is_none(), "no arbitration was opened");
8580 assert_eq!(taps.get(), 0);
8581 }
8582
8583 #[test]
8585 fn a_touch_tap_outside_a_menu_closes_it_and_actuates_nothing() {
8586 let (mut tree, page, taps) = page_with_a_menu();
8587 let finger = contact_id(2);
8588 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8589 tree.dispatch_pointer(touch(finger, PointerPhase::Up, outside()));
8590
8591 assert!(tree.active_overlays().is_empty(), "the menu closed");
8592 assert_eq!(taps.get(), 0, "the page beneath was never tapped");
8593 assert_eq!(tree.pressed_by(page), None);
8594 tree.assert_no_leaked_pointer_state();
8595 }
8596
8597 #[test]
8600 fn a_cancelled_press_aborts_the_arm() {
8601 let (mut tree, page, taps) = page_with_a_menu();
8602 let finger = contact_id(3);
8603 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8604 tree.dispatch_pointer(touch(finger, PointerPhase::Cancel, outside()));
8605
8606 assert_eq!(tree.active_overlays().len(), 1, "the menu survives");
8607 assert_eq!(taps.get(), 0);
8608 assert_eq!(tree.pressed_by(page), None);
8609 tree.assert_no_leaked_pointer_state();
8610 }
8611
8612 #[test]
8616 fn a_press_slid_onto_the_menu_dismisses_nothing() {
8617 let (mut tree, _page, taps) = page_with_a_menu();
8618 let finger = contact_id(4);
8619 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8620 tree.dispatch_pointer(touch(finger, PointerPhase::Move, inside_menu()));
8621 tree.dispatch_pointer(touch(finger, PointerPhase::Up, inside_menu()));
8622
8623 assert_eq!(tree.active_overlays().len(), 1, "the menu survives");
8624 assert_eq!(taps.get(), 0);
8625 tree.assert_no_leaked_pointer_state();
8626 }
8627
8628 #[test]
8631 fn a_second_contact_cannot_dismiss_what_the_first_is_manipulating() {
8632 let (mut tree, _page, _taps) = page_with_a_menu();
8633 let first = contact_id(5);
8634 let second = contact_id(6);
8635
8636 tree.dispatch_pointer(touch(first, PointerPhase::Down, inside_menu()));
8637 assert!(
8638 tree.pointers
8639 .get(first)
8640 .is_some_and(|e| e.sequence.is_some()),
8641 "the first contact holds a live press inside the menu"
8642 );
8643
8644 tree.dispatch_pointer(touch(second, PointerPhase::Down, outside()));
8645 tree.dispatch_pointer(touch(second, PointerPhase::Up, outside()));
8646 assert_eq!(
8647 tree.active_overlays().len(),
8648 1,
8649 "the menu the first finger is holding must not close under it"
8650 );
8651
8652 tree.dispatch_pointer(touch(first, PointerPhase::Up, inside_menu()));
8655 let third = contact_id(7);
8656 tree.dispatch_pointer(touch(third, PointerPhase::Down, outside()));
8657 tree.dispatch_pointer(touch(third, PointerPhase::Up, outside()));
8658 assert!(tree.active_overlays().is_empty());
8659 tree.assert_no_leaked_pointer_state();
8660 }
8661
8662 #[test]
8666 fn an_arm_is_not_itself_a_press_that_blocks_another_contact() {
8667 let (mut tree, _page, _taps) = page_with_a_menu();
8668 let first = contact_id(8);
8669 let second = contact_id(9);
8670
8671 tree.dispatch_pointer(touch(first, PointerPhase::Down, outside()));
8672 assert!(
8673 tree.overlay_manager().has_armed_dismiss(first),
8674 "the first contact armed"
8675 );
8676 assert!(tree.busy_press_points(second).is_empty());
8678
8679 tree.dispatch_pointer(touch(second, PointerPhase::Down, outside()));
8680 assert!(tree.overlay_manager().has_armed_dismiss(second));
8681 tree.dispatch_pointer(touch(second, PointerPhase::Up, outside()));
8682 assert!(tree.active_overlays().is_empty());
8683
8684 tree.dispatch_pointer(touch(first, PointerPhase::Up, outside()));
8687 tree.assert_no_leaked_pointer_state();
8688 }
8689
8690 #[test]
8693 fn a_touch_inside_the_menu_arms_nothing() {
8694 let (mut tree, _page, _taps) = page_with_a_menu();
8695 let finger = contact_id(10);
8696 tree.dispatch_pointer(touch(finger, PointerPhase::Down, inside_menu()));
8697 assert!(!tree.overlay_manager().has_armed_dismiss(finger));
8698 tree.dispatch_pointer(touch(finger, PointerPhase::Up, inside_menu()));
8699 assert_eq!(tree.active_overlays().len(), 1);
8700 tree.assert_no_leaked_pointer_state();
8701 }
8702
8703 #[test]
8713 fn a_contact_with_no_press_still_ends_when_the_platform_revokes_it() {
8714 let mut tree = WidgetTree::new();
8715 tree.layout(SizeProposal::exact(800.0, 600.0));
8716 let finger = contact_id(99);
8717 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8718 tree.dispatch_pointer(touch(finger, PointerPhase::Cancel, outside()));
8719 tree.assert_no_leaked_pointer_state();
8720 }
8721
8722 #[test]
8729 fn a_mouse_click_outside_a_menu_dismisses_on_the_press_and_falls_through() {
8730 let (mut tree, _page, taps) = page_with_a_menu();
8731 tree.dispatch_event(WidgetEvent::pointer_down(
8732 outside(),
8733 PointerButton::Primary,
8734 Modifiers::NONE,
8735 ));
8736 assert!(
8737 tree.active_overlays().is_empty(),
8738 "the mouse still dismisses on the press"
8739 );
8740 assert!(
8741 tree.pointers
8742 .get(PointerId::MOUSE)
8743 .is_some_and(|e| e.sequence.is_some()),
8744 "and the press still reaches the page beneath"
8745 );
8746 tree.dispatch_event(WidgetEvent::pointer_up(
8747 outside(),
8748 PointerButton::Primary,
8749 Modifiers::NONE,
8750 ));
8751 assert_eq!(taps.get(), 1, "the control beneath activated");
8752 assert!(
8753 !tree.overlay_manager().has_armed_dismiss(PointerId::MOUSE),
8754 "a mouse never arms"
8755 );
8756 tree.assert_no_leaked_pointer_state();
8757 }
8758
8759 #[test]
8766 fn a_context_menu_avoids_the_contact_that_opened_it() {
8767 fn menu_bounds(pointer: PointerInfo, at: Point) -> Rect {
8768 let mut tree = WidgetTree::new();
8769 let page = tree.add(
8770 FillWidget::new()
8771 .focusable()
8772 .context_menu(|_p, _ctx| Some(Box::new(Panel(Size::new(200.0, 160.0))))),
8773 );
8774 let _ = page;
8775 tree.layout(SizeProposal::exact(800.0, 600.0));
8776 tree.dispatch_pointer(PointerSample {
8777 pointer,
8778 phase: PointerPhase::Down,
8779 position: at,
8780 button: Some(PointerButton::Secondary),
8781 modifiers: Modifiers::NONE,
8782 coalesced: Vec::new(),
8783 });
8784 tree.layout(SizeProposal::exact(800.0, 600.0));
8785 let id = *tree
8786 .active_overlays()
8787 .first()
8788 .expect("the context menu opened");
8789 tree.overlay_manager().bounds_for(id).expect("bounds")
8790 }
8791
8792 let at = Point::new(400.0, 300.0);
8793 let finger = menu_bounds(PointerInfo::touch(contact_id(11), EventTime::ZERO), at);
8794 let contact = crate::overlay::rect_centred_on(at, crate::overlay::ASSUMED_CONTACT_PATCH);
8795 assert!(
8796 finger.x < contact.x && finger.right() <= contact.x,
8797 "a touch menu clears the contact patch: {finger:?} vs {contact:?}"
8798 );
8799
8800 let mouse = menu_bounds(PointerInfo::mouse(EventTime::ZERO), at);
8801 assert_eq!(
8802 (mouse.x, mouse.y),
8803 (at.x, at.y),
8804 "a mouse menu still opens with its corner on the pointer"
8805 );
8806 }
8807}
8808
8809#[cfg(test)]
8825mod pass_through_overlay_tests {
8826 use teksilo_canvas::{Point, Rect, SizeProposal};
8827
8828 use crate::WidgetId;
8829 use crate::overlay::{DismissBehavior, OverlayPlacement, OverlayRequest};
8830 use crate::test_widgets::FillWidget;
8831 use crate::widget::{LayoutContext, LayoutResponse, Widget};
8832 use crate::widget_builder::WidgetBuilder;
8833 use crate::widget_tree::WidgetTree;
8834
8835 #[derive(Debug)]
8838 struct HandleLayer {
8839 handle: WidgetId,
8840 at: Rect,
8841 }
8842
8843 impl Widget for HandleLayer {
8844 fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
8845 proposal.resolve(0.0, 0.0).into()
8846 }
8847
8848 fn place_children(
8849 &self,
8850 _bounds: Rect,
8851 _proposal: SizeProposal,
8852 children: &mut [crate::widget::WidgetPlacement],
8853 _ctx: &LayoutContext,
8854 ) {
8855 if let Some(child) = children.get_mut(0) {
8856 child.origin = self.at.origin();
8857 child.size = self.at.size();
8858 }
8859 }
8860
8861 fn children(&self) -> Vec<WidgetId> {
8862 vec![self.handle]
8863 }
8864 }
8865
8866 const HANDLE: Rect = Rect {
8867 x: 10.0,
8868 y: 10.0,
8869 width: 20.0,
8870 height: 20.0,
8871 };
8872
8873 #[derive(Clone, Copy, PartialEq)]
8875 enum RootFlag {
8876 PassThrough,
8878 HitTransparent,
8881 }
8882
8883 fn field_under_a_layer(flag: RootFlag) -> (WidgetTree, WidgetId, WidgetId) {
8889 let mut tree = WidgetTree::new();
8890 let field = tree.add(FillWidget::new());
8891 let handle = tree.add(FillWidget::new());
8892 let layer = HandleLayer { handle, at: HANDLE };
8893 let layer = match flag {
8894 RootFlag::PassThrough => tree.add(layer.event_pass_through(true)),
8895 RootFlag::HitTransparent => tree.add(layer.hit_transparent(true)),
8896 };
8897 tree.show_overlay(OverlayRequest::new(
8898 layer,
8899 field,
8900 OverlayPlacement::FullViewport,
8901 DismissBehavior::Manual,
8902 ));
8903 tree.layout(SizeProposal::exact(400.0, 200.0));
8904 assert_eq!(tree.active_overlays().len(), 1, "the layer is up");
8905 (tree, field, handle)
8906 }
8907
8908 #[test]
8910 fn a_pass_through_layer_hands_back_what_it_did_not_claim() {
8911 let (tree, field, handle) = field_under_a_layer(RootFlag::PassThrough);
8912
8913 assert_eq!(
8914 tree.hit_test(Point::new(15.0, 15.0)),
8915 Some(handle),
8916 "the layer must still win the point its own child covers"
8917 );
8918 assert_eq!(
8919 tree.hit_test(Point::new(200.0, 100.0)),
8920 Some(field),
8921 "a press the layer did not claim must reach the surface beneath it"
8922 );
8923 }
8924
8925 #[test]
8936 fn a_pass_through_root_still_hears_a_press_that_landed_on_its_child() {
8937 use std::cell::Cell;
8938 use std::rc::Rc;
8939
8940 let seen = Rc::new(Cell::new(0u32));
8941 let counter = seen.clone();
8942 let mut tree = WidgetTree::new();
8943 let field = tree.add(FillWidget::new());
8944 let handle = tree.add(FillWidget::new().on_tap(|_e, _c| {}));
8945 let layer = tree.add(
8946 HandleLayer { handle, at: HANDLE }
8947 .event_pass_through(true)
8948 .on_pointer_event(move |_event, _ctx| {
8949 counter.set(counter.get() + 1);
8950 crate::event::EventResponse::Ignored
8951 }),
8952 );
8953 tree.show_overlay(OverlayRequest::new(
8954 layer,
8955 field,
8956 OverlayPlacement::FullViewport,
8957 DismissBehavior::Manual,
8958 ));
8959 tree.layout(SizeProposal::exact(400.0, 200.0));
8960
8961 tree.pointer_down_button(Point::new(15.0, 15.0), crate::event::PointerButton::Primary);
8962 assert!(
8963 seen.get() > 0,
8964 "a pass-through root heard nothing about a press on its own child"
8965 );
8966
8967 let after_child = seen.get();
8968 tree.pointer_up_button(Point::new(15.0, 15.0), crate::event::PointerButton::Primary);
8969 tree.pointer_down_button(
8972 Point::new(200.0, 100.0),
8973 crate::event::PointerButton::Primary,
8974 );
8975 assert_eq!(
8976 seen.get(),
8977 after_child + 1,
8978 "the press-and-lift on the child accounts for the count, and the \
8979 press on the surface beneath added nothing"
8980 );
8981 }
8982
8983 #[test]
8997 fn a_hit_transparent_root_does_not_get_the_fall_through() {
8998 let (tree, field, _handle) = field_under_a_layer(RootFlag::HitTransparent);
8999
9000 assert_eq!(
9002 tree.hit_test(Point::new(15.0, 15.0)),
9003 None,
9004 "hit_transparent must exclude the subtree, or this fixture is not \
9005 testing the gate"
9006 );
9007 assert_eq!(
9008 tree.hit_test(Point::new(200.0, 100.0)),
9009 None,
9010 "the fall-through reached past an overlay that did not ask for it"
9011 );
9012 let _ = field;
9013 }
9014}