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 { origin } => {
2447 let previous_origin = ctx.focus_origin.replace(*origin);
2451 let mut fired = false;
2452 if let Some(h) = node.external_handlers.on_focus.as_mut() {
2453 h(true, ctx);
2454 fired = true;
2455 }
2456 if let Some(h) = node.handlers.on_focus.as_mut() {
2457 h(true, ctx);
2458 fired = true;
2459 }
2460 ctx.focus_origin = previous_origin;
2461 fired.then_some(EventResponse::Handled)
2462 }
2463 WidgetEvent::FocusLost => {
2464 let mut fired = false;
2465 if let Some(h) = node.external_handlers.on_focus.as_mut() {
2466 h(false, ctx);
2467 fired = true;
2468 }
2469 if let Some(h) = node.handlers.on_focus.as_mut() {
2470 h(false, ctx);
2471 fired = true;
2472 }
2473 fired.then_some(EventResponse::Handled)
2474 }
2475 WidgetEvent::KeyDown { .. }
2476 | WidgetEvent::KeyUp { .. }
2477 | WidgetEvent::ImeComposition { .. }
2478 | WidgetEvent::ImeCommit { .. } => {
2479 if node.external_handlers.on_key.is_some() || node.handlers.on_key.is_some() {
2480 Some(fire_event_handler_both(
2481 &mut node.external_handlers.on_key,
2482 &mut node.handlers.on_key,
2483 event,
2484 ctx,
2485 ))
2486 } else {
2487 None
2488 }
2489 }
2490 WidgetEvent::Scroll { .. } | WidgetEvent::ScrollIntoView { .. } => {
2491 if node.external_handlers.on_scroll.is_some() || node.handlers.on_scroll.is_some() {
2492 Some(fire_event_handler_both(
2493 &mut node.external_handlers.on_scroll,
2494 &mut node.handlers.on_scroll,
2495 event,
2496 ctx,
2497 ))
2498 } else {
2499 None
2500 }
2501 }
2502 WidgetEvent::AccessAction {
2503 action,
2504 target_node,
2505 data,
2506 ..
2507 } => {
2508 let saved_a11y_source = ctx
2528 .current_source
2529 .replace(crate::telemetry::IntentSource::Accessibility);
2530 let mut any_slot = false;
2531 let mut any_handled = false;
2532 if let Some(h) = node.external_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.handlers.on_access_action_request.as_mut() {
2538 any_slot = true;
2539 any_handled |=
2540 h(*action, *target_node, data.clone(), ctx) == EventResponse::Handled;
2541 }
2542 if let Some(h) = node.external_handlers.on_access_action.as_mut() {
2543 any_slot = true;
2544 any_handled |= h(*action, ctx) == EventResponse::Handled;
2545 }
2546 if let Some(h) = node.handlers.on_access_action.as_mut() {
2547 any_slot = true;
2548 any_handled |= h(*action, ctx) == EventResponse::Handled;
2549 }
2550 let user_handled = any_slot.then_some(if any_handled {
2551 EventResponse::Handled
2552 } else {
2553 EventResponse::Ignored
2554 });
2555
2556 let mut override_handled = false;
2562 if let Some(ov) = node.access_overrides.as_deref_mut() {
2563 if matches!(action, accesskit::Action::CustomAction) {
2564 if let Some(accesskit::ActionData::CustomAction(idx)) = data
2565 && let Some((_, cb)) = ov.custom_actions.get_mut(*idx as usize)
2566 {
2567 cb(ctx);
2568 override_handled = true;
2569 }
2570 } else {
2571 for (a, cb) in ov.actions.iter_mut() {
2572 if *a == *action {
2573 cb(ctx);
2574 override_handled = true;
2575 }
2576 }
2577 }
2578 }
2579
2580 ctx.current_source = saved_a11y_source;
2581 match (user_handled, override_handled) {
2582 (Some(EventResponse::Handled), _) | (_, true) => Some(EventResponse::Handled),
2583 (Some(EventResponse::Ignored), false) => Some(EventResponse::Ignored),
2584 (None, false) => None,
2585 }
2586 }
2587 WidgetEvent::Gesture { gesture } => {
2588 let matched = matches!(
2592 gesture,
2593 GestureEvent::PinchStarted { .. }
2594 | GestureEvent::PinchChanged { .. }
2595 | GestureEvent::PinchEnded
2596 | GestureEvent::Swipe { .. }
2597 | GestureEvent::DoubleTap { .. }
2598 | GestureEvent::TripleTap { .. }
2599 ) && {
2600 let has_handler = match gesture {
2601 GestureEvent::PinchStarted { .. }
2602 | GestureEvent::PinchChanged { .. }
2603 | GestureEvent::PinchEnded => node.any_handler(|h| h.on_pinch.is_some()),
2604 GestureEvent::Swipe { .. } => node.any_handler(|h| h.on_swipe.is_some()),
2605 GestureEvent::DoubleTap { .. } => {
2606 node.any_handler(|h| h.on_double_tap.is_some())
2607 }
2608 GestureEvent::TripleTap { .. } => {
2609 node.any_handler(|h| h.on_triple_tap.is_some())
2610 }
2611 _ => false,
2612 };
2613 if has_handler {
2614 Self::dispatch_recognized_gesture(node, *gesture, ctx);
2615 }
2616 has_handler
2617 };
2618 if matched {
2619 Some(EventResponse::Handled)
2620 } else {
2621 None
2622 }
2623 }
2624 WidgetEvent::PointerDown {
2625 position,
2626 button,
2627 modifiers,
2628 ..
2629 } => {
2630 if fire_on_pointer_event {
2637 let r = fire_event_handler_both(
2638 &mut node.external_handlers.on_pointer_event,
2639 &mut node.handlers.on_pointer_event,
2640 event,
2641 ctx,
2642 );
2643 if r == EventResponse::Handled {
2644 return Some(EventResponse::Handled);
2645 }
2646 }
2647 if arena_blocked {
2648 return None;
2652 }
2653 Self::ensure_gesture_arena(node, node_id, gesture_owners);
2654 if let Some(arena) = node.handlers.gesture_arena.as_mut() {
2655 let cx = gesture_cx;
2656 ctx.capture_pointer_implicit();
2671 let result = arena.process(
2672 &RawPointerEvent::Down {
2673 position: *position,
2674 button: *button,
2675 modifiers: *modifiers,
2676 pointer: cx.pointer,
2677 time: cx.now,
2678 },
2679 &cx,
2680 );
2681 if let Some(gesture) = result {
2682 Self::dispatch_recognized_gesture(node, gesture, ctx);
2683 }
2684 return Some(EventResponse::Handled);
2685 }
2686 None
2687 }
2688 WidgetEvent::PointerUp {
2689 position,
2690 button,
2691 modifiers,
2692 ..
2693 } => {
2694 if fire_on_pointer_event {
2695 let r = fire_event_handler_both(
2696 &mut node.external_handlers.on_pointer_event,
2697 &mut node.handlers.on_pointer_event,
2698 event,
2699 ctx,
2700 );
2701 if r == EventResponse::Handled {
2702 return Some(EventResponse::Handled);
2703 }
2704 }
2705 if arena_blocked {
2706 return None;
2707 }
2708 if let Some(arena) = node.handlers.gesture_arena.as_mut() {
2709 let cx = gesture_cx;
2710 let result = arena.process(
2711 &RawPointerEvent::Up {
2712 position: *position,
2713 button: *button,
2714 modifiers: *modifiers,
2715 pointer: cx.pointer,
2716 time: cx.now,
2717 },
2718 &cx,
2719 );
2720 if let Some(gesture) = result {
2721 Self::dispatch_recognized_gesture(node, gesture, ctx);
2722 }
2723 return Some(EventResponse::Handled);
2724 }
2725 None
2726 }
2727 WidgetEvent::PointerMove { position, .. } => {
2728 if fire_on_pointer_event {
2729 let r = fire_event_handler_both(
2730 &mut node.external_handlers.on_pointer_event,
2731 &mut node.handlers.on_pointer_event,
2732 event,
2733 ctx,
2734 );
2735 if r == EventResponse::Handled {
2736 return Some(EventResponse::Handled);
2737 }
2738 }
2739 if arena_blocked {
2740 return None;
2741 }
2742 if let Some(arena) = node.handlers.gesture_arena.as_mut() {
2743 let cx = gesture_cx;
2744 let result = arena.process(
2745 &RawPointerEvent::Move {
2746 position: *position,
2747 pointer: cx.pointer,
2748 time: cx.now,
2749 },
2750 &cx,
2751 );
2752 if let Some(gesture) = result {
2753 Self::dispatch_recognized_gesture(node, gesture, ctx);
2754 return Some(EventResponse::Handled);
2766 }
2767 return Some(EventResponse::Ignored);
2768 }
2769 None
2770 }
2771 WidgetEvent::PointerCancel {
2772 reason, pointer, ..
2773 } => {
2774 for slot in [
2782 &mut node.external_handlers.on_pointer_cancel,
2783 &mut node.handlers.on_pointer_cancel,
2784 ] {
2785 if let Some(handler) = slot.as_mut() {
2786 handler(pointer, *reason, ctx);
2787 }
2788 }
2789 if fire_on_pointer_event {
2790 let r = fire_event_handler_both(
2791 &mut node.external_handlers.on_pointer_event,
2792 &mut node.handlers.on_pointer_event,
2793 event,
2794 ctx,
2795 );
2796 if r == EventResponse::Handled {
2797 return Some(EventResponse::Handled);
2798 }
2799 }
2800 None
2801 }
2802 }
2803 }
2804
2805 pub(super) fn collect_from_ctx<'ops>(
2806 &mut self,
2807 mut ctx: EventContext<'ops>,
2808 source_widget: WidgetId,
2809 ) {
2810 let local_ops = ctx.window_ops.take();
2816 let mut noop = crate::window::NoopWindowOps;
2817 let ops: &mut dyn crate::window::WindowOps = match local_ops {
2818 Some(o) => o,
2819 None => &mut noop,
2820 };
2821 if ctx.frame_requested {
2822 self.request_frame();
2823 }
2824 if let Some(declared) = ctx.declared_cursor_request {
2828 self.node_declared_cursor = declared;
2829 self.current_cursor = declared;
2830 }
2831 match ctx.cursor_request {
2832 Some(crate::widget::CursorRequest::Set(cursor)) => {
2833 self.current_cursor = cursor;
2834 }
2835 Some(crate::widget::CursorRequest::Release) => {
2839 self.current_cursor = self.node_declared_cursor;
2840 }
2841 None => {}
2842 }
2843 for intent in ctx.pending_intents {
2849 self.enqueue_intent(source_widget, intent, true);
2850 }
2851 if ctx.cancel_key_capture {
2860 self.cancel_key_capture();
2861 }
2862 if let Some(slot) = ctx.pending_key_capture {
2863 self.key_capture = Some(slot);
2864 }
2865 for mutation in ctx.pending_shortcut_mutations {
2867 match mutation {
2868 crate::widget::ShortcutMutation::RebindPrimary { id, keystroke } => {
2869 self.shortcut_registry.rebind_primary(id, keystroke);
2870 }
2871 crate::widget::ShortcutMutation::RebindSecondary { id, keystroke } => {
2872 self.shortcut_registry.rebind_secondary(id, keystroke);
2873 }
2874 crate::widget::ShortcutMutation::ClearOverride { id } => {
2875 self.shortcut_registry.clear_override(&id);
2876 }
2877 }
2878 }
2879 if ctx.close_window_requested {
2880 self.close_window_requested = true;
2881 }
2882 if ctx.force_close_requested {
2883 self.force_close_requested = true;
2884 }
2885 self.pending_modal_requests
2886 .extend(ctx.modal_requests.into_iter().map(|request| {
2887 crate::modal::QueuedModalRequest {
2888 source_widget,
2889 request,
2890 }
2891 }));
2892 if ctx.dismiss_modal && !self.dismiss_modal_for_source(source_widget, &mut *ops) {
2893 self.pending_modal_dismissal = true;
2894 }
2895 if !ctx.pending_focus_intents.is_empty() {
2898 let from = self
2899 .focused
2900 .filter(|id| self.arena.is_active(*id))
2901 .unwrap_or(source_widget);
2902 for intent in ctx.pending_focus_intents {
2903 self.enqueue_intent(from, intent, true);
2904 }
2905 }
2906 for callback in ctx.idle_callbacks {
2907 self.idle_queue.push_boxed(callback);
2908 }
2909 match ctx.dismiss_scope {
2910 Some(crate::widget::DismissScope::All) => {
2911 let dismissed = self.overlay_manager.dismiss_all();
2912 self.dormant_dismissed_content(&dismissed, &mut *ops);
2913 }
2914 Some(crate::widget::DismissScope::AllExceptHosts) => {
2915 self.dismiss_all_overlays_except_hosts(&mut *ops);
2916 }
2917 Some(crate::widget::DismissScope::SelfChain) => {
2918 self.dismiss_self_overlay_chain_for_source(source_widget, &mut *ops);
2919 }
2920 Some(crate::widget::DismissScope::Top) => {
2921 if let Some((_id, content_ids, focus_restore)) = self.overlay_manager.dismiss_top()
2922 {
2923 self.dormant_dismissed_content(&content_ids, &mut *ops);
2924 if let Some(restore_id) = focus_restore
2925 && self.arena.is_active(restore_id)
2926 {
2927 self.focus_ops(restore_id, &mut *ops);
2928 }
2929 }
2930 }
2931 None => {
2932 for id in ctx.overlay_dismissals {
2933 let dismissed = self.overlay_manager.dismiss(id);
2934 self.dormant_dismissed_content(&dismissed, &mut *ops);
2935 }
2936 }
2937 }
2938 for content_id in ctx.overlay_content_dismissals {
2945 if let Some(overlay_id) = self.overlay_manager.find_by_content(content_id) {
2946 let dismissed = self.overlay_manager.dismiss(overlay_id);
2947 self.dormant_dismissed_content(&dismissed, &mut *ops);
2948 }
2949 }
2950 for (id, pause) in ctx.overlay_pause_requests {
2957 if pause {
2958 self.overlay_manager.pause_auto_dismiss(id);
2959 } else {
2960 self.overlay_manager.resume_auto_dismiss(id);
2961 }
2962 }
2963 for preserve_content in ctx.dismiss_descendant_overlays {
2964 self.dismiss_child_overlays_for_source(source_widget, preserve_content, &mut *ops);
2965 }
2966 let deferred_row_activations =
2967 self.apply_tree_mutations(std::mem::take(&mut ctx.tree_mutations));
2968 if ctx.request_a11y_update {
2969 self.a11y_dirty = true;
2970 }
2971 if let Some(visible) = ctx.soft_keyboard_request.take() {
2972 self.request_soft_keyboard(visible);
2973 }
2974 for (message, politeness) in std::mem::take(&mut ctx.announcements) {
2977 self.announce_with(message, politeness);
2978 }
2979 for mut req in ctx.overlay_requests {
2980 if req.parent_overlay.is_none() {
2981 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
2982 }
2983 if self
2984 .overlay_manager
2985 .find_by_content(req.content_id)
2986 .is_some()
2987 {
2988 continue;
2989 }
2990 let current_focus = self.focused;
2991 self.overlay_manager.show(req);
2992 self.a11y_dirty = true;
2996 if let Some(focus_id) = current_focus {
2997 self.overlay_manager.set_top_focus_restore(focus_id);
2998 }
2999 }
3000 for (mut req, band) in ctx.overlay_band_requests {
3001 if req.parent_overlay.is_none() {
3002 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3003 }
3004 if self
3005 .overlay_manager
3006 .find_by_content(req.content_id)
3007 .is_some()
3008 {
3009 continue;
3010 }
3011 let content_id = req.content_id;
3012 self.overlay_manager.show_in_band(req, band);
3013 self.arena.activate(content_id);
3014 self.a11y_dirty = true;
3015 }
3019 for (content_id, placement) in ctx.overlay_placement_updates {
3022 if let Some(overlay_id) = self.overlay_manager.find_by_content(content_id) {
3023 self.overlay_manager.update_placement(overlay_id, placement);
3024 }
3025 }
3026 for (content_id, source) in ctx.overlay_placement_sources {
3027 if let Some(overlay_id) = self.overlay_manager.find_by_content(content_id) {
3028 self.overlay_manager
3029 .set_placement_source(overlay_id, source);
3030 }
3031 }
3032 for (mut req, duration) in ctx.timed_overlay_requests {
3033 if req.parent_overlay.is_none() {
3034 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3035 }
3036 if self
3037 .overlay_manager
3038 .find_by_content(req.content_id)
3039 .is_some()
3040 {
3041 continue;
3042 }
3043 let current_focus = self.focused;
3044 let overlay_id = self.overlay_manager.show_for(req, duration);
3045 self.overlay_manager
3046 .set_shown_at_sim(overlay_id, self.sim_clock);
3047 self.a11y_dirty = true;
3048 if let Some(focus_id) = current_focus {
3049 self.overlay_manager.set_top_focus_restore(focus_id);
3050 }
3051 }
3052 for (mut req, progress, duration) in ctx.reveal_overlay_requests {
3053 if req.parent_overlay.is_none() {
3054 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3055 }
3056 if self
3057 .overlay_manager
3058 .find_by_content(req.content_id)
3059 .is_some()
3060 {
3061 continue;
3062 }
3063 let content_id = req.content_id;
3064 let current_focus = self.focused;
3065 let overlay_id = self.overlay_manager.show(req);
3066 self.overlay_manager
3067 .set_shown_at_sim(overlay_id, self.sim_clock);
3068 self.a11y_dirty = true;
3069 if let Some(focus_id) = current_focus {
3070 self.overlay_manager.set_top_focus_restore(focus_id);
3071 }
3072 self.register_animated_signal(&progress, content_id);
3078 let _ = progress.try_animate_with_options(crate::animation::AnimationRequest {
3079 target: 1.0,
3080 duration,
3081 easing: teksilo_tokens::Easing::EaseOut,
3082 frame_interval: None,
3083 looping: false,
3084 epsilon: 0.0,
3085 max_duration: None,
3086 });
3087 self.overlay_manager
3088 .attach_fade(overlay_id, progress, duration);
3089 }
3090 if let Some((pointer, capture)) = ctx.pointer_capture {
3091 let named = pointer;
3095 let pointer = pointer.unwrap_or_else(|| self.current_pointer_id());
3096 self.set_pointer_capture(pointer, capture.then_some(source_widget));
3097 if capture && ctx.explicit_capture && named.is_none() {
3101 self.note_explicit_capture(source_widget);
3102 }
3103 }
3104 if let Some(activation) = ctx.drag_activation_override.take() {
3105 self.note_drag_activation_override(source_widget, activation);
3109 }
3110 if ctx.recognized_owning_gesture {
3111 self.note_gesture_recognized(source_widget);
3114 }
3115 if !ctx.gesture_acts.is_empty() {
3116 let acts = std::mem::take(&mut ctx.gesture_acts);
3117 self.apply_gesture_acts(&acts, source_widget);
3118 }
3119 if let Some(reason) = ctx.cancel_pointer_request {
3120 let pointer = self.current_pointer_id();
3121 self.cancel_pointer(pointer, reason, &mut *ops);
3122 }
3123 for (mut request, delay, focus_target, replace_siblings) in ctx.delayed_overlay_requests {
3124 if request.parent_overlay.is_none() {
3125 request.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3126 }
3127 if self
3128 .overlay_manager
3129 .find_by_content(request.content_id)
3130 .is_some()
3131 {
3132 continue;
3133 }
3134 let content_id = request.content_id;
3135 self.pending_delayed_overlays
3136 .retain(|pending| pending.request.content_id != content_id);
3137 self.pending_delayed_overlays.push(PendingDelayedOverlay {
3138 request,
3139 delay,
3140 focus_target,
3141 replace_siblings,
3142 real_requested_at: std::time::Instant::now(),
3143 sim_requested_at: self.sim_clock,
3144 });
3145 self.arena.mark_needs_paint(source_widget);
3146 }
3147 for content_id in ctx.cancel_delayed_overlays {
3148 self.pending_delayed_overlays
3149 .retain(|pending| pending.request.content_id != content_id);
3150 }
3151 for content_id in ctx.safe_region_arm_requests {
3158 if let Some(apex) = self
3159 .previous_pointer_position
3160 .or_else(|| self.hover_owner_position())
3161 {
3162 self.overlay_manager.arm_safe_region(
3163 content_id,
3164 apex,
3165 std::time::Instant::now(),
3166 self.sim_clock,
3167 );
3168 }
3169 }
3170 for id in ctx.repaint_requests {
3171 self.arena.mark_needs_paint(id);
3172 }
3173 for id in ctx.synthetic_clicks {
3174 self.synthesise_tap_with_ops(id, &mut *ops);
3179 }
3180 if ctx.clear_focus_requested {
3183 self.clear_focus_ops(&mut *ops);
3184 }
3185 if let Some(&id) = ctx.focus_requests.last() {
3190 self.request_focus_with_ops(id, super::FocusRequestMode::SelfOrDescendant, &mut *ops);
3200 }
3201 if let Some(&id) = ctx.focus_into_requests.last() {
3202 self.request_focus_with_ops(id, super::FocusRequestMode::Into, &mut *ops);
3209 }
3210
3211 for req in ctx.scroll_into_view_requests {
3220 self.scroll_rect_into_view(
3221 req.from.unwrap_or(source_widget),
3224 req.rect,
3225 req.margin,
3226 req.align,
3227 req.motion,
3228 &mut *ops,
3229 );
3230 }
3231 for (id, margin) in ctx.scroll_widget_into_view_requests {
3238 if self.arena.get(id).is_some() {
3239 let bounds = self.arena.bounds(id);
3240 self.scroll_rect_into_view(
3241 id,
3242 bounds,
3243 margin,
3244 crate::event::ScrollAlign::Minimal,
3245 crate::event::ScrollMotion::Instant,
3246 &mut *ops,
3247 );
3248 }
3249 }
3250
3251 if let Some(&id) = ctx.highlight_tooltip_requests.last() {
3256 self.show_highlight_tooltip(id, &mut *ops);
3257 }
3258
3259 if let Some((source_widget, payload, preview_widget)) = ctx.drag_start_request {
3261 let (preview_content_id, preview_overlay_id) = if let Some(preview) = preview_widget {
3262 let content_id = self.add_boxed(preview);
3269 let overlay_id = self
3270 .overlay_manager
3271 .show(crate::overlay::OverlayRequest::new(
3272 content_id,
3273 source_widget,
3274 crate::overlay::OverlayPlacement::AtPointer(teksilo_canvas::Point::ZERO),
3275 crate::overlay::DismissBehavior::Manual,
3276 ));
3277 self.arena.mark_needs_layout(content_id);
3281 (Some(content_id), Some(overlay_id))
3282 } else {
3283 (None, None)
3284 };
3285 self.active_drag = Some(crate::drag_state::DragSession {
3286 payload,
3287 pointer: self.current_input.pointer,
3293 source_widget: Some(source_widget),
3294 is_external: false,
3295 current_position: teksilo_canvas::Point::ZERO,
3296 current_target: None,
3297 feedback: crate::drag_state::DropFeedback::NoFeedback,
3298 preview_content_id,
3299 preview_overlay_id,
3300 });
3301 self.set_current_pointer_capture(Some(source_widget));
3302 self.current_cursor = crate::widget::CursorIcon::Grabbing;
3305 }
3306 if ctx.cancel_drag {
3307 self.cancel_active_drag(&mut *ops);
3308 }
3309
3310 if let Some(theme) = ctx.theme_request {
3312 self.pending_theme_request = Some(theme);
3318 }
3319 if ctx.follow_system_request {
3320 self.pending_follow_system_request = true;
3324 }
3325 if let Some(locale) = ctx.locale_request {
3326 self.pending_locale_request = Some(locale);
3332 }
3333 if let Some(scale) = ctx.text_scale_request {
3334 self.pending_text_scale_request = Some(scale);
3339 }
3340 if !deferred_row_activations.is_empty() {
3347 self.run_with_event_context(&mut *ops, move |ctx| {
3348 for action in deferred_row_activations {
3349 action(ctx);
3350 }
3351 });
3352 }
3353 }
3354
3355 #[must_use]
3361 fn apply_tree_mutations(
3362 &mut self,
3363 mutations: Vec<crate::widget::TreeMutation>,
3364 ) -> Vec<std::rc::Rc<dyn Fn(&mut crate::widget::EventContext)>> {
3365 let mut deferred_row_activations = Vec::new();
3366 use crate::binding::BindingLevel;
3367 use crate::widget::TreeMutation;
3368
3369 for mutation in mutations {
3370 match mutation {
3371 TreeMutation::SetDormant(id) => {
3372 self.park_subtree(id);
3373 }
3374 TreeMutation::Activate(id) => self.arena.activate(id),
3375 TreeMutation::Destroy(id) => {
3376 if let Some(overlay_id) = self.overlay_manager().find_by_content(id) {
3389 self.dismiss_overlay(overlay_id);
3390 }
3391 self.destroy_subtree(id);
3392 }
3393 TreeMutation::MaterializeNow(id) => {
3397 if self.arena.get(id).is_some() {
3398 self.rebuild_single_widget(id);
3399 }
3400 }
3401 TreeMutation::RowSpaceActivate { row, fallback } => {
3402 deferred_row_activations.push(self.keyboard_toggle_in(row).unwrap_or(fallback));
3412 }
3413 TreeMutation::WithWidgetMut { id, dirty, apply } => {
3414 let existed = if let Some(any) =
3419 self.arena.get_mut(id).and_then(|n| n.widget.as_any_mut())
3420 {
3421 apply(any);
3422 true
3423 } else {
3424 false
3425 };
3426 if existed {
3427 match dirty {
3428 BindingLevel::RepaintOnly => self.arena.mark_needs_paint(id),
3429 BindingLevel::SubtreeRepaint => self.arena.mark_subtree_needs_paint(id),
3430 BindingLevel::Relayout => {
3431 self.arena.mark_needs_layout(id);
3432 self.arena.mark_ancestors_need_layout(id);
3433 }
3434 BindingLevel::Rebuild => {
3435 self.arena.mark_needs_rebuild(id);
3436 self.arena.mark_ancestors_need_layout(id);
3437 }
3438 BindingLevel::AccessibilityOnly => self.a11y_dirty = true,
3439 }
3440 }
3441 }
3442 }
3443 }
3444 deferred_row_activations
3445 }
3446
3447 pub fn hit_test(&self, point: Point) -> Option<WidgetId> {
3456 self.hit_test_excluding_overlay_and_widget(point, None, None)
3457 }
3458
3459 pub fn hit_test_for(
3470 &self,
3471 point: Point,
3472 pointer: &crate::pointer::PointerInfo,
3473 ) -> Option<WidgetId> {
3474 self.hit_test_for_excluding(point, pointer, None, None)
3475 }
3476
3477 pub fn hit_test_for_excluding(
3480 &self,
3481 point: Point,
3482 pointer: &crate::pointer::PointerInfo,
3483 exclude_overlay: Option<crate::overlay::OverlayId>,
3484 exclude_widget: Option<WidgetId>,
3485 ) -> Option<WidgetId> {
3486 let surfaces = self.text_surfaces();
3487 let read_only = |id: WidgetId| surfaces.is_read_only(id);
3488 let hit =
3489 crate::pointer::hit_slop::HitContext::new(pointer.kind, &self.effective_theme.input)
3490 .direction(self.layout_direction)
3491 .read_only_probe(&read_only);
3492 self.hit_test_with(point, exclude_overlay, exclude_widget, &hit)
3493 }
3494
3495 pub fn hit_test_excluding_overlay_and_widget(
3502 &self,
3503 point: Point,
3504 exclude_overlay: Option<crate::overlay::OverlayId>,
3505 exclude_widget: Option<WidgetId>,
3506 ) -> Option<WidgetId> {
3507 self.hit_test_with(
3508 point,
3509 exclude_overlay,
3510 exclude_widget,
3511 &crate::pointer::hit_slop::HitContext::mouse(),
3512 )
3513 }
3514
3515 pub(crate) fn hit_test_with(
3518 &self,
3519 point: Point,
3520 exclude_overlay: Option<crate::overlay::OverlayId>,
3521 exclude_widget: Option<WidgetId>,
3522 hit: &crate::pointer::hit_slop::HitContext<'_>,
3523 ) -> Option<WidgetId> {
3524 if let Some(overlay_id) = self.overlay_manager.hit_test(point) {
3525 if Some(overlay_id) == exclude_overlay {
3526 } else if let Some(overlay) = self.overlay_manager.overlay(overlay_id) {
3528 let content_id = overlay.content_id;
3532 if let Some(found) =
3533 self.arena
3534 .hit_test_in_subtree_with_slop(content_id, point, exclude_widget, hit)
3535 {
3536 return Some(found);
3537 }
3538 if !self
3556 .arena
3557 .get(content_id)
3558 .is_some_and(|node| node.event_pass_through)
3559 {
3560 return None;
3561 }
3562 }
3563 }
3564
3565 if self.overlay_manager.topmost_modal().is_some() {
3566 return None;
3567 }
3568
3569 let mut roots = self.arena.roots();
3575 let inert: Vec<WidgetId> = self
3576 .overlay_manager
3577 .stack
3578 .iter()
3579 .filter(|overlay| overlay.inert)
3580 .map(|overlay| overlay.content_id)
3581 .collect();
3582 if !inert.is_empty() {
3583 roots.retain(|root| !inert.contains(root));
3584 }
3585 self.arena
3586 .hit_test_roots_with_slop(roots, point, exclude_widget, hit)
3587 }
3588}
3589
3590fn is_context_menu_chord(key: Key, modifiers: Modifiers) -> bool {
3608 match key {
3609 Key::ContextMenu => modifiers == Modifiers::NONE,
3610 Key::F10 => modifiers == Modifiers::SHIFT,
3611 #[cfg(target_os = "macos")]
3612 Key::M => modifiers == Modifiers::CTRL | Modifiers::SHIFT,
3613 _ => false,
3614 }
3615}
3616
3617#[cfg(test)]
3618mod tests {
3619 use super::*;
3620 use crate::test_widgets::FillWidget;
3621 use crate::widget::CursorIcon;
3622 use crate::widget_builder::WidgetBuilder;
3623
3624 #[test]
3625 fn pointer_enter_leave_synthesized() {
3626 let mut tree = WidgetTree::new();
3627 let widget = tree.add(FillWidget::new());
3628 tree.layout(SizeProposal::exact(100.0, 50.0));
3629 tree.pointer_move(Point::new(50.0, 25.0));
3630 assert_eq!(tree.hovered(), Some(widget));
3631 tree.pointer_move(Point::new(200.0, 200.0));
3632 assert_eq!(tree.hovered(), None);
3633 }
3634
3635 #[test]
3636 fn pointer_hover_updates_current_cursor() {
3637 let mut tree = WidgetTree::new();
3638 tree.add(FillWidget::new().cursor(CursorIcon::ColResize));
3639 tree.layout(SizeProposal::exact(100.0, 50.0));
3640
3641 tree.pointer_move(Point::new(50.0, 25.0));
3642 assert_eq!(tree.current_cursor(), CursorIcon::ColResize);
3643
3644 tree.pointer_move(Point::new(200.0, 200.0));
3645 assert_eq!(tree.current_cursor(), CursorIcon::Default);
3646 }
3647
3648 #[derive(Debug)]
3651 struct SelfDeclared(CursorIcon);
3652
3653 impl crate::widget::Widget for SelfDeclared {
3654 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
3655 ctx.apply_self_handlers(crate::widget_builder::HandlerSet::new().cursor(self.0));
3656 Vec::new()
3657 }
3658
3659 fn layout_response(
3660 &self,
3661 proposal: SizeProposal,
3662 _ctx: &crate::widget::LayoutContext,
3663 ) -> crate::widget::LayoutResponse {
3664 proposal.resolve(100.0, 50.0).into()
3665 }
3666 }
3667
3668 #[test]
3674 fn declared_cursor_reads_what_a_node_asks_for_and_current_cursor_follows_the_hover() {
3675 let over = Point::new(50.0, 25.0);
3676 let away = Point::new(200.0, 200.0);
3677
3678 let mut tree = WidgetTree::new();
3680 let built = tree.add(FillWidget::new().cursor(CursorIcon::ColResize));
3681 tree.layout(SizeProposal::exact(100.0, 50.0));
3682 assert_eq!(tree.declared_cursor(built), Some(CursorIcon::ColResize));
3683 assert_eq!(
3684 tree.current_cursor(),
3685 CursorIcon::Default,
3686 "not hovered yet"
3687 );
3688 tree.pointer_move(over);
3689 assert_eq!(tree.current_cursor(), CursorIcon::ColResize);
3690 assert_eq!(tree.declared_cursor(built), Some(CursorIcon::ColResize));
3691 tree.pointer_move(away);
3692 assert_eq!(tree.current_cursor(), CursorIcon::Default);
3693 assert_eq!(
3694 tree.declared_cursor(built),
3695 Some(CursorIcon::ColResize),
3696 "the declaration does not depend on the pointer",
3697 );
3698
3699 let mut tree = WidgetTree::new();
3701 let own = tree.add(SelfDeclared(CursorIcon::Crosshair));
3702 tree.layout(SizeProposal::exact(100.0, 50.0));
3703 assert_eq!(tree.declared_cursor(own), Some(CursorIcon::Crosshair));
3704 tree.pointer_move(over);
3705 assert_eq!(tree.current_cursor(), CursorIcon::Crosshair);
3706 tree.pointer_move(away);
3707 assert_eq!(tree.current_cursor(), CursorIcon::Default);
3708
3709 let mut tree = WidgetTree::new();
3711 let plain = tree.add(FillWidget::new());
3712 tree.layout(SizeProposal::exact(100.0, 50.0));
3713 assert_eq!(tree.declared_cursor(plain), None);
3714 let mut tree = WidgetTree::new();
3715 let explicit = tree.add(FillWidget::new().cursor(CursorIcon::Default));
3716 tree.layout(SizeProposal::exact(100.0, 50.0));
3717 assert_eq!(
3718 tree.declared_cursor(explicit),
3719 Some(CursorIcon::Default),
3720 "declaring the default cursor is not declaring nothing",
3721 );
3722 }
3723
3724 #[test]
3734 fn release_cursor_hands_the_cursor_back_to_the_node_that_declared_it() {
3735 let mut tree = WidgetTree::new();
3739 tree.add(
3740 FillWidget::new()
3741 .cursor(CursorIcon::ColResize)
3742 .on_pointer_event(|event, ctx| {
3743 if let WidgetEvent::PointerMove { position, .. } = event {
3744 if position.x < 30.0 {
3745 ctx.set_cursor(CursorIcon::Crosshair);
3746 } else {
3747 ctx.release_cursor();
3748 }
3749 }
3750 EventResponse::Ignored
3751 }),
3752 );
3753 tree.layout(SizeProposal::exact(100.0, 50.0));
3754
3755 tree.pointer_move(Point::new(10.0, 25.0));
3756 assert_eq!(
3757 tree.current_cursor(),
3758 CursorIcon::Crosshair,
3759 "the handler outranks the node it is attached to",
3760 );
3761 tree.pointer_move(Point::new(60.0, 25.0));
3762 assert_eq!(
3763 tree.current_cursor(),
3764 CursorIcon::ColResize,
3765 "and hands it back to the node, not to Default — no enter/leave \
3766 fires on a move within one node, so nothing else could",
3767 );
3768 tree.pointer_move(Point::new(10.0, 25.0));
3769 assert_eq!(
3770 tree.current_cursor(),
3771 CursorIcon::Crosshair,
3772 "and can take it again"
3773 );
3774 tree.pointer_move(Point::new(200.0, 200.0));
3775 assert_eq!(
3776 tree.current_cursor(),
3777 CursorIcon::Default,
3778 "leaving the node clears both the override and the declaration",
3779 );
3780 }
3781
3782 #[test]
3786 fn release_cursor_is_a_no_op_with_nothing_to_undo() {
3787 let mut tree = WidgetTree::new();
3788 tree.add(
3789 FillWidget::new()
3790 .cursor(CursorIcon::ColResize)
3791 .on_pointer_event(|event, ctx| {
3792 if matches!(event, WidgetEvent::PointerMove { .. }) {
3793 ctx.release_cursor();
3794 }
3795 EventResponse::Ignored
3796 }),
3797 );
3798 tree.layout(SizeProposal::exact(100.0, 50.0));
3799 tree.pointer_move(Point::new(50.0, 25.0));
3800 assert_eq!(tree.current_cursor(), CursorIcon::ColResize);
3801
3802 let mut tree = WidgetTree::new();
3804 tree.add(FillWidget::new().on_pointer_event(|event, ctx| {
3805 if let WidgetEvent::PointerMove { position, .. } = event {
3806 if position.x < 30.0 {
3807 ctx.set_cursor(CursorIcon::Crosshair);
3808 } else {
3809 ctx.release_cursor();
3810 }
3811 }
3812 EventResponse::Ignored
3813 }));
3814 tree.layout(SizeProposal::exact(100.0, 50.0));
3815 tree.pointer_move(Point::new(10.0, 25.0));
3816 assert_eq!(tree.current_cursor(), CursorIcon::Crosshair);
3817 tree.pointer_move(Point::new(60.0, 25.0));
3818 assert_eq!(
3819 tree.current_cursor(),
3820 CursorIcon::Default,
3821 "nothing declared a cursor for this chain, so the hand-back \
3822 resolves to Default",
3823 );
3824 }
3825
3826 #[test]
3834 fn an_on_hover_override_does_not_erase_the_declaration_it_outranks() {
3835 let mut tree = WidgetTree::new();
3836 tree.add(
3837 FillWidget::new()
3838 .cursor(CursorIcon::ColResize)
3839 .on_hover(|entered, ctx| {
3840 if entered {
3841 ctx.set_cursor(CursorIcon::Crosshair);
3842 }
3843 })
3844 .on_pointer_event(|event, ctx| {
3845 if let WidgetEvent::PointerMove { position, .. } = event
3846 && position.x >= 30.0
3847 {
3848 ctx.release_cursor();
3849 }
3850 EventResponse::Ignored
3851 }),
3852 );
3853 tree.layout(SizeProposal::exact(100.0, 50.0));
3854
3855 tree.pointer_move(Point::new(10.0, 25.0));
3856 assert_eq!(
3857 tree.current_cursor(),
3858 CursorIcon::Crosshair,
3859 "on_hover runs after the node cursor is applied, so it wins",
3860 );
3861 tree.pointer_move(Point::new(60.0, 25.0));
3862 assert_eq!(
3863 tree.current_cursor(),
3864 CursorIcon::ColResize,
3865 "and the node's declaration survived being overridden",
3866 );
3867 }
3868
3869 #[derive(Debug)]
3872 struct Bumpable {
3873 value: i32,
3874 }
3875
3876 impl crate::widget::Widget for Bumpable {
3877 fn layout_response(
3878 &self,
3879 proposal: SizeProposal,
3880 _ctx: &crate::widget::LayoutContext,
3881 ) -> crate::widget::LayoutResponse {
3882 proposal.resolve(10.0, 10.0).into()
3883 }
3884 fn as_any(&self) -> Option<&dyn std::any::Any> {
3885 Some(self)
3886 }
3887 fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
3888 Some(self)
3889 }
3890 }
3891
3892 #[test]
3893 fn with_widget_mut_applies_and_dirty_marks() {
3894 let mut tree = WidgetTree::new();
3895 let id = tree.add(Bumpable { value: 0 });
3896 tree.layout(SizeProposal::exact(100.0, 100.0));
3897
3898 let mut ctx = EventContext::new();
3899 ctx.with_widget_mut::<Bumpable>(id, crate::binding::BindingLevel::Relayout, |b| {
3900 b.value = 42;
3901 });
3902 tree.collect_from_ctx(ctx, id);
3903
3904 let value = tree
3905 .widget_as_any(id)
3906 .and_then(|a| a.downcast_ref::<Bumpable>())
3907 .map(|b| b.value);
3908 assert_eq!(
3909 value,
3910 Some(42),
3911 "the deferred closure must mutate the live widget"
3912 );
3913 assert!(
3914 tree.needs_layout(),
3915 "Relayout dirty level must mark the tree for relayout"
3916 );
3917 }
3918
3919 #[test]
3920 #[cfg(debug_assertions)]
3921 #[should_panic(expected = "not the requested type")]
3922 fn with_widget_mut_wrong_type_panics_in_debug() {
3923 struct Other;
3924 let mut tree = WidgetTree::new();
3925 let id = tree.add(Bumpable { value: 0 });
3926 let mut ctx = EventContext::new();
3927 ctx.with_widget_mut::<Other>(
3928 id,
3929 crate::binding::BindingLevel::RepaintOnly,
3930 |_o: &mut Other| {},
3931 );
3932 tree.collect_from_ctx(ctx, id);
3935 }
3936
3937 #[test]
3938 fn with_widget_mut_closure_may_fire_observed_signals() {
3939 use crate::signal::Signal;
3947 let mut tree = WidgetTree::new();
3948 let id = tree.add(Bumpable { value: 0 });
3949 tree.layout(SizeProposal::exact(100.0, 100.0));
3950
3951 let trigger = Signal::new(0_u64);
3952 let echo = Signal::new(0_u64);
3953 let echo_for_obs = echo.clone();
3954 let _obs = trigger.observe(move |v| echo_for_obs.set(*v));
3955
3956 let trigger_in = trigger.clone();
3957 let mut ctx = EventContext::new();
3958 ctx.with_widget_mut::<Bumpable>(id, crate::binding::BindingLevel::RepaintOnly, move |b| {
3959 b.value = 7;
3960 trigger_in.set(99);
3962 });
3963 tree.collect_from_ctx(ctx, id); assert_eq!(
3966 echo.get(),
3967 99,
3968 "the observer ran during the deferred mutation"
3969 );
3970 let value = tree
3971 .widget_as_any(id)
3972 .and_then(|a| a.downcast_ref::<Bumpable>())
3973 .map(|b| b.value);
3974 assert_eq!(value, Some(7));
3975 }
3976
3977 #[test]
3978 fn request_accessibility_update_forces_rewalk() {
3979 let mut tree = WidgetTree::new();
3980 let id = tree.add(Bumpable { value: 0 });
3981 tree.layout(SizeProposal::exact(100.0, 100.0));
3982 let _ = tree.sync_accessibility(); assert!(
3984 !tree.a11y_dirty,
3985 "sync_accessibility should clear the dirty flag"
3986 );
3987
3988 let mut ctx = EventContext::new();
3989 ctx.request_accessibility_update();
3990 tree.collect_from_ctx(ctx, id);
3991 assert!(
3992 tree.a11y_dirty,
3993 "request_accessibility_update must force an AT re-walk"
3994 );
3995 }
3996
3997 #[test]
3998 fn rebuild_dirties_accessibility_tree() {
3999 let mut tree = WidgetTree::new();
4006 let id = tree.add(Bumpable { value: 0 });
4007 tree.layout(SizeProposal::exact(100.0, 100.0));
4008 let _ = tree.sync_accessibility(); assert!(
4010 !tree.a11y_dirty,
4011 "sync_accessibility should clear the dirty flag"
4012 );
4013
4014 tree.arena_mark_needs_rebuild_for_testing(id);
4017 tree.layout(SizeProposal::exact(100.0, 100.0));
4018 assert!(
4019 tree.a11y_dirty,
4020 "a rebuild must dirty the AT tree so the next sync re-walks"
4021 );
4022 }
4023
4024 #[test]
4025 fn bound_access_label_change_dirties_accessibility_tree() {
4026 use crate::signal::Signal;
4027 use crate::test_widgets::FillWidget;
4028 use crate::widget_builder::WidgetBuilder;
4029
4030 let label = Signal::new("first".to_string());
4036 let mut tree = WidgetTree::new();
4037 let _id = tree.add(FillWidget::new().access_label(label.clone()));
4038 tree.layout(SizeProposal::exact(100.0, 100.0));
4039 let _ = tree.sync_accessibility(); assert!(!tree.a11y_dirty, "sync_accessibility should clear the flag");
4041
4042 label.set("second".to_string());
4043 tree.layout(SizeProposal::exact(100.0, 100.0));
4044 assert!(
4045 tree.a11y_dirty,
4046 "changing a bound access_label must dirty the AT tree"
4047 );
4048 }
4049
4050 #[test]
4051 fn disabled_ancestor_blocks_event_to_descendant() {
4052 use crate::signal::Signal;
4053 use crate::test_widgets::StackWidget;
4054 use std::cell::Cell;
4055 use std::rc::Rc;
4056
4057 let tapped = Rc::new(Cell::new(false));
4058 let flag = tapped.clone();
4059 let enabled = Signal::new(true);
4060
4061 let mut tree = WidgetTree::new();
4062 let child = tree.add(FillWidget::new().on_tap(move |_pos, _ctx| {
4063 flag.set(true);
4064 }));
4065 let parent = tree.add(StackWidget::new().child(child));
4066 tree.enabled_when(parent, enabled.clone());
4067 tree.layout(SizeProposal::exact(100.0, 50.0));
4068
4069 enabled.set(false);
4070 tree.click(child);
4071 assert!(
4072 !tapped.get(),
4073 "disabled ancestor should block descendant tap"
4074 );
4075
4076 enabled.set(true);
4077 tree.click(child);
4078 assert!(tapped.get(), "re-enabling should restore dispatch");
4079 }
4080
4081 #[test]
4082 fn pointer_positions_are_widget_local_at_nonzero_origin() {
4083 use crate::event::{Modifiers, PointerButton};
4084 use crate::test_widgets::InsetWidget;
4085 use std::cell::Cell;
4086 use std::rc::Rc;
4087
4088 let tap_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
4090 let down_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
4091 let drag_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
4092 let (tp, dp, gp) = (tap_pos.clone(), down_pos.clone(), drag_pos.clone());
4093
4094 let mut tree = WidgetTree::new();
4095 let child = tree.add(
4096 FillWidget::new()
4097 .on_tap(move |ev, _ctx| tp.set(Some(ev.position)))
4098 .on_pointer_event(move |ev, _ctx| {
4099 if let WidgetEvent::PointerDown { position, .. } = ev {
4100 dp.set(Some(*position));
4101 }
4102 crate::event::EventResponse::Ignored
4103 })
4104 .on_drag(move |phase, _ctx| {
4105 use crate::gesture::DragPhase;
4106 match phase {
4107 DragPhase::Started { position, .. }
4108 | DragPhase::Moved { position, .. }
4109 | DragPhase::Ended { position, .. } => gp.set(Some(position)),
4110 _ => {}
4111 }
4112 }),
4113 );
4114 let inset = tree.add(InsetWidget::new(20.0).set_child(child));
4115 let _ = inset;
4116 tree.layout(SizeProposal::exact(200.0, 200.0));
4117 assert_eq!(tree.bounds(child).origin(), Point::new(20.0, 20.0));
4118
4119 tree.dispatch_event(WidgetEvent::pointer_down(
4121 Point::new(50.0, 40.0),
4122 PointerButton::Primary,
4123 Modifiers::NONE,
4124 ));
4125 assert_eq!(
4126 down_pos.get(),
4127 Some(Point::new(30.0, 20.0)),
4128 "on_pointer_event PointerDown must be widget-local"
4129 );
4130 tree.dispatch_event(WidgetEvent::pointer_up(
4131 Point::new(50.0, 40.0),
4132 PointerButton::Primary,
4133 Modifiers::NONE,
4134 ));
4135 assert_eq!(
4136 tap_pos.get(),
4137 Some(Point::new(30.0, 20.0)),
4138 "on_tap position must be widget-local"
4139 );
4140
4141 tree.dispatch_event(WidgetEvent::pointer_down(
4144 Point::new(50.0, 40.0),
4145 PointerButton::Primary,
4146 Modifiers::NONE,
4147 ));
4148 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(65.0, 55.0)));
4151 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(90.0, 70.0)));
4152 assert_eq!(
4153 drag_pos.get(),
4154 Some(Point::new(70.0, 50.0)),
4155 "on_drag position must be widget-local"
4156 );
4157 }
4158
4159 #[test]
4160 fn dormant_widget_not_hit_tested() {
4161 let mut tree = WidgetTree::new();
4162 let widget = tree.add(FillWidget::new());
4163 tree.layout(SizeProposal::exact(100.0, 50.0));
4164
4165 tree.pointer_move(Point::new(50.0, 25.0));
4166 assert_eq!(tree.hovered(), Some(widget));
4167
4168 tree.set_dormant(widget);
4169 tree.pointer_move(Point::new(200.0, 200.0));
4170 tree.pointer_move(Point::new(50.0, 25.0));
4171 assert_eq!(tree.hovered(), None);
4172 }
4173
4174 #[test]
4175 fn ancestor_pointer_handler_does_not_suppress_descendant_hover() {
4176 use crate::event::EventResponse;
4177 use crate::test_widgets::StackWidget;
4178 use std::cell::Cell;
4179 use std::rc::Rc;
4180
4181 let hovered = Rc::new(Cell::new(false));
4183 let h = hovered.clone();
4184
4185 let mut tree = WidgetTree::new();
4186 let child = tree.add(FillWidget::new().on_hover(move |entered, _ctx| h.set(entered)));
4187 tree.add(
4192 StackWidget::new()
4193 .child(child)
4194 .on_pointer_event(|_event, _ctx| EventResponse::Handled),
4195 );
4196 tree.layout(SizeProposal::exact(100.0, 50.0));
4197
4198 tree.pointer_move(Point::new(50.0, 25.0));
4199 assert!(
4200 hovered.get(),
4201 "a greedy ancestor on_pointer_event must NOT swallow the child's PointerEnter"
4202 );
4203
4204 tree.pointer_move(Point::new(500.0, 500.0));
4205 assert!(
4206 !hovered.get(),
4207 "PointerLeave must likewise reach the child despite the ancestor"
4208 );
4209 }
4210
4211 #[test]
4219 fn a_child_hover_handler_does_not_swallow_its_ancestors() {
4220 use crate::test_widgets::StackWidget;
4221 use std::cell::Cell;
4222 use std::rc::Rc;
4223
4224 let (row, button) = (Rc::new(Cell::new(false)), Rc::new(Cell::new(false)));
4225 let (r, b) = (row.clone(), button.clone());
4226
4227 let mut tree = WidgetTree::new();
4228 let child = tree.add(FillWidget::new().on_hover(move |entered, _ctx| b.set(entered)));
4229 tree.add(
4230 StackWidget::new()
4231 .child(child)
4232 .on_hover(move |entered, _ctx| r.set(entered)),
4233 );
4234 tree.layout(SizeProposal::exact(100.0, 50.0));
4235
4236 tree.pointer_move(Point::new(50.0, 25.0));
4237 assert!(button.get(), "the child is hovered");
4238 assert!(row.get(), "and so is the row it is inside");
4239
4240 tree.pointer_move(Point::new(500.0, 500.0));
4241 assert!(!button.get(), "the child heard the leave");
4242 assert!(
4243 !row.get(),
4244 "and so did the row — a container is not still hovered because the \
4245 pointer left it through a button"
4246 );
4247 }
4248
4249 #[test]
4256 fn key_preview_consumes_before_focused_on_key() {
4257 use crate::event::EventResponse;
4260 use crate::test_widgets::StackWidget;
4261 use std::cell::Cell;
4262 use std::rc::Rc;
4263
4264 let leaf_fired = Rc::new(Cell::new(false));
4265 let leaf_flag = leaf_fired.clone();
4266 let preview_fired = Rc::new(Cell::new(false));
4267 let preview_flag = preview_fired.clone();
4268
4269 let mut tree = WidgetTree::new();
4270 let leaf = tree.add(FillWidget::new().focusable().on_key(move |event, _c| {
4271 if matches!(event, WidgetEvent::KeyDown { .. }) {
4274 leaf_flag.set(true);
4275 }
4276 EventResponse::Handled
4277 }));
4278 let mid = tree.add(StackWidget::new().child(leaf));
4279 let _root = tree.add(
4280 StackWidget::new()
4281 .child(mid)
4282 .on_key_preview(move |event, _c| match event {
4283 WidgetEvent::KeyDown {
4284 key: Key::Enter, ..
4285 } => {
4286 preview_flag.set(true);
4287 EventResponse::Handled
4288 }
4289 _ => EventResponse::Ignored,
4290 }),
4291 );
4292
4293 tree.layout(SizeProposal::exact(100.0, 50.0));
4294 tree.focus(leaf);
4295 tree.press_key(Key::Enter, Modifiers::NONE);
4296
4297 assert!(
4298 preview_fired.get(),
4299 "ancestor on_key_preview must fire for KeyDown on a focused descendant"
4300 );
4301 assert!(
4302 !leaf_fired.get(),
4303 "consuming the event in preview must prevent the focused widget's on_key from running"
4304 );
4305 }
4306
4307 #[test]
4308 fn key_preview_falls_through_when_returning_ignored() {
4309 use crate::event::EventResponse;
4312 use crate::test_widgets::StackWidget;
4313 use std::cell::Cell;
4314 use std::rc::Rc;
4315
4316 let leaf_fired = Rc::new(Cell::new(false));
4317 let leaf_flag = leaf_fired.clone();
4318 let preview_fired = Rc::new(Cell::new(false));
4319 let preview_flag = preview_fired.clone();
4320
4321 let mut tree = WidgetTree::new();
4322 let leaf = tree.add(FillWidget::new().focusable().on_key(move |_e, _c| {
4323 leaf_flag.set(true);
4324 EventResponse::Handled
4325 }));
4326 let mid = tree.add(StackWidget::new().child(leaf));
4327 let _root = tree.add(
4328 StackWidget::new()
4329 .child(mid)
4330 .on_key_preview(move |_event, _c| {
4331 preview_flag.set(true);
4332 EventResponse::Ignored
4333 }),
4334 );
4335
4336 tree.layout(SizeProposal::exact(100.0, 50.0));
4337 tree.focus(leaf);
4338 tree.press_key(Key::Enter, Modifiers::NONE);
4339
4340 assert!(preview_fired.get(), "preview must always be invoked");
4341 assert!(
4342 leaf_fired.get(),
4343 "preview returning Ignored must not block the focused widget's on_key"
4344 );
4345 }
4346
4347 #[test]
4348 fn key_preview_excludes_focused_target_itself() {
4349 use crate::event::EventResponse;
4353 use std::cell::Cell;
4354 use std::rc::Rc;
4355
4356 let preview_on_target = Rc::new(Cell::new(false));
4357 let pf = preview_on_target.clone();
4358
4359 let mut tree = WidgetTree::new();
4360 let leaf = tree.add(FillWidget::new().focusable().on_key_preview(move |_e, _c| {
4361 pf.set(true);
4362 EventResponse::Handled
4363 }));
4364 tree.layout(SizeProposal::exact(100.0, 50.0));
4365 tree.focus(leaf);
4366 tree.press_key(Key::Enter, Modifiers::NONE);
4367
4368 assert!(
4369 !preview_on_target.get(),
4370 "the focused widget itself must not see its own on_key_preview"
4371 );
4372 }
4373
4374 #[test]
4375 fn key_preview_root_to_target_order() {
4376 use crate::event::EventResponse;
4380 use crate::test_widgets::StackWidget;
4381 use std::cell::RefCell;
4382 use std::rc::Rc;
4383
4384 let order: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
4385 let outer_log = order.clone();
4386 let inner_log = order.clone();
4387
4388 let mut tree = WidgetTree::new();
4389 let leaf = tree.add(FillWidget::new().focusable());
4390 let inner = tree.add(
4391 StackWidget::new()
4392 .child(leaf)
4393 .on_key_preview(move |event, _c| {
4394 if matches!(event, WidgetEvent::KeyDown { .. }) {
4395 inner_log.borrow_mut().push("inner");
4396 }
4397 EventResponse::Ignored
4398 }),
4399 );
4400 let _outer = tree.add(
4401 StackWidget::new()
4402 .child(inner)
4403 .on_key_preview(move |event, _c| {
4404 if matches!(event, WidgetEvent::KeyDown { .. }) {
4405 outer_log.borrow_mut().push("outer");
4406 }
4407 EventResponse::Ignored
4408 }),
4409 );
4410
4411 tree.layout(SizeProposal::exact(100.0, 50.0));
4412 tree.focus(leaf);
4413 tree.dispatch_event(WidgetEvent::KeyDown {
4414 key: Key::Enter,
4415 modifiers: Modifiers::NONE,
4416 text: None,
4417 });
4418
4419 assert_eq!(
4420 *order.borrow(),
4421 vec!["outer", "inner"],
4422 "preview must walk root → parent-of-target"
4423 );
4424 }
4425
4426 #[test]
4427 fn access_action_routes_to_cursored_target_not_focus() {
4428 use crate::signal::Signal;
4432 let a_fired = Signal::new(false);
4433 let b_fired = Signal::new(false);
4434 let a_cb = a_fired.clone();
4435 let b_cb = b_fired.clone();
4436
4437 let mut tree = WidgetTree::new();
4438 let a = tree.add(
4439 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| a_cb.set(true)),
4440 );
4441 let b = tree.add(
4442 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| b_cb.set(true)),
4443 );
4444 tree.layout(SizeProposal::exact(200.0, 100.0));
4445
4446 tree.focus(a);
4447 tree.dispatch_event(WidgetEvent::AccessAction {
4448 action: accesskit::Action::Click,
4449 target: Some(b),
4450 target_node: crate::accessibility::widget_id_to_node_id(b),
4451 data: None,
4452 });
4453
4454 assert!(b_fired.get(), "the cursored target must receive the action");
4455 assert!(
4456 !a_fired.get(),
4457 "the keyboard-focused widget must NOT receive an action targeting another node"
4458 );
4459 }
4460
4461 #[test]
4462 fn access_action_without_target_is_dropped_not_redirected_to_focus() {
4463 use crate::signal::Signal;
4466 let fired = Signal::new(false);
4467 let cb = fired.clone();
4468
4469 let mut tree = WidgetTree::new();
4470 let widget = tree.add(
4471 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| cb.set(true)),
4472 );
4473 tree.layout(SizeProposal::exact(200.0, 100.0));
4474
4475 tree.focus(widget);
4476 tree.dispatch_event(WidgetEvent::AccessAction {
4477 action: accesskit::Action::Click,
4478 target: None,
4479 target_node: crate::accessibility::root_node_id(),
4480 data: None,
4481 });
4482
4483 assert!(
4484 !fired.get(),
4485 "a target-less action must not be redirected to the focused widget"
4486 );
4487 }
4488
4489 #[test]
4496 fn shortcut_fires_matching_action_on_source_widget() {
4497 use crate::action::Action;
4498 use crate::shortcut::{KeyStroke, Shortcut};
4499 use std::cell::Cell;
4500 use std::rc::Rc;
4501
4502 let fired = Rc::new(Cell::new(false));
4503 let fired_flag = fired.clone();
4504
4505 let mut tree = WidgetTree::new();
4506 let widget = tree.add(FillWidget::new().focusable());
4507 tree.push_action(
4508 widget,
4509 Action::new("app.save").on_invoke(move |_intent, _ctx| {
4510 fired_flag.set(true);
4511 }),
4512 );
4513 tree.shortcut_registry_mut().register(
4514 Shortcut::new("app.save")
4515 .primary(KeyStroke::command(Key::S))
4516 .build(),
4517 );
4518
4519 tree.layout(SizeProposal::exact(100.0, 50.0));
4520 tree.focus(widget);
4521
4522 tree.press_key(Key::S, Modifiers::COMMAND);
4523 assert!(fired.get(), "matching action must fire on KeyDown");
4524 }
4525
4526 #[test]
4527 fn global_shortcut_fires_without_focused_widget() {
4528 use crate::action::Action;
4529 use crate::shortcut::{KeyStroke, Shortcut};
4530 use std::cell::Cell;
4531 use std::rc::Rc;
4532
4533 let fired = Rc::new(Cell::new(false));
4537 let fired_flag = fired.clone();
4538
4539 let mut tree = WidgetTree::new();
4540 let root = tree.add(FillWidget::new());
4541 tree.push_action(
4542 root,
4543 Action::new("app.save").on_invoke(move |_intent, _ctx| {
4544 fired_flag.set(true);
4545 }),
4546 );
4547 tree.shortcut_registry_mut().register(
4548 Shortcut::new("app.save")
4549 .primary(KeyStroke::command(Key::S))
4550 .build(),
4551 );
4552
4553 tree.layout(SizeProposal::exact(100.0, 50.0));
4554 tree.press_key(Key::S, Modifiers::COMMAND);
4557 assert!(
4558 fired.get(),
4559 "global shortcut must fire without a focused widget"
4560 );
4561 }
4562
4563 #[test]
4564 fn global_shortcut_fires_after_focused_widget_destroyed() {
4565 use crate::action::Action;
4566 use crate::shortcut::{KeyStroke, Shortcut};
4567 use std::cell::Cell;
4568 use std::rc::Rc;
4569
4570 let fired = Rc::new(Cell::new(false));
4575 let fired_flag = fired.clone();
4576
4577 let mut tree = WidgetTree::new();
4578 let root = tree.add(FillWidget::new());
4579 let focusable = tree.add_child(root, FillWidget::new().focusable());
4580 tree.push_action(
4581 root,
4582 Action::new("app.save").on_invoke(move |_intent, _ctx| {
4583 fired_flag.set(true);
4584 }),
4585 );
4586 tree.shortcut_registry_mut().register(
4587 Shortcut::new("app.save")
4588 .primary(KeyStroke::command(Key::S))
4589 .build(),
4590 );
4591
4592 tree.layout(SizeProposal::exact(100.0, 50.0));
4593 tree.focus(focusable);
4594 assert_eq!(tree.focused(), Some(focusable));
4595
4596 tree.destroy_subtree(focusable);
4599 assert_eq!(tree.focused(), None, "focus must clear when destroyed");
4600
4601 tree.press_key(Key::S, Modifiers::COMMAND);
4602 assert!(
4603 fired.get(),
4604 "global shortcut must still fire after the focused widget is destroyed"
4605 );
4606 }
4607
4608 #[test]
4609 fn scoped_shortcut_matches_only_when_focus_in_scope() {
4610 use crate::action::Action;
4611 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
4612 use std::cell::Cell;
4613 use std::rc::Rc;
4614
4615 let fired = Rc::new(Cell::new(0));
4616 let fired_flag = fired.clone();
4617
4618 let mut tree = WidgetTree::new();
4619 let scope_root = tree.add(FillWidget::new().focusable());
4620 let inside = tree.add_child(scope_root, FillWidget::new().focusable());
4621 let outside = tree.add(FillWidget::new().focusable());
4622
4623 tree.push_action(
4624 scope_root,
4625 Action::new("editor.find").on_invoke(move |_i, _c| {
4626 fired_flag.set(fired_flag.get() + 1);
4627 }),
4628 );
4629 tree.shortcut_registry_mut().register(
4630 Shortcut::new("editor.find")
4631 .primary(KeyStroke::command(Key::F))
4632 .scope(ShortcutScope::Scoped(scope_root))
4633 .build(),
4634 );
4635
4636 tree.layout(SizeProposal::exact(200.0, 100.0));
4637
4638 tree.focus(outside);
4640 tree.press_key(Key::F, Modifiers::COMMAND);
4641 assert_eq!(
4642 fired.get(),
4643 0,
4644 "scoped shortcut must not fire outside scope"
4645 );
4646
4647 tree.focus(inside);
4649 tree.press_key(Key::F, Modifiers::COMMAND);
4650 assert_eq!(
4651 fired.get(),
4652 1,
4653 "scoped shortcut must fire when focus in scope"
4654 );
4655 }
4656
4657 #[test]
4658 fn same_chord_scoped_first_falls_back_to_global_when_focus_outside() {
4659 use crate::action::Action;
4665 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
4666 use std::cell::Cell;
4667 use std::rc::Rc;
4668
4669 let scoped_fired = Rc::new(Cell::new(0));
4670 let global_fired = Rc::new(Cell::new(0));
4671 let sf = scoped_fired.clone();
4672 let gf = global_fired.clone();
4673
4674 let mut tree = WidgetTree::new();
4675 let root = tree.add(FillWidget::new());
4676 let editor = tree.add_child(root, FillWidget::new().focusable());
4677 let _editor_inner = tree.add_child(editor, FillWidget::new().focusable());
4678 let sidebar = tree.add_child(root, FillWidget::new().focusable());
4679
4680 tree.push_action(
4681 editor,
4682 Action::new("editor.saveBlock").on_invoke(move |_i, _c| sf.set(sf.get() + 1)),
4683 );
4684 tree.push_action(
4685 root,
4686 Action::new("zzz.global.save").on_invoke(move |_i, _c| gf.set(gf.get() + 1)),
4687 );
4688 tree.shortcut_registry_mut().register(
4689 Shortcut::new("editor.saveBlock")
4690 .primary(KeyStroke::command(Key::S))
4691 .scope(ShortcutScope::Scoped(editor))
4692 .build(),
4693 );
4694 tree.shortcut_registry_mut().register(
4695 Shortcut::new("zzz.global.save")
4696 .primary(KeyStroke::command(Key::S))
4697 .build(),
4698 );
4699
4700 tree.layout(SizeProposal::exact(200.0, 100.0));
4701 tree.focus(sidebar);
4702 tree.press_key(Key::S, Modifiers::COMMAND);
4703
4704 assert_eq!(global_fired.get(), 1, "applicable global must fire");
4705 assert_eq!(
4706 scoped_fired.get(),
4707 0,
4708 "inapplicable scoped binding must not eat the chord"
4709 );
4710 }
4711
4712 #[test]
4713 fn same_chord_global_first_yields_to_scoped_when_focus_inside() {
4714 use crate::action::Action;
4719 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
4720 use std::cell::Cell;
4721 use std::rc::Rc;
4722
4723 let scoped_fired = Rc::new(Cell::new(0));
4724 let global_fired = Rc::new(Cell::new(0));
4725 let sf = scoped_fired.clone();
4726 let gf = global_fired.clone();
4727
4728 let mut tree = WidgetTree::new();
4729 let root = tree.add(FillWidget::new());
4730 let editor = tree.add_child(root, FillWidget::new().focusable());
4731 let editor_inner = tree.add_child(editor, FillWidget::new().focusable());
4732 let sidebar = tree.add_child(root, FillWidget::new().focusable());
4733
4734 tree.push_action(
4735 editor,
4736 Action::new("editor.saveBlock").on_invoke(move |_i, _c| sf.set(sf.get() + 1)),
4737 );
4738 tree.push_action(
4739 root,
4740 Action::new("app.save").on_invoke(move |_i, _c| gf.set(gf.get() + 1)),
4741 );
4742 tree.shortcut_registry_mut().register(
4743 Shortcut::new("app.save")
4744 .primary(KeyStroke::command(Key::S))
4745 .build(),
4746 );
4747 tree.shortcut_registry_mut().register(
4748 Shortcut::new("editor.saveBlock")
4749 .primary(KeyStroke::command(Key::S))
4750 .scope(ShortcutScope::Scoped(editor))
4751 .build(),
4752 );
4753
4754 tree.layout(SizeProposal::exact(200.0, 100.0));
4755
4756 tree.focus(editor_inner);
4758 tree.press_key(Key::S, Modifiers::COMMAND);
4759 assert_eq!(
4760 scoped_fired.get(),
4761 1,
4762 "in-focus scoped must win over global"
4763 );
4764 assert_eq!(
4765 global_fired.get(),
4766 0,
4767 "global must yield to the scoped binding"
4768 );
4769
4770 tree.focus(sidebar);
4772 tree.press_key(Key::S, Modifiers::COMMAND);
4773 assert_eq!(scoped_fired.get(), 1, "scoped stays put outside its scope");
4774 assert_eq!(
4775 global_fired.get(),
4776 1,
4777 "global fires when focus leaves the scope"
4778 );
4779 }
4780
4781 #[test]
4782 fn propagated_action_lets_ancestor_handle() {
4783 use crate::action::Action;
4784 use crate::intent::IntentResponse;
4785 use crate::shortcut::{KeyStroke, Shortcut};
4786 use std::cell::Cell;
4787 use std::rc::Rc;
4788
4789 let inner_seen = Rc::new(Cell::new(false));
4790 let outer_seen = Rc::new(Cell::new(false));
4791 let inner_flag = inner_seen.clone();
4792 let outer_flag = outer_seen.clone();
4793
4794 let mut tree = WidgetTree::new();
4795 let outer = tree.add(FillWidget::new().focusable());
4796 let inner = tree.add_child(outer, FillWidget::new().focusable());
4797
4798 tree.push_action(
4800 inner,
4801 Action::new("app.save").on_invoke_with_response(move |_i, _c| {
4802 inner_flag.set(true);
4803 IntentResponse::Propagated
4804 }),
4805 );
4806 tree.push_action(
4807 outer,
4808 Action::new("app.save").on_invoke(move |_i, _c| {
4809 outer_flag.set(true);
4810 }),
4811 );
4812 tree.shortcut_registry_mut().register(
4813 Shortcut::new("app.save")
4814 .primary(KeyStroke::command(Key::S))
4815 .build(),
4816 );
4817
4818 tree.layout(SizeProposal::exact(100.0, 50.0));
4819 tree.focus(inner);
4820
4821 tree.press_key(Key::S, Modifiers::COMMAND);
4822 assert!(inner_seen.get(), "inner action observed the intent");
4823 assert!(outer_seen.get(), "outer action reached after Propagated");
4824 }
4825
4826 #[test]
4827 fn handled_action_stops_propagation() {
4828 use crate::action::Action;
4829 use crate::shortcut::{KeyStroke, Shortcut};
4830 use std::cell::Cell;
4831 use std::rc::Rc;
4832
4833 let inner_seen = Rc::new(Cell::new(false));
4834 let outer_seen = Rc::new(Cell::new(false));
4835 let inner_flag = inner_seen.clone();
4836 let outer_flag = outer_seen.clone();
4837
4838 let mut tree = WidgetTree::new();
4839 let outer = tree.add(FillWidget::new().focusable());
4840 let inner = tree.add_child(outer, FillWidget::new().focusable());
4841
4842 tree.push_action(
4843 inner,
4844 Action::new("app.save").on_invoke(move |_i, _c| {
4845 inner_flag.set(true);
4846 }),
4847 );
4848 tree.push_action(
4849 outer,
4850 Action::new("app.save").on_invoke(move |_i, _c| {
4851 outer_flag.set(true);
4852 }),
4853 );
4854 tree.shortcut_registry_mut().register(
4855 Shortcut::new("app.save")
4856 .primary(KeyStroke::command(Key::S))
4857 .build(),
4858 );
4859
4860 tree.layout(SizeProposal::exact(100.0, 50.0));
4861 tree.focus(inner);
4862
4863 tree.press_key(Key::S, Modifiers::COMMAND);
4864 assert!(inner_seen.get());
4865 assert!(!outer_seen.get(), "Handled at inner must stop propagation");
4866 }
4867
4868 #[test]
4869 fn disabled_action_propagates_by_default() {
4870 use crate::action::Action;
4871 use crate::shortcut::{KeyStroke, Shortcut};
4872 use crate::signal::Signal;
4873 use std::cell::Cell;
4874 use std::rc::Rc;
4875
4876 let inner_seen = Rc::new(Cell::new(false));
4877 let outer_seen = Rc::new(Cell::new(false));
4878 let inner_flag = inner_seen.clone();
4879 let outer_flag = outer_seen.clone();
4880
4881 let mut tree = WidgetTree::new();
4882 let outer = tree.add(FillWidget::new().focusable());
4883 let inner = tree.add_child(outer, FillWidget::new().focusable());
4884
4885 let enabled = Signal::new(false);
4886 tree.push_action(
4887 inner,
4888 Action::new("app.save")
4889 .enabled_when(enabled.clone())
4890 .on_invoke(move |_i, _c| {
4891 inner_flag.set(true);
4892 }),
4893 );
4894 tree.push_action(
4895 outer,
4896 Action::new("app.save").on_invoke(move |_i, _c| {
4897 outer_flag.set(true);
4898 }),
4899 );
4900 tree.shortcut_registry_mut().register(
4901 Shortcut::new("app.save")
4902 .primary(KeyStroke::command(Key::S))
4903 .build(),
4904 );
4905
4906 tree.layout(SizeProposal::exact(100.0, 50.0));
4907 tree.focus(inner);
4908
4909 tree.press_key(Key::S, Modifiers::COMMAND);
4910 assert!(!inner_seen.get(), "disabled inner must not run");
4911 assert!(
4912 outer_seen.get(),
4913 "intent must propagate past disabled inner"
4914 );
4915 }
4916
4917 #[test]
4918 fn disabled_action_with_non_propagating_shortcut_consumes() {
4919 use crate::action::Action;
4920 use crate::shortcut::{KeyStroke, Shortcut};
4921 use crate::signal::Signal;
4922 use std::cell::Cell;
4923 use std::rc::Rc;
4924
4925 let inner_seen = Rc::new(Cell::new(false));
4926 let outer_seen = Rc::new(Cell::new(false));
4927 let inner_flag = inner_seen.clone();
4928 let outer_flag = outer_seen.clone();
4929
4930 let mut tree = WidgetTree::new();
4931 let outer = tree.add(FillWidget::new().focusable());
4932 let inner = tree.add_child(outer, FillWidget::new().focusable());
4933
4934 let enabled = Signal::new(false);
4935 tree.push_action(
4936 inner,
4937 Action::new("app.save")
4938 .enabled_when(enabled.clone())
4939 .on_invoke(move |_i, _c| {
4940 inner_flag.set(true);
4941 }),
4942 );
4943 tree.push_action(
4944 outer,
4945 Action::new("app.save").on_invoke(move |_i, _c| {
4946 outer_flag.set(true);
4947 }),
4948 );
4949 tree.shortcut_registry_mut().register(
4950 Shortcut::new("app.save")
4951 .primary(KeyStroke::command(Key::S))
4952 .propagate_when_disabled(false)
4953 .build(),
4954 );
4955
4956 tree.layout(SizeProposal::exact(100.0, 50.0));
4957 tree.focus(inner);
4958
4959 tree.press_key(Key::S, Modifiers::COMMAND);
4960 assert!(!inner_seen.get(), "disabled inner still does not run");
4961 assert!(
4962 !outer_seen.get(),
4963 "intent must NOT propagate when shortcut disallows it"
4964 );
4965 }
4966
4967 #[test]
4968 fn send_intent_from_handler_reaches_ancestor_action() {
4969 use crate::action::Action;
4970 use crate::intent::Intent;
4971 use std::cell::Cell;
4972 use std::rc::Rc;
4973
4974 let save_seen = Rc::new(Cell::new(false));
4975 let save_flag = save_seen.clone();
4976
4977 let mut tree = WidgetTree::new();
4978 let root = tree.add(FillWidget::new());
4979 let button = tree.add_child(
4980 root,
4981 FillWidget::new().on_tap(|_pos, ctx| {
4982 ctx.send_intent(Intent::new("app.save"));
4983 }),
4984 );
4985 tree.push_action(
4986 root,
4987 Action::new("app.save").on_invoke(move |_i, _c| {
4988 save_flag.set(true);
4989 }),
4990 );
4991
4992 tree.layout(SizeProposal::exact(100.0, 50.0));
4993 tree.click(button);
4994 assert!(
4995 save_seen.get(),
4996 "ctx.send_intent must reach ancestor action"
4997 );
4998 }
4999
5000 #[test]
5001 fn widget_type_histogram_counts_distinct_types() {
5002 let mut tree = WidgetTree::new();
5007 let _ = tree.add(FillWidget::new());
5008 let _ = tree.add(FillWidget::new());
5009 let _ = tree.add(FillWidget::new());
5010 tree.layout(SizeProposal::exact(100.0, 100.0));
5011 let histogram = tree.widget_type_histogram();
5012 let total: u32 = histogram.values().sum();
5013 assert!(
5014 total >= 3,
5015 "expected at least 3 active widgets, got {total}: {histogram:?}"
5016 );
5017 let fillwidget_entries: u32 = histogram
5018 .iter()
5019 .filter(|(k, _)| k.contains("FillWidget"))
5020 .map(|(_, v)| *v)
5021 .sum();
5022 assert!(
5023 fillwidget_entries >= 3,
5024 "expected ≥3 FillWidget instances; histogram = {histogram:?}"
5025 );
5026 assert_eq!(tree.active_widget_count() as u32, total);
5027 }
5028
5029 #[test]
5030 fn intent_source_tagged_handler_for_tap_activation() {
5031 use crate::action::Action;
5035 use crate::intent::Intent;
5036 use crate::telemetry::IntentSource;
5037 use std::cell::Cell;
5038 use std::rc::Rc;
5039 let captured = Rc::new(Cell::new(IntentSource::Unknown));
5040 let captured_for_action = captured.clone();
5041
5042 let mut tree = WidgetTree::new();
5043 let root = tree.add(FillWidget::new());
5044 let button = tree.add_child(
5045 root,
5046 FillWidget::new().on_tap(|_pos, ctx| {
5047 ctx.send_intent(Intent::new("app.save"));
5048 }),
5049 );
5050 tree.push_action(
5051 root,
5052 Action::new("app.save").on_invoke(move |intent, _c| {
5053 captured_for_action.set(intent.source);
5054 }),
5055 );
5056
5057 tree.layout(SizeProposal::exact(100.0, 50.0));
5058 tree.click(button);
5059 assert_eq!(
5060 captured.get(),
5061 IntentSource::Handler,
5062 "tap-driven intent must tag IntentSource::Handler"
5063 );
5064 }
5065
5066 #[test]
5067 fn intent_source_programmatic_when_no_handler_active() {
5068 use crate::intent::Intent;
5069 use crate::telemetry::IntentSource;
5070 let intent = Intent::new("app.demo");
5071 assert_eq!(intent.source, IntentSource::Programmatic);
5072
5073 let mut ctx = EventContext::new();
5075 ctx.send_intent(Intent::new("app.demo"));
5076 let queued = ctx.pending_intents.first().expect("intent queued");
5077 assert_eq!(queued.source, IntentSource::Programmatic);
5078 }
5079
5080 #[test]
5081 fn with_intent_source_overrides_for_managed_widgets() {
5082 use crate::intent::Intent;
5083 use crate::telemetry::IntentSource;
5084 let mut ctx = EventContext::new();
5085 ctx.with_intent_source(IntentSource::Menu, |ctx| {
5086 ctx.send_intent(Intent::new("app.demo"));
5087 });
5088 let queued = ctx.pending_intents.first().expect("intent queued");
5089 assert_eq!(
5090 queued.source,
5091 IntentSource::Menu,
5092 "with_intent_source(Menu) must tag the dispatched intent"
5093 );
5094
5095 ctx.send_intent(Intent::new("app.next"));
5099 let next = ctx.pending_intents.last().expect("second intent");
5100 assert_eq!(next.source, IntentSource::Programmatic);
5101 }
5102
5103 #[test]
5104 fn disabled_shortcut_falls_through_to_focused_widget() {
5105 use crate::action::Action;
5106 use crate::shortcut::{KeyStroke, Shortcut};
5107 use crate::signal::Signal;
5108 use std::cell::Cell;
5109 use std::rc::Rc;
5110
5111 let action_fired = Rc::new(Cell::new(false));
5112 let on_key_fired = Rc::new(Cell::new(false));
5113 let af = action_fired.clone();
5114 let kf = on_key_fired.clone();
5115
5116 let enabled = Signal::new(false);
5117
5118 let mut tree = WidgetTree::new();
5119 let widget = tree.add(FillWidget::new().focusable().on_key(move |event, _ctx| {
5120 if matches!(
5121 event,
5122 WidgetEvent::KeyDown {
5123 key: Key::S,
5124 modifiers,
5125 ..
5126 } if modifiers.command()
5127 ) {
5128 kf.set(true);
5129 return EventResponse::Handled;
5130 }
5131 EventResponse::Ignored
5132 }));
5133 tree.push_action(
5134 widget,
5135 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
5136 );
5137 tree.shortcut_registry_mut().register(
5138 Shortcut::new("app.save")
5139 .primary(KeyStroke::command(Key::S))
5140 .enabled_when(enabled.clone())
5141 .build(),
5142 );
5143
5144 tree.layout(SizeProposal::exact(100.0, 50.0));
5145 tree.focus(widget);
5146
5147 tree.press_key(Key::S, Modifiers::COMMAND);
5149 assert!(
5150 !action_fired.get(),
5151 "disabled shortcut must not invoke its action"
5152 );
5153 assert!(
5154 on_key_fired.get(),
5155 "disabled shortcut must let KeyDown reach the focused widget"
5156 );
5157
5158 on_key_fired.set(false);
5160 enabled.set(true);
5161 tree.press_key(Key::S, Modifiers::COMMAND);
5162 assert!(action_fired.get(), "re-enabled shortcut must dispatch");
5163 assert!(
5164 !on_key_fired.get(),
5165 "enabled shortcut must consume the KeyDown"
5166 );
5167 }
5168
5169 #[test]
5170 fn keyboard_capture_bypasses_shortcut() {
5171 use crate::action::Action;
5172 use crate::shortcut::{KeyStroke, Shortcut};
5173 use std::cell::Cell;
5174 use std::rc::Rc;
5175
5176 fn run(capture: bool) -> (bool, bool) {
5182 let action_fired = Rc::new(Cell::new(false));
5183 let on_key_fired = Rc::new(Cell::new(false));
5184 let af = action_fired.clone();
5185 let kf = on_key_fired.clone();
5186
5187 let mut tree = WidgetTree::new();
5188 let widget = tree.add(
5189 FillWidget::new()
5190 .focusable()
5191 .keyboard_capture(capture)
5192 .on_key(move |event, _ctx| {
5193 if matches!(
5194 event,
5195 WidgetEvent::KeyDown { key: Key::S, modifiers, .. } if modifiers.command()
5196 ) {
5197 kf.set(true);
5198 return EventResponse::Handled;
5199 }
5200 EventResponse::Ignored
5201 }),
5202 );
5203 tree.push_action(
5204 widget,
5205 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
5206 );
5207 tree.shortcut_registry_mut().register(
5208 Shortcut::new("app.save")
5209 .primary(KeyStroke::command(Key::S))
5210 .build(),
5211 );
5212
5213 tree.layout(SizeProposal::exact(100.0, 50.0));
5214 tree.focus(widget);
5215 tree.press_key(Key::S, Modifiers::COMMAND);
5216 (action_fired.get(), on_key_fired.get())
5217 }
5218
5219 let (action, on_key) = run(true);
5221 assert!(
5222 !action,
5223 "keyboard_capture must suppress the shortcut action"
5224 );
5225 assert!(on_key, "keyboard_capture must deliver the raw KeyDown");
5226
5227 let (action, on_key) = run(false);
5229 assert!(action, "without capture the shortcut must fire");
5230 assert!(!on_key, "without capture the widget must not see the key");
5231 }
5232
5233 fn key_recorder(
5235 tree: &mut WidgetTree,
5236 ) -> (WidgetId, std::rc::Rc<std::cell::RefCell<Vec<Key>>>) {
5237 let seen = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
5238 let log = seen.clone();
5239 let widget = tree.add(FillWidget::new().focusable().on_key(move |event, _ctx| {
5240 if let WidgetEvent::KeyDown { key, .. } = event {
5241 log.borrow_mut().push(*key);
5242 }
5243 EventResponse::Ignored
5244 }));
5245 tree.layout(SizeProposal::exact(100.0, 50.0));
5246 tree.focus(widget);
5247 (widget, seen)
5248 }
5249
5250 #[test]
5251 fn a_shortcut_no_action_takes_leaves_its_key_to_the_focused_widget() {
5252 use crate::action::Action;
5253 use crate::intent::IntentResponse;
5254 use crate::shortcut::{KeyStroke, Shortcut};
5255
5256 let mut tree = WidgetTree::new();
5257 let (widget, seen) = key_recorder(&mut tree);
5258 tree.shortcut_registry_mut().register(
5259 Shortcut::new("app.save")
5260 .primary(KeyStroke::command(Key::S))
5261 .build(),
5262 );
5263
5264 tree.press_key(Key::S, Modifiers::COMMAND);
5266 assert_eq!(
5267 *seen.borrow(),
5268 vec![Key::S],
5269 "an unhandled intent must not eat the key"
5270 );
5271
5272 tree.push_action(
5274 widget,
5275 Action::new("app.save").on_invoke_with_response(|_i, _c| IntentResponse::Propagated),
5276 );
5277 tree.press_key(Key::S, Modifiers::COMMAND);
5278 assert_eq!(
5279 seen.borrow().len(),
5280 2,
5281 "a propagated intent must leave the key too"
5282 );
5283
5284 let mut tree = WidgetTree::new();
5287 let (widget, seen) = key_recorder(&mut tree);
5288 tree.shortcut_registry_mut().register(
5289 Shortcut::new("app.save")
5290 .primary(KeyStroke::command(Key::S))
5291 .build(),
5292 );
5293 tree.push_action(widget, Action::new("app.save").on_invoke(|_i, _c| {}));
5294 tree.press_key(Key::S, Modifiers::COMMAND);
5295 assert!(
5296 seen.borrow().is_empty(),
5297 "a handled shortcut consumes its key"
5298 );
5299 }
5300
5301 #[test]
5302 fn standard_edit_chords_follow_a_rebind() {
5303 use crate::action::Action;
5304 use crate::shortcut::{KeyStroke, StandardEdit};
5305 use std::cell::Cell;
5306 use std::rc::Rc;
5307
5308 let mut tree = WidgetTree::new();
5309 let (widget, seen) = key_recorder(&mut tree);
5310 let copies = Rc::new(Cell::new(0));
5311 let count = copies.clone();
5312 tree.push_action(
5313 widget,
5314 Action::new(StandardEdit::Copy.id())
5315 .on_invoke(move |_i, _c| count.set(count.get() + 1)),
5316 );
5317
5318 tree.press_key(Key::C, Modifiers::COMMAND);
5320 assert_eq!(copies.get(), 1, "Ctrl+C reaches the edit.copy action");
5321 assert!(seen.borrow().is_empty());
5322
5323 let elsewhere = KeyStroke::new(Key::Character('à'), Modifiers::CTRL);
5326 tree.shortcut_registry_mut()
5327 .rebind_primary(StandardEdit::Copy.id(), Some(elsewhere));
5328 tree.press_key(Key::C, Modifiers::COMMAND);
5329 assert_eq!(copies.get(), 1, "the replaced chord must not copy");
5330 assert_eq!(*seen.borrow(), vec![Key::C]);
5331 tree.press_key(Key::Character('à'), Modifiers::CTRL);
5332 assert_eq!(copies.get(), 2, "the new chord copies");
5333 }
5334
5335 #[test]
5336 fn ctrl_tab_always_escapes_a_keyboard_capture_surface() {
5337 use std::cell::Cell;
5338 use std::rc::Rc;
5339
5340 let saw_key = Rc::new(Cell::new(false));
5346 let sk = saw_key.clone();
5347
5348 let mut tree = WidgetTree::new();
5349 let capture = tree.add(
5350 FillWidget::new()
5351 .focusable()
5352 .keyboard_capture(true)
5353 .on_key(move |_event, _ctx| {
5355 sk.set(true);
5356 EventResponse::Handled
5357 }),
5358 );
5359 let neighbour = tree.add(FillWidget::new().focusable());
5360 tree.layout(SizeProposal::exact(100.0, 50.0));
5361
5362 tree.focus(capture);
5365 tree.press_key(Key::Tab, Modifiers::NONE);
5366 assert!(saw_key.get(), "plain Tab must reach the capture surface");
5367 assert_eq!(
5368 tree.focused(),
5369 Some(capture),
5370 "plain Tab must not move focus off a capture surface"
5371 );
5372
5373 saw_key.set(false);
5375 tree.press_key(Key::Tab, Modifiers::CTRL);
5376 assert!(
5377 !saw_key.get(),
5378 "Ctrl+Tab is reserved and must not reach the capture surface"
5379 );
5380 assert_eq!(
5381 tree.focused(),
5382 Some(neighbour),
5383 "Ctrl+Tab must move focus out of a capture surface"
5384 );
5385
5386 tree.focus(capture);
5388 tree.press_key(Key::Tab, Modifiers::CTRL | Modifiers::SHIFT);
5389 assert_eq!(
5390 tree.focused(),
5391 Some(neighbour),
5392 "Ctrl+Shift+Tab must move focus out of a capture surface"
5393 );
5394 }
5395
5396 #[test]
5397 fn scope_mismatch_does_not_invoke_on_activate() {
5398 use crate::intent::Intent;
5399 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
5400 use std::cell::Cell;
5401 use std::rc::Rc;
5402
5403 let activated = Rc::new(Cell::new(false));
5409 let activated_flag = activated.clone();
5410
5411 let mut tree = WidgetTree::new();
5412 let scope_root = tree.add(FillWidget::new().focusable());
5413 let outside = tree.add(FillWidget::new().focusable());
5414
5415 tree.shortcut_registry_mut().register(
5416 Shortcut::new("editor.find")
5417 .primary(KeyStroke::command(Key::F))
5418 .scope(ShortcutScope::Scoped(scope_root))
5419 .on_activate(move |_ks, _ctx| {
5420 activated_flag.set(true);
5421 Intent::new("editor.find")
5422 })
5423 .build(),
5424 );
5425
5426 tree.layout(SizeProposal::exact(200.0, 100.0));
5427 tree.focus(outside);
5428
5429 tree.press_key(Key::F, Modifiers::COMMAND);
5430 assert!(
5431 !activated.get(),
5432 "on_activate must not run when focus is outside the shortcut's scope"
5433 );
5434 }
5435
5436 #[test]
5437 fn key_capture_runs_callback_and_bypasses_registry() {
5438 use crate::action::Action;
5439 use crate::shortcut::{KeyStroke, Shortcut};
5440 use std::cell::Cell;
5441 use std::rc::Rc;
5442
5443 let action_fired = Rc::new(Cell::new(false));
5444 let af = action_fired.clone();
5445 let captured = Rc::new(Cell::new(None));
5446 let cf = captured.clone();
5447
5448 let mut tree = WidgetTree::new();
5449 let widget = tree.add(FillWidget::new().focusable());
5450 tree.push_action(
5451 widget,
5452 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
5453 );
5454 tree.shortcut_registry_mut().register(
5455 Shortcut::new("app.save")
5456 .primary(KeyStroke::command(Key::S))
5457 .build(),
5458 );
5459
5460 tree.layout(SizeProposal::exact(100.0, 50.0));
5461 tree.focus(widget);
5462
5463 let handle = tree.begin_key_capture(move |ks, _reg, _ctx| cf.set(Some(ks)));
5464 assert!(tree.is_capturing_keys());
5465
5466 tree.press_key(Key::S, Modifiers::COMMAND);
5467 assert_eq!(
5468 captured.get(),
5469 Some(KeyStroke::command(Key::S)),
5470 "capture callback must receive the chord"
5471 );
5472 assert!(
5473 !action_fired.get(),
5474 "shortcut action must not fire while capture is armed"
5475 );
5476 assert!(
5477 !tree.is_capturing_keys(),
5478 "capture is one-shot; next KeyDown flows normally"
5479 );
5480 drop(handle);
5481 }
5482
5483 #[test]
5484 fn key_capture_can_rebind_through_registry() {
5485 use crate::shortcut::{KeyStroke, Shortcut};
5486
5487 let mut tree = WidgetTree::new();
5488 let widget = tree.add(FillWidget::new().focusable());
5489 tree.shortcut_registry_mut().register(
5490 Shortcut::new("app.save")
5491 .primary(KeyStroke::command(Key::S))
5492 .build(),
5493 );
5494
5495 tree.layout(SizeProposal::exact(100.0, 50.0));
5496 tree.focus(widget);
5497
5498 let _h = tree.begin_key_capture(|ks, reg, _ctx| {
5500 reg.rebind_primary("app.save", Some(ks));
5501 });
5502
5503 tree.press_key(Key::B, Modifiers::COMMAND | Modifiers::SHIFT);
5504 assert_eq!(
5505 tree.shortcut_registry()
5506 .effective("app.save")
5507 .unwrap()
5508 .primary,
5509 Some(KeyStroke::command_shift(Key::B))
5510 );
5511 }
5512
5513 #[test]
5514 fn dropping_capture_handle_cancels_capture() {
5515 use crate::shortcut::{KeyStroke, Shortcut};
5516 use std::cell::Cell;
5517 use std::rc::Rc;
5518
5519 let action_fired = Rc::new(Cell::new(false));
5520 let af = action_fired.clone();
5521 let capture_fired = Rc::new(Cell::new(false));
5522 let cf = capture_fired.clone();
5523
5524 let mut tree = WidgetTree::new();
5525 let widget = tree.add(FillWidget::new().focusable());
5526 tree.push_action(
5527 widget,
5528 crate::action::Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
5529 );
5530 tree.shortcut_registry_mut().register(
5531 Shortcut::new("app.save")
5532 .primary(KeyStroke::command(Key::S))
5533 .build(),
5534 );
5535 tree.layout(SizeProposal::exact(100.0, 50.0));
5536 tree.focus(widget);
5537
5538 {
5542 let _h = tree.begin_key_capture(move |_ks, _reg, _ctx| cf.set(true));
5543 assert!(tree.is_capturing_keys());
5544 }
5546 assert!(
5547 !tree.is_capturing_keys(),
5548 "dropping the handle must cancel the capture"
5549 );
5550
5551 tree.press_key(Key::S, Modifiers::COMMAND);
5552 assert!(!capture_fired.get(), "cancelled capture must not fire");
5553 assert!(
5554 action_fired.get(),
5555 "shortcut action runs after capture was cancelled"
5556 );
5557 }
5558
5559 #[test]
5560 fn second_begin_key_capture_does_not_racecancel_first() {
5561 use std::cell::Cell;
5562 use std::rc::Rc;
5563
5564 let first = Rc::new(Cell::new(false));
5565 let second = Rc::new(Cell::new(false));
5566 let f = first.clone();
5567 let s = second.clone();
5568
5569 let mut tree = WidgetTree::new();
5570 let widget = tree.add(FillWidget::new().focusable());
5571 tree.layout(SizeProposal::exact(100.0, 50.0));
5572 tree.focus(widget);
5573
5574 let h1 = tree.begin_key_capture(move |_ks, _reg, _ctx| f.set(true));
5580 let _h2 = tree.begin_key_capture(move |_ks, _reg, _ctx| s.set(true));
5581 drop(h1);
5582
5583 assert!(
5584 tree.is_capturing_keys(),
5585 "dropping the older handle must not cancel the active capture"
5586 );
5587 tree.press_key(Key::K, Modifiers::COMMAND);
5588 assert!(!first.get());
5589 assert!(second.get(), "newest capture wins");
5590 }
5591
5592 #[test]
5593 fn capture_callback_can_send_intent() {
5594 use crate::action::Action;
5595 use crate::intent::Intent;
5596
5597 use std::cell::Cell;
5598 use std::rc::Rc;
5599
5600 let ran = Rc::new(Cell::new(false));
5601 let flag = ran.clone();
5602
5603 let mut tree = WidgetTree::new();
5604 let widget = tree.add(FillWidget::new().focusable());
5605 tree.push_action(
5606 widget,
5607 Action::new("app.save").on_invoke(move |_i, _c| flag.set(true)),
5608 );
5609 tree.layout(SizeProposal::exact(100.0, 50.0));
5610 tree.focus(widget);
5611
5612 let _h = tree.begin_key_capture(|_ks, _reg, ctx| {
5613 ctx.send_intent(Intent::new("app.save"));
5614 });
5615 tree.press_key(Key::X, Modifiers::COMMAND);
5616 assert!(
5617 ran.get(),
5618 "intent queued from capture callback must dispatch"
5619 );
5620 }
5621
5622 #[test]
5623 fn binding_registry_does_not_accumulate_across_rebuilds() {
5624 use crate::binding::BindingLevel;
5625 use crate::signal::Signal;
5626
5627 #[derive(Debug)]
5628 struct BoundLeaf {
5629 tick: Signal<u64>,
5630 }
5631 impl crate::widget::Widget for BoundLeaf {
5632 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
5633 self.tick.bind_to(
5634 ctx.self_id(),
5635 ctx.binding_registry(),
5636 BindingLevel::Relayout,
5637 );
5638 Vec::new()
5639 }
5640 fn layout_response(
5641 &self,
5642 proposal: SizeProposal,
5643 _ctx: &crate::widget::LayoutContext,
5644 ) -> crate::widget::LayoutResponse {
5645 proposal.resolve(10.0, 10.0).into()
5646 }
5647 }
5648
5649 let mut tree = WidgetTree::new();
5650 let tick = Signal::new(0_u64);
5651 let widget = tree.add(BoundLeaf { tick: tick.clone() });
5652 tree.layout(SizeProposal::exact(200.0, 200.0));
5653 let after_first_build = tree.binding_registry().len();
5654 assert!(after_first_build >= 1);
5655
5656 for _ in 0..5 {
5660 tree.arena.mark_needs_rebuild(widget);
5661 tree.layout(SizeProposal::exact(200.0, 200.0));
5662 }
5663 assert_eq!(
5664 tree.binding_registry().len(),
5665 after_first_build,
5666 "bindings must be cleared on rebuild"
5667 );
5668
5669 tree.destroy_subtree(widget);
5670 assert_eq!(
5671 tree.binding_registry().len(),
5672 0,
5673 "bindings must be cleared on destroy"
5674 );
5675 let _ = tick;
5677 }
5678
5679 #[test]
5680 fn ctx_destroy_cancels_animations_and_bindings_via_deferred_path() {
5681 use crate::binding::BindingLevel;
5689 use crate::signal::Signal;
5690 use std::time::{Duration, Instant};
5691 use teksilo_tokens::Easing;
5692
5693 #[derive(Debug)]
5694 struct BoundLeaf {
5695 tick: Signal<u64>,
5696 }
5697 impl crate::widget::Widget for BoundLeaf {
5698 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
5699 self.tick.bind_to(
5700 ctx.self_id(),
5701 ctx.binding_registry(),
5702 BindingLevel::Relayout,
5703 );
5704 Vec::new()
5705 }
5706 fn layout_response(
5707 &self,
5708 proposal: SizeProposal,
5709 _ctx: &crate::widget::LayoutContext,
5710 ) -> crate::widget::LayoutResponse {
5711 proposal.resolve(10.0, 10.0).into()
5712 }
5713 }
5714
5715 let mut tree = WidgetTree::new();
5716 let widget = tree.add(BoundLeaf {
5717 tick: Signal::new(0_u64),
5718 });
5719 tree.layout(SizeProposal::exact(200.0, 200.0));
5720 assert!(tree.binding_registry().len() >= 1);
5721
5722 let anim = Signal::<f32>::new_animated(0.0);
5725 tree.animation_scheduler.animate(
5726 &anim,
5727 widget,
5728 1.0,
5729 Duration::from_secs(10),
5730 Easing::Linear,
5731 Instant::now(),
5732 );
5733 assert_eq!(tree.animation_scheduler.active_count(), 1);
5734
5735 let mut noop = crate::window::NoopWindowOps;
5737 tree.run_with_event_context(&mut noop, |ctx| ctx.destroy(widget));
5738
5739 assert_eq!(
5740 tree.animation_scheduler.active_count(),
5741 0,
5742 "ctx.destroy must cancel animations owned by the destroyed widget"
5743 );
5744 assert_eq!(
5745 tree.binding_registry().len(),
5746 0,
5747 "ctx.destroy must unregister the destroyed widget's bindings"
5748 );
5749 assert!(
5750 tree.arena.get(widget).is_none(),
5751 "node must be removed from the arena"
5752 );
5753 }
5754
5755 #[test]
5756 fn clear_shortcut_override_via_event_context_restores_default() {
5757 use crate::shortcut::{KeyStroke, Shortcut};
5758
5759 let mut tree = WidgetTree::new();
5760 tree.shortcut_registry_mut().register(
5761 Shortcut::new("app.save")
5762 .primary(KeyStroke::command(Key::S))
5763 .build(),
5764 );
5765 tree.shortcut_registry_mut()
5766 .rebind_primary("app.save", Some(KeyStroke::alt(Key::S)));
5767
5768 let source = tree.add(FillWidget::new());
5769 let mut ctx = EventContext::new();
5770 ctx.clear_shortcut_override("app.save");
5771 tree.collect_from_ctx(ctx, source);
5772
5773 assert_eq!(
5774 tree.shortcut_registry()
5775 .effective("app.save")
5776 .unwrap()
5777 .primary,
5778 Some(KeyStroke::command(Key::S))
5779 );
5780 }
5781
5782 #[test]
5783 fn rebind_shortcut_primary_via_event_context() {
5784 use crate::shortcut::{KeyStroke, Shortcut};
5785
5786 let mut tree = WidgetTree::new();
5787 tree.shortcut_registry_mut().register(
5788 Shortcut::new("app.save")
5789 .primary(KeyStroke::command(Key::S))
5790 .build(),
5791 );
5792 let source = tree.add(FillWidget::new());
5793
5794 let mut ctx = EventContext::new();
5795 ctx.rebind_shortcut_primary("app.save", Some(KeyStroke::alt(Key::S)));
5796 tree.collect_from_ctx(ctx, source);
5797
5798 assert_eq!(
5799 tree.shortcut_registry()
5800 .effective("app.save")
5801 .unwrap()
5802 .primary,
5803 Some(KeyStroke::alt(Key::S))
5804 );
5805 }
5806
5807 #[test]
5808 fn unregister_all_for_owner_called_on_destroy() {
5809 use crate::shortcut::{KeyStroke, Shortcut};
5810
5811 let mut tree = WidgetTree::new();
5812 let widget = tree.add(FillWidget::new());
5813 let widget_owner = widget;
5814 tree.shortcut_registry_mut().register_owned(
5815 Shortcut::new("scoped.thing")
5816 .primary(KeyStroke::command(Key::K))
5817 .build(),
5818 widget_owner,
5819 );
5820 assert!(
5821 tree.shortcut_registry()
5822 .get_default("scoped.thing")
5823 .is_some()
5824 );
5825
5826 tree.destroy_subtree(widget);
5827 assert!(
5828 tree.shortcut_registry()
5829 .get_default("scoped.thing")
5830 .is_none(),
5831 "destroying the owner must unregister its shortcut"
5832 );
5833 }
5834
5835 #[test]
5839 fn global_action_reached_from_unrelated_source() {
5840 use crate::action::Action;
5841 use crate::intent::Intent;
5842 use std::cell::Cell;
5843 use std::rc::Rc;
5844
5845 #[derive(Debug)]
5846 struct Registrar(Rc<Cell<bool>>);
5847 impl crate::widget::Widget for Registrar {
5848 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
5849 let flag = self.0.clone();
5850 ctx.register_action_global(
5851 Action::new("test.global").on_invoke(move |_i, _c| flag.set(true)),
5852 );
5853 vec![]
5854 }
5855 fn layout_response(
5856 &self,
5857 _p: teksilo_canvas::SizeProposal,
5858 _c: &crate::widget::LayoutContext,
5859 ) -> crate::widget::LayoutResponse {
5860 teksilo_canvas::Size::new(0.0, 0.0).into()
5861 }
5862 }
5863
5864 let mut tree = WidgetTree::new();
5865 let fired = Rc::new(Cell::new(false));
5866 let registrar = tree.add(Registrar(fired.clone()));
5867 let source = tree.add(FillWidget::new()); let mut ops = crate::window::NoopWindowOps;
5869
5870 tree.dispatch_intent(source, Intent::new("test.global"), true, &mut ops);
5871 assert!(
5872 fired.get(),
5873 "global action must fire from an unrelated source"
5874 );
5875
5876 fired.set(false);
5878 tree.destroy_subtree(registrar);
5879 tree.dispatch_intent(source, Intent::new("test.global"), true, &mut ops);
5880 assert!(
5881 !fired.get(),
5882 "destroying the owner must remove its global action"
5883 );
5884 }
5885
5886 #[test]
5898 fn hit_test_through_translate_scope() {
5899 use crate::test_widgets::StackWidget;
5900 let mut tree = WidgetTree::new();
5901 let child = tree.add(FillWidget::new());
5902 let parent = tree.add(StackWidget::new().child(child));
5903 tree.set_transform(parent, teksilo_canvas::Transform2D::translate(100.0, 0.0));
5905 tree.layout(SizeProposal::exact(100.0, 50.0));
5906
5907 assert_eq!(
5911 tree.hit_test(Point::new(50.0, 25.0)),
5912 None,
5913 "pre-transform area is not visually populated and must not hit"
5914 );
5915 assert_eq!(
5917 tree.hit_test(Point::new(150.0, 25.0)),
5918 Some(child),
5919 "visually-rendered area must hit the child"
5920 );
5921 assert_eq!(tree.hit_test(Point::new(250.0, 25.0)), None);
5923 }
5924
5925 #[test]
5926 fn hit_test_through_scale_scope() {
5927 use crate::test_widgets::StackWidget;
5928 let mut tree = WidgetTree::new();
5929 let child = tree.add(FillWidget::new());
5930 let parent = tree.add(StackWidget::new().child(child));
5931 tree.set_transform(parent, teksilo_canvas::Transform2D::scale(0.5, 0.5));
5934 tree.layout(SizeProposal::exact(100.0, 50.0));
5935
5936 assert_eq!(tree.hit_test(Point::new(25.0, 12.0)), Some(child));
5938 assert_eq!(
5941 tree.hit_test(Point::new(75.0, 25.0)),
5942 None,
5943 "scaled-out region must not hit"
5944 );
5945 }
5946
5947 #[test]
5948 fn hit_test_through_nested_transforms_compose() {
5949 use crate::test_widgets::StackWidget;
5950 let mut tree = WidgetTree::new();
5951 let leaf = tree.add(FillWidget::new());
5952 let inner = tree.add(StackWidget::new().child(leaf));
5953 let outer = tree.add(StackWidget::new().child(inner));
5954 tree.set_transform(outer, teksilo_canvas::Transform2D::translate(100.0, 0.0));
5958 tree.set_transform(inner, teksilo_canvas::Transform2D::scale(2.0, 2.0));
5959 tree.layout(SizeProposal::exact(50.0, 25.0));
5960
5961 assert_eq!(tree.hit_test(Point::new(150.0, 25.0)), Some(leaf));
5965 assert_eq!(tree.hit_test(Point::new(50.0, 25.0)), None);
5966 assert_eq!(tree.hit_test(Point::new(250.0, 25.0)), None);
5967 }
5968
5969 #[test]
5970 fn hit_test_identity_transform_unchanged() {
5971 let mut tree = WidgetTree::new();
5975 let widget = tree.add(FillWidget::new());
5976 tree.set_transform(widget, teksilo_canvas::Transform2D::IDENTITY);
5977 tree.layout(SizeProposal::exact(100.0, 50.0));
5978 assert_eq!(tree.hit_test(Point::new(50.0, 25.0)), Some(widget));
5979 }
5980
5981 #[test]
5982 fn arena_effective_transform_composes_ancestors() {
5983 use crate::test_widgets::StackWidget;
5994 let mut tree = WidgetTree::new();
5995 let leaf = tree.add(FillWidget::new());
5996 let inner = tree.add(StackWidget::new().child(leaf));
5997 let outer = tree.add(StackWidget::new().child(inner));
5998 tree.set_transform(outer, teksilo_canvas::Transform2D::translate(100.0, 0.0));
5999 tree.set_transform(inner, teksilo_canvas::Transform2D::scale(2.0, 2.0));
6000 tree.layout(SizeProposal::exact(50.0, 25.0));
6001
6002 let eff = tree.arena.effective_transform(leaf);
6003 let expected = teksilo_canvas::Transform2D::scale(2.0, 2.0)
6004 .then(&teksilo_canvas::Transform2D::translate(100.0, 0.0));
6005 for (a, b) in eff.m.iter().zip(expected.m.iter()) {
6006 assert!(
6007 (a - b).abs() < 1e-5,
6008 "effective_transform mismatch: got {:?}, want {:?}",
6009 eff.m,
6010 expected.m
6011 );
6012 }
6013
6014 let screen_origin = eff.apply_point(Point::new(0.0, 0.0));
6020 assert!((screen_origin.x - 100.0).abs() < 1e-5);
6021 assert!((screen_origin.y - 0.0).abs() < 1e-5);
6022 let screen_corner = eff.apply_point(Point::new(50.0, 25.0));
6025 assert!((screen_corner.x - 200.0).abs() < 1e-5);
6026 assert!((screen_corner.y - 50.0).abs() < 1e-5);
6027 }
6028
6029 #[derive(Debug)]
6034 struct StubMenu;
6035 impl crate::widget::Widget for StubMenu {
6036 fn layout_response(
6037 &self,
6038 _proposal: SizeProposal,
6039 _ctx: &crate::widget::LayoutContext,
6040 ) -> crate::widget::LayoutResponse {
6041 teksilo_canvas::Size::new(100.0, 40.0).into()
6042 }
6043 }
6044
6045 #[derive(Debug)]
6057 struct NominatingWidget {
6058 row: std::cell::Cell<Option<WidgetId>>,
6059 }
6060
6061 impl crate::widget::Widget for NominatingWidget {
6062 fn layout_response(
6063 &self,
6064 proposal: SizeProposal,
6065 _ctx: &LayoutContext,
6066 ) -> crate::widget::LayoutResponse {
6067 proposal.resolve(50.0, 20.0).into()
6068 }
6069
6070 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6071 ctx.apply_self_handlers(crate::widget_builder::HandlerSet::new().focusable(true));
6072 Vec::new()
6073 }
6074
6075 fn context_menu_key_target(&self) -> Option<WidgetId> {
6076 self.row.get()
6077 }
6078 }
6079
6080 fn press(tree: &mut WidgetTree, key: Key, modifiers: Modifiers) {
6081 tree.dispatch_event(WidgetEvent::KeyDown {
6082 key,
6083 modifiers,
6084 text: None,
6085 });
6086 }
6087
6088 #[test]
6089 fn the_context_menu_key_opens_the_focused_widget_menu() {
6090 use std::cell::Cell;
6091 use std::rc::Rc;
6092
6093 let opened = Rc::new(Cell::new(false));
6094 let flag = opened.clone();
6095 let mut tree = WidgetTree::new();
6096 let widget = tree.add(
6097 FillWidget::new()
6098 .focusable()
6099 .context_menu(move |_pos, _ctx| {
6100 flag.set(true);
6101 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6102 }),
6103 );
6104 tree.layout(SizeProposal::exact(200.0, 100.0));
6105 tree.focus(widget);
6106
6107 press(&mut tree, Key::ContextMenu, Modifiers::NONE);
6108 assert!(opened.get(), "the dedicated Menu key must open the menu");
6109 }
6110
6111 #[test]
6114 fn shift_f10_opens_the_focused_widget_menu() {
6115 use std::cell::Cell;
6116 use std::rc::Rc;
6117
6118 let opened = Rc::new(Cell::new(false));
6119 let flag = opened.clone();
6120 let mut tree = WidgetTree::new();
6121 let widget = tree.add(
6122 FillWidget::new()
6123 .focusable()
6124 .context_menu(move |_pos, _ctx| {
6125 flag.set(true);
6126 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6127 }),
6128 );
6129 tree.layout(SizeProposal::exact(200.0, 100.0));
6130 tree.focus(widget);
6131
6132 press(&mut tree, Key::F10, Modifiers::SHIFT);
6133 assert!(opened.get(), "Shift+F10 must open the menu");
6134 }
6135
6136 #[test]
6139 fn a_near_miss_chord_is_not_a_context_menu_request() {
6140 use std::cell::Cell;
6141 use std::rc::Rc;
6142
6143 let opened = Rc::new(Cell::new(false));
6144 let flag = opened.clone();
6145 let mut tree = WidgetTree::new();
6146 let widget = tree.add(
6147 FillWidget::new()
6148 .focusable()
6149 .context_menu(move |_pos, _ctx| {
6150 flag.set(true);
6151 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6152 }),
6153 );
6154 tree.layout(SizeProposal::exact(200.0, 100.0));
6155 tree.focus(widget);
6156
6157 press(&mut tree, Key::F10, Modifiers::SHIFT | Modifiers::CTRL);
6158 press(&mut tree, Key::F10, Modifiers::NONE);
6159 assert!(!opened.get(), "only Shift+F10 exactly asks for a menu");
6160 }
6161
6162 #[test]
6166 fn the_keyboard_target_can_be_a_row_rather_than_the_focused_container() {
6167 use std::cell::Cell;
6168 use std::rc::Rc;
6169
6170 let menu_owner = Rc::new(Cell::new(None::<&'static str>));
6171
6172 let row_flag = menu_owner.clone();
6173 let container_flag = menu_owner.clone();
6174
6175 let mut tree = WidgetTree::new();
6176 let row = tree.add(FillWidget::new().context_menu(move |_pos, _ctx| {
6177 row_flag.set(Some("row"));
6178 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6179 }));
6180 let container = tree.add(
6181 crate::test_widgets::StackWidget::new()
6182 .child(row)
6183 .context_menu(move |_pos, _ctx| {
6184 container_flag.set(Some("container"));
6185 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6186 }),
6187 );
6188 tree.layout(SizeProposal::exact(200.0, 100.0));
6189
6190 let nominator = tree.add(NominatingWidget {
6192 row: std::cell::Cell::new(Some(row)),
6193 });
6194 tree.layout(SizeProposal::exact(200.0, 100.0));
6195 tree.focus(nominator);
6196 let _ = container;
6197
6198 press(&mut tree, Key::ContextMenu, Modifiers::NONE);
6199 assert_eq!(
6200 menu_owner.get(),
6201 Some("row"),
6202 "the nominated row's factory must be the one that runs"
6203 );
6204 }
6205
6206 #[test]
6209 fn the_chord_falls_through_when_there_is_no_menu_to_show() {
6210 use std::cell::Cell;
6211 use std::rc::Rc;
6212
6213 let saw_key = Rc::new(Cell::new(false));
6214 let flag = saw_key.clone();
6215 let mut tree = WidgetTree::new();
6216 let widget = tree.add(FillWidget::new().focusable().on_key(move |ev, _ctx| {
6217 if matches!(ev, WidgetEvent::KeyDown { key: Key::F10, .. }) {
6218 flag.set(true);
6219 }
6220 crate::event::EventResponse::Ignored
6221 }));
6222 tree.layout(SizeProposal::exact(200.0, 100.0));
6223 tree.focus(widget);
6224
6225 press(&mut tree, Key::F10, Modifiers::SHIFT);
6226 assert!(
6227 saw_key.get(),
6228 "with no factory anywhere, the key must reach the widget"
6229 );
6230 }
6231
6232 #[test]
6238 fn each_route_to_a_menu_tells_the_factory_what_asked_for_it() {
6239 use crate::widget_builder::ContextMenuTrigger;
6240 use std::cell::RefCell;
6241 use std::rc::Rc;
6242
6243 let seen = Rc::new(RefCell::new(Vec::new()));
6244 let log = seen.clone();
6245 let mut tree = WidgetTree::new();
6246 let widget = tree.add(
6247 FillWidget::new()
6248 .focusable()
6249 .context_menu(move |_pos, ctx| {
6250 log.borrow_mut().push(ctx.context_menu_trigger());
6251 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6252 }),
6253 );
6254 tree.layout(SizeProposal::exact(200.0, 100.0));
6255 tree.focus(widget);
6256 let inside = tree.bounds(widget).center();
6257
6258 press(&mut tree, Key::F10, Modifiers::SHIFT);
6259 press(&mut tree, Key::Escape, Modifiers::NONE);
6260 press(&mut tree, Key::ContextMenu, Modifiers::NONE);
6261 press(&mut tree, Key::Escape, Modifiers::NONE);
6262 tree.dispatch_event(WidgetEvent::pointer_down(
6263 inside,
6264 crate::event::PointerButton::Secondary,
6265 Modifiers::NONE,
6266 ));
6267 tree.dispatch_event(WidgetEvent::pointer_up(
6268 inside,
6269 crate::event::PointerButton::Secondary,
6270 Modifiers::NONE,
6271 ));
6272 press(&mut tree, Key::Escape, Modifiers::NONE);
6273 tree.dispatch_event(WidgetEvent::AccessAction {
6274 action: accesskit::Action::ShowContextMenu,
6275 target: Some(widget),
6276 target_node: crate::accessibility::widget_id_to_node_id(widget),
6277 data: None,
6278 });
6279 press(&mut tree, Key::Escape, Modifiers::NONE);
6280 tree.long_press_at(teksilo_tokens::PointerKind::Touch, inside);
6281
6282 assert_eq!(
6283 *seen.borrow(),
6284 vec![
6285 Some(ContextMenuTrigger::Keyboard),
6286 Some(ContextMenuTrigger::Keyboard),
6287 Some(ContextMenuTrigger::Pointer),
6288 Some(ContextMenuTrigger::Accessibility),
6289 Some(ContextMenuTrigger::Pointer),
6290 ],
6291 "Shift+F10, the Menu key, a right-click, the assistive action and a \
6292 hold, in that order"
6293 );
6294 }
6295
6296 #[test]
6297 fn context_menu_factory_receives_click_position() {
6298 use crate::event::{Modifiers, PointerButton};
6299 use std::cell::Cell;
6300 use std::rc::Rc;
6301
6302 let captured_position = Rc::new(Cell::new(None::<Point>));
6303 let cap = captured_position.clone();
6304 let mut tree = WidgetTree::new();
6305 let widget = tree.add(FillWidget::new().context_menu(move |pos, _ctx| {
6306 cap.set(Some(pos));
6307 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6308 }));
6309 tree.layout(SizeProposal::exact(200.0, 100.0));
6310
6311 let click = Point::new(73.0, 42.0);
6312 tree.dispatch_event(WidgetEvent::pointer_down(
6313 click,
6314 PointerButton::Secondary,
6315 Modifiers::NONE,
6316 ));
6317
6318 let got = captured_position.get();
6319 assert_eq!(
6320 got,
6321 Some(click),
6322 "factory must receive the click position; got {:?}",
6323 got
6324 );
6325 let _ = widget;
6326 }
6327
6328 #[test]
6329 fn context_menu_factory_returning_none_falls_through_to_parent() {
6330 use crate::event::{Modifiers, PointerButton};
6331 use crate::test_widgets::StackWidget;
6332 use std::cell::Cell;
6333 use std::rc::Rc;
6334
6335 let outer_called = Rc::new(Cell::new(0_u32));
6339 let outer_flag = outer_called.clone();
6340 let mut tree = WidgetTree::new();
6341 let inner = tree.add(FillWidget::new().context_menu(|_pos, _ctx| None));
6342 let _outer = tree.add(
6343 StackWidget::new()
6344 .child(inner)
6345 .context_menu(move |_pos, _ctx| {
6346 outer_flag.set(outer_flag.get() + 1);
6347 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6348 }),
6349 );
6350 tree.layout(SizeProposal::exact(200.0, 100.0));
6351
6352 tree.dispatch_event(WidgetEvent::pointer_down(
6353 Point::new(50.0, 25.0),
6354 PointerButton::Secondary,
6355 Modifiers::NONE,
6356 ));
6357
6358 assert_eq!(
6359 outer_called.get(),
6360 1,
6361 "inner returning None must fall through to the outer factory"
6362 );
6363 }
6364
6365 #[test]
6366 fn context_menu_factory_none_throughout_chain_does_not_show_overlay() {
6367 use crate::event::{Modifiers, PointerButton};
6368
6369 let mut tree = WidgetTree::new();
6371 tree.add(FillWidget::new().context_menu(|_pos, _ctx| None));
6372 tree.layout(SizeProposal::exact(200.0, 100.0));
6373
6374 let overlay_count_before = tree.overlay_manager.len();
6375 tree.dispatch_event(WidgetEvent::pointer_down(
6376 Point::new(50.0, 25.0),
6377 PointerButton::Secondary,
6378 Modifiers::NONE,
6379 ));
6380 let overlay_count_after = tree.overlay_manager.len();
6381 assert_eq!(
6382 overlay_count_before, overlay_count_after,
6383 "a factory returning None must not mount any overlay"
6384 );
6385 }
6386
6387 #[derive(Debug)]
6398 struct FreshChildHost {
6399 preserve: bool,
6400 }
6401 impl Widget for FreshChildHost {
6402 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6403 vec![ctx.add(FillWidget::new())]
6404 }
6405 fn layout_response(
6406 &self,
6407 p: SizeProposal,
6408 _c: &LayoutContext,
6409 ) -> crate::widget::LayoutResponse {
6410 p.resolve(10.0, 10.0).into()
6411 }
6412 fn preserves_children_on_rebuild(&self) -> bool {
6413 self.preserve
6414 }
6415 }
6416
6417 #[test]
6418 fn reconcile_reaps_dropped_children_no_leak() {
6419 for preserve in [false, true] {
6424 let mut tree = WidgetTree::new();
6425 let host = tree.add(FreshChildHost { preserve });
6426 tree.layout(SizeProposal::exact(100.0, 100.0));
6427 let total0 = tree.arena.len();
6428 let active0 = tree.active_widget_count();
6429 for _ in 0..5 {
6430 tree.arena_mark_needs_rebuild_for_testing(host);
6431 tree.layout(SizeProposal::exact(100.0, 100.0));
6432 }
6433 assert_eq!(
6434 tree.arena.len(),
6435 total0,
6436 "preserve={preserve}: dropped children must be reaped, not leaked"
6437 );
6438 assert_eq!(
6439 tree.active_widget_count(),
6440 active0,
6441 "preserve={preserve}: no stranded still-active orphans"
6442 );
6443 }
6444 }
6445
6446 #[derive(Debug)]
6452 struct DetachedContentHost {
6453 preserve: bool,
6454 }
6455 impl Widget for DetachedContentHost {
6456 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6457 let popup = ctx.add_detached(FillWidget::new());
6458 ctx.set_dormant(popup);
6459 vec![ctx.add(FillWidget::new())]
6460 }
6461 fn layout_response(
6462 &self,
6463 p: SizeProposal,
6464 _c: &LayoutContext,
6465 ) -> crate::widget::LayoutResponse {
6466 p.resolve(10.0, 10.0).into()
6467 }
6468 fn preserves_children_on_rebuild(&self) -> bool {
6469 self.preserve
6470 }
6471 }
6472
6473 #[test]
6474 fn rebuilding_reaps_detached_content_no_leak() {
6475 for preserve in [false, true] {
6481 let mut tree = WidgetTree::new();
6482 let host = tree.add(DetachedContentHost { preserve });
6483 tree.layout(SizeProposal::exact(100.0, 100.0));
6484 let total0 = tree.arena.len();
6485 for _ in 0..5 {
6486 tree.arena_mark_needs_rebuild_for_testing(host);
6487 tree.layout(SizeProposal::exact(100.0, 100.0));
6488 }
6489 assert_eq!(
6490 tree.arena.len(),
6491 total0,
6492 "preserve={preserve}: the previous build's detached content must be reaped"
6493 );
6494 }
6495 }
6496
6497 #[test]
6498 fn destroying_a_host_reaps_its_detached_content() {
6499 let mut tree = WidgetTree::new();
6500 let outer = tree.add(FillWidget::new());
6501 tree.layout(SizeProposal::exact(100.0, 100.0));
6502 let empty = tree.arena.len();
6503
6504 let host = tree.add_child(outer, DetachedContentHost { preserve: false });
6505 tree.layout(SizeProposal::exact(100.0, 100.0));
6506 assert!(tree.arena.len() > empty);
6507
6508 tree.destroy_subtree(host);
6509 assert_eq!(
6510 tree.arena.len(),
6511 empty,
6512 "the popup must die with the widget that built it"
6513 );
6514 }
6515
6516 #[derive(Debug)]
6518 struct StableChildHost {
6519 child: Option<WidgetId>,
6520 probe: std::rc::Rc<std::cell::Cell<Option<WidgetId>>>,
6521 }
6522 impl Widget for StableChildHost {
6523 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6524 let id = match self.child {
6525 Some(id) => id,
6526 None => {
6527 let id = ctx.add(FillWidget::new());
6528 self.child = Some(id);
6529 self.probe.set(Some(id));
6530 id
6531 }
6532 };
6533 vec![id]
6534 }
6535 fn layout_response(
6536 &self,
6537 p: SizeProposal,
6538 _c: &LayoutContext,
6539 ) -> crate::widget::LayoutResponse {
6540 p.resolve(10.0, 10.0).into()
6541 }
6542 fn preserves_children_on_rebuild(&self) -> bool {
6543 true
6544 }
6545 }
6546
6547 #[test]
6548 fn reconcile_preserves_reattached_child() {
6549 let probe = std::rc::Rc::new(std::cell::Cell::new(None));
6550 let mut tree = WidgetTree::new();
6551 let host = tree.add(StableChildHost {
6552 child: None,
6553 probe: probe.clone(),
6554 });
6555 tree.layout(SizeProposal::exact(100.0, 100.0));
6556 let child = probe.get().expect("child mounted");
6557 let total0 = tree.arena.len();
6558 for _ in 0..5 {
6559 tree.arena_mark_needs_rebuild_for_testing(host);
6560 tree.layout(SizeProposal::exact(100.0, 100.0));
6561 }
6562 assert!(
6563 tree.arena.is_active(child),
6564 "the re-attached child must survive every rebuild"
6565 );
6566 assert_eq!(tree.arena.len(), total0, "no growth — same child reused");
6567 }
6568
6569 #[derive(Debug)]
6571 struct Wrapper {
6572 child: WidgetId,
6573 }
6574 impl Widget for Wrapper {
6575 fn build(&mut self, _ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6576 vec![self.child]
6577 }
6578 fn layout_response(
6579 &self,
6580 p: SizeProposal,
6581 _c: &LayoutContext,
6582 ) -> crate::widget::LayoutResponse {
6583 p.resolve(10.0, 10.0).into()
6584 }
6585 }
6586
6587 #[derive(Debug)]
6591 struct ReparentHost {
6592 body: Option<WidgetId>,
6593 probe: std::rc::Rc<std::cell::Cell<Option<WidgetId>>>,
6594 }
6595 impl Widget for ReparentHost {
6596 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6597 let body = match self.body {
6598 Some(id) => id,
6599 None => {
6600 let id = ctx.add(FillWidget::new());
6601 self.body = Some(id);
6602 self.probe.set(Some(id));
6603 id
6604 }
6605 };
6606 vec![ctx.add(Wrapper { child: body })]
6607 }
6608 fn layout_response(
6609 &self,
6610 p: SizeProposal,
6611 _c: &LayoutContext,
6612 ) -> crate::widget::LayoutResponse {
6613 p.resolve(10.0, 10.0).into()
6614 }
6615 fn preserves_children_on_rebuild(&self) -> bool {
6616 true
6617 }
6618 }
6619
6620 #[test]
6621 fn reconcile_spares_reparented_survivor() {
6622 let probe = std::rc::Rc::new(std::cell::Cell::new(None));
6629 let mut tree = WidgetTree::new();
6630 let host = tree.add(ReparentHost {
6631 body: None,
6632 probe: probe.clone(),
6633 });
6634 tree.layout(SizeProposal::exact(100.0, 100.0));
6635 let body = probe.get().expect("body mounted");
6636 let total0 = tree.arena.len();
6637 for _ in 0..5 {
6638 tree.arena_mark_needs_rebuild_for_testing(host);
6639 tree.layout(SizeProposal::exact(100.0, 100.0));
6640 }
6641 assert!(
6642 tree.arena.is_active(body),
6643 "the re-parented body must survive — it was moved into the new tree, \
6644 not swept with the dropped wrapper"
6645 );
6646 assert_eq!(
6647 tree.arena.len(),
6648 total0,
6649 "dropped wrappers reaped — no per-rebuild leak"
6650 );
6651 }
6652
6653 #[derive(Debug)]
6663 struct BelowContainer {
6664 child: Option<WidgetId>,
6665 offset: f32,
6666 }
6667
6668 impl crate::widget::Widget for BelowContainer {
6669 fn layout_response(
6670 &self,
6671 proposal: SizeProposal,
6672 _ctx: &crate::widget::LayoutContext,
6673 ) -> crate::widget::LayoutResponse {
6674 proposal.resolve(0.0, 0.0).into()
6675 }
6676 fn place_children(
6677 &self,
6678 bounds: Rect,
6679 _proposal: SizeProposal,
6680 children: &mut [crate::widget::WidgetPlacement],
6681 _ctx: &crate::widget::LayoutContext,
6682 ) {
6683 for c in children.iter_mut() {
6684 c.origin = Point::new(bounds.x, bounds.y + self.offset);
6685 c.size = bounds.size();
6686 }
6687 }
6688 fn children(&self) -> Vec<WidgetId> {
6689 self.child.into_iter().collect()
6690 }
6691 }
6692
6693 fn recording_scroll_container(
6696 tree: &mut WidgetTree,
6697 child: WidgetId,
6698 recorded: std::rc::Rc<std::cell::Cell<Option<Rect>>>,
6699 ) -> WidgetId {
6700 use crate::test_widgets::StackWidget;
6701 tree.add(
6702 StackWidget::new()
6703 .child(child)
6704 .on_scroll(move |ev, _ctx| match ev {
6705 WidgetEvent::ScrollIntoView { target_bounds, .. } => {
6706 recorded.set(Some(*target_bounds));
6707 EventResponse::Handled
6708 }
6709 _ => EventResponse::Ignored,
6710 })
6711 .clips_children(true),
6712 )
6713 }
6714
6715 #[test]
6716 fn ensure_visible_dispatches_scroll_into_view_to_clipping_ancestor() {
6717 use std::cell::Cell;
6718 use std::rc::Rc;
6719 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6720 let mut tree = WidgetTree::new();
6721 let actor = tree.add(FillWidget::new());
6722 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
6723 tree.layout(SizeProposal::exact(100.0, 100.0));
6724
6725 let target = Rect::new(10.0, 500.0, 20.0, 15.0);
6728 let mut ctx = EventContext::new();
6729 ctx.ensure_visible(target);
6730 tree.collect_from_ctx(ctx, actor);
6731
6732 assert_eq!(
6733 recorded.get(),
6734 Some(target),
6735 "ensure_visible(rect) must dispatch ScrollIntoView with the exact rect \
6736 to the clips_children ancestor"
6737 );
6738 }
6739
6740 #[test]
6741 fn ensure_visible_is_noop_when_rect_already_visible() {
6742 use std::cell::Cell;
6743 use std::rc::Rc;
6744 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6745 let mut tree = WidgetTree::new();
6746 let actor = tree.add(FillWidget::new());
6747 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
6748 tree.layout(SizeProposal::exact(100.0, 100.0));
6749
6750 let mut ctx = EventContext::new();
6753 ctx.ensure_visible(Rect::new(10.0, 10.0, 20.0, 15.0));
6754 tree.collect_from_ctx(ctx, actor);
6755
6756 assert_eq!(
6757 recorded.get(),
6758 None,
6759 "a rect already inside the viewport must not trigger a scroll"
6760 );
6761 }
6762
6763 #[test]
6764 fn ensure_visible_margin_forces_scroll_near_edge() {
6765 use std::cell::Cell;
6766 use std::rc::Rc;
6767 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6768 let mut tree = WidgetTree::new();
6769 let actor = tree.add(FillWidget::new());
6770 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
6771 tree.layout(SizeProposal::exact(100.0, 100.0));
6772
6773 let rect = Rect::new(10.0, 95.0, 20.0, 4.0);
6776 let mut ctx = EventContext::new();
6777 ctx.ensure_visible_with_margin(rect, 10.0);
6778 tree.collect_from_ctx(ctx, actor);
6779
6780 assert_eq!(
6781 recorded.get(),
6782 Some(rect),
6783 "the margin must widen the visibility test so a near-edge rect scrolls"
6784 );
6785 }
6786
6787 fn recording_align_container(
6790 tree: &mut WidgetTree,
6791 child: WidgetId,
6792 recorded: std::rc::Rc<
6793 std::cell::Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>,
6794 >,
6795 ) -> WidgetId {
6796 use crate::test_widgets::StackWidget;
6797 tree.add(
6798 StackWidget::new()
6799 .child(child)
6800 .on_scroll(move |ev, _ctx| match ev {
6801 WidgetEvent::ScrollIntoView { align, motion, .. } => {
6802 recorded.set(Some((*align, *motion)));
6803 EventResponse::Handled
6804 }
6805 _ => EventResponse::Ignored,
6806 })
6807 .clips_children(true),
6808 )
6809 }
6810
6811 #[test]
6812 fn ensure_visible_aligned_scrolls_even_when_already_visible() {
6813 use std::cell::Cell;
6814 use std::rc::Rc;
6815 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6816 let mut tree = WidgetTree::new();
6817 let actor = tree.add(FillWidget::new());
6818 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
6819 tree.layout(SizeProposal::exact(100.0, 100.0));
6820
6821 let target = Rect::new(10.0, 10.0, 20.0, 15.0);
6826 let mut ctx = EventContext::new();
6827 ctx.ensure_visible_aligned(target, 0.5, crate::event::ScrollMotion::Instant);
6828 tree.collect_from_ctx(ctx, actor);
6829
6830 assert_eq!(
6831 recorded.get(),
6832 Some(target),
6833 "an aligned reveal must dispatch even when the rect is already visible"
6834 );
6835 }
6836
6837 #[test]
6838 fn ensure_visible_aligned_forwards_fraction_and_motion() {
6839 use std::cell::Cell;
6840 use std::rc::Rc;
6841 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6842 Rc::new(Cell::new(None));
6843 let mut tree = WidgetTree::new();
6844 let actor = tree.add(FillWidget::new());
6845 let _container = recording_align_container(&mut tree, actor, recorded.clone());
6846 tree.layout(SizeProposal::exact(100.0, 100.0));
6847
6848 let mut ctx = EventContext::new();
6849 ctx.ensure_visible_aligned(
6850 Rect::new(10.0, 10.0, 20.0, 15.0),
6851 0.25,
6852 crate::event::ScrollMotion::Smooth,
6853 );
6854 tree.collect_from_ctx(ctx, actor);
6855
6856 assert_eq!(
6857 recorded.get(),
6858 Some((
6859 crate::event::ScrollAlign::Fraction(0.25),
6860 crate::event::ScrollMotion::Smooth
6861 )),
6862 "the container must receive the requested fraction and motion verbatim"
6863 );
6864 }
6865
6866 #[test]
6867 fn ensure_visible_aligned_clamps_the_fraction() {
6868 use std::cell::Cell;
6869 use std::rc::Rc;
6870 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6871 Rc::new(Cell::new(None));
6872 let mut tree = WidgetTree::new();
6873 let actor = tree.add(FillWidget::new());
6874 let _container = recording_align_container(&mut tree, actor, recorded.clone());
6875 tree.layout(SizeProposal::exact(100.0, 100.0));
6876
6877 let mut ctx = EventContext::new();
6878 ctx.ensure_visible_aligned(
6879 Rect::new(10.0, 10.0, 20.0, 15.0),
6880 4.2,
6881 crate::event::ScrollMotion::Instant,
6882 );
6883 tree.collect_from_ctx(ctx, actor);
6884
6885 assert_eq!(
6886 recorded.get().map(|(a, _)| a),
6887 Some(crate::event::ScrollAlign::Fraction(1.0)),
6888 "an out-of-range fraction must clamp rather than aim the pin off-screen"
6889 );
6890 }
6891
6892 #[test]
6893 fn plain_ensure_visible_requests_minimal_alignment() {
6894 use std::cell::Cell;
6895 use std::rc::Rc;
6896 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6897 Rc::new(Cell::new(None));
6898 let mut tree = WidgetTree::new();
6899 let actor = tree.add(FillWidget::new());
6900 let _container = recording_align_container(&mut tree, actor, recorded.clone());
6901 tree.layout(SizeProposal::exact(100.0, 100.0));
6902
6903 let mut ctx = EventContext::new();
6904 ctx.ensure_visible(Rect::new(10.0, 500.0, 20.0, 15.0));
6905 tree.collect_from_ctx(ctx, actor);
6906
6907 assert_eq!(
6908 recorded.get(),
6909 Some((
6910 crate::event::ScrollAlign::Minimal,
6911 crate::event::ScrollMotion::Instant
6912 )),
6913 "the pre-existing reveal API must keep its exact semantics"
6914 );
6915 }
6916
6917 #[test]
6918 fn only_the_innermost_container_aligns() {
6919 use std::cell::Cell;
6920 use std::rc::Rc;
6921 let inner_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6922 Rc::new(Cell::new(None));
6923 let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6924 Rc::new(Cell::new(None));
6925
6926 let mut tree = WidgetTree::new();
6927 let actor = tree.add(FillWidget::new());
6928 let inner = recording_align_container(&mut tree, actor, inner_rec.clone());
6929 let _outer = recording_align_container(&mut tree, inner, outer_rec.clone());
6930 tree.layout(SizeProposal::exact(100.0, 100.0));
6931
6932 let mut ctx = EventContext::new();
6935 ctx.ensure_visible_aligned(
6936 Rect::new(10.0, 500.0, 20.0, 15.0),
6937 0.5,
6938 crate::event::ScrollMotion::Instant,
6939 );
6940 tree.collect_from_ctx(ctx, actor);
6941
6942 assert_eq!(
6943 inner_rec.get().map(|(a, _)| a),
6944 Some(crate::event::ScrollAlign::Fraction(0.5)),
6945 "the innermost clipping ancestor owns the pin"
6946 );
6947 assert_eq!(
6948 outer_rec.get().map(|(a, _)| a),
6949 Some(crate::event::ScrollAlign::Minimal),
6950 "an outer container must only bring the inner viewport into view — a \
6951 fraction names a height in one viewport, not in every ancestor's"
6952 );
6953 }
6954
6955 #[test]
6956 fn a_clipper_that_does_not_scroll_passes_the_pin_on() {
6957 use crate::test_widgets::StackWidget;
6964 use std::cell::Cell;
6965 use std::rc::Rc;
6966 let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6967 Rc::new(Cell::new(None));
6968
6969 let mut tree = WidgetTree::new();
6970 let actor = tree.add(FillWidget::new());
6971 let clipper = tree.add(StackWidget::new().child(actor).clips_children_on(true));
6972 let _scroller = recording_align_container(&mut tree, clipper, outer_rec.clone());
6973 tree.layout(SizeProposal::exact(100.0, 100.0));
6974
6975 let mut ctx = EventContext::new();
6978 ctx.ensure_visible_aligned(
6979 Rect::new(10.0, 10.0, 20.0, 15.0),
6980 0.5,
6981 crate::event::ScrollMotion::Instant,
6982 );
6983 tree.collect_from_ctx(ctx, actor);
6984
6985 assert_eq!(
6986 outer_rec.get().map(|(a, _)| a),
6987 Some(crate::event::ScrollAlign::Fraction(0.5)),
6988 "a clipping wrapper with no scroll handler must pass the pin through \
6989 to the scroll container above it"
6990 );
6991 }
6992
6993 #[test]
6994 fn a_scroller_that_declines_to_move_still_holds_the_pin() {
6995 use crate::test_widgets::StackWidget;
7000 use std::cell::Cell;
7001 use std::rc::Rc;
7002 let inner_rec: Rc<Cell<Option<crate::event::ScrollAlign>>> = Rc::new(Cell::new(None));
7003 let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
7004 Rc::new(Cell::new(None));
7005
7006 let mut tree = WidgetTree::new();
7007 let actor = tree.add(FillWidget::new());
7008 let rec = inner_rec.clone();
7009 let inner = tree.add(
7010 StackWidget::new()
7011 .child(actor)
7012 .on_scroll(move |ev, _ctx| match ev {
7013 WidgetEvent::ScrollIntoView { align, .. } => {
7014 rec.set(Some(*align));
7015 EventResponse::Ignored
7016 }
7017 _ => EventResponse::Ignored,
7018 })
7019 .clips_children(true),
7020 );
7021 let _outer = recording_align_container(&mut tree, inner, outer_rec.clone());
7022 tree.layout(SizeProposal::exact(100.0, 100.0));
7023
7024 let mut ctx = EventContext::new();
7026 ctx.ensure_visible_aligned(
7027 Rect::new(10.0, 500.0, 20.0, 15.0),
7028 0.5,
7029 crate::event::ScrollMotion::Instant,
7030 );
7031 tree.collect_from_ctx(ctx, actor);
7032
7033 assert_eq!(
7034 inner_rec.get(),
7035 Some(crate::event::ScrollAlign::Fraction(0.5)),
7036 "the innermost scroller is offered the pin"
7037 );
7038 assert_eq!(
7039 outer_rec.get().map(|(a, _)| a),
7040 Some(crate::event::ScrollAlign::Minimal),
7041 "declining to move must not pass the pin outward"
7042 );
7043 }
7044
7045 #[test]
7046 fn ensure_widget_visible_uses_target_arena_bounds() {
7047 use std::cell::Cell;
7048 use std::rc::Rc;
7049 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
7050 let mut tree = WidgetTree::new();
7051 let target = tree.add(FillWidget::new());
7053 let rec = recorded.clone();
7054 let container = tree.add(
7055 BelowContainer {
7056 child: Some(target),
7057 offset: 500.0,
7058 }
7059 .on_scroll(move |ev, _ctx| match ev {
7060 WidgetEvent::ScrollIntoView { target_bounds, .. } => {
7061 rec.set(Some(*target_bounds));
7062 EventResponse::Handled
7063 }
7064 _ => EventResponse::Ignored,
7065 })
7066 .clips_children(true),
7067 );
7068 tree.layout(SizeProposal::exact(100.0, 100.0));
7069
7070 let expected = tree.bounds(target);
7071 assert!(
7072 expected.y > 100.0,
7073 "fixture sanity: the target must sit below the viewport (y={})",
7074 expected.y
7075 );
7076
7077 let mut ctx = EventContext::new();
7080 ctx.ensure_widget_visible(target);
7081 tree.collect_from_ctx(ctx, container);
7082
7083 assert_eq!(
7084 recorded.get(),
7085 Some(expected),
7086 "ensure_widget_visible(id) must dispatch ScrollIntoView with the \
7087 target's current arena bounds"
7088 );
7089 }
7090
7091 #[test]
7092 fn ensure_widget_visible_ignores_missing_widget() {
7093 use std::cell::Cell;
7095 use std::rc::Rc;
7096 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
7097 let mut tree = WidgetTree::new();
7098 let actor = tree.add(FillWidget::new());
7099 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
7100 tree.layout(SizeProposal::exact(100.0, 100.0));
7101
7102 let mut ctx = EventContext::new();
7103 ctx.ensure_widget_visible(WidgetId::default());
7104 tree.collect_from_ctx(ctx, actor); assert_eq!(
7107 recorded.get(),
7108 None,
7109 "an unmounted id must not trigger a scroll"
7110 );
7111 }
7112
7113 #[test]
7114 fn context_menu_inside_a_modal_keeps_the_modal() {
7115 use crate::event::{Modifiers, PointerButton, WidgetEvent};
7119 use crate::overlay::{DismissBehavior, OverlayPlacement, OverlayRequest};
7120 use crate::test_widgets::{FillWidget, StackWidget};
7121
7122 let mut tree = WidgetTree::new();
7123 let modal_content = tree.add(StackWidget::new());
7126 let _editor = tree.add_child(
7127 modal_content,
7128 FillWidget::new()
7129 .context_menu(|_pos, _ctx| Some(Box::new(FillWidget::new()) as Box<dyn Widget>)),
7130 );
7131 tree.layout(SizeProposal::exact(200.0, 100.0));
7132
7133 let modal = tree.overlay_manager.show(OverlayRequest::new(
7134 modal_content,
7135 modal_content,
7136 OverlayPlacement::Centered,
7137 DismissBehavior::EscapeKey,
7138 ));
7139 tree.overlay_manager
7141 .stack
7142 .iter_mut()
7143 .find(|o| o.id == modal)
7144 .unwrap()
7145 .bounds = Rect::new(0.0, 0.0, 200.0, 100.0);
7146 assert_eq!(tree.overlay_manager.len(), 1);
7147
7148 tree.dispatch_event(WidgetEvent::pointer_down(
7150 Point::new(50.0, 25.0),
7151 PointerButton::Secondary,
7152 Modifiers::NONE,
7153 ));
7154
7155 assert!(
7156 tree.overlay_manager.active_ids().contains(&modal),
7157 "the modal must survive opening a context menu inside it"
7158 );
7159 assert_eq!(
7160 tree.overlay_manager.len(),
7161 2,
7162 "the context menu should now be open on top of the surviving modal"
7163 );
7164 }
7165}
7166
7167#[cfg(test)]
7170mod input_ingress_tests {
7171 use super::*;
7172 use crate::event::{EventResponse, Modifiers, PointerButton, ScrollDelta};
7173 use crate::pointer::{
7174 EventTime, PointerId, PointerInfo, PointerPhase, PointerSample, ScrollPhase, ScrollSample,
7175 ScrollSource,
7176 };
7177 use crate::test_widgets::FillWidget;
7178 use crate::widget::{LayoutContext, WidgetPlacement};
7179 use crate::widget_builder::WidgetBuilder;
7180 use std::cell::RefCell;
7181 use std::rc::Rc;
7182
7183 #[derive(Debug)]
7190 struct RowWidget {
7191 children: Vec<WidgetId>,
7192 }
7193
7194 impl crate::widget::Widget for RowWidget {
7195 fn layout_response(
7196 &self,
7197 proposal: SizeProposal,
7198 _ctx: &LayoutContext,
7199 ) -> crate::widget::LayoutResponse {
7200 proposal.resolve(0.0, 0.0).into()
7201 }
7202
7203 fn place_children(
7204 &self,
7205 bounds: Rect,
7206 _proposal: SizeProposal,
7207 children: &mut [WidgetPlacement],
7208 _ctx: &LayoutContext,
7209 ) {
7210 let n = children.len().max(1) as f32;
7211 let w = bounds.width / n;
7212 for (i, child) in children.iter_mut().enumerate() {
7213 child.origin = Point::new(bounds.x + w * i as f32, bounds.y);
7214 child.size = teksilo_canvas::Size::new(w, bounds.height);
7215 }
7216 }
7217
7218 fn children(&self) -> Vec<WidgetId> {
7219 self.children.clone()
7220 }
7221 }
7222
7223 fn record(drive: impl FnOnce(&mut WidgetTree)) -> Vec<String> {
7226 let log: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(Vec::new()));
7227
7228 let mut tree = WidgetTree::new();
7229 let pointer_log = log.clone();
7230 let hover_log = log.clone();
7231 tree.add(
7232 FillWidget::new()
7233 .on_pointer_event(move |event, _ctx| {
7234 pointer_log.borrow_mut().push(format!("{event:?}"));
7235 EventResponse::Ignored
7236 })
7237 .on_hover(move |entered, _ctx| {
7238 hover_log.borrow_mut().push(format!("hover({entered})"));
7239 }),
7240 );
7241 tree.layout(SizeProposal::exact(100.0, 100.0));
7242
7243 drive(&mut tree);
7244 log.borrow().clone()
7245 }
7246
7247 #[test]
7253 fn a_mouse_sample_reproduces_todays_event_stream() {
7254 let inside = Point::new(40.0, 40.0);
7255 let moved = Point::new(60.0, 55.0);
7256 let outside = Point::new(400.0, 400.0);
7257
7258 let legacy = record(|tree| {
7259 tree.dispatch_event(WidgetEvent::pointer_move(inside));
7260 tree.dispatch_event(WidgetEvent::pointer_down(
7261 inside,
7262 PointerButton::Primary,
7263 Modifiers::NONE,
7264 ));
7265 tree.dispatch_event(WidgetEvent::pointer_move(moved));
7266 tree.dispatch_event(WidgetEvent::pointer_up(
7267 moved,
7268 PointerButton::Primary,
7269 Modifiers::NONE,
7270 ));
7271 tree.dispatch_event(WidgetEvent::pointer_move(outside));
7272 });
7273
7274 let sampled = record(|tree| {
7275 let t = EventTime::ZERO;
7276 tree.dispatch_pointer(PointerSample::mouse(PointerPhase::Move, inside, t));
7277 tree.dispatch_pointer(
7278 PointerSample::mouse(PointerPhase::Down, inside, t)
7279 .with_button(PointerButton::Primary),
7280 );
7281 tree.dispatch_pointer(PointerSample::mouse(PointerPhase::Move, moved, t));
7282 tree.dispatch_pointer(
7283 PointerSample::mouse(PointerPhase::Up, moved, t)
7284 .with_button(PointerButton::Primary),
7285 );
7286 tree.dispatch_pointer(PointerSample::mouse(PointerPhase::Move, outside, t));
7287 });
7288
7289 assert_eq!(legacy, sampled);
7290 assert!(
7291 legacy.contains(&"hover(true)".to_string())
7292 && legacy.contains(&"hover(false)".to_string()),
7293 "the fixture must actually exercise enter and leave: {legacy:?}"
7294 );
7295 }
7296
7297 #[test]
7301 fn a_buttonless_press_lowers_to_primary() {
7302 let events = record(|tree| {
7303 tree.dispatch_pointer(PointerSample::mouse(
7304 PointerPhase::Down,
7305 Point::new(20.0, 20.0),
7306 EventTime::ZERO,
7307 ));
7308 });
7309 assert!(
7310 events.iter().any(|e| e.contains("button: Primary")),
7311 "{events:?}"
7312 );
7313 }
7314
7315 #[test]
7321 fn a_sampled_press_and_release_form_a_tap() {
7322 let taps = Rc::new(std::cell::Cell::new(0));
7323 let seen = taps.clone();
7324 let mut tree = WidgetTree::new();
7325 tree.add(FillWidget::new().on_tap(move |_event, _ctx| seen.set(seen.get() + 1)));
7326 tree.layout(SizeProposal::exact(100.0, 100.0));
7327
7328 let at = Point::new(50.0, 50.0);
7329 tree.dispatch_pointer(PointerSample::mouse(
7330 PointerPhase::Down,
7331 at,
7332 EventTime::from_millis(0),
7333 ));
7334 tree.dispatch_pointer(PointerSample::mouse(
7335 PointerPhase::Up,
7336 at,
7337 EventTime::from_millis(5),
7338 ));
7339
7340 assert_eq!(taps.get(), 1);
7341 }
7342
7343 fn scroll_fixture() -> (WidgetTree, Rc<RefCell<Vec<&'static str>>>) {
7348 let hits: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
7349 let mut tree = WidgetTree::new();
7350
7351 let leading_hits = hits.clone();
7352 let trailing_hits = hits.clone();
7353 let leading = tree.add(FillWidget::new().on_scroll(move |_event, _ctx| {
7354 leading_hits.borrow_mut().push("leading");
7355 EventResponse::Handled
7356 }));
7357 let trailing = tree.add(FillWidget::new().on_scroll(move |_event, _ctx| {
7358 trailing_hits.borrow_mut().push("trailing");
7359 EventResponse::Handled
7360 }));
7361 tree.add(RowWidget {
7362 children: vec![leading, trailing],
7363 });
7364 tree.layout(SizeProposal::exact(200.0, 100.0));
7365 (tree, hits)
7366 }
7367
7368 fn notch() -> ScrollDelta {
7369 ScrollDelta::Lines { x: 0.0, y: -1.0 }
7370 }
7371
7372 #[test]
7375 fn a_positionless_scroll_routes_by_hover() {
7376 let (mut tree, hits) = scroll_fixture();
7377
7378 tree.pointer_move(Point::new(150.0, 50.0)); tree.dispatch_event(WidgetEvent::scroll(notch(), Modifiers::NONE));
7380 assert_eq!(*hits.borrow(), vec!["trailing"]);
7381
7382 tree.pointer_move(Point::new(50.0, 50.0)); tree.dispatch_scroll(ScrollSample::wheel(
7384 notch(),
7385 Modifiers::NONE,
7386 EventTime::ZERO,
7387 ));
7388 assert_eq!(*hits.borrow(), vec!["trailing", "leading"]);
7389 }
7390
7391 #[test]
7396 fn a_positioned_scroll_routes_by_hit_test() {
7397 let (mut tree, hits) = scroll_fixture();
7398 tree.pointer_move(Point::new(150.0, 50.0)); tree.dispatch_event(WidgetEvent::scroll_at(
7401 notch(),
7402 Modifiers::NONE,
7403 Point::new(50.0, 50.0), ));
7405 assert_eq!(*hits.borrow(), vec!["leading"]);
7406
7407 tree.dispatch_scroll(
7408 ScrollSample::wheel(notch(), Modifiers::NONE, EventTime::ZERO)
7409 .at(Point::new(150.0, 50.0)),
7410 );
7411 assert_eq!(*hits.borrow(), vec!["leading", "trailing"]);
7412 }
7413
7414 #[test]
7418 fn a_positionless_scroll_with_no_hover_goes_nowhere() {
7419 let (mut tree, hits) = scroll_fixture();
7420 tree.dispatch_event(WidgetEvent::scroll(notch(), Modifiers::NONE));
7421 assert!(hits.borrow().is_empty());
7422 }
7423
7424 #[test]
7429 fn a_handler_sees_the_sample_it_is_serving() {
7430 let seen: Rc<RefCell<Vec<(ScrollPhase, ScrollSource, Option<Point>)>>> =
7431 Rc::new(RefCell::new(Vec::new()));
7432 let sink = seen.clone();
7433
7434 let mut tree = WidgetTree::new();
7435 tree.add(
7442 FillWidget::new()
7443 .scroll_container(crate::pointer::touch_action::PanAxes::BOTH)
7444 .on_scroll(move |_event, ctx| {
7445 sink.borrow_mut().push((
7446 ctx.scroll_phase(),
7447 ctx.scroll_source(),
7448 ctx.pointer_position(),
7449 ));
7450 EventResponse::Handled
7451 }),
7452 );
7453 tree.layout(SizeProposal::exact(100.0, 100.0));
7454
7455 tree.dispatch_scroll(ScrollSample {
7456 delta: notch(),
7457 position: Some(Point::new(50.0, 50.0)),
7458 phase: ScrollPhase::Momentum,
7459 source: ScrollSource::TouchPan,
7460 pointer: PointerInfo::mouse(EventTime::from_millis(12)),
7461 modifiers: Modifiers::NONE,
7462 });
7463
7464 assert_eq!(
7465 *seen.borrow(),
7466 vec![(
7467 ScrollPhase::Momentum,
7468 ScrollSource::TouchPan,
7469 Some(Point::new(50.0, 50.0))
7470 )]
7471 );
7472 }
7473
7474 #[test]
7477 fn a_legacy_event_reports_the_mouse_at_the_epoch() {
7478 let seen: Rc<RefCell<Option<(PointerId, ScrollPhase)>>> = Rc::new(RefCell::new(None));
7479 let sink = seen.clone();
7480
7481 let mut tree = WidgetTree::new();
7482 tree.add(FillWidget::new().on_pointer_event(move |_event, ctx| {
7483 *sink.borrow_mut() = Some((ctx.pointer().id, ctx.scroll_phase()));
7484 EventResponse::Ignored
7485 }));
7486 tree.layout(SizeProposal::exact(100.0, 100.0));
7487 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(50.0, 50.0)));
7488
7489 assert_eq!(
7490 *seen.borrow(),
7491 Some((PointerId::MOUSE, ScrollPhase::Discrete))
7492 );
7493 }
7494
7495 #[test]
7499 fn the_snapshot_is_restored_after_a_dispatch() {
7500 let mut tree = WidgetTree::new();
7501 tree.add(FillWidget::new());
7502 tree.layout(SizeProposal::exact(100.0, 100.0));
7503
7504 let before = tree.current_input.clone();
7505 tree.dispatch_scroll(
7506 ScrollSample::wheel(notch(), Modifiers::NONE, EventTime::ZERO)
7507 .at(Point::new(10.0, 10.0)),
7508 );
7509 assert_eq!(tree.current_input, before);
7510 }
7511}
7512
7513#[cfg(test)]
7514mod press_and_focus_tests {
7515 use std::cell::{Cell, RefCell};
7520 use std::rc::Rc;
7521 use teksilo_canvas::{Point, SizeProposal};
7522
7523 use crate::WidgetId;
7524 use crate::event::{EventResponse, Key, Modifiers, PointerButton, WidgetEvent};
7525 use crate::focus::FocusOrigin;
7526 use crate::pointer::clock::ManualClock;
7527 use crate::pointer::touch_action::PanClaim;
7528 use crate::pointer::{
7529 BackendDeviceKey, EventTime, PointerId, PointerIdAllocator, PointerInfo, PointerPhase,
7530 PointerSample,
7531 };
7532 use crate::test_widgets::{FillWidget, StackWidget};
7533 use crate::widget_builder::WidgetBuilder;
7534 use crate::widget_tree::WidgetTree;
7535
7536 fn contact_id() -> PointerId {
7542 use std::sync::atomic::{AtomicU64, Ordering};
7543 static NEXT: AtomicU64 = AtomicU64::new(1);
7544 PointerIdAllocator::global().begin(
7545 BackendDeviceKey::new(0x0B11),
7546 NEXT.fetch_add(1, Ordering::Relaxed),
7547 )
7548 }
7549
7550 fn contact(id: PointerId, phase: PointerPhase, at: Point, t: EventTime) -> PointerSample {
7551 PointerSample {
7552 pointer: PointerInfo::touch(id, t),
7553 phase,
7554 position: at,
7555 button: None,
7556 modifiers: Modifiers::NONE,
7557 coalesced: Vec::new(),
7558 }
7559 }
7560
7561 fn tree_on_a_clock() -> (WidgetTree, Rc<ManualClock>) {
7564 let mut tree = WidgetTree::new();
7565 let clock = Rc::new(ManualClock::new(EventTime::ZERO));
7566 tree.set_input_clock(clock.clone());
7567 (tree, clock)
7568 }
7569
7570 fn tappable(tree: &mut WidgetTree) -> (WidgetId, crate::signal::Signal<bool>) {
7577 let id = tree.add(FillWidget::new().focusable().on_tap(|_e, _c| {}));
7578 let signal = tree.pressed_signal(id);
7579 (id, signal)
7580 }
7581
7582 #[test]
7589 fn a_pointer_origin_names_its_device() {
7590 use teksilo_tokens::{PenKind, PointerKind};
7591
7592 assert!(FocusOrigin::Pointer(PointerKind::Touch).is_pointer());
7593 assert!(FocusOrigin::Pointer(PointerKind::Pen(PenKind::Pen)).is_pointer());
7594 assert!(FocusOrigin::POINTER.is_pointer());
7595 assert!(!FocusOrigin::Keyboard.is_pointer());
7596 assert!(!FocusOrigin::Programmatic.is_pointer());
7597 assert!(!FocusOrigin::Accessibility.is_pointer());
7598
7599 assert_eq!(
7600 FocusOrigin::Pointer(PointerKind::Touch).pointer_kind(),
7601 Some(PointerKind::Touch),
7602 );
7603 assert_eq!(FocusOrigin::Keyboard.pointer_kind(), None);
7604 assert_eq!(
7605 FocusOrigin::POINTER.pointer_kind(),
7606 Some(PointerKind::Unknown),
7607 "a widget deriving its own origin says so rather than naming a device it never saw",
7608 );
7609 }
7610
7611 #[test]
7615 fn only_a_real_navigation_declares_a_modality() {
7616 use teksilo_tokens::PointerKind;
7617
7618 assert_eq!(FocusOrigin::Keyboard.focus_visible(), Some(true));
7619 assert_eq!(FocusOrigin::Accessibility.focus_visible(), Some(true));
7620 assert_eq!(
7621 FocusOrigin::Pointer(PointerKind::Touch).focus_visible(),
7622 Some(false),
7623 );
7624 assert_eq!(FocusOrigin::Programmatic.focus_visible(), None);
7625 }
7626
7627 #[test]
7630 fn the_tier_three_configs_are_unchanged() {
7631 let origin: crate::signal::Signal<Option<FocusOrigin>> =
7632 crate::signal::Signal::new(Some(FocusOrigin::Keyboard));
7633 let slider: crate::signal::Signal<Option<FocusOrigin>> = origin.clone();
7634 let splitter: crate::signal::Signal<Option<FocusOrigin>> = origin.clone();
7635 let segmented: crate::signal::Signal<Option<FocusOrigin>> = origin.clone();
7636 for field in [slider, splitter, segmented] {
7637 assert_eq!(field.get(), Some(FocusOrigin::Keyboard));
7638 }
7639 origin.set(Some(FocusOrigin::POINTER));
7640 assert_ne!(
7641 origin.get(),
7642 Some(FocusOrigin::Keyboard),
7643 "the `== Some(Keyboard)` test every consumer makes still discriminates",
7644 );
7645 }
7646
7647 #[test]
7658 fn a_touch_tap_after_keyboard_focus_leaves_no_ring() {
7659 let (mut tree, _clock) = tree_on_a_clock();
7660 let a = tree.add(FillWidget::new().focusable());
7661 let b = tree.add(FillWidget::new().focusable());
7662 let root = tree.add(SideBySide {
7663 children: vec![a, b],
7664 });
7665 tree.layout(SizeProposal::exact(200.0, 100.0));
7666 let _ = root;
7667
7668 let visible = tree.focus_visible_signal();
7669 tree.press_key(Key::Tab, Modifiers::NONE);
7670 assert_eq!(tree.focused(), Some(a));
7671 assert!(visible.get(), "keyboard navigation reveals the ring");
7672
7673 let id = contact_id();
7674 let at = tree.bounds(b).center();
7675 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
7676 tree.dispatch_pointer(contact(
7677 id,
7678 PointerPhase::Up,
7679 at,
7680 EventTime::from_millis(30),
7681 ));
7682
7683 assert_eq!(tree.focused(), Some(b), "the release moved focus");
7684 assert!(!visible.get(), "and the ring did not come with it");
7685 assert_eq!(
7686 tree.focus_origin(),
7687 Some(FocusOrigin::Pointer(teksilo_tokens::PointerKind::Touch)),
7688 );
7689 }
7690
7691 #[test]
7696 fn an_assistive_focus_reveals_the_ring() {
7697 let mut tree = WidgetTree::new();
7698 let a = tree.add(FillWidget::new().focusable());
7699 let b = tree.add(FillWidget::new().focusable());
7700 let root = tree.add(SideBySide {
7701 children: vec![a, b],
7702 });
7703 tree.layout(SizeProposal::exact(200.0, 50.0));
7704 let _ = root;
7705 let visible = tree.focus_visible_signal();
7706
7707 tree.click(a);
7708 assert_eq!(tree.focused(), Some(a));
7709 assert!(!visible.get(), "the click hid it");
7710
7711 tree.dispatch_event(WidgetEvent::AccessAction {
7712 target: Some(b),
7713 action: accesskit::Action::Focus,
7714 target_node: crate::accessibility::widget_id_to_node_id(b),
7715 data: None,
7716 });
7717 assert_eq!(tree.focused(), Some(b));
7718 assert!(visible.get(), "an assistive move is a navigation");
7719 assert_eq!(tree.focus_origin(), Some(FocusOrigin::Accessibility));
7720 }
7721
7722 #[test]
7727 fn a_programmatic_focus_leaves_the_modality_alone() {
7728 let mut tree = WidgetTree::new();
7729 let a = tree.add(FillWidget::new().focusable());
7730 let b = tree.add(FillWidget::new().focusable());
7731 let root = tree.add(SideBySide {
7732 children: vec![a, b],
7733 });
7734 tree.layout(SizeProposal::exact(200.0, 100.0));
7735 let _ = root;
7736 let visible = tree.focus_visible_signal();
7737
7738 tree.press_key(Key::Tab, Modifiers::NONE);
7739 assert_eq!(tree.focused(), Some(a));
7740 assert!(visible.get());
7741 tree.focus(b);
7742 assert!(
7743 visible.get(),
7744 "a scripted focus does not hide a keyboard ring"
7745 );
7746
7747 tree.click(a);
7748 assert!(!visible.get());
7749 tree.focus(b);
7750 assert!(
7751 !visible.get(),
7752 "and does not reveal one after a click either",
7753 );
7754 }
7755
7756 #[test]
7762 fn a_mouse_still_focuses_on_press() {
7763 let mut tree = WidgetTree::new();
7764 let w = tree.add(FillWidget::new().focusable());
7765 tree.layout(SizeProposal::exact(100.0, 50.0));
7766 let center = tree.bounds(w).center();
7767
7768 tree.dispatch_event(WidgetEvent::pointer_down(
7769 center,
7770 PointerButton::Primary,
7771 Modifiers::NONE,
7772 ));
7773 assert_eq!(
7774 tree.focused(),
7775 Some(w),
7776 "focus lands on the press for an indirect pointer",
7777 );
7778 }
7779
7780 #[test]
7782 fn a_finger_focuses_on_release() {
7783 let (mut tree, _clock) = tree_on_a_clock();
7784 let w = tree.add(FillWidget::new().focusable());
7785 tree.layout(SizeProposal::exact(100.0, 50.0));
7786 let center = tree.bounds(w).center();
7787
7788 let id = contact_id();
7789 tree.dispatch_pointer(contact(id, PointerPhase::Down, center, EventTime::ZERO));
7790 assert_eq!(
7791 tree.focused(),
7792 None,
7793 "a finger that has only landed has chosen nothing yet",
7794 );
7795
7796 tree.dispatch_pointer(contact(
7797 id,
7798 PointerPhase::Up,
7799 center,
7800 EventTime::from_millis(40),
7801 ));
7802 assert_eq!(tree.focused(), Some(w));
7803 assert_eq!(
7804 tree.focus_origin().and_then(FocusOrigin::pointer_kind),
7805 Some(teksilo_tokens::PointerKind::Touch),
7806 "the origin names the device that delivered it",
7807 );
7808 }
7809
7810 #[test]
7814 fn a_release_on_a_different_focusable_moves_no_focus() {
7815 let (mut tree, _clock) = tree_on_a_clock();
7816 let a = tree.add(FillWidget::new().focusable());
7817 let b = tree.add(FillWidget::new().focusable());
7818 let root = tree.add(SideBySide {
7819 children: vec![a, b],
7820 });
7821 tree.layout(SizeProposal::exact(200.0, 50.0));
7822 let _ = root;
7823
7824 let on_a = tree.bounds(a).center();
7825 let on_b = tree.bounds(b).center();
7826 let id = contact_id();
7827 tree.dispatch_pointer(contact(id, PointerPhase::Down, on_a, EventTime::ZERO));
7828 tree.dispatch_pointer(contact(
7829 id,
7830 PointerPhase::Move,
7831 on_b,
7832 EventTime::from_millis(20),
7833 ));
7834 tree.dispatch_pointer(contact(
7835 id,
7836 PointerPhase::Up,
7837 on_b,
7838 EventTime::from_millis(40),
7839 ));
7840
7841 assert_eq!(
7842 tree.focused(),
7843 None,
7844 "the guard is `the release landed on the same focusable as the press`",
7845 );
7846 }
7847
7848 #[test]
7855 fn a_mouse_press_visual_is_unchanged() {
7856 let mut tree = WidgetTree::new();
7857 let (w, pressed) = tappable(&mut tree);
7858 tree.layout(SizeProposal::exact(100.0, 50.0));
7859 let center = tree.bounds(w).center();
7860
7861 assert!(!pressed.get(), "not pressed at rest");
7862 tree.dispatch_event(WidgetEvent::pointer_down(
7863 center,
7864 PointerButton::Primary,
7865 Modifiers::NONE,
7866 ));
7867 assert!(pressed.get(), "an indirect pointer lights up on the press");
7868 assert!(
7869 !tree.press_pending(w),
7870 "and never waits: a mouse opens no pan session, so there is no \
7871 ambiguity to wait out",
7872 );
7873 assert_eq!(tree.pressed_by(w), Some(PointerId::MOUSE));
7874
7875 tree.dispatch_event(WidgetEvent::pointer_up(
7876 center,
7877 PointerButton::Primary,
7878 Modifiers::NONE,
7879 ));
7880 assert!(!pressed.get(), "the release clears it");
7881 assert_eq!(tree.pressed_by(w), None);
7882 }
7883
7884 #[test]
7888 fn a_slide_off_clears_the_visual_and_re_entry_restores_it() {
7889 let (mut tree, _clock) = tree_on_a_clock();
7890 let (w, pressed) = tappable(&mut tree);
7891 tree.layout(SizeProposal::exact(100.0, 200.0));
7892 let inside = tree.bounds(w).center();
7893 let outside = Point::new(inside.x, inside.y + 400.0);
7894
7895 let id = contact_id();
7896 tree.dispatch_pointer(contact(id, PointerPhase::Down, inside, EventTime::ZERO));
7897 assert!(
7898 pressed.get(),
7899 "nothing here claims a pan, so no delay applies"
7900 );
7901
7902 tree.dispatch_pointer(contact(
7903 id,
7904 PointerPhase::Move,
7905 outside,
7906 EventTime::from_millis(20),
7907 ));
7908 assert!(!pressed.get(), "the press has left its target");
7909 assert!(!tree.press_is_inside(w));
7910 assert_eq!(
7911 tree.pressed_by(w),
7912 Some(id),
7913 "the contact still holds the press — it is the *visual* that is off",
7914 );
7915
7916 tree.dispatch_pointer(contact(
7917 id,
7918 PointerPhase::Move,
7919 inside,
7920 EventTime::from_millis(40),
7921 ));
7922 assert!(pressed.get(), "sliding back on restores it");
7923
7924 tree.dispatch_pointer(contact(
7925 id,
7926 PointerPhase::Up,
7927 inside,
7928 EventTime::from_millis(60),
7929 ));
7930 assert!(!pressed.get());
7931 }
7932
7933 #[test]
7936 fn a_second_contact_cannot_clear_the_first_visual() {
7937 let (mut tree, _clock) = tree_on_a_clock();
7938 let (w, pressed) = tappable(&mut tree);
7939 tree.layout(SizeProposal::exact(100.0, 50.0));
7940 let bounds = tree.bounds(w);
7941 let first_at = Point::new(bounds.x + 20.0, bounds.y + 25.0);
7942 let second_at = Point::new(bounds.x + 70.0, bounds.y + 25.0);
7943
7944 let first = contact_id();
7945 let second = contact_id();
7946 tree.dispatch_pointer(contact(
7947 first,
7948 PointerPhase::Down,
7949 first_at,
7950 EventTime::ZERO,
7951 ));
7952 assert!(pressed.get());
7953 assert_eq!(tree.pressed_by(w), Some(first));
7954
7955 tree.dispatch_pointer(contact(
7956 second,
7957 PointerPhase::Down,
7958 second_at,
7959 EventTime::from_millis(10),
7960 ));
7961 assert_eq!(
7962 tree.pressed_by(w),
7963 Some(first),
7964 "under `MultiContact::First` the second contact is terminated before \
7965 it reaches the arena at all",
7966 );
7967
7968 tree.dispatch_pointer(contact(
7969 second,
7970 PointerPhase::Up,
7971 second_at,
7972 EventTime::from_millis(20),
7973 ));
7974 assert!(
7975 pressed.get(),
7976 "so the second contact's release cannot clear a visual it never owned",
7977 );
7978 assert_eq!(tree.pressed_by(w), Some(first));
7979
7980 tree.dispatch_pointer(contact(
7981 first,
7982 PointerPhase::Up,
7983 first_at,
7984 EventTime::from_millis(30),
7985 ));
7986 assert!(!pressed.get(), "the owner's release does clear it");
7987 }
7988
7989 #[test]
7992 fn a_cancel_clears_the_visual() {
7993 let (mut tree, _clock) = tree_on_a_clock();
7994 let (w, pressed) = tappable(&mut tree);
7995 tree.layout(SizeProposal::exact(100.0, 50.0));
7996 let center = tree.bounds(w).center();
7997
7998 let id = contact_id();
7999 tree.dispatch_pointer(contact(id, PointerPhase::Down, center, EventTime::ZERO));
8000 assert!(pressed.get());
8001
8002 tree.dispatch_pointer(contact(
8003 id,
8004 PointerPhase::Cancel,
8005 center,
8006 EventTime::from_millis(20),
8007 ));
8008 assert!(!pressed.get(), "a press that was taken away is not painted");
8009 assert_eq!(tree.pressed_by(w), None);
8010 }
8011
8012 #[test]
8015 fn a_pan_claim_clears_the_visual() {
8016 let (mut tree, clock) = tree_on_a_clock();
8017 let (row, pressed) = tappable(&mut tree);
8018 let list = tree.add(
8019 StackWidget::new()
8020 .child(row)
8021 .pan_claim(PanClaim::vertical())
8022 .on_scroll(|_e, _c| EventResponse::Handled),
8023 );
8024 tree.layout(SizeProposal::exact(200.0, 400.0));
8025 let _ = list;
8026 let start = Point::new(100.0, 200.0);
8027
8028 let id = contact_id();
8029 tree.dispatch_pointer(contact(id, PointerPhase::Down, start, EventTime::ZERO));
8030 assert!(tree.press_pending(row));
8032 clock.set(EventTime::from_millis(100));
8033 tree.tick_gestures(std::time::Instant::now());
8034 assert!(pressed.get(), "…and appears once the delay has elapsed");
8035
8036 tree.dispatch_pointer(contact(
8038 id,
8039 PointerPhase::Move,
8040 Point::new(100.0, 260.0),
8041 EventTime::from_millis(120),
8042 ));
8043 assert!(
8044 !pressed.get(),
8045 "the row lost the press to the list and must stop advertising it",
8046 );
8047 assert_eq!(tree.pressed_by(row), None);
8048 }
8049
8050 #[test]
8059 fn a_button_the_control_cannot_act_on_raises_no_visual() {
8060 let taps = Rc::new(Cell::new(0u32));
8061 let count = taps.clone();
8062 let mut tree = WidgetTree::new();
8063 let w = tree.add(FillWidget::new().focusable().on_tap(move |_e, _c| {
8064 count.set(count.get() + 1);
8065 }));
8066 let pressed = tree.pressed_signal(w);
8067 tree.layout(SizeProposal::exact(100.0, 50.0));
8068 let center = tree.bounds(w).center();
8069
8070 for button in [
8071 PointerButton::Middle,
8072 PointerButton::Back,
8073 PointerButton::Forward,
8074 ] {
8075 tree.dispatch_event(WidgetEvent::pointer_down(center, button, Modifiers::NONE));
8076 assert!(
8077 !pressed.get(),
8078 "{button:?} cannot activate an `on_tap` node, so it must not light one up",
8079 );
8080 assert_eq!(tree.pressed_by(w), None, "and owns no visual to clear");
8081 tree.dispatch_event(WidgetEvent::pointer_up(center, button, Modifiers::NONE));
8082 assert_eq!(
8083 taps.get(),
8084 0,
8085 "the tap recognizer refuses {button:?} too — that is the point",
8086 );
8087 }
8088
8089 tree.dispatch_event(WidgetEvent::pointer_down(
8091 center,
8092 PointerButton::Primary,
8093 Modifiers::NONE,
8094 ));
8095 assert!(pressed.get(), "a primary press lights up as it always has");
8096 assert_eq!(tree.pressed_by(w), Some(PointerId::MOUSE));
8097 tree.dispatch_event(WidgetEvent::pointer_up(
8098 center,
8099 PointerButton::Primary,
8100 Modifiers::NONE,
8101 ));
8102 assert!(!pressed.get());
8103 assert_eq!(taps.get(), 1);
8104 }
8105
8106 #[test]
8110 fn a_secondary_press_with_no_context_menu_raises_no_visual() {
8111 let mut tree = WidgetTree::new();
8112 let (w, pressed) = tappable(&mut tree);
8113 tree.layout(SizeProposal::exact(100.0, 50.0));
8114 let center = tree.bounds(w).center();
8115
8116 tree.dispatch_event(WidgetEvent::pointer_down(
8117 center,
8118 PointerButton::Secondary,
8119 Modifiers::NONE,
8120 ));
8121 assert!(
8122 !pressed.get(),
8123 "nothing opened, and nothing may look pressed either",
8124 );
8125 assert_eq!(tree.pressed_by(w), None);
8126 assert!(!tree.press_is_inside(w));
8127 }
8128
8129 #[test]
8133 fn a_widened_mask_widens_the_visual_with_it() {
8134 use crate::event::ButtonMask;
8135
8136 let mut tree = WidgetTree::new();
8137 let w = tree.add(
8138 FillWidget::new()
8139 .on_tap(|_e, _c| {})
8140 .accept_tap_buttons(ButtonMask::PRIMARY | ButtonMask::MIDDLE),
8141 );
8142 let pressed = tree.pressed_signal(w);
8143 tree.layout(SizeProposal::exact(100.0, 50.0));
8144 let center = tree.bounds(w).center();
8145
8146 tree.dispatch_event(WidgetEvent::pointer_down(
8147 center,
8148 PointerButton::Middle,
8149 Modifiers::NONE,
8150 ));
8151 assert!(pressed.get(), "this node really does act on a middle-click");
8152
8153 tree.dispatch_event(WidgetEvent::pointer_up(
8154 center,
8155 PointerButton::Middle,
8156 Modifiers::NONE,
8157 ));
8158 assert!(!pressed.get());
8159 }
8160
8161 #[test]
8167 fn a_press_that_raises_no_visual_still_defers_its_focus() {
8168 use teksilo_tokens::{PenKind, PointerKind};
8169
8170 let (mut tree, _clock) = tree_on_a_clock();
8171 let (w, pressed) = tappable(&mut tree);
8172 tree.layout(SizeProposal::exact(100.0, 50.0));
8173 let center = tree.bounds(w).center();
8174
8175 let id = contact_id();
8176 let barrel = |phase, t| PointerSample {
8177 pointer: PointerInfo {
8178 kind: PointerKind::Pen(PenKind::Pen),
8179 ..PointerInfo::touch(id, t)
8180 },
8181 phase,
8182 position: center,
8183 button: Some(PointerButton::Secondary),
8184 modifiers: Modifiers::NONE,
8185 coalesced: Vec::new(),
8186 };
8187
8188 tree.dispatch_pointer(barrel(PointerPhase::Down, EventTime::ZERO));
8189 assert!(!pressed.get(), "the barrel button activates nothing here");
8190 assert_eq!(tree.pressed_by(w), None);
8191 assert_eq!(
8192 tree.focused(),
8193 None,
8194 "a direct pointer has chosen nothing until it lifts",
8195 );
8196
8197 tree.dispatch_pointer(barrel(PointerPhase::Up, EventTime::from_millis(40)));
8198 assert_eq!(
8199 tree.focused(),
8200 Some(w),
8201 "the deferral is not button-gated: the release landed where the press did",
8202 );
8203 assert_eq!(
8204 tree.focus_origin().and_then(FocusOrigin::pointer_kind),
8205 Some(PointerKind::Pen(PenKind::Pen)),
8206 );
8207 }
8208
8209 #[test]
8216 fn the_feedback_delay_applies_only_inside_a_claimant() {
8217 let (mut tree, _clock) = tree_on_a_clock();
8219 let (w, pressed) = tappable(&mut tree);
8220 tree.layout(SizeProposal::exact(200.0, 400.0));
8221 let id = contact_id();
8222 let at = tree.bounds(w).center();
8223 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8224 assert!(
8225 !tree.press_pending(w),
8226 "no claimant above it, so nothing to rule out",
8227 );
8228 assert!(pressed.get());
8229
8230 let (mut tree, clock) = tree_on_a_clock();
8232 let (row, pressed) = tappable(&mut tree);
8233 let list = tree.add(
8234 StackWidget::new()
8235 .child(row)
8236 .pan_claim(PanClaim::vertical())
8237 .on_scroll(|_e, _c| EventResponse::Handled),
8238 );
8239 tree.layout(SizeProposal::exact(200.0, 400.0));
8240 let _ = list;
8241 let id = contact_id();
8242 let at = Point::new(100.0, 200.0);
8243 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8244 assert!(tree.press_pending(row), "a finger might be about to scroll");
8245 assert!(!pressed.get(), "so the row does not flash");
8246 assert!(
8247 tree.press_is_inside(row),
8248 "the press is real; only its visual is being withheld",
8249 );
8250
8251 clock.set(EventTime::from_millis(99));
8252 tree.tick_gestures(std::time::Instant::now());
8253 assert!(!pressed.get(), "99 ms is under the 100 ms delay");
8254
8255 clock.set(EventTime::from_millis(100));
8256 tree.tick_gestures(std::time::Instant::now());
8257 assert!(pressed.get(), "and 100 ms is it");
8258 assert!(!tree.press_pending(row));
8259 }
8260
8261 #[test]
8264 fn a_mouse_inside_a_claimant_never_waits() {
8265 let mut tree = WidgetTree::new();
8266 let (row, pressed) = tappable(&mut tree);
8267 let list = tree.add(
8268 StackWidget::new()
8269 .child(row)
8270 .pan_claim(PanClaim::vertical())
8271 .on_scroll(|_e, _c| EventResponse::Handled),
8272 );
8273 tree.layout(SizeProposal::exact(200.0, 400.0));
8274 let _ = list;
8275
8276 tree.dispatch_event(WidgetEvent::pointer_down(
8277 Point::new(100.0, 200.0),
8278 PointerButton::Primary,
8279 Modifiers::NONE,
8280 ));
8281 assert!(!tree.press_pending(row));
8282 assert!(pressed.get(), "Compact with a mouse is exactly as it was");
8283 }
8284
8285 #[test]
8292 fn a_handler_can_read_the_press_it_is_inside() {
8293 let (mut tree, _clock) = tree_on_a_clock();
8294 let seen: Rc<RefCell<Vec<(bool, bool, bool)>>> = Rc::new(RefCell::new(Vec::new()));
8295 let log = seen.clone();
8296 let row = tree.add(FillWidget::new().on_tap(|_e, _c| {}).on_pointer_event(
8297 move |event, ctx| {
8298 if matches!(event, WidgetEvent::PointerUp { .. }) {
8299 log.borrow_mut().push((
8300 ctx.is_pressed(),
8301 ctx.press_is_inside(),
8302 ctx.press_pending(),
8303 ));
8304 }
8305 EventResponse::Ignored
8306 },
8307 ));
8308 let list = tree.add(
8309 StackWidget::new()
8310 .child(row)
8311 .pan_claim(PanClaim::vertical())
8312 .on_scroll(|_e, _c| EventResponse::Handled),
8313 );
8314 tree.layout(SizeProposal::exact(200.0, 400.0));
8315 let _ = list;
8316
8317 let id = contact_id();
8318 let at = Point::new(100.0, 200.0);
8319 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8320 tree.dispatch_pointer(contact(
8321 id,
8322 PointerPhase::Up,
8323 at,
8324 EventTime::from_millis(30),
8325 ));
8326
8327 assert_eq!(
8328 seen.borrow().as_slice(),
8329 &[(false, true, true)],
8330 "released inside the claimant before the delay elapsed: inside, \
8331 pending, and therefore not yet showing",
8332 );
8333 }
8334
8335 #[test]
8338 fn a_handler_outside_a_press_reads_nothing() {
8339 let mut tree = WidgetTree::new();
8340 let asked = Rc::new(Cell::new(false));
8341 let flag = asked.clone();
8342 let w = tree.add(FillWidget::new().focusable().on_key(move |_e, ctx| {
8343 flag.set(ctx.is_pressed() || ctx.press_is_inside() || ctx.press_pending());
8344 EventResponse::Ignored
8345 }));
8346 tree.layout(SizeProposal::exact(100.0, 50.0));
8347 tree.focus(w);
8348 tree.press_key(Key::ArrowDown, Modifiers::NONE);
8349 assert!(!asked.get());
8350 }
8351
8352 #[test]
8356 fn a_completed_press_leaves_the_detector_clean() {
8357 let (mut tree, _clock) = tree_on_a_clock();
8358 let (w, pressed) = tappable(&mut tree);
8359 tree.layout(SizeProposal::exact(100.0, 50.0));
8360 let at = tree.bounds(w).center();
8361
8362 let id = contact_id();
8363 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8364 assert!(pressed.get());
8365 tree.dispatch_pointer(contact(
8366 id,
8367 PointerPhase::Up,
8368 at,
8369 EventTime::from_millis(30),
8370 ));
8371 tree.assert_no_leaked_pointer_state();
8372 }
8373
8374 #[test]
8378 fn the_detector_reports_a_press_still_held() {
8379 let (mut tree, _clock) = tree_on_a_clock();
8380 let (w, _pressed) = tappable(&mut tree);
8381 tree.layout(SizeProposal::exact(100.0, 50.0));
8382 let at = tree.bounds(w).center();
8383
8384 let id = contact_id();
8385 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8386 let held = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
8387 tree.assert_no_leaked_pointer_state()
8388 }));
8389 let message = *held
8390 .expect_err("a live press is leaked state")
8391 .downcast::<String>()
8392 .expect("the detector panics with a message");
8393 assert!(
8394 message.contains("is still pressed by"),
8395 "the press is named in the report, got: {message}",
8396 );
8397 }
8398
8399 #[derive(Debug)]
8403 struct SideBySide {
8404 children: Vec<WidgetId>,
8405 }
8406
8407 impl crate::widget::Widget for SideBySide {
8408 fn layout_response(
8409 &self,
8410 proposal: SizeProposal,
8411 _ctx: &crate::widget::LayoutContext,
8412 ) -> crate::widget::LayoutResponse {
8413 proposal.resolve(0.0, 0.0).into()
8414 }
8415
8416 fn place_children(
8417 &self,
8418 bounds: teksilo_canvas::Rect,
8419 _proposal: SizeProposal,
8420 children: &mut [crate::widget::WidgetPlacement],
8421 _ctx: &crate::widget::LayoutContext,
8422 ) {
8423 let n = children.len().max(1) as f32;
8424 let w = bounds.width / n;
8425 for (i, child) in children.iter_mut().enumerate() {
8426 child.origin = Point::new(bounds.x + w * i as f32, bounds.y);
8427 child.size = teksilo_canvas::Size::new(w, bounds.height);
8428 }
8429 }
8430
8431 fn children(&self) -> Vec<WidgetId> {
8432 self.children.clone()
8433 }
8434 }
8435}
8436
8437#[cfg(test)]
8438mod overlay_release_dismissal_tests {
8439 use std::cell::Cell;
8449 use std::rc::Rc;
8450
8451 use teksilo_canvas::{Point, Rect, Size, SizeProposal};
8452
8453 use crate::WidgetId;
8454 use crate::event::{Modifiers, PointerButton, WidgetEvent};
8455 use crate::overlay::{DismissBehavior, OverlayPlacement, OverlayRequest};
8456 use crate::pointer::{
8457 BackendDeviceKey, EventTime, PointerId, PointerIdAllocator, PointerInfo, PointerPhase,
8458 PointerSample,
8459 };
8460 use crate::test_widgets::FillWidget;
8461 use crate::widget::{LayoutContext, LayoutResponse, Widget};
8462 use crate::widget_builder::WidgetBuilder;
8463 use crate::widget_tree::WidgetTree;
8464
8465 #[derive(Debug)]
8472 struct Panel(Size);
8473
8474 impl Widget for Panel {
8475 fn layout_response(&self, _proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
8476 self.0.into()
8477 }
8478 }
8479
8480 #[derive(Debug)]
8488 struct PageAndTrigger {
8489 children: Vec<WidgetId>,
8490 }
8491
8492 impl Widget for PageAndTrigger {
8493 fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
8494 proposal.resolve(0.0, 0.0).into()
8495 }
8496
8497 fn place_children(
8498 &self,
8499 bounds: Rect,
8500 _proposal: SizeProposal,
8501 children: &mut [crate::widget::WidgetPlacement],
8502 _ctx: &LayoutContext,
8503 ) {
8504 if let Some(page) = children.get_mut(0) {
8505 page.origin = bounds.origin();
8506 page.size = bounds.size();
8507 }
8508 if let Some(trigger) = children.get_mut(1) {
8509 trigger.origin = bounds.origin();
8510 trigger.size = Size::new(10.0, 10.0);
8511 }
8512 }
8513
8514 fn children(&self) -> Vec<WidgetId> {
8515 self.children.clone()
8516 }
8517 }
8518
8519 fn contact_id(n: u64) -> PointerId {
8520 PointerIdAllocator::global().begin(BackendDeviceKey::new(0x0FA2), n)
8521 }
8522
8523 fn touch(id: PointerId, phase: PointerPhase, at: Point) -> PointerSample {
8524 PointerSample {
8525 pointer: PointerInfo::touch(id, EventTime::ZERO),
8526 phase,
8527 position: at,
8528 button: None,
8529 modifiers: Modifiers::NONE,
8530 coalesced: Vec::new(),
8531 }
8532 }
8533
8534 fn page_with_a_menu() -> (WidgetTree, WidgetId, Rc<Cell<u32>>) {
8541 let mut tree = WidgetTree::new();
8542 let taps = Rc::new(Cell::new(0u32));
8543 let counter = taps.clone();
8544 let page = tree.add(
8545 FillWidget::new()
8546 .focusable()
8547 .on_tap(move |_e, _c| counter.set(counter.get() + 1)),
8548 );
8549 let trigger = tree.add(Panel(Size::new(10.0, 10.0)));
8552 let _root = tree.add(PageAndTrigger {
8553 children: vec![page, trigger],
8554 });
8555 let menu = tree.add(Panel(Size::new(200.0, 200.0)));
8556 tree.show_overlay(OverlayRequest::new(
8557 menu,
8558 trigger,
8559 OverlayPlacement::AtPointer(Point::new(100.0, 100.0)),
8560 DismissBehavior::ClickOutside,
8561 ));
8562 tree.layout(SizeProposal::exact(800.0, 600.0));
8563 assert_eq!(tree.active_overlays().len(), 1, "the menu is open");
8564 (tree, page, taps)
8565 }
8566
8567 fn outside() -> Point {
8568 Point::new(600.0, 500.0)
8569 }
8570
8571 fn inside_menu() -> Point {
8572 Point::new(150.0, 150.0)
8573 }
8574
8575 #[test]
8582 fn the_suppressed_down_leaves_nothing_pressed_or_captured_beneath() {
8583 let (mut tree, page, taps) = page_with_a_menu();
8584 let finger = contact_id(1);
8585 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8586
8587 assert_eq!(
8588 tree.active_overlays().len(),
8589 1,
8590 "the press does not close the menu; the release does"
8591 );
8592 assert_eq!(tree.pressed_by(page), None, "nothing beneath is pressed");
8593 let entry = tree.pointers.get(finger).expect("the contact is live");
8594 assert_eq!(entry.captured_by, None, "nothing beneath captured it");
8595 assert!(entry.sequence.is_none(), "no arbitration was opened");
8596 assert_eq!(taps.get(), 0);
8597 }
8598
8599 #[test]
8601 fn a_touch_tap_outside_a_menu_closes_it_and_actuates_nothing() {
8602 let (mut tree, page, taps) = page_with_a_menu();
8603 let finger = contact_id(2);
8604 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8605 tree.dispatch_pointer(touch(finger, PointerPhase::Up, outside()));
8606
8607 assert!(tree.active_overlays().is_empty(), "the menu closed");
8608 assert_eq!(taps.get(), 0, "the page beneath was never tapped");
8609 assert_eq!(tree.pressed_by(page), None);
8610 tree.assert_no_leaked_pointer_state();
8611 }
8612
8613 #[test]
8616 fn a_cancelled_press_aborts_the_arm() {
8617 let (mut tree, page, taps) = page_with_a_menu();
8618 let finger = contact_id(3);
8619 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8620 tree.dispatch_pointer(touch(finger, PointerPhase::Cancel, outside()));
8621
8622 assert_eq!(tree.active_overlays().len(), 1, "the menu survives");
8623 assert_eq!(taps.get(), 0);
8624 assert_eq!(tree.pressed_by(page), None);
8625 tree.assert_no_leaked_pointer_state();
8626 }
8627
8628 #[test]
8632 fn a_press_slid_onto_the_menu_dismisses_nothing() {
8633 let (mut tree, _page, taps) = page_with_a_menu();
8634 let finger = contact_id(4);
8635 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8636 tree.dispatch_pointer(touch(finger, PointerPhase::Move, inside_menu()));
8637 tree.dispatch_pointer(touch(finger, PointerPhase::Up, inside_menu()));
8638
8639 assert_eq!(tree.active_overlays().len(), 1, "the menu survives");
8640 assert_eq!(taps.get(), 0);
8641 tree.assert_no_leaked_pointer_state();
8642 }
8643
8644 #[test]
8647 fn a_second_contact_cannot_dismiss_what_the_first_is_manipulating() {
8648 let (mut tree, _page, _taps) = page_with_a_menu();
8649 let first = contact_id(5);
8650 let second = contact_id(6);
8651
8652 tree.dispatch_pointer(touch(first, PointerPhase::Down, inside_menu()));
8653 assert!(
8654 tree.pointers
8655 .get(first)
8656 .is_some_and(|e| e.sequence.is_some()),
8657 "the first contact holds a live press inside the menu"
8658 );
8659
8660 tree.dispatch_pointer(touch(second, PointerPhase::Down, outside()));
8661 tree.dispatch_pointer(touch(second, PointerPhase::Up, outside()));
8662 assert_eq!(
8663 tree.active_overlays().len(),
8664 1,
8665 "the menu the first finger is holding must not close under it"
8666 );
8667
8668 tree.dispatch_pointer(touch(first, PointerPhase::Up, inside_menu()));
8671 let third = contact_id(7);
8672 tree.dispatch_pointer(touch(third, PointerPhase::Down, outside()));
8673 tree.dispatch_pointer(touch(third, PointerPhase::Up, outside()));
8674 assert!(tree.active_overlays().is_empty());
8675 tree.assert_no_leaked_pointer_state();
8676 }
8677
8678 #[test]
8682 fn an_arm_is_not_itself_a_press_that_blocks_another_contact() {
8683 let (mut tree, _page, _taps) = page_with_a_menu();
8684 let first = contact_id(8);
8685 let second = contact_id(9);
8686
8687 tree.dispatch_pointer(touch(first, PointerPhase::Down, outside()));
8688 assert!(
8689 tree.overlay_manager().has_armed_dismiss(first),
8690 "the first contact armed"
8691 );
8692 assert!(tree.busy_press_points(second).is_empty());
8694
8695 tree.dispatch_pointer(touch(second, PointerPhase::Down, outside()));
8696 assert!(tree.overlay_manager().has_armed_dismiss(second));
8697 tree.dispatch_pointer(touch(second, PointerPhase::Up, outside()));
8698 assert!(tree.active_overlays().is_empty());
8699
8700 tree.dispatch_pointer(touch(first, PointerPhase::Up, outside()));
8703 tree.assert_no_leaked_pointer_state();
8704 }
8705
8706 #[test]
8709 fn a_touch_inside_the_menu_arms_nothing() {
8710 let (mut tree, _page, _taps) = page_with_a_menu();
8711 let finger = contact_id(10);
8712 tree.dispatch_pointer(touch(finger, PointerPhase::Down, inside_menu()));
8713 assert!(!tree.overlay_manager().has_armed_dismiss(finger));
8714 tree.dispatch_pointer(touch(finger, PointerPhase::Up, inside_menu()));
8715 assert_eq!(tree.active_overlays().len(), 1);
8716 tree.assert_no_leaked_pointer_state();
8717 }
8718
8719 #[test]
8729 fn a_contact_with_no_press_still_ends_when_the_platform_revokes_it() {
8730 let mut tree = WidgetTree::new();
8731 tree.layout(SizeProposal::exact(800.0, 600.0));
8732 let finger = contact_id(99);
8733 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8734 tree.dispatch_pointer(touch(finger, PointerPhase::Cancel, outside()));
8735 tree.assert_no_leaked_pointer_state();
8736 }
8737
8738 #[test]
8745 fn a_mouse_click_outside_a_menu_dismisses_on_the_press_and_falls_through() {
8746 let (mut tree, _page, taps) = page_with_a_menu();
8747 tree.dispatch_event(WidgetEvent::pointer_down(
8748 outside(),
8749 PointerButton::Primary,
8750 Modifiers::NONE,
8751 ));
8752 assert!(
8753 tree.active_overlays().is_empty(),
8754 "the mouse still dismisses on the press"
8755 );
8756 assert!(
8757 tree.pointers
8758 .get(PointerId::MOUSE)
8759 .is_some_and(|e| e.sequence.is_some()),
8760 "and the press still reaches the page beneath"
8761 );
8762 tree.dispatch_event(WidgetEvent::pointer_up(
8763 outside(),
8764 PointerButton::Primary,
8765 Modifiers::NONE,
8766 ));
8767 assert_eq!(taps.get(), 1, "the control beneath activated");
8768 assert!(
8769 !tree.overlay_manager().has_armed_dismiss(PointerId::MOUSE),
8770 "a mouse never arms"
8771 );
8772 tree.assert_no_leaked_pointer_state();
8773 }
8774
8775 #[test]
8782 fn a_context_menu_avoids_the_contact_that_opened_it() {
8783 fn menu_bounds(pointer: PointerInfo, at: Point) -> Rect {
8784 let mut tree = WidgetTree::new();
8785 let page = tree.add(
8786 FillWidget::new()
8787 .focusable()
8788 .context_menu(|_p, _ctx| Some(Box::new(Panel(Size::new(200.0, 160.0))))),
8789 );
8790 let _ = page;
8791 tree.layout(SizeProposal::exact(800.0, 600.0));
8792 tree.dispatch_pointer(PointerSample {
8793 pointer,
8794 phase: PointerPhase::Down,
8795 position: at,
8796 button: Some(PointerButton::Secondary),
8797 modifiers: Modifiers::NONE,
8798 coalesced: Vec::new(),
8799 });
8800 tree.layout(SizeProposal::exact(800.0, 600.0));
8801 let id = *tree
8802 .active_overlays()
8803 .first()
8804 .expect("the context menu opened");
8805 tree.overlay_manager().bounds_for(id).expect("bounds")
8806 }
8807
8808 let at = Point::new(400.0, 300.0);
8809 let finger = menu_bounds(PointerInfo::touch(contact_id(11), EventTime::ZERO), at);
8810 let contact = crate::overlay::rect_centred_on(at, crate::overlay::ASSUMED_CONTACT_PATCH);
8811 assert!(
8812 finger.x < contact.x && finger.right() <= contact.x,
8813 "a touch menu clears the contact patch: {finger:?} vs {contact:?}"
8814 );
8815
8816 let mouse = menu_bounds(PointerInfo::mouse(EventTime::ZERO), at);
8817 assert_eq!(
8818 (mouse.x, mouse.y),
8819 (at.x, at.y),
8820 "a mouse menu still opens with its corner on the pointer"
8821 );
8822 }
8823}
8824
8825#[cfg(test)]
8841mod pass_through_overlay_tests {
8842 use teksilo_canvas::{Point, Rect, SizeProposal};
8843
8844 use crate::WidgetId;
8845 use crate::overlay::{DismissBehavior, OverlayPlacement, OverlayRequest};
8846 use crate::test_widgets::FillWidget;
8847 use crate::widget::{LayoutContext, LayoutResponse, Widget};
8848 use crate::widget_builder::WidgetBuilder;
8849 use crate::widget_tree::WidgetTree;
8850
8851 #[derive(Debug)]
8854 struct HandleLayer {
8855 handle: WidgetId,
8856 at: Rect,
8857 }
8858
8859 impl Widget for HandleLayer {
8860 fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
8861 proposal.resolve(0.0, 0.0).into()
8862 }
8863
8864 fn place_children(
8865 &self,
8866 _bounds: Rect,
8867 _proposal: SizeProposal,
8868 children: &mut [crate::widget::WidgetPlacement],
8869 _ctx: &LayoutContext,
8870 ) {
8871 if let Some(child) = children.get_mut(0) {
8872 child.origin = self.at.origin();
8873 child.size = self.at.size();
8874 }
8875 }
8876
8877 fn children(&self) -> Vec<WidgetId> {
8878 vec![self.handle]
8879 }
8880 }
8881
8882 const HANDLE: Rect = Rect {
8883 x: 10.0,
8884 y: 10.0,
8885 width: 20.0,
8886 height: 20.0,
8887 };
8888
8889 #[derive(Clone, Copy, PartialEq)]
8891 enum RootFlag {
8892 PassThrough,
8894 HitTransparent,
8897 }
8898
8899 fn field_under_a_layer(flag: RootFlag) -> (WidgetTree, WidgetId, WidgetId) {
8905 let mut tree = WidgetTree::new();
8906 let field = tree.add(FillWidget::new());
8907 let handle = tree.add(FillWidget::new());
8908 let layer = HandleLayer { handle, at: HANDLE };
8909 let layer = match flag {
8910 RootFlag::PassThrough => tree.add(layer.event_pass_through(true)),
8911 RootFlag::HitTransparent => tree.add(layer.hit_transparent(true)),
8912 };
8913 tree.show_overlay(OverlayRequest::new(
8914 layer,
8915 field,
8916 OverlayPlacement::FullViewport,
8917 DismissBehavior::Manual,
8918 ));
8919 tree.layout(SizeProposal::exact(400.0, 200.0));
8920 assert_eq!(tree.active_overlays().len(), 1, "the layer is up");
8921 (tree, field, handle)
8922 }
8923
8924 #[test]
8926 fn a_pass_through_layer_hands_back_what_it_did_not_claim() {
8927 let (tree, field, handle) = field_under_a_layer(RootFlag::PassThrough);
8928
8929 assert_eq!(
8930 tree.hit_test(Point::new(15.0, 15.0)),
8931 Some(handle),
8932 "the layer must still win the point its own child covers"
8933 );
8934 assert_eq!(
8935 tree.hit_test(Point::new(200.0, 100.0)),
8936 Some(field),
8937 "a press the layer did not claim must reach the surface beneath it"
8938 );
8939 }
8940
8941 #[test]
8952 fn a_pass_through_root_still_hears_a_press_that_landed_on_its_child() {
8953 use std::cell::Cell;
8954 use std::rc::Rc;
8955
8956 let seen = Rc::new(Cell::new(0u32));
8957 let counter = seen.clone();
8958 let mut tree = WidgetTree::new();
8959 let field = tree.add(FillWidget::new());
8960 let handle = tree.add(FillWidget::new().on_tap(|_e, _c| {}));
8961 let layer = tree.add(
8962 HandleLayer { handle, at: HANDLE }
8963 .event_pass_through(true)
8964 .on_pointer_event(move |_event, _ctx| {
8965 counter.set(counter.get() + 1);
8966 crate::event::EventResponse::Ignored
8967 }),
8968 );
8969 tree.show_overlay(OverlayRequest::new(
8970 layer,
8971 field,
8972 OverlayPlacement::FullViewport,
8973 DismissBehavior::Manual,
8974 ));
8975 tree.layout(SizeProposal::exact(400.0, 200.0));
8976
8977 tree.pointer_down_button(Point::new(15.0, 15.0), crate::event::PointerButton::Primary);
8978 assert!(
8979 seen.get() > 0,
8980 "a pass-through root heard nothing about a press on its own child"
8981 );
8982
8983 let after_child = seen.get();
8984 tree.pointer_up_button(Point::new(15.0, 15.0), crate::event::PointerButton::Primary);
8985 tree.pointer_down_button(
8988 Point::new(200.0, 100.0),
8989 crate::event::PointerButton::Primary,
8990 );
8991 assert_eq!(
8992 seen.get(),
8993 after_child + 1,
8994 "the press-and-lift on the child accounts for the count, and the \
8995 press on the surface beneath added nothing"
8996 );
8997 }
8998
8999 #[test]
9013 fn a_hit_transparent_root_does_not_get_the_fall_through() {
9014 let (tree, field, _handle) = field_under_a_layer(RootFlag::HitTransparent);
9015
9016 assert_eq!(
9018 tree.hit_test(Point::new(15.0, 15.0)),
9019 None,
9020 "hit_transparent must exclude the subtree, or this fixture is not \
9021 testing the gate"
9022 );
9023 assert_eq!(
9024 tree.hit_test(Point::new(200.0, 100.0)),
9025 None,
9026 "the fall-through reached past an overlay that did not ask for it"
9027 );
9028 let _ = field;
9029 }
9030}