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 for callback in ctx.idle_callbacks {
2896 self.idle_queue.push_boxed(callback);
2897 }
2898 match ctx.dismiss_scope {
2899 Some(crate::widget::DismissScope::All) => {
2900 let dismissed = self.overlay_manager.dismiss_all();
2901 self.dormant_dismissed_content(&dismissed, &mut *ops);
2902 }
2903 Some(crate::widget::DismissScope::AllExceptHosts) => {
2904 self.dismiss_all_overlays_except_hosts(&mut *ops);
2905 }
2906 Some(crate::widget::DismissScope::SelfChain) => {
2907 self.dismiss_self_overlay_chain_for_source(source_widget, &mut *ops);
2908 }
2909 Some(crate::widget::DismissScope::Top) => {
2910 if let Some((_id, content_ids, focus_restore)) = self.overlay_manager.dismiss_top()
2911 {
2912 self.dormant_dismissed_content(&content_ids, &mut *ops);
2913 if let Some(restore_id) = focus_restore
2914 && self.arena.is_active(restore_id)
2915 {
2916 self.focus_ops(restore_id, &mut *ops);
2917 }
2918 }
2919 }
2920 None => {
2921 for id in ctx.overlay_dismissals {
2922 let dismissed = self.overlay_manager.dismiss(id);
2923 self.dormant_dismissed_content(&dismissed, &mut *ops);
2924 }
2925 }
2926 }
2927 for content_id in ctx.overlay_content_dismissals {
2934 if let Some(overlay_id) = self.overlay_manager.find_by_content(content_id) {
2935 let dismissed = self.overlay_manager.dismiss(overlay_id);
2936 self.dormant_dismissed_content(&dismissed, &mut *ops);
2937 }
2938 }
2939 for (id, pause) in ctx.overlay_pause_requests {
2946 if pause {
2947 self.overlay_manager.pause_auto_dismiss(id);
2948 } else {
2949 self.overlay_manager.resume_auto_dismiss(id);
2950 }
2951 }
2952 for preserve_content in ctx.dismiss_descendant_overlays {
2953 self.dismiss_child_overlays_for_source(source_widget, preserve_content, &mut *ops);
2954 }
2955 let deferred_row_activations =
2956 self.apply_tree_mutations(std::mem::take(&mut ctx.tree_mutations));
2957 if ctx.request_a11y_update {
2958 self.a11y_dirty = true;
2959 }
2960 if let Some(visible) = ctx.soft_keyboard_request.take() {
2961 self.request_soft_keyboard(visible);
2962 }
2963 for (message, politeness) in std::mem::take(&mut ctx.announcements) {
2966 self.announce_with(message, politeness);
2967 }
2968 for mut req in ctx.overlay_requests {
2969 if req.parent_overlay.is_none() {
2970 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
2971 }
2972 if self
2973 .overlay_manager
2974 .find_by_content(req.content_id)
2975 .is_some()
2976 {
2977 continue;
2978 }
2979 let current_focus = self.focused;
2980 self.overlay_manager.show(req);
2981 self.a11y_dirty = true;
2985 if let Some(focus_id) = current_focus {
2986 self.overlay_manager.set_top_focus_restore(focus_id);
2987 }
2988 }
2989 for (mut req, band) in ctx.overlay_band_requests {
2990 if req.parent_overlay.is_none() {
2991 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
2992 }
2993 if self
2994 .overlay_manager
2995 .find_by_content(req.content_id)
2996 .is_some()
2997 {
2998 continue;
2999 }
3000 let content_id = req.content_id;
3001 self.overlay_manager.show_in_band(req, band);
3002 self.arena.activate(content_id);
3003 self.a11y_dirty = true;
3004 }
3008 for (content_id, placement) in ctx.overlay_placement_updates {
3011 if let Some(overlay_id) = self.overlay_manager.find_by_content(content_id) {
3012 self.overlay_manager.update_placement(overlay_id, placement);
3013 }
3014 }
3015 for (content_id, source) in ctx.overlay_placement_sources {
3016 if let Some(overlay_id) = self.overlay_manager.find_by_content(content_id) {
3017 self.overlay_manager
3018 .set_placement_source(overlay_id, source);
3019 }
3020 }
3021 for (mut req, duration) in ctx.timed_overlay_requests {
3022 if req.parent_overlay.is_none() {
3023 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3024 }
3025 if self
3026 .overlay_manager
3027 .find_by_content(req.content_id)
3028 .is_some()
3029 {
3030 continue;
3031 }
3032 let current_focus = self.focused;
3033 let overlay_id = self.overlay_manager.show_for(req, duration);
3034 self.overlay_manager
3035 .set_shown_at_sim(overlay_id, self.sim_clock);
3036 self.a11y_dirty = true;
3037 if let Some(focus_id) = current_focus {
3038 self.overlay_manager.set_top_focus_restore(focus_id);
3039 }
3040 }
3041 for (mut req, progress, duration) in ctx.reveal_overlay_requests {
3042 if req.parent_overlay.is_none() {
3043 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3044 }
3045 if self
3046 .overlay_manager
3047 .find_by_content(req.content_id)
3048 .is_some()
3049 {
3050 continue;
3051 }
3052 let content_id = req.content_id;
3053 let current_focus = self.focused;
3054 let overlay_id = self.overlay_manager.show(req);
3055 self.overlay_manager
3056 .set_shown_at_sim(overlay_id, self.sim_clock);
3057 self.a11y_dirty = true;
3058 if let Some(focus_id) = current_focus {
3059 self.overlay_manager.set_top_focus_restore(focus_id);
3060 }
3061 self.register_animated_signal(&progress, content_id);
3067 let _ = progress.try_animate_with_options(crate::animation::AnimationRequest {
3068 target: 1.0,
3069 duration,
3070 easing: teksilo_tokens::Easing::EaseOut,
3071 frame_interval: None,
3072 looping: false,
3073 epsilon: 0.0,
3074 max_duration: None,
3075 });
3076 self.overlay_manager
3077 .attach_fade(overlay_id, progress, duration);
3078 }
3079 if let Some((pointer, capture)) = ctx.pointer_capture {
3080 let named = pointer;
3084 let pointer = pointer.unwrap_or_else(|| self.current_pointer_id());
3085 self.set_pointer_capture(pointer, capture.then_some(source_widget));
3086 if capture && ctx.explicit_capture && named.is_none() {
3090 self.note_explicit_capture(source_widget);
3091 }
3092 }
3093 if let Some(activation) = ctx.drag_activation_override.take() {
3094 self.note_drag_activation_override(source_widget, activation);
3098 }
3099 if ctx.recognized_owning_gesture {
3100 self.note_gesture_recognized(source_widget);
3103 }
3104 if !ctx.gesture_acts.is_empty() {
3105 let acts = std::mem::take(&mut ctx.gesture_acts);
3106 self.apply_gesture_acts(&acts, source_widget);
3107 }
3108 if let Some(reason) = ctx.cancel_pointer_request {
3109 let pointer = self.current_pointer_id();
3110 self.cancel_pointer(pointer, reason, &mut *ops);
3111 }
3112 for (mut request, delay, focus_target, replace_siblings) in ctx.delayed_overlay_requests {
3113 if request.parent_overlay.is_none() {
3114 request.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3115 }
3116 if self
3117 .overlay_manager
3118 .find_by_content(request.content_id)
3119 .is_some()
3120 {
3121 continue;
3122 }
3123 let content_id = request.content_id;
3124 self.pending_delayed_overlays
3125 .retain(|pending| pending.request.content_id != content_id);
3126 self.pending_delayed_overlays.push(PendingDelayedOverlay {
3127 request,
3128 delay,
3129 focus_target,
3130 replace_siblings,
3131 real_requested_at: std::time::Instant::now(),
3132 sim_requested_at: self.sim_clock,
3133 });
3134 self.arena.mark_needs_paint(source_widget);
3135 }
3136 for content_id in ctx.cancel_delayed_overlays {
3137 self.pending_delayed_overlays
3138 .retain(|pending| pending.request.content_id != content_id);
3139 }
3140 for content_id in ctx.safe_region_arm_requests {
3147 if let Some(apex) = self
3148 .previous_pointer_position
3149 .or_else(|| self.hover_owner_position())
3150 {
3151 self.overlay_manager.arm_safe_region(
3152 content_id,
3153 apex,
3154 std::time::Instant::now(),
3155 self.sim_clock,
3156 );
3157 }
3158 }
3159 for id in ctx.repaint_requests {
3160 self.arena.mark_needs_paint(id);
3161 }
3162 for id in ctx.synthetic_clicks {
3163 self.synthesise_tap_with_ops(id, &mut *ops);
3168 }
3169 if ctx.clear_focus_requested {
3172 self.clear_focus_ops(&mut *ops);
3173 }
3174 if let Some(&id) = ctx.focus_requests.last() {
3179 self.request_focus_with_ops(id, super::FocusRequestMode::SelfOrDescendant, &mut *ops);
3189 }
3190 if let Some(&id) = ctx.focus_into_requests.last() {
3191 self.request_focus_with_ops(id, super::FocusRequestMode::Into, &mut *ops);
3198 }
3199
3200 for req in ctx.scroll_into_view_requests {
3209 self.scroll_rect_into_view(
3210 req.from.unwrap_or(source_widget),
3213 req.rect,
3214 req.margin,
3215 req.align,
3216 req.motion,
3217 &mut *ops,
3218 );
3219 }
3220 for (id, margin) in ctx.scroll_widget_into_view_requests {
3227 if self.arena.get(id).is_some() {
3228 let bounds = self.arena.bounds(id);
3229 self.scroll_rect_into_view(
3230 id,
3231 bounds,
3232 margin,
3233 crate::event::ScrollAlign::Minimal,
3234 crate::event::ScrollMotion::Instant,
3235 &mut *ops,
3236 );
3237 }
3238 }
3239
3240 if let Some(&id) = ctx.highlight_tooltip_requests.last() {
3245 self.show_highlight_tooltip(id, &mut *ops);
3246 }
3247
3248 if let Some((source_widget, payload, preview_widget)) = ctx.drag_start_request {
3250 let (preview_content_id, preview_overlay_id) = if let Some(preview) = preview_widget {
3251 let content_id = self.add_boxed(preview);
3258 let overlay_id = self
3259 .overlay_manager
3260 .show(crate::overlay::OverlayRequest::new(
3261 content_id,
3262 source_widget,
3263 crate::overlay::OverlayPlacement::AtPointer(teksilo_canvas::Point::ZERO),
3264 crate::overlay::DismissBehavior::Manual,
3265 ));
3266 self.arena.mark_needs_layout(content_id);
3270 (Some(content_id), Some(overlay_id))
3271 } else {
3272 (None, None)
3273 };
3274 self.active_drag = Some(crate::drag_state::DragSession {
3275 payload,
3276 pointer: self.current_input.pointer,
3282 source_widget: Some(source_widget),
3283 is_external: false,
3284 current_position: teksilo_canvas::Point::ZERO,
3285 current_target: None,
3286 feedback: crate::drag_state::DropFeedback::NoFeedback,
3287 preview_content_id,
3288 preview_overlay_id,
3289 });
3290 self.set_current_pointer_capture(Some(source_widget));
3291 self.current_cursor = crate::widget::CursorIcon::Grabbing;
3294 }
3295 if ctx.cancel_drag {
3296 self.cancel_active_drag(&mut *ops);
3297 }
3298
3299 if let Some(theme) = ctx.theme_request {
3301 self.pending_theme_request = Some(theme);
3307 }
3308 if ctx.follow_system_request {
3309 self.pending_follow_system_request = true;
3313 }
3314 if let Some(locale) = ctx.locale_request {
3315 self.pending_locale_request = Some(locale);
3321 }
3322 if let Some(scale) = ctx.text_scale_request {
3323 self.pending_text_scale_request = Some(scale);
3328 }
3329 if !deferred_row_activations.is_empty() {
3336 self.run_with_event_context(&mut *ops, move |ctx| {
3337 for action in deferred_row_activations {
3338 action(ctx);
3339 }
3340 });
3341 }
3342 }
3343
3344 #[must_use]
3350 fn apply_tree_mutations(
3351 &mut self,
3352 mutations: Vec<crate::widget::TreeMutation>,
3353 ) -> Vec<std::rc::Rc<dyn Fn(&mut crate::widget::EventContext)>> {
3354 let mut deferred_row_activations = Vec::new();
3355 use crate::binding::BindingLevel;
3356 use crate::widget::TreeMutation;
3357
3358 for mutation in mutations {
3359 match mutation {
3360 TreeMutation::SetDormant(id) => {
3361 self.park_subtree(id);
3362 }
3363 TreeMutation::Activate(id) => self.arena.activate(id),
3364 TreeMutation::Destroy(id) => {
3365 if let Some(overlay_id) = self.overlay_manager().find_by_content(id) {
3378 self.dismiss_overlay(overlay_id);
3379 }
3380 self.destroy_subtree(id);
3381 }
3382 TreeMutation::MaterializeNow(id) => {
3386 if self.arena.get(id).is_some() {
3387 self.rebuild_single_widget(id);
3388 }
3389 }
3390 TreeMutation::RowSpaceActivate { row, fallback } => {
3391 deferred_row_activations.push(self.keyboard_toggle_in(row).unwrap_or(fallback));
3401 }
3402 TreeMutation::WithWidgetMut { id, dirty, apply } => {
3403 let existed = if let Some(any) =
3408 self.arena.get_mut(id).and_then(|n| n.widget.as_any_mut())
3409 {
3410 apply(any);
3411 true
3412 } else {
3413 false
3414 };
3415 if existed {
3416 match dirty {
3417 BindingLevel::RepaintOnly => self.arena.mark_needs_paint(id),
3418 BindingLevel::SubtreeRepaint => self.arena.mark_subtree_needs_paint(id),
3419 BindingLevel::Relayout => {
3420 self.arena.mark_needs_layout(id);
3421 self.arena.mark_ancestors_need_layout(id);
3422 }
3423 BindingLevel::Rebuild => {
3424 self.arena.mark_needs_rebuild(id);
3425 self.arena.mark_ancestors_need_layout(id);
3426 }
3427 BindingLevel::AccessibilityOnly => self.a11y_dirty = true,
3428 }
3429 }
3430 }
3431 }
3432 }
3433 deferred_row_activations
3434 }
3435
3436 pub fn hit_test(&self, point: Point) -> Option<WidgetId> {
3445 self.hit_test_excluding_overlay_and_widget(point, None, None)
3446 }
3447
3448 pub fn hit_test_for(
3459 &self,
3460 point: Point,
3461 pointer: &crate::pointer::PointerInfo,
3462 ) -> Option<WidgetId> {
3463 self.hit_test_for_excluding(point, pointer, None, None)
3464 }
3465
3466 pub fn hit_test_for_excluding(
3469 &self,
3470 point: Point,
3471 pointer: &crate::pointer::PointerInfo,
3472 exclude_overlay: Option<crate::overlay::OverlayId>,
3473 exclude_widget: Option<WidgetId>,
3474 ) -> Option<WidgetId> {
3475 let surfaces = self.text_surfaces();
3476 let read_only = |id: WidgetId| surfaces.is_read_only(id);
3477 let hit =
3478 crate::pointer::hit_slop::HitContext::new(pointer.kind, &self.effective_theme.input)
3479 .direction(self.layout_direction)
3480 .read_only_probe(&read_only);
3481 self.hit_test_with(point, exclude_overlay, exclude_widget, &hit)
3482 }
3483
3484 pub fn hit_test_excluding_overlay_and_widget(
3491 &self,
3492 point: Point,
3493 exclude_overlay: Option<crate::overlay::OverlayId>,
3494 exclude_widget: Option<WidgetId>,
3495 ) -> Option<WidgetId> {
3496 self.hit_test_with(
3497 point,
3498 exclude_overlay,
3499 exclude_widget,
3500 &crate::pointer::hit_slop::HitContext::mouse(),
3501 )
3502 }
3503
3504 pub(crate) fn hit_test_with(
3507 &self,
3508 point: Point,
3509 exclude_overlay: Option<crate::overlay::OverlayId>,
3510 exclude_widget: Option<WidgetId>,
3511 hit: &crate::pointer::hit_slop::HitContext<'_>,
3512 ) -> Option<WidgetId> {
3513 if let Some(overlay_id) = self.overlay_manager.hit_test(point) {
3514 if Some(overlay_id) == exclude_overlay {
3515 } else if let Some(overlay) = self.overlay_manager.overlay(overlay_id) {
3517 let content_id = overlay.content_id;
3521 if let Some(found) =
3522 self.arena
3523 .hit_test_in_subtree_with_slop(content_id, point, exclude_widget, hit)
3524 {
3525 return Some(found);
3526 }
3527 if !self
3545 .arena
3546 .get(content_id)
3547 .is_some_and(|node| node.event_pass_through)
3548 {
3549 return None;
3550 }
3551 }
3552 }
3553
3554 if self.overlay_manager.topmost_centered().is_some() {
3555 return None;
3556 }
3557
3558 let mut roots = self.arena.roots();
3564 let inert: Vec<WidgetId> = self
3565 .overlay_manager
3566 .stack
3567 .iter()
3568 .filter(|overlay| overlay.inert)
3569 .map(|overlay| overlay.content_id)
3570 .collect();
3571 if !inert.is_empty() {
3572 roots.retain(|root| !inert.contains(root));
3573 }
3574 self.arena
3575 .hit_test_roots_with_slop(roots, point, exclude_widget, hit)
3576 }
3577}
3578
3579fn is_context_menu_chord(key: Key, modifiers: Modifiers) -> bool {
3597 match key {
3598 Key::ContextMenu => modifiers == Modifiers::NONE,
3599 Key::F10 => modifiers == Modifiers::SHIFT,
3600 #[cfg(target_os = "macos")]
3601 Key::M => modifiers == Modifiers::CTRL | Modifiers::SHIFT,
3602 _ => false,
3603 }
3604}
3605
3606#[cfg(test)]
3607mod tests {
3608 use super::*;
3609 use crate::test_widgets::FillWidget;
3610 use crate::widget::CursorIcon;
3611 use crate::widget_builder::WidgetBuilder;
3612
3613 #[test]
3614 fn pointer_enter_leave_synthesized() {
3615 let mut tree = WidgetTree::new();
3616 let widget = tree.add(FillWidget::new());
3617 tree.layout(SizeProposal::exact(100.0, 50.0));
3618 tree.pointer_move(Point::new(50.0, 25.0));
3619 assert_eq!(tree.hovered(), Some(widget));
3620 tree.pointer_move(Point::new(200.0, 200.0));
3621 assert_eq!(tree.hovered(), None);
3622 }
3623
3624 #[test]
3625 fn pointer_hover_updates_current_cursor() {
3626 let mut tree = WidgetTree::new();
3627 tree.add(FillWidget::new().cursor(CursorIcon::ColResize));
3628 tree.layout(SizeProposal::exact(100.0, 50.0));
3629
3630 tree.pointer_move(Point::new(50.0, 25.0));
3631 assert_eq!(tree.current_cursor(), CursorIcon::ColResize);
3632
3633 tree.pointer_move(Point::new(200.0, 200.0));
3634 assert_eq!(tree.current_cursor(), CursorIcon::Default);
3635 }
3636
3637 #[derive(Debug)]
3640 struct SelfDeclared(CursorIcon);
3641
3642 impl crate::widget::Widget for SelfDeclared {
3643 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
3644 ctx.apply_self_handlers(crate::widget_builder::HandlerSet::new().cursor(self.0));
3645 Vec::new()
3646 }
3647
3648 fn layout_response(
3649 &self,
3650 proposal: SizeProposal,
3651 _ctx: &crate::widget::LayoutContext,
3652 ) -> crate::widget::LayoutResponse {
3653 proposal.resolve(100.0, 50.0).into()
3654 }
3655 }
3656
3657 #[test]
3663 fn declared_cursor_reads_what_a_node_asks_for_and_current_cursor_follows_the_hover() {
3664 let over = Point::new(50.0, 25.0);
3665 let away = Point::new(200.0, 200.0);
3666
3667 let mut tree = WidgetTree::new();
3669 let built = tree.add(FillWidget::new().cursor(CursorIcon::ColResize));
3670 tree.layout(SizeProposal::exact(100.0, 50.0));
3671 assert_eq!(tree.declared_cursor(built), Some(CursorIcon::ColResize));
3672 assert_eq!(
3673 tree.current_cursor(),
3674 CursorIcon::Default,
3675 "not hovered yet"
3676 );
3677 tree.pointer_move(over);
3678 assert_eq!(tree.current_cursor(), CursorIcon::ColResize);
3679 assert_eq!(tree.declared_cursor(built), Some(CursorIcon::ColResize));
3680 tree.pointer_move(away);
3681 assert_eq!(tree.current_cursor(), CursorIcon::Default);
3682 assert_eq!(
3683 tree.declared_cursor(built),
3684 Some(CursorIcon::ColResize),
3685 "the declaration does not depend on the pointer",
3686 );
3687
3688 let mut tree = WidgetTree::new();
3690 let own = tree.add(SelfDeclared(CursorIcon::Crosshair));
3691 tree.layout(SizeProposal::exact(100.0, 50.0));
3692 assert_eq!(tree.declared_cursor(own), Some(CursorIcon::Crosshair));
3693 tree.pointer_move(over);
3694 assert_eq!(tree.current_cursor(), CursorIcon::Crosshair);
3695 tree.pointer_move(away);
3696 assert_eq!(tree.current_cursor(), CursorIcon::Default);
3697
3698 let mut tree = WidgetTree::new();
3700 let plain = tree.add(FillWidget::new());
3701 tree.layout(SizeProposal::exact(100.0, 50.0));
3702 assert_eq!(tree.declared_cursor(plain), None);
3703 let mut tree = WidgetTree::new();
3704 let explicit = tree.add(FillWidget::new().cursor(CursorIcon::Default));
3705 tree.layout(SizeProposal::exact(100.0, 50.0));
3706 assert_eq!(
3707 tree.declared_cursor(explicit),
3708 Some(CursorIcon::Default),
3709 "declaring the default cursor is not declaring nothing",
3710 );
3711 }
3712
3713 #[test]
3723 fn release_cursor_hands_the_cursor_back_to_the_node_that_declared_it() {
3724 let mut tree = WidgetTree::new();
3728 tree.add(
3729 FillWidget::new()
3730 .cursor(CursorIcon::ColResize)
3731 .on_pointer_event(|event, ctx| {
3732 if let WidgetEvent::PointerMove { position, .. } = event {
3733 if position.x < 30.0 {
3734 ctx.set_cursor(CursorIcon::Crosshair);
3735 } else {
3736 ctx.release_cursor();
3737 }
3738 }
3739 EventResponse::Ignored
3740 }),
3741 );
3742 tree.layout(SizeProposal::exact(100.0, 50.0));
3743
3744 tree.pointer_move(Point::new(10.0, 25.0));
3745 assert_eq!(
3746 tree.current_cursor(),
3747 CursorIcon::Crosshair,
3748 "the handler outranks the node it is attached to",
3749 );
3750 tree.pointer_move(Point::new(60.0, 25.0));
3751 assert_eq!(
3752 tree.current_cursor(),
3753 CursorIcon::ColResize,
3754 "and hands it back to the node, not to Default — no enter/leave \
3755 fires on a move within one node, so nothing else could",
3756 );
3757 tree.pointer_move(Point::new(10.0, 25.0));
3758 assert_eq!(
3759 tree.current_cursor(),
3760 CursorIcon::Crosshair,
3761 "and can take it again"
3762 );
3763 tree.pointer_move(Point::new(200.0, 200.0));
3764 assert_eq!(
3765 tree.current_cursor(),
3766 CursorIcon::Default,
3767 "leaving the node clears both the override and the declaration",
3768 );
3769 }
3770
3771 #[test]
3775 fn release_cursor_is_a_no_op_with_nothing_to_undo() {
3776 let mut tree = WidgetTree::new();
3777 tree.add(
3778 FillWidget::new()
3779 .cursor(CursorIcon::ColResize)
3780 .on_pointer_event(|event, ctx| {
3781 if matches!(event, WidgetEvent::PointerMove { .. }) {
3782 ctx.release_cursor();
3783 }
3784 EventResponse::Ignored
3785 }),
3786 );
3787 tree.layout(SizeProposal::exact(100.0, 50.0));
3788 tree.pointer_move(Point::new(50.0, 25.0));
3789 assert_eq!(tree.current_cursor(), CursorIcon::ColResize);
3790
3791 let mut tree = WidgetTree::new();
3793 tree.add(FillWidget::new().on_pointer_event(|event, ctx| {
3794 if let WidgetEvent::PointerMove { position, .. } = event {
3795 if position.x < 30.0 {
3796 ctx.set_cursor(CursorIcon::Crosshair);
3797 } else {
3798 ctx.release_cursor();
3799 }
3800 }
3801 EventResponse::Ignored
3802 }));
3803 tree.layout(SizeProposal::exact(100.0, 50.0));
3804 tree.pointer_move(Point::new(10.0, 25.0));
3805 assert_eq!(tree.current_cursor(), CursorIcon::Crosshair);
3806 tree.pointer_move(Point::new(60.0, 25.0));
3807 assert_eq!(
3808 tree.current_cursor(),
3809 CursorIcon::Default,
3810 "nothing declared a cursor for this chain, so the hand-back \
3811 resolves to Default",
3812 );
3813 }
3814
3815 #[test]
3823 fn an_on_hover_override_does_not_erase_the_declaration_it_outranks() {
3824 let mut tree = WidgetTree::new();
3825 tree.add(
3826 FillWidget::new()
3827 .cursor(CursorIcon::ColResize)
3828 .on_hover(|entered, ctx| {
3829 if entered {
3830 ctx.set_cursor(CursorIcon::Crosshair);
3831 }
3832 })
3833 .on_pointer_event(|event, ctx| {
3834 if let WidgetEvent::PointerMove { position, .. } = event
3835 && position.x >= 30.0
3836 {
3837 ctx.release_cursor();
3838 }
3839 EventResponse::Ignored
3840 }),
3841 );
3842 tree.layout(SizeProposal::exact(100.0, 50.0));
3843
3844 tree.pointer_move(Point::new(10.0, 25.0));
3845 assert_eq!(
3846 tree.current_cursor(),
3847 CursorIcon::Crosshair,
3848 "on_hover runs after the node cursor is applied, so it wins",
3849 );
3850 tree.pointer_move(Point::new(60.0, 25.0));
3851 assert_eq!(
3852 tree.current_cursor(),
3853 CursorIcon::ColResize,
3854 "and the node's declaration survived being overridden",
3855 );
3856 }
3857
3858 #[derive(Debug)]
3861 struct Bumpable {
3862 value: i32,
3863 }
3864
3865 impl crate::widget::Widget for Bumpable {
3866 fn layout_response(
3867 &self,
3868 proposal: SizeProposal,
3869 _ctx: &crate::widget::LayoutContext,
3870 ) -> crate::widget::LayoutResponse {
3871 proposal.resolve(10.0, 10.0).into()
3872 }
3873 fn as_any(&self) -> Option<&dyn std::any::Any> {
3874 Some(self)
3875 }
3876 fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
3877 Some(self)
3878 }
3879 }
3880
3881 #[test]
3882 fn with_widget_mut_applies_and_dirty_marks() {
3883 let mut tree = WidgetTree::new();
3884 let id = tree.add(Bumpable { value: 0 });
3885 tree.layout(SizeProposal::exact(100.0, 100.0));
3886
3887 let mut ctx = EventContext::new();
3888 ctx.with_widget_mut::<Bumpable>(id, crate::binding::BindingLevel::Relayout, |b| {
3889 b.value = 42;
3890 });
3891 tree.collect_from_ctx(ctx, id);
3892
3893 let value = tree
3894 .widget_as_any(id)
3895 .and_then(|a| a.downcast_ref::<Bumpable>())
3896 .map(|b| b.value);
3897 assert_eq!(
3898 value,
3899 Some(42),
3900 "the deferred closure must mutate the live widget"
3901 );
3902 assert!(
3903 tree.needs_layout(),
3904 "Relayout dirty level must mark the tree for relayout"
3905 );
3906 }
3907
3908 #[test]
3909 #[cfg(debug_assertions)]
3910 #[should_panic(expected = "not the requested type")]
3911 fn with_widget_mut_wrong_type_panics_in_debug() {
3912 struct Other;
3913 let mut tree = WidgetTree::new();
3914 let id = tree.add(Bumpable { value: 0 });
3915 let mut ctx = EventContext::new();
3916 ctx.with_widget_mut::<Other>(
3917 id,
3918 crate::binding::BindingLevel::RepaintOnly,
3919 |_o: &mut Other| {},
3920 );
3921 tree.collect_from_ctx(ctx, id);
3924 }
3925
3926 #[test]
3927 fn with_widget_mut_closure_may_fire_observed_signals() {
3928 use crate::signal::Signal;
3936 let mut tree = WidgetTree::new();
3937 let id = tree.add(Bumpable { value: 0 });
3938 tree.layout(SizeProposal::exact(100.0, 100.0));
3939
3940 let trigger = Signal::new(0_u64);
3941 let echo = Signal::new(0_u64);
3942 let echo_for_obs = echo.clone();
3943 let _obs = trigger.observe(move |v| echo_for_obs.set(*v));
3944
3945 let trigger_in = trigger.clone();
3946 let mut ctx = EventContext::new();
3947 ctx.with_widget_mut::<Bumpable>(id, crate::binding::BindingLevel::RepaintOnly, move |b| {
3948 b.value = 7;
3949 trigger_in.set(99);
3951 });
3952 tree.collect_from_ctx(ctx, id); assert_eq!(
3955 echo.get(),
3956 99,
3957 "the observer ran during the deferred mutation"
3958 );
3959 let value = tree
3960 .widget_as_any(id)
3961 .and_then(|a| a.downcast_ref::<Bumpable>())
3962 .map(|b| b.value);
3963 assert_eq!(value, Some(7));
3964 }
3965
3966 #[test]
3967 fn request_accessibility_update_forces_rewalk() {
3968 let mut tree = WidgetTree::new();
3969 let id = tree.add(Bumpable { value: 0 });
3970 tree.layout(SizeProposal::exact(100.0, 100.0));
3971 let _ = tree.sync_accessibility(); assert!(
3973 !tree.a11y_dirty,
3974 "sync_accessibility should clear the dirty flag"
3975 );
3976
3977 let mut ctx = EventContext::new();
3978 ctx.request_accessibility_update();
3979 tree.collect_from_ctx(ctx, id);
3980 assert!(
3981 tree.a11y_dirty,
3982 "request_accessibility_update must force an AT re-walk"
3983 );
3984 }
3985
3986 #[test]
3987 fn rebuild_dirties_accessibility_tree() {
3988 let mut tree = WidgetTree::new();
3995 let id = tree.add(Bumpable { value: 0 });
3996 tree.layout(SizeProposal::exact(100.0, 100.0));
3997 let _ = tree.sync_accessibility(); assert!(
3999 !tree.a11y_dirty,
4000 "sync_accessibility should clear the dirty flag"
4001 );
4002
4003 tree.arena_mark_needs_rebuild_for_testing(id);
4006 tree.layout(SizeProposal::exact(100.0, 100.0));
4007 assert!(
4008 tree.a11y_dirty,
4009 "a rebuild must dirty the AT tree so the next sync re-walks"
4010 );
4011 }
4012
4013 #[test]
4014 fn bound_access_label_change_dirties_accessibility_tree() {
4015 use crate::signal::Signal;
4016 use crate::test_widgets::FillWidget;
4017 use crate::widget_builder::WidgetBuilder;
4018
4019 let label = Signal::new("first".to_string());
4025 let mut tree = WidgetTree::new();
4026 let _id = tree.add(FillWidget::new().access_label(label.clone()));
4027 tree.layout(SizeProposal::exact(100.0, 100.0));
4028 let _ = tree.sync_accessibility(); assert!(!tree.a11y_dirty, "sync_accessibility should clear the flag");
4030
4031 label.set("second".to_string());
4032 tree.layout(SizeProposal::exact(100.0, 100.0));
4033 assert!(
4034 tree.a11y_dirty,
4035 "changing a bound access_label must dirty the AT tree"
4036 );
4037 }
4038
4039 #[test]
4040 fn disabled_ancestor_blocks_event_to_descendant() {
4041 use crate::signal::Signal;
4042 use crate::test_widgets::StackWidget;
4043 use std::cell::Cell;
4044 use std::rc::Rc;
4045
4046 let tapped = Rc::new(Cell::new(false));
4047 let flag = tapped.clone();
4048 let enabled = Signal::new(true);
4049
4050 let mut tree = WidgetTree::new();
4051 let child = tree.add(FillWidget::new().on_tap(move |_pos, _ctx| {
4052 flag.set(true);
4053 }));
4054 let parent = tree.add(StackWidget::new().child(child));
4055 tree.enabled_when(parent, enabled.clone());
4056 tree.layout(SizeProposal::exact(100.0, 50.0));
4057
4058 enabled.set(false);
4059 tree.click(child);
4060 assert!(
4061 !tapped.get(),
4062 "disabled ancestor should block descendant tap"
4063 );
4064
4065 enabled.set(true);
4066 tree.click(child);
4067 assert!(tapped.get(), "re-enabling should restore dispatch");
4068 }
4069
4070 #[test]
4071 fn pointer_positions_are_widget_local_at_nonzero_origin() {
4072 use crate::event::{Modifiers, PointerButton};
4073 use crate::test_widgets::InsetWidget;
4074 use std::cell::Cell;
4075 use std::rc::Rc;
4076
4077 let tap_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
4079 let down_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
4080 let drag_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
4081 let (tp, dp, gp) = (tap_pos.clone(), down_pos.clone(), drag_pos.clone());
4082
4083 let mut tree = WidgetTree::new();
4084 let child = tree.add(
4085 FillWidget::new()
4086 .on_tap(move |ev, _ctx| tp.set(Some(ev.position)))
4087 .on_pointer_event(move |ev, _ctx| {
4088 if let WidgetEvent::PointerDown { position, .. } = ev {
4089 dp.set(Some(*position));
4090 }
4091 crate::event::EventResponse::Ignored
4092 })
4093 .on_drag(move |phase, _ctx| {
4094 use crate::gesture::DragPhase;
4095 match phase {
4096 DragPhase::Started { position, .. }
4097 | DragPhase::Moved { position, .. }
4098 | DragPhase::Ended { position, .. } => gp.set(Some(position)),
4099 _ => {}
4100 }
4101 }),
4102 );
4103 let inset = tree.add(InsetWidget::new(20.0).set_child(child));
4104 let _ = inset;
4105 tree.layout(SizeProposal::exact(200.0, 200.0));
4106 assert_eq!(tree.bounds(child).origin(), Point::new(20.0, 20.0));
4107
4108 tree.dispatch_event(WidgetEvent::pointer_down(
4110 Point::new(50.0, 40.0),
4111 PointerButton::Primary,
4112 Modifiers::NONE,
4113 ));
4114 assert_eq!(
4115 down_pos.get(),
4116 Some(Point::new(30.0, 20.0)),
4117 "on_pointer_event PointerDown must be widget-local"
4118 );
4119 tree.dispatch_event(WidgetEvent::pointer_up(
4120 Point::new(50.0, 40.0),
4121 PointerButton::Primary,
4122 Modifiers::NONE,
4123 ));
4124 assert_eq!(
4125 tap_pos.get(),
4126 Some(Point::new(30.0, 20.0)),
4127 "on_tap position must be widget-local"
4128 );
4129
4130 tree.dispatch_event(WidgetEvent::pointer_down(
4133 Point::new(50.0, 40.0),
4134 PointerButton::Primary,
4135 Modifiers::NONE,
4136 ));
4137 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(65.0, 55.0)));
4140 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(90.0, 70.0)));
4141 assert_eq!(
4142 drag_pos.get(),
4143 Some(Point::new(70.0, 50.0)),
4144 "on_drag position must be widget-local"
4145 );
4146 }
4147
4148 #[test]
4149 fn dormant_widget_not_hit_tested() {
4150 let mut tree = WidgetTree::new();
4151 let widget = tree.add(FillWidget::new());
4152 tree.layout(SizeProposal::exact(100.0, 50.0));
4153
4154 tree.pointer_move(Point::new(50.0, 25.0));
4155 assert_eq!(tree.hovered(), Some(widget));
4156
4157 tree.set_dormant(widget);
4158 tree.pointer_move(Point::new(200.0, 200.0));
4159 tree.pointer_move(Point::new(50.0, 25.0));
4160 assert_eq!(tree.hovered(), None);
4161 }
4162
4163 #[test]
4164 fn ancestor_pointer_handler_does_not_suppress_descendant_hover() {
4165 use crate::event::EventResponse;
4166 use crate::test_widgets::StackWidget;
4167 use std::cell::Cell;
4168 use std::rc::Rc;
4169
4170 let hovered = Rc::new(Cell::new(false));
4172 let h = hovered.clone();
4173
4174 let mut tree = WidgetTree::new();
4175 let child = tree.add(FillWidget::new().on_hover(move |entered, _ctx| h.set(entered)));
4176 tree.add(
4181 StackWidget::new()
4182 .child(child)
4183 .on_pointer_event(|_event, _ctx| EventResponse::Handled),
4184 );
4185 tree.layout(SizeProposal::exact(100.0, 50.0));
4186
4187 tree.pointer_move(Point::new(50.0, 25.0));
4188 assert!(
4189 hovered.get(),
4190 "a greedy ancestor on_pointer_event must NOT swallow the child's PointerEnter"
4191 );
4192
4193 tree.pointer_move(Point::new(500.0, 500.0));
4194 assert!(
4195 !hovered.get(),
4196 "PointerLeave must likewise reach the child despite the ancestor"
4197 );
4198 }
4199
4200 #[test]
4208 fn a_child_hover_handler_does_not_swallow_its_ancestors() {
4209 use crate::test_widgets::StackWidget;
4210 use std::cell::Cell;
4211 use std::rc::Rc;
4212
4213 let (row, button) = (Rc::new(Cell::new(false)), Rc::new(Cell::new(false)));
4214 let (r, b) = (row.clone(), button.clone());
4215
4216 let mut tree = WidgetTree::new();
4217 let child = tree.add(FillWidget::new().on_hover(move |entered, _ctx| b.set(entered)));
4218 tree.add(
4219 StackWidget::new()
4220 .child(child)
4221 .on_hover(move |entered, _ctx| r.set(entered)),
4222 );
4223 tree.layout(SizeProposal::exact(100.0, 50.0));
4224
4225 tree.pointer_move(Point::new(50.0, 25.0));
4226 assert!(button.get(), "the child is hovered");
4227 assert!(row.get(), "and so is the row it is inside");
4228
4229 tree.pointer_move(Point::new(500.0, 500.0));
4230 assert!(!button.get(), "the child heard the leave");
4231 assert!(
4232 !row.get(),
4233 "and so did the row — a container is not still hovered because the \
4234 pointer left it through a button"
4235 );
4236 }
4237
4238 #[test]
4245 fn key_preview_consumes_before_focused_on_key() {
4246 use crate::event::EventResponse;
4249 use crate::test_widgets::StackWidget;
4250 use std::cell::Cell;
4251 use std::rc::Rc;
4252
4253 let leaf_fired = Rc::new(Cell::new(false));
4254 let leaf_flag = leaf_fired.clone();
4255 let preview_fired = Rc::new(Cell::new(false));
4256 let preview_flag = preview_fired.clone();
4257
4258 let mut tree = WidgetTree::new();
4259 let leaf = tree.add(FillWidget::new().focusable().on_key(move |event, _c| {
4260 if matches!(event, WidgetEvent::KeyDown { .. }) {
4263 leaf_flag.set(true);
4264 }
4265 EventResponse::Handled
4266 }));
4267 let mid = tree.add(StackWidget::new().child(leaf));
4268 let _root = tree.add(
4269 StackWidget::new()
4270 .child(mid)
4271 .on_key_preview(move |event, _c| match event {
4272 WidgetEvent::KeyDown {
4273 key: Key::Enter, ..
4274 } => {
4275 preview_flag.set(true);
4276 EventResponse::Handled
4277 }
4278 _ => EventResponse::Ignored,
4279 }),
4280 );
4281
4282 tree.layout(SizeProposal::exact(100.0, 50.0));
4283 tree.focus(leaf);
4284 tree.press_key(Key::Enter, Modifiers::NONE);
4285
4286 assert!(
4287 preview_fired.get(),
4288 "ancestor on_key_preview must fire for KeyDown on a focused descendant"
4289 );
4290 assert!(
4291 !leaf_fired.get(),
4292 "consuming the event in preview must prevent the focused widget's on_key from running"
4293 );
4294 }
4295
4296 #[test]
4297 fn key_preview_falls_through_when_returning_ignored() {
4298 use crate::event::EventResponse;
4301 use crate::test_widgets::StackWidget;
4302 use std::cell::Cell;
4303 use std::rc::Rc;
4304
4305 let leaf_fired = Rc::new(Cell::new(false));
4306 let leaf_flag = leaf_fired.clone();
4307 let preview_fired = Rc::new(Cell::new(false));
4308 let preview_flag = preview_fired.clone();
4309
4310 let mut tree = WidgetTree::new();
4311 let leaf = tree.add(FillWidget::new().focusable().on_key(move |_e, _c| {
4312 leaf_flag.set(true);
4313 EventResponse::Handled
4314 }));
4315 let mid = tree.add(StackWidget::new().child(leaf));
4316 let _root = tree.add(
4317 StackWidget::new()
4318 .child(mid)
4319 .on_key_preview(move |_event, _c| {
4320 preview_flag.set(true);
4321 EventResponse::Ignored
4322 }),
4323 );
4324
4325 tree.layout(SizeProposal::exact(100.0, 50.0));
4326 tree.focus(leaf);
4327 tree.press_key(Key::Enter, Modifiers::NONE);
4328
4329 assert!(preview_fired.get(), "preview must always be invoked");
4330 assert!(
4331 leaf_fired.get(),
4332 "preview returning Ignored must not block the focused widget's on_key"
4333 );
4334 }
4335
4336 #[test]
4337 fn key_preview_excludes_focused_target_itself() {
4338 use crate::event::EventResponse;
4342 use std::cell::Cell;
4343 use std::rc::Rc;
4344
4345 let preview_on_target = Rc::new(Cell::new(false));
4346 let pf = preview_on_target.clone();
4347
4348 let mut tree = WidgetTree::new();
4349 let leaf = tree.add(FillWidget::new().focusable().on_key_preview(move |_e, _c| {
4350 pf.set(true);
4351 EventResponse::Handled
4352 }));
4353 tree.layout(SizeProposal::exact(100.0, 50.0));
4354 tree.focus(leaf);
4355 tree.press_key(Key::Enter, Modifiers::NONE);
4356
4357 assert!(
4358 !preview_on_target.get(),
4359 "the focused widget itself must not see its own on_key_preview"
4360 );
4361 }
4362
4363 #[test]
4364 fn key_preview_root_to_target_order() {
4365 use crate::event::EventResponse;
4369 use crate::test_widgets::StackWidget;
4370 use std::cell::RefCell;
4371 use std::rc::Rc;
4372
4373 let order: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
4374 let outer_log = order.clone();
4375 let inner_log = order.clone();
4376
4377 let mut tree = WidgetTree::new();
4378 let leaf = tree.add(FillWidget::new().focusable());
4379 let inner = tree.add(
4380 StackWidget::new()
4381 .child(leaf)
4382 .on_key_preview(move |event, _c| {
4383 if matches!(event, WidgetEvent::KeyDown { .. }) {
4384 inner_log.borrow_mut().push("inner");
4385 }
4386 EventResponse::Ignored
4387 }),
4388 );
4389 let _outer = tree.add(
4390 StackWidget::new()
4391 .child(inner)
4392 .on_key_preview(move |event, _c| {
4393 if matches!(event, WidgetEvent::KeyDown { .. }) {
4394 outer_log.borrow_mut().push("outer");
4395 }
4396 EventResponse::Ignored
4397 }),
4398 );
4399
4400 tree.layout(SizeProposal::exact(100.0, 50.0));
4401 tree.focus(leaf);
4402 tree.dispatch_event(WidgetEvent::KeyDown {
4403 key: Key::Enter,
4404 modifiers: Modifiers::NONE,
4405 text: None,
4406 });
4407
4408 assert_eq!(
4409 *order.borrow(),
4410 vec!["outer", "inner"],
4411 "preview must walk root → parent-of-target"
4412 );
4413 }
4414
4415 #[test]
4416 fn access_action_routes_to_cursored_target_not_focus() {
4417 use crate::signal::Signal;
4421 let a_fired = Signal::new(false);
4422 let b_fired = Signal::new(false);
4423 let a_cb = a_fired.clone();
4424 let b_cb = b_fired.clone();
4425
4426 let mut tree = WidgetTree::new();
4427 let a = tree.add(
4428 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| a_cb.set(true)),
4429 );
4430 let b = tree.add(
4431 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| b_cb.set(true)),
4432 );
4433 tree.layout(SizeProposal::exact(200.0, 100.0));
4434
4435 tree.focus(a);
4436 tree.dispatch_event(WidgetEvent::AccessAction {
4437 action: accesskit::Action::Click,
4438 target: Some(b),
4439 target_node: crate::accessibility::widget_id_to_node_id(b),
4440 data: None,
4441 });
4442
4443 assert!(b_fired.get(), "the cursored target must receive the action");
4444 assert!(
4445 !a_fired.get(),
4446 "the keyboard-focused widget must NOT receive an action targeting another node"
4447 );
4448 }
4449
4450 #[test]
4451 fn access_action_without_target_is_dropped_not_redirected_to_focus() {
4452 use crate::signal::Signal;
4455 let fired = Signal::new(false);
4456 let cb = fired.clone();
4457
4458 let mut tree = WidgetTree::new();
4459 let widget = tree.add(
4460 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| cb.set(true)),
4461 );
4462 tree.layout(SizeProposal::exact(200.0, 100.0));
4463
4464 tree.focus(widget);
4465 tree.dispatch_event(WidgetEvent::AccessAction {
4466 action: accesskit::Action::Click,
4467 target: None,
4468 target_node: crate::accessibility::root_node_id(),
4469 data: None,
4470 });
4471
4472 assert!(
4473 !fired.get(),
4474 "a target-less action must not be redirected to the focused widget"
4475 );
4476 }
4477
4478 #[test]
4485 fn shortcut_fires_matching_action_on_source_widget() {
4486 use crate::action::Action;
4487 use crate::shortcut::{KeyStroke, Shortcut};
4488 use std::cell::Cell;
4489 use std::rc::Rc;
4490
4491 let fired = Rc::new(Cell::new(false));
4492 let fired_flag = fired.clone();
4493
4494 let mut tree = WidgetTree::new();
4495 let widget = tree.add(FillWidget::new().focusable());
4496 tree.push_action(
4497 widget,
4498 Action::new("app.save").on_invoke(move |_intent, _ctx| {
4499 fired_flag.set(true);
4500 }),
4501 );
4502 tree.shortcut_registry_mut().register(
4503 Shortcut::new("app.save")
4504 .primary(KeyStroke::command(Key::S))
4505 .build(),
4506 );
4507
4508 tree.layout(SizeProposal::exact(100.0, 50.0));
4509 tree.focus(widget);
4510
4511 tree.press_key(Key::S, Modifiers::COMMAND);
4512 assert!(fired.get(), "matching action must fire on KeyDown");
4513 }
4514
4515 #[test]
4516 fn global_shortcut_fires_without_focused_widget() {
4517 use crate::action::Action;
4518 use crate::shortcut::{KeyStroke, Shortcut};
4519 use std::cell::Cell;
4520 use std::rc::Rc;
4521
4522 let fired = Rc::new(Cell::new(false));
4526 let fired_flag = fired.clone();
4527
4528 let mut tree = WidgetTree::new();
4529 let root = tree.add(FillWidget::new());
4530 tree.push_action(
4531 root,
4532 Action::new("app.save").on_invoke(move |_intent, _ctx| {
4533 fired_flag.set(true);
4534 }),
4535 );
4536 tree.shortcut_registry_mut().register(
4537 Shortcut::new("app.save")
4538 .primary(KeyStroke::command(Key::S))
4539 .build(),
4540 );
4541
4542 tree.layout(SizeProposal::exact(100.0, 50.0));
4543 tree.press_key(Key::S, Modifiers::COMMAND);
4546 assert!(
4547 fired.get(),
4548 "global shortcut must fire without a focused widget"
4549 );
4550 }
4551
4552 #[test]
4553 fn global_shortcut_fires_after_focused_widget_destroyed() {
4554 use crate::action::Action;
4555 use crate::shortcut::{KeyStroke, Shortcut};
4556 use std::cell::Cell;
4557 use std::rc::Rc;
4558
4559 let fired = Rc::new(Cell::new(false));
4564 let fired_flag = fired.clone();
4565
4566 let mut tree = WidgetTree::new();
4567 let root = tree.add(FillWidget::new());
4568 let focusable = tree.add_child(root, FillWidget::new().focusable());
4569 tree.push_action(
4570 root,
4571 Action::new("app.save").on_invoke(move |_intent, _ctx| {
4572 fired_flag.set(true);
4573 }),
4574 );
4575 tree.shortcut_registry_mut().register(
4576 Shortcut::new("app.save")
4577 .primary(KeyStroke::command(Key::S))
4578 .build(),
4579 );
4580
4581 tree.layout(SizeProposal::exact(100.0, 50.0));
4582 tree.focus(focusable);
4583 assert_eq!(tree.focused(), Some(focusable));
4584
4585 tree.destroy_subtree(focusable);
4588 assert_eq!(tree.focused(), None, "focus must clear when destroyed");
4589
4590 tree.press_key(Key::S, Modifiers::COMMAND);
4591 assert!(
4592 fired.get(),
4593 "global shortcut must still fire after the focused widget is destroyed"
4594 );
4595 }
4596
4597 #[test]
4598 fn scoped_shortcut_matches_only_when_focus_in_scope() {
4599 use crate::action::Action;
4600 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
4601 use std::cell::Cell;
4602 use std::rc::Rc;
4603
4604 let fired = Rc::new(Cell::new(0));
4605 let fired_flag = fired.clone();
4606
4607 let mut tree = WidgetTree::new();
4608 let scope_root = tree.add(FillWidget::new().focusable());
4609 let inside = tree.add_child(scope_root, FillWidget::new().focusable());
4610 let outside = tree.add(FillWidget::new().focusable());
4611
4612 tree.push_action(
4613 scope_root,
4614 Action::new("editor.find").on_invoke(move |_i, _c| {
4615 fired_flag.set(fired_flag.get() + 1);
4616 }),
4617 );
4618 tree.shortcut_registry_mut().register(
4619 Shortcut::new("editor.find")
4620 .primary(KeyStroke::command(Key::F))
4621 .scope(ShortcutScope::Scoped(scope_root))
4622 .build(),
4623 );
4624
4625 tree.layout(SizeProposal::exact(200.0, 100.0));
4626
4627 tree.focus(outside);
4629 tree.press_key(Key::F, Modifiers::COMMAND);
4630 assert_eq!(
4631 fired.get(),
4632 0,
4633 "scoped shortcut must not fire outside scope"
4634 );
4635
4636 tree.focus(inside);
4638 tree.press_key(Key::F, Modifiers::COMMAND);
4639 assert_eq!(
4640 fired.get(),
4641 1,
4642 "scoped shortcut must fire when focus in scope"
4643 );
4644 }
4645
4646 #[test]
4647 fn same_chord_scoped_first_falls_back_to_global_when_focus_outside() {
4648 use crate::action::Action;
4654 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
4655 use std::cell::Cell;
4656 use std::rc::Rc;
4657
4658 let scoped_fired = Rc::new(Cell::new(0));
4659 let global_fired = Rc::new(Cell::new(0));
4660 let sf = scoped_fired.clone();
4661 let gf = global_fired.clone();
4662
4663 let mut tree = WidgetTree::new();
4664 let root = tree.add(FillWidget::new());
4665 let editor = tree.add_child(root, FillWidget::new().focusable());
4666 let _editor_inner = tree.add_child(editor, FillWidget::new().focusable());
4667 let sidebar = tree.add_child(root, FillWidget::new().focusable());
4668
4669 tree.push_action(
4670 editor,
4671 Action::new("editor.saveBlock").on_invoke(move |_i, _c| sf.set(sf.get() + 1)),
4672 );
4673 tree.push_action(
4674 root,
4675 Action::new("zzz.global.save").on_invoke(move |_i, _c| gf.set(gf.get() + 1)),
4676 );
4677 tree.shortcut_registry_mut().register(
4678 Shortcut::new("editor.saveBlock")
4679 .primary(KeyStroke::command(Key::S))
4680 .scope(ShortcutScope::Scoped(editor))
4681 .build(),
4682 );
4683 tree.shortcut_registry_mut().register(
4684 Shortcut::new("zzz.global.save")
4685 .primary(KeyStroke::command(Key::S))
4686 .build(),
4687 );
4688
4689 tree.layout(SizeProposal::exact(200.0, 100.0));
4690 tree.focus(sidebar);
4691 tree.press_key(Key::S, Modifiers::COMMAND);
4692
4693 assert_eq!(global_fired.get(), 1, "applicable global must fire");
4694 assert_eq!(
4695 scoped_fired.get(),
4696 0,
4697 "inapplicable scoped binding must not eat the chord"
4698 );
4699 }
4700
4701 #[test]
4702 fn same_chord_global_first_yields_to_scoped_when_focus_inside() {
4703 use crate::action::Action;
4708 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
4709 use std::cell::Cell;
4710 use std::rc::Rc;
4711
4712 let scoped_fired = Rc::new(Cell::new(0));
4713 let global_fired = Rc::new(Cell::new(0));
4714 let sf = scoped_fired.clone();
4715 let gf = global_fired.clone();
4716
4717 let mut tree = WidgetTree::new();
4718 let root = tree.add(FillWidget::new());
4719 let editor = tree.add_child(root, FillWidget::new().focusable());
4720 let editor_inner = tree.add_child(editor, FillWidget::new().focusable());
4721 let sidebar = tree.add_child(root, FillWidget::new().focusable());
4722
4723 tree.push_action(
4724 editor,
4725 Action::new("editor.saveBlock").on_invoke(move |_i, _c| sf.set(sf.get() + 1)),
4726 );
4727 tree.push_action(
4728 root,
4729 Action::new("app.save").on_invoke(move |_i, _c| gf.set(gf.get() + 1)),
4730 );
4731 tree.shortcut_registry_mut().register(
4732 Shortcut::new("app.save")
4733 .primary(KeyStroke::command(Key::S))
4734 .build(),
4735 );
4736 tree.shortcut_registry_mut().register(
4737 Shortcut::new("editor.saveBlock")
4738 .primary(KeyStroke::command(Key::S))
4739 .scope(ShortcutScope::Scoped(editor))
4740 .build(),
4741 );
4742
4743 tree.layout(SizeProposal::exact(200.0, 100.0));
4744
4745 tree.focus(editor_inner);
4747 tree.press_key(Key::S, Modifiers::COMMAND);
4748 assert_eq!(
4749 scoped_fired.get(),
4750 1,
4751 "in-focus scoped must win over global"
4752 );
4753 assert_eq!(
4754 global_fired.get(),
4755 0,
4756 "global must yield to the scoped binding"
4757 );
4758
4759 tree.focus(sidebar);
4761 tree.press_key(Key::S, Modifiers::COMMAND);
4762 assert_eq!(scoped_fired.get(), 1, "scoped stays put outside its scope");
4763 assert_eq!(
4764 global_fired.get(),
4765 1,
4766 "global fires when focus leaves the scope"
4767 );
4768 }
4769
4770 #[test]
4771 fn propagated_action_lets_ancestor_handle() {
4772 use crate::action::Action;
4773 use crate::intent::IntentResponse;
4774 use crate::shortcut::{KeyStroke, Shortcut};
4775 use std::cell::Cell;
4776 use std::rc::Rc;
4777
4778 let inner_seen = Rc::new(Cell::new(false));
4779 let outer_seen = Rc::new(Cell::new(false));
4780 let inner_flag = inner_seen.clone();
4781 let outer_flag = outer_seen.clone();
4782
4783 let mut tree = WidgetTree::new();
4784 let outer = tree.add(FillWidget::new().focusable());
4785 let inner = tree.add_child(outer, FillWidget::new().focusable());
4786
4787 tree.push_action(
4789 inner,
4790 Action::new("app.save").on_invoke_with_response(move |_i, _c| {
4791 inner_flag.set(true);
4792 IntentResponse::Propagated
4793 }),
4794 );
4795 tree.push_action(
4796 outer,
4797 Action::new("app.save").on_invoke(move |_i, _c| {
4798 outer_flag.set(true);
4799 }),
4800 );
4801 tree.shortcut_registry_mut().register(
4802 Shortcut::new("app.save")
4803 .primary(KeyStroke::command(Key::S))
4804 .build(),
4805 );
4806
4807 tree.layout(SizeProposal::exact(100.0, 50.0));
4808 tree.focus(inner);
4809
4810 tree.press_key(Key::S, Modifiers::COMMAND);
4811 assert!(inner_seen.get(), "inner action observed the intent");
4812 assert!(outer_seen.get(), "outer action reached after Propagated");
4813 }
4814
4815 #[test]
4816 fn handled_action_stops_propagation() {
4817 use crate::action::Action;
4818 use crate::shortcut::{KeyStroke, Shortcut};
4819 use std::cell::Cell;
4820 use std::rc::Rc;
4821
4822 let inner_seen = Rc::new(Cell::new(false));
4823 let outer_seen = Rc::new(Cell::new(false));
4824 let inner_flag = inner_seen.clone();
4825 let outer_flag = outer_seen.clone();
4826
4827 let mut tree = WidgetTree::new();
4828 let outer = tree.add(FillWidget::new().focusable());
4829 let inner = tree.add_child(outer, FillWidget::new().focusable());
4830
4831 tree.push_action(
4832 inner,
4833 Action::new("app.save").on_invoke(move |_i, _c| {
4834 inner_flag.set(true);
4835 }),
4836 );
4837 tree.push_action(
4838 outer,
4839 Action::new("app.save").on_invoke(move |_i, _c| {
4840 outer_flag.set(true);
4841 }),
4842 );
4843 tree.shortcut_registry_mut().register(
4844 Shortcut::new("app.save")
4845 .primary(KeyStroke::command(Key::S))
4846 .build(),
4847 );
4848
4849 tree.layout(SizeProposal::exact(100.0, 50.0));
4850 tree.focus(inner);
4851
4852 tree.press_key(Key::S, Modifiers::COMMAND);
4853 assert!(inner_seen.get());
4854 assert!(!outer_seen.get(), "Handled at inner must stop propagation");
4855 }
4856
4857 #[test]
4858 fn disabled_action_propagates_by_default() {
4859 use crate::action::Action;
4860 use crate::shortcut::{KeyStroke, Shortcut};
4861 use crate::signal::Signal;
4862 use std::cell::Cell;
4863 use std::rc::Rc;
4864
4865 let inner_seen = Rc::new(Cell::new(false));
4866 let outer_seen = Rc::new(Cell::new(false));
4867 let inner_flag = inner_seen.clone();
4868 let outer_flag = outer_seen.clone();
4869
4870 let mut tree = WidgetTree::new();
4871 let outer = tree.add(FillWidget::new().focusable());
4872 let inner = tree.add_child(outer, FillWidget::new().focusable());
4873
4874 let enabled = Signal::new(false);
4875 tree.push_action(
4876 inner,
4877 Action::new("app.save")
4878 .enabled_when(enabled.clone())
4879 .on_invoke(move |_i, _c| {
4880 inner_flag.set(true);
4881 }),
4882 );
4883 tree.push_action(
4884 outer,
4885 Action::new("app.save").on_invoke(move |_i, _c| {
4886 outer_flag.set(true);
4887 }),
4888 );
4889 tree.shortcut_registry_mut().register(
4890 Shortcut::new("app.save")
4891 .primary(KeyStroke::command(Key::S))
4892 .build(),
4893 );
4894
4895 tree.layout(SizeProposal::exact(100.0, 50.0));
4896 tree.focus(inner);
4897
4898 tree.press_key(Key::S, Modifiers::COMMAND);
4899 assert!(!inner_seen.get(), "disabled inner must not run");
4900 assert!(
4901 outer_seen.get(),
4902 "intent must propagate past disabled inner"
4903 );
4904 }
4905
4906 #[test]
4907 fn disabled_action_with_non_propagating_shortcut_consumes() {
4908 use crate::action::Action;
4909 use crate::shortcut::{KeyStroke, Shortcut};
4910 use crate::signal::Signal;
4911 use std::cell::Cell;
4912 use std::rc::Rc;
4913
4914 let inner_seen = Rc::new(Cell::new(false));
4915 let outer_seen = Rc::new(Cell::new(false));
4916 let inner_flag = inner_seen.clone();
4917 let outer_flag = outer_seen.clone();
4918
4919 let mut tree = WidgetTree::new();
4920 let outer = tree.add(FillWidget::new().focusable());
4921 let inner = tree.add_child(outer, FillWidget::new().focusable());
4922
4923 let enabled = Signal::new(false);
4924 tree.push_action(
4925 inner,
4926 Action::new("app.save")
4927 .enabled_when(enabled.clone())
4928 .on_invoke(move |_i, _c| {
4929 inner_flag.set(true);
4930 }),
4931 );
4932 tree.push_action(
4933 outer,
4934 Action::new("app.save").on_invoke(move |_i, _c| {
4935 outer_flag.set(true);
4936 }),
4937 );
4938 tree.shortcut_registry_mut().register(
4939 Shortcut::new("app.save")
4940 .primary(KeyStroke::command(Key::S))
4941 .propagate_when_disabled(false)
4942 .build(),
4943 );
4944
4945 tree.layout(SizeProposal::exact(100.0, 50.0));
4946 tree.focus(inner);
4947
4948 tree.press_key(Key::S, Modifiers::COMMAND);
4949 assert!(!inner_seen.get(), "disabled inner still does not run");
4950 assert!(
4951 !outer_seen.get(),
4952 "intent must NOT propagate when shortcut disallows it"
4953 );
4954 }
4955
4956 #[test]
4957 fn send_intent_from_handler_reaches_ancestor_action() {
4958 use crate::action::Action;
4959 use crate::intent::Intent;
4960 use std::cell::Cell;
4961 use std::rc::Rc;
4962
4963 let save_seen = Rc::new(Cell::new(false));
4964 let save_flag = save_seen.clone();
4965
4966 let mut tree = WidgetTree::new();
4967 let root = tree.add(FillWidget::new());
4968 let button = tree.add_child(
4969 root,
4970 FillWidget::new().on_tap(|_pos, ctx| {
4971 ctx.send_intent(Intent::new("app.save"));
4972 }),
4973 );
4974 tree.push_action(
4975 root,
4976 Action::new("app.save").on_invoke(move |_i, _c| {
4977 save_flag.set(true);
4978 }),
4979 );
4980
4981 tree.layout(SizeProposal::exact(100.0, 50.0));
4982 tree.click(button);
4983 assert!(
4984 save_seen.get(),
4985 "ctx.send_intent must reach ancestor action"
4986 );
4987 }
4988
4989 #[test]
4990 fn widget_type_histogram_counts_distinct_types() {
4991 let mut tree = WidgetTree::new();
4996 let _ = tree.add(FillWidget::new());
4997 let _ = tree.add(FillWidget::new());
4998 let _ = tree.add(FillWidget::new());
4999 tree.layout(SizeProposal::exact(100.0, 100.0));
5000 let histogram = tree.widget_type_histogram();
5001 let total: u32 = histogram.values().sum();
5002 assert!(
5003 total >= 3,
5004 "expected at least 3 active widgets, got {total}: {histogram:?}"
5005 );
5006 let fillwidget_entries: u32 = histogram
5007 .iter()
5008 .filter(|(k, _)| k.contains("FillWidget"))
5009 .map(|(_, v)| *v)
5010 .sum();
5011 assert!(
5012 fillwidget_entries >= 3,
5013 "expected ≥3 FillWidget instances; histogram = {histogram:?}"
5014 );
5015 assert_eq!(tree.active_widget_count() as u32, total);
5016 }
5017
5018 #[test]
5019 fn intent_source_tagged_handler_for_tap_activation() {
5020 use crate::action::Action;
5024 use crate::intent::Intent;
5025 use crate::telemetry::IntentSource;
5026 use std::cell::Cell;
5027 use std::rc::Rc;
5028 let captured = Rc::new(Cell::new(IntentSource::Unknown));
5029 let captured_for_action = captured.clone();
5030
5031 let mut tree = WidgetTree::new();
5032 let root = tree.add(FillWidget::new());
5033 let button = tree.add_child(
5034 root,
5035 FillWidget::new().on_tap(|_pos, ctx| {
5036 ctx.send_intent(Intent::new("app.save"));
5037 }),
5038 );
5039 tree.push_action(
5040 root,
5041 Action::new("app.save").on_invoke(move |intent, _c| {
5042 captured_for_action.set(intent.source);
5043 }),
5044 );
5045
5046 tree.layout(SizeProposal::exact(100.0, 50.0));
5047 tree.click(button);
5048 assert_eq!(
5049 captured.get(),
5050 IntentSource::Handler,
5051 "tap-driven intent must tag IntentSource::Handler"
5052 );
5053 }
5054
5055 #[test]
5056 fn intent_source_programmatic_when_no_handler_active() {
5057 use crate::intent::Intent;
5058 use crate::telemetry::IntentSource;
5059 let intent = Intent::new("app.demo");
5060 assert_eq!(intent.source, IntentSource::Programmatic);
5061
5062 let mut ctx = EventContext::new();
5064 ctx.send_intent(Intent::new("app.demo"));
5065 let queued = ctx.pending_intents.first().expect("intent queued");
5066 assert_eq!(queued.source, IntentSource::Programmatic);
5067 }
5068
5069 #[test]
5070 fn with_intent_source_overrides_for_managed_widgets() {
5071 use crate::intent::Intent;
5072 use crate::telemetry::IntentSource;
5073 let mut ctx = EventContext::new();
5074 ctx.with_intent_source(IntentSource::Menu, |ctx| {
5075 ctx.send_intent(Intent::new("app.demo"));
5076 });
5077 let queued = ctx.pending_intents.first().expect("intent queued");
5078 assert_eq!(
5079 queued.source,
5080 IntentSource::Menu,
5081 "with_intent_source(Menu) must tag the dispatched intent"
5082 );
5083
5084 ctx.send_intent(Intent::new("app.next"));
5088 let next = ctx.pending_intents.last().expect("second intent");
5089 assert_eq!(next.source, IntentSource::Programmatic);
5090 }
5091
5092 #[test]
5093 fn disabled_shortcut_falls_through_to_focused_widget() {
5094 use crate::action::Action;
5095 use crate::shortcut::{KeyStroke, Shortcut};
5096 use crate::signal::Signal;
5097 use std::cell::Cell;
5098 use std::rc::Rc;
5099
5100 let action_fired = Rc::new(Cell::new(false));
5101 let on_key_fired = Rc::new(Cell::new(false));
5102 let af = action_fired.clone();
5103 let kf = on_key_fired.clone();
5104
5105 let enabled = Signal::new(false);
5106
5107 let mut tree = WidgetTree::new();
5108 let widget = tree.add(FillWidget::new().focusable().on_key(move |event, _ctx| {
5109 if matches!(
5110 event,
5111 WidgetEvent::KeyDown {
5112 key: Key::S,
5113 modifiers,
5114 ..
5115 } if modifiers.command()
5116 ) {
5117 kf.set(true);
5118 return EventResponse::Handled;
5119 }
5120 EventResponse::Ignored
5121 }));
5122 tree.push_action(
5123 widget,
5124 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
5125 );
5126 tree.shortcut_registry_mut().register(
5127 Shortcut::new("app.save")
5128 .primary(KeyStroke::command(Key::S))
5129 .enabled_when(enabled.clone())
5130 .build(),
5131 );
5132
5133 tree.layout(SizeProposal::exact(100.0, 50.0));
5134 tree.focus(widget);
5135
5136 tree.press_key(Key::S, Modifiers::COMMAND);
5138 assert!(
5139 !action_fired.get(),
5140 "disabled shortcut must not invoke its action"
5141 );
5142 assert!(
5143 on_key_fired.get(),
5144 "disabled shortcut must let KeyDown reach the focused widget"
5145 );
5146
5147 on_key_fired.set(false);
5149 enabled.set(true);
5150 tree.press_key(Key::S, Modifiers::COMMAND);
5151 assert!(action_fired.get(), "re-enabled shortcut must dispatch");
5152 assert!(
5153 !on_key_fired.get(),
5154 "enabled shortcut must consume the KeyDown"
5155 );
5156 }
5157
5158 #[test]
5159 fn keyboard_capture_bypasses_shortcut() {
5160 use crate::action::Action;
5161 use crate::shortcut::{KeyStroke, Shortcut};
5162 use std::cell::Cell;
5163 use std::rc::Rc;
5164
5165 fn run(capture: bool) -> (bool, bool) {
5171 let action_fired = Rc::new(Cell::new(false));
5172 let on_key_fired = Rc::new(Cell::new(false));
5173 let af = action_fired.clone();
5174 let kf = on_key_fired.clone();
5175
5176 let mut tree = WidgetTree::new();
5177 let widget = tree.add(
5178 FillWidget::new()
5179 .focusable()
5180 .keyboard_capture(capture)
5181 .on_key(move |event, _ctx| {
5182 if matches!(
5183 event,
5184 WidgetEvent::KeyDown { key: Key::S, modifiers, .. } if modifiers.command()
5185 ) {
5186 kf.set(true);
5187 return EventResponse::Handled;
5188 }
5189 EventResponse::Ignored
5190 }),
5191 );
5192 tree.push_action(
5193 widget,
5194 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
5195 );
5196 tree.shortcut_registry_mut().register(
5197 Shortcut::new("app.save")
5198 .primary(KeyStroke::command(Key::S))
5199 .build(),
5200 );
5201
5202 tree.layout(SizeProposal::exact(100.0, 50.0));
5203 tree.focus(widget);
5204 tree.press_key(Key::S, Modifiers::COMMAND);
5205 (action_fired.get(), on_key_fired.get())
5206 }
5207
5208 let (action, on_key) = run(true);
5210 assert!(
5211 !action,
5212 "keyboard_capture must suppress the shortcut action"
5213 );
5214 assert!(on_key, "keyboard_capture must deliver the raw KeyDown");
5215
5216 let (action, on_key) = run(false);
5218 assert!(action, "without capture the shortcut must fire");
5219 assert!(!on_key, "without capture the widget must not see the key");
5220 }
5221
5222 fn key_recorder(
5224 tree: &mut WidgetTree,
5225 ) -> (WidgetId, std::rc::Rc<std::cell::RefCell<Vec<Key>>>) {
5226 let seen = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
5227 let log = seen.clone();
5228 let widget = tree.add(FillWidget::new().focusable().on_key(move |event, _ctx| {
5229 if let WidgetEvent::KeyDown { key, .. } = event {
5230 log.borrow_mut().push(*key);
5231 }
5232 EventResponse::Ignored
5233 }));
5234 tree.layout(SizeProposal::exact(100.0, 50.0));
5235 tree.focus(widget);
5236 (widget, seen)
5237 }
5238
5239 #[test]
5240 fn a_shortcut_no_action_takes_leaves_its_key_to_the_focused_widget() {
5241 use crate::action::Action;
5242 use crate::intent::IntentResponse;
5243 use crate::shortcut::{KeyStroke, Shortcut};
5244
5245 let mut tree = WidgetTree::new();
5246 let (widget, seen) = key_recorder(&mut tree);
5247 tree.shortcut_registry_mut().register(
5248 Shortcut::new("app.save")
5249 .primary(KeyStroke::command(Key::S))
5250 .build(),
5251 );
5252
5253 tree.press_key(Key::S, Modifiers::COMMAND);
5255 assert_eq!(
5256 *seen.borrow(),
5257 vec![Key::S],
5258 "an unhandled intent must not eat the key"
5259 );
5260
5261 tree.push_action(
5263 widget,
5264 Action::new("app.save").on_invoke_with_response(|_i, _c| IntentResponse::Propagated),
5265 );
5266 tree.press_key(Key::S, Modifiers::COMMAND);
5267 assert_eq!(
5268 seen.borrow().len(),
5269 2,
5270 "a propagated intent must leave the key too"
5271 );
5272
5273 let mut tree = WidgetTree::new();
5276 let (widget, seen) = key_recorder(&mut tree);
5277 tree.shortcut_registry_mut().register(
5278 Shortcut::new("app.save")
5279 .primary(KeyStroke::command(Key::S))
5280 .build(),
5281 );
5282 tree.push_action(widget, Action::new("app.save").on_invoke(|_i, _c| {}));
5283 tree.press_key(Key::S, Modifiers::COMMAND);
5284 assert!(
5285 seen.borrow().is_empty(),
5286 "a handled shortcut consumes its key"
5287 );
5288 }
5289
5290 #[test]
5291 fn standard_edit_chords_follow_a_rebind() {
5292 use crate::action::Action;
5293 use crate::shortcut::{KeyStroke, StandardEdit};
5294 use std::cell::Cell;
5295 use std::rc::Rc;
5296
5297 let mut tree = WidgetTree::new();
5298 let (widget, seen) = key_recorder(&mut tree);
5299 let copies = Rc::new(Cell::new(0));
5300 let count = copies.clone();
5301 tree.push_action(
5302 widget,
5303 Action::new(StandardEdit::Copy.id())
5304 .on_invoke(move |_i, _c| count.set(count.get() + 1)),
5305 );
5306
5307 tree.press_key(Key::C, Modifiers::COMMAND);
5309 assert_eq!(copies.get(), 1, "Ctrl+C reaches the edit.copy action");
5310 assert!(seen.borrow().is_empty());
5311
5312 let elsewhere = KeyStroke::new(Key::Character('à'), Modifiers::CTRL);
5315 tree.shortcut_registry_mut()
5316 .rebind_primary(StandardEdit::Copy.id(), Some(elsewhere));
5317 tree.press_key(Key::C, Modifiers::COMMAND);
5318 assert_eq!(copies.get(), 1, "the replaced chord must not copy");
5319 assert_eq!(*seen.borrow(), vec![Key::C]);
5320 tree.press_key(Key::Character('à'), Modifiers::CTRL);
5321 assert_eq!(copies.get(), 2, "the new chord copies");
5322 }
5323
5324 #[test]
5325 fn ctrl_tab_always_escapes_a_keyboard_capture_surface() {
5326 use std::cell::Cell;
5327 use std::rc::Rc;
5328
5329 let saw_key = Rc::new(Cell::new(false));
5335 let sk = saw_key.clone();
5336
5337 let mut tree = WidgetTree::new();
5338 let capture = tree.add(
5339 FillWidget::new()
5340 .focusable()
5341 .keyboard_capture(true)
5342 .on_key(move |_event, _ctx| {
5344 sk.set(true);
5345 EventResponse::Handled
5346 }),
5347 );
5348 let neighbour = tree.add(FillWidget::new().focusable());
5349 tree.layout(SizeProposal::exact(100.0, 50.0));
5350
5351 tree.focus(capture);
5354 tree.press_key(Key::Tab, Modifiers::NONE);
5355 assert!(saw_key.get(), "plain Tab must reach the capture surface");
5356 assert_eq!(
5357 tree.focused(),
5358 Some(capture),
5359 "plain Tab must not move focus off a capture surface"
5360 );
5361
5362 saw_key.set(false);
5364 tree.press_key(Key::Tab, Modifiers::CTRL);
5365 assert!(
5366 !saw_key.get(),
5367 "Ctrl+Tab is reserved and must not reach the capture surface"
5368 );
5369 assert_eq!(
5370 tree.focused(),
5371 Some(neighbour),
5372 "Ctrl+Tab must move focus out of a capture surface"
5373 );
5374
5375 tree.focus(capture);
5377 tree.press_key(Key::Tab, Modifiers::CTRL | Modifiers::SHIFT);
5378 assert_eq!(
5379 tree.focused(),
5380 Some(neighbour),
5381 "Ctrl+Shift+Tab must move focus out of a capture surface"
5382 );
5383 }
5384
5385 #[test]
5386 fn scope_mismatch_does_not_invoke_on_activate() {
5387 use crate::intent::Intent;
5388 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
5389 use std::cell::Cell;
5390 use std::rc::Rc;
5391
5392 let activated = Rc::new(Cell::new(false));
5398 let activated_flag = activated.clone();
5399
5400 let mut tree = WidgetTree::new();
5401 let scope_root = tree.add(FillWidget::new().focusable());
5402 let outside = tree.add(FillWidget::new().focusable());
5403
5404 tree.shortcut_registry_mut().register(
5405 Shortcut::new("editor.find")
5406 .primary(KeyStroke::command(Key::F))
5407 .scope(ShortcutScope::Scoped(scope_root))
5408 .on_activate(move |_ks, _ctx| {
5409 activated_flag.set(true);
5410 Intent::new("editor.find")
5411 })
5412 .build(),
5413 );
5414
5415 tree.layout(SizeProposal::exact(200.0, 100.0));
5416 tree.focus(outside);
5417
5418 tree.press_key(Key::F, Modifiers::COMMAND);
5419 assert!(
5420 !activated.get(),
5421 "on_activate must not run when focus is outside the shortcut's scope"
5422 );
5423 }
5424
5425 #[test]
5426 fn key_capture_runs_callback_and_bypasses_registry() {
5427 use crate::action::Action;
5428 use crate::shortcut::{KeyStroke, Shortcut};
5429 use std::cell::Cell;
5430 use std::rc::Rc;
5431
5432 let action_fired = Rc::new(Cell::new(false));
5433 let af = action_fired.clone();
5434 let captured = Rc::new(Cell::new(None));
5435 let cf = captured.clone();
5436
5437 let mut tree = WidgetTree::new();
5438 let widget = tree.add(FillWidget::new().focusable());
5439 tree.push_action(
5440 widget,
5441 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
5442 );
5443 tree.shortcut_registry_mut().register(
5444 Shortcut::new("app.save")
5445 .primary(KeyStroke::command(Key::S))
5446 .build(),
5447 );
5448
5449 tree.layout(SizeProposal::exact(100.0, 50.0));
5450 tree.focus(widget);
5451
5452 let handle = tree.begin_key_capture(move |ks, _reg, _ctx| cf.set(Some(ks)));
5453 assert!(tree.is_capturing_keys());
5454
5455 tree.press_key(Key::S, Modifiers::COMMAND);
5456 assert_eq!(
5457 captured.get(),
5458 Some(KeyStroke::command(Key::S)),
5459 "capture callback must receive the chord"
5460 );
5461 assert!(
5462 !action_fired.get(),
5463 "shortcut action must not fire while capture is armed"
5464 );
5465 assert!(
5466 !tree.is_capturing_keys(),
5467 "capture is one-shot; next KeyDown flows normally"
5468 );
5469 drop(handle);
5470 }
5471
5472 #[test]
5473 fn key_capture_can_rebind_through_registry() {
5474 use crate::shortcut::{KeyStroke, Shortcut};
5475
5476 let mut tree = WidgetTree::new();
5477 let widget = tree.add(FillWidget::new().focusable());
5478 tree.shortcut_registry_mut().register(
5479 Shortcut::new("app.save")
5480 .primary(KeyStroke::command(Key::S))
5481 .build(),
5482 );
5483
5484 tree.layout(SizeProposal::exact(100.0, 50.0));
5485 tree.focus(widget);
5486
5487 let _h = tree.begin_key_capture(|ks, reg, _ctx| {
5489 reg.rebind_primary("app.save", Some(ks));
5490 });
5491
5492 tree.press_key(Key::B, Modifiers::COMMAND | Modifiers::SHIFT);
5493 assert_eq!(
5494 tree.shortcut_registry()
5495 .effective("app.save")
5496 .unwrap()
5497 .primary,
5498 Some(KeyStroke::command_shift(Key::B))
5499 );
5500 }
5501
5502 #[test]
5503 fn dropping_capture_handle_cancels_capture() {
5504 use crate::shortcut::{KeyStroke, Shortcut};
5505 use std::cell::Cell;
5506 use std::rc::Rc;
5507
5508 let action_fired = Rc::new(Cell::new(false));
5509 let af = action_fired.clone();
5510 let capture_fired = Rc::new(Cell::new(false));
5511 let cf = capture_fired.clone();
5512
5513 let mut tree = WidgetTree::new();
5514 let widget = tree.add(FillWidget::new().focusable());
5515 tree.push_action(
5516 widget,
5517 crate::action::Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
5518 );
5519 tree.shortcut_registry_mut().register(
5520 Shortcut::new("app.save")
5521 .primary(KeyStroke::command(Key::S))
5522 .build(),
5523 );
5524 tree.layout(SizeProposal::exact(100.0, 50.0));
5525 tree.focus(widget);
5526
5527 {
5531 let _h = tree.begin_key_capture(move |_ks, _reg, _ctx| cf.set(true));
5532 assert!(tree.is_capturing_keys());
5533 }
5535 assert!(
5536 !tree.is_capturing_keys(),
5537 "dropping the handle must cancel the capture"
5538 );
5539
5540 tree.press_key(Key::S, Modifiers::COMMAND);
5541 assert!(!capture_fired.get(), "cancelled capture must not fire");
5542 assert!(
5543 action_fired.get(),
5544 "shortcut action runs after capture was cancelled"
5545 );
5546 }
5547
5548 #[test]
5549 fn second_begin_key_capture_does_not_racecancel_first() {
5550 use std::cell::Cell;
5551 use std::rc::Rc;
5552
5553 let first = Rc::new(Cell::new(false));
5554 let second = Rc::new(Cell::new(false));
5555 let f = first.clone();
5556 let s = second.clone();
5557
5558 let mut tree = WidgetTree::new();
5559 let widget = tree.add(FillWidget::new().focusable());
5560 tree.layout(SizeProposal::exact(100.0, 50.0));
5561 tree.focus(widget);
5562
5563 let h1 = tree.begin_key_capture(move |_ks, _reg, _ctx| f.set(true));
5569 let _h2 = tree.begin_key_capture(move |_ks, _reg, _ctx| s.set(true));
5570 drop(h1);
5571
5572 assert!(
5573 tree.is_capturing_keys(),
5574 "dropping the older handle must not cancel the active capture"
5575 );
5576 tree.press_key(Key::K, Modifiers::COMMAND);
5577 assert!(!first.get());
5578 assert!(second.get(), "newest capture wins");
5579 }
5580
5581 #[test]
5582 fn capture_callback_can_send_intent() {
5583 use crate::action::Action;
5584 use crate::intent::Intent;
5585
5586 use std::cell::Cell;
5587 use std::rc::Rc;
5588
5589 let ran = Rc::new(Cell::new(false));
5590 let flag = ran.clone();
5591
5592 let mut tree = WidgetTree::new();
5593 let widget = tree.add(FillWidget::new().focusable());
5594 tree.push_action(
5595 widget,
5596 Action::new("app.save").on_invoke(move |_i, _c| flag.set(true)),
5597 );
5598 tree.layout(SizeProposal::exact(100.0, 50.0));
5599 tree.focus(widget);
5600
5601 let _h = tree.begin_key_capture(|_ks, _reg, ctx| {
5602 ctx.send_intent(Intent::new("app.save"));
5603 });
5604 tree.press_key(Key::X, Modifiers::COMMAND);
5605 assert!(
5606 ran.get(),
5607 "intent queued from capture callback must dispatch"
5608 );
5609 }
5610
5611 #[test]
5612 fn binding_registry_does_not_accumulate_across_rebuilds() {
5613 use crate::binding::BindingLevel;
5614 use crate::signal::Signal;
5615
5616 #[derive(Debug)]
5617 struct BoundLeaf {
5618 tick: Signal<u64>,
5619 }
5620 impl crate::widget::Widget for BoundLeaf {
5621 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
5622 self.tick.bind_to(
5623 ctx.self_id(),
5624 ctx.binding_registry(),
5625 BindingLevel::Relayout,
5626 );
5627 Vec::new()
5628 }
5629 fn layout_response(
5630 &self,
5631 proposal: SizeProposal,
5632 _ctx: &crate::widget::LayoutContext,
5633 ) -> crate::widget::LayoutResponse {
5634 proposal.resolve(10.0, 10.0).into()
5635 }
5636 }
5637
5638 let mut tree = WidgetTree::new();
5639 let tick = Signal::new(0_u64);
5640 let widget = tree.add(BoundLeaf { tick: tick.clone() });
5641 tree.layout(SizeProposal::exact(200.0, 200.0));
5642 let after_first_build = tree.binding_registry().len();
5643 assert!(after_first_build >= 1);
5644
5645 for _ in 0..5 {
5649 tree.arena.mark_needs_rebuild(widget);
5650 tree.layout(SizeProposal::exact(200.0, 200.0));
5651 }
5652 assert_eq!(
5653 tree.binding_registry().len(),
5654 after_first_build,
5655 "bindings must be cleared on rebuild"
5656 );
5657
5658 tree.destroy_subtree(widget);
5659 assert_eq!(
5660 tree.binding_registry().len(),
5661 0,
5662 "bindings must be cleared on destroy"
5663 );
5664 let _ = tick;
5666 }
5667
5668 #[test]
5669 fn ctx_destroy_cancels_animations_and_bindings_via_deferred_path() {
5670 use crate::binding::BindingLevel;
5678 use crate::signal::Signal;
5679 use std::time::{Duration, Instant};
5680 use teksilo_tokens::Easing;
5681
5682 #[derive(Debug)]
5683 struct BoundLeaf {
5684 tick: Signal<u64>,
5685 }
5686 impl crate::widget::Widget for BoundLeaf {
5687 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
5688 self.tick.bind_to(
5689 ctx.self_id(),
5690 ctx.binding_registry(),
5691 BindingLevel::Relayout,
5692 );
5693 Vec::new()
5694 }
5695 fn layout_response(
5696 &self,
5697 proposal: SizeProposal,
5698 _ctx: &crate::widget::LayoutContext,
5699 ) -> crate::widget::LayoutResponse {
5700 proposal.resolve(10.0, 10.0).into()
5701 }
5702 }
5703
5704 let mut tree = WidgetTree::new();
5705 let widget = tree.add(BoundLeaf {
5706 tick: Signal::new(0_u64),
5707 });
5708 tree.layout(SizeProposal::exact(200.0, 200.0));
5709 assert!(tree.binding_registry().len() >= 1);
5710
5711 let anim = Signal::<f32>::new_animated(0.0);
5714 tree.animation_scheduler.animate(
5715 &anim,
5716 widget,
5717 1.0,
5718 Duration::from_secs(10),
5719 Easing::Linear,
5720 Instant::now(),
5721 );
5722 assert_eq!(tree.animation_scheduler.active_count(), 1);
5723
5724 let mut noop = crate::window::NoopWindowOps;
5726 tree.run_with_event_context(&mut noop, |ctx| ctx.destroy(widget));
5727
5728 assert_eq!(
5729 tree.animation_scheduler.active_count(),
5730 0,
5731 "ctx.destroy must cancel animations owned by the destroyed widget"
5732 );
5733 assert_eq!(
5734 tree.binding_registry().len(),
5735 0,
5736 "ctx.destroy must unregister the destroyed widget's bindings"
5737 );
5738 assert!(
5739 tree.arena.get(widget).is_none(),
5740 "node must be removed from the arena"
5741 );
5742 }
5743
5744 #[test]
5745 fn clear_shortcut_override_via_event_context_restores_default() {
5746 use crate::shortcut::{KeyStroke, Shortcut};
5747
5748 let mut tree = WidgetTree::new();
5749 tree.shortcut_registry_mut().register(
5750 Shortcut::new("app.save")
5751 .primary(KeyStroke::command(Key::S))
5752 .build(),
5753 );
5754 tree.shortcut_registry_mut()
5755 .rebind_primary("app.save", Some(KeyStroke::alt(Key::S)));
5756
5757 let source = tree.add(FillWidget::new());
5758 let mut ctx = EventContext::new();
5759 ctx.clear_shortcut_override("app.save");
5760 tree.collect_from_ctx(ctx, source);
5761
5762 assert_eq!(
5763 tree.shortcut_registry()
5764 .effective("app.save")
5765 .unwrap()
5766 .primary,
5767 Some(KeyStroke::command(Key::S))
5768 );
5769 }
5770
5771 #[test]
5772 fn rebind_shortcut_primary_via_event_context() {
5773 use crate::shortcut::{KeyStroke, Shortcut};
5774
5775 let mut tree = WidgetTree::new();
5776 tree.shortcut_registry_mut().register(
5777 Shortcut::new("app.save")
5778 .primary(KeyStroke::command(Key::S))
5779 .build(),
5780 );
5781 let source = tree.add(FillWidget::new());
5782
5783 let mut ctx = EventContext::new();
5784 ctx.rebind_shortcut_primary("app.save", Some(KeyStroke::alt(Key::S)));
5785 tree.collect_from_ctx(ctx, source);
5786
5787 assert_eq!(
5788 tree.shortcut_registry()
5789 .effective("app.save")
5790 .unwrap()
5791 .primary,
5792 Some(KeyStroke::alt(Key::S))
5793 );
5794 }
5795
5796 #[test]
5797 fn unregister_all_for_owner_called_on_destroy() {
5798 use crate::shortcut::{KeyStroke, Shortcut};
5799
5800 let mut tree = WidgetTree::new();
5801 let widget = tree.add(FillWidget::new());
5802 let widget_owner = widget;
5803 tree.shortcut_registry_mut().register_owned(
5804 Shortcut::new("scoped.thing")
5805 .primary(KeyStroke::command(Key::K))
5806 .build(),
5807 widget_owner,
5808 );
5809 assert!(
5810 tree.shortcut_registry()
5811 .get_default("scoped.thing")
5812 .is_some()
5813 );
5814
5815 tree.destroy_subtree(widget);
5816 assert!(
5817 tree.shortcut_registry()
5818 .get_default("scoped.thing")
5819 .is_none(),
5820 "destroying the owner must unregister its shortcut"
5821 );
5822 }
5823
5824 #[test]
5828 fn global_action_reached_from_unrelated_source() {
5829 use crate::action::Action;
5830 use crate::intent::Intent;
5831 use std::cell::Cell;
5832 use std::rc::Rc;
5833
5834 #[derive(Debug)]
5835 struct Registrar(Rc<Cell<bool>>);
5836 impl crate::widget::Widget for Registrar {
5837 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
5838 let flag = self.0.clone();
5839 ctx.register_action_global(
5840 Action::new("test.global").on_invoke(move |_i, _c| flag.set(true)),
5841 );
5842 vec![]
5843 }
5844 fn layout_response(
5845 &self,
5846 _p: teksilo_canvas::SizeProposal,
5847 _c: &crate::widget::LayoutContext,
5848 ) -> crate::widget::LayoutResponse {
5849 teksilo_canvas::Size::new(0.0, 0.0).into()
5850 }
5851 }
5852
5853 let mut tree = WidgetTree::new();
5854 let fired = Rc::new(Cell::new(false));
5855 let registrar = tree.add(Registrar(fired.clone()));
5856 let source = tree.add(FillWidget::new()); let mut ops = crate::window::NoopWindowOps;
5858
5859 tree.dispatch_intent(source, Intent::new("test.global"), true, &mut ops);
5860 assert!(
5861 fired.get(),
5862 "global action must fire from an unrelated source"
5863 );
5864
5865 fired.set(false);
5867 tree.destroy_subtree(registrar);
5868 tree.dispatch_intent(source, Intent::new("test.global"), true, &mut ops);
5869 assert!(
5870 !fired.get(),
5871 "destroying the owner must remove its global action"
5872 );
5873 }
5874
5875 #[test]
5887 fn hit_test_through_translate_scope() {
5888 use crate::test_widgets::StackWidget;
5889 let mut tree = WidgetTree::new();
5890 let child = tree.add(FillWidget::new());
5891 let parent = tree.add(StackWidget::new().child(child));
5892 tree.set_transform(parent, teksilo_canvas::Transform2D::translate(100.0, 0.0));
5894 tree.layout(SizeProposal::exact(100.0, 50.0));
5895
5896 assert_eq!(
5900 tree.hit_test(Point::new(50.0, 25.0)),
5901 None,
5902 "pre-transform area is not visually populated and must not hit"
5903 );
5904 assert_eq!(
5906 tree.hit_test(Point::new(150.0, 25.0)),
5907 Some(child),
5908 "visually-rendered area must hit the child"
5909 );
5910 assert_eq!(tree.hit_test(Point::new(250.0, 25.0)), None);
5912 }
5913
5914 #[test]
5915 fn hit_test_through_scale_scope() {
5916 use crate::test_widgets::StackWidget;
5917 let mut tree = WidgetTree::new();
5918 let child = tree.add(FillWidget::new());
5919 let parent = tree.add(StackWidget::new().child(child));
5920 tree.set_transform(parent, teksilo_canvas::Transform2D::scale(0.5, 0.5));
5923 tree.layout(SizeProposal::exact(100.0, 50.0));
5924
5925 assert_eq!(tree.hit_test(Point::new(25.0, 12.0)), Some(child));
5927 assert_eq!(
5930 tree.hit_test(Point::new(75.0, 25.0)),
5931 None,
5932 "scaled-out region must not hit"
5933 );
5934 }
5935
5936 #[test]
5937 fn hit_test_through_nested_transforms_compose() {
5938 use crate::test_widgets::StackWidget;
5939 let mut tree = WidgetTree::new();
5940 let leaf = tree.add(FillWidget::new());
5941 let inner = tree.add(StackWidget::new().child(leaf));
5942 let outer = tree.add(StackWidget::new().child(inner));
5943 tree.set_transform(outer, teksilo_canvas::Transform2D::translate(100.0, 0.0));
5947 tree.set_transform(inner, teksilo_canvas::Transform2D::scale(2.0, 2.0));
5948 tree.layout(SizeProposal::exact(50.0, 25.0));
5949
5950 assert_eq!(tree.hit_test(Point::new(150.0, 25.0)), Some(leaf));
5954 assert_eq!(tree.hit_test(Point::new(50.0, 25.0)), None);
5955 assert_eq!(tree.hit_test(Point::new(250.0, 25.0)), None);
5956 }
5957
5958 #[test]
5959 fn hit_test_identity_transform_unchanged() {
5960 let mut tree = WidgetTree::new();
5964 let widget = tree.add(FillWidget::new());
5965 tree.set_transform(widget, teksilo_canvas::Transform2D::IDENTITY);
5966 tree.layout(SizeProposal::exact(100.0, 50.0));
5967 assert_eq!(tree.hit_test(Point::new(50.0, 25.0)), Some(widget));
5968 }
5969
5970 #[test]
5971 fn arena_effective_transform_composes_ancestors() {
5972 use crate::test_widgets::StackWidget;
5983 let mut tree = WidgetTree::new();
5984 let leaf = tree.add(FillWidget::new());
5985 let inner = tree.add(StackWidget::new().child(leaf));
5986 let outer = tree.add(StackWidget::new().child(inner));
5987 tree.set_transform(outer, teksilo_canvas::Transform2D::translate(100.0, 0.0));
5988 tree.set_transform(inner, teksilo_canvas::Transform2D::scale(2.0, 2.0));
5989 tree.layout(SizeProposal::exact(50.0, 25.0));
5990
5991 let eff = tree.arena.effective_transform(leaf);
5992 let expected = teksilo_canvas::Transform2D::scale(2.0, 2.0)
5993 .then(&teksilo_canvas::Transform2D::translate(100.0, 0.0));
5994 for (a, b) in eff.m.iter().zip(expected.m.iter()) {
5995 assert!(
5996 (a - b).abs() < 1e-5,
5997 "effective_transform mismatch: got {:?}, want {:?}",
5998 eff.m,
5999 expected.m
6000 );
6001 }
6002
6003 let screen_origin = eff.apply_point(Point::new(0.0, 0.0));
6009 assert!((screen_origin.x - 100.0).abs() < 1e-5);
6010 assert!((screen_origin.y - 0.0).abs() < 1e-5);
6011 let screen_corner = eff.apply_point(Point::new(50.0, 25.0));
6014 assert!((screen_corner.x - 200.0).abs() < 1e-5);
6015 assert!((screen_corner.y - 50.0).abs() < 1e-5);
6016 }
6017
6018 #[derive(Debug)]
6023 struct StubMenu;
6024 impl crate::widget::Widget for StubMenu {
6025 fn layout_response(
6026 &self,
6027 _proposal: SizeProposal,
6028 _ctx: &crate::widget::LayoutContext,
6029 ) -> crate::widget::LayoutResponse {
6030 teksilo_canvas::Size::new(100.0, 40.0).into()
6031 }
6032 }
6033
6034 #[derive(Debug)]
6046 struct NominatingWidget {
6047 row: std::cell::Cell<Option<WidgetId>>,
6048 }
6049
6050 impl crate::widget::Widget for NominatingWidget {
6051 fn layout_response(
6052 &self,
6053 proposal: SizeProposal,
6054 _ctx: &LayoutContext,
6055 ) -> crate::widget::LayoutResponse {
6056 proposal.resolve(50.0, 20.0).into()
6057 }
6058
6059 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6060 ctx.apply_self_handlers(crate::widget_builder::HandlerSet::new().focusable(true));
6061 Vec::new()
6062 }
6063
6064 fn context_menu_key_target(&self) -> Option<WidgetId> {
6065 self.row.get()
6066 }
6067 }
6068
6069 fn press(tree: &mut WidgetTree, key: Key, modifiers: Modifiers) {
6070 tree.dispatch_event(WidgetEvent::KeyDown {
6071 key,
6072 modifiers,
6073 text: None,
6074 });
6075 }
6076
6077 #[test]
6078 fn the_context_menu_key_opens_the_focused_widget_menu() {
6079 use std::cell::Cell;
6080 use std::rc::Rc;
6081
6082 let opened = Rc::new(Cell::new(false));
6083 let flag = opened.clone();
6084 let mut tree = WidgetTree::new();
6085 let widget = tree.add(
6086 FillWidget::new()
6087 .focusable()
6088 .context_menu(move |_pos, _ctx| {
6089 flag.set(true);
6090 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6091 }),
6092 );
6093 tree.layout(SizeProposal::exact(200.0, 100.0));
6094 tree.focus(widget);
6095
6096 press(&mut tree, Key::ContextMenu, Modifiers::NONE);
6097 assert!(opened.get(), "the dedicated Menu key must open the menu");
6098 }
6099
6100 #[test]
6103 fn shift_f10_opens_the_focused_widget_menu() {
6104 use std::cell::Cell;
6105 use std::rc::Rc;
6106
6107 let opened = Rc::new(Cell::new(false));
6108 let flag = opened.clone();
6109 let mut tree = WidgetTree::new();
6110 let widget = tree.add(
6111 FillWidget::new()
6112 .focusable()
6113 .context_menu(move |_pos, _ctx| {
6114 flag.set(true);
6115 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6116 }),
6117 );
6118 tree.layout(SizeProposal::exact(200.0, 100.0));
6119 tree.focus(widget);
6120
6121 press(&mut tree, Key::F10, Modifiers::SHIFT);
6122 assert!(opened.get(), "Shift+F10 must open the menu");
6123 }
6124
6125 #[test]
6128 fn a_near_miss_chord_is_not_a_context_menu_request() {
6129 use std::cell::Cell;
6130 use std::rc::Rc;
6131
6132 let opened = Rc::new(Cell::new(false));
6133 let flag = opened.clone();
6134 let mut tree = WidgetTree::new();
6135 let widget = tree.add(
6136 FillWidget::new()
6137 .focusable()
6138 .context_menu(move |_pos, _ctx| {
6139 flag.set(true);
6140 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6141 }),
6142 );
6143 tree.layout(SizeProposal::exact(200.0, 100.0));
6144 tree.focus(widget);
6145
6146 press(&mut tree, Key::F10, Modifiers::SHIFT | Modifiers::CTRL);
6147 press(&mut tree, Key::F10, Modifiers::NONE);
6148 assert!(!opened.get(), "only Shift+F10 exactly asks for a menu");
6149 }
6150
6151 #[test]
6155 fn the_keyboard_target_can_be_a_row_rather_than_the_focused_container() {
6156 use std::cell::Cell;
6157 use std::rc::Rc;
6158
6159 let menu_owner = Rc::new(Cell::new(None::<&'static str>));
6160
6161 let row_flag = menu_owner.clone();
6162 let container_flag = menu_owner.clone();
6163
6164 let mut tree = WidgetTree::new();
6165 let row = tree.add(FillWidget::new().context_menu(move |_pos, _ctx| {
6166 row_flag.set(Some("row"));
6167 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6168 }));
6169 let container = tree.add(
6170 crate::test_widgets::StackWidget::new()
6171 .child(row)
6172 .context_menu(move |_pos, _ctx| {
6173 container_flag.set(Some("container"));
6174 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6175 }),
6176 );
6177 tree.layout(SizeProposal::exact(200.0, 100.0));
6178
6179 let nominator = tree.add(NominatingWidget {
6181 row: std::cell::Cell::new(Some(row)),
6182 });
6183 tree.layout(SizeProposal::exact(200.0, 100.0));
6184 tree.focus(nominator);
6185 let _ = container;
6186
6187 press(&mut tree, Key::ContextMenu, Modifiers::NONE);
6188 assert_eq!(
6189 menu_owner.get(),
6190 Some("row"),
6191 "the nominated row's factory must be the one that runs"
6192 );
6193 }
6194
6195 #[test]
6198 fn the_chord_falls_through_when_there_is_no_menu_to_show() {
6199 use std::cell::Cell;
6200 use std::rc::Rc;
6201
6202 let saw_key = Rc::new(Cell::new(false));
6203 let flag = saw_key.clone();
6204 let mut tree = WidgetTree::new();
6205 let widget = tree.add(FillWidget::new().focusable().on_key(move |ev, _ctx| {
6206 if matches!(ev, WidgetEvent::KeyDown { key: Key::F10, .. }) {
6207 flag.set(true);
6208 }
6209 crate::event::EventResponse::Ignored
6210 }));
6211 tree.layout(SizeProposal::exact(200.0, 100.0));
6212 tree.focus(widget);
6213
6214 press(&mut tree, Key::F10, Modifiers::SHIFT);
6215 assert!(
6216 saw_key.get(),
6217 "with no factory anywhere, the key must reach the widget"
6218 );
6219 }
6220
6221 #[test]
6227 fn each_route_to_a_menu_tells_the_factory_what_asked_for_it() {
6228 use crate::widget_builder::ContextMenuTrigger;
6229 use std::cell::RefCell;
6230 use std::rc::Rc;
6231
6232 let seen = Rc::new(RefCell::new(Vec::new()));
6233 let log = seen.clone();
6234 let mut tree = WidgetTree::new();
6235 let widget = tree.add(
6236 FillWidget::new()
6237 .focusable()
6238 .context_menu(move |_pos, ctx| {
6239 log.borrow_mut().push(ctx.context_menu_trigger());
6240 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6241 }),
6242 );
6243 tree.layout(SizeProposal::exact(200.0, 100.0));
6244 tree.focus(widget);
6245 let inside = tree.bounds(widget).center();
6246
6247 press(&mut tree, Key::F10, Modifiers::SHIFT);
6248 press(&mut tree, Key::Escape, Modifiers::NONE);
6249 press(&mut tree, Key::ContextMenu, Modifiers::NONE);
6250 press(&mut tree, Key::Escape, Modifiers::NONE);
6251 tree.dispatch_event(WidgetEvent::pointer_down(
6252 inside,
6253 crate::event::PointerButton::Secondary,
6254 Modifiers::NONE,
6255 ));
6256 tree.dispatch_event(WidgetEvent::pointer_up(
6257 inside,
6258 crate::event::PointerButton::Secondary,
6259 Modifiers::NONE,
6260 ));
6261 press(&mut tree, Key::Escape, Modifiers::NONE);
6262 tree.dispatch_event(WidgetEvent::AccessAction {
6263 action: accesskit::Action::ShowContextMenu,
6264 target: Some(widget),
6265 target_node: crate::accessibility::widget_id_to_node_id(widget),
6266 data: None,
6267 });
6268 press(&mut tree, Key::Escape, Modifiers::NONE);
6269 tree.long_press_at(teksilo_tokens::PointerKind::Touch, inside);
6270
6271 assert_eq!(
6272 *seen.borrow(),
6273 vec![
6274 Some(ContextMenuTrigger::Keyboard),
6275 Some(ContextMenuTrigger::Keyboard),
6276 Some(ContextMenuTrigger::Pointer),
6277 Some(ContextMenuTrigger::Accessibility),
6278 Some(ContextMenuTrigger::Pointer),
6279 ],
6280 "Shift+F10, the Menu key, a right-click, the assistive action and a \
6281 hold, in that order"
6282 );
6283 }
6284
6285 #[test]
6286 fn context_menu_factory_receives_click_position() {
6287 use crate::event::{Modifiers, PointerButton};
6288 use std::cell::Cell;
6289 use std::rc::Rc;
6290
6291 let captured_position = Rc::new(Cell::new(None::<Point>));
6292 let cap = captured_position.clone();
6293 let mut tree = WidgetTree::new();
6294 let widget = tree.add(FillWidget::new().context_menu(move |pos, _ctx| {
6295 cap.set(Some(pos));
6296 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6297 }));
6298 tree.layout(SizeProposal::exact(200.0, 100.0));
6299
6300 let click = Point::new(73.0, 42.0);
6301 tree.dispatch_event(WidgetEvent::pointer_down(
6302 click,
6303 PointerButton::Secondary,
6304 Modifiers::NONE,
6305 ));
6306
6307 let got = captured_position.get();
6308 assert_eq!(
6309 got,
6310 Some(click),
6311 "factory must receive the click position; got {:?}",
6312 got
6313 );
6314 let _ = widget;
6315 }
6316
6317 #[test]
6318 fn context_menu_factory_returning_none_falls_through_to_parent() {
6319 use crate::event::{Modifiers, PointerButton};
6320 use crate::test_widgets::StackWidget;
6321 use std::cell::Cell;
6322 use std::rc::Rc;
6323
6324 let outer_called = Rc::new(Cell::new(0_u32));
6328 let outer_flag = outer_called.clone();
6329 let mut tree = WidgetTree::new();
6330 let inner = tree.add(FillWidget::new().context_menu(|_pos, _ctx| None));
6331 let _outer = tree.add(
6332 StackWidget::new()
6333 .child(inner)
6334 .context_menu(move |_pos, _ctx| {
6335 outer_flag.set(outer_flag.get() + 1);
6336 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6337 }),
6338 );
6339 tree.layout(SizeProposal::exact(200.0, 100.0));
6340
6341 tree.dispatch_event(WidgetEvent::pointer_down(
6342 Point::new(50.0, 25.0),
6343 PointerButton::Secondary,
6344 Modifiers::NONE,
6345 ));
6346
6347 assert_eq!(
6348 outer_called.get(),
6349 1,
6350 "inner returning None must fall through to the outer factory"
6351 );
6352 }
6353
6354 #[test]
6355 fn context_menu_factory_none_throughout_chain_does_not_show_overlay() {
6356 use crate::event::{Modifiers, PointerButton};
6357
6358 let mut tree = WidgetTree::new();
6360 tree.add(FillWidget::new().context_menu(|_pos, _ctx| None));
6361 tree.layout(SizeProposal::exact(200.0, 100.0));
6362
6363 let overlay_count_before = tree.overlay_manager.len();
6364 tree.dispatch_event(WidgetEvent::pointer_down(
6365 Point::new(50.0, 25.0),
6366 PointerButton::Secondary,
6367 Modifiers::NONE,
6368 ));
6369 let overlay_count_after = tree.overlay_manager.len();
6370 assert_eq!(
6371 overlay_count_before, overlay_count_after,
6372 "a factory returning None must not mount any overlay"
6373 );
6374 }
6375
6376 #[derive(Debug)]
6387 struct FreshChildHost {
6388 preserve: bool,
6389 }
6390 impl Widget for FreshChildHost {
6391 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6392 vec![ctx.add(FillWidget::new())]
6393 }
6394 fn layout_response(
6395 &self,
6396 p: SizeProposal,
6397 _c: &LayoutContext,
6398 ) -> crate::widget::LayoutResponse {
6399 p.resolve(10.0, 10.0).into()
6400 }
6401 fn preserves_children_on_rebuild(&self) -> bool {
6402 self.preserve
6403 }
6404 }
6405
6406 #[test]
6407 fn reconcile_reaps_dropped_children_no_leak() {
6408 for preserve in [false, true] {
6413 let mut tree = WidgetTree::new();
6414 let host = tree.add(FreshChildHost { preserve });
6415 tree.layout(SizeProposal::exact(100.0, 100.0));
6416 let total0 = tree.arena.len();
6417 let active0 = tree.active_widget_count();
6418 for _ in 0..5 {
6419 tree.arena_mark_needs_rebuild_for_testing(host);
6420 tree.layout(SizeProposal::exact(100.0, 100.0));
6421 }
6422 assert_eq!(
6423 tree.arena.len(),
6424 total0,
6425 "preserve={preserve}: dropped children must be reaped, not leaked"
6426 );
6427 assert_eq!(
6428 tree.active_widget_count(),
6429 active0,
6430 "preserve={preserve}: no stranded still-active orphans"
6431 );
6432 }
6433 }
6434
6435 #[derive(Debug)]
6441 struct DetachedContentHost {
6442 preserve: bool,
6443 }
6444 impl Widget for DetachedContentHost {
6445 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6446 let popup = ctx.add_detached(FillWidget::new());
6447 ctx.set_dormant(popup);
6448 vec![ctx.add(FillWidget::new())]
6449 }
6450 fn layout_response(
6451 &self,
6452 p: SizeProposal,
6453 _c: &LayoutContext,
6454 ) -> crate::widget::LayoutResponse {
6455 p.resolve(10.0, 10.0).into()
6456 }
6457 fn preserves_children_on_rebuild(&self) -> bool {
6458 self.preserve
6459 }
6460 }
6461
6462 #[test]
6463 fn rebuilding_reaps_detached_content_no_leak() {
6464 for preserve in [false, true] {
6470 let mut tree = WidgetTree::new();
6471 let host = tree.add(DetachedContentHost { preserve });
6472 tree.layout(SizeProposal::exact(100.0, 100.0));
6473 let total0 = tree.arena.len();
6474 for _ in 0..5 {
6475 tree.arena_mark_needs_rebuild_for_testing(host);
6476 tree.layout(SizeProposal::exact(100.0, 100.0));
6477 }
6478 assert_eq!(
6479 tree.arena.len(),
6480 total0,
6481 "preserve={preserve}: the previous build's detached content must be reaped"
6482 );
6483 }
6484 }
6485
6486 #[test]
6487 fn destroying_a_host_reaps_its_detached_content() {
6488 let mut tree = WidgetTree::new();
6489 let outer = tree.add(FillWidget::new());
6490 tree.layout(SizeProposal::exact(100.0, 100.0));
6491 let empty = tree.arena.len();
6492
6493 let host = tree.add_child(outer, DetachedContentHost { preserve: false });
6494 tree.layout(SizeProposal::exact(100.0, 100.0));
6495 assert!(tree.arena.len() > empty);
6496
6497 tree.destroy_subtree(host);
6498 assert_eq!(
6499 tree.arena.len(),
6500 empty,
6501 "the popup must die with the widget that built it"
6502 );
6503 }
6504
6505 #[derive(Debug)]
6507 struct StableChildHost {
6508 child: Option<WidgetId>,
6509 probe: std::rc::Rc<std::cell::Cell<Option<WidgetId>>>,
6510 }
6511 impl Widget for StableChildHost {
6512 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6513 let id = match self.child {
6514 Some(id) => id,
6515 None => {
6516 let id = ctx.add(FillWidget::new());
6517 self.child = Some(id);
6518 self.probe.set(Some(id));
6519 id
6520 }
6521 };
6522 vec![id]
6523 }
6524 fn layout_response(
6525 &self,
6526 p: SizeProposal,
6527 _c: &LayoutContext,
6528 ) -> crate::widget::LayoutResponse {
6529 p.resolve(10.0, 10.0).into()
6530 }
6531 fn preserves_children_on_rebuild(&self) -> bool {
6532 true
6533 }
6534 }
6535
6536 #[test]
6537 fn reconcile_preserves_reattached_child() {
6538 let probe = std::rc::Rc::new(std::cell::Cell::new(None));
6539 let mut tree = WidgetTree::new();
6540 let host = tree.add(StableChildHost {
6541 child: None,
6542 probe: probe.clone(),
6543 });
6544 tree.layout(SizeProposal::exact(100.0, 100.0));
6545 let child = probe.get().expect("child mounted");
6546 let total0 = tree.arena.len();
6547 for _ in 0..5 {
6548 tree.arena_mark_needs_rebuild_for_testing(host);
6549 tree.layout(SizeProposal::exact(100.0, 100.0));
6550 }
6551 assert!(
6552 tree.arena.is_active(child),
6553 "the re-attached child must survive every rebuild"
6554 );
6555 assert_eq!(tree.arena.len(), total0, "no growth — same child reused");
6556 }
6557
6558 #[derive(Debug)]
6560 struct Wrapper {
6561 child: WidgetId,
6562 }
6563 impl Widget for Wrapper {
6564 fn build(&mut self, _ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6565 vec![self.child]
6566 }
6567 fn layout_response(
6568 &self,
6569 p: SizeProposal,
6570 _c: &LayoutContext,
6571 ) -> crate::widget::LayoutResponse {
6572 p.resolve(10.0, 10.0).into()
6573 }
6574 }
6575
6576 #[derive(Debug)]
6580 struct ReparentHost {
6581 body: Option<WidgetId>,
6582 probe: std::rc::Rc<std::cell::Cell<Option<WidgetId>>>,
6583 }
6584 impl Widget for ReparentHost {
6585 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6586 let body = match self.body {
6587 Some(id) => id,
6588 None => {
6589 let id = ctx.add(FillWidget::new());
6590 self.body = Some(id);
6591 self.probe.set(Some(id));
6592 id
6593 }
6594 };
6595 vec![ctx.add(Wrapper { child: body })]
6596 }
6597 fn layout_response(
6598 &self,
6599 p: SizeProposal,
6600 _c: &LayoutContext,
6601 ) -> crate::widget::LayoutResponse {
6602 p.resolve(10.0, 10.0).into()
6603 }
6604 fn preserves_children_on_rebuild(&self) -> bool {
6605 true
6606 }
6607 }
6608
6609 #[test]
6610 fn reconcile_spares_reparented_survivor() {
6611 let probe = std::rc::Rc::new(std::cell::Cell::new(None));
6618 let mut tree = WidgetTree::new();
6619 let host = tree.add(ReparentHost {
6620 body: None,
6621 probe: probe.clone(),
6622 });
6623 tree.layout(SizeProposal::exact(100.0, 100.0));
6624 let body = probe.get().expect("body mounted");
6625 let total0 = tree.arena.len();
6626 for _ in 0..5 {
6627 tree.arena_mark_needs_rebuild_for_testing(host);
6628 tree.layout(SizeProposal::exact(100.0, 100.0));
6629 }
6630 assert!(
6631 tree.arena.is_active(body),
6632 "the re-parented body must survive — it was moved into the new tree, \
6633 not swept with the dropped wrapper"
6634 );
6635 assert_eq!(
6636 tree.arena.len(),
6637 total0,
6638 "dropped wrappers reaped — no per-rebuild leak"
6639 );
6640 }
6641
6642 #[derive(Debug)]
6652 struct BelowContainer {
6653 child: Option<WidgetId>,
6654 offset: f32,
6655 }
6656
6657 impl crate::widget::Widget for BelowContainer {
6658 fn layout_response(
6659 &self,
6660 proposal: SizeProposal,
6661 _ctx: &crate::widget::LayoutContext,
6662 ) -> crate::widget::LayoutResponse {
6663 proposal.resolve(0.0, 0.0).into()
6664 }
6665 fn place_children(
6666 &self,
6667 bounds: Rect,
6668 _proposal: SizeProposal,
6669 children: &mut [crate::widget::WidgetPlacement],
6670 _ctx: &crate::widget::LayoutContext,
6671 ) {
6672 for c in children.iter_mut() {
6673 c.origin = Point::new(bounds.x, bounds.y + self.offset);
6674 c.size = bounds.size();
6675 }
6676 }
6677 fn children(&self) -> Vec<WidgetId> {
6678 self.child.into_iter().collect()
6679 }
6680 }
6681
6682 fn recording_scroll_container(
6685 tree: &mut WidgetTree,
6686 child: WidgetId,
6687 recorded: std::rc::Rc<std::cell::Cell<Option<Rect>>>,
6688 ) -> WidgetId {
6689 use crate::test_widgets::StackWidget;
6690 tree.add(
6691 StackWidget::new()
6692 .child(child)
6693 .on_scroll(move |ev, _ctx| match ev {
6694 WidgetEvent::ScrollIntoView { target_bounds, .. } => {
6695 recorded.set(Some(*target_bounds));
6696 EventResponse::Handled
6697 }
6698 _ => EventResponse::Ignored,
6699 })
6700 .clips_children(true),
6701 )
6702 }
6703
6704 #[test]
6705 fn ensure_visible_dispatches_scroll_into_view_to_clipping_ancestor() {
6706 use std::cell::Cell;
6707 use std::rc::Rc;
6708 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6709 let mut tree = WidgetTree::new();
6710 let actor = tree.add(FillWidget::new());
6711 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
6712 tree.layout(SizeProposal::exact(100.0, 100.0));
6713
6714 let target = Rect::new(10.0, 500.0, 20.0, 15.0);
6717 let mut ctx = EventContext::new();
6718 ctx.ensure_visible(target);
6719 tree.collect_from_ctx(ctx, actor);
6720
6721 assert_eq!(
6722 recorded.get(),
6723 Some(target),
6724 "ensure_visible(rect) must dispatch ScrollIntoView with the exact rect \
6725 to the clips_children ancestor"
6726 );
6727 }
6728
6729 #[test]
6730 fn ensure_visible_is_noop_when_rect_already_visible() {
6731 use std::cell::Cell;
6732 use std::rc::Rc;
6733 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6734 let mut tree = WidgetTree::new();
6735 let actor = tree.add(FillWidget::new());
6736 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
6737 tree.layout(SizeProposal::exact(100.0, 100.0));
6738
6739 let mut ctx = EventContext::new();
6742 ctx.ensure_visible(Rect::new(10.0, 10.0, 20.0, 15.0));
6743 tree.collect_from_ctx(ctx, actor);
6744
6745 assert_eq!(
6746 recorded.get(),
6747 None,
6748 "a rect already inside the viewport must not trigger a scroll"
6749 );
6750 }
6751
6752 #[test]
6753 fn ensure_visible_margin_forces_scroll_near_edge() {
6754 use std::cell::Cell;
6755 use std::rc::Rc;
6756 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6757 let mut tree = WidgetTree::new();
6758 let actor = tree.add(FillWidget::new());
6759 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
6760 tree.layout(SizeProposal::exact(100.0, 100.0));
6761
6762 let rect = Rect::new(10.0, 95.0, 20.0, 4.0);
6765 let mut ctx = EventContext::new();
6766 ctx.ensure_visible_with_margin(rect, 10.0);
6767 tree.collect_from_ctx(ctx, actor);
6768
6769 assert_eq!(
6770 recorded.get(),
6771 Some(rect),
6772 "the margin must widen the visibility test so a near-edge rect scrolls"
6773 );
6774 }
6775
6776 fn recording_align_container(
6779 tree: &mut WidgetTree,
6780 child: WidgetId,
6781 recorded: std::rc::Rc<
6782 std::cell::Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>,
6783 >,
6784 ) -> WidgetId {
6785 use crate::test_widgets::StackWidget;
6786 tree.add(
6787 StackWidget::new()
6788 .child(child)
6789 .on_scroll(move |ev, _ctx| match ev {
6790 WidgetEvent::ScrollIntoView { align, motion, .. } => {
6791 recorded.set(Some((*align, *motion)));
6792 EventResponse::Handled
6793 }
6794 _ => EventResponse::Ignored,
6795 })
6796 .clips_children(true),
6797 )
6798 }
6799
6800 #[test]
6801 fn ensure_visible_aligned_scrolls_even_when_already_visible() {
6802 use std::cell::Cell;
6803 use std::rc::Rc;
6804 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6805 let mut tree = WidgetTree::new();
6806 let actor = tree.add(FillWidget::new());
6807 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
6808 tree.layout(SizeProposal::exact(100.0, 100.0));
6809
6810 let target = Rect::new(10.0, 10.0, 20.0, 15.0);
6815 let mut ctx = EventContext::new();
6816 ctx.ensure_visible_aligned(target, 0.5, crate::event::ScrollMotion::Instant);
6817 tree.collect_from_ctx(ctx, actor);
6818
6819 assert_eq!(
6820 recorded.get(),
6821 Some(target),
6822 "an aligned reveal must dispatch even when the rect is already visible"
6823 );
6824 }
6825
6826 #[test]
6827 fn ensure_visible_aligned_forwards_fraction_and_motion() {
6828 use std::cell::Cell;
6829 use std::rc::Rc;
6830 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6831 Rc::new(Cell::new(None));
6832 let mut tree = WidgetTree::new();
6833 let actor = tree.add(FillWidget::new());
6834 let _container = recording_align_container(&mut tree, actor, recorded.clone());
6835 tree.layout(SizeProposal::exact(100.0, 100.0));
6836
6837 let mut ctx = EventContext::new();
6838 ctx.ensure_visible_aligned(
6839 Rect::new(10.0, 10.0, 20.0, 15.0),
6840 0.25,
6841 crate::event::ScrollMotion::Smooth,
6842 );
6843 tree.collect_from_ctx(ctx, actor);
6844
6845 assert_eq!(
6846 recorded.get(),
6847 Some((
6848 crate::event::ScrollAlign::Fraction(0.25),
6849 crate::event::ScrollMotion::Smooth
6850 )),
6851 "the container must receive the requested fraction and motion verbatim"
6852 );
6853 }
6854
6855 #[test]
6856 fn ensure_visible_aligned_clamps_the_fraction() {
6857 use std::cell::Cell;
6858 use std::rc::Rc;
6859 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6860 Rc::new(Cell::new(None));
6861 let mut tree = WidgetTree::new();
6862 let actor = tree.add(FillWidget::new());
6863 let _container = recording_align_container(&mut tree, actor, recorded.clone());
6864 tree.layout(SizeProposal::exact(100.0, 100.0));
6865
6866 let mut ctx = EventContext::new();
6867 ctx.ensure_visible_aligned(
6868 Rect::new(10.0, 10.0, 20.0, 15.0),
6869 4.2,
6870 crate::event::ScrollMotion::Instant,
6871 );
6872 tree.collect_from_ctx(ctx, actor);
6873
6874 assert_eq!(
6875 recorded.get().map(|(a, _)| a),
6876 Some(crate::event::ScrollAlign::Fraction(1.0)),
6877 "an out-of-range fraction must clamp rather than aim the pin off-screen"
6878 );
6879 }
6880
6881 #[test]
6882 fn plain_ensure_visible_requests_minimal_alignment() {
6883 use std::cell::Cell;
6884 use std::rc::Rc;
6885 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6886 Rc::new(Cell::new(None));
6887 let mut tree = WidgetTree::new();
6888 let actor = tree.add(FillWidget::new());
6889 let _container = recording_align_container(&mut tree, actor, recorded.clone());
6890 tree.layout(SizeProposal::exact(100.0, 100.0));
6891
6892 let mut ctx = EventContext::new();
6893 ctx.ensure_visible(Rect::new(10.0, 500.0, 20.0, 15.0));
6894 tree.collect_from_ctx(ctx, actor);
6895
6896 assert_eq!(
6897 recorded.get(),
6898 Some((
6899 crate::event::ScrollAlign::Minimal,
6900 crate::event::ScrollMotion::Instant
6901 )),
6902 "the pre-existing reveal API must keep its exact semantics"
6903 );
6904 }
6905
6906 #[test]
6907 fn only_the_innermost_container_aligns() {
6908 use std::cell::Cell;
6909 use std::rc::Rc;
6910 let inner_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6911 Rc::new(Cell::new(None));
6912 let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6913 Rc::new(Cell::new(None));
6914
6915 let mut tree = WidgetTree::new();
6916 let actor = tree.add(FillWidget::new());
6917 let inner = recording_align_container(&mut tree, actor, inner_rec.clone());
6918 let _outer = recording_align_container(&mut tree, inner, outer_rec.clone());
6919 tree.layout(SizeProposal::exact(100.0, 100.0));
6920
6921 let mut ctx = EventContext::new();
6924 ctx.ensure_visible_aligned(
6925 Rect::new(10.0, 500.0, 20.0, 15.0),
6926 0.5,
6927 crate::event::ScrollMotion::Instant,
6928 );
6929 tree.collect_from_ctx(ctx, actor);
6930
6931 assert_eq!(
6932 inner_rec.get().map(|(a, _)| a),
6933 Some(crate::event::ScrollAlign::Fraction(0.5)),
6934 "the innermost clipping ancestor owns the pin"
6935 );
6936 assert_eq!(
6937 outer_rec.get().map(|(a, _)| a),
6938 Some(crate::event::ScrollAlign::Minimal),
6939 "an outer container must only bring the inner viewport into view — a \
6940 fraction names a height in one viewport, not in every ancestor's"
6941 );
6942 }
6943
6944 #[test]
6945 fn a_clipper_that_does_not_scroll_passes_the_pin_on() {
6946 use crate::test_widgets::StackWidget;
6953 use std::cell::Cell;
6954 use std::rc::Rc;
6955 let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6956 Rc::new(Cell::new(None));
6957
6958 let mut tree = WidgetTree::new();
6959 let actor = tree.add(FillWidget::new());
6960 let clipper = tree.add(StackWidget::new().child(actor).clips_children_on(true));
6961 let _scroller = recording_align_container(&mut tree, clipper, outer_rec.clone());
6962 tree.layout(SizeProposal::exact(100.0, 100.0));
6963
6964 let mut ctx = EventContext::new();
6967 ctx.ensure_visible_aligned(
6968 Rect::new(10.0, 10.0, 20.0, 15.0),
6969 0.5,
6970 crate::event::ScrollMotion::Instant,
6971 );
6972 tree.collect_from_ctx(ctx, actor);
6973
6974 assert_eq!(
6975 outer_rec.get().map(|(a, _)| a),
6976 Some(crate::event::ScrollAlign::Fraction(0.5)),
6977 "a clipping wrapper with no scroll handler must pass the pin through \
6978 to the scroll container above it"
6979 );
6980 }
6981
6982 #[test]
6983 fn a_scroller_that_declines_to_move_still_holds_the_pin() {
6984 use crate::test_widgets::StackWidget;
6989 use std::cell::Cell;
6990 use std::rc::Rc;
6991 let inner_rec: Rc<Cell<Option<crate::event::ScrollAlign>>> = Rc::new(Cell::new(None));
6992 let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6993 Rc::new(Cell::new(None));
6994
6995 let mut tree = WidgetTree::new();
6996 let actor = tree.add(FillWidget::new());
6997 let rec = inner_rec.clone();
6998 let inner = tree.add(
6999 StackWidget::new()
7000 .child(actor)
7001 .on_scroll(move |ev, _ctx| match ev {
7002 WidgetEvent::ScrollIntoView { align, .. } => {
7003 rec.set(Some(*align));
7004 EventResponse::Ignored
7005 }
7006 _ => EventResponse::Ignored,
7007 })
7008 .clips_children(true),
7009 );
7010 let _outer = recording_align_container(&mut tree, inner, outer_rec.clone());
7011 tree.layout(SizeProposal::exact(100.0, 100.0));
7012
7013 let mut ctx = EventContext::new();
7015 ctx.ensure_visible_aligned(
7016 Rect::new(10.0, 500.0, 20.0, 15.0),
7017 0.5,
7018 crate::event::ScrollMotion::Instant,
7019 );
7020 tree.collect_from_ctx(ctx, actor);
7021
7022 assert_eq!(
7023 inner_rec.get(),
7024 Some(crate::event::ScrollAlign::Fraction(0.5)),
7025 "the innermost scroller is offered the pin"
7026 );
7027 assert_eq!(
7028 outer_rec.get().map(|(a, _)| a),
7029 Some(crate::event::ScrollAlign::Minimal),
7030 "declining to move must not pass the pin outward"
7031 );
7032 }
7033
7034 #[test]
7035 fn ensure_widget_visible_uses_target_arena_bounds() {
7036 use std::cell::Cell;
7037 use std::rc::Rc;
7038 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
7039 let mut tree = WidgetTree::new();
7040 let target = tree.add(FillWidget::new());
7042 let rec = recorded.clone();
7043 let container = tree.add(
7044 BelowContainer {
7045 child: Some(target),
7046 offset: 500.0,
7047 }
7048 .on_scroll(move |ev, _ctx| match ev {
7049 WidgetEvent::ScrollIntoView { target_bounds, .. } => {
7050 rec.set(Some(*target_bounds));
7051 EventResponse::Handled
7052 }
7053 _ => EventResponse::Ignored,
7054 })
7055 .clips_children(true),
7056 );
7057 tree.layout(SizeProposal::exact(100.0, 100.0));
7058
7059 let expected = tree.bounds(target);
7060 assert!(
7061 expected.y > 100.0,
7062 "fixture sanity: the target must sit below the viewport (y={})",
7063 expected.y
7064 );
7065
7066 let mut ctx = EventContext::new();
7069 ctx.ensure_widget_visible(target);
7070 tree.collect_from_ctx(ctx, container);
7071
7072 assert_eq!(
7073 recorded.get(),
7074 Some(expected),
7075 "ensure_widget_visible(id) must dispatch ScrollIntoView with the \
7076 target's current arena bounds"
7077 );
7078 }
7079
7080 #[test]
7081 fn ensure_widget_visible_ignores_missing_widget() {
7082 use std::cell::Cell;
7084 use std::rc::Rc;
7085 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
7086 let mut tree = WidgetTree::new();
7087 let actor = tree.add(FillWidget::new());
7088 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
7089 tree.layout(SizeProposal::exact(100.0, 100.0));
7090
7091 let mut ctx = EventContext::new();
7092 ctx.ensure_widget_visible(WidgetId::default());
7093 tree.collect_from_ctx(ctx, actor); assert_eq!(
7096 recorded.get(),
7097 None,
7098 "an unmounted id must not trigger a scroll"
7099 );
7100 }
7101
7102 #[test]
7103 fn context_menu_inside_a_modal_keeps_the_modal() {
7104 use crate::event::{Modifiers, PointerButton, WidgetEvent};
7108 use crate::overlay::{DismissBehavior, OverlayPlacement, OverlayRequest};
7109 use crate::test_widgets::{FillWidget, StackWidget};
7110
7111 let mut tree = WidgetTree::new();
7112 let modal_content = tree.add(StackWidget::new());
7115 let _editor = tree.add_child(
7116 modal_content,
7117 FillWidget::new()
7118 .context_menu(|_pos, _ctx| Some(Box::new(FillWidget::new()) as Box<dyn Widget>)),
7119 );
7120 tree.layout(SizeProposal::exact(200.0, 100.0));
7121
7122 let modal = tree.overlay_manager.show(OverlayRequest::new(
7123 modal_content,
7124 modal_content,
7125 OverlayPlacement::Centered,
7126 DismissBehavior::EscapeKey,
7127 ));
7128 tree.overlay_manager
7130 .stack
7131 .iter_mut()
7132 .find(|o| o.id == modal)
7133 .unwrap()
7134 .bounds = Rect::new(0.0, 0.0, 200.0, 100.0);
7135 assert_eq!(tree.overlay_manager.len(), 1);
7136
7137 tree.dispatch_event(WidgetEvent::pointer_down(
7139 Point::new(50.0, 25.0),
7140 PointerButton::Secondary,
7141 Modifiers::NONE,
7142 ));
7143
7144 assert!(
7145 tree.overlay_manager.active_ids().contains(&modal),
7146 "the modal must survive opening a context menu inside it"
7147 );
7148 assert_eq!(
7149 tree.overlay_manager.len(),
7150 2,
7151 "the context menu should now be open on top of the surviving modal"
7152 );
7153 }
7154}
7155
7156#[cfg(test)]
7159mod input_ingress_tests {
7160 use super::*;
7161 use crate::event::{EventResponse, Modifiers, PointerButton, ScrollDelta};
7162 use crate::pointer::{
7163 EventTime, PointerId, PointerInfo, PointerPhase, PointerSample, ScrollPhase, ScrollSample,
7164 ScrollSource,
7165 };
7166 use crate::test_widgets::FillWidget;
7167 use crate::widget::{LayoutContext, WidgetPlacement};
7168 use crate::widget_builder::WidgetBuilder;
7169 use std::cell::RefCell;
7170 use std::rc::Rc;
7171
7172 #[derive(Debug)]
7179 struct RowWidget {
7180 children: Vec<WidgetId>,
7181 }
7182
7183 impl crate::widget::Widget for RowWidget {
7184 fn layout_response(
7185 &self,
7186 proposal: SizeProposal,
7187 _ctx: &LayoutContext,
7188 ) -> crate::widget::LayoutResponse {
7189 proposal.resolve(0.0, 0.0).into()
7190 }
7191
7192 fn place_children(
7193 &self,
7194 bounds: Rect,
7195 _proposal: SizeProposal,
7196 children: &mut [WidgetPlacement],
7197 _ctx: &LayoutContext,
7198 ) {
7199 let n = children.len().max(1) as f32;
7200 let w = bounds.width / n;
7201 for (i, child) in children.iter_mut().enumerate() {
7202 child.origin = Point::new(bounds.x + w * i as f32, bounds.y);
7203 child.size = teksilo_canvas::Size::new(w, bounds.height);
7204 }
7205 }
7206
7207 fn children(&self) -> Vec<WidgetId> {
7208 self.children.clone()
7209 }
7210 }
7211
7212 fn record(drive: impl FnOnce(&mut WidgetTree)) -> Vec<String> {
7215 let log: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(Vec::new()));
7216
7217 let mut tree = WidgetTree::new();
7218 let pointer_log = log.clone();
7219 let hover_log = log.clone();
7220 tree.add(
7221 FillWidget::new()
7222 .on_pointer_event(move |event, _ctx| {
7223 pointer_log.borrow_mut().push(format!("{event:?}"));
7224 EventResponse::Ignored
7225 })
7226 .on_hover(move |entered, _ctx| {
7227 hover_log.borrow_mut().push(format!("hover({entered})"));
7228 }),
7229 );
7230 tree.layout(SizeProposal::exact(100.0, 100.0));
7231
7232 drive(&mut tree);
7233 log.borrow().clone()
7234 }
7235
7236 #[test]
7242 fn a_mouse_sample_reproduces_todays_event_stream() {
7243 let inside = Point::new(40.0, 40.0);
7244 let moved = Point::new(60.0, 55.0);
7245 let outside = Point::new(400.0, 400.0);
7246
7247 let legacy = record(|tree| {
7248 tree.dispatch_event(WidgetEvent::pointer_move(inside));
7249 tree.dispatch_event(WidgetEvent::pointer_down(
7250 inside,
7251 PointerButton::Primary,
7252 Modifiers::NONE,
7253 ));
7254 tree.dispatch_event(WidgetEvent::pointer_move(moved));
7255 tree.dispatch_event(WidgetEvent::pointer_up(
7256 moved,
7257 PointerButton::Primary,
7258 Modifiers::NONE,
7259 ));
7260 tree.dispatch_event(WidgetEvent::pointer_move(outside));
7261 });
7262
7263 let sampled = record(|tree| {
7264 let t = EventTime::ZERO;
7265 tree.dispatch_pointer(PointerSample::mouse(PointerPhase::Move, inside, t));
7266 tree.dispatch_pointer(
7267 PointerSample::mouse(PointerPhase::Down, inside, t)
7268 .with_button(PointerButton::Primary),
7269 );
7270 tree.dispatch_pointer(PointerSample::mouse(PointerPhase::Move, moved, t));
7271 tree.dispatch_pointer(
7272 PointerSample::mouse(PointerPhase::Up, moved, t)
7273 .with_button(PointerButton::Primary),
7274 );
7275 tree.dispatch_pointer(PointerSample::mouse(PointerPhase::Move, outside, t));
7276 });
7277
7278 assert_eq!(legacy, sampled);
7279 assert!(
7280 legacy.contains(&"hover(true)".to_string())
7281 && legacy.contains(&"hover(false)".to_string()),
7282 "the fixture must actually exercise enter and leave: {legacy:?}"
7283 );
7284 }
7285
7286 #[test]
7290 fn a_buttonless_press_lowers_to_primary() {
7291 let events = record(|tree| {
7292 tree.dispatch_pointer(PointerSample::mouse(
7293 PointerPhase::Down,
7294 Point::new(20.0, 20.0),
7295 EventTime::ZERO,
7296 ));
7297 });
7298 assert!(
7299 events.iter().any(|e| e.contains("button: Primary")),
7300 "{events:?}"
7301 );
7302 }
7303
7304 #[test]
7310 fn a_sampled_press_and_release_form_a_tap() {
7311 let taps = Rc::new(std::cell::Cell::new(0));
7312 let seen = taps.clone();
7313 let mut tree = WidgetTree::new();
7314 tree.add(FillWidget::new().on_tap(move |_event, _ctx| seen.set(seen.get() + 1)));
7315 tree.layout(SizeProposal::exact(100.0, 100.0));
7316
7317 let at = Point::new(50.0, 50.0);
7318 tree.dispatch_pointer(PointerSample::mouse(
7319 PointerPhase::Down,
7320 at,
7321 EventTime::from_millis(0),
7322 ));
7323 tree.dispatch_pointer(PointerSample::mouse(
7324 PointerPhase::Up,
7325 at,
7326 EventTime::from_millis(5),
7327 ));
7328
7329 assert_eq!(taps.get(), 1);
7330 }
7331
7332 fn scroll_fixture() -> (WidgetTree, Rc<RefCell<Vec<&'static str>>>) {
7337 let hits: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
7338 let mut tree = WidgetTree::new();
7339
7340 let leading_hits = hits.clone();
7341 let trailing_hits = hits.clone();
7342 let leading = tree.add(FillWidget::new().on_scroll(move |_event, _ctx| {
7343 leading_hits.borrow_mut().push("leading");
7344 EventResponse::Handled
7345 }));
7346 let trailing = tree.add(FillWidget::new().on_scroll(move |_event, _ctx| {
7347 trailing_hits.borrow_mut().push("trailing");
7348 EventResponse::Handled
7349 }));
7350 tree.add(RowWidget {
7351 children: vec![leading, trailing],
7352 });
7353 tree.layout(SizeProposal::exact(200.0, 100.0));
7354 (tree, hits)
7355 }
7356
7357 fn notch() -> ScrollDelta {
7358 ScrollDelta::Lines { x: 0.0, y: -1.0 }
7359 }
7360
7361 #[test]
7364 fn a_positionless_scroll_routes_by_hover() {
7365 let (mut tree, hits) = scroll_fixture();
7366
7367 tree.pointer_move(Point::new(150.0, 50.0)); tree.dispatch_event(WidgetEvent::scroll(notch(), Modifiers::NONE));
7369 assert_eq!(*hits.borrow(), vec!["trailing"]);
7370
7371 tree.pointer_move(Point::new(50.0, 50.0)); tree.dispatch_scroll(ScrollSample::wheel(
7373 notch(),
7374 Modifiers::NONE,
7375 EventTime::ZERO,
7376 ));
7377 assert_eq!(*hits.borrow(), vec!["trailing", "leading"]);
7378 }
7379
7380 #[test]
7385 fn a_positioned_scroll_routes_by_hit_test() {
7386 let (mut tree, hits) = scroll_fixture();
7387 tree.pointer_move(Point::new(150.0, 50.0)); tree.dispatch_event(WidgetEvent::scroll_at(
7390 notch(),
7391 Modifiers::NONE,
7392 Point::new(50.0, 50.0), ));
7394 assert_eq!(*hits.borrow(), vec!["leading"]);
7395
7396 tree.dispatch_scroll(
7397 ScrollSample::wheel(notch(), Modifiers::NONE, EventTime::ZERO)
7398 .at(Point::new(150.0, 50.0)),
7399 );
7400 assert_eq!(*hits.borrow(), vec!["leading", "trailing"]);
7401 }
7402
7403 #[test]
7407 fn a_positionless_scroll_with_no_hover_goes_nowhere() {
7408 let (mut tree, hits) = scroll_fixture();
7409 tree.dispatch_event(WidgetEvent::scroll(notch(), Modifiers::NONE));
7410 assert!(hits.borrow().is_empty());
7411 }
7412
7413 #[test]
7418 fn a_handler_sees_the_sample_it_is_serving() {
7419 let seen: Rc<RefCell<Vec<(ScrollPhase, ScrollSource, Option<Point>)>>> =
7420 Rc::new(RefCell::new(Vec::new()));
7421 let sink = seen.clone();
7422
7423 let mut tree = WidgetTree::new();
7424 tree.add(
7431 FillWidget::new()
7432 .scroll_container(crate::pointer::touch_action::PanAxes::BOTH)
7433 .on_scroll(move |_event, ctx| {
7434 sink.borrow_mut().push((
7435 ctx.scroll_phase(),
7436 ctx.scroll_source(),
7437 ctx.pointer_position(),
7438 ));
7439 EventResponse::Handled
7440 }),
7441 );
7442 tree.layout(SizeProposal::exact(100.0, 100.0));
7443
7444 tree.dispatch_scroll(ScrollSample {
7445 delta: notch(),
7446 position: Some(Point::new(50.0, 50.0)),
7447 phase: ScrollPhase::Momentum,
7448 source: ScrollSource::TouchPan,
7449 pointer: PointerInfo::mouse(EventTime::from_millis(12)),
7450 modifiers: Modifiers::NONE,
7451 });
7452
7453 assert_eq!(
7454 *seen.borrow(),
7455 vec![(
7456 ScrollPhase::Momentum,
7457 ScrollSource::TouchPan,
7458 Some(Point::new(50.0, 50.0))
7459 )]
7460 );
7461 }
7462
7463 #[test]
7466 fn a_legacy_event_reports_the_mouse_at_the_epoch() {
7467 let seen: Rc<RefCell<Option<(PointerId, ScrollPhase)>>> = Rc::new(RefCell::new(None));
7468 let sink = seen.clone();
7469
7470 let mut tree = WidgetTree::new();
7471 tree.add(FillWidget::new().on_pointer_event(move |_event, ctx| {
7472 *sink.borrow_mut() = Some((ctx.pointer().id, ctx.scroll_phase()));
7473 EventResponse::Ignored
7474 }));
7475 tree.layout(SizeProposal::exact(100.0, 100.0));
7476 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(50.0, 50.0)));
7477
7478 assert_eq!(
7479 *seen.borrow(),
7480 Some((PointerId::MOUSE, ScrollPhase::Discrete))
7481 );
7482 }
7483
7484 #[test]
7488 fn the_snapshot_is_restored_after_a_dispatch() {
7489 let mut tree = WidgetTree::new();
7490 tree.add(FillWidget::new());
7491 tree.layout(SizeProposal::exact(100.0, 100.0));
7492
7493 let before = tree.current_input.clone();
7494 tree.dispatch_scroll(
7495 ScrollSample::wheel(notch(), Modifiers::NONE, EventTime::ZERO)
7496 .at(Point::new(10.0, 10.0)),
7497 );
7498 assert_eq!(tree.current_input, before);
7499 }
7500}
7501
7502#[cfg(test)]
7503mod press_and_focus_tests {
7504 use std::cell::{Cell, RefCell};
7509 use std::rc::Rc;
7510 use teksilo_canvas::{Point, SizeProposal};
7511
7512 use crate::WidgetId;
7513 use crate::event::{EventResponse, Key, Modifiers, PointerButton, WidgetEvent};
7514 use crate::focus::FocusOrigin;
7515 use crate::pointer::clock::ManualClock;
7516 use crate::pointer::touch_action::PanClaim;
7517 use crate::pointer::{
7518 BackendDeviceKey, EventTime, PointerId, PointerIdAllocator, PointerInfo, PointerPhase,
7519 PointerSample,
7520 };
7521 use crate::test_widgets::{FillWidget, StackWidget};
7522 use crate::widget_builder::WidgetBuilder;
7523 use crate::widget_tree::WidgetTree;
7524
7525 fn contact_id() -> PointerId {
7531 use std::sync::atomic::{AtomicU64, Ordering};
7532 static NEXT: AtomicU64 = AtomicU64::new(1);
7533 PointerIdAllocator::global().begin(
7534 BackendDeviceKey::new(0x0B11),
7535 NEXT.fetch_add(1, Ordering::Relaxed),
7536 )
7537 }
7538
7539 fn contact(id: PointerId, phase: PointerPhase, at: Point, t: EventTime) -> PointerSample {
7540 PointerSample {
7541 pointer: PointerInfo::touch(id, t),
7542 phase,
7543 position: at,
7544 button: None,
7545 modifiers: Modifiers::NONE,
7546 coalesced: Vec::new(),
7547 }
7548 }
7549
7550 fn tree_on_a_clock() -> (WidgetTree, Rc<ManualClock>) {
7553 let mut tree = WidgetTree::new();
7554 let clock = Rc::new(ManualClock::new(EventTime::ZERO));
7555 tree.set_input_clock(clock.clone());
7556 (tree, clock)
7557 }
7558
7559 fn tappable(tree: &mut WidgetTree) -> (WidgetId, crate::signal::Signal<bool>) {
7566 let id = tree.add(FillWidget::new().focusable().on_tap(|_e, _c| {}));
7567 let signal = tree.pressed_signal(id);
7568 (id, signal)
7569 }
7570
7571 #[test]
7578 fn a_pointer_origin_names_its_device() {
7579 use teksilo_tokens::{PenKind, PointerKind};
7580
7581 assert!(FocusOrigin::Pointer(PointerKind::Touch).is_pointer());
7582 assert!(FocusOrigin::Pointer(PointerKind::Pen(PenKind::Pen)).is_pointer());
7583 assert!(FocusOrigin::POINTER.is_pointer());
7584 assert!(!FocusOrigin::Keyboard.is_pointer());
7585 assert!(!FocusOrigin::Programmatic.is_pointer());
7586 assert!(!FocusOrigin::Accessibility.is_pointer());
7587
7588 assert_eq!(
7589 FocusOrigin::Pointer(PointerKind::Touch).pointer_kind(),
7590 Some(PointerKind::Touch),
7591 );
7592 assert_eq!(FocusOrigin::Keyboard.pointer_kind(), None);
7593 assert_eq!(
7594 FocusOrigin::POINTER.pointer_kind(),
7595 Some(PointerKind::Unknown),
7596 "a widget deriving its own origin says so rather than naming a device it never saw",
7597 );
7598 }
7599
7600 #[test]
7604 fn only_a_real_navigation_declares_a_modality() {
7605 use teksilo_tokens::PointerKind;
7606
7607 assert_eq!(FocusOrigin::Keyboard.focus_visible(), Some(true));
7608 assert_eq!(FocusOrigin::Accessibility.focus_visible(), Some(true));
7609 assert_eq!(
7610 FocusOrigin::Pointer(PointerKind::Touch).focus_visible(),
7611 Some(false),
7612 );
7613 assert_eq!(FocusOrigin::Programmatic.focus_visible(), None);
7614 }
7615
7616 #[test]
7619 fn the_tier_three_configs_are_unchanged() {
7620 let origin: crate::signal::Signal<Option<FocusOrigin>> =
7621 crate::signal::Signal::new(Some(FocusOrigin::Keyboard));
7622 let slider: crate::signal::Signal<Option<FocusOrigin>> = origin.clone();
7623 let splitter: crate::signal::Signal<Option<FocusOrigin>> = origin.clone();
7624 let segmented: crate::signal::Signal<Option<FocusOrigin>> = origin.clone();
7625 for field in [slider, splitter, segmented] {
7626 assert_eq!(field.get(), Some(FocusOrigin::Keyboard));
7627 }
7628 origin.set(Some(FocusOrigin::POINTER));
7629 assert_ne!(
7630 origin.get(),
7631 Some(FocusOrigin::Keyboard),
7632 "the `== Some(Keyboard)` test every consumer makes still discriminates",
7633 );
7634 }
7635
7636 #[test]
7647 fn a_touch_tap_after_keyboard_focus_leaves_no_ring() {
7648 let (mut tree, _clock) = tree_on_a_clock();
7649 let a = tree.add(FillWidget::new().focusable());
7650 let b = tree.add(FillWidget::new().focusable());
7651 let root = tree.add(SideBySide {
7652 children: vec![a, b],
7653 });
7654 tree.layout(SizeProposal::exact(200.0, 100.0));
7655 let _ = root;
7656
7657 let visible = tree.focus_visible_signal();
7658 tree.press_key(Key::Tab, Modifiers::NONE);
7659 assert_eq!(tree.focused(), Some(a));
7660 assert!(visible.get(), "keyboard navigation reveals the ring");
7661
7662 let id = contact_id();
7663 let at = tree.bounds(b).center();
7664 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
7665 tree.dispatch_pointer(contact(
7666 id,
7667 PointerPhase::Up,
7668 at,
7669 EventTime::from_millis(30),
7670 ));
7671
7672 assert_eq!(tree.focused(), Some(b), "the release moved focus");
7673 assert!(!visible.get(), "and the ring did not come with it");
7674 assert_eq!(
7675 tree.focus_origin(),
7676 Some(FocusOrigin::Pointer(teksilo_tokens::PointerKind::Touch)),
7677 );
7678 }
7679
7680 #[test]
7685 fn an_assistive_focus_reveals_the_ring() {
7686 let mut tree = WidgetTree::new();
7687 let a = tree.add(FillWidget::new().focusable());
7688 let b = tree.add(FillWidget::new().focusable());
7689 let root = tree.add(SideBySide {
7690 children: vec![a, b],
7691 });
7692 tree.layout(SizeProposal::exact(200.0, 50.0));
7693 let _ = root;
7694 let visible = tree.focus_visible_signal();
7695
7696 tree.click(a);
7697 assert_eq!(tree.focused(), Some(a));
7698 assert!(!visible.get(), "the click hid it");
7699
7700 tree.dispatch_event(WidgetEvent::AccessAction {
7701 target: Some(b),
7702 action: accesskit::Action::Focus,
7703 target_node: crate::accessibility::widget_id_to_node_id(b),
7704 data: None,
7705 });
7706 assert_eq!(tree.focused(), Some(b));
7707 assert!(visible.get(), "an assistive move is a navigation");
7708 assert_eq!(tree.focus_origin(), Some(FocusOrigin::Accessibility));
7709 }
7710
7711 #[test]
7716 fn a_programmatic_focus_leaves_the_modality_alone() {
7717 let mut tree = WidgetTree::new();
7718 let a = tree.add(FillWidget::new().focusable());
7719 let b = tree.add(FillWidget::new().focusable());
7720 let root = tree.add(SideBySide {
7721 children: vec![a, b],
7722 });
7723 tree.layout(SizeProposal::exact(200.0, 100.0));
7724 let _ = root;
7725 let visible = tree.focus_visible_signal();
7726
7727 tree.press_key(Key::Tab, Modifiers::NONE);
7728 assert_eq!(tree.focused(), Some(a));
7729 assert!(visible.get());
7730 tree.focus(b);
7731 assert!(
7732 visible.get(),
7733 "a scripted focus does not hide a keyboard ring"
7734 );
7735
7736 tree.click(a);
7737 assert!(!visible.get());
7738 tree.focus(b);
7739 assert!(
7740 !visible.get(),
7741 "and does not reveal one after a click either",
7742 );
7743 }
7744
7745 #[test]
7751 fn a_mouse_still_focuses_on_press() {
7752 let mut tree = WidgetTree::new();
7753 let w = tree.add(FillWidget::new().focusable());
7754 tree.layout(SizeProposal::exact(100.0, 50.0));
7755 let center = tree.bounds(w).center();
7756
7757 tree.dispatch_event(WidgetEvent::pointer_down(
7758 center,
7759 PointerButton::Primary,
7760 Modifiers::NONE,
7761 ));
7762 assert_eq!(
7763 tree.focused(),
7764 Some(w),
7765 "focus lands on the press for an indirect pointer",
7766 );
7767 }
7768
7769 #[test]
7771 fn a_finger_focuses_on_release() {
7772 let (mut tree, _clock) = tree_on_a_clock();
7773 let w = tree.add(FillWidget::new().focusable());
7774 tree.layout(SizeProposal::exact(100.0, 50.0));
7775 let center = tree.bounds(w).center();
7776
7777 let id = contact_id();
7778 tree.dispatch_pointer(contact(id, PointerPhase::Down, center, EventTime::ZERO));
7779 assert_eq!(
7780 tree.focused(),
7781 None,
7782 "a finger that has only landed has chosen nothing yet",
7783 );
7784
7785 tree.dispatch_pointer(contact(
7786 id,
7787 PointerPhase::Up,
7788 center,
7789 EventTime::from_millis(40),
7790 ));
7791 assert_eq!(tree.focused(), Some(w));
7792 assert_eq!(
7793 tree.focus_origin().and_then(FocusOrigin::pointer_kind),
7794 Some(teksilo_tokens::PointerKind::Touch),
7795 "the origin names the device that delivered it",
7796 );
7797 }
7798
7799 #[test]
7803 fn a_release_on_a_different_focusable_moves_no_focus() {
7804 let (mut tree, _clock) = tree_on_a_clock();
7805 let a = tree.add(FillWidget::new().focusable());
7806 let b = tree.add(FillWidget::new().focusable());
7807 let root = tree.add(SideBySide {
7808 children: vec![a, b],
7809 });
7810 tree.layout(SizeProposal::exact(200.0, 50.0));
7811 let _ = root;
7812
7813 let on_a = tree.bounds(a).center();
7814 let on_b = tree.bounds(b).center();
7815 let id = contact_id();
7816 tree.dispatch_pointer(contact(id, PointerPhase::Down, on_a, EventTime::ZERO));
7817 tree.dispatch_pointer(contact(
7818 id,
7819 PointerPhase::Move,
7820 on_b,
7821 EventTime::from_millis(20),
7822 ));
7823 tree.dispatch_pointer(contact(
7824 id,
7825 PointerPhase::Up,
7826 on_b,
7827 EventTime::from_millis(40),
7828 ));
7829
7830 assert_eq!(
7831 tree.focused(),
7832 None,
7833 "the guard is `the release landed on the same focusable as the press`",
7834 );
7835 }
7836
7837 #[test]
7844 fn a_mouse_press_visual_is_unchanged() {
7845 let mut tree = WidgetTree::new();
7846 let (w, pressed) = tappable(&mut tree);
7847 tree.layout(SizeProposal::exact(100.0, 50.0));
7848 let center = tree.bounds(w).center();
7849
7850 assert!(!pressed.get(), "not pressed at rest");
7851 tree.dispatch_event(WidgetEvent::pointer_down(
7852 center,
7853 PointerButton::Primary,
7854 Modifiers::NONE,
7855 ));
7856 assert!(pressed.get(), "an indirect pointer lights up on the press");
7857 assert!(
7858 !tree.press_pending(w),
7859 "and never waits: a mouse opens no pan session, so there is no \
7860 ambiguity to wait out",
7861 );
7862 assert_eq!(tree.pressed_by(w), Some(PointerId::MOUSE));
7863
7864 tree.dispatch_event(WidgetEvent::pointer_up(
7865 center,
7866 PointerButton::Primary,
7867 Modifiers::NONE,
7868 ));
7869 assert!(!pressed.get(), "the release clears it");
7870 assert_eq!(tree.pressed_by(w), None);
7871 }
7872
7873 #[test]
7877 fn a_slide_off_clears_the_visual_and_re_entry_restores_it() {
7878 let (mut tree, _clock) = tree_on_a_clock();
7879 let (w, pressed) = tappable(&mut tree);
7880 tree.layout(SizeProposal::exact(100.0, 200.0));
7881 let inside = tree.bounds(w).center();
7882 let outside = Point::new(inside.x, inside.y + 400.0);
7883
7884 let id = contact_id();
7885 tree.dispatch_pointer(contact(id, PointerPhase::Down, inside, EventTime::ZERO));
7886 assert!(
7887 pressed.get(),
7888 "nothing here claims a pan, so no delay applies"
7889 );
7890
7891 tree.dispatch_pointer(contact(
7892 id,
7893 PointerPhase::Move,
7894 outside,
7895 EventTime::from_millis(20),
7896 ));
7897 assert!(!pressed.get(), "the press has left its target");
7898 assert!(!tree.press_is_inside(w));
7899 assert_eq!(
7900 tree.pressed_by(w),
7901 Some(id),
7902 "the contact still holds the press — it is the *visual* that is off",
7903 );
7904
7905 tree.dispatch_pointer(contact(
7906 id,
7907 PointerPhase::Move,
7908 inside,
7909 EventTime::from_millis(40),
7910 ));
7911 assert!(pressed.get(), "sliding back on restores it");
7912
7913 tree.dispatch_pointer(contact(
7914 id,
7915 PointerPhase::Up,
7916 inside,
7917 EventTime::from_millis(60),
7918 ));
7919 assert!(!pressed.get());
7920 }
7921
7922 #[test]
7925 fn a_second_contact_cannot_clear_the_first_visual() {
7926 let (mut tree, _clock) = tree_on_a_clock();
7927 let (w, pressed) = tappable(&mut tree);
7928 tree.layout(SizeProposal::exact(100.0, 50.0));
7929 let bounds = tree.bounds(w);
7930 let first_at = Point::new(bounds.x + 20.0, bounds.y + 25.0);
7931 let second_at = Point::new(bounds.x + 70.0, bounds.y + 25.0);
7932
7933 let first = contact_id();
7934 let second = contact_id();
7935 tree.dispatch_pointer(contact(
7936 first,
7937 PointerPhase::Down,
7938 first_at,
7939 EventTime::ZERO,
7940 ));
7941 assert!(pressed.get());
7942 assert_eq!(tree.pressed_by(w), Some(first));
7943
7944 tree.dispatch_pointer(contact(
7945 second,
7946 PointerPhase::Down,
7947 second_at,
7948 EventTime::from_millis(10),
7949 ));
7950 assert_eq!(
7951 tree.pressed_by(w),
7952 Some(first),
7953 "under `MultiContact::First` the second contact is terminated before \
7954 it reaches the arena at all",
7955 );
7956
7957 tree.dispatch_pointer(contact(
7958 second,
7959 PointerPhase::Up,
7960 second_at,
7961 EventTime::from_millis(20),
7962 ));
7963 assert!(
7964 pressed.get(),
7965 "so the second contact's release cannot clear a visual it never owned",
7966 );
7967 assert_eq!(tree.pressed_by(w), Some(first));
7968
7969 tree.dispatch_pointer(contact(
7970 first,
7971 PointerPhase::Up,
7972 first_at,
7973 EventTime::from_millis(30),
7974 ));
7975 assert!(!pressed.get(), "the owner's release does clear it");
7976 }
7977
7978 #[test]
7981 fn a_cancel_clears_the_visual() {
7982 let (mut tree, _clock) = tree_on_a_clock();
7983 let (w, pressed) = tappable(&mut tree);
7984 tree.layout(SizeProposal::exact(100.0, 50.0));
7985 let center = tree.bounds(w).center();
7986
7987 let id = contact_id();
7988 tree.dispatch_pointer(contact(id, PointerPhase::Down, center, EventTime::ZERO));
7989 assert!(pressed.get());
7990
7991 tree.dispatch_pointer(contact(
7992 id,
7993 PointerPhase::Cancel,
7994 center,
7995 EventTime::from_millis(20),
7996 ));
7997 assert!(!pressed.get(), "a press that was taken away is not painted");
7998 assert_eq!(tree.pressed_by(w), None);
7999 }
8000
8001 #[test]
8004 fn a_pan_claim_clears_the_visual() {
8005 let (mut tree, clock) = tree_on_a_clock();
8006 let (row, pressed) = tappable(&mut tree);
8007 let list = tree.add(
8008 StackWidget::new()
8009 .child(row)
8010 .pan_claim(PanClaim::vertical())
8011 .on_scroll(|_e, _c| EventResponse::Handled),
8012 );
8013 tree.layout(SizeProposal::exact(200.0, 400.0));
8014 let _ = list;
8015 let start = Point::new(100.0, 200.0);
8016
8017 let id = contact_id();
8018 tree.dispatch_pointer(contact(id, PointerPhase::Down, start, EventTime::ZERO));
8019 assert!(tree.press_pending(row));
8021 clock.set(EventTime::from_millis(100));
8022 tree.tick_gestures(std::time::Instant::now());
8023 assert!(pressed.get(), "…and appears once the delay has elapsed");
8024
8025 tree.dispatch_pointer(contact(
8027 id,
8028 PointerPhase::Move,
8029 Point::new(100.0, 260.0),
8030 EventTime::from_millis(120),
8031 ));
8032 assert!(
8033 !pressed.get(),
8034 "the row lost the press to the list and must stop advertising it",
8035 );
8036 assert_eq!(tree.pressed_by(row), None);
8037 }
8038
8039 #[test]
8048 fn a_button_the_control_cannot_act_on_raises_no_visual() {
8049 let taps = Rc::new(Cell::new(0u32));
8050 let count = taps.clone();
8051 let mut tree = WidgetTree::new();
8052 let w = tree.add(FillWidget::new().focusable().on_tap(move |_e, _c| {
8053 count.set(count.get() + 1);
8054 }));
8055 let pressed = tree.pressed_signal(w);
8056 tree.layout(SizeProposal::exact(100.0, 50.0));
8057 let center = tree.bounds(w).center();
8058
8059 for button in [
8060 PointerButton::Middle,
8061 PointerButton::Back,
8062 PointerButton::Forward,
8063 ] {
8064 tree.dispatch_event(WidgetEvent::pointer_down(center, button, Modifiers::NONE));
8065 assert!(
8066 !pressed.get(),
8067 "{button:?} cannot activate an `on_tap` node, so it must not light one up",
8068 );
8069 assert_eq!(tree.pressed_by(w), None, "and owns no visual to clear");
8070 tree.dispatch_event(WidgetEvent::pointer_up(center, button, Modifiers::NONE));
8071 assert_eq!(
8072 taps.get(),
8073 0,
8074 "the tap recognizer refuses {button:?} too — that is the point",
8075 );
8076 }
8077
8078 tree.dispatch_event(WidgetEvent::pointer_down(
8080 center,
8081 PointerButton::Primary,
8082 Modifiers::NONE,
8083 ));
8084 assert!(pressed.get(), "a primary press lights up as it always has");
8085 assert_eq!(tree.pressed_by(w), Some(PointerId::MOUSE));
8086 tree.dispatch_event(WidgetEvent::pointer_up(
8087 center,
8088 PointerButton::Primary,
8089 Modifiers::NONE,
8090 ));
8091 assert!(!pressed.get());
8092 assert_eq!(taps.get(), 1);
8093 }
8094
8095 #[test]
8099 fn a_secondary_press_with_no_context_menu_raises_no_visual() {
8100 let mut tree = WidgetTree::new();
8101 let (w, pressed) = tappable(&mut tree);
8102 tree.layout(SizeProposal::exact(100.0, 50.0));
8103 let center = tree.bounds(w).center();
8104
8105 tree.dispatch_event(WidgetEvent::pointer_down(
8106 center,
8107 PointerButton::Secondary,
8108 Modifiers::NONE,
8109 ));
8110 assert!(
8111 !pressed.get(),
8112 "nothing opened, and nothing may look pressed either",
8113 );
8114 assert_eq!(tree.pressed_by(w), None);
8115 assert!(!tree.press_is_inside(w));
8116 }
8117
8118 #[test]
8122 fn a_widened_mask_widens_the_visual_with_it() {
8123 use crate::event::ButtonMask;
8124
8125 let mut tree = WidgetTree::new();
8126 let w = tree.add(
8127 FillWidget::new()
8128 .on_tap(|_e, _c| {})
8129 .accept_tap_buttons(ButtonMask::PRIMARY | ButtonMask::MIDDLE),
8130 );
8131 let pressed = tree.pressed_signal(w);
8132 tree.layout(SizeProposal::exact(100.0, 50.0));
8133 let center = tree.bounds(w).center();
8134
8135 tree.dispatch_event(WidgetEvent::pointer_down(
8136 center,
8137 PointerButton::Middle,
8138 Modifiers::NONE,
8139 ));
8140 assert!(pressed.get(), "this node really does act on a middle-click");
8141
8142 tree.dispatch_event(WidgetEvent::pointer_up(
8143 center,
8144 PointerButton::Middle,
8145 Modifiers::NONE,
8146 ));
8147 assert!(!pressed.get());
8148 }
8149
8150 #[test]
8156 fn a_press_that_raises_no_visual_still_defers_its_focus() {
8157 use teksilo_tokens::{PenKind, PointerKind};
8158
8159 let (mut tree, _clock) = tree_on_a_clock();
8160 let (w, pressed) = tappable(&mut tree);
8161 tree.layout(SizeProposal::exact(100.0, 50.0));
8162 let center = tree.bounds(w).center();
8163
8164 let id = contact_id();
8165 let barrel = |phase, t| PointerSample {
8166 pointer: PointerInfo {
8167 kind: PointerKind::Pen(PenKind::Pen),
8168 ..PointerInfo::touch(id, t)
8169 },
8170 phase,
8171 position: center,
8172 button: Some(PointerButton::Secondary),
8173 modifiers: Modifiers::NONE,
8174 coalesced: Vec::new(),
8175 };
8176
8177 tree.dispatch_pointer(barrel(PointerPhase::Down, EventTime::ZERO));
8178 assert!(!pressed.get(), "the barrel button activates nothing here");
8179 assert_eq!(tree.pressed_by(w), None);
8180 assert_eq!(
8181 tree.focused(),
8182 None,
8183 "a direct pointer has chosen nothing until it lifts",
8184 );
8185
8186 tree.dispatch_pointer(barrel(PointerPhase::Up, EventTime::from_millis(40)));
8187 assert_eq!(
8188 tree.focused(),
8189 Some(w),
8190 "the deferral is not button-gated: the release landed where the press did",
8191 );
8192 assert_eq!(
8193 tree.focus_origin().and_then(FocusOrigin::pointer_kind),
8194 Some(PointerKind::Pen(PenKind::Pen)),
8195 );
8196 }
8197
8198 #[test]
8205 fn the_feedback_delay_applies_only_inside_a_claimant() {
8206 let (mut tree, _clock) = tree_on_a_clock();
8208 let (w, pressed) = tappable(&mut tree);
8209 tree.layout(SizeProposal::exact(200.0, 400.0));
8210 let id = contact_id();
8211 let at = tree.bounds(w).center();
8212 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8213 assert!(
8214 !tree.press_pending(w),
8215 "no claimant above it, so nothing to rule out",
8216 );
8217 assert!(pressed.get());
8218
8219 let (mut tree, clock) = tree_on_a_clock();
8221 let (row, pressed) = tappable(&mut tree);
8222 let list = tree.add(
8223 StackWidget::new()
8224 .child(row)
8225 .pan_claim(PanClaim::vertical())
8226 .on_scroll(|_e, _c| EventResponse::Handled),
8227 );
8228 tree.layout(SizeProposal::exact(200.0, 400.0));
8229 let _ = list;
8230 let id = contact_id();
8231 let at = Point::new(100.0, 200.0);
8232 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8233 assert!(tree.press_pending(row), "a finger might be about to scroll");
8234 assert!(!pressed.get(), "so the row does not flash");
8235 assert!(
8236 tree.press_is_inside(row),
8237 "the press is real; only its visual is being withheld",
8238 );
8239
8240 clock.set(EventTime::from_millis(99));
8241 tree.tick_gestures(std::time::Instant::now());
8242 assert!(!pressed.get(), "99 ms is under the 100 ms delay");
8243
8244 clock.set(EventTime::from_millis(100));
8245 tree.tick_gestures(std::time::Instant::now());
8246 assert!(pressed.get(), "and 100 ms is it");
8247 assert!(!tree.press_pending(row));
8248 }
8249
8250 #[test]
8253 fn a_mouse_inside_a_claimant_never_waits() {
8254 let mut tree = WidgetTree::new();
8255 let (row, pressed) = tappable(&mut tree);
8256 let list = tree.add(
8257 StackWidget::new()
8258 .child(row)
8259 .pan_claim(PanClaim::vertical())
8260 .on_scroll(|_e, _c| EventResponse::Handled),
8261 );
8262 tree.layout(SizeProposal::exact(200.0, 400.0));
8263 let _ = list;
8264
8265 tree.dispatch_event(WidgetEvent::pointer_down(
8266 Point::new(100.0, 200.0),
8267 PointerButton::Primary,
8268 Modifiers::NONE,
8269 ));
8270 assert!(!tree.press_pending(row));
8271 assert!(pressed.get(), "Compact with a mouse is exactly as it was");
8272 }
8273
8274 #[test]
8281 fn a_handler_can_read_the_press_it_is_inside() {
8282 let (mut tree, _clock) = tree_on_a_clock();
8283 let seen: Rc<RefCell<Vec<(bool, bool, bool)>>> = Rc::new(RefCell::new(Vec::new()));
8284 let log = seen.clone();
8285 let row = tree.add(FillWidget::new().on_tap(|_e, _c| {}).on_pointer_event(
8286 move |event, ctx| {
8287 if matches!(event, WidgetEvent::PointerUp { .. }) {
8288 log.borrow_mut().push((
8289 ctx.is_pressed(),
8290 ctx.press_is_inside(),
8291 ctx.press_pending(),
8292 ));
8293 }
8294 EventResponse::Ignored
8295 },
8296 ));
8297 let list = tree.add(
8298 StackWidget::new()
8299 .child(row)
8300 .pan_claim(PanClaim::vertical())
8301 .on_scroll(|_e, _c| EventResponse::Handled),
8302 );
8303 tree.layout(SizeProposal::exact(200.0, 400.0));
8304 let _ = list;
8305
8306 let id = contact_id();
8307 let at = Point::new(100.0, 200.0);
8308 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8309 tree.dispatch_pointer(contact(
8310 id,
8311 PointerPhase::Up,
8312 at,
8313 EventTime::from_millis(30),
8314 ));
8315
8316 assert_eq!(
8317 seen.borrow().as_slice(),
8318 &[(false, true, true)],
8319 "released inside the claimant before the delay elapsed: inside, \
8320 pending, and therefore not yet showing",
8321 );
8322 }
8323
8324 #[test]
8327 fn a_handler_outside_a_press_reads_nothing() {
8328 let mut tree = WidgetTree::new();
8329 let asked = Rc::new(Cell::new(false));
8330 let flag = asked.clone();
8331 let w = tree.add(FillWidget::new().focusable().on_key(move |_e, ctx| {
8332 flag.set(ctx.is_pressed() || ctx.press_is_inside() || ctx.press_pending());
8333 EventResponse::Ignored
8334 }));
8335 tree.layout(SizeProposal::exact(100.0, 50.0));
8336 tree.focus(w);
8337 tree.press_key(Key::ArrowDown, Modifiers::NONE);
8338 assert!(!asked.get());
8339 }
8340
8341 #[test]
8345 fn a_completed_press_leaves_the_detector_clean() {
8346 let (mut tree, _clock) = tree_on_a_clock();
8347 let (w, pressed) = tappable(&mut tree);
8348 tree.layout(SizeProposal::exact(100.0, 50.0));
8349 let at = tree.bounds(w).center();
8350
8351 let id = contact_id();
8352 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8353 assert!(pressed.get());
8354 tree.dispatch_pointer(contact(
8355 id,
8356 PointerPhase::Up,
8357 at,
8358 EventTime::from_millis(30),
8359 ));
8360 tree.assert_no_leaked_pointer_state();
8361 }
8362
8363 #[test]
8367 fn the_detector_reports_a_press_still_held() {
8368 let (mut tree, _clock) = tree_on_a_clock();
8369 let (w, _pressed) = tappable(&mut tree);
8370 tree.layout(SizeProposal::exact(100.0, 50.0));
8371 let at = tree.bounds(w).center();
8372
8373 let id = contact_id();
8374 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8375 let held = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
8376 tree.assert_no_leaked_pointer_state()
8377 }));
8378 let message = *held
8379 .expect_err("a live press is leaked state")
8380 .downcast::<String>()
8381 .expect("the detector panics with a message");
8382 assert!(
8383 message.contains("is still pressed by"),
8384 "the press is named in the report, got: {message}",
8385 );
8386 }
8387
8388 #[derive(Debug)]
8392 struct SideBySide {
8393 children: Vec<WidgetId>,
8394 }
8395
8396 impl crate::widget::Widget for SideBySide {
8397 fn layout_response(
8398 &self,
8399 proposal: SizeProposal,
8400 _ctx: &crate::widget::LayoutContext,
8401 ) -> crate::widget::LayoutResponse {
8402 proposal.resolve(0.0, 0.0).into()
8403 }
8404
8405 fn place_children(
8406 &self,
8407 bounds: teksilo_canvas::Rect,
8408 _proposal: SizeProposal,
8409 children: &mut [crate::widget::WidgetPlacement],
8410 _ctx: &crate::widget::LayoutContext,
8411 ) {
8412 let n = children.len().max(1) as f32;
8413 let w = bounds.width / n;
8414 for (i, child) in children.iter_mut().enumerate() {
8415 child.origin = Point::new(bounds.x + w * i as f32, bounds.y);
8416 child.size = teksilo_canvas::Size::new(w, bounds.height);
8417 }
8418 }
8419
8420 fn children(&self) -> Vec<WidgetId> {
8421 self.children.clone()
8422 }
8423 }
8424}
8425
8426#[cfg(test)]
8427mod overlay_release_dismissal_tests {
8428 use std::cell::Cell;
8438 use std::rc::Rc;
8439
8440 use teksilo_canvas::{Point, Rect, Size, SizeProposal};
8441
8442 use crate::WidgetId;
8443 use crate::event::{Modifiers, PointerButton, WidgetEvent};
8444 use crate::overlay::{DismissBehavior, OverlayPlacement, OverlayRequest};
8445 use crate::pointer::{
8446 BackendDeviceKey, EventTime, PointerId, PointerIdAllocator, PointerInfo, PointerPhase,
8447 PointerSample,
8448 };
8449 use crate::test_widgets::FillWidget;
8450 use crate::widget::{LayoutContext, LayoutResponse, Widget};
8451 use crate::widget_builder::WidgetBuilder;
8452 use crate::widget_tree::WidgetTree;
8453
8454 #[derive(Debug)]
8461 struct Panel(Size);
8462
8463 impl Widget for Panel {
8464 fn layout_response(&self, _proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
8465 self.0.into()
8466 }
8467 }
8468
8469 #[derive(Debug)]
8477 struct PageAndTrigger {
8478 children: Vec<WidgetId>,
8479 }
8480
8481 impl Widget for PageAndTrigger {
8482 fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
8483 proposal.resolve(0.0, 0.0).into()
8484 }
8485
8486 fn place_children(
8487 &self,
8488 bounds: Rect,
8489 _proposal: SizeProposal,
8490 children: &mut [crate::widget::WidgetPlacement],
8491 _ctx: &LayoutContext,
8492 ) {
8493 if let Some(page) = children.get_mut(0) {
8494 page.origin = bounds.origin();
8495 page.size = bounds.size();
8496 }
8497 if let Some(trigger) = children.get_mut(1) {
8498 trigger.origin = bounds.origin();
8499 trigger.size = Size::new(10.0, 10.0);
8500 }
8501 }
8502
8503 fn children(&self) -> Vec<WidgetId> {
8504 self.children.clone()
8505 }
8506 }
8507
8508 fn contact_id(n: u64) -> PointerId {
8509 PointerIdAllocator::global().begin(BackendDeviceKey::new(0x0FA2), n)
8510 }
8511
8512 fn touch(id: PointerId, phase: PointerPhase, at: Point) -> PointerSample {
8513 PointerSample {
8514 pointer: PointerInfo::touch(id, EventTime::ZERO),
8515 phase,
8516 position: at,
8517 button: None,
8518 modifiers: Modifiers::NONE,
8519 coalesced: Vec::new(),
8520 }
8521 }
8522
8523 fn page_with_a_menu() -> (WidgetTree, WidgetId, Rc<Cell<u32>>) {
8530 let mut tree = WidgetTree::new();
8531 let taps = Rc::new(Cell::new(0u32));
8532 let counter = taps.clone();
8533 let page = tree.add(
8534 FillWidget::new()
8535 .focusable()
8536 .on_tap(move |_e, _c| counter.set(counter.get() + 1)),
8537 );
8538 let trigger = tree.add(Panel(Size::new(10.0, 10.0)));
8541 let _root = tree.add(PageAndTrigger {
8542 children: vec![page, trigger],
8543 });
8544 let menu = tree.add(Panel(Size::new(200.0, 200.0)));
8545 tree.show_overlay(OverlayRequest::new(
8546 menu,
8547 trigger,
8548 OverlayPlacement::AtPointer(Point::new(100.0, 100.0)),
8549 DismissBehavior::ClickOutside,
8550 ));
8551 tree.layout(SizeProposal::exact(800.0, 600.0));
8552 assert_eq!(tree.active_overlays().len(), 1, "the menu is open");
8553 (tree, page, taps)
8554 }
8555
8556 fn outside() -> Point {
8557 Point::new(600.0, 500.0)
8558 }
8559
8560 fn inside_menu() -> Point {
8561 Point::new(150.0, 150.0)
8562 }
8563
8564 #[test]
8571 fn the_suppressed_down_leaves_nothing_pressed_or_captured_beneath() {
8572 let (mut tree, page, taps) = page_with_a_menu();
8573 let finger = contact_id(1);
8574 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8575
8576 assert_eq!(
8577 tree.active_overlays().len(),
8578 1,
8579 "the press does not close the menu; the release does"
8580 );
8581 assert_eq!(tree.pressed_by(page), None, "nothing beneath is pressed");
8582 let entry = tree.pointers.get(finger).expect("the contact is live");
8583 assert_eq!(entry.captured_by, None, "nothing beneath captured it");
8584 assert!(entry.sequence.is_none(), "no arbitration was opened");
8585 assert_eq!(taps.get(), 0);
8586 }
8587
8588 #[test]
8590 fn a_touch_tap_outside_a_menu_closes_it_and_actuates_nothing() {
8591 let (mut tree, page, taps) = page_with_a_menu();
8592 let finger = contact_id(2);
8593 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8594 tree.dispatch_pointer(touch(finger, PointerPhase::Up, outside()));
8595
8596 assert!(tree.active_overlays().is_empty(), "the menu closed");
8597 assert_eq!(taps.get(), 0, "the page beneath was never tapped");
8598 assert_eq!(tree.pressed_by(page), None);
8599 tree.assert_no_leaked_pointer_state();
8600 }
8601
8602 #[test]
8605 fn a_cancelled_press_aborts_the_arm() {
8606 let (mut tree, page, taps) = page_with_a_menu();
8607 let finger = contact_id(3);
8608 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8609 tree.dispatch_pointer(touch(finger, PointerPhase::Cancel, outside()));
8610
8611 assert_eq!(tree.active_overlays().len(), 1, "the menu survives");
8612 assert_eq!(taps.get(), 0);
8613 assert_eq!(tree.pressed_by(page), None);
8614 tree.assert_no_leaked_pointer_state();
8615 }
8616
8617 #[test]
8621 fn a_press_slid_onto_the_menu_dismisses_nothing() {
8622 let (mut tree, _page, taps) = page_with_a_menu();
8623 let finger = contact_id(4);
8624 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8625 tree.dispatch_pointer(touch(finger, PointerPhase::Move, inside_menu()));
8626 tree.dispatch_pointer(touch(finger, PointerPhase::Up, inside_menu()));
8627
8628 assert_eq!(tree.active_overlays().len(), 1, "the menu survives");
8629 assert_eq!(taps.get(), 0);
8630 tree.assert_no_leaked_pointer_state();
8631 }
8632
8633 #[test]
8636 fn a_second_contact_cannot_dismiss_what_the_first_is_manipulating() {
8637 let (mut tree, _page, _taps) = page_with_a_menu();
8638 let first = contact_id(5);
8639 let second = contact_id(6);
8640
8641 tree.dispatch_pointer(touch(first, PointerPhase::Down, inside_menu()));
8642 assert!(
8643 tree.pointers
8644 .get(first)
8645 .is_some_and(|e| e.sequence.is_some()),
8646 "the first contact holds a live press inside the menu"
8647 );
8648
8649 tree.dispatch_pointer(touch(second, PointerPhase::Down, outside()));
8650 tree.dispatch_pointer(touch(second, PointerPhase::Up, outside()));
8651 assert_eq!(
8652 tree.active_overlays().len(),
8653 1,
8654 "the menu the first finger is holding must not close under it"
8655 );
8656
8657 tree.dispatch_pointer(touch(first, PointerPhase::Up, inside_menu()));
8660 let third = contact_id(7);
8661 tree.dispatch_pointer(touch(third, PointerPhase::Down, outside()));
8662 tree.dispatch_pointer(touch(third, PointerPhase::Up, outside()));
8663 assert!(tree.active_overlays().is_empty());
8664 tree.assert_no_leaked_pointer_state();
8665 }
8666
8667 #[test]
8671 fn an_arm_is_not_itself_a_press_that_blocks_another_contact() {
8672 let (mut tree, _page, _taps) = page_with_a_menu();
8673 let first = contact_id(8);
8674 let second = contact_id(9);
8675
8676 tree.dispatch_pointer(touch(first, PointerPhase::Down, outside()));
8677 assert!(
8678 tree.overlay_manager().has_armed_dismiss(first),
8679 "the first contact armed"
8680 );
8681 assert!(tree.busy_press_points(second).is_empty());
8683
8684 tree.dispatch_pointer(touch(second, PointerPhase::Down, outside()));
8685 assert!(tree.overlay_manager().has_armed_dismiss(second));
8686 tree.dispatch_pointer(touch(second, PointerPhase::Up, outside()));
8687 assert!(tree.active_overlays().is_empty());
8688
8689 tree.dispatch_pointer(touch(first, PointerPhase::Up, outside()));
8692 tree.assert_no_leaked_pointer_state();
8693 }
8694
8695 #[test]
8698 fn a_touch_inside_the_menu_arms_nothing() {
8699 let (mut tree, _page, _taps) = page_with_a_menu();
8700 let finger = contact_id(10);
8701 tree.dispatch_pointer(touch(finger, PointerPhase::Down, inside_menu()));
8702 assert!(!tree.overlay_manager().has_armed_dismiss(finger));
8703 tree.dispatch_pointer(touch(finger, PointerPhase::Up, inside_menu()));
8704 assert_eq!(tree.active_overlays().len(), 1);
8705 tree.assert_no_leaked_pointer_state();
8706 }
8707
8708 #[test]
8718 fn a_contact_with_no_press_still_ends_when_the_platform_revokes_it() {
8719 let mut tree = WidgetTree::new();
8720 tree.layout(SizeProposal::exact(800.0, 600.0));
8721 let finger = contact_id(99);
8722 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8723 tree.dispatch_pointer(touch(finger, PointerPhase::Cancel, outside()));
8724 tree.assert_no_leaked_pointer_state();
8725 }
8726
8727 #[test]
8734 fn a_mouse_click_outside_a_menu_dismisses_on_the_press_and_falls_through() {
8735 let (mut tree, _page, taps) = page_with_a_menu();
8736 tree.dispatch_event(WidgetEvent::pointer_down(
8737 outside(),
8738 PointerButton::Primary,
8739 Modifiers::NONE,
8740 ));
8741 assert!(
8742 tree.active_overlays().is_empty(),
8743 "the mouse still dismisses on the press"
8744 );
8745 assert!(
8746 tree.pointers
8747 .get(PointerId::MOUSE)
8748 .is_some_and(|e| e.sequence.is_some()),
8749 "and the press still reaches the page beneath"
8750 );
8751 tree.dispatch_event(WidgetEvent::pointer_up(
8752 outside(),
8753 PointerButton::Primary,
8754 Modifiers::NONE,
8755 ));
8756 assert_eq!(taps.get(), 1, "the control beneath activated");
8757 assert!(
8758 !tree.overlay_manager().has_armed_dismiss(PointerId::MOUSE),
8759 "a mouse never arms"
8760 );
8761 tree.assert_no_leaked_pointer_state();
8762 }
8763
8764 #[test]
8771 fn a_context_menu_avoids_the_contact_that_opened_it() {
8772 fn menu_bounds(pointer: PointerInfo, at: Point) -> Rect {
8773 let mut tree = WidgetTree::new();
8774 let page = tree.add(
8775 FillWidget::new()
8776 .focusable()
8777 .context_menu(|_p, _ctx| Some(Box::new(Panel(Size::new(200.0, 160.0))))),
8778 );
8779 let _ = page;
8780 tree.layout(SizeProposal::exact(800.0, 600.0));
8781 tree.dispatch_pointer(PointerSample {
8782 pointer,
8783 phase: PointerPhase::Down,
8784 position: at,
8785 button: Some(PointerButton::Secondary),
8786 modifiers: Modifiers::NONE,
8787 coalesced: Vec::new(),
8788 });
8789 tree.layout(SizeProposal::exact(800.0, 600.0));
8790 let id = *tree
8791 .active_overlays()
8792 .first()
8793 .expect("the context menu opened");
8794 tree.overlay_manager().bounds_for(id).expect("bounds")
8795 }
8796
8797 let at = Point::new(400.0, 300.0);
8798 let finger = menu_bounds(PointerInfo::touch(contact_id(11), EventTime::ZERO), at);
8799 let contact = crate::overlay::rect_centred_on(at, crate::overlay::ASSUMED_CONTACT_PATCH);
8800 assert!(
8801 finger.x < contact.x && finger.right() <= contact.x,
8802 "a touch menu clears the contact patch: {finger:?} vs {contact:?}"
8803 );
8804
8805 let mouse = menu_bounds(PointerInfo::mouse(EventTime::ZERO), at);
8806 assert_eq!(
8807 (mouse.x, mouse.y),
8808 (at.x, at.y),
8809 "a mouse menu still opens with its corner on the pointer"
8810 );
8811 }
8812}
8813
8814#[cfg(test)]
8830mod pass_through_overlay_tests {
8831 use teksilo_canvas::{Point, Rect, SizeProposal};
8832
8833 use crate::WidgetId;
8834 use crate::overlay::{DismissBehavior, OverlayPlacement, OverlayRequest};
8835 use crate::test_widgets::FillWidget;
8836 use crate::widget::{LayoutContext, LayoutResponse, Widget};
8837 use crate::widget_builder::WidgetBuilder;
8838 use crate::widget_tree::WidgetTree;
8839
8840 #[derive(Debug)]
8843 struct HandleLayer {
8844 handle: WidgetId,
8845 at: Rect,
8846 }
8847
8848 impl Widget for HandleLayer {
8849 fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
8850 proposal.resolve(0.0, 0.0).into()
8851 }
8852
8853 fn place_children(
8854 &self,
8855 _bounds: Rect,
8856 _proposal: SizeProposal,
8857 children: &mut [crate::widget::WidgetPlacement],
8858 _ctx: &LayoutContext,
8859 ) {
8860 if let Some(child) = children.get_mut(0) {
8861 child.origin = self.at.origin();
8862 child.size = self.at.size();
8863 }
8864 }
8865
8866 fn children(&self) -> Vec<WidgetId> {
8867 vec![self.handle]
8868 }
8869 }
8870
8871 const HANDLE: Rect = Rect {
8872 x: 10.0,
8873 y: 10.0,
8874 width: 20.0,
8875 height: 20.0,
8876 };
8877
8878 #[derive(Clone, Copy, PartialEq)]
8880 enum RootFlag {
8881 PassThrough,
8883 HitTransparent,
8886 }
8887
8888 fn field_under_a_layer(flag: RootFlag) -> (WidgetTree, WidgetId, WidgetId) {
8894 let mut tree = WidgetTree::new();
8895 let field = tree.add(FillWidget::new());
8896 let handle = tree.add(FillWidget::new());
8897 let layer = HandleLayer { handle, at: HANDLE };
8898 let layer = match flag {
8899 RootFlag::PassThrough => tree.add(layer.event_pass_through(true)),
8900 RootFlag::HitTransparent => tree.add(layer.hit_transparent(true)),
8901 };
8902 tree.show_overlay(OverlayRequest::new(
8903 layer,
8904 field,
8905 OverlayPlacement::FullViewport,
8906 DismissBehavior::Manual,
8907 ));
8908 tree.layout(SizeProposal::exact(400.0, 200.0));
8909 assert_eq!(tree.active_overlays().len(), 1, "the layer is up");
8910 (tree, field, handle)
8911 }
8912
8913 #[test]
8915 fn a_pass_through_layer_hands_back_what_it_did_not_claim() {
8916 let (tree, field, handle) = field_under_a_layer(RootFlag::PassThrough);
8917
8918 assert_eq!(
8919 tree.hit_test(Point::new(15.0, 15.0)),
8920 Some(handle),
8921 "the layer must still win the point its own child covers"
8922 );
8923 assert_eq!(
8924 tree.hit_test(Point::new(200.0, 100.0)),
8925 Some(field),
8926 "a press the layer did not claim must reach the surface beneath it"
8927 );
8928 }
8929
8930 #[test]
8941 fn a_pass_through_root_still_hears_a_press_that_landed_on_its_child() {
8942 use std::cell::Cell;
8943 use std::rc::Rc;
8944
8945 let seen = Rc::new(Cell::new(0u32));
8946 let counter = seen.clone();
8947 let mut tree = WidgetTree::new();
8948 let field = tree.add(FillWidget::new());
8949 let handle = tree.add(FillWidget::new().on_tap(|_e, _c| {}));
8950 let layer = tree.add(
8951 HandleLayer { handle, at: HANDLE }
8952 .event_pass_through(true)
8953 .on_pointer_event(move |_event, _ctx| {
8954 counter.set(counter.get() + 1);
8955 crate::event::EventResponse::Ignored
8956 }),
8957 );
8958 tree.show_overlay(OverlayRequest::new(
8959 layer,
8960 field,
8961 OverlayPlacement::FullViewport,
8962 DismissBehavior::Manual,
8963 ));
8964 tree.layout(SizeProposal::exact(400.0, 200.0));
8965
8966 tree.pointer_down_button(Point::new(15.0, 15.0), crate::event::PointerButton::Primary);
8967 assert!(
8968 seen.get() > 0,
8969 "a pass-through root heard nothing about a press on its own child"
8970 );
8971
8972 let after_child = seen.get();
8973 tree.pointer_up_button(Point::new(15.0, 15.0), crate::event::PointerButton::Primary);
8974 tree.pointer_down_button(
8977 Point::new(200.0, 100.0),
8978 crate::event::PointerButton::Primary,
8979 );
8980 assert_eq!(
8981 seen.get(),
8982 after_child + 1,
8983 "the press-and-lift on the child accounts for the count, and the \
8984 press on the surface beneath added nothing"
8985 );
8986 }
8987
8988 #[test]
9002 fn a_hit_transparent_root_does_not_get_the_fall_through() {
9003 let (tree, field, _handle) = field_under_a_layer(RootFlag::HitTransparent);
9004
9005 assert_eq!(
9007 tree.hit_test(Point::new(15.0, 15.0)),
9008 None,
9009 "hit_transparent must exclude the subtree, or this fixture is not \
9010 testing the gate"
9011 );
9012 assert_eq!(
9013 tree.hit_test(Point::new(200.0, 100.0)),
9014 None,
9015 "the fall-through reached past an overlay that did not ask for it"
9016 );
9017 let _ = field;
9018 }
9019}