1use super::*;
9
10use crate::gesture::{GestureEvent, RawPointerEvent, TapEvent};
11
12fn fire_event_handler_both(
16 external: &mut Option<Box<dyn FnMut(&WidgetEvent, &mut EventContext) -> EventResponse>>,
17 own: &mut Option<Box<dyn FnMut(&WidgetEvent, &mut EventContext) -> EventResponse>>,
18 event: &WidgetEvent,
19 ctx: &mut EventContext,
20) -> EventResponse {
21 let r1 = external
22 .as_mut()
23 .map(|h| h(event, ctx))
24 .unwrap_or(EventResponse::Ignored);
25 let r2 = own
26 .as_mut()
27 .map(|h| h(event, ctx))
28 .unwrap_or(EventResponse::Ignored);
29 if r1 == EventResponse::Handled || r2 == EventResponse::Handled {
30 EventResponse::Handled
31 } else {
32 EventResponse::Ignored
33 }
34}
35
36pub(super) enum QueuedDispatch {
43 Event {
50 event: WidgetEvent,
51 snapshot: Box<crate::pointer::InputSnapshot>,
52 },
53 Cancel {
64 pointer: crate::pointer::PointerId,
65 reason: crate::pointer::CancelReason,
66 recipient: Option<WidgetId>,
74 },
75}
76
77fn pointer_event_position(event: &WidgetEvent) -> Option<Point> {
83 match event {
84 WidgetEvent::PointerDown { position, .. }
85 | WidgetEvent::PointerUp { position, .. }
86 | WidgetEvent::PointerMove { position, .. } => Some(*position),
87 _ => None,
91 }
92}
93
94#[derive(Copy, Clone)]
99struct BubbleGates {
100 fire_on_pointer_event: bool,
108 arena_blocked: bool,
113}
114
115impl WidgetTree {
116 fn scope_distance_from_focus(&self, focus: WidgetId, scope_id: WidgetId) -> Option<usize> {
120 let mut hops = 0usize;
121 let mut current = Some(focus);
122 while let Some(c) = current {
123 if c == scope_id {
124 return Some(hops);
125 }
126 current = self.arena.parent(c);
127 hops += 1;
128 }
129 None
130 }
131
132 fn select_shortcut_for_focus(
142 &self,
143 candidates: &[(&'static str, crate::shortcut::ShortcutScope, bool)],
144 ) -> Option<(&'static str, crate::shortcut::ShortcutScope, bool)> {
145 use crate::shortcut::ShortcutScope;
146 let mut best: Option<(usize, (&'static str, ShortcutScope, bool))> = None;
147 for &(id, scope, propagate) in candidates {
148 let score = match scope {
153 ShortcutScope::Global => Some(0usize),
154 ShortcutScope::Scoped(scope_id) => self
155 .focused
156 .and_then(|f| self.scope_distance_from_focus(f, scope_id))
157 .map(|hops| usize::MAX - hops),
158 };
159 let Some(score) = score else { continue };
160 if best.as_ref().is_none_or(|(b, _)| score > *b) {
163 best = Some((score, (id, scope, propagate)));
164 }
165 }
166 best.map(|(_, c)| c)
167 }
168
169 pub fn dispatch_event_with_ops(
183 &mut self,
184 event: WidgetEvent,
185 ops: &mut dyn crate::window::WindowOps,
186 ) {
187 let mut snapshot = crate::pointer::InputSnapshot::from_event(&event);
190 if snapshot.pointer.time == crate::pointer::EventTime::ZERO {
195 snapshot.pointer.time = self.input_now();
196 }
197 self.dispatch_with_input_snapshot(event, snapshot, ops)
198 }
199
200 pub fn dispatch_event(&mut self, event: WidgetEvent) {
206 let mut noop = crate::window::NoopWindowOps;
207 self.dispatch_event_with_ops(event, &mut noop);
208 }
209
210 pub fn dispatch_pointer(&mut self, sample: crate::pointer::PointerSample) {
229 let mut noop = crate::window::NoopWindowOps;
230 self.dispatch_pointer_with_ops(sample, &mut noop);
231 }
232
233 pub fn dispatch_pointer_with_ops(
237 &mut self,
238 sample: crate::pointer::PointerSample,
239 ops: &mut dyn crate::window::WindowOps,
240 ) {
241 use crate::pointer::PointerPhase;
242
243 crate::trace_input!(
244 Samples,
245 "{:?} {:?} at {:?} buttons={:?} t={:?}",
246 sample.phase,
247 sample.pointer.id,
248 sample.position,
249 sample.pointer.buttons,
250 sample.pointer.time
251 );
252
253 let button = sample
257 .button
258 .unwrap_or(crate::event::PointerButton::Primary);
259 let event = match sample.phase {
260 PointerPhase::Down => WidgetEvent::PointerDown {
261 position: sample.position,
262 button,
263 modifiers: sample.modifiers,
264 pointer: sample.pointer,
265 },
266 PointerPhase::Move => WidgetEvent::PointerMove {
267 position: sample.position,
268 modifiers: sample.modifiers,
269 pointer: sample.pointer,
270 },
271 PointerPhase::Up => WidgetEvent::PointerUp {
272 position: sample.position,
273 button,
274 modifiers: sample.modifiers,
275 pointer: sample.pointer,
276 },
277 PointerPhase::Cancel => {
283 self.overlay_manager.abort_dismiss(sample.pointer.id);
288 self.cancel_pointer(
289 sample.pointer.id,
290 crate::pointer::CancelReason::Platform,
291 ops,
292 );
293 if !sample.pointer.kind.hovers() {
300 self.pointers.end(sample.pointer.id);
301 }
302 return;
303 }
304 };
305
306 if !self.pointers.would_admit(&sample.pointer) {
312 return;
313 }
314 let ends_pointer = matches!(sample.phase, PointerPhase::Up | PointerPhase::Cancel)
318 && !sample.pointer.kind.hovers();
319 let pointer_id = sample.pointer.id;
320
321 let snapshot = crate::pointer::InputSnapshot::from_pointer_sample(&sample);
322 self.dispatch_with_input_snapshot(event, snapshot, ops);
323
324 if ends_pointer {
325 self.pointers.end(pointer_id);
326 }
327 }
328
329 pub fn dispatch_scroll(&mut self, sample: crate::pointer::ScrollSample) {
337 let mut noop = crate::window::NoopWindowOps;
338 self.dispatch_scroll_with_ops(sample, &mut noop);
339 }
340
341 pub fn dispatch_scroll_with_ops(
344 &mut self,
345 sample: crate::pointer::ScrollSample,
346 ops: &mut dyn crate::window::WindowOps,
347 ) {
348 crate::trace_input!(
349 Samples,
350 "scroll {:?} {:?}/{:?} at {:?}",
351 sample.delta,
352 sample.phase,
353 sample.source,
354 sample.position
355 );
356
357 let event = WidgetEvent::Scroll {
358 delta: sample.delta,
359 modifiers: sample.modifiers,
360 window_position: sample.position,
361 phase: sample.phase,
362 pointer: sample.pointer,
363 };
364 let snapshot = crate::pointer::InputSnapshot::from_scroll_sample(&sample);
365 self.dispatch_with_input_snapshot(event, snapshot, ops);
366 }
367
368 pub fn pointer_left_window(&mut self, ops: &mut dyn crate::window::WindowOps) {
393 if self
397 .pointers
398 .hover_owner_id()
399 .and_then(|id| self.captured_by(id))
400 .is_some()
401 {
402 return;
403 }
404 let Some(hovered) = self.hovered_id() else {
405 return;
406 };
407 let leave = WidgetEvent::PointerLeave {
410 pointer: self.hover_transition_pointer(),
411 };
412 self.dispatch_to_widget(hovered, &leave, &mut *ops);
413 self.tooltip_pointer_leave(hovered, &mut *ops);
414 self.set_hovered(None);
415 self.update_hover_within_signals(Some(hovered), None);
416 }
417
418 pub(super) fn hover_transition_pointer(&self) -> crate::pointer::PointerInfo {
435 self.pointers
436 .hover_owner()
437 .map(|entry| entry.info)
438 .unwrap_or_else(|| crate::pointer::PointerInfo::mouse(self.input_now()))
439 }
440
441 fn dispatch_with_input_snapshot(
448 &mut self,
449 event: WidgetEvent,
450 snapshot: crate::pointer::InputSnapshot,
451 ops: &mut dyn crate::window::WindowOps,
452 ) {
453 self.pending_dispatch.push_back(QueuedDispatch::Event {
463 event,
464 snapshot: Box::new(snapshot),
465 });
466 if self.dispatch_depth > 0 {
467 return;
468 }
469 self.drain_pending_dispatch(&mut *ops);
470 }
471
472 pub(super) fn drain_pending_dispatch(&mut self, ops: &mut dyn crate::window::WindowOps) {
478 while let Some(queued) = self.pending_dispatch.pop_front() {
479 match queued {
480 QueuedDispatch::Event { event, snapshot } => {
481 self.run_one_dispatch(event, *snapshot, &mut *ops);
482 }
483 QueuedDispatch::Cancel {
484 pointer,
485 reason,
486 recipient,
487 } => {
488 self.run_one_cancel(pointer, reason, recipient, &mut *ops);
489 }
490 }
491 }
492 }
493
494 fn run_one_dispatch(
501 &mut self,
502 event: WidgetEvent,
503 snapshot: crate::pointer::InputSnapshot,
504 ops: &mut dyn crate::window::WindowOps,
505 ) {
506 let previous = std::mem::replace(&mut self.current_input, snapshot);
507 self.dispatch_depth += 1;
508 self.dispatch_event_impl(event, ops);
509 self.dispatch_depth -= 1;
510 self.current_input = previous;
511 }
512
513 fn dispatch_event_impl(&mut self, event: WidgetEvent, ops: &mut dyn crate::window::WindowOps) {
514 if let Some(position) = pointer_event_position(&event) {
520 let is_down = matches!(event, WidgetEvent::PointerDown { .. });
521 let is_move = matches!(event, WidgetEvent::PointerMove { .. });
522 if !self.admit_current_pointer(position, is_down, is_move) {
523 return;
524 }
525 if self.current_input.pointer.kind.hovers() {
529 self.claim_hover_owner_for_current(&mut *ops);
530 }
531 }
532
533 match &event {
537 WidgetEvent::KeyDown { .. } if !self.focus_visible.get() => {
538 self.focus_visible.set(true);
539 }
540 WidgetEvent::PointerDown { .. } if self.focus_visible.get() => {
541 self.focus_visible.set(false);
542 }
543 _ => {}
544 }
545
546 let overlay_back_key = match self.layout_direction {
552 crate::environment::LayoutDirection::RightToLeft => Key::ArrowRight,
553 crate::environment::LayoutDirection::LeftToRight => Key::ArrowLeft,
554 };
555 if let WidgetEvent::KeyDown { key, .. } = &event
556 && *key == overlay_back_key
557 {
558 let nested_menu_overlays = {
583 let ids: Vec<_> = self
584 .overlay_manager
585 .stack
586 .iter()
587 .filter(|o| o.band == crate::overlay::OverlayBand::Standard)
588 .map(|o| o.id)
589 .collect();
590 ids.into_iter()
591 .filter(|&id| !self.overlay_is_host_surface(id))
592 .count()
593 };
594 let top_id = self.overlay_manager.stack.last().map(|o| o.id);
600 let top_is_back_navigable = top_id.is_some_and(|id| !self.overlay_is_host_surface(id));
601 if nested_menu_overlays > 1 && top_is_back_navigable {
602 if let Some((_id, content_ids, focus_restore)) = self.overlay_manager.dismiss_top()
603 {
604 self.dormant_dismissed_content(&content_ids, &mut *ops);
605 if let Some(restore_id) = focus_restore
606 && self.arena.is_active(restore_id)
607 {
608 self.focus_ops(restore_id, &mut *ops);
609 }
610 }
611 return;
612 }
613 }
614
615 if let WidgetEvent::KeyDown {
621 key: Key::Escape, ..
622 } = &event
623 {
624 self.tooltip_escape_pressed();
625 }
626
627 if let WidgetEvent::KeyDown {
628 key: Key::Escape, ..
629 } = &event
630 && !self.overlay_manager.is_empty()
631 && let Some((_id, content_ids, focus_restore)) =
632 self.overlay_manager.try_dismiss_top_on_escape()
633 {
634 self.dormant_dismissed_content(&content_ids, &mut *ops);
635 if let Some(restore_id) = focus_restore
636 && self.arena.is_active(restore_id)
637 {
638 self.focus_ops(restore_id, &mut *ops);
639 }
640 return;
641 }
642
643 match &event {
648 WidgetEvent::PointerDown {
649 position, button, ..
650 } => {
651 if self.arm_outside_press_dismissal(*position, *button, &mut *ops) {
652 return;
653 }
654 }
655 WidgetEvent::PointerUp { position, .. } => {
656 if self.commit_outside_press_dismissal(*position, &mut *ops) {
657 return;
658 }
659 }
660 WidgetEvent::PointerCancel { pointer, .. } => {
661 self.overlay_manager.abort_dismiss(pointer.id);
665 }
666 _ => {}
667 }
668
669 if let WidgetEvent::KeyDown { key, modifiers, .. } = &event
680 && let Some(callback) = self.take_key_capture()
681 {
682 let keystroke = crate::shortcut::KeyStroke::new(*key, *modifiers);
683 let mut cap_ctx = self.make_event_context(&mut *ops);
684 callback(keystroke, self.shortcut_registry_mut(), &mut cap_ctx);
685 let anchor = self.focused.or_else(|| self.arena.roots().first().copied());
688 if let Some(anchor_id) = anchor {
689 self.collect_from_ctx(cap_ctx, anchor_id);
690 self.drain_pending_intents(&mut *ops);
691 }
692 return;
693 }
694
695 let focus_captures_keys = matches!(&event, WidgetEvent::KeyDown { .. })
703 && self.focused.is_some_and(|f| self.is_keyboard_capture(f));
704
705 if !focus_captures_keys && let WidgetEvent::KeyDown { key, modifiers, .. } = &event {
720 let keystroke = crate::shortcut::KeyStroke::new(*key, *modifiers);
721 let candidates: Vec<(&'static str, crate::shortcut::ShortcutScope, bool)> = self
730 .shortcut_registry
731 .matches_by_keystroke(keystroke)
732 .map(|eff| {
733 (
734 eff.shortcut.id,
735 eff.shortcut.scope,
736 eff.shortcut.propagate_when_disabled,
737 )
738 })
739 .collect();
740 let lookup = self.select_shortcut_for_focus(&candidates);
741 if let Some((id, scope, propagate_when_disabled)) = lookup {
742 let anchor = match scope {
743 crate::shortcut::ShortcutScope::Global => {
748 self.focused.or_else(|| self.arena.roots().first().copied())
749 }
750 crate::shortcut::ShortcutScope::Scoped(scope_id) => {
751 self.focused.filter(|f| self.is_descendant_of(*f, scope_id))
752 }
753 };
754 if let Some(anchor_id) = anchor {
755 let mut act_ctx = self.make_event_context(&mut *ops);
756 if let Some(intent) =
757 self.shortcut_registry
758 .invoke_on_activate(id, keystroke, &mut act_ctx)
759 {
760 self.collect_from_ctx(act_ctx, anchor_id);
761 let intent = intent.with_source(crate::telemetry::IntentSource::Shortcut);
765 self.enqueue_intent(anchor_id, intent, propagate_when_disabled);
766 self.drain_pending_intents(&mut *ops);
767 return;
768 }
769 }
770 }
775 }
778
779 if self.outbound_drag_source.is_some()
785 && let WidgetEvent::KeyDown {
786 key: Key::Escape, ..
787 } = &event
788 {
789 ops.cancel_os_drag();
790 }
795
796 if self.active_drag.is_some() {
798 match &event {
799 WidgetEvent::PointerMove { position, .. } => {
800 self.handle_drag_move(*position, &mut *ops);
801 return;
802 }
803 WidgetEvent::PointerUp { position, .. } => {
804 self.handle_drag_drop(*position, &mut *ops);
805 return;
806 }
807 WidgetEvent::KeyDown {
808 key: Key::Escape, ..
809 } => {
810 self.cancel_active_drag(&mut *ops);
811 return;
812 }
813 WidgetEvent::Scroll { .. } => {
814 let target_and_pos = self
820 .active_drag
821 .as_ref()
822 .and_then(|d| d.current_target.map(|t| (t, d.current_position)));
823 if let Some((target, _pos)) = target_and_pos {
824 self.dispatch_to_widget(target, &event, &mut *ops);
825 }
826 if let Some((_, pos)) = target_and_pos
827 && self.active_drag.is_some()
828 {
829 self.handle_drag_move(pos, &mut *ops);
830 }
831 return;
832 }
833 _ => {}
834 }
835 }
836
837 if let WidgetEvent::KeyDown { key, modifiers, .. } = &event
846 && is_context_menu_chord(*key, *modifiers)
847 && self.open_context_menu_from_keyboard(&mut *ops)
848 {
849 return;
850 }
851
852 match &event {
853 WidgetEvent::PointerMove { position, .. } => {
854 self.note_sequence_position(*position);
858 self.revalidate_sequence(&mut *ops);
859 self.tick_sequence_timers();
862 self.note_palm_sample(*position);
867 self.feed_pinch(super::pan_arbiter::PinchFeed::Move, *position, &mut *ops);
868 if let Some(captured) = self.current_pointer_capture() {
869 self.dispatch_to_widget(captured, &event, &mut *ops);
870 if self.active_drag.is_none() {
875 self.advance_sequence(&event, &mut *ops);
876 }
877 } else {
878 self.handle_pointer_move(&event, *position, &mut *ops);
879 if self.active_drag.is_none() {
884 self.advance_sequence(&event, &mut *ops);
885 }
886 }
887 self.advance_pan(*position, &mut *ops);
890 self.update_press(*position);
893 self.touch_route_moved(self.current_pointer_id());
896 if self.pointers.hover_owner_id() == Some(self.current_pointer_id()) {
906 self.update_pointer_leave_overlays(*position, &mut *ops);
907 }
908 }
909 WidgetEvent::PointerDown {
910 position, button, ..
911 } => {
912 let pressed = self.current_pointer_id();
915 self.cancelled_pointers.retain(|p| *p != pressed);
916 self.tooltip_pointer_press(Some(*position));
923 let pressing = self.current_input.pointer;
927 if let Some(target) = self.hit_test_for(*position, &pressing) {
928 if *button == PointerButton::Secondary
929 && self.show_context_menu_for(target, *position, &mut *ops)
930 {
931 return;
932 }
933 self.begin_sequence(target, *position);
939 let modifiers = match &event {
944 WidgetEvent::PointerDown { modifiers, .. } => *modifiers,
945 _ => crate::event::Modifiers::NONE,
946 };
947 self.begin_pan(target, *position, modifiers);
948 self.begin_palm_watch(*position);
949 self.feed_pinch(super::pan_arbiter::PinchFeed::Down, *position, &mut *ops);
950 let focusable = self.find_focusable_at_or_above(target);
954 self.begin_press(focusable);
955 if !pressing.kind.is_direct()
962 && let Some(focusable) = focusable
963 {
964 self.focus_with_origin_ops(
965 focusable,
966 crate::focus::FocusOrigin::Pointer(pressing.kind),
967 &mut *ops,
968 );
969 }
970 self.dispatch_to_widget(target, &event, &mut *ops);
971 if self.active_drag.is_none() {
976 self.enrol_sequence_members(&event, &mut *ops);
977 }
978 self.adopt_press_owner(*button);
982 self.arm_touch_route(target, *position);
987 }
988 }
989 WidgetEvent::PointerUp { position, .. } => {
990 let released = self.current_pointer_id();
996 self.cancel_touch_route(released);
1000 if let Some(index) = self.cancelled_pointers.iter().position(|p| *p == released) {
1001 self.cancelled_pointers.swap_remove(index);
1002 crate::trace_input!(
1003 Samples,
1004 "swallowing the Up for {released:?}: its press was cancelled"
1005 );
1006 return;
1007 }
1008 self.feed_pinch(super::pan_arbiter::PinchFeed::Up, *position, &mut *ops);
1015 if self.take_palm_verdict(released) {
1016 crate::trace_input!(
1017 Samples,
1018 "{released:?} released as a palm: large, and it never moved"
1019 );
1020 self.cancel_pointer(
1021 released,
1022 crate::pointer::CancelReason::PalmRejected,
1023 &mut *ops,
1024 );
1025 return;
1026 }
1027 self.end_pan(*position, &mut *ops);
1030 self.note_sequence_position(*position);
1037 self.end_sequence(&event, &mut *ops);
1038 self.focus_on_release(*position, &mut *ops);
1042 if let Some(captured) = self.current_pointer_capture() {
1043 self.dispatch_to_widget(captured, &event, &mut *ops);
1044 self.set_current_pointer_capture(None);
1047 } else {
1048 let releasing = self.current_input.pointer;
1049 if let Some(target) = self.hit_test_for(*position, &releasing) {
1050 self.dispatch_to_widget(target, &event, &mut *ops);
1051 }
1052 }
1053 let released = self.current_pointer_id();
1056 self.release_arenas_following(released);
1057 self.end_press(released);
1061 }
1062 WidgetEvent::PointerCancel {
1063 reason, pointer, ..
1064 } => {
1065 let (reason, pointer) = (*reason, pointer.id);
1070 self.cancel_pointer(pointer, reason, &mut *ops);
1071 }
1072 WidgetEvent::Scroll {
1073 window_position: position,
1074 ..
1075 } => {
1076 use super::pan_arbiter::ScrollDelivery;
1077 match ScrollDelivery::for_source(self.current_input.scroll_source) {
1078 ScrollDelivery::ClaimantChain => {
1083 let position = *position;
1084 self.route_scroll_along_chain(&event, position, &mut *ops);
1085 }
1086 ScrollDelivery::Bubble => {
1092 let scrolling = self.current_input.pointer;
1093 let target = match position {
1094 Some(p) => self.hit_test_for(*p, &scrolling),
1095 None => self.hovered_id().or(self.focused),
1096 };
1097 if let Some(target) = target {
1098 self.dispatch_to_widget(target, &event, &mut *ops);
1099 }
1100 }
1101 }
1102 }
1103 WidgetEvent::KeyDown { key, modifiers, .. } => {
1104 if *key == Key::Tab {
1105 let captured_focus = self
1120 .focused
1121 .is_some_and(|focused| self.is_keyboard_capture(focused));
1122 if captured_focus && modifiers.ctrl() {
1123 self.cycle_focus(modifiers.shift(), &mut *ops);
1124 return;
1125 }
1126 let handled = self
1133 .focused
1134 .map(|focused| {
1135 self.dispatch_to_widget_returning_handled(focused, &event, &mut *ops)
1136 })
1137 .unwrap_or(false);
1138 if !handled {
1139 self.cycle_focus(modifiers.shift(), &mut *ops);
1140 }
1141 } else if let Some(focused) = self.focused {
1142 self.dispatch_to_widget(focused, &event, &mut *ops);
1143 }
1144 }
1145 WidgetEvent::KeyUp { .. }
1146 | WidgetEvent::ImeComposition { .. }
1147 | WidgetEvent::ImeCommit { .. } => {
1148 if let Some(focused) = self.focused {
1149 self.dispatch_to_widget(focused, &event, &mut *ops);
1150 }
1151 }
1152 WidgetEvent::AccessAction { target, action, .. } => {
1153 if let Some(id) = target.filter(|id| self.arena.is_active(*id)) {
1163 if *action == accesskit::Action::Focus {
1164 if self.advertises_focus_action(id) {
1188 let target = self.first_focusable_descendant(id).unwrap_or(id);
1189 self.focus_with_origin_ops(
1195 target,
1196 crate::focus::FocusOrigin::Accessibility,
1197 &mut *ops,
1198 );
1199 self.access_action_handled = self.focused == Some(target);
1202 } else {
1203 self.access_action_handled = false;
1204 }
1205 } else if *action == accesskit::Action::ShowContextMenu {
1206 self.access_action_handled =
1222 if self.dispatch_to_widget_returning_handled(id, &event, &mut *ops) {
1223 true
1224 } else {
1225 let position = self.arena.bounds(id).center();
1226 self.show_context_menu_for(id, position, &mut *ops)
1227 };
1228 } else {
1229 self.access_action_handled =
1230 self.dispatch_to_widget_returning_handled(id, &event, &mut *ops);
1231 }
1232 }
1233 }
1234 WidgetEvent::Gesture { .. } => {
1235 if let Some(target) = self.hovered_id().or(self.focused) {
1236 self.dispatch_to_widget(target, &event, &mut *ops);
1237 }
1238 }
1239 WidgetEvent::ScrollIntoView { .. }
1240 | WidgetEvent::PointerEnter { .. }
1241 | WidgetEvent::PointerLeave { .. }
1242 | WidgetEvent::FocusGained { .. }
1243 | WidgetEvent::FocusLost => {}
1244 }
1245 self.drain_pending_intents(&mut *ops);
1250 }
1251
1252 fn open_context_menu_from_keyboard(&mut self, ops: &mut dyn crate::window::WindowOps) -> bool {
1265 let Some(focused) = self.focused else {
1266 return false;
1267 };
1268 let target = self
1269 .arena
1270 .get(focused)
1271 .and_then(|node| node.widget.context_menu_key_target())
1272 .filter(|id| self.arena.is_active(*id))
1273 .unwrap_or(focused);
1274
1275 let bounds = self.bounds(target);
1279 let anchor = Point {
1280 x: bounds.x + bounds.width / 2.0,
1281 y: bounds.y + bounds.height / 2.0,
1282 };
1283 self.show_context_menu_for(target, anchor, ops)
1284 }
1285
1286 pub(super) fn show_context_menu_for(
1287 &mut self,
1288 target: WidgetId,
1289 position: Point,
1290 ops: &mut dyn crate::window::WindowOps,
1291 ) -> bool {
1292 let mut ctx = self.make_event_context(&mut *ops);
1298 let mut walker = Some(target);
1299 let menu_decision: Option<(WidgetId, Box<dyn Widget>)> = loop {
1300 let owner_id = {
1303 let mut probe = walker;
1304 loop {
1305 match probe {
1306 None => break None,
1307 Some(id) => {
1308 if self
1309 .arena
1310 .get(id)
1311 .is_some_and(|node| node.context_menu_factory.is_some())
1312 {
1313 break Some(id);
1314 }
1315 probe = self.arena.get(id).and_then(|node| node.parent);
1316 }
1317 }
1318 }
1319 };
1320 let Some(owner_id) = owner_id else {
1321 break None;
1322 };
1323 let outcome: Option<Box<dyn Widget>> = {
1330 let node = self
1331 .arena
1332 .get(owner_id)
1333 .expect("owner_id from active arena walk");
1334 let factory = node
1335 .context_menu_factory
1336 .as_ref()
1337 .expect("owner_id only set when factory present");
1338 factory(position, &mut ctx)
1339 };
1340 match outcome {
1341 Some(menu) => break Some((owner_id, menu)),
1342 None => {
1343 walker = self.arena.get(owner_id).and_then(|n| n.parent);
1345 }
1346 }
1347 };
1348
1349 let drain_anchor = menu_decision
1353 .as_ref()
1354 .map(|(id, _)| *id)
1355 .or_else(|| self.arena.roots().first().copied())
1356 .unwrap_or(target);
1357 self.collect_from_ctx(ctx, drain_anchor);
1358
1359 let Some((owner_id, menu_widget)) = menu_decision else {
1360 return false;
1361 };
1362
1363 let keep: std::collections::HashSet<WidgetId> = self
1369 .overlay_manager
1370 .stack
1371 .iter()
1372 .map(|o| o.content_id)
1373 .filter(|&content_id| self.is_descendant_of(owner_id, content_id))
1374 .collect();
1375 let dismissed = self.overlay_manager.dismiss_except(&keep);
1376 self.dormant_dismissed_content(&dismissed, &mut *ops);
1377
1378 let content_id = self.add_boxed(menu_widget);
1379 let prev_focus = self.focused;
1380 let placement =
1387 crate::overlay::OverlayPlacement::at_pointer_for(position, &self.current_input.pointer);
1388 self.overlay_manager.show(crate::overlay::OverlayRequest {
1389 content_id,
1390 anchor: owner_id,
1391 placement,
1392 dismiss: crate::overlay::DismissBehavior::EscapeOrClickOutside,
1393 layer: crate::overlay::OverlayLayer::InTree,
1394 parent_overlay: None,
1395 on_dismiss: None,
1396 fade_duration: None,
1397 });
1398 if let Some(focus_id) = prev_focus {
1399 self.overlay_manager.set_top_focus_restore(focus_id);
1400 }
1401 self.focus_ops(content_id, &mut *ops);
1402 self.drain_pending_intents(&mut *ops);
1407 true
1408 }
1409
1410 fn arm_outside_press_dismissal(
1431 &mut self,
1432 position: Point,
1433 button: PointerButton,
1434 ops: &mut dyn crate::window::WindowOps,
1435 ) -> bool {
1436 self.prune_stale_dismiss_arms();
1437 let pressing = self.current_input.pointer;
1438 let busy = self.busy_press_points(pressing.id);
1439
1440 if pressing.kind.is_direct() {
1441 return self
1442 .overlay_manager
1443 .arm_dismiss(pressing.id, position, &busy)
1444 .suppress_beneath;
1445 }
1446
1447 let (dismissed, focus_restore, toggle_anchors) =
1448 self.overlay_manager.dismiss_outside_press(position, &busy);
1449 if dismissed.is_empty() {
1450 return false;
1451 }
1452 self.dormant_dismissed_content(&dismissed, &mut *ops);
1453 if let Some(restore_id) = focus_restore
1454 && self.arena.is_active(restore_id)
1455 {
1456 self.focus_ops(restore_id, &mut *ops);
1457 }
1458 button == PointerButton::Primary
1468 && toggle_anchors.iter().any(|&anchor| {
1469 self.arena.is_active(anchor) && self.arena.bounds(anchor).contains(position)
1470 })
1471 }
1472
1473 fn commit_outside_press_dismissal(
1480 &mut self,
1481 position: Point,
1482 ops: &mut dyn crate::window::WindowOps,
1483 ) -> bool {
1484 let pointer = self.current_pointer_id();
1485 if !self.overlay_manager.has_armed_dismiss(pointer) {
1486 return false;
1487 }
1488 let (dismissed, focus_restore, _anchors) =
1489 self.overlay_manager.commit_dismiss(pointer, position);
1490 if !dismissed.is_empty() {
1491 self.dormant_dismissed_content(&dismissed, &mut *ops);
1492 if let Some(restore_id) = focus_restore
1493 && self.arena.is_active(restore_id)
1494 {
1495 self.focus_ops(restore_id, &mut *ops);
1496 }
1497 }
1498 true
1499 }
1500
1501 fn busy_press_points(&self, exclude: crate::pointer::PointerId) -> Vec<Point> {
1518 self.pointers
1519 .iter()
1520 .filter(|entry| entry.info.id != exclude)
1521 .filter(|entry| self.press_is_revocable(entry.info.id))
1522 .map(|entry| entry.down_position)
1523 .collect()
1524 }
1525
1526 fn prune_stale_dismiss_arms(&mut self) {
1533 let stale: Vec<crate::pointer::PointerId> = self
1534 .overlay_manager
1535 .armed_pointers()
1536 .into_iter()
1537 .filter(|id| self.pointers.get(*id).is_none())
1538 .collect();
1539 for id in stale {
1540 self.overlay_manager.abort_dismiss(id);
1541 }
1542 }
1543
1544 fn begin_press(&mut self, focusable: Option<WidgetId>) {
1565 let pointer = self.current_pointer_id();
1566 let now = self.sequence_now();
1567 let delay = self
1568 .pan_session_open(pointer)
1569 .then(|| self.current_profile().press_feedback_delay)
1570 .filter(|d| !d.is_zero());
1571 self.presses.press(pointer, focusable, now, delay);
1572 }
1573
1574 fn adopt_press_owner(&mut self, button: PointerButton) {
1597 let pointer = self.current_pointer_id();
1598 let Some(owner) = self.current_sequence().and_then(|s| s.pressed_owner()) else {
1599 return;
1600 };
1601 if !self.press_buttons(owner).contains(button) {
1602 crate::trace_input!(
1603 Samples,
1604 "no press visual for {owner:?}: {button:?} is not one of its accepted buttons"
1605 );
1606 return;
1607 }
1608 self.presses.set_owner(pointer, owner);
1609 self.publish_pressed(owner);
1610 }
1611
1612 fn update_press(&mut self, position: Point) {
1625 let pointer = self.current_pointer_id();
1626 if self.presses.get(pointer).is_none() {
1627 return;
1628 }
1629 let claimed_elsewhere = self.current_sequence().is_some_and(|sequence| {
1632 sequence
1633 .winner()
1634 .is_some_and(|winner| Some(winner) != sequence.pressed_owner())
1635 });
1636 if claimed_elsewhere {
1637 self.end_press(pointer);
1638 return;
1639 }
1640 let profile = self.current_profile();
1641 let origin = self.current_sequence().map(|s| s.press_origin());
1642 let owner = self.presses.get(pointer).and_then(|p| p.owner);
1643 let inside = match (origin, owner) {
1644 (Some(origin), Some(owner)) => {
1645 let bounds = self
1646 .arena
1647 .is_active(owner)
1648 .then(|| self.arena.bounds(owner));
1649 !crate::gesture::TapBoundary::for_pointer(&self.current_input.pointer, &profile)
1650 .left(origin, position, bounds, &profile)
1651 }
1652 _ => true,
1655 };
1656 let Some(press) = self.presses.get_mut(pointer) else {
1657 return;
1658 };
1659 if press.inside == inside {
1660 return;
1661 }
1662 press.inside = inside;
1663 if let Some(owner) = owner {
1664 self.publish_pressed(owner);
1665 }
1666 }
1667
1668 pub(crate) fn end_press(&mut self, pointer: crate::pointer::PointerId) {
1674 if let Some(owner) = self.presses.release(pointer) {
1675 self.publish_pressed(owner);
1676 }
1677 }
1678
1679 pub(crate) fn resolve_press_delays(&mut self, now: crate::pointer::EventTime) {
1682 if self.presses.is_empty() {
1683 return;
1684 }
1685 for id in self.presses.resolve_delays(now) {
1686 self.publish_pressed(id);
1687 self.arena.mark_needs_paint(id);
1688 }
1689 }
1690
1691 pub(crate) fn next_press_deadline(&self) -> Option<std::time::Instant> {
1694 self.presses.next_deadline().map(|t| self.instant_for(t))
1695 }
1696
1697 fn focus_on_release(&mut self, position: Point, ops: &mut dyn crate::window::WindowOps) {
1708 let releasing = self.current_input.pointer;
1709 if !releasing.kind.is_direct() {
1710 return;
1711 }
1712 let pointer = self.current_pointer_id();
1713 let Some(pressed) = self.presses.get(pointer).and_then(|p| p.focusable) else {
1714 return;
1715 };
1716 let released_on = self
1717 .hit_test_for(position, &releasing)
1718 .and_then(|target| self.find_focusable_at_or_above(target));
1719 if released_on == Some(pressed) {
1720 self.focus_with_origin_ops(
1721 pressed,
1722 crate::focus::FocusOrigin::Pointer(releasing.kind),
1723 &mut *ops,
1724 );
1725 }
1726 }
1727
1728 fn handle_pointer_move(
1748 &mut self,
1749 event: &WidgetEvent,
1750 position: Point,
1751 ops: &mut dyn crate::window::WindowOps,
1752 ) {
1753 let moving = self.current_input.pointer;
1758 if self.pointers.hover_owner_id() != Some(self.current_pointer_id()) {
1759 if let Some(target) = self.hit_test_for(position, &moving) {
1760 self.dispatch_to_widget(target, event, &mut *ops);
1761 }
1762 return;
1763 }
1764 let target = self.hit_test_for(position, &moving);
1765
1766 if target != self.hovered_id() {
1767 let hover = self.hover_transition_pointer();
1769 let previously_hovered = self.hovered_id();
1770 if let Some(old) = previously_hovered {
1771 self.dispatch_to_widget(
1772 old,
1773 &WidgetEvent::PointerLeave { pointer: hover },
1774 &mut *ops,
1775 );
1776 self.tooltip_pointer_leave(old, &mut *ops);
1777 }
1778 if let Some(new) = target {
1779 self.dispatch_to_widget(
1780 new,
1781 &WidgetEvent::PointerEnter { pointer: hover },
1782 &mut *ops,
1783 );
1784 self.tooltip_pointer_enter(new);
1785 }
1786 self.set_hovered(target);
1787 self.update_hover_within_signals(previously_hovered, target);
1788 } else if let Some(target) = target {
1789 self.tooltip_pointer_moved(target, position);
1792 }
1793
1794 if let Some(target) = target {
1795 self.dispatch_to_widget(target, event, &mut *ops);
1796 }
1797 }
1798
1799 pub(super) fn dispatch_to_widget(
1800 &mut self,
1801 target: WidgetId,
1802 event: &WidgetEvent,
1803 ops: &mut dyn crate::window::WindowOps,
1804 ) {
1805 self.dispatch_to_widget_returning_handled(target, event, ops);
1806 }
1807
1808 pub(super) fn localize_event(&self, id: WidgetId, event: &WidgetEvent) -> Option<WidgetEvent> {
1820 match event {
1821 WidgetEvent::PointerDown {
1822 position,
1823 button,
1824 modifiers,
1825 pointer,
1826 } => Some(WidgetEvent::PointerDown {
1827 position: self.arena.local_pointer_position(id, *position),
1828 button: *button,
1829 modifiers: *modifiers,
1830 pointer: *pointer,
1831 }),
1832 WidgetEvent::PointerUp {
1833 position,
1834 button,
1835 modifiers,
1836 pointer,
1837 } => Some(WidgetEvent::PointerUp {
1838 position: self.arena.local_pointer_position(id, *position),
1839 button: *button,
1840 modifiers: *modifiers,
1841 pointer: *pointer,
1842 }),
1843 WidgetEvent::PointerMove {
1844 position,
1845 modifiers,
1846 pointer,
1847 } => Some(WidgetEvent::PointerMove {
1848 position: self.arena.local_pointer_position(id, *position),
1849 modifiers: *modifiers,
1850 pointer: *pointer,
1851 }),
1852 WidgetEvent::Gesture { gesture } => Some(WidgetEvent::Gesture {
1853 gesture: self.localize_gesture(id, gesture),
1854 }),
1855 _ => None,
1861 }
1862 }
1863
1864 fn localize_gesture(&self, id: WidgetId, gesture: &GestureEvent) -> GestureEvent {
1871 let loc = |p: teksilo_canvas::Point| self.arena.local_pointer_position(id, p);
1872 let tap = |t: &TapEvent| {
1873 TapEvent::new(loc(t.position), t.button, t.modifiers).with_pointer(t.pointer)
1874 };
1875 match gesture {
1876 GestureEvent::Tap(t) => GestureEvent::Tap(tap(t)),
1877 GestureEvent::DoubleTap(t) => GestureEvent::DoubleTap(tap(t)),
1878 GestureEvent::TripleTap(t) => GestureEvent::TripleTap(tap(t)),
1879 GestureEvent::LongPress(t) => GestureEvent::LongPress(tap(t)),
1880 GestureEvent::DragStarted {
1881 position,
1882 button,
1883 pointer,
1884 } => GestureEvent::DragStarted {
1885 position: loc(*position),
1886 button: *button,
1887 pointer: *pointer,
1888 },
1889 GestureEvent::DragMoved {
1890 position,
1891 delta,
1892 pointer,
1893 } => GestureEvent::DragMoved {
1894 position: loc(*position),
1895 delta: *delta,
1896 pointer: *pointer,
1897 },
1898 GestureEvent::DragEnded { position, pointer } => GestureEvent::DragEnded {
1899 position: loc(*position),
1900 pointer: *pointer,
1901 },
1902 GestureEvent::DragCancelled {
1903 position,
1904 pointer,
1905 reason,
1906 } => GestureEvent::DragCancelled {
1907 position: loc(*position),
1908 pointer: *pointer,
1909 reason: *reason,
1910 },
1911 GestureEvent::PinchStarted { center } => GestureEvent::PinchStarted {
1912 center: loc(*center),
1913 },
1914 GestureEvent::PinchChanged {
1915 center,
1916 scale,
1917 rotation,
1918 } => GestureEvent::PinchChanged {
1919 center: loc(*center),
1920 scale: *scale,
1921 rotation: *rotation,
1922 },
1923 GestureEvent::PinchEnded => GestureEvent::PinchEnded,
1924 GestureEvent::PinchCancelled { reason } => {
1925 GestureEvent::PinchCancelled { reason: *reason }
1926 }
1927 GestureEvent::Swipe {
1928 direction,
1929 velocity,
1930 } => GestureEvent::Swipe {
1931 direction: *direction,
1932 velocity: *velocity,
1933 },
1934 }
1935 }
1936
1937 pub(super) fn dispatch_to_widget_returning_handled(
1943 &mut self,
1944 target: WidgetId,
1945 event: &WidgetEvent,
1946 ops: &mut dyn crate::window::WindowOps,
1947 ) -> bool {
1948 if !self.arena.is_enabled(target) {
1949 return false;
1950 }
1951
1952 let mut ancestors = Vec::new();
1953 let mut current = self.arena.parent(target);
1954 while let Some(id) = current {
1955 ancestors.push(id);
1956 current = self.arena.parent(id);
1957 }
1958 ancestors.reverse();
1959
1960 let tap_owner: Option<WidgetId> = if matches!(
1978 event,
1979 WidgetEvent::PointerDown { .. } | WidgetEvent::PointerUp { .. }
1980 ) {
1981 let mut owner = None;
1982 let mut cur = Some(target);
1983 while let Some(id) = cur {
1984 if self.arena.get(id).is_some_and(|n| {
1985 n.any_handler(|h| h.on_tap.is_some() || h.on_long_press.is_some())
1986 }) {
1987 owner = Some(id);
1988 break;
1989 }
1990 cur = self.arena.parent(id);
1991 }
1992 owner
1993 } else {
1994 None
1995 };
1996
1997 for &id in &ancestors {
1998 let mut ctx = self
1999 .make_event_context(&mut *ops)
2000 .with_dispatch_node(id)
2001 .with_dispatch_target(target);
2002 ctx.press_claimed_by_interactive_child =
2003 tap_owner.is_some_and(|owner| owner != id && self.is_descendant_of(owner, id));
2004 let localized = self.localize_event(id, event);
2007 let event = localized.as_ref().unwrap_or(event);
2008 let response = if let Some(node) = self.arena.get_mut(id) {
2009 Self::try_handler_preview(node, event, &mut ctx).unwrap_or(EventResponse::Ignored)
2010 } else {
2011 EventResponse::Ignored
2012 };
2013 self.collect_from_ctx(ctx, id);
2014 if response == EventResponse::Handled {
2015 self.arena.mark_needs_paint(id);
2016 if matches!(event, WidgetEvent::PointerDown { .. }) {
2024 self.note_preview_claim(id);
2025 }
2026 return true;
2027 }
2028 }
2029
2030 let needs_layout_on_handle = matches!(
2031 event,
2032 WidgetEvent::Scroll { .. } | WidgetEvent::ScrollIntoView { .. }
2033 );
2034 let mut current = Some(target);
2035 let mut is_target = true;
2036 while let Some(id) = current {
2037 let mut ctx = self
2038 .make_event_context(&mut *ops)
2039 .with_dispatch_node(id)
2040 .with_dispatch_target(target);
2041 ctx.press_claimed_by_interactive_child =
2042 tap_owner.is_some_and(|owner| owner != id && self.is_descendant_of(owner, id));
2043 let localized = self.localize_event(id, event);
2046 let gesture_cx = self.recognizer_context(id);
2047 let arena_blocked = self.sequence_blocks_arena(id);
2050 let WidgetTree {
2051 arena,
2052 gesture_owners,
2053 ..
2054 } = self;
2055 let event = localized.as_ref().unwrap_or(event);
2056 let response = if let Some(node) = arena.get_mut(id) {
2057 Self::try_handler_bubble(
2058 node,
2059 event,
2060 &mut ctx,
2061 BubbleGates {
2062 fire_on_pointer_event: is_target,
2063 arena_blocked,
2064 },
2065 id,
2066 gesture_owners,
2067 gesture_cx,
2068 )
2069 .unwrap_or(EventResponse::Ignored)
2070 } else {
2071 EventResponse::Ignored
2072 };
2073 self.collect_from_ctx(ctx, id);
2074 if response == EventResponse::Handled {
2075 if needs_layout_on_handle {
2076 self.arena.mark_needs_layout(id);
2077 } else {
2078 self.arena.mark_needs_paint(id);
2079 }
2080 self.note_pointer_acceptance(id, event);
2081 if !matches!(
2096 event,
2097 WidgetEvent::PointerEnter { .. } | WidgetEvent::PointerLeave { .. }
2098 ) {
2099 return true;
2100 }
2101 }
2102 is_target = false;
2103 current = self.arena.parent(id);
2104 }
2105 false
2106 }
2107
2108 pub(super) fn dispatch_to_widget_direct(
2109 &mut self,
2110 target: WidgetId,
2111 event: &WidgetEvent,
2112 ops: &mut dyn crate::window::WindowOps,
2113 ) {
2114 self.dispatch_to_widget_direct_returning_handled(target, event, ops);
2115 }
2116
2117 pub(super) fn dispatch_to_widget_direct_returning_handled(
2127 &mut self,
2128 target: WidgetId,
2129 event: &WidgetEvent,
2130 ops: &mut dyn crate::window::WindowOps,
2131 ) -> bool {
2132 if !self.arena.is_enabled(target) {
2133 return false;
2134 }
2135
2136 let mut ctx = self
2137 .make_event_context(&mut *ops)
2138 .with_dispatch_node(target)
2139 .with_dispatch_target(target);
2140 let gesture_cx = self.recognizer_context(target);
2141 let arena_blocked = self.sequence_blocks_arena(target);
2142 let WidgetTree {
2143 arena,
2144 gesture_owners,
2145 ..
2146 } = self;
2147 let response = if let Some(node) = arena.get_mut(target) {
2148 Self::try_handler_bubble(
2149 node,
2150 event,
2151 &mut ctx,
2152 BubbleGates {
2153 fire_on_pointer_event: true,
2154 arena_blocked,
2155 },
2156 target,
2157 gesture_owners,
2158 gesture_cx,
2159 )
2160 .unwrap_or(EventResponse::Ignored)
2161 } else {
2162 EventResponse::Ignored
2163 };
2164 self.collect_from_ctx(ctx, target);
2165
2166 if response == EventResponse::Handled {
2167 if matches!(
2170 event,
2171 WidgetEvent::Scroll { .. } | WidgetEvent::ScrollIntoView { .. }
2172 ) {
2173 self.arena.mark_needs_layout(target);
2174 } else {
2175 self.arena.mark_needs_paint(target);
2176 }
2177 self.note_pointer_acceptance(target, event);
2178 }
2179 response == EventResponse::Handled
2180 }
2181
2182 pub(super) fn note_pointer_acceptance(&mut self, target: WidgetId, event: &WidgetEvent) {
2191 if !matches!(
2192 event,
2193 WidgetEvent::PointerDown { .. }
2194 | WidgetEvent::PointerMove { .. }
2195 | WidgetEvent::PointerUp { .. }
2196 ) {
2197 return;
2198 }
2199 let pointer = self.current_pointer_id();
2200 if let Some(entry) = self.pointers.get_mut(pointer) {
2201 entry.last_accepted = Some(target);
2202 }
2203 }
2204
2205 fn try_handler_preview(
2206 node: &mut crate::arena::WidgetNode,
2207 event: &WidgetEvent,
2208 ctx: &mut EventContext,
2209 ) -> Option<EventResponse> {
2210 match event {
2211 WidgetEvent::KeyDown { .. }
2219 | WidgetEvent::KeyUp { .. }
2220 | WidgetEvent::ImeComposition { .. }
2221 | WidgetEvent::ImeCommit { .. } => {
2222 let has = node.external_handlers.on_key_preview.is_some()
2223 || node.handlers.on_key_preview.is_some();
2224 if !has {
2225 return None;
2226 }
2227 Some(fire_event_handler_both(
2228 &mut node.external_handlers.on_key_preview,
2229 &mut node.handlers.on_key_preview,
2230 event,
2231 ctx,
2232 ))
2233 }
2234 WidgetEvent::PointerEnter { .. } | WidgetEvent::PointerLeave { .. } => None,
2246 _ => {
2247 let has = node.external_handlers.on_pointer_event.is_some()
2248 || node.handlers.on_pointer_event.is_some();
2249 if !has {
2250 return None;
2251 }
2252 Some(fire_event_handler_both(
2253 &mut node.external_handlers.on_pointer_event,
2254 &mut node.handlers.on_pointer_event,
2255 event,
2256 ctx,
2257 ))
2258 }
2259 }
2260 }
2261
2262 fn try_handler_bubble(
2263 node: &mut crate::arena::WidgetNode,
2264 event: &WidgetEvent,
2265 ctx: &mut EventContext,
2266 gates: BubbleGates,
2267 node_id: WidgetId,
2268 gesture_owners: &mut std::collections::HashSet<WidgetId>,
2269 gesture_cx: crate::gesture::RecognizerContext<'_>,
2270 ) -> Option<EventResponse> {
2271 let BubbleGates {
2272 fire_on_pointer_event,
2273 arena_blocked,
2274 } = gates;
2275 match event {
2276 WidgetEvent::PointerEnter { .. } => {
2277 if let Some(cursor) = node.node_cursor {
2278 ctx.declared_cursor_request = Some(cursor);
2283 }
2284 let mut fired = false;
2285 if let Some(h) = node.external_handlers.on_hover.as_mut() {
2286 h(true, ctx);
2287 fired = true;
2288 }
2289 if let Some(h) = node.handlers.on_hover.as_mut() {
2290 h(true, ctx);
2291 fired = true;
2292 }
2293 if fired {
2294 Some(EventResponse::Handled)
2295 } else {
2296 node.node_cursor.map(|_| EventResponse::Handled)
2297 }
2298 }
2299 WidgetEvent::PointerLeave { .. } => {
2300 if node.node_cursor.is_some() {
2301 ctx.declared_cursor_request = Some(crate::widget::CursorIcon::Default);
2302 }
2303 let mut fired = false;
2304 if let Some(h) = node.external_handlers.on_hover.as_mut() {
2305 h(false, ctx);
2306 fired = true;
2307 }
2308 if let Some(h) = node.handlers.on_hover.as_mut() {
2309 h(false, ctx);
2310 fired = true;
2311 }
2312 if fired {
2313 Some(EventResponse::Handled)
2314 } else {
2315 node.node_cursor.map(|_| EventResponse::Handled)
2316 }
2317 }
2318 WidgetEvent::FocusGained { .. } => {
2319 let mut fired = false;
2320 if let Some(h) = node.external_handlers.on_focus.as_mut() {
2321 h(true, ctx);
2322 fired = true;
2323 }
2324 if let Some(h) = node.handlers.on_focus.as_mut() {
2325 h(true, ctx);
2326 fired = true;
2327 }
2328 fired.then_some(EventResponse::Handled)
2329 }
2330 WidgetEvent::FocusLost => {
2331 let mut fired = false;
2332 if let Some(h) = node.external_handlers.on_focus.as_mut() {
2333 h(false, ctx);
2334 fired = true;
2335 }
2336 if let Some(h) = node.handlers.on_focus.as_mut() {
2337 h(false, ctx);
2338 fired = true;
2339 }
2340 fired.then_some(EventResponse::Handled)
2341 }
2342 WidgetEvent::KeyDown { .. }
2343 | WidgetEvent::KeyUp { .. }
2344 | WidgetEvent::ImeComposition { .. }
2345 | WidgetEvent::ImeCommit { .. } => {
2346 if node.external_handlers.on_key.is_some() || node.handlers.on_key.is_some() {
2347 Some(fire_event_handler_both(
2348 &mut node.external_handlers.on_key,
2349 &mut node.handlers.on_key,
2350 event,
2351 ctx,
2352 ))
2353 } else {
2354 None
2355 }
2356 }
2357 WidgetEvent::Scroll { .. } | WidgetEvent::ScrollIntoView { .. } => {
2358 if node.external_handlers.on_scroll.is_some() || node.handlers.on_scroll.is_some() {
2359 Some(fire_event_handler_both(
2360 &mut node.external_handlers.on_scroll,
2361 &mut node.handlers.on_scroll,
2362 event,
2363 ctx,
2364 ))
2365 } else {
2366 None
2367 }
2368 }
2369 WidgetEvent::AccessAction {
2370 action,
2371 target_node,
2372 data,
2373 ..
2374 } => {
2375 let saved_a11y_source = ctx
2395 .current_source
2396 .replace(crate::telemetry::IntentSource::Accessibility);
2397 let mut any_slot = false;
2398 let mut any_handled = false;
2399 if let Some(h) = node.external_handlers.on_access_action_request.as_mut() {
2400 any_slot = true;
2401 any_handled |=
2402 h(*action, *target_node, data.clone(), ctx) == EventResponse::Handled;
2403 }
2404 if let Some(h) = node.handlers.on_access_action_request.as_mut() {
2405 any_slot = true;
2406 any_handled |=
2407 h(*action, *target_node, data.clone(), ctx) == EventResponse::Handled;
2408 }
2409 if let Some(h) = node.external_handlers.on_access_action.as_mut() {
2410 any_slot = true;
2411 any_handled |= h(*action, ctx) == EventResponse::Handled;
2412 }
2413 if let Some(h) = node.handlers.on_access_action.as_mut() {
2414 any_slot = true;
2415 any_handled |= h(*action, ctx) == EventResponse::Handled;
2416 }
2417 let user_handled = any_slot.then_some(if any_handled {
2418 EventResponse::Handled
2419 } else {
2420 EventResponse::Ignored
2421 });
2422
2423 let mut override_handled = false;
2429 if let Some(ov) = node.access_overrides.as_deref_mut() {
2430 if matches!(action, accesskit::Action::CustomAction) {
2431 if let Some(accesskit::ActionData::CustomAction(idx)) = data
2432 && let Some((_, cb)) = ov.custom_actions.get_mut(*idx as usize)
2433 {
2434 cb(ctx);
2435 override_handled = true;
2436 }
2437 } else {
2438 for (a, cb) in ov.actions.iter_mut() {
2439 if *a == *action {
2440 cb(ctx);
2441 override_handled = true;
2442 }
2443 }
2444 }
2445 }
2446
2447 ctx.current_source = saved_a11y_source;
2448 match (user_handled, override_handled) {
2449 (Some(EventResponse::Handled), _) | (_, true) => Some(EventResponse::Handled),
2450 (Some(EventResponse::Ignored), false) => Some(EventResponse::Ignored),
2451 (None, false) => None,
2452 }
2453 }
2454 WidgetEvent::Gesture { gesture } => {
2455 let matched = matches!(
2459 gesture,
2460 GestureEvent::PinchStarted { .. }
2461 | GestureEvent::PinchChanged { .. }
2462 | GestureEvent::PinchEnded
2463 | GestureEvent::Swipe { .. }
2464 | GestureEvent::DoubleTap { .. }
2465 | GestureEvent::TripleTap { .. }
2466 ) && {
2467 let has_handler = match gesture {
2468 GestureEvent::PinchStarted { .. }
2469 | GestureEvent::PinchChanged { .. }
2470 | GestureEvent::PinchEnded => node.any_handler(|h| h.on_pinch.is_some()),
2471 GestureEvent::Swipe { .. } => node.any_handler(|h| h.on_swipe.is_some()),
2472 GestureEvent::DoubleTap { .. } => {
2473 node.any_handler(|h| h.on_double_tap.is_some())
2474 }
2475 GestureEvent::TripleTap { .. } => {
2476 node.any_handler(|h| h.on_triple_tap.is_some())
2477 }
2478 _ => false,
2479 };
2480 if has_handler {
2481 Self::dispatch_recognized_gesture(node, *gesture, ctx);
2482 }
2483 has_handler
2484 };
2485 if matched {
2486 Some(EventResponse::Handled)
2487 } else {
2488 None
2489 }
2490 }
2491 WidgetEvent::PointerDown {
2492 position,
2493 button,
2494 modifiers,
2495 ..
2496 } => {
2497 if fire_on_pointer_event {
2504 let r = fire_event_handler_both(
2505 &mut node.external_handlers.on_pointer_event,
2506 &mut node.handlers.on_pointer_event,
2507 event,
2508 ctx,
2509 );
2510 if r == EventResponse::Handled {
2511 return Some(EventResponse::Handled);
2512 }
2513 }
2514 if arena_blocked {
2515 return None;
2519 }
2520 Self::ensure_gesture_arena(node, node_id, gesture_owners);
2521 if let Some(arena) = node.handlers.gesture_arena.as_mut() {
2522 let cx = gesture_cx;
2523 ctx.capture_pointer_implicit();
2538 let result = arena.process(
2539 &RawPointerEvent::Down {
2540 position: *position,
2541 button: *button,
2542 modifiers: *modifiers,
2543 pointer: cx.pointer,
2544 time: cx.now,
2545 },
2546 &cx,
2547 );
2548 if let Some(gesture) = result {
2549 Self::dispatch_recognized_gesture(node, gesture, ctx);
2550 }
2551 return Some(EventResponse::Handled);
2552 }
2553 None
2554 }
2555 WidgetEvent::PointerUp {
2556 position,
2557 button,
2558 modifiers,
2559 ..
2560 } => {
2561 if fire_on_pointer_event {
2562 let r = fire_event_handler_both(
2563 &mut node.external_handlers.on_pointer_event,
2564 &mut node.handlers.on_pointer_event,
2565 event,
2566 ctx,
2567 );
2568 if r == EventResponse::Handled {
2569 return Some(EventResponse::Handled);
2570 }
2571 }
2572 if arena_blocked {
2573 return None;
2574 }
2575 if let Some(arena) = node.handlers.gesture_arena.as_mut() {
2576 let cx = gesture_cx;
2577 let result = arena.process(
2578 &RawPointerEvent::Up {
2579 position: *position,
2580 button: *button,
2581 modifiers: *modifiers,
2582 pointer: cx.pointer,
2583 time: cx.now,
2584 },
2585 &cx,
2586 );
2587 if let Some(gesture) = result {
2588 Self::dispatch_recognized_gesture(node, gesture, ctx);
2589 }
2590 return Some(EventResponse::Handled);
2591 }
2592 None
2593 }
2594 WidgetEvent::PointerMove { position, .. } => {
2595 if fire_on_pointer_event {
2596 let r = fire_event_handler_both(
2597 &mut node.external_handlers.on_pointer_event,
2598 &mut node.handlers.on_pointer_event,
2599 event,
2600 ctx,
2601 );
2602 if r == EventResponse::Handled {
2603 return Some(EventResponse::Handled);
2604 }
2605 }
2606 if arena_blocked {
2607 return None;
2608 }
2609 if let Some(arena) = node.handlers.gesture_arena.as_mut() {
2610 let cx = gesture_cx;
2611 let result = arena.process(
2612 &RawPointerEvent::Move {
2613 position: *position,
2614 pointer: cx.pointer,
2615 time: cx.now,
2616 },
2617 &cx,
2618 );
2619 if let Some(gesture) = result {
2620 Self::dispatch_recognized_gesture(node, gesture, ctx);
2621 return Some(EventResponse::Handled);
2633 }
2634 return Some(EventResponse::Ignored);
2635 }
2636 None
2637 }
2638 WidgetEvent::PointerCancel {
2639 reason, pointer, ..
2640 } => {
2641 for slot in [
2649 &mut node.external_handlers.on_pointer_cancel,
2650 &mut node.handlers.on_pointer_cancel,
2651 ] {
2652 if let Some(handler) = slot.as_mut() {
2653 handler(pointer, *reason, ctx);
2654 }
2655 }
2656 if fire_on_pointer_event {
2657 let r = fire_event_handler_both(
2658 &mut node.external_handlers.on_pointer_event,
2659 &mut node.handlers.on_pointer_event,
2660 event,
2661 ctx,
2662 );
2663 if r == EventResponse::Handled {
2664 return Some(EventResponse::Handled);
2665 }
2666 }
2667 None
2668 }
2669 }
2670 }
2671
2672 pub(super) fn collect_from_ctx<'ops>(
2673 &mut self,
2674 mut ctx: EventContext<'ops>,
2675 source_widget: WidgetId,
2676 ) {
2677 let local_ops = ctx.window_ops.take();
2683 let mut noop = crate::window::NoopWindowOps;
2684 let ops: &mut dyn crate::window::WindowOps = match local_ops {
2685 Some(o) => o,
2686 None => &mut noop,
2687 };
2688 if ctx.frame_requested {
2689 self.request_frame();
2690 }
2691 if let Some(declared) = ctx.declared_cursor_request {
2695 self.node_declared_cursor = declared;
2696 self.current_cursor = declared;
2697 }
2698 match ctx.cursor_request {
2699 Some(crate::widget::CursorRequest::Set(cursor)) => {
2700 self.current_cursor = cursor;
2701 }
2702 Some(crate::widget::CursorRequest::Release) => {
2706 self.current_cursor = self.node_declared_cursor;
2707 }
2708 None => {}
2709 }
2710 for intent in ctx.pending_intents {
2716 self.enqueue_intent(source_widget, intent, true);
2717 }
2718 if ctx.cancel_key_capture {
2727 self.cancel_key_capture();
2728 }
2729 if let Some(slot) = ctx.pending_key_capture {
2730 self.key_capture = Some(slot);
2731 }
2732 for mutation in ctx.pending_shortcut_mutations {
2734 match mutation {
2735 crate::widget::ShortcutMutation::RebindPrimary { id, keystroke } => {
2736 self.shortcut_registry.rebind_primary(id, keystroke);
2737 }
2738 crate::widget::ShortcutMutation::RebindSecondary { id, keystroke } => {
2739 self.shortcut_registry.rebind_secondary(id, keystroke);
2740 }
2741 crate::widget::ShortcutMutation::ClearOverride { id } => {
2742 self.shortcut_registry.clear_override(&id);
2743 }
2744 }
2745 }
2746 if ctx.close_window_requested {
2747 self.close_window_requested = true;
2748 }
2749 if ctx.force_close_requested {
2750 self.force_close_requested = true;
2751 }
2752 self.pending_modal_requests
2753 .extend(ctx.modal_requests.into_iter().map(|request| {
2754 crate::modal::QueuedModalRequest {
2755 source_widget,
2756 request,
2757 }
2758 }));
2759 if ctx.dismiss_modal && !self.dismiss_modal_for_source(source_widget, &mut *ops) {
2760 self.pending_modal_dismissal = true;
2761 }
2762 for callback in ctx.idle_callbacks {
2763 self.idle_queue.push_boxed(callback);
2764 }
2765 match ctx.dismiss_scope {
2766 Some(crate::widget::DismissScope::All) => {
2767 let dismissed = self.overlay_manager.dismiss_all();
2768 self.dormant_dismissed_content(&dismissed, &mut *ops);
2769 }
2770 Some(crate::widget::DismissScope::AllExceptHosts) => {
2771 self.dismiss_all_overlays_except_hosts(&mut *ops);
2772 }
2773 Some(crate::widget::DismissScope::SelfChain) => {
2774 self.dismiss_self_overlay_chain_for_source(source_widget, &mut *ops);
2775 }
2776 Some(crate::widget::DismissScope::Top) => {
2777 if let Some((_id, content_ids, focus_restore)) = self.overlay_manager.dismiss_top()
2778 {
2779 self.dormant_dismissed_content(&content_ids, &mut *ops);
2780 if let Some(restore_id) = focus_restore
2781 && self.arena.is_active(restore_id)
2782 {
2783 self.focus_ops(restore_id, &mut *ops);
2784 }
2785 }
2786 }
2787 None => {
2788 for id in ctx.overlay_dismissals {
2789 let dismissed = self.overlay_manager.dismiss(id);
2790 self.dormant_dismissed_content(&dismissed, &mut *ops);
2791 }
2792 }
2793 }
2794 for content_id in ctx.overlay_content_dismissals {
2801 if let Some(overlay_id) = self.overlay_manager.find_by_content(content_id) {
2802 let dismissed = self.overlay_manager.dismiss(overlay_id);
2803 self.dormant_dismissed_content(&dismissed, &mut *ops);
2804 }
2805 }
2806 for (id, pause) in ctx.overlay_pause_requests {
2813 if pause {
2814 self.overlay_manager.pause_auto_dismiss(id);
2815 } else {
2816 self.overlay_manager.resume_auto_dismiss(id);
2817 }
2818 }
2819 for preserve_content in ctx.dismiss_descendant_overlays {
2820 self.dismiss_child_overlays_for_source(source_widget, preserve_content, &mut *ops);
2821 }
2822 let deferred_row_activations =
2823 self.apply_tree_mutations(std::mem::take(&mut ctx.tree_mutations));
2824 if ctx.request_a11y_update {
2825 self.a11y_dirty = true;
2826 }
2827 if let Some(visible) = ctx.soft_keyboard_request.take() {
2828 self.request_soft_keyboard(visible);
2829 }
2830 for (message, politeness) in std::mem::take(&mut ctx.announcements) {
2833 self.announce_with(message, politeness);
2834 }
2835 for mut req in ctx.overlay_requests {
2836 if req.parent_overlay.is_none() {
2837 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
2838 }
2839 if self
2840 .overlay_manager
2841 .find_by_content(req.content_id)
2842 .is_some()
2843 {
2844 continue;
2845 }
2846 let current_focus = self.focused;
2847 self.overlay_manager.show(req);
2848 self.a11y_dirty = true;
2852 if let Some(focus_id) = current_focus {
2853 self.overlay_manager.set_top_focus_restore(focus_id);
2854 }
2855 }
2856 for (mut req, band) in ctx.overlay_band_requests {
2857 if req.parent_overlay.is_none() {
2858 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
2859 }
2860 if self
2861 .overlay_manager
2862 .find_by_content(req.content_id)
2863 .is_some()
2864 {
2865 continue;
2866 }
2867 let content_id = req.content_id;
2868 self.overlay_manager.show_in_band(req, band);
2869 self.arena.activate(content_id);
2870 self.a11y_dirty = true;
2871 }
2875 for (content_id, placement) in ctx.overlay_placement_updates {
2878 if let Some(overlay_id) = self.overlay_manager.find_by_content(content_id) {
2879 self.overlay_manager.update_placement(overlay_id, placement);
2880 }
2881 }
2882 for (mut req, duration) in ctx.timed_overlay_requests {
2883 if req.parent_overlay.is_none() {
2884 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
2885 }
2886 if self
2887 .overlay_manager
2888 .find_by_content(req.content_id)
2889 .is_some()
2890 {
2891 continue;
2892 }
2893 let current_focus = self.focused;
2894 let overlay_id = self.overlay_manager.show_for(req, duration);
2895 self.overlay_manager
2896 .set_shown_at_sim(overlay_id, self.sim_clock);
2897 self.a11y_dirty = true;
2898 if let Some(focus_id) = current_focus {
2899 self.overlay_manager.set_top_focus_restore(focus_id);
2900 }
2901 }
2902 for (mut req, progress, duration) in ctx.reveal_overlay_requests {
2903 if req.parent_overlay.is_none() {
2904 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
2905 }
2906 if self
2907 .overlay_manager
2908 .find_by_content(req.content_id)
2909 .is_some()
2910 {
2911 continue;
2912 }
2913 let content_id = req.content_id;
2914 let current_focus = self.focused;
2915 let overlay_id = self.overlay_manager.show(req);
2916 self.overlay_manager
2917 .set_shown_at_sim(overlay_id, self.sim_clock);
2918 self.a11y_dirty = true;
2919 if let Some(focus_id) = current_focus {
2920 self.overlay_manager.set_top_focus_restore(focus_id);
2921 }
2922 self.register_animated_signal(&progress, content_id);
2928 let _ = progress.try_animate_with_options(crate::animation::AnimationRequest {
2929 target: 1.0,
2930 duration,
2931 easing: teksilo_tokens::Easing::EaseOut,
2932 frame_interval: None,
2933 looping: false,
2934 epsilon: 0.0,
2935 max_duration: None,
2936 });
2937 self.overlay_manager
2938 .attach_fade(overlay_id, progress, duration);
2939 }
2940 if let Some((pointer, capture)) = ctx.pointer_capture {
2941 let named = pointer;
2945 let pointer = pointer.unwrap_or_else(|| self.current_pointer_id());
2946 self.set_pointer_capture(pointer, capture.then_some(source_widget));
2947 if capture && ctx.explicit_capture && named.is_none() {
2951 self.note_explicit_capture(source_widget);
2952 }
2953 }
2954 if let Some(activation) = ctx.drag_activation_override.take() {
2955 self.note_drag_activation_override(source_widget, activation);
2959 }
2960 if ctx.recognized_owning_gesture {
2961 self.note_gesture_recognized(source_widget);
2964 }
2965 if !ctx.gesture_acts.is_empty() {
2966 let acts = std::mem::take(&mut ctx.gesture_acts);
2967 self.apply_gesture_acts(&acts, source_widget);
2968 }
2969 if let Some(reason) = ctx.cancel_pointer_request {
2970 let pointer = self.current_pointer_id();
2971 self.cancel_pointer(pointer, reason, &mut *ops);
2972 }
2973 for (mut request, delay, focus_target, replace_siblings) in ctx.delayed_overlay_requests {
2974 if request.parent_overlay.is_none() {
2975 request.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
2976 }
2977 if self
2978 .overlay_manager
2979 .find_by_content(request.content_id)
2980 .is_some()
2981 {
2982 continue;
2983 }
2984 let content_id = request.content_id;
2985 self.pending_delayed_overlays
2986 .retain(|pending| pending.request.content_id != content_id);
2987 self.pending_delayed_overlays.push(PendingDelayedOverlay {
2988 request,
2989 delay,
2990 focus_target,
2991 replace_siblings,
2992 real_requested_at: std::time::Instant::now(),
2993 sim_requested_at: self.sim_clock,
2994 });
2995 self.arena.mark_needs_paint(source_widget);
2996 }
2997 for content_id in ctx.cancel_delayed_overlays {
2998 self.pending_delayed_overlays
2999 .retain(|pending| pending.request.content_id != content_id);
3000 }
3001 for content_id in ctx.safe_region_arm_requests {
3008 if let Some(apex) = self
3009 .previous_pointer_position
3010 .or_else(|| self.hover_owner_position())
3011 {
3012 self.overlay_manager.arm_safe_region(
3013 content_id,
3014 apex,
3015 std::time::Instant::now(),
3016 self.sim_clock,
3017 );
3018 }
3019 }
3020 for id in ctx.repaint_requests {
3021 self.arena.mark_needs_paint(id);
3022 }
3023 for id in ctx.synthetic_clicks {
3024 self.synthesise_tap_with_ops(id, &mut *ops);
3029 }
3030 if let Some(&id) = ctx.focus_requests.last() {
3031 let target = self.first_focusable_descendant(id).unwrap_or(id);
3041 self.focus_ops(target, &mut *ops);
3042 }
3043 if let Some(&id) = ctx.focus_into_requests.last() {
3044 if let Some(target) = self.first_focusable_descendant(id) {
3051 self.focus_ops(target, &mut *ops);
3052 }
3053 }
3054
3055 for req in ctx.scroll_into_view_requests {
3064 self.scroll_rect_into_view(
3065 req.from.unwrap_or(source_widget),
3068 req.rect,
3069 req.margin,
3070 req.align,
3071 req.motion,
3072 &mut *ops,
3073 );
3074 }
3075 for (id, margin) in ctx.scroll_widget_into_view_requests {
3082 if self.arena.get(id).is_some() {
3083 let bounds = self.arena.bounds(id);
3084 self.scroll_rect_into_view(
3085 id,
3086 bounds,
3087 margin,
3088 crate::event::ScrollAlign::Minimal,
3089 crate::event::ScrollMotion::Instant,
3090 &mut *ops,
3091 );
3092 }
3093 }
3094
3095 if let Some(&id) = ctx.highlight_tooltip_requests.last() {
3100 self.show_highlight_tooltip(id, &mut *ops);
3101 }
3102
3103 if let Some((source_widget, payload, preview_widget)) = ctx.drag_start_request {
3105 let (preview_content_id, preview_overlay_id) = if let Some(preview) = preview_widget {
3106 let content_id = self.add_boxed(preview);
3113 let overlay_id = self.overlay_manager.show(crate::overlay::OverlayRequest {
3114 content_id,
3115 anchor: source_widget,
3116 placement: crate::overlay::OverlayPlacement::AtPointer(
3117 teksilo_canvas::Point::ZERO,
3118 ),
3119 dismiss: crate::overlay::DismissBehavior::Manual,
3120 layer: crate::overlay::OverlayLayer::InTree,
3121 parent_overlay: None,
3122 on_dismiss: None,
3123 fade_duration: None,
3124 });
3125 self.arena.mark_needs_layout(content_id);
3129 (Some(content_id), Some(overlay_id))
3130 } else {
3131 (None, None)
3132 };
3133 self.active_drag = Some(crate::drag_state::DragSession {
3134 payload,
3135 pointer: self.current_input.pointer,
3141 source_widget: Some(source_widget),
3142 is_external: false,
3143 current_position: teksilo_canvas::Point::ZERO,
3144 current_target: None,
3145 feedback: crate::drag_state::DropFeedback::NoFeedback,
3146 preview_content_id,
3147 preview_overlay_id,
3148 });
3149 self.set_current_pointer_capture(Some(source_widget));
3150 self.current_cursor = crate::widget::CursorIcon::Grabbing;
3153 }
3154 if ctx.cancel_drag {
3155 self.cancel_active_drag(&mut *ops);
3156 }
3157
3158 if let Some(theme) = ctx.theme_request {
3160 self.pending_theme_request = Some(theme);
3166 }
3167 if ctx.follow_system_request {
3168 self.pending_follow_system_request = true;
3172 }
3173 if let Some(locale) = ctx.locale_request {
3174 self.pending_locale_request = Some(locale);
3180 }
3181 if let Some(scale) = ctx.text_scale_request {
3182 self.pending_text_scale_request = Some(scale);
3187 }
3188 if !deferred_row_activations.is_empty() {
3195 self.run_with_event_context(&mut *ops, move |ctx| {
3196 for action in deferred_row_activations {
3197 action(ctx);
3198 }
3199 });
3200 }
3201 }
3202
3203 #[must_use]
3209 fn apply_tree_mutations(
3210 &mut self,
3211 mutations: Vec<crate::widget::TreeMutation>,
3212 ) -> Vec<std::rc::Rc<dyn Fn(&mut crate::widget::EventContext)>> {
3213 let mut deferred_row_activations = Vec::new();
3214 use crate::binding::BindingLevel;
3215 use crate::widget::TreeMutation;
3216
3217 for mutation in mutations {
3218 match mutation {
3219 TreeMutation::SetDormant(id) => {
3220 self.park_subtree(id);
3221 }
3222 TreeMutation::Activate(id) => self.arena.activate(id),
3223 TreeMutation::Destroy(id) => {
3224 if let Some(overlay_id) = self.overlay_manager().find_by_content(id) {
3237 self.dismiss_overlay(overlay_id);
3238 }
3239 self.destroy_subtree(id);
3240 }
3241 TreeMutation::MaterializeNow(id) => {
3245 if self.arena.get(id).is_some() {
3246 self.rebuild_single_widget(id);
3247 }
3248 }
3249 TreeMutation::RowSpaceActivate { row, fallback } => {
3250 deferred_row_activations.push(self.keyboard_toggle_in(row).unwrap_or(fallback));
3260 }
3261 TreeMutation::WithWidgetMut { id, dirty, apply } => {
3262 let existed = if let Some(any) =
3267 self.arena.get_mut(id).and_then(|n| n.widget.as_any_mut())
3268 {
3269 apply(any);
3270 true
3271 } else {
3272 false
3273 };
3274 if existed {
3275 match dirty {
3276 BindingLevel::RepaintOnly => self.arena.mark_needs_paint(id),
3277 BindingLevel::SubtreeRepaint => self.arena.mark_subtree_needs_paint(id),
3278 BindingLevel::Relayout => {
3279 self.arena.mark_needs_layout(id);
3280 self.arena.mark_ancestors_need_layout(id);
3281 }
3282 BindingLevel::Rebuild => {
3283 self.arena.mark_needs_rebuild(id);
3284 self.arena.mark_ancestors_need_layout(id);
3285 }
3286 BindingLevel::AccessibilityOnly => self.a11y_dirty = true,
3287 }
3288 }
3289 }
3290 }
3291 }
3292 deferred_row_activations
3293 }
3294
3295 pub fn hit_test(&self, point: Point) -> Option<WidgetId> {
3304 self.hit_test_excluding_overlay_and_widget(point, None, None)
3305 }
3306
3307 pub fn hit_test_for(
3318 &self,
3319 point: Point,
3320 pointer: &crate::pointer::PointerInfo,
3321 ) -> Option<WidgetId> {
3322 self.hit_test_for_excluding(point, pointer, None, None)
3323 }
3324
3325 pub fn hit_test_for_excluding(
3328 &self,
3329 point: Point,
3330 pointer: &crate::pointer::PointerInfo,
3331 exclude_overlay: Option<crate::overlay::OverlayId>,
3332 exclude_widget: Option<WidgetId>,
3333 ) -> Option<WidgetId> {
3334 let surfaces = self.text_surfaces();
3335 let read_only = |id: WidgetId| surfaces.is_read_only(id);
3336 let hit =
3337 crate::pointer::hit_slop::HitContext::new(pointer.kind, &self.effective_theme.input)
3338 .direction(self.layout_direction)
3339 .read_only_probe(&read_only);
3340 self.hit_test_with(point, exclude_overlay, exclude_widget, &hit)
3341 }
3342
3343 pub fn hit_test_excluding_overlay_and_widget(
3350 &self,
3351 point: Point,
3352 exclude_overlay: Option<crate::overlay::OverlayId>,
3353 exclude_widget: Option<WidgetId>,
3354 ) -> Option<WidgetId> {
3355 self.hit_test_with(
3356 point,
3357 exclude_overlay,
3358 exclude_widget,
3359 &crate::pointer::hit_slop::HitContext::mouse(),
3360 )
3361 }
3362
3363 pub(crate) fn hit_test_with(
3366 &self,
3367 point: Point,
3368 exclude_overlay: Option<crate::overlay::OverlayId>,
3369 exclude_widget: Option<WidgetId>,
3370 hit: &crate::pointer::hit_slop::HitContext<'_>,
3371 ) -> Option<WidgetId> {
3372 if let Some(overlay_id) = self.overlay_manager.hit_test(point) {
3373 if Some(overlay_id) == exclude_overlay {
3374 } else if let Some(overlay) = self.overlay_manager.overlay(overlay_id) {
3376 let content_id = overlay.content_id;
3380 if let Some(found) =
3381 self.arena
3382 .hit_test_in_subtree_with_slop(content_id, point, exclude_widget, hit)
3383 {
3384 return Some(found);
3385 }
3386 if !self
3404 .arena
3405 .get(content_id)
3406 .is_some_and(|node| node.event_pass_through)
3407 {
3408 return None;
3409 }
3410 }
3411 }
3412
3413 if self.overlay_manager.topmost_centered().is_some() {
3414 return None;
3415 }
3416
3417 self.arena.hit_test_at_with_slop(point, exclude_widget, hit)
3420 }
3421}
3422
3423fn is_context_menu_chord(key: Key, modifiers: Modifiers) -> bool {
3441 match key {
3442 Key::ContextMenu => modifiers == Modifiers::NONE,
3443 Key::F10 => modifiers == Modifiers::SHIFT,
3444 #[cfg(target_os = "macos")]
3445 Key::M => modifiers == Modifiers::CTRL | Modifiers::SHIFT,
3446 _ => false,
3447 }
3448}
3449
3450#[cfg(test)]
3451mod tests {
3452 use super::*;
3453 use crate::test_widgets::FillWidget;
3454 use crate::widget::CursorIcon;
3455 use crate::widget_builder::WidgetBuilder;
3456
3457 #[test]
3458 fn pointer_enter_leave_synthesized() {
3459 let mut tree = WidgetTree::new();
3460 let widget = tree.add(FillWidget::new());
3461 tree.layout(SizeProposal::exact(100.0, 50.0));
3462 tree.pointer_move(Point::new(50.0, 25.0));
3463 assert_eq!(tree.hovered(), Some(widget));
3464 tree.pointer_move(Point::new(200.0, 200.0));
3465 assert_eq!(tree.hovered(), None);
3466 }
3467
3468 #[test]
3469 fn pointer_hover_updates_current_cursor() {
3470 let mut tree = WidgetTree::new();
3471 tree.add(FillWidget::new().cursor(CursorIcon::ColResize));
3472 tree.layout(SizeProposal::exact(100.0, 50.0));
3473
3474 tree.pointer_move(Point::new(50.0, 25.0));
3475 assert_eq!(tree.current_cursor(), CursorIcon::ColResize);
3476
3477 tree.pointer_move(Point::new(200.0, 200.0));
3478 assert_eq!(tree.current_cursor(), CursorIcon::Default);
3479 }
3480
3481 #[test]
3491 fn release_cursor_hands_the_cursor_back_to_the_node_that_declared_it() {
3492 let mut tree = WidgetTree::new();
3496 tree.add(
3497 FillWidget::new()
3498 .cursor(CursorIcon::ColResize)
3499 .on_pointer_event(|event, ctx| {
3500 if let WidgetEvent::PointerMove { position, .. } = event {
3501 if position.x < 30.0 {
3502 ctx.set_cursor(CursorIcon::Crosshair);
3503 } else {
3504 ctx.release_cursor();
3505 }
3506 }
3507 EventResponse::Ignored
3508 }),
3509 );
3510 tree.layout(SizeProposal::exact(100.0, 50.0));
3511
3512 tree.pointer_move(Point::new(10.0, 25.0));
3513 assert_eq!(
3514 tree.current_cursor(),
3515 CursorIcon::Crosshair,
3516 "the handler outranks the node it is attached to",
3517 );
3518 tree.pointer_move(Point::new(60.0, 25.0));
3519 assert_eq!(
3520 tree.current_cursor(),
3521 CursorIcon::ColResize,
3522 "and hands it back to the node, not to Default — no enter/leave \
3523 fires on a move within one node, so nothing else could",
3524 );
3525 tree.pointer_move(Point::new(10.0, 25.0));
3526 assert_eq!(
3527 tree.current_cursor(),
3528 CursorIcon::Crosshair,
3529 "and can take it again"
3530 );
3531 tree.pointer_move(Point::new(200.0, 200.0));
3532 assert_eq!(
3533 tree.current_cursor(),
3534 CursorIcon::Default,
3535 "leaving the node clears both the override and the declaration",
3536 );
3537 }
3538
3539 #[test]
3543 fn release_cursor_is_a_no_op_with_nothing_to_undo() {
3544 let mut tree = WidgetTree::new();
3545 tree.add(
3546 FillWidget::new()
3547 .cursor(CursorIcon::ColResize)
3548 .on_pointer_event(|event, ctx| {
3549 if matches!(event, WidgetEvent::PointerMove { .. }) {
3550 ctx.release_cursor();
3551 }
3552 EventResponse::Ignored
3553 }),
3554 );
3555 tree.layout(SizeProposal::exact(100.0, 50.0));
3556 tree.pointer_move(Point::new(50.0, 25.0));
3557 assert_eq!(tree.current_cursor(), CursorIcon::ColResize);
3558
3559 let mut tree = WidgetTree::new();
3561 tree.add(FillWidget::new().on_pointer_event(|event, ctx| {
3562 if let WidgetEvent::PointerMove { position, .. } = event {
3563 if position.x < 30.0 {
3564 ctx.set_cursor(CursorIcon::Crosshair);
3565 } else {
3566 ctx.release_cursor();
3567 }
3568 }
3569 EventResponse::Ignored
3570 }));
3571 tree.layout(SizeProposal::exact(100.0, 50.0));
3572 tree.pointer_move(Point::new(10.0, 25.0));
3573 assert_eq!(tree.current_cursor(), CursorIcon::Crosshair);
3574 tree.pointer_move(Point::new(60.0, 25.0));
3575 assert_eq!(
3576 tree.current_cursor(),
3577 CursorIcon::Default,
3578 "nothing declared a cursor for this chain, so the hand-back \
3579 resolves to Default",
3580 );
3581 }
3582
3583 #[test]
3591 fn an_on_hover_override_does_not_erase_the_declaration_it_outranks() {
3592 let mut tree = WidgetTree::new();
3593 tree.add(
3594 FillWidget::new()
3595 .cursor(CursorIcon::ColResize)
3596 .on_hover(|entered, ctx| {
3597 if entered {
3598 ctx.set_cursor(CursorIcon::Crosshair);
3599 }
3600 })
3601 .on_pointer_event(|event, ctx| {
3602 if let WidgetEvent::PointerMove { position, .. } = event
3603 && position.x >= 30.0
3604 {
3605 ctx.release_cursor();
3606 }
3607 EventResponse::Ignored
3608 }),
3609 );
3610 tree.layout(SizeProposal::exact(100.0, 50.0));
3611
3612 tree.pointer_move(Point::new(10.0, 25.0));
3613 assert_eq!(
3614 tree.current_cursor(),
3615 CursorIcon::Crosshair,
3616 "on_hover runs after the node cursor is applied, so it wins",
3617 );
3618 tree.pointer_move(Point::new(60.0, 25.0));
3619 assert_eq!(
3620 tree.current_cursor(),
3621 CursorIcon::ColResize,
3622 "and the node's declaration survived being overridden",
3623 );
3624 }
3625
3626 #[derive(Debug)]
3629 struct Bumpable {
3630 value: i32,
3631 }
3632
3633 impl crate::widget::Widget for Bumpable {
3634 fn layout_response(
3635 &self,
3636 proposal: SizeProposal,
3637 _ctx: &crate::widget::LayoutContext,
3638 ) -> crate::widget::LayoutResponse {
3639 proposal.resolve(10.0, 10.0).into()
3640 }
3641 fn as_any(&self) -> Option<&dyn std::any::Any> {
3642 Some(self)
3643 }
3644 fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
3645 Some(self)
3646 }
3647 }
3648
3649 #[test]
3650 fn with_widget_mut_applies_and_dirty_marks() {
3651 let mut tree = WidgetTree::new();
3652 let id = tree.add(Bumpable { value: 0 });
3653 tree.layout(SizeProposal::exact(100.0, 100.0));
3654
3655 let mut ctx = EventContext::new();
3656 ctx.with_widget_mut::<Bumpable>(id, crate::binding::BindingLevel::Relayout, |b| {
3657 b.value = 42;
3658 });
3659 tree.collect_from_ctx(ctx, id);
3660
3661 let value = tree
3662 .widget_as_any(id)
3663 .and_then(|a| a.downcast_ref::<Bumpable>())
3664 .map(|b| b.value);
3665 assert_eq!(
3666 value,
3667 Some(42),
3668 "the deferred closure must mutate the live widget"
3669 );
3670 assert!(
3671 tree.needs_layout(),
3672 "Relayout dirty level must mark the tree for relayout"
3673 );
3674 }
3675
3676 #[test]
3677 #[cfg(debug_assertions)]
3678 #[should_panic(expected = "not the requested type")]
3679 fn with_widget_mut_wrong_type_panics_in_debug() {
3680 struct Other;
3681 let mut tree = WidgetTree::new();
3682 let id = tree.add(Bumpable { value: 0 });
3683 let mut ctx = EventContext::new();
3684 ctx.with_widget_mut::<Other>(
3685 id,
3686 crate::binding::BindingLevel::RepaintOnly,
3687 |_o: &mut Other| {},
3688 );
3689 tree.collect_from_ctx(ctx, id);
3692 }
3693
3694 #[test]
3695 fn with_widget_mut_closure_may_fire_observed_signals() {
3696 use crate::signal::Signal;
3704 let mut tree = WidgetTree::new();
3705 let id = tree.add(Bumpable { value: 0 });
3706 tree.layout(SizeProposal::exact(100.0, 100.0));
3707
3708 let trigger = Signal::new(0_u64);
3709 let echo = Signal::new(0_u64);
3710 let echo_for_obs = echo.clone();
3711 let _obs = trigger.observe(move |v| echo_for_obs.set(*v));
3712
3713 let trigger_in = trigger.clone();
3714 let mut ctx = EventContext::new();
3715 ctx.with_widget_mut::<Bumpable>(id, crate::binding::BindingLevel::RepaintOnly, move |b| {
3716 b.value = 7;
3717 trigger_in.set(99);
3719 });
3720 tree.collect_from_ctx(ctx, id); assert_eq!(
3723 echo.get(),
3724 99,
3725 "the observer ran during the deferred mutation"
3726 );
3727 let value = tree
3728 .widget_as_any(id)
3729 .and_then(|a| a.downcast_ref::<Bumpable>())
3730 .map(|b| b.value);
3731 assert_eq!(value, Some(7));
3732 }
3733
3734 #[test]
3735 fn request_accessibility_update_forces_rewalk() {
3736 let mut tree = WidgetTree::new();
3737 let id = tree.add(Bumpable { value: 0 });
3738 tree.layout(SizeProposal::exact(100.0, 100.0));
3739 let _ = tree.sync_accessibility(); assert!(
3741 !tree.a11y_dirty,
3742 "sync_accessibility should clear the dirty flag"
3743 );
3744
3745 let mut ctx = EventContext::new();
3746 ctx.request_accessibility_update();
3747 tree.collect_from_ctx(ctx, id);
3748 assert!(
3749 tree.a11y_dirty,
3750 "request_accessibility_update must force an AT re-walk"
3751 );
3752 }
3753
3754 #[test]
3755 fn rebuild_dirties_accessibility_tree() {
3756 let mut tree = WidgetTree::new();
3763 let id = tree.add(Bumpable { value: 0 });
3764 tree.layout(SizeProposal::exact(100.0, 100.0));
3765 let _ = tree.sync_accessibility(); assert!(
3767 !tree.a11y_dirty,
3768 "sync_accessibility should clear the dirty flag"
3769 );
3770
3771 tree.arena_mark_needs_rebuild_for_testing(id);
3774 tree.layout(SizeProposal::exact(100.0, 100.0));
3775 assert!(
3776 tree.a11y_dirty,
3777 "a rebuild must dirty the AT tree so the next sync re-walks"
3778 );
3779 }
3780
3781 #[test]
3782 fn bound_access_label_change_dirties_accessibility_tree() {
3783 use crate::signal::Signal;
3784 use crate::test_widgets::FillWidget;
3785 use crate::widget_builder::WidgetBuilder;
3786
3787 let label = Signal::new("first".to_string());
3793 let mut tree = WidgetTree::new();
3794 let _id = tree.add(FillWidget::new().access_label(label.clone()));
3795 tree.layout(SizeProposal::exact(100.0, 100.0));
3796 let _ = tree.sync_accessibility(); assert!(!tree.a11y_dirty, "sync_accessibility should clear the flag");
3798
3799 label.set("second".to_string());
3800 tree.layout(SizeProposal::exact(100.0, 100.0));
3801 assert!(
3802 tree.a11y_dirty,
3803 "changing a bound access_label must dirty the AT tree"
3804 );
3805 }
3806
3807 #[test]
3808 fn disabled_ancestor_blocks_event_to_descendant() {
3809 use crate::signal::Signal;
3810 use crate::test_widgets::StackWidget;
3811 use std::cell::Cell;
3812 use std::rc::Rc;
3813
3814 let tapped = Rc::new(Cell::new(false));
3815 let flag = tapped.clone();
3816 let enabled = Signal::new(true);
3817
3818 let mut tree = WidgetTree::new();
3819 let child = tree.add(FillWidget::new().on_tap(move |_pos, _ctx| {
3820 flag.set(true);
3821 }));
3822 let parent = tree.add(StackWidget::new().child(child));
3823 tree.enabled_when(parent, enabled.clone());
3824 tree.layout(SizeProposal::exact(100.0, 50.0));
3825
3826 enabled.set(false);
3827 tree.click(child);
3828 assert!(
3829 !tapped.get(),
3830 "disabled ancestor should block descendant tap"
3831 );
3832
3833 enabled.set(true);
3834 tree.click(child);
3835 assert!(tapped.get(), "re-enabling should restore dispatch");
3836 }
3837
3838 #[test]
3839 fn pointer_positions_are_widget_local_at_nonzero_origin() {
3840 use crate::event::{Modifiers, PointerButton};
3841 use crate::test_widgets::InsetWidget;
3842 use std::cell::Cell;
3843 use std::rc::Rc;
3844
3845 let tap_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
3847 let down_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
3848 let drag_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
3849 let (tp, dp, gp) = (tap_pos.clone(), down_pos.clone(), drag_pos.clone());
3850
3851 let mut tree = WidgetTree::new();
3852 let child = tree.add(
3853 FillWidget::new()
3854 .on_tap(move |ev, _ctx| tp.set(Some(ev.position)))
3855 .on_pointer_event(move |ev, _ctx| {
3856 if let WidgetEvent::PointerDown { position, .. } = ev {
3857 dp.set(Some(*position));
3858 }
3859 crate::event::EventResponse::Ignored
3860 })
3861 .on_drag(move |phase, _ctx| {
3862 use crate::gesture::DragPhase;
3863 match phase {
3864 DragPhase::Started { position, .. }
3865 | DragPhase::Moved { position, .. }
3866 | DragPhase::Ended { position, .. } => gp.set(Some(position)),
3867 _ => {}
3868 }
3869 }),
3870 );
3871 let inset = tree.add(InsetWidget::new(20.0).set_child(child));
3872 let _ = inset;
3873 tree.layout(SizeProposal::exact(200.0, 200.0));
3874 assert_eq!(tree.bounds(child).origin(), Point::new(20.0, 20.0));
3875
3876 tree.dispatch_event(WidgetEvent::pointer_down(
3878 Point::new(50.0, 40.0),
3879 PointerButton::Primary,
3880 Modifiers::NONE,
3881 ));
3882 assert_eq!(
3883 down_pos.get(),
3884 Some(Point::new(30.0, 20.0)),
3885 "on_pointer_event PointerDown must be widget-local"
3886 );
3887 tree.dispatch_event(WidgetEvent::pointer_up(
3888 Point::new(50.0, 40.0),
3889 PointerButton::Primary,
3890 Modifiers::NONE,
3891 ));
3892 assert_eq!(
3893 tap_pos.get(),
3894 Some(Point::new(30.0, 20.0)),
3895 "on_tap position must be widget-local"
3896 );
3897
3898 tree.dispatch_event(WidgetEvent::pointer_down(
3901 Point::new(50.0, 40.0),
3902 PointerButton::Primary,
3903 Modifiers::NONE,
3904 ));
3905 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(65.0, 55.0)));
3908 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(90.0, 70.0)));
3909 assert_eq!(
3910 drag_pos.get(),
3911 Some(Point::new(70.0, 50.0)),
3912 "on_drag position must be widget-local"
3913 );
3914 }
3915
3916 #[test]
3917 fn dormant_widget_not_hit_tested() {
3918 let mut tree = WidgetTree::new();
3919 let widget = tree.add(FillWidget::new());
3920 tree.layout(SizeProposal::exact(100.0, 50.0));
3921
3922 tree.pointer_move(Point::new(50.0, 25.0));
3923 assert_eq!(tree.hovered(), Some(widget));
3924
3925 tree.set_dormant(widget);
3926 tree.pointer_move(Point::new(200.0, 200.0));
3927 tree.pointer_move(Point::new(50.0, 25.0));
3928 assert_eq!(tree.hovered(), None);
3929 }
3930
3931 #[test]
3932 fn ancestor_pointer_handler_does_not_suppress_descendant_hover() {
3933 use crate::event::EventResponse;
3934 use crate::test_widgets::StackWidget;
3935 use std::cell::Cell;
3936 use std::rc::Rc;
3937
3938 let hovered = Rc::new(Cell::new(false));
3940 let h = hovered.clone();
3941
3942 let mut tree = WidgetTree::new();
3943 let child = tree.add(FillWidget::new().on_hover(move |entered, _ctx| h.set(entered)));
3944 tree.add(
3949 StackWidget::new()
3950 .child(child)
3951 .on_pointer_event(|_event, _ctx| EventResponse::Handled),
3952 );
3953 tree.layout(SizeProposal::exact(100.0, 50.0));
3954
3955 tree.pointer_move(Point::new(50.0, 25.0));
3956 assert!(
3957 hovered.get(),
3958 "a greedy ancestor on_pointer_event must NOT swallow the child's PointerEnter"
3959 );
3960
3961 tree.pointer_move(Point::new(500.0, 500.0));
3962 assert!(
3963 !hovered.get(),
3964 "PointerLeave must likewise reach the child despite the ancestor"
3965 );
3966 }
3967
3968 #[test]
3976 fn a_child_hover_handler_does_not_swallow_its_ancestors() {
3977 use crate::test_widgets::StackWidget;
3978 use std::cell::Cell;
3979 use std::rc::Rc;
3980
3981 let (row, button) = (Rc::new(Cell::new(false)), Rc::new(Cell::new(false)));
3982 let (r, b) = (row.clone(), button.clone());
3983
3984 let mut tree = WidgetTree::new();
3985 let child = tree.add(FillWidget::new().on_hover(move |entered, _ctx| b.set(entered)));
3986 tree.add(
3987 StackWidget::new()
3988 .child(child)
3989 .on_hover(move |entered, _ctx| r.set(entered)),
3990 );
3991 tree.layout(SizeProposal::exact(100.0, 50.0));
3992
3993 tree.pointer_move(Point::new(50.0, 25.0));
3994 assert!(button.get(), "the child is hovered");
3995 assert!(row.get(), "and so is the row it is inside");
3996
3997 tree.pointer_move(Point::new(500.0, 500.0));
3998 assert!(!button.get(), "the child heard the leave");
3999 assert!(
4000 !row.get(),
4001 "and so did the row — a container is not still hovered because the \
4002 pointer left it through a button"
4003 );
4004 }
4005
4006 #[test]
4013 fn key_preview_consumes_before_focused_on_key() {
4014 use crate::event::EventResponse;
4017 use crate::test_widgets::StackWidget;
4018 use std::cell::Cell;
4019 use std::rc::Rc;
4020
4021 let leaf_fired = Rc::new(Cell::new(false));
4022 let leaf_flag = leaf_fired.clone();
4023 let preview_fired = Rc::new(Cell::new(false));
4024 let preview_flag = preview_fired.clone();
4025
4026 let mut tree = WidgetTree::new();
4027 let leaf = tree.add(FillWidget::new().focusable().on_key(move |event, _c| {
4028 if matches!(event, WidgetEvent::KeyDown { .. }) {
4031 leaf_flag.set(true);
4032 }
4033 EventResponse::Handled
4034 }));
4035 let mid = tree.add(StackWidget::new().child(leaf));
4036 let _root = tree.add(
4037 StackWidget::new()
4038 .child(mid)
4039 .on_key_preview(move |event, _c| match event {
4040 WidgetEvent::KeyDown {
4041 key: Key::Enter, ..
4042 } => {
4043 preview_flag.set(true);
4044 EventResponse::Handled
4045 }
4046 _ => EventResponse::Ignored,
4047 }),
4048 );
4049
4050 tree.layout(SizeProposal::exact(100.0, 50.0));
4051 tree.focus(leaf);
4052 tree.press_key(Key::Enter, Modifiers::NONE);
4053
4054 assert!(
4055 preview_fired.get(),
4056 "ancestor on_key_preview must fire for KeyDown on a focused descendant"
4057 );
4058 assert!(
4059 !leaf_fired.get(),
4060 "consuming the event in preview must prevent the focused widget's on_key from running"
4061 );
4062 }
4063
4064 #[test]
4065 fn key_preview_falls_through_when_returning_ignored() {
4066 use crate::event::EventResponse;
4069 use crate::test_widgets::StackWidget;
4070 use std::cell::Cell;
4071 use std::rc::Rc;
4072
4073 let leaf_fired = Rc::new(Cell::new(false));
4074 let leaf_flag = leaf_fired.clone();
4075 let preview_fired = Rc::new(Cell::new(false));
4076 let preview_flag = preview_fired.clone();
4077
4078 let mut tree = WidgetTree::new();
4079 let leaf = tree.add(FillWidget::new().focusable().on_key(move |_e, _c| {
4080 leaf_flag.set(true);
4081 EventResponse::Handled
4082 }));
4083 let mid = tree.add(StackWidget::new().child(leaf));
4084 let _root = tree.add(
4085 StackWidget::new()
4086 .child(mid)
4087 .on_key_preview(move |_event, _c| {
4088 preview_flag.set(true);
4089 EventResponse::Ignored
4090 }),
4091 );
4092
4093 tree.layout(SizeProposal::exact(100.0, 50.0));
4094 tree.focus(leaf);
4095 tree.press_key(Key::Enter, Modifiers::NONE);
4096
4097 assert!(preview_fired.get(), "preview must always be invoked");
4098 assert!(
4099 leaf_fired.get(),
4100 "preview returning Ignored must not block the focused widget's on_key"
4101 );
4102 }
4103
4104 #[test]
4105 fn key_preview_excludes_focused_target_itself() {
4106 use crate::event::EventResponse;
4110 use std::cell::Cell;
4111 use std::rc::Rc;
4112
4113 let preview_on_target = Rc::new(Cell::new(false));
4114 let pf = preview_on_target.clone();
4115
4116 let mut tree = WidgetTree::new();
4117 let leaf = tree.add(FillWidget::new().focusable().on_key_preview(move |_e, _c| {
4118 pf.set(true);
4119 EventResponse::Handled
4120 }));
4121 tree.layout(SizeProposal::exact(100.0, 50.0));
4122 tree.focus(leaf);
4123 tree.press_key(Key::Enter, Modifiers::NONE);
4124
4125 assert!(
4126 !preview_on_target.get(),
4127 "the focused widget itself must not see its own on_key_preview"
4128 );
4129 }
4130
4131 #[test]
4132 fn key_preview_root_to_target_order() {
4133 use crate::event::EventResponse;
4137 use crate::test_widgets::StackWidget;
4138 use std::cell::RefCell;
4139 use std::rc::Rc;
4140
4141 let order: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
4142 let outer_log = order.clone();
4143 let inner_log = order.clone();
4144
4145 let mut tree = WidgetTree::new();
4146 let leaf = tree.add(FillWidget::new().focusable());
4147 let inner = tree.add(
4148 StackWidget::new()
4149 .child(leaf)
4150 .on_key_preview(move |event, _c| {
4151 if matches!(event, WidgetEvent::KeyDown { .. }) {
4152 inner_log.borrow_mut().push("inner");
4153 }
4154 EventResponse::Ignored
4155 }),
4156 );
4157 let _outer = tree.add(
4158 StackWidget::new()
4159 .child(inner)
4160 .on_key_preview(move |event, _c| {
4161 if matches!(event, WidgetEvent::KeyDown { .. }) {
4162 outer_log.borrow_mut().push("outer");
4163 }
4164 EventResponse::Ignored
4165 }),
4166 );
4167
4168 tree.layout(SizeProposal::exact(100.0, 50.0));
4169 tree.focus(leaf);
4170 tree.dispatch_event(WidgetEvent::KeyDown {
4171 key: Key::Enter,
4172 modifiers: Modifiers::NONE,
4173 text: None,
4174 });
4175
4176 assert_eq!(
4177 *order.borrow(),
4178 vec!["outer", "inner"],
4179 "preview must walk root → parent-of-target"
4180 );
4181 }
4182
4183 #[test]
4184 fn access_action_routes_to_cursored_target_not_focus() {
4185 use crate::signal::Signal;
4189 let a_fired = Signal::new(false);
4190 let b_fired = Signal::new(false);
4191 let a_cb = a_fired.clone();
4192 let b_cb = b_fired.clone();
4193
4194 let mut tree = WidgetTree::new();
4195 let a = tree.add(
4196 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| a_cb.set(true)),
4197 );
4198 let b = tree.add(
4199 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| b_cb.set(true)),
4200 );
4201 tree.layout(SizeProposal::exact(200.0, 100.0));
4202
4203 tree.focus(a);
4204 tree.dispatch_event(WidgetEvent::AccessAction {
4205 action: accesskit::Action::Click,
4206 target: Some(b),
4207 target_node: crate::accessibility::widget_id_to_node_id(b),
4208 data: None,
4209 });
4210
4211 assert!(b_fired.get(), "the cursored target must receive the action");
4212 assert!(
4213 !a_fired.get(),
4214 "the keyboard-focused widget must NOT receive an action targeting another node"
4215 );
4216 }
4217
4218 #[test]
4219 fn access_action_without_target_is_dropped_not_redirected_to_focus() {
4220 use crate::signal::Signal;
4223 let fired = Signal::new(false);
4224 let cb = fired.clone();
4225
4226 let mut tree = WidgetTree::new();
4227 let widget = tree.add(
4228 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| cb.set(true)),
4229 );
4230 tree.layout(SizeProposal::exact(200.0, 100.0));
4231
4232 tree.focus(widget);
4233 tree.dispatch_event(WidgetEvent::AccessAction {
4234 action: accesskit::Action::Click,
4235 target: None,
4236 target_node: crate::accessibility::root_node_id(),
4237 data: None,
4238 });
4239
4240 assert!(
4241 !fired.get(),
4242 "a target-less action must not be redirected to the focused widget"
4243 );
4244 }
4245
4246 #[test]
4253 fn shortcut_fires_matching_action_on_source_widget() {
4254 use crate::action::Action;
4255 use crate::shortcut::{KeyStroke, Shortcut};
4256 use std::cell::Cell;
4257 use std::rc::Rc;
4258
4259 let fired = Rc::new(Cell::new(false));
4260 let fired_flag = fired.clone();
4261
4262 let mut tree = WidgetTree::new();
4263 let widget = tree.add(FillWidget::new().focusable());
4264 tree.push_action(
4265 widget,
4266 Action::new("app.save").on_invoke(move |_intent, _ctx| {
4267 fired_flag.set(true);
4268 }),
4269 );
4270 tree.shortcut_registry_mut().register(
4271 Shortcut::new("app.save")
4272 .primary(KeyStroke::command(Key::S))
4273 .build(),
4274 );
4275
4276 tree.layout(SizeProposal::exact(100.0, 50.0));
4277 tree.focus(widget);
4278
4279 tree.press_key(Key::S, Modifiers::COMMAND);
4280 assert!(fired.get(), "matching action must fire on KeyDown");
4281 }
4282
4283 #[test]
4284 fn global_shortcut_fires_without_focused_widget() {
4285 use crate::action::Action;
4286 use crate::shortcut::{KeyStroke, Shortcut};
4287 use std::cell::Cell;
4288 use std::rc::Rc;
4289
4290 let fired = Rc::new(Cell::new(false));
4294 let fired_flag = fired.clone();
4295
4296 let mut tree = WidgetTree::new();
4297 let root = tree.add(FillWidget::new());
4298 tree.push_action(
4299 root,
4300 Action::new("app.save").on_invoke(move |_intent, _ctx| {
4301 fired_flag.set(true);
4302 }),
4303 );
4304 tree.shortcut_registry_mut().register(
4305 Shortcut::new("app.save")
4306 .primary(KeyStroke::command(Key::S))
4307 .build(),
4308 );
4309
4310 tree.layout(SizeProposal::exact(100.0, 50.0));
4311 tree.press_key(Key::S, Modifiers::COMMAND);
4314 assert!(
4315 fired.get(),
4316 "global shortcut must fire without a focused widget"
4317 );
4318 }
4319
4320 #[test]
4321 fn global_shortcut_fires_after_focused_widget_destroyed() {
4322 use crate::action::Action;
4323 use crate::shortcut::{KeyStroke, Shortcut};
4324 use std::cell::Cell;
4325 use std::rc::Rc;
4326
4327 let fired = Rc::new(Cell::new(false));
4332 let fired_flag = fired.clone();
4333
4334 let mut tree = WidgetTree::new();
4335 let root = tree.add(FillWidget::new());
4336 let focusable = tree.add_child(root, FillWidget::new().focusable());
4337 tree.push_action(
4338 root,
4339 Action::new("app.save").on_invoke(move |_intent, _ctx| {
4340 fired_flag.set(true);
4341 }),
4342 );
4343 tree.shortcut_registry_mut().register(
4344 Shortcut::new("app.save")
4345 .primary(KeyStroke::command(Key::S))
4346 .build(),
4347 );
4348
4349 tree.layout(SizeProposal::exact(100.0, 50.0));
4350 tree.focus(focusable);
4351 assert_eq!(tree.focused(), Some(focusable));
4352
4353 tree.destroy_subtree(focusable);
4356 assert_eq!(tree.focused(), None, "focus must clear when destroyed");
4357
4358 tree.press_key(Key::S, Modifiers::COMMAND);
4359 assert!(
4360 fired.get(),
4361 "global shortcut must still fire after the focused widget is destroyed"
4362 );
4363 }
4364
4365 #[test]
4366 fn scoped_shortcut_matches_only_when_focus_in_scope() {
4367 use crate::action::Action;
4368 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
4369 use std::cell::Cell;
4370 use std::rc::Rc;
4371
4372 let fired = Rc::new(Cell::new(0));
4373 let fired_flag = fired.clone();
4374
4375 let mut tree = WidgetTree::new();
4376 let scope_root = tree.add(FillWidget::new().focusable());
4377 let inside = tree.add_child(scope_root, FillWidget::new().focusable());
4378 let outside = tree.add(FillWidget::new().focusable());
4379
4380 tree.push_action(
4381 scope_root,
4382 Action::new("editor.find").on_invoke(move |_i, _c| {
4383 fired_flag.set(fired_flag.get() + 1);
4384 }),
4385 );
4386 tree.shortcut_registry_mut().register(
4387 Shortcut::new("editor.find")
4388 .primary(KeyStroke::command(Key::F))
4389 .scope(ShortcutScope::Scoped(scope_root))
4390 .build(),
4391 );
4392
4393 tree.layout(SizeProposal::exact(200.0, 100.0));
4394
4395 tree.focus(outside);
4397 tree.press_key(Key::F, Modifiers::COMMAND);
4398 assert_eq!(
4399 fired.get(),
4400 0,
4401 "scoped shortcut must not fire outside scope"
4402 );
4403
4404 tree.focus(inside);
4406 tree.press_key(Key::F, Modifiers::COMMAND);
4407 assert_eq!(
4408 fired.get(),
4409 1,
4410 "scoped shortcut must fire when focus in scope"
4411 );
4412 }
4413
4414 #[test]
4415 fn same_chord_scoped_first_falls_back_to_global_when_focus_outside() {
4416 use crate::action::Action;
4422 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
4423 use std::cell::Cell;
4424 use std::rc::Rc;
4425
4426 let scoped_fired = Rc::new(Cell::new(0));
4427 let global_fired = Rc::new(Cell::new(0));
4428 let sf = scoped_fired.clone();
4429 let gf = global_fired.clone();
4430
4431 let mut tree = WidgetTree::new();
4432 let root = tree.add(FillWidget::new());
4433 let editor = tree.add_child(root, FillWidget::new().focusable());
4434 let _editor_inner = tree.add_child(editor, FillWidget::new().focusable());
4435 let sidebar = tree.add_child(root, FillWidget::new().focusable());
4436
4437 tree.push_action(
4438 editor,
4439 Action::new("editor.saveBlock").on_invoke(move |_i, _c| sf.set(sf.get() + 1)),
4440 );
4441 tree.push_action(
4442 root,
4443 Action::new("zzz.global.save").on_invoke(move |_i, _c| gf.set(gf.get() + 1)),
4444 );
4445 tree.shortcut_registry_mut().register(
4446 Shortcut::new("editor.saveBlock")
4447 .primary(KeyStroke::command(Key::S))
4448 .scope(ShortcutScope::Scoped(editor))
4449 .build(),
4450 );
4451 tree.shortcut_registry_mut().register(
4452 Shortcut::new("zzz.global.save")
4453 .primary(KeyStroke::command(Key::S))
4454 .build(),
4455 );
4456
4457 tree.layout(SizeProposal::exact(200.0, 100.0));
4458 tree.focus(sidebar);
4459 tree.press_key(Key::S, Modifiers::COMMAND);
4460
4461 assert_eq!(global_fired.get(), 1, "applicable global must fire");
4462 assert_eq!(
4463 scoped_fired.get(),
4464 0,
4465 "inapplicable scoped binding must not eat the chord"
4466 );
4467 }
4468
4469 #[test]
4470 fn same_chord_global_first_yields_to_scoped_when_focus_inside() {
4471 use crate::action::Action;
4476 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
4477 use std::cell::Cell;
4478 use std::rc::Rc;
4479
4480 let scoped_fired = Rc::new(Cell::new(0));
4481 let global_fired = Rc::new(Cell::new(0));
4482 let sf = scoped_fired.clone();
4483 let gf = global_fired.clone();
4484
4485 let mut tree = WidgetTree::new();
4486 let root = tree.add(FillWidget::new());
4487 let editor = tree.add_child(root, FillWidget::new().focusable());
4488 let editor_inner = tree.add_child(editor, FillWidget::new().focusable());
4489 let sidebar = tree.add_child(root, FillWidget::new().focusable());
4490
4491 tree.push_action(
4492 editor,
4493 Action::new("editor.saveBlock").on_invoke(move |_i, _c| sf.set(sf.get() + 1)),
4494 );
4495 tree.push_action(
4496 root,
4497 Action::new("app.save").on_invoke(move |_i, _c| gf.set(gf.get() + 1)),
4498 );
4499 tree.shortcut_registry_mut().register(
4500 Shortcut::new("app.save")
4501 .primary(KeyStroke::command(Key::S))
4502 .build(),
4503 );
4504 tree.shortcut_registry_mut().register(
4505 Shortcut::new("editor.saveBlock")
4506 .primary(KeyStroke::command(Key::S))
4507 .scope(ShortcutScope::Scoped(editor))
4508 .build(),
4509 );
4510
4511 tree.layout(SizeProposal::exact(200.0, 100.0));
4512
4513 tree.focus(editor_inner);
4515 tree.press_key(Key::S, Modifiers::COMMAND);
4516 assert_eq!(
4517 scoped_fired.get(),
4518 1,
4519 "in-focus scoped must win over global"
4520 );
4521 assert_eq!(
4522 global_fired.get(),
4523 0,
4524 "global must yield to the scoped binding"
4525 );
4526
4527 tree.focus(sidebar);
4529 tree.press_key(Key::S, Modifiers::COMMAND);
4530 assert_eq!(scoped_fired.get(), 1, "scoped stays put outside its scope");
4531 assert_eq!(
4532 global_fired.get(),
4533 1,
4534 "global fires when focus leaves the scope"
4535 );
4536 }
4537
4538 #[test]
4539 fn propagated_action_lets_ancestor_handle() {
4540 use crate::action::Action;
4541 use crate::intent::IntentResponse;
4542 use crate::shortcut::{KeyStroke, Shortcut};
4543 use std::cell::Cell;
4544 use std::rc::Rc;
4545
4546 let inner_seen = Rc::new(Cell::new(false));
4547 let outer_seen = Rc::new(Cell::new(false));
4548 let inner_flag = inner_seen.clone();
4549 let outer_flag = outer_seen.clone();
4550
4551 let mut tree = WidgetTree::new();
4552 let outer = tree.add(FillWidget::new().focusable());
4553 let inner = tree.add_child(outer, FillWidget::new().focusable());
4554
4555 tree.push_action(
4557 inner,
4558 Action::new("app.save").on_invoke_with_response(move |_i, _c| {
4559 inner_flag.set(true);
4560 IntentResponse::Propagated
4561 }),
4562 );
4563 tree.push_action(
4564 outer,
4565 Action::new("app.save").on_invoke(move |_i, _c| {
4566 outer_flag.set(true);
4567 }),
4568 );
4569 tree.shortcut_registry_mut().register(
4570 Shortcut::new("app.save")
4571 .primary(KeyStroke::command(Key::S))
4572 .build(),
4573 );
4574
4575 tree.layout(SizeProposal::exact(100.0, 50.0));
4576 tree.focus(inner);
4577
4578 tree.press_key(Key::S, Modifiers::COMMAND);
4579 assert!(inner_seen.get(), "inner action observed the intent");
4580 assert!(outer_seen.get(), "outer action reached after Propagated");
4581 }
4582
4583 #[test]
4584 fn handled_action_stops_propagation() {
4585 use crate::action::Action;
4586 use crate::shortcut::{KeyStroke, Shortcut};
4587 use std::cell::Cell;
4588 use std::rc::Rc;
4589
4590 let inner_seen = Rc::new(Cell::new(false));
4591 let outer_seen = Rc::new(Cell::new(false));
4592 let inner_flag = inner_seen.clone();
4593 let outer_flag = outer_seen.clone();
4594
4595 let mut tree = WidgetTree::new();
4596 let outer = tree.add(FillWidget::new().focusable());
4597 let inner = tree.add_child(outer, FillWidget::new().focusable());
4598
4599 tree.push_action(
4600 inner,
4601 Action::new("app.save").on_invoke(move |_i, _c| {
4602 inner_flag.set(true);
4603 }),
4604 );
4605 tree.push_action(
4606 outer,
4607 Action::new("app.save").on_invoke(move |_i, _c| {
4608 outer_flag.set(true);
4609 }),
4610 );
4611 tree.shortcut_registry_mut().register(
4612 Shortcut::new("app.save")
4613 .primary(KeyStroke::command(Key::S))
4614 .build(),
4615 );
4616
4617 tree.layout(SizeProposal::exact(100.0, 50.0));
4618 tree.focus(inner);
4619
4620 tree.press_key(Key::S, Modifiers::COMMAND);
4621 assert!(inner_seen.get());
4622 assert!(!outer_seen.get(), "Handled at inner must stop propagation");
4623 }
4624
4625 #[test]
4626 fn disabled_action_propagates_by_default() {
4627 use crate::action::Action;
4628 use crate::shortcut::{KeyStroke, Shortcut};
4629 use crate::signal::Signal;
4630 use std::cell::Cell;
4631 use std::rc::Rc;
4632
4633 let inner_seen = Rc::new(Cell::new(false));
4634 let outer_seen = Rc::new(Cell::new(false));
4635 let inner_flag = inner_seen.clone();
4636 let outer_flag = outer_seen.clone();
4637
4638 let mut tree = WidgetTree::new();
4639 let outer = tree.add(FillWidget::new().focusable());
4640 let inner = tree.add_child(outer, FillWidget::new().focusable());
4641
4642 let enabled = Signal::new(false);
4643 tree.push_action(
4644 inner,
4645 Action::new("app.save")
4646 .enabled_when(enabled.clone())
4647 .on_invoke(move |_i, _c| {
4648 inner_flag.set(true);
4649 }),
4650 );
4651 tree.push_action(
4652 outer,
4653 Action::new("app.save").on_invoke(move |_i, _c| {
4654 outer_flag.set(true);
4655 }),
4656 );
4657 tree.shortcut_registry_mut().register(
4658 Shortcut::new("app.save")
4659 .primary(KeyStroke::command(Key::S))
4660 .build(),
4661 );
4662
4663 tree.layout(SizeProposal::exact(100.0, 50.0));
4664 tree.focus(inner);
4665
4666 tree.press_key(Key::S, Modifiers::COMMAND);
4667 assert!(!inner_seen.get(), "disabled inner must not run");
4668 assert!(
4669 outer_seen.get(),
4670 "intent must propagate past disabled inner"
4671 );
4672 }
4673
4674 #[test]
4675 fn disabled_action_with_non_propagating_shortcut_consumes() {
4676 use crate::action::Action;
4677 use crate::shortcut::{KeyStroke, Shortcut};
4678 use crate::signal::Signal;
4679 use std::cell::Cell;
4680 use std::rc::Rc;
4681
4682 let inner_seen = Rc::new(Cell::new(false));
4683 let outer_seen = Rc::new(Cell::new(false));
4684 let inner_flag = inner_seen.clone();
4685 let outer_flag = outer_seen.clone();
4686
4687 let mut tree = WidgetTree::new();
4688 let outer = tree.add(FillWidget::new().focusable());
4689 let inner = tree.add_child(outer, FillWidget::new().focusable());
4690
4691 let enabled = Signal::new(false);
4692 tree.push_action(
4693 inner,
4694 Action::new("app.save")
4695 .enabled_when(enabled.clone())
4696 .on_invoke(move |_i, _c| {
4697 inner_flag.set(true);
4698 }),
4699 );
4700 tree.push_action(
4701 outer,
4702 Action::new("app.save").on_invoke(move |_i, _c| {
4703 outer_flag.set(true);
4704 }),
4705 );
4706 tree.shortcut_registry_mut().register(
4707 Shortcut::new("app.save")
4708 .primary(KeyStroke::command(Key::S))
4709 .propagate_when_disabled(false)
4710 .build(),
4711 );
4712
4713 tree.layout(SizeProposal::exact(100.0, 50.0));
4714 tree.focus(inner);
4715
4716 tree.press_key(Key::S, Modifiers::COMMAND);
4717 assert!(!inner_seen.get(), "disabled inner still does not run");
4718 assert!(
4719 !outer_seen.get(),
4720 "intent must NOT propagate when shortcut disallows it"
4721 );
4722 }
4723
4724 #[test]
4725 fn send_intent_from_handler_reaches_ancestor_action() {
4726 use crate::action::Action;
4727 use crate::intent::Intent;
4728 use std::cell::Cell;
4729 use std::rc::Rc;
4730
4731 let save_seen = Rc::new(Cell::new(false));
4732 let save_flag = save_seen.clone();
4733
4734 let mut tree = WidgetTree::new();
4735 let root = tree.add(FillWidget::new());
4736 let button = tree.add_child(
4737 root,
4738 FillWidget::new().on_tap(|_pos, ctx| {
4739 ctx.send_intent(Intent::new("app.save"));
4740 }),
4741 );
4742 tree.push_action(
4743 root,
4744 Action::new("app.save").on_invoke(move |_i, _c| {
4745 save_flag.set(true);
4746 }),
4747 );
4748
4749 tree.layout(SizeProposal::exact(100.0, 50.0));
4750 tree.click(button);
4751 assert!(
4752 save_seen.get(),
4753 "ctx.send_intent must reach ancestor action"
4754 );
4755 }
4756
4757 #[test]
4758 fn widget_type_histogram_counts_distinct_types() {
4759 let mut tree = WidgetTree::new();
4764 let _ = tree.add(FillWidget::new());
4765 let _ = tree.add(FillWidget::new());
4766 let _ = tree.add(FillWidget::new());
4767 tree.layout(SizeProposal::exact(100.0, 100.0));
4768 let histogram = tree.widget_type_histogram();
4769 let total: u32 = histogram.values().sum();
4770 assert!(
4771 total >= 3,
4772 "expected at least 3 active widgets, got {total}: {histogram:?}"
4773 );
4774 let fillwidget_entries: u32 = histogram
4775 .iter()
4776 .filter(|(k, _)| k.contains("FillWidget"))
4777 .map(|(_, v)| *v)
4778 .sum();
4779 assert!(
4780 fillwidget_entries >= 3,
4781 "expected ≥3 FillWidget instances; histogram = {histogram:?}"
4782 );
4783 assert_eq!(tree.active_widget_count() as u32, total);
4784 }
4785
4786 #[test]
4787 fn intent_source_tagged_handler_for_tap_activation() {
4788 use crate::action::Action;
4792 use crate::intent::Intent;
4793 use crate::telemetry::IntentSource;
4794 use std::cell::Cell;
4795 use std::rc::Rc;
4796 let captured = Rc::new(Cell::new(IntentSource::Unknown));
4797 let captured_for_action = captured.clone();
4798
4799 let mut tree = WidgetTree::new();
4800 let root = tree.add(FillWidget::new());
4801 let button = tree.add_child(
4802 root,
4803 FillWidget::new().on_tap(|_pos, ctx| {
4804 ctx.send_intent(Intent::new("app.save"));
4805 }),
4806 );
4807 tree.push_action(
4808 root,
4809 Action::new("app.save").on_invoke(move |intent, _c| {
4810 captured_for_action.set(intent.source);
4811 }),
4812 );
4813
4814 tree.layout(SizeProposal::exact(100.0, 50.0));
4815 tree.click(button);
4816 assert_eq!(
4817 captured.get(),
4818 IntentSource::Handler,
4819 "tap-driven intent must tag IntentSource::Handler"
4820 );
4821 }
4822
4823 #[test]
4824 fn intent_source_programmatic_when_no_handler_active() {
4825 use crate::intent::Intent;
4826 use crate::telemetry::IntentSource;
4827 let intent = Intent::new("app.demo");
4828 assert_eq!(intent.source, IntentSource::Programmatic);
4829
4830 let mut ctx = EventContext::new();
4832 ctx.send_intent(Intent::new("app.demo"));
4833 let queued = ctx.pending_intents.first().expect("intent queued");
4834 assert_eq!(queued.source, IntentSource::Programmatic);
4835 }
4836
4837 #[test]
4838 fn with_intent_source_overrides_for_managed_widgets() {
4839 use crate::intent::Intent;
4840 use crate::telemetry::IntentSource;
4841 let mut ctx = EventContext::new();
4842 ctx.with_intent_source(IntentSource::Menu, |ctx| {
4843 ctx.send_intent(Intent::new("app.demo"));
4844 });
4845 let queued = ctx.pending_intents.first().expect("intent queued");
4846 assert_eq!(
4847 queued.source,
4848 IntentSource::Menu,
4849 "with_intent_source(Menu) must tag the dispatched intent"
4850 );
4851
4852 ctx.send_intent(Intent::new("app.next"));
4856 let next = ctx.pending_intents.last().expect("second intent");
4857 assert_eq!(next.source, IntentSource::Programmatic);
4858 }
4859
4860 #[test]
4861 fn disabled_shortcut_falls_through_to_focused_widget() {
4862 use crate::action::Action;
4863 use crate::shortcut::{KeyStroke, Shortcut};
4864 use crate::signal::Signal;
4865 use std::cell::Cell;
4866 use std::rc::Rc;
4867
4868 let action_fired = Rc::new(Cell::new(false));
4869 let on_key_fired = Rc::new(Cell::new(false));
4870 let af = action_fired.clone();
4871 let kf = on_key_fired.clone();
4872
4873 let enabled = Signal::new(false);
4874
4875 let mut tree = WidgetTree::new();
4876 let widget = tree.add(FillWidget::new().focusable().on_key(move |event, _ctx| {
4877 if matches!(
4878 event,
4879 WidgetEvent::KeyDown {
4880 key: Key::S,
4881 modifiers,
4882 ..
4883 } if modifiers.command()
4884 ) {
4885 kf.set(true);
4886 return EventResponse::Handled;
4887 }
4888 EventResponse::Ignored
4889 }));
4890 tree.push_action(
4891 widget,
4892 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
4893 );
4894 tree.shortcut_registry_mut().register(
4895 Shortcut::new("app.save")
4896 .primary(KeyStroke::command(Key::S))
4897 .enabled_when(enabled.clone())
4898 .build(),
4899 );
4900
4901 tree.layout(SizeProposal::exact(100.0, 50.0));
4902 tree.focus(widget);
4903
4904 tree.press_key(Key::S, Modifiers::COMMAND);
4906 assert!(
4907 !action_fired.get(),
4908 "disabled shortcut must not invoke its action"
4909 );
4910 assert!(
4911 on_key_fired.get(),
4912 "disabled shortcut must let KeyDown reach the focused widget"
4913 );
4914
4915 on_key_fired.set(false);
4917 enabled.set(true);
4918 tree.press_key(Key::S, Modifiers::COMMAND);
4919 assert!(action_fired.get(), "re-enabled shortcut must dispatch");
4920 assert!(
4921 !on_key_fired.get(),
4922 "enabled shortcut must consume the KeyDown"
4923 );
4924 }
4925
4926 #[test]
4927 fn keyboard_capture_bypasses_shortcut() {
4928 use crate::action::Action;
4929 use crate::shortcut::{KeyStroke, Shortcut};
4930 use std::cell::Cell;
4931 use std::rc::Rc;
4932
4933 fn run(capture: bool) -> (bool, bool) {
4939 let action_fired = Rc::new(Cell::new(false));
4940 let on_key_fired = Rc::new(Cell::new(false));
4941 let af = action_fired.clone();
4942 let kf = on_key_fired.clone();
4943
4944 let mut tree = WidgetTree::new();
4945 let widget = tree.add(
4946 FillWidget::new()
4947 .focusable()
4948 .keyboard_capture(capture)
4949 .on_key(move |event, _ctx| {
4950 if matches!(
4951 event,
4952 WidgetEvent::KeyDown { key: Key::S, modifiers, .. } if modifiers.command()
4953 ) {
4954 kf.set(true);
4955 return EventResponse::Handled;
4956 }
4957 EventResponse::Ignored
4958 }),
4959 );
4960 tree.push_action(
4961 widget,
4962 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
4963 );
4964 tree.shortcut_registry_mut().register(
4965 Shortcut::new("app.save")
4966 .primary(KeyStroke::command(Key::S))
4967 .build(),
4968 );
4969
4970 tree.layout(SizeProposal::exact(100.0, 50.0));
4971 tree.focus(widget);
4972 tree.press_key(Key::S, Modifiers::COMMAND);
4973 (action_fired.get(), on_key_fired.get())
4974 }
4975
4976 let (action, on_key) = run(true);
4978 assert!(
4979 !action,
4980 "keyboard_capture must suppress the shortcut action"
4981 );
4982 assert!(on_key, "keyboard_capture must deliver the raw KeyDown");
4983
4984 let (action, on_key) = run(false);
4986 assert!(action, "without capture the shortcut must fire");
4987 assert!(!on_key, "without capture the widget must not see the key");
4988 }
4989
4990 #[test]
4991 fn ctrl_tab_always_escapes_a_keyboard_capture_surface() {
4992 use std::cell::Cell;
4993 use std::rc::Rc;
4994
4995 let saw_key = Rc::new(Cell::new(false));
5001 let sk = saw_key.clone();
5002
5003 let mut tree = WidgetTree::new();
5004 let capture = tree.add(
5005 FillWidget::new()
5006 .focusable()
5007 .keyboard_capture(true)
5008 .on_key(move |_event, _ctx| {
5010 sk.set(true);
5011 EventResponse::Handled
5012 }),
5013 );
5014 let neighbour = tree.add(FillWidget::new().focusable());
5015 tree.layout(SizeProposal::exact(100.0, 50.0));
5016
5017 tree.focus(capture);
5020 tree.press_key(Key::Tab, Modifiers::NONE);
5021 assert!(saw_key.get(), "plain Tab must reach the capture surface");
5022 assert_eq!(
5023 tree.focused(),
5024 Some(capture),
5025 "plain Tab must not move focus off a capture surface"
5026 );
5027
5028 saw_key.set(false);
5030 tree.press_key(Key::Tab, Modifiers::CTRL);
5031 assert!(
5032 !saw_key.get(),
5033 "Ctrl+Tab is reserved and must not reach the capture surface"
5034 );
5035 assert_eq!(
5036 tree.focused(),
5037 Some(neighbour),
5038 "Ctrl+Tab must move focus out of a capture surface"
5039 );
5040
5041 tree.focus(capture);
5043 tree.press_key(Key::Tab, Modifiers::CTRL | Modifiers::SHIFT);
5044 assert_eq!(
5045 tree.focused(),
5046 Some(neighbour),
5047 "Ctrl+Shift+Tab must move focus out of a capture surface"
5048 );
5049 }
5050
5051 #[test]
5052 fn scope_mismatch_does_not_invoke_on_activate() {
5053 use crate::intent::Intent;
5054 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
5055 use std::cell::Cell;
5056 use std::rc::Rc;
5057
5058 let activated = Rc::new(Cell::new(false));
5064 let activated_flag = activated.clone();
5065
5066 let mut tree = WidgetTree::new();
5067 let scope_root = tree.add(FillWidget::new().focusable());
5068 let outside = tree.add(FillWidget::new().focusable());
5069
5070 tree.shortcut_registry_mut().register(
5071 Shortcut::new("editor.find")
5072 .primary(KeyStroke::command(Key::F))
5073 .scope(ShortcutScope::Scoped(scope_root))
5074 .on_activate(move |_ks, _ctx| {
5075 activated_flag.set(true);
5076 Intent::new("editor.find")
5077 })
5078 .build(),
5079 );
5080
5081 tree.layout(SizeProposal::exact(200.0, 100.0));
5082 tree.focus(outside);
5083
5084 tree.press_key(Key::F, Modifiers::COMMAND);
5085 assert!(
5086 !activated.get(),
5087 "on_activate must not run when focus is outside the shortcut's scope"
5088 );
5089 }
5090
5091 #[test]
5092 fn key_capture_runs_callback_and_bypasses_registry() {
5093 use crate::action::Action;
5094 use crate::shortcut::{KeyStroke, Shortcut};
5095 use std::cell::Cell;
5096 use std::rc::Rc;
5097
5098 let action_fired = Rc::new(Cell::new(false));
5099 let af = action_fired.clone();
5100 let captured = Rc::new(Cell::new(None));
5101 let cf = captured.clone();
5102
5103 let mut tree = WidgetTree::new();
5104 let widget = tree.add(FillWidget::new().focusable());
5105 tree.push_action(
5106 widget,
5107 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
5108 );
5109 tree.shortcut_registry_mut().register(
5110 Shortcut::new("app.save")
5111 .primary(KeyStroke::command(Key::S))
5112 .build(),
5113 );
5114
5115 tree.layout(SizeProposal::exact(100.0, 50.0));
5116 tree.focus(widget);
5117
5118 let handle = tree.begin_key_capture(move |ks, _reg, _ctx| cf.set(Some(ks)));
5119 assert!(tree.is_capturing_keys());
5120
5121 tree.press_key(Key::S, Modifiers::COMMAND);
5122 assert_eq!(
5123 captured.get(),
5124 Some(KeyStroke::command(Key::S)),
5125 "capture callback must receive the chord"
5126 );
5127 assert!(
5128 !action_fired.get(),
5129 "shortcut action must not fire while capture is armed"
5130 );
5131 assert!(
5132 !tree.is_capturing_keys(),
5133 "capture is one-shot; next KeyDown flows normally"
5134 );
5135 drop(handle);
5136 }
5137
5138 #[test]
5139 fn key_capture_can_rebind_through_registry() {
5140 use crate::shortcut::{KeyStroke, Shortcut};
5141
5142 let mut tree = WidgetTree::new();
5143 let widget = tree.add(FillWidget::new().focusable());
5144 tree.shortcut_registry_mut().register(
5145 Shortcut::new("app.save")
5146 .primary(KeyStroke::command(Key::S))
5147 .build(),
5148 );
5149
5150 tree.layout(SizeProposal::exact(100.0, 50.0));
5151 tree.focus(widget);
5152
5153 let _h = tree.begin_key_capture(|ks, reg, _ctx| {
5155 reg.rebind_primary("app.save", Some(ks));
5156 });
5157
5158 tree.press_key(Key::B, Modifiers::COMMAND | Modifiers::SHIFT);
5159 assert_eq!(
5160 tree.shortcut_registry()
5161 .effective("app.save")
5162 .unwrap()
5163 .primary,
5164 Some(KeyStroke::command_shift(Key::B))
5165 );
5166 }
5167
5168 #[test]
5169 fn dropping_capture_handle_cancels_capture() {
5170 use crate::shortcut::{KeyStroke, Shortcut};
5171 use std::cell::Cell;
5172 use std::rc::Rc;
5173
5174 let action_fired = Rc::new(Cell::new(false));
5175 let af = action_fired.clone();
5176 let capture_fired = Rc::new(Cell::new(false));
5177 let cf = capture_fired.clone();
5178
5179 let mut tree = WidgetTree::new();
5180 let widget = tree.add(FillWidget::new().focusable());
5181 tree.push_action(
5182 widget,
5183 crate::action::Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
5184 );
5185 tree.shortcut_registry_mut().register(
5186 Shortcut::new("app.save")
5187 .primary(KeyStroke::command(Key::S))
5188 .build(),
5189 );
5190 tree.layout(SizeProposal::exact(100.0, 50.0));
5191 tree.focus(widget);
5192
5193 {
5197 let _h = tree.begin_key_capture(move |_ks, _reg, _ctx| cf.set(true));
5198 assert!(tree.is_capturing_keys());
5199 }
5201 assert!(
5202 !tree.is_capturing_keys(),
5203 "dropping the handle must cancel the capture"
5204 );
5205
5206 tree.press_key(Key::S, Modifiers::COMMAND);
5207 assert!(!capture_fired.get(), "cancelled capture must not fire");
5208 assert!(
5209 action_fired.get(),
5210 "shortcut action runs after capture was cancelled"
5211 );
5212 }
5213
5214 #[test]
5215 fn second_begin_key_capture_does_not_racecancel_first() {
5216 use std::cell::Cell;
5217 use std::rc::Rc;
5218
5219 let first = Rc::new(Cell::new(false));
5220 let second = Rc::new(Cell::new(false));
5221 let f = first.clone();
5222 let s = second.clone();
5223
5224 let mut tree = WidgetTree::new();
5225 let widget = tree.add(FillWidget::new().focusable());
5226 tree.layout(SizeProposal::exact(100.0, 50.0));
5227 tree.focus(widget);
5228
5229 let h1 = tree.begin_key_capture(move |_ks, _reg, _ctx| f.set(true));
5235 let _h2 = tree.begin_key_capture(move |_ks, _reg, _ctx| s.set(true));
5236 drop(h1);
5237
5238 assert!(
5239 tree.is_capturing_keys(),
5240 "dropping the older handle must not cancel the active capture"
5241 );
5242 tree.press_key(Key::K, Modifiers::COMMAND);
5243 assert!(!first.get());
5244 assert!(second.get(), "newest capture wins");
5245 }
5246
5247 #[test]
5248 fn capture_callback_can_send_intent() {
5249 use crate::action::Action;
5250 use crate::intent::Intent;
5251
5252 use std::cell::Cell;
5253 use std::rc::Rc;
5254
5255 let ran = Rc::new(Cell::new(false));
5256 let flag = ran.clone();
5257
5258 let mut tree = WidgetTree::new();
5259 let widget = tree.add(FillWidget::new().focusable());
5260 tree.push_action(
5261 widget,
5262 Action::new("app.save").on_invoke(move |_i, _c| flag.set(true)),
5263 );
5264 tree.layout(SizeProposal::exact(100.0, 50.0));
5265 tree.focus(widget);
5266
5267 let _h = tree.begin_key_capture(|_ks, _reg, ctx| {
5268 ctx.send_intent(Intent::new("app.save"));
5269 });
5270 tree.press_key(Key::X, Modifiers::COMMAND);
5271 assert!(
5272 ran.get(),
5273 "intent queued from capture callback must dispatch"
5274 );
5275 }
5276
5277 #[test]
5278 fn binding_registry_does_not_accumulate_across_rebuilds() {
5279 use crate::binding::BindingLevel;
5280 use crate::signal::Signal;
5281
5282 #[derive(Debug)]
5283 struct BoundLeaf {
5284 tick: Signal<u64>,
5285 }
5286 impl crate::widget::Widget for BoundLeaf {
5287 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
5288 self.tick.bind_to(
5289 ctx.self_id(),
5290 ctx.binding_registry(),
5291 BindingLevel::Relayout,
5292 );
5293 Vec::new()
5294 }
5295 fn layout_response(
5296 &self,
5297 proposal: SizeProposal,
5298 _ctx: &crate::widget::LayoutContext,
5299 ) -> crate::widget::LayoutResponse {
5300 proposal.resolve(10.0, 10.0).into()
5301 }
5302 }
5303
5304 let mut tree = WidgetTree::new();
5305 let tick = Signal::new(0_u64);
5306 let widget = tree.add(BoundLeaf { tick: tick.clone() });
5307 tree.layout(SizeProposal::exact(200.0, 200.0));
5308 let after_first_build = tree.binding_registry().len();
5309 assert!(after_first_build >= 1);
5310
5311 for _ in 0..5 {
5315 tree.arena.mark_needs_rebuild(widget);
5316 tree.layout(SizeProposal::exact(200.0, 200.0));
5317 }
5318 assert_eq!(
5319 tree.binding_registry().len(),
5320 after_first_build,
5321 "bindings must be cleared on rebuild"
5322 );
5323
5324 tree.destroy_subtree(widget);
5325 assert_eq!(
5326 tree.binding_registry().len(),
5327 0,
5328 "bindings must be cleared on destroy"
5329 );
5330 let _ = tick;
5332 }
5333
5334 #[test]
5335 fn ctx_destroy_cancels_animations_and_bindings_via_deferred_path() {
5336 use crate::binding::BindingLevel;
5344 use crate::signal::Signal;
5345 use std::time::{Duration, Instant};
5346 use teksilo_tokens::Easing;
5347
5348 #[derive(Debug)]
5349 struct BoundLeaf {
5350 tick: Signal<u64>,
5351 }
5352 impl crate::widget::Widget for BoundLeaf {
5353 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
5354 self.tick.bind_to(
5355 ctx.self_id(),
5356 ctx.binding_registry(),
5357 BindingLevel::Relayout,
5358 );
5359 Vec::new()
5360 }
5361 fn layout_response(
5362 &self,
5363 proposal: SizeProposal,
5364 _ctx: &crate::widget::LayoutContext,
5365 ) -> crate::widget::LayoutResponse {
5366 proposal.resolve(10.0, 10.0).into()
5367 }
5368 }
5369
5370 let mut tree = WidgetTree::new();
5371 let widget = tree.add(BoundLeaf {
5372 tick: Signal::new(0_u64),
5373 });
5374 tree.layout(SizeProposal::exact(200.0, 200.0));
5375 assert!(tree.binding_registry().len() >= 1);
5376
5377 let anim = Signal::<f32>::new_animated(0.0);
5380 tree.animation_scheduler.animate(
5381 &anim,
5382 widget,
5383 1.0,
5384 Duration::from_secs(10),
5385 Easing::Linear,
5386 Instant::now(),
5387 );
5388 assert_eq!(tree.animation_scheduler.active_count(), 1);
5389
5390 let mut noop = crate::window::NoopWindowOps;
5392 tree.run_with_event_context(&mut noop, |ctx| ctx.destroy(widget));
5393
5394 assert_eq!(
5395 tree.animation_scheduler.active_count(),
5396 0,
5397 "ctx.destroy must cancel animations owned by the destroyed widget"
5398 );
5399 assert_eq!(
5400 tree.binding_registry().len(),
5401 0,
5402 "ctx.destroy must unregister the destroyed widget's bindings"
5403 );
5404 assert!(
5405 tree.arena.get(widget).is_none(),
5406 "node must be removed from the arena"
5407 );
5408 }
5409
5410 #[test]
5411 fn clear_shortcut_override_via_event_context_restores_default() {
5412 use crate::shortcut::{KeyStroke, Shortcut};
5413
5414 let mut tree = WidgetTree::new();
5415 tree.shortcut_registry_mut().register(
5416 Shortcut::new("app.save")
5417 .primary(KeyStroke::command(Key::S))
5418 .build(),
5419 );
5420 tree.shortcut_registry_mut()
5421 .rebind_primary("app.save", Some(KeyStroke::alt(Key::S)));
5422
5423 let source = tree.add(FillWidget::new());
5424 let mut ctx = EventContext::new();
5425 ctx.clear_shortcut_override("app.save");
5426 tree.collect_from_ctx(ctx, source);
5427
5428 assert_eq!(
5429 tree.shortcut_registry()
5430 .effective("app.save")
5431 .unwrap()
5432 .primary,
5433 Some(KeyStroke::command(Key::S))
5434 );
5435 }
5436
5437 #[test]
5438 fn rebind_shortcut_primary_via_event_context() {
5439 use crate::shortcut::{KeyStroke, Shortcut};
5440
5441 let mut tree = WidgetTree::new();
5442 tree.shortcut_registry_mut().register(
5443 Shortcut::new("app.save")
5444 .primary(KeyStroke::command(Key::S))
5445 .build(),
5446 );
5447 let source = tree.add(FillWidget::new());
5448
5449 let mut ctx = EventContext::new();
5450 ctx.rebind_shortcut_primary("app.save", Some(KeyStroke::alt(Key::S)));
5451 tree.collect_from_ctx(ctx, source);
5452
5453 assert_eq!(
5454 tree.shortcut_registry()
5455 .effective("app.save")
5456 .unwrap()
5457 .primary,
5458 Some(KeyStroke::alt(Key::S))
5459 );
5460 }
5461
5462 #[test]
5463 fn unregister_all_for_owner_called_on_destroy() {
5464 use crate::shortcut::{KeyStroke, Shortcut};
5465
5466 let mut tree = WidgetTree::new();
5467 let widget = tree.add(FillWidget::new());
5468 let widget_owner = widget;
5469 tree.shortcut_registry_mut().register_owned(
5470 Shortcut::new("scoped.thing")
5471 .primary(KeyStroke::command(Key::K))
5472 .build(),
5473 widget_owner,
5474 );
5475 assert!(
5476 tree.shortcut_registry()
5477 .get_default("scoped.thing")
5478 .is_some()
5479 );
5480
5481 tree.destroy_subtree(widget);
5482 assert!(
5483 tree.shortcut_registry()
5484 .get_default("scoped.thing")
5485 .is_none(),
5486 "destroying the owner must unregister its shortcut"
5487 );
5488 }
5489
5490 #[test]
5494 fn global_action_reached_from_unrelated_source() {
5495 use crate::action::Action;
5496 use crate::intent::Intent;
5497 use std::cell::Cell;
5498 use std::rc::Rc;
5499
5500 #[derive(Debug)]
5501 struct Registrar(Rc<Cell<bool>>);
5502 impl crate::widget::Widget for Registrar {
5503 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
5504 let flag = self.0.clone();
5505 ctx.register_action_global(
5506 Action::new("test.global").on_invoke(move |_i, _c| flag.set(true)),
5507 );
5508 vec![]
5509 }
5510 fn layout_response(
5511 &self,
5512 _p: teksilo_canvas::SizeProposal,
5513 _c: &crate::widget::LayoutContext,
5514 ) -> crate::widget::LayoutResponse {
5515 teksilo_canvas::Size::new(0.0, 0.0).into()
5516 }
5517 }
5518
5519 let mut tree = WidgetTree::new();
5520 let fired = Rc::new(Cell::new(false));
5521 let registrar = tree.add(Registrar(fired.clone()));
5522 let source = tree.add(FillWidget::new()); let mut ops = crate::window::NoopWindowOps;
5524
5525 tree.dispatch_intent(source, Intent::new("test.global"), true, &mut ops);
5526 assert!(
5527 fired.get(),
5528 "global action must fire from an unrelated source"
5529 );
5530
5531 fired.set(false);
5533 tree.destroy_subtree(registrar);
5534 tree.dispatch_intent(source, Intent::new("test.global"), true, &mut ops);
5535 assert!(
5536 !fired.get(),
5537 "destroying the owner must remove its global action"
5538 );
5539 }
5540
5541 #[test]
5553 fn hit_test_through_translate_scope() {
5554 use crate::test_widgets::StackWidget;
5555 let mut tree = WidgetTree::new();
5556 let child = tree.add(FillWidget::new());
5557 let parent = tree.add(StackWidget::new().child(child));
5558 tree.set_transform(parent, teksilo_canvas::Transform2D::translate(100.0, 0.0));
5560 tree.layout(SizeProposal::exact(100.0, 50.0));
5561
5562 assert_eq!(
5566 tree.hit_test(Point::new(50.0, 25.0)),
5567 None,
5568 "pre-transform area is not visually populated and must not hit"
5569 );
5570 assert_eq!(
5572 tree.hit_test(Point::new(150.0, 25.0)),
5573 Some(child),
5574 "visually-rendered area must hit the child"
5575 );
5576 assert_eq!(tree.hit_test(Point::new(250.0, 25.0)), None);
5578 }
5579
5580 #[test]
5581 fn hit_test_through_scale_scope() {
5582 use crate::test_widgets::StackWidget;
5583 let mut tree = WidgetTree::new();
5584 let child = tree.add(FillWidget::new());
5585 let parent = tree.add(StackWidget::new().child(child));
5586 tree.set_transform(parent, teksilo_canvas::Transform2D::scale(0.5, 0.5));
5589 tree.layout(SizeProposal::exact(100.0, 50.0));
5590
5591 assert_eq!(tree.hit_test(Point::new(25.0, 12.0)), Some(child));
5593 assert_eq!(
5596 tree.hit_test(Point::new(75.0, 25.0)),
5597 None,
5598 "scaled-out region must not hit"
5599 );
5600 }
5601
5602 #[test]
5603 fn hit_test_through_nested_transforms_compose() {
5604 use crate::test_widgets::StackWidget;
5605 let mut tree = WidgetTree::new();
5606 let leaf = tree.add(FillWidget::new());
5607 let inner = tree.add(StackWidget::new().child(leaf));
5608 let outer = tree.add(StackWidget::new().child(inner));
5609 tree.set_transform(outer, teksilo_canvas::Transform2D::translate(100.0, 0.0));
5613 tree.set_transform(inner, teksilo_canvas::Transform2D::scale(2.0, 2.0));
5614 tree.layout(SizeProposal::exact(50.0, 25.0));
5615
5616 assert_eq!(tree.hit_test(Point::new(150.0, 25.0)), Some(leaf));
5620 assert_eq!(tree.hit_test(Point::new(50.0, 25.0)), None);
5621 assert_eq!(tree.hit_test(Point::new(250.0, 25.0)), None);
5622 }
5623
5624 #[test]
5625 fn hit_test_identity_transform_unchanged() {
5626 let mut tree = WidgetTree::new();
5630 let widget = tree.add(FillWidget::new());
5631 tree.set_transform(widget, teksilo_canvas::Transform2D::IDENTITY);
5632 tree.layout(SizeProposal::exact(100.0, 50.0));
5633 assert_eq!(tree.hit_test(Point::new(50.0, 25.0)), Some(widget));
5634 }
5635
5636 #[test]
5637 fn arena_effective_transform_composes_ancestors() {
5638 use crate::test_widgets::StackWidget;
5649 let mut tree = WidgetTree::new();
5650 let leaf = tree.add(FillWidget::new());
5651 let inner = tree.add(StackWidget::new().child(leaf));
5652 let outer = tree.add(StackWidget::new().child(inner));
5653 tree.set_transform(outer, teksilo_canvas::Transform2D::translate(100.0, 0.0));
5654 tree.set_transform(inner, teksilo_canvas::Transform2D::scale(2.0, 2.0));
5655 tree.layout(SizeProposal::exact(50.0, 25.0));
5656
5657 let eff = tree.arena.effective_transform(leaf);
5658 let expected = teksilo_canvas::Transform2D::scale(2.0, 2.0)
5659 .then(&teksilo_canvas::Transform2D::translate(100.0, 0.0));
5660 for (a, b) in eff.m.iter().zip(expected.m.iter()) {
5661 assert!(
5662 (a - b).abs() < 1e-5,
5663 "effective_transform mismatch: got {:?}, want {:?}",
5664 eff.m,
5665 expected.m
5666 );
5667 }
5668
5669 let screen_origin = eff.apply_point(Point::new(0.0, 0.0));
5675 assert!((screen_origin.x - 100.0).abs() < 1e-5);
5676 assert!((screen_origin.y - 0.0).abs() < 1e-5);
5677 let screen_corner = eff.apply_point(Point::new(50.0, 25.0));
5680 assert!((screen_corner.x - 200.0).abs() < 1e-5);
5681 assert!((screen_corner.y - 50.0).abs() < 1e-5);
5682 }
5683
5684 #[derive(Debug)]
5689 struct StubMenu;
5690 impl crate::widget::Widget for StubMenu {
5691 fn layout_response(
5692 &self,
5693 _proposal: SizeProposal,
5694 _ctx: &crate::widget::LayoutContext,
5695 ) -> crate::widget::LayoutResponse {
5696 teksilo_canvas::Size::new(100.0, 40.0).into()
5697 }
5698 }
5699
5700 #[derive(Debug)]
5712 struct NominatingWidget {
5713 row: std::cell::Cell<Option<WidgetId>>,
5714 }
5715
5716 impl crate::widget::Widget for NominatingWidget {
5717 fn layout_response(
5718 &self,
5719 proposal: SizeProposal,
5720 _ctx: &LayoutContext,
5721 ) -> crate::widget::LayoutResponse {
5722 proposal.resolve(50.0, 20.0).into()
5723 }
5724
5725 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
5726 ctx.apply_self_handlers(crate::widget_builder::HandlerSet::new().focusable(true));
5727 Vec::new()
5728 }
5729
5730 fn context_menu_key_target(&self) -> Option<WidgetId> {
5731 self.row.get()
5732 }
5733 }
5734
5735 fn press(tree: &mut WidgetTree, key: Key, modifiers: Modifiers) {
5736 tree.dispatch_event(WidgetEvent::KeyDown {
5737 key,
5738 modifiers,
5739 text: None,
5740 });
5741 }
5742
5743 #[test]
5744 fn the_context_menu_key_opens_the_focused_widget_menu() {
5745 use std::cell::Cell;
5746 use std::rc::Rc;
5747
5748 let opened = Rc::new(Cell::new(false));
5749 let flag = opened.clone();
5750 let mut tree = WidgetTree::new();
5751 let widget = tree.add(
5752 FillWidget::new()
5753 .focusable()
5754 .context_menu(move |_pos, _ctx| {
5755 flag.set(true);
5756 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
5757 }),
5758 );
5759 tree.layout(SizeProposal::exact(200.0, 100.0));
5760 tree.focus(widget);
5761
5762 press(&mut tree, Key::ContextMenu, Modifiers::NONE);
5763 assert!(opened.get(), "the dedicated Menu key must open the menu");
5764 }
5765
5766 #[test]
5769 fn shift_f10_opens_the_focused_widget_menu() {
5770 use std::cell::Cell;
5771 use std::rc::Rc;
5772
5773 let opened = Rc::new(Cell::new(false));
5774 let flag = opened.clone();
5775 let mut tree = WidgetTree::new();
5776 let widget = tree.add(
5777 FillWidget::new()
5778 .focusable()
5779 .context_menu(move |_pos, _ctx| {
5780 flag.set(true);
5781 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
5782 }),
5783 );
5784 tree.layout(SizeProposal::exact(200.0, 100.0));
5785 tree.focus(widget);
5786
5787 press(&mut tree, Key::F10, Modifiers::SHIFT);
5788 assert!(opened.get(), "Shift+F10 must open the menu");
5789 }
5790
5791 #[test]
5794 fn a_near_miss_chord_is_not_a_context_menu_request() {
5795 use std::cell::Cell;
5796 use std::rc::Rc;
5797
5798 let opened = Rc::new(Cell::new(false));
5799 let flag = opened.clone();
5800 let mut tree = WidgetTree::new();
5801 let widget = tree.add(
5802 FillWidget::new()
5803 .focusable()
5804 .context_menu(move |_pos, _ctx| {
5805 flag.set(true);
5806 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
5807 }),
5808 );
5809 tree.layout(SizeProposal::exact(200.0, 100.0));
5810 tree.focus(widget);
5811
5812 press(&mut tree, Key::F10, Modifiers::SHIFT | Modifiers::CTRL);
5813 press(&mut tree, Key::F10, Modifiers::NONE);
5814 assert!(!opened.get(), "only Shift+F10 exactly asks for a menu");
5815 }
5816
5817 #[test]
5821 fn the_keyboard_target_can_be_a_row_rather_than_the_focused_container() {
5822 use std::cell::Cell;
5823 use std::rc::Rc;
5824
5825 let menu_owner = Rc::new(Cell::new(None::<&'static str>));
5826
5827 let row_flag = menu_owner.clone();
5828 let container_flag = menu_owner.clone();
5829
5830 let mut tree = WidgetTree::new();
5831 let row = tree.add(FillWidget::new().context_menu(move |_pos, _ctx| {
5832 row_flag.set(Some("row"));
5833 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
5834 }));
5835 let container = tree.add(
5836 crate::test_widgets::StackWidget::new()
5837 .child(row)
5838 .context_menu(move |_pos, _ctx| {
5839 container_flag.set(Some("container"));
5840 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
5841 }),
5842 );
5843 tree.layout(SizeProposal::exact(200.0, 100.0));
5844
5845 let nominator = tree.add(NominatingWidget {
5847 row: std::cell::Cell::new(Some(row)),
5848 });
5849 tree.layout(SizeProposal::exact(200.0, 100.0));
5850 tree.focus(nominator);
5851 let _ = container;
5852
5853 press(&mut tree, Key::ContextMenu, Modifiers::NONE);
5854 assert_eq!(
5855 menu_owner.get(),
5856 Some("row"),
5857 "the nominated row's factory must be the one that runs"
5858 );
5859 }
5860
5861 #[test]
5864 fn the_chord_falls_through_when_there_is_no_menu_to_show() {
5865 use std::cell::Cell;
5866 use std::rc::Rc;
5867
5868 let saw_key = Rc::new(Cell::new(false));
5869 let flag = saw_key.clone();
5870 let mut tree = WidgetTree::new();
5871 let widget = tree.add(FillWidget::new().focusable().on_key(move |ev, _ctx| {
5872 if matches!(ev, WidgetEvent::KeyDown { key: Key::F10, .. }) {
5873 flag.set(true);
5874 }
5875 crate::event::EventResponse::Ignored
5876 }));
5877 tree.layout(SizeProposal::exact(200.0, 100.0));
5878 tree.focus(widget);
5879
5880 press(&mut tree, Key::F10, Modifiers::SHIFT);
5881 assert!(
5882 saw_key.get(),
5883 "with no factory anywhere, the key must reach the widget"
5884 );
5885 }
5886
5887 #[test]
5888 fn context_menu_factory_receives_click_position() {
5889 use crate::event::{Modifiers, PointerButton};
5890 use std::cell::Cell;
5891 use std::rc::Rc;
5892
5893 let captured_position = Rc::new(Cell::new(None::<Point>));
5894 let cap = captured_position.clone();
5895 let mut tree = WidgetTree::new();
5896 let widget = tree.add(FillWidget::new().context_menu(move |pos, _ctx| {
5897 cap.set(Some(pos));
5898 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
5899 }));
5900 tree.layout(SizeProposal::exact(200.0, 100.0));
5901
5902 let click = Point::new(73.0, 42.0);
5903 tree.dispatch_event(WidgetEvent::pointer_down(
5904 click,
5905 PointerButton::Secondary,
5906 Modifiers::NONE,
5907 ));
5908
5909 let got = captured_position.get();
5910 assert_eq!(
5911 got,
5912 Some(click),
5913 "factory must receive the click position; got {:?}",
5914 got
5915 );
5916 let _ = widget;
5917 }
5918
5919 #[test]
5920 fn context_menu_factory_returning_none_falls_through_to_parent() {
5921 use crate::event::{Modifiers, PointerButton};
5922 use crate::test_widgets::StackWidget;
5923 use std::cell::Cell;
5924 use std::rc::Rc;
5925
5926 let outer_called = Rc::new(Cell::new(0_u32));
5930 let outer_flag = outer_called.clone();
5931 let mut tree = WidgetTree::new();
5932 let inner = tree.add(FillWidget::new().context_menu(|_pos, _ctx| None));
5933 let _outer = tree.add(
5934 StackWidget::new()
5935 .child(inner)
5936 .context_menu(move |_pos, _ctx| {
5937 outer_flag.set(outer_flag.get() + 1);
5938 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
5939 }),
5940 );
5941 tree.layout(SizeProposal::exact(200.0, 100.0));
5942
5943 tree.dispatch_event(WidgetEvent::pointer_down(
5944 Point::new(50.0, 25.0),
5945 PointerButton::Secondary,
5946 Modifiers::NONE,
5947 ));
5948
5949 assert_eq!(
5950 outer_called.get(),
5951 1,
5952 "inner returning None must fall through to the outer factory"
5953 );
5954 }
5955
5956 #[test]
5957 fn context_menu_factory_none_throughout_chain_does_not_show_overlay() {
5958 use crate::event::{Modifiers, PointerButton};
5959
5960 let mut tree = WidgetTree::new();
5962 tree.add(FillWidget::new().context_menu(|_pos, _ctx| None));
5963 tree.layout(SizeProposal::exact(200.0, 100.0));
5964
5965 let overlay_count_before = tree.overlay_manager.len();
5966 tree.dispatch_event(WidgetEvent::pointer_down(
5967 Point::new(50.0, 25.0),
5968 PointerButton::Secondary,
5969 Modifiers::NONE,
5970 ));
5971 let overlay_count_after = tree.overlay_manager.len();
5972 assert_eq!(
5973 overlay_count_before, overlay_count_after,
5974 "a factory returning None must not mount any overlay"
5975 );
5976 }
5977
5978 #[derive(Debug)]
5989 struct FreshChildHost {
5990 preserve: bool,
5991 }
5992 impl Widget for FreshChildHost {
5993 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
5994 vec![ctx.add(FillWidget::new())]
5995 }
5996 fn layout_response(
5997 &self,
5998 p: SizeProposal,
5999 _c: &LayoutContext,
6000 ) -> crate::widget::LayoutResponse {
6001 p.resolve(10.0, 10.0).into()
6002 }
6003 fn preserves_children_on_rebuild(&self) -> bool {
6004 self.preserve
6005 }
6006 }
6007
6008 #[test]
6009 fn reconcile_reaps_dropped_children_no_leak() {
6010 for preserve in [false, true] {
6015 let mut tree = WidgetTree::new();
6016 let host = tree.add(FreshChildHost { preserve });
6017 tree.layout(SizeProposal::exact(100.0, 100.0));
6018 let total0 = tree.arena.len();
6019 let active0 = tree.active_widget_count();
6020 for _ in 0..5 {
6021 tree.arena_mark_needs_rebuild_for_testing(host);
6022 tree.layout(SizeProposal::exact(100.0, 100.0));
6023 }
6024 assert_eq!(
6025 tree.arena.len(),
6026 total0,
6027 "preserve={preserve}: dropped children must be reaped, not leaked"
6028 );
6029 assert_eq!(
6030 tree.active_widget_count(),
6031 active0,
6032 "preserve={preserve}: no stranded still-active orphans"
6033 );
6034 }
6035 }
6036
6037 #[derive(Debug)]
6043 struct DetachedContentHost {
6044 preserve: bool,
6045 }
6046 impl Widget for DetachedContentHost {
6047 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6048 let popup = ctx.add_detached(FillWidget::new());
6049 ctx.set_dormant(popup);
6050 vec![ctx.add(FillWidget::new())]
6051 }
6052 fn layout_response(
6053 &self,
6054 p: SizeProposal,
6055 _c: &LayoutContext,
6056 ) -> crate::widget::LayoutResponse {
6057 p.resolve(10.0, 10.0).into()
6058 }
6059 fn preserves_children_on_rebuild(&self) -> bool {
6060 self.preserve
6061 }
6062 }
6063
6064 #[test]
6065 fn rebuilding_reaps_detached_content_no_leak() {
6066 for preserve in [false, true] {
6072 let mut tree = WidgetTree::new();
6073 let host = tree.add(DetachedContentHost { preserve });
6074 tree.layout(SizeProposal::exact(100.0, 100.0));
6075 let total0 = tree.arena.len();
6076 for _ in 0..5 {
6077 tree.arena_mark_needs_rebuild_for_testing(host);
6078 tree.layout(SizeProposal::exact(100.0, 100.0));
6079 }
6080 assert_eq!(
6081 tree.arena.len(),
6082 total0,
6083 "preserve={preserve}: the previous build's detached content must be reaped"
6084 );
6085 }
6086 }
6087
6088 #[test]
6089 fn destroying_a_host_reaps_its_detached_content() {
6090 let mut tree = WidgetTree::new();
6091 let outer = tree.add(FillWidget::new());
6092 tree.layout(SizeProposal::exact(100.0, 100.0));
6093 let empty = tree.arena.len();
6094
6095 let host = tree.add_child(outer, DetachedContentHost { preserve: false });
6096 tree.layout(SizeProposal::exact(100.0, 100.0));
6097 assert!(tree.arena.len() > empty);
6098
6099 tree.destroy_subtree(host);
6100 assert_eq!(
6101 tree.arena.len(),
6102 empty,
6103 "the popup must die with the widget that built it"
6104 );
6105 }
6106
6107 #[derive(Debug)]
6109 struct StableChildHost {
6110 child: Option<WidgetId>,
6111 probe: std::rc::Rc<std::cell::Cell<Option<WidgetId>>>,
6112 }
6113 impl Widget for StableChildHost {
6114 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6115 let id = match self.child {
6116 Some(id) => id,
6117 None => {
6118 let id = ctx.add(FillWidget::new());
6119 self.child = Some(id);
6120 self.probe.set(Some(id));
6121 id
6122 }
6123 };
6124 vec![id]
6125 }
6126 fn layout_response(
6127 &self,
6128 p: SizeProposal,
6129 _c: &LayoutContext,
6130 ) -> crate::widget::LayoutResponse {
6131 p.resolve(10.0, 10.0).into()
6132 }
6133 fn preserves_children_on_rebuild(&self) -> bool {
6134 true
6135 }
6136 }
6137
6138 #[test]
6139 fn reconcile_preserves_reattached_child() {
6140 let probe = std::rc::Rc::new(std::cell::Cell::new(None));
6141 let mut tree = WidgetTree::new();
6142 let host = tree.add(StableChildHost {
6143 child: None,
6144 probe: probe.clone(),
6145 });
6146 tree.layout(SizeProposal::exact(100.0, 100.0));
6147 let child = probe.get().expect("child mounted");
6148 let total0 = tree.arena.len();
6149 for _ in 0..5 {
6150 tree.arena_mark_needs_rebuild_for_testing(host);
6151 tree.layout(SizeProposal::exact(100.0, 100.0));
6152 }
6153 assert!(
6154 tree.arena.is_active(child),
6155 "the re-attached child must survive every rebuild"
6156 );
6157 assert_eq!(tree.arena.len(), total0, "no growth — same child reused");
6158 }
6159
6160 #[derive(Debug)]
6162 struct Wrapper {
6163 child: WidgetId,
6164 }
6165 impl Widget for Wrapper {
6166 fn build(&mut self, _ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6167 vec![self.child]
6168 }
6169 fn layout_response(
6170 &self,
6171 p: SizeProposal,
6172 _c: &LayoutContext,
6173 ) -> crate::widget::LayoutResponse {
6174 p.resolve(10.0, 10.0).into()
6175 }
6176 }
6177
6178 #[derive(Debug)]
6182 struct ReparentHost {
6183 body: Option<WidgetId>,
6184 probe: std::rc::Rc<std::cell::Cell<Option<WidgetId>>>,
6185 }
6186 impl Widget for ReparentHost {
6187 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6188 let body = match self.body {
6189 Some(id) => id,
6190 None => {
6191 let id = ctx.add(FillWidget::new());
6192 self.body = Some(id);
6193 self.probe.set(Some(id));
6194 id
6195 }
6196 };
6197 vec![ctx.add(Wrapper { child: body })]
6198 }
6199 fn layout_response(
6200 &self,
6201 p: SizeProposal,
6202 _c: &LayoutContext,
6203 ) -> crate::widget::LayoutResponse {
6204 p.resolve(10.0, 10.0).into()
6205 }
6206 fn preserves_children_on_rebuild(&self) -> bool {
6207 true
6208 }
6209 }
6210
6211 #[test]
6212 fn reconcile_spares_reparented_survivor() {
6213 let probe = std::rc::Rc::new(std::cell::Cell::new(None));
6220 let mut tree = WidgetTree::new();
6221 let host = tree.add(ReparentHost {
6222 body: None,
6223 probe: probe.clone(),
6224 });
6225 tree.layout(SizeProposal::exact(100.0, 100.0));
6226 let body = probe.get().expect("body mounted");
6227 let total0 = tree.arena.len();
6228 for _ in 0..5 {
6229 tree.arena_mark_needs_rebuild_for_testing(host);
6230 tree.layout(SizeProposal::exact(100.0, 100.0));
6231 }
6232 assert!(
6233 tree.arena.is_active(body),
6234 "the re-parented body must survive — it was moved into the new tree, \
6235 not swept with the dropped wrapper"
6236 );
6237 assert_eq!(
6238 tree.arena.len(),
6239 total0,
6240 "dropped wrappers reaped — no per-rebuild leak"
6241 );
6242 }
6243
6244 #[derive(Debug)]
6254 struct BelowContainer {
6255 child: Option<WidgetId>,
6256 offset: f32,
6257 }
6258
6259 impl crate::widget::Widget for BelowContainer {
6260 fn layout_response(
6261 &self,
6262 proposal: SizeProposal,
6263 _ctx: &crate::widget::LayoutContext,
6264 ) -> crate::widget::LayoutResponse {
6265 proposal.resolve(0.0, 0.0).into()
6266 }
6267 fn place_children(
6268 &self,
6269 bounds: Rect,
6270 _proposal: SizeProposal,
6271 children: &mut [crate::widget::WidgetPlacement],
6272 _ctx: &crate::widget::LayoutContext,
6273 ) {
6274 for c in children.iter_mut() {
6275 c.origin = Point::new(bounds.x, bounds.y + self.offset);
6276 c.size = bounds.size();
6277 }
6278 }
6279 fn children(&self) -> Vec<WidgetId> {
6280 self.child.into_iter().collect()
6281 }
6282 }
6283
6284 fn recording_scroll_container(
6287 tree: &mut WidgetTree,
6288 child: WidgetId,
6289 recorded: std::rc::Rc<std::cell::Cell<Option<Rect>>>,
6290 ) -> WidgetId {
6291 use crate::test_widgets::StackWidget;
6292 tree.add(
6293 StackWidget::new()
6294 .child(child)
6295 .on_scroll(move |ev, _ctx| match ev {
6296 WidgetEvent::ScrollIntoView { target_bounds, .. } => {
6297 recorded.set(Some(*target_bounds));
6298 EventResponse::Handled
6299 }
6300 _ => EventResponse::Ignored,
6301 })
6302 .clips_children(true),
6303 )
6304 }
6305
6306 #[test]
6307 fn ensure_visible_dispatches_scroll_into_view_to_clipping_ancestor() {
6308 use std::cell::Cell;
6309 use std::rc::Rc;
6310 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6311 let mut tree = WidgetTree::new();
6312 let actor = tree.add(FillWidget::new());
6313 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
6314 tree.layout(SizeProposal::exact(100.0, 100.0));
6315
6316 let target = Rect::new(10.0, 500.0, 20.0, 15.0);
6319 let mut ctx = EventContext::new();
6320 ctx.ensure_visible(target);
6321 tree.collect_from_ctx(ctx, actor);
6322
6323 assert_eq!(
6324 recorded.get(),
6325 Some(target),
6326 "ensure_visible(rect) must dispatch ScrollIntoView with the exact rect \
6327 to the clips_children ancestor"
6328 );
6329 }
6330
6331 #[test]
6332 fn ensure_visible_is_noop_when_rect_already_visible() {
6333 use std::cell::Cell;
6334 use std::rc::Rc;
6335 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6336 let mut tree = WidgetTree::new();
6337 let actor = tree.add(FillWidget::new());
6338 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
6339 tree.layout(SizeProposal::exact(100.0, 100.0));
6340
6341 let mut ctx = EventContext::new();
6344 ctx.ensure_visible(Rect::new(10.0, 10.0, 20.0, 15.0));
6345 tree.collect_from_ctx(ctx, actor);
6346
6347 assert_eq!(
6348 recorded.get(),
6349 None,
6350 "a rect already inside the viewport must not trigger a scroll"
6351 );
6352 }
6353
6354 #[test]
6355 fn ensure_visible_margin_forces_scroll_near_edge() {
6356 use std::cell::Cell;
6357 use std::rc::Rc;
6358 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6359 let mut tree = WidgetTree::new();
6360 let actor = tree.add(FillWidget::new());
6361 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
6362 tree.layout(SizeProposal::exact(100.0, 100.0));
6363
6364 let rect = Rect::new(10.0, 95.0, 20.0, 4.0);
6367 let mut ctx = EventContext::new();
6368 ctx.ensure_visible_with_margin(rect, 10.0);
6369 tree.collect_from_ctx(ctx, actor);
6370
6371 assert_eq!(
6372 recorded.get(),
6373 Some(rect),
6374 "the margin must widen the visibility test so a near-edge rect scrolls"
6375 );
6376 }
6377
6378 fn recording_align_container(
6381 tree: &mut WidgetTree,
6382 child: WidgetId,
6383 recorded: std::rc::Rc<
6384 std::cell::Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>,
6385 >,
6386 ) -> WidgetId {
6387 use crate::test_widgets::StackWidget;
6388 tree.add(
6389 StackWidget::new()
6390 .child(child)
6391 .on_scroll(move |ev, _ctx| match ev {
6392 WidgetEvent::ScrollIntoView { align, motion, .. } => {
6393 recorded.set(Some((*align, *motion)));
6394 EventResponse::Handled
6395 }
6396 _ => EventResponse::Ignored,
6397 })
6398 .clips_children(true),
6399 )
6400 }
6401
6402 #[test]
6403 fn ensure_visible_aligned_scrolls_even_when_already_visible() {
6404 use std::cell::Cell;
6405 use std::rc::Rc;
6406 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6407 let mut tree = WidgetTree::new();
6408 let actor = tree.add(FillWidget::new());
6409 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
6410 tree.layout(SizeProposal::exact(100.0, 100.0));
6411
6412 let target = Rect::new(10.0, 10.0, 20.0, 15.0);
6417 let mut ctx = EventContext::new();
6418 ctx.ensure_visible_aligned(target, 0.5, crate::event::ScrollMotion::Instant);
6419 tree.collect_from_ctx(ctx, actor);
6420
6421 assert_eq!(
6422 recorded.get(),
6423 Some(target),
6424 "an aligned reveal must dispatch even when the rect is already visible"
6425 );
6426 }
6427
6428 #[test]
6429 fn ensure_visible_aligned_forwards_fraction_and_motion() {
6430 use std::cell::Cell;
6431 use std::rc::Rc;
6432 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6433 Rc::new(Cell::new(None));
6434 let mut tree = WidgetTree::new();
6435 let actor = tree.add(FillWidget::new());
6436 let _container = recording_align_container(&mut tree, actor, recorded.clone());
6437 tree.layout(SizeProposal::exact(100.0, 100.0));
6438
6439 let mut ctx = EventContext::new();
6440 ctx.ensure_visible_aligned(
6441 Rect::new(10.0, 10.0, 20.0, 15.0),
6442 0.25,
6443 crate::event::ScrollMotion::Smooth,
6444 );
6445 tree.collect_from_ctx(ctx, actor);
6446
6447 assert_eq!(
6448 recorded.get(),
6449 Some((
6450 crate::event::ScrollAlign::Fraction(0.25),
6451 crate::event::ScrollMotion::Smooth
6452 )),
6453 "the container must receive the requested fraction and motion verbatim"
6454 );
6455 }
6456
6457 #[test]
6458 fn ensure_visible_aligned_clamps_the_fraction() {
6459 use std::cell::Cell;
6460 use std::rc::Rc;
6461 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6462 Rc::new(Cell::new(None));
6463 let mut tree = WidgetTree::new();
6464 let actor = tree.add(FillWidget::new());
6465 let _container = recording_align_container(&mut tree, actor, recorded.clone());
6466 tree.layout(SizeProposal::exact(100.0, 100.0));
6467
6468 let mut ctx = EventContext::new();
6469 ctx.ensure_visible_aligned(
6470 Rect::new(10.0, 10.0, 20.0, 15.0),
6471 4.2,
6472 crate::event::ScrollMotion::Instant,
6473 );
6474 tree.collect_from_ctx(ctx, actor);
6475
6476 assert_eq!(
6477 recorded.get().map(|(a, _)| a),
6478 Some(crate::event::ScrollAlign::Fraction(1.0)),
6479 "an out-of-range fraction must clamp rather than aim the pin off-screen"
6480 );
6481 }
6482
6483 #[test]
6484 fn plain_ensure_visible_requests_minimal_alignment() {
6485 use std::cell::Cell;
6486 use std::rc::Rc;
6487 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6488 Rc::new(Cell::new(None));
6489 let mut tree = WidgetTree::new();
6490 let actor = tree.add(FillWidget::new());
6491 let _container = recording_align_container(&mut tree, actor, recorded.clone());
6492 tree.layout(SizeProposal::exact(100.0, 100.0));
6493
6494 let mut ctx = EventContext::new();
6495 ctx.ensure_visible(Rect::new(10.0, 500.0, 20.0, 15.0));
6496 tree.collect_from_ctx(ctx, actor);
6497
6498 assert_eq!(
6499 recorded.get(),
6500 Some((
6501 crate::event::ScrollAlign::Minimal,
6502 crate::event::ScrollMotion::Instant
6503 )),
6504 "the pre-existing reveal API must keep its exact semantics"
6505 );
6506 }
6507
6508 #[test]
6509 fn only_the_innermost_container_aligns() {
6510 use std::cell::Cell;
6511 use std::rc::Rc;
6512 let inner_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6513 Rc::new(Cell::new(None));
6514 let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6515 Rc::new(Cell::new(None));
6516
6517 let mut tree = WidgetTree::new();
6518 let actor = tree.add(FillWidget::new());
6519 let inner = recording_align_container(&mut tree, actor, inner_rec.clone());
6520 let _outer = recording_align_container(&mut tree, inner, outer_rec.clone());
6521 tree.layout(SizeProposal::exact(100.0, 100.0));
6522
6523 let mut ctx = EventContext::new();
6526 ctx.ensure_visible_aligned(
6527 Rect::new(10.0, 500.0, 20.0, 15.0),
6528 0.5,
6529 crate::event::ScrollMotion::Instant,
6530 );
6531 tree.collect_from_ctx(ctx, actor);
6532
6533 assert_eq!(
6534 inner_rec.get().map(|(a, _)| a),
6535 Some(crate::event::ScrollAlign::Fraction(0.5)),
6536 "the innermost clipping ancestor owns the pin"
6537 );
6538 assert_eq!(
6539 outer_rec.get().map(|(a, _)| a),
6540 Some(crate::event::ScrollAlign::Minimal),
6541 "an outer container must only bring the inner viewport into view — a \
6542 fraction names a height in one viewport, not in every ancestor's"
6543 );
6544 }
6545
6546 #[test]
6547 fn ensure_widget_visible_uses_target_arena_bounds() {
6548 use std::cell::Cell;
6549 use std::rc::Rc;
6550 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6551 let mut tree = WidgetTree::new();
6552 let target = tree.add(FillWidget::new());
6554 let rec = recorded.clone();
6555 let container = tree.add(
6556 BelowContainer {
6557 child: Some(target),
6558 offset: 500.0,
6559 }
6560 .on_scroll(move |ev, _ctx| match ev {
6561 WidgetEvent::ScrollIntoView { target_bounds, .. } => {
6562 rec.set(Some(*target_bounds));
6563 EventResponse::Handled
6564 }
6565 _ => EventResponse::Ignored,
6566 })
6567 .clips_children(true),
6568 );
6569 tree.layout(SizeProposal::exact(100.0, 100.0));
6570
6571 let expected = tree.bounds(target);
6572 assert!(
6573 expected.y > 100.0,
6574 "fixture sanity: the target must sit below the viewport (y={})",
6575 expected.y
6576 );
6577
6578 let mut ctx = EventContext::new();
6581 ctx.ensure_widget_visible(target);
6582 tree.collect_from_ctx(ctx, container);
6583
6584 assert_eq!(
6585 recorded.get(),
6586 Some(expected),
6587 "ensure_widget_visible(id) must dispatch ScrollIntoView with the \
6588 target's current arena bounds"
6589 );
6590 }
6591
6592 #[test]
6593 fn ensure_widget_visible_ignores_missing_widget() {
6594 use std::cell::Cell;
6596 use std::rc::Rc;
6597 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6598 let mut tree = WidgetTree::new();
6599 let actor = tree.add(FillWidget::new());
6600 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
6601 tree.layout(SizeProposal::exact(100.0, 100.0));
6602
6603 let mut ctx = EventContext::new();
6604 ctx.ensure_widget_visible(WidgetId::default());
6605 tree.collect_from_ctx(ctx, actor); assert_eq!(
6608 recorded.get(),
6609 None,
6610 "an unmounted id must not trigger a scroll"
6611 );
6612 }
6613
6614 #[test]
6615 fn context_menu_inside_a_modal_keeps_the_modal() {
6616 use crate::event::{Modifiers, PointerButton, WidgetEvent};
6620 use crate::overlay::{DismissBehavior, OverlayLayer, OverlayPlacement, OverlayRequest};
6621 use crate::test_widgets::{FillWidget, StackWidget};
6622
6623 let mut tree = WidgetTree::new();
6624 let modal_content = tree.add(StackWidget::new());
6627 let _editor = tree.add_child(
6628 modal_content,
6629 FillWidget::new()
6630 .context_menu(|_pos, _ctx| Some(Box::new(FillWidget::new()) as Box<dyn Widget>)),
6631 );
6632 tree.layout(SizeProposal::exact(200.0, 100.0));
6633
6634 let modal = tree.overlay_manager.show(OverlayRequest {
6635 content_id: modal_content,
6636 anchor: modal_content,
6637 placement: OverlayPlacement::Centered,
6638 dismiss: DismissBehavior::EscapeKey,
6639 layer: OverlayLayer::InTree,
6640 parent_overlay: None,
6641 on_dismiss: None,
6642 fade_duration: None,
6643 });
6644 tree.overlay_manager
6646 .stack
6647 .iter_mut()
6648 .find(|o| o.id == modal)
6649 .unwrap()
6650 .bounds = Rect::new(0.0, 0.0, 200.0, 100.0);
6651 assert_eq!(tree.overlay_manager.len(), 1);
6652
6653 tree.dispatch_event(WidgetEvent::pointer_down(
6655 Point::new(50.0, 25.0),
6656 PointerButton::Secondary,
6657 Modifiers::NONE,
6658 ));
6659
6660 assert!(
6661 tree.overlay_manager.active_ids().contains(&modal),
6662 "the modal must survive opening a context menu inside it"
6663 );
6664 assert_eq!(
6665 tree.overlay_manager.len(),
6666 2,
6667 "the context menu should now be open on top of the surviving modal"
6668 );
6669 }
6670}
6671
6672#[cfg(test)]
6675mod input_ingress_tests {
6676 use super::*;
6677 use crate::event::{EventResponse, Modifiers, PointerButton, ScrollDelta};
6678 use crate::pointer::{
6679 EventTime, PointerId, PointerInfo, PointerPhase, PointerSample, ScrollPhase, ScrollSample,
6680 ScrollSource,
6681 };
6682 use crate::test_widgets::FillWidget;
6683 use crate::widget::{LayoutContext, WidgetPlacement};
6684 use crate::widget_builder::WidgetBuilder;
6685 use std::cell::RefCell;
6686 use std::rc::Rc;
6687
6688 #[derive(Debug)]
6695 struct RowWidget {
6696 children: Vec<WidgetId>,
6697 }
6698
6699 impl crate::widget::Widget for RowWidget {
6700 fn layout_response(
6701 &self,
6702 proposal: SizeProposal,
6703 _ctx: &LayoutContext,
6704 ) -> crate::widget::LayoutResponse {
6705 proposal.resolve(0.0, 0.0).into()
6706 }
6707
6708 fn place_children(
6709 &self,
6710 bounds: Rect,
6711 _proposal: SizeProposal,
6712 children: &mut [WidgetPlacement],
6713 _ctx: &LayoutContext,
6714 ) {
6715 let n = children.len().max(1) as f32;
6716 let w = bounds.width / n;
6717 for (i, child) in children.iter_mut().enumerate() {
6718 child.origin = Point::new(bounds.x + w * i as f32, bounds.y);
6719 child.size = teksilo_canvas::Size::new(w, bounds.height);
6720 }
6721 }
6722
6723 fn children(&self) -> Vec<WidgetId> {
6724 self.children.clone()
6725 }
6726 }
6727
6728 fn record(drive: impl FnOnce(&mut WidgetTree)) -> Vec<String> {
6731 let log: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(Vec::new()));
6732
6733 let mut tree = WidgetTree::new();
6734 let pointer_log = log.clone();
6735 let hover_log = log.clone();
6736 tree.add(
6737 FillWidget::new()
6738 .on_pointer_event(move |event, _ctx| {
6739 pointer_log.borrow_mut().push(format!("{event:?}"));
6740 EventResponse::Ignored
6741 })
6742 .on_hover(move |entered, _ctx| {
6743 hover_log.borrow_mut().push(format!("hover({entered})"));
6744 }),
6745 );
6746 tree.layout(SizeProposal::exact(100.0, 100.0));
6747
6748 drive(&mut tree);
6749 log.borrow().clone()
6750 }
6751
6752 #[test]
6758 fn a_mouse_sample_reproduces_todays_event_stream() {
6759 let inside = Point::new(40.0, 40.0);
6760 let moved = Point::new(60.0, 55.0);
6761 let outside = Point::new(400.0, 400.0);
6762
6763 let legacy = record(|tree| {
6764 tree.dispatch_event(WidgetEvent::pointer_move(inside));
6765 tree.dispatch_event(WidgetEvent::pointer_down(
6766 inside,
6767 PointerButton::Primary,
6768 Modifiers::NONE,
6769 ));
6770 tree.dispatch_event(WidgetEvent::pointer_move(moved));
6771 tree.dispatch_event(WidgetEvent::pointer_up(
6772 moved,
6773 PointerButton::Primary,
6774 Modifiers::NONE,
6775 ));
6776 tree.dispatch_event(WidgetEvent::pointer_move(outside));
6777 });
6778
6779 let sampled = record(|tree| {
6780 let t = EventTime::ZERO;
6781 tree.dispatch_pointer(PointerSample::mouse(PointerPhase::Move, inside, t));
6782 tree.dispatch_pointer(
6783 PointerSample::mouse(PointerPhase::Down, inside, t)
6784 .with_button(PointerButton::Primary),
6785 );
6786 tree.dispatch_pointer(PointerSample::mouse(PointerPhase::Move, moved, t));
6787 tree.dispatch_pointer(
6788 PointerSample::mouse(PointerPhase::Up, moved, t)
6789 .with_button(PointerButton::Primary),
6790 );
6791 tree.dispatch_pointer(PointerSample::mouse(PointerPhase::Move, outside, t));
6792 });
6793
6794 assert_eq!(legacy, sampled);
6795 assert!(
6796 legacy.contains(&"hover(true)".to_string())
6797 && legacy.contains(&"hover(false)".to_string()),
6798 "the fixture must actually exercise enter and leave: {legacy:?}"
6799 );
6800 }
6801
6802 #[test]
6806 fn a_buttonless_press_lowers_to_primary() {
6807 let events = record(|tree| {
6808 tree.dispatch_pointer(PointerSample::mouse(
6809 PointerPhase::Down,
6810 Point::new(20.0, 20.0),
6811 EventTime::ZERO,
6812 ));
6813 });
6814 assert!(
6815 events.iter().any(|e| e.contains("button: Primary")),
6816 "{events:?}"
6817 );
6818 }
6819
6820 fn scroll_fixture() -> (WidgetTree, Rc<RefCell<Vec<&'static str>>>) {
6825 let hits: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
6826 let mut tree = WidgetTree::new();
6827
6828 let leading_hits = hits.clone();
6829 let trailing_hits = hits.clone();
6830 let leading = tree.add(FillWidget::new().on_scroll(move |_event, _ctx| {
6831 leading_hits.borrow_mut().push("leading");
6832 EventResponse::Handled
6833 }));
6834 let trailing = tree.add(FillWidget::new().on_scroll(move |_event, _ctx| {
6835 trailing_hits.borrow_mut().push("trailing");
6836 EventResponse::Handled
6837 }));
6838 tree.add(RowWidget {
6839 children: vec![leading, trailing],
6840 });
6841 tree.layout(SizeProposal::exact(200.0, 100.0));
6842 (tree, hits)
6843 }
6844
6845 fn notch() -> ScrollDelta {
6846 ScrollDelta::Lines { x: 0.0, y: -1.0 }
6847 }
6848
6849 #[test]
6852 fn a_positionless_scroll_routes_by_hover() {
6853 let (mut tree, hits) = scroll_fixture();
6854
6855 tree.pointer_move(Point::new(150.0, 50.0)); tree.dispatch_event(WidgetEvent::scroll(notch(), Modifiers::NONE));
6857 assert_eq!(*hits.borrow(), vec!["trailing"]);
6858
6859 tree.pointer_move(Point::new(50.0, 50.0)); tree.dispatch_scroll(ScrollSample::wheel(
6861 notch(),
6862 Modifiers::NONE,
6863 EventTime::ZERO,
6864 ));
6865 assert_eq!(*hits.borrow(), vec!["trailing", "leading"]);
6866 }
6867
6868 #[test]
6873 fn a_positioned_scroll_routes_by_hit_test() {
6874 let (mut tree, hits) = scroll_fixture();
6875 tree.pointer_move(Point::new(150.0, 50.0)); tree.dispatch_event(WidgetEvent::scroll_at(
6878 notch(),
6879 Modifiers::NONE,
6880 Point::new(50.0, 50.0), ));
6882 assert_eq!(*hits.borrow(), vec!["leading"]);
6883
6884 tree.dispatch_scroll(
6885 ScrollSample::wheel(notch(), Modifiers::NONE, EventTime::ZERO)
6886 .at(Point::new(150.0, 50.0)),
6887 );
6888 assert_eq!(*hits.borrow(), vec!["leading", "trailing"]);
6889 }
6890
6891 #[test]
6895 fn a_positionless_scroll_with_no_hover_goes_nowhere() {
6896 let (mut tree, hits) = scroll_fixture();
6897 tree.dispatch_event(WidgetEvent::scroll(notch(), Modifiers::NONE));
6898 assert!(hits.borrow().is_empty());
6899 }
6900
6901 #[test]
6906 fn a_handler_sees_the_sample_it_is_serving() {
6907 let seen: Rc<RefCell<Vec<(ScrollPhase, ScrollSource, Option<Point>)>>> =
6908 Rc::new(RefCell::new(Vec::new()));
6909 let sink = seen.clone();
6910
6911 let mut tree = WidgetTree::new();
6912 tree.add(
6919 FillWidget::new()
6920 .scroll_container(crate::pointer::touch_action::PanAxes::BOTH)
6921 .on_scroll(move |_event, ctx| {
6922 sink.borrow_mut().push((
6923 ctx.scroll_phase(),
6924 ctx.scroll_source(),
6925 ctx.pointer_position(),
6926 ));
6927 EventResponse::Handled
6928 }),
6929 );
6930 tree.layout(SizeProposal::exact(100.0, 100.0));
6931
6932 tree.dispatch_scroll(ScrollSample {
6933 delta: notch(),
6934 position: Some(Point::new(50.0, 50.0)),
6935 phase: ScrollPhase::Momentum,
6936 source: ScrollSource::TouchPan,
6937 pointer: PointerInfo::mouse(EventTime::from_millis(12)),
6938 modifiers: Modifiers::NONE,
6939 });
6940
6941 assert_eq!(
6942 *seen.borrow(),
6943 vec![(
6944 ScrollPhase::Momentum,
6945 ScrollSource::TouchPan,
6946 Some(Point::new(50.0, 50.0))
6947 )]
6948 );
6949 }
6950
6951 #[test]
6954 fn a_legacy_event_reports_the_mouse_at_the_epoch() {
6955 let seen: Rc<RefCell<Option<(PointerId, ScrollPhase)>>> = Rc::new(RefCell::new(None));
6956 let sink = seen.clone();
6957
6958 let mut tree = WidgetTree::new();
6959 tree.add(FillWidget::new().on_pointer_event(move |_event, ctx| {
6960 *sink.borrow_mut() = Some((ctx.pointer().id, ctx.scroll_phase()));
6961 EventResponse::Ignored
6962 }));
6963 tree.layout(SizeProposal::exact(100.0, 100.0));
6964 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(50.0, 50.0)));
6965
6966 assert_eq!(
6967 *seen.borrow(),
6968 Some((PointerId::MOUSE, ScrollPhase::Discrete))
6969 );
6970 }
6971
6972 #[test]
6976 fn the_snapshot_is_restored_after_a_dispatch() {
6977 let mut tree = WidgetTree::new();
6978 tree.add(FillWidget::new());
6979 tree.layout(SizeProposal::exact(100.0, 100.0));
6980
6981 let before = tree.current_input.clone();
6982 tree.dispatch_scroll(
6983 ScrollSample::wheel(notch(), Modifiers::NONE, EventTime::ZERO)
6984 .at(Point::new(10.0, 10.0)),
6985 );
6986 assert_eq!(tree.current_input, before);
6987 }
6988}
6989
6990#[cfg(test)]
6991mod press_and_focus_tests {
6992 use std::cell::{Cell, RefCell};
6997 use std::rc::Rc;
6998 use teksilo_canvas::{Point, SizeProposal};
6999
7000 use crate::WidgetId;
7001 use crate::event::{EventResponse, Key, Modifiers, PointerButton, WidgetEvent};
7002 use crate::focus::FocusOrigin;
7003 use crate::pointer::clock::ManualClock;
7004 use crate::pointer::touch_action::PanClaim;
7005 use crate::pointer::{
7006 BackendDeviceKey, EventTime, PointerId, PointerIdAllocator, PointerInfo, PointerPhase,
7007 PointerSample,
7008 };
7009 use crate::test_widgets::{FillWidget, StackWidget};
7010 use crate::widget_builder::WidgetBuilder;
7011 use crate::widget_tree::WidgetTree;
7012
7013 fn contact_id() -> PointerId {
7019 use std::sync::atomic::{AtomicU64, Ordering};
7020 static NEXT: AtomicU64 = AtomicU64::new(1);
7021 PointerIdAllocator::global().begin(
7022 BackendDeviceKey::new(0x0B11),
7023 NEXT.fetch_add(1, Ordering::Relaxed),
7024 )
7025 }
7026
7027 fn contact(id: PointerId, phase: PointerPhase, at: Point, t: EventTime) -> PointerSample {
7028 PointerSample {
7029 pointer: PointerInfo::touch(id, t),
7030 phase,
7031 position: at,
7032 button: None,
7033 modifiers: Modifiers::NONE,
7034 coalesced: Vec::new(),
7035 }
7036 }
7037
7038 fn tree_on_a_clock() -> (WidgetTree, Rc<ManualClock>) {
7041 let mut tree = WidgetTree::new();
7042 let clock = Rc::new(ManualClock::new(EventTime::ZERO));
7043 tree.set_input_clock(clock.clone());
7044 (tree, clock)
7045 }
7046
7047 fn tappable(tree: &mut WidgetTree) -> (WidgetId, crate::signal::Signal<bool>) {
7054 let id = tree.add(FillWidget::new().focusable().on_tap(|_e, _c| {}));
7055 let signal = tree.pressed_signal(id);
7056 (id, signal)
7057 }
7058
7059 #[test]
7066 fn a_pointer_origin_names_its_device() {
7067 use teksilo_tokens::{PenKind, PointerKind};
7068
7069 assert!(FocusOrigin::Pointer(PointerKind::Touch).is_pointer());
7070 assert!(FocusOrigin::Pointer(PointerKind::Pen(PenKind::Pen)).is_pointer());
7071 assert!(FocusOrigin::POINTER.is_pointer());
7072 assert!(!FocusOrigin::Keyboard.is_pointer());
7073 assert!(!FocusOrigin::Programmatic.is_pointer());
7074 assert!(!FocusOrigin::Accessibility.is_pointer());
7075
7076 assert_eq!(
7077 FocusOrigin::Pointer(PointerKind::Touch).pointer_kind(),
7078 Some(PointerKind::Touch),
7079 );
7080 assert_eq!(FocusOrigin::Keyboard.pointer_kind(), None);
7081 assert_eq!(
7082 FocusOrigin::POINTER.pointer_kind(),
7083 Some(PointerKind::Unknown),
7084 "a widget deriving its own origin says so rather than naming a device it never saw",
7085 );
7086 }
7087
7088 #[test]
7092 fn only_a_real_navigation_declares_a_modality() {
7093 use teksilo_tokens::PointerKind;
7094
7095 assert_eq!(FocusOrigin::Keyboard.focus_visible(), Some(true));
7096 assert_eq!(FocusOrigin::Accessibility.focus_visible(), Some(true));
7097 assert_eq!(
7098 FocusOrigin::Pointer(PointerKind::Touch).focus_visible(),
7099 Some(false),
7100 );
7101 assert_eq!(FocusOrigin::Programmatic.focus_visible(), None);
7102 }
7103
7104 #[test]
7107 fn the_tier_three_configs_are_unchanged() {
7108 let origin: crate::signal::Signal<Option<FocusOrigin>> =
7109 crate::signal::Signal::new(Some(FocusOrigin::Keyboard));
7110 let slider: crate::signal::Signal<Option<FocusOrigin>> = origin.clone();
7111 let splitter: crate::signal::Signal<Option<FocusOrigin>> = origin.clone();
7112 let segmented: crate::signal::Signal<Option<FocusOrigin>> = origin.clone();
7113 for field in [slider, splitter, segmented] {
7114 assert_eq!(field.get(), Some(FocusOrigin::Keyboard));
7115 }
7116 origin.set(Some(FocusOrigin::POINTER));
7117 assert_ne!(
7118 origin.get(),
7119 Some(FocusOrigin::Keyboard),
7120 "the `== Some(Keyboard)` test every consumer makes still discriminates",
7121 );
7122 }
7123
7124 #[test]
7135 fn a_touch_tap_after_keyboard_focus_leaves_no_ring() {
7136 let (mut tree, _clock) = tree_on_a_clock();
7137 let a = tree.add(FillWidget::new().focusable());
7138 let b = tree.add(FillWidget::new().focusable());
7139 let root = tree.add(SideBySide {
7140 children: vec![a, b],
7141 });
7142 tree.layout(SizeProposal::exact(200.0, 100.0));
7143 let _ = root;
7144
7145 let visible = tree.focus_visible_signal();
7146 tree.press_key(Key::Tab, Modifiers::NONE);
7147 assert_eq!(tree.focused(), Some(a));
7148 assert!(visible.get(), "keyboard navigation reveals the ring");
7149
7150 let id = contact_id();
7151 let at = tree.bounds(b).center();
7152 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
7153 tree.dispatch_pointer(contact(
7154 id,
7155 PointerPhase::Up,
7156 at,
7157 EventTime::from_millis(30),
7158 ));
7159
7160 assert_eq!(tree.focused(), Some(b), "the release moved focus");
7161 assert!(!visible.get(), "and the ring did not come with it");
7162 assert_eq!(
7163 tree.focus_origin(),
7164 Some(FocusOrigin::Pointer(teksilo_tokens::PointerKind::Touch)),
7165 );
7166 }
7167
7168 #[test]
7173 fn an_assistive_focus_reveals_the_ring() {
7174 let mut tree = WidgetTree::new();
7175 let a = tree.add(FillWidget::new().focusable());
7176 let b = tree.add(FillWidget::new().focusable());
7177 let root = tree.add(SideBySide {
7178 children: vec![a, b],
7179 });
7180 tree.layout(SizeProposal::exact(200.0, 50.0));
7181 let _ = root;
7182 let visible = tree.focus_visible_signal();
7183
7184 tree.click(a);
7185 assert_eq!(tree.focused(), Some(a));
7186 assert!(!visible.get(), "the click hid it");
7187
7188 tree.dispatch_event(WidgetEvent::AccessAction {
7189 target: Some(b),
7190 action: accesskit::Action::Focus,
7191 target_node: crate::accessibility::widget_id_to_node_id(b),
7192 data: None,
7193 });
7194 assert_eq!(tree.focused(), Some(b));
7195 assert!(visible.get(), "an assistive move is a navigation");
7196 assert_eq!(tree.focus_origin(), Some(FocusOrigin::Accessibility));
7197 }
7198
7199 #[test]
7204 fn a_programmatic_focus_leaves_the_modality_alone() {
7205 let mut tree = WidgetTree::new();
7206 let a = tree.add(FillWidget::new().focusable());
7207 let b = tree.add(FillWidget::new().focusable());
7208 let root = tree.add(SideBySide {
7209 children: vec![a, b],
7210 });
7211 tree.layout(SizeProposal::exact(200.0, 100.0));
7212 let _ = root;
7213 let visible = tree.focus_visible_signal();
7214
7215 tree.press_key(Key::Tab, Modifiers::NONE);
7216 assert_eq!(tree.focused(), Some(a));
7217 assert!(visible.get());
7218 tree.focus(b);
7219 assert!(
7220 visible.get(),
7221 "a scripted focus does not hide a keyboard ring"
7222 );
7223
7224 tree.click(a);
7225 assert!(!visible.get());
7226 tree.focus(b);
7227 assert!(
7228 !visible.get(),
7229 "and does not reveal one after a click either",
7230 );
7231 }
7232
7233 #[test]
7239 fn a_mouse_still_focuses_on_press() {
7240 let mut tree = WidgetTree::new();
7241 let w = tree.add(FillWidget::new().focusable());
7242 tree.layout(SizeProposal::exact(100.0, 50.0));
7243 let center = tree.bounds(w).center();
7244
7245 tree.dispatch_event(WidgetEvent::pointer_down(
7246 center,
7247 PointerButton::Primary,
7248 Modifiers::NONE,
7249 ));
7250 assert_eq!(
7251 tree.focused(),
7252 Some(w),
7253 "focus lands on the press for an indirect pointer",
7254 );
7255 }
7256
7257 #[test]
7259 fn a_finger_focuses_on_release() {
7260 let (mut tree, _clock) = tree_on_a_clock();
7261 let w = tree.add(FillWidget::new().focusable());
7262 tree.layout(SizeProposal::exact(100.0, 50.0));
7263 let center = tree.bounds(w).center();
7264
7265 let id = contact_id();
7266 tree.dispatch_pointer(contact(id, PointerPhase::Down, center, EventTime::ZERO));
7267 assert_eq!(
7268 tree.focused(),
7269 None,
7270 "a finger that has only landed has chosen nothing yet",
7271 );
7272
7273 tree.dispatch_pointer(contact(
7274 id,
7275 PointerPhase::Up,
7276 center,
7277 EventTime::from_millis(40),
7278 ));
7279 assert_eq!(tree.focused(), Some(w));
7280 assert_eq!(
7281 tree.focus_origin().and_then(FocusOrigin::pointer_kind),
7282 Some(teksilo_tokens::PointerKind::Touch),
7283 "the origin names the device that delivered it",
7284 );
7285 }
7286
7287 #[test]
7291 fn a_release_on_a_different_focusable_moves_no_focus() {
7292 let (mut tree, _clock) = tree_on_a_clock();
7293 let a = tree.add(FillWidget::new().focusable());
7294 let b = tree.add(FillWidget::new().focusable());
7295 let root = tree.add(SideBySide {
7296 children: vec![a, b],
7297 });
7298 tree.layout(SizeProposal::exact(200.0, 50.0));
7299 let _ = root;
7300
7301 let on_a = tree.bounds(a).center();
7302 let on_b = tree.bounds(b).center();
7303 let id = contact_id();
7304 tree.dispatch_pointer(contact(id, PointerPhase::Down, on_a, EventTime::ZERO));
7305 tree.dispatch_pointer(contact(
7306 id,
7307 PointerPhase::Move,
7308 on_b,
7309 EventTime::from_millis(20),
7310 ));
7311 tree.dispatch_pointer(contact(
7312 id,
7313 PointerPhase::Up,
7314 on_b,
7315 EventTime::from_millis(40),
7316 ));
7317
7318 assert_eq!(
7319 tree.focused(),
7320 None,
7321 "the guard is `the release landed on the same focusable as the press`",
7322 );
7323 }
7324
7325 #[test]
7332 fn a_mouse_press_visual_is_unchanged() {
7333 let mut tree = WidgetTree::new();
7334 let (w, pressed) = tappable(&mut tree);
7335 tree.layout(SizeProposal::exact(100.0, 50.0));
7336 let center = tree.bounds(w).center();
7337
7338 assert!(!pressed.get(), "not pressed at rest");
7339 tree.dispatch_event(WidgetEvent::pointer_down(
7340 center,
7341 PointerButton::Primary,
7342 Modifiers::NONE,
7343 ));
7344 assert!(pressed.get(), "an indirect pointer lights up on the press");
7345 assert!(
7346 !tree.press_pending(w),
7347 "and never waits: a mouse opens no pan session, so there is no \
7348 ambiguity to wait out",
7349 );
7350 assert_eq!(tree.pressed_by(w), Some(PointerId::MOUSE));
7351
7352 tree.dispatch_event(WidgetEvent::pointer_up(
7353 center,
7354 PointerButton::Primary,
7355 Modifiers::NONE,
7356 ));
7357 assert!(!pressed.get(), "the release clears it");
7358 assert_eq!(tree.pressed_by(w), None);
7359 }
7360
7361 #[test]
7365 fn a_slide_off_clears_the_visual_and_re_entry_restores_it() {
7366 let (mut tree, _clock) = tree_on_a_clock();
7367 let (w, pressed) = tappable(&mut tree);
7368 tree.layout(SizeProposal::exact(100.0, 200.0));
7369 let inside = tree.bounds(w).center();
7370 let outside = Point::new(inside.x, inside.y + 400.0);
7371
7372 let id = contact_id();
7373 tree.dispatch_pointer(contact(id, PointerPhase::Down, inside, EventTime::ZERO));
7374 assert!(
7375 pressed.get(),
7376 "nothing here claims a pan, so no delay applies"
7377 );
7378
7379 tree.dispatch_pointer(contact(
7380 id,
7381 PointerPhase::Move,
7382 outside,
7383 EventTime::from_millis(20),
7384 ));
7385 assert!(!pressed.get(), "the press has left its target");
7386 assert!(!tree.press_is_inside(w));
7387 assert_eq!(
7388 tree.pressed_by(w),
7389 Some(id),
7390 "the contact still holds the press — it is the *visual* that is off",
7391 );
7392
7393 tree.dispatch_pointer(contact(
7394 id,
7395 PointerPhase::Move,
7396 inside,
7397 EventTime::from_millis(40),
7398 ));
7399 assert!(pressed.get(), "sliding back on restores it");
7400
7401 tree.dispatch_pointer(contact(
7402 id,
7403 PointerPhase::Up,
7404 inside,
7405 EventTime::from_millis(60),
7406 ));
7407 assert!(!pressed.get());
7408 }
7409
7410 #[test]
7413 fn a_second_contact_cannot_clear_the_first_visual() {
7414 let (mut tree, _clock) = tree_on_a_clock();
7415 let (w, pressed) = tappable(&mut tree);
7416 tree.layout(SizeProposal::exact(100.0, 50.0));
7417 let bounds = tree.bounds(w);
7418 let first_at = Point::new(bounds.x + 20.0, bounds.y + 25.0);
7419 let second_at = Point::new(bounds.x + 70.0, bounds.y + 25.0);
7420
7421 let first = contact_id();
7422 let second = contact_id();
7423 tree.dispatch_pointer(contact(
7424 first,
7425 PointerPhase::Down,
7426 first_at,
7427 EventTime::ZERO,
7428 ));
7429 assert!(pressed.get());
7430 assert_eq!(tree.pressed_by(w), Some(first));
7431
7432 tree.dispatch_pointer(contact(
7433 second,
7434 PointerPhase::Down,
7435 second_at,
7436 EventTime::from_millis(10),
7437 ));
7438 assert_eq!(
7439 tree.pressed_by(w),
7440 Some(first),
7441 "under `MultiContact::First` the second contact is terminated before \
7442 it reaches the arena at all",
7443 );
7444
7445 tree.dispatch_pointer(contact(
7446 second,
7447 PointerPhase::Up,
7448 second_at,
7449 EventTime::from_millis(20),
7450 ));
7451 assert!(
7452 pressed.get(),
7453 "so the second contact's release cannot clear a visual it never owned",
7454 );
7455 assert_eq!(tree.pressed_by(w), Some(first));
7456
7457 tree.dispatch_pointer(contact(
7458 first,
7459 PointerPhase::Up,
7460 first_at,
7461 EventTime::from_millis(30),
7462 ));
7463 assert!(!pressed.get(), "the owner's release does clear it");
7464 }
7465
7466 #[test]
7469 fn a_cancel_clears_the_visual() {
7470 let (mut tree, _clock) = tree_on_a_clock();
7471 let (w, pressed) = tappable(&mut tree);
7472 tree.layout(SizeProposal::exact(100.0, 50.0));
7473 let center = tree.bounds(w).center();
7474
7475 let id = contact_id();
7476 tree.dispatch_pointer(contact(id, PointerPhase::Down, center, EventTime::ZERO));
7477 assert!(pressed.get());
7478
7479 tree.dispatch_pointer(contact(
7480 id,
7481 PointerPhase::Cancel,
7482 center,
7483 EventTime::from_millis(20),
7484 ));
7485 assert!(!pressed.get(), "a press that was taken away is not painted");
7486 assert_eq!(tree.pressed_by(w), None);
7487 }
7488
7489 #[test]
7492 fn a_pan_claim_clears_the_visual() {
7493 let (mut tree, clock) = tree_on_a_clock();
7494 let (row, pressed) = tappable(&mut tree);
7495 let list = tree.add(
7496 StackWidget::new()
7497 .child(row)
7498 .pan_claim(PanClaim::vertical())
7499 .on_scroll(|_e, _c| EventResponse::Handled),
7500 );
7501 tree.layout(SizeProposal::exact(200.0, 400.0));
7502 let _ = list;
7503 let start = Point::new(100.0, 200.0);
7504
7505 let id = contact_id();
7506 tree.dispatch_pointer(contact(id, PointerPhase::Down, start, EventTime::ZERO));
7507 assert!(tree.press_pending(row));
7509 clock.set(EventTime::from_millis(100));
7510 tree.tick_gestures(std::time::Instant::now());
7511 assert!(pressed.get(), "…and appears once the delay has elapsed");
7512
7513 tree.dispatch_pointer(contact(
7515 id,
7516 PointerPhase::Move,
7517 Point::new(100.0, 260.0),
7518 EventTime::from_millis(120),
7519 ));
7520 assert!(
7521 !pressed.get(),
7522 "the row lost the press to the list and must stop advertising it",
7523 );
7524 assert_eq!(tree.pressed_by(row), None);
7525 }
7526
7527 #[test]
7536 fn a_button_the_control_cannot_act_on_raises_no_visual() {
7537 let taps = Rc::new(Cell::new(0u32));
7538 let count = taps.clone();
7539 let mut tree = WidgetTree::new();
7540 let w = tree.add(FillWidget::new().focusable().on_tap(move |_e, _c| {
7541 count.set(count.get() + 1);
7542 }));
7543 let pressed = tree.pressed_signal(w);
7544 tree.layout(SizeProposal::exact(100.0, 50.0));
7545 let center = tree.bounds(w).center();
7546
7547 for button in [
7548 PointerButton::Middle,
7549 PointerButton::Back,
7550 PointerButton::Forward,
7551 ] {
7552 tree.dispatch_event(WidgetEvent::pointer_down(center, button, Modifiers::NONE));
7553 assert!(
7554 !pressed.get(),
7555 "{button:?} cannot activate an `on_tap` node, so it must not light one up",
7556 );
7557 assert_eq!(tree.pressed_by(w), None, "and owns no visual to clear");
7558 tree.dispatch_event(WidgetEvent::pointer_up(center, button, Modifiers::NONE));
7559 assert_eq!(
7560 taps.get(),
7561 0,
7562 "the tap recognizer refuses {button:?} too — that is the point",
7563 );
7564 }
7565
7566 tree.dispatch_event(WidgetEvent::pointer_down(
7568 center,
7569 PointerButton::Primary,
7570 Modifiers::NONE,
7571 ));
7572 assert!(pressed.get(), "a primary press lights up as it always has");
7573 assert_eq!(tree.pressed_by(w), Some(PointerId::MOUSE));
7574 tree.dispatch_event(WidgetEvent::pointer_up(
7575 center,
7576 PointerButton::Primary,
7577 Modifiers::NONE,
7578 ));
7579 assert!(!pressed.get());
7580 assert_eq!(taps.get(), 1);
7581 }
7582
7583 #[test]
7587 fn a_secondary_press_with_no_context_menu_raises_no_visual() {
7588 let mut tree = WidgetTree::new();
7589 let (w, pressed) = tappable(&mut tree);
7590 tree.layout(SizeProposal::exact(100.0, 50.0));
7591 let center = tree.bounds(w).center();
7592
7593 tree.dispatch_event(WidgetEvent::pointer_down(
7594 center,
7595 PointerButton::Secondary,
7596 Modifiers::NONE,
7597 ));
7598 assert!(
7599 !pressed.get(),
7600 "nothing opened, and nothing may look pressed either",
7601 );
7602 assert_eq!(tree.pressed_by(w), None);
7603 assert!(!tree.press_is_inside(w));
7604 }
7605
7606 #[test]
7610 fn a_widened_mask_widens_the_visual_with_it() {
7611 use crate::event::ButtonMask;
7612
7613 let mut tree = WidgetTree::new();
7614 let w = tree.add(
7615 FillWidget::new()
7616 .on_tap(|_e, _c| {})
7617 .accept_tap_buttons(ButtonMask::PRIMARY | ButtonMask::MIDDLE),
7618 );
7619 let pressed = tree.pressed_signal(w);
7620 tree.layout(SizeProposal::exact(100.0, 50.0));
7621 let center = tree.bounds(w).center();
7622
7623 tree.dispatch_event(WidgetEvent::pointer_down(
7624 center,
7625 PointerButton::Middle,
7626 Modifiers::NONE,
7627 ));
7628 assert!(pressed.get(), "this node really does act on a middle-click");
7629
7630 tree.dispatch_event(WidgetEvent::pointer_up(
7631 center,
7632 PointerButton::Middle,
7633 Modifiers::NONE,
7634 ));
7635 assert!(!pressed.get());
7636 }
7637
7638 #[test]
7644 fn a_press_that_raises_no_visual_still_defers_its_focus() {
7645 use teksilo_tokens::{PenKind, PointerKind};
7646
7647 let (mut tree, _clock) = tree_on_a_clock();
7648 let (w, pressed) = tappable(&mut tree);
7649 tree.layout(SizeProposal::exact(100.0, 50.0));
7650 let center = tree.bounds(w).center();
7651
7652 let id = contact_id();
7653 let barrel = |phase, t| PointerSample {
7654 pointer: PointerInfo {
7655 kind: PointerKind::Pen(PenKind::Pen),
7656 ..PointerInfo::touch(id, t)
7657 },
7658 phase,
7659 position: center,
7660 button: Some(PointerButton::Secondary),
7661 modifiers: Modifiers::NONE,
7662 coalesced: Vec::new(),
7663 };
7664
7665 tree.dispatch_pointer(barrel(PointerPhase::Down, EventTime::ZERO));
7666 assert!(!pressed.get(), "the barrel button activates nothing here");
7667 assert_eq!(tree.pressed_by(w), None);
7668 assert_eq!(
7669 tree.focused(),
7670 None,
7671 "a direct pointer has chosen nothing until it lifts",
7672 );
7673
7674 tree.dispatch_pointer(barrel(PointerPhase::Up, EventTime::from_millis(40)));
7675 assert_eq!(
7676 tree.focused(),
7677 Some(w),
7678 "the deferral is not button-gated: the release landed where the press did",
7679 );
7680 assert_eq!(
7681 tree.focus_origin().and_then(FocusOrigin::pointer_kind),
7682 Some(PointerKind::Pen(PenKind::Pen)),
7683 );
7684 }
7685
7686 #[test]
7693 fn the_feedback_delay_applies_only_inside_a_claimant() {
7694 let (mut tree, _clock) = tree_on_a_clock();
7696 let (w, pressed) = tappable(&mut tree);
7697 tree.layout(SizeProposal::exact(200.0, 400.0));
7698 let id = contact_id();
7699 let at = tree.bounds(w).center();
7700 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
7701 assert!(
7702 !tree.press_pending(w),
7703 "no claimant above it, so nothing to rule out",
7704 );
7705 assert!(pressed.get());
7706
7707 let (mut tree, clock) = tree_on_a_clock();
7709 let (row, pressed) = tappable(&mut tree);
7710 let list = tree.add(
7711 StackWidget::new()
7712 .child(row)
7713 .pan_claim(PanClaim::vertical())
7714 .on_scroll(|_e, _c| EventResponse::Handled),
7715 );
7716 tree.layout(SizeProposal::exact(200.0, 400.0));
7717 let _ = list;
7718 let id = contact_id();
7719 let at = Point::new(100.0, 200.0);
7720 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
7721 assert!(tree.press_pending(row), "a finger might be about to scroll");
7722 assert!(!pressed.get(), "so the row does not flash");
7723 assert!(
7724 tree.press_is_inside(row),
7725 "the press is real; only its visual is being withheld",
7726 );
7727
7728 clock.set(EventTime::from_millis(99));
7729 tree.tick_gestures(std::time::Instant::now());
7730 assert!(!pressed.get(), "99 ms is under the 100 ms delay");
7731
7732 clock.set(EventTime::from_millis(100));
7733 tree.tick_gestures(std::time::Instant::now());
7734 assert!(pressed.get(), "and 100 ms is it");
7735 assert!(!tree.press_pending(row));
7736 }
7737
7738 #[test]
7741 fn a_mouse_inside_a_claimant_never_waits() {
7742 let mut tree = WidgetTree::new();
7743 let (row, pressed) = tappable(&mut tree);
7744 let list = tree.add(
7745 StackWidget::new()
7746 .child(row)
7747 .pan_claim(PanClaim::vertical())
7748 .on_scroll(|_e, _c| EventResponse::Handled),
7749 );
7750 tree.layout(SizeProposal::exact(200.0, 400.0));
7751 let _ = list;
7752
7753 tree.dispatch_event(WidgetEvent::pointer_down(
7754 Point::new(100.0, 200.0),
7755 PointerButton::Primary,
7756 Modifiers::NONE,
7757 ));
7758 assert!(!tree.press_pending(row));
7759 assert!(pressed.get(), "Compact with a mouse is exactly as it was");
7760 }
7761
7762 #[test]
7769 fn a_handler_can_read_the_press_it_is_inside() {
7770 let (mut tree, _clock) = tree_on_a_clock();
7771 let seen: Rc<RefCell<Vec<(bool, bool, bool)>>> = Rc::new(RefCell::new(Vec::new()));
7772 let log = seen.clone();
7773 let row = tree.add(FillWidget::new().on_tap(|_e, _c| {}).on_pointer_event(
7774 move |event, ctx| {
7775 if matches!(event, WidgetEvent::PointerUp { .. }) {
7776 log.borrow_mut().push((
7777 ctx.is_pressed(),
7778 ctx.press_is_inside(),
7779 ctx.press_pending(),
7780 ));
7781 }
7782 EventResponse::Ignored
7783 },
7784 ));
7785 let list = tree.add(
7786 StackWidget::new()
7787 .child(row)
7788 .pan_claim(PanClaim::vertical())
7789 .on_scroll(|_e, _c| EventResponse::Handled),
7790 );
7791 tree.layout(SizeProposal::exact(200.0, 400.0));
7792 let _ = list;
7793
7794 let id = contact_id();
7795 let at = Point::new(100.0, 200.0);
7796 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
7797 tree.dispatch_pointer(contact(
7798 id,
7799 PointerPhase::Up,
7800 at,
7801 EventTime::from_millis(30),
7802 ));
7803
7804 assert_eq!(
7805 seen.borrow().as_slice(),
7806 &[(false, true, true)],
7807 "released inside the claimant before the delay elapsed: inside, \
7808 pending, and therefore not yet showing",
7809 );
7810 }
7811
7812 #[test]
7815 fn a_handler_outside_a_press_reads_nothing() {
7816 let mut tree = WidgetTree::new();
7817 let asked = Rc::new(Cell::new(false));
7818 let flag = asked.clone();
7819 let w = tree.add(FillWidget::new().focusable().on_key(move |_e, ctx| {
7820 flag.set(ctx.is_pressed() || ctx.press_is_inside() || ctx.press_pending());
7821 EventResponse::Ignored
7822 }));
7823 tree.layout(SizeProposal::exact(100.0, 50.0));
7824 tree.focus(w);
7825 tree.press_key(Key::ArrowDown, Modifiers::NONE);
7826 assert!(!asked.get());
7827 }
7828
7829 #[test]
7833 fn a_completed_press_leaves_the_detector_clean() {
7834 let (mut tree, _clock) = tree_on_a_clock();
7835 let (w, pressed) = tappable(&mut tree);
7836 tree.layout(SizeProposal::exact(100.0, 50.0));
7837 let at = tree.bounds(w).center();
7838
7839 let id = contact_id();
7840 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
7841 assert!(pressed.get());
7842 tree.dispatch_pointer(contact(
7843 id,
7844 PointerPhase::Up,
7845 at,
7846 EventTime::from_millis(30),
7847 ));
7848 tree.assert_no_leaked_pointer_state();
7849 }
7850
7851 #[test]
7855 fn the_detector_reports_a_press_still_held() {
7856 let (mut tree, _clock) = tree_on_a_clock();
7857 let (w, _pressed) = tappable(&mut tree);
7858 tree.layout(SizeProposal::exact(100.0, 50.0));
7859 let at = tree.bounds(w).center();
7860
7861 let id = contact_id();
7862 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
7863 let held = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
7864 tree.assert_no_leaked_pointer_state()
7865 }));
7866 let message = *held
7867 .expect_err("a live press is leaked state")
7868 .downcast::<String>()
7869 .expect("the detector panics with a message");
7870 assert!(
7871 message.contains("is still pressed by"),
7872 "the press is named in the report, got: {message}",
7873 );
7874 }
7875
7876 #[derive(Debug)]
7880 struct SideBySide {
7881 children: Vec<WidgetId>,
7882 }
7883
7884 impl crate::widget::Widget for SideBySide {
7885 fn layout_response(
7886 &self,
7887 proposal: SizeProposal,
7888 _ctx: &crate::widget::LayoutContext,
7889 ) -> crate::widget::LayoutResponse {
7890 proposal.resolve(0.0, 0.0).into()
7891 }
7892
7893 fn place_children(
7894 &self,
7895 bounds: teksilo_canvas::Rect,
7896 _proposal: SizeProposal,
7897 children: &mut [crate::widget::WidgetPlacement],
7898 _ctx: &crate::widget::LayoutContext,
7899 ) {
7900 let n = children.len().max(1) as f32;
7901 let w = bounds.width / n;
7902 for (i, child) in children.iter_mut().enumerate() {
7903 child.origin = Point::new(bounds.x + w * i as f32, bounds.y);
7904 child.size = teksilo_canvas::Size::new(w, bounds.height);
7905 }
7906 }
7907
7908 fn children(&self) -> Vec<WidgetId> {
7909 self.children.clone()
7910 }
7911 }
7912}
7913
7914#[cfg(test)]
7915mod overlay_release_dismissal_tests {
7916 use std::cell::Cell;
7926 use std::rc::Rc;
7927
7928 use teksilo_canvas::{Point, Rect, Size, SizeProposal};
7929
7930 use crate::WidgetId;
7931 use crate::event::{Modifiers, PointerButton, WidgetEvent};
7932 use crate::overlay::{DismissBehavior, OverlayLayer, OverlayPlacement, OverlayRequest};
7933 use crate::pointer::{
7934 BackendDeviceKey, EventTime, PointerId, PointerIdAllocator, PointerInfo, PointerPhase,
7935 PointerSample,
7936 };
7937 use crate::test_widgets::FillWidget;
7938 use crate::widget::{LayoutContext, LayoutResponse, Widget};
7939 use crate::widget_builder::WidgetBuilder;
7940 use crate::widget_tree::WidgetTree;
7941
7942 #[derive(Debug)]
7949 struct Panel(Size);
7950
7951 impl Widget for Panel {
7952 fn layout_response(&self, _proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
7953 self.0.into()
7954 }
7955 }
7956
7957 #[derive(Debug)]
7965 struct PageAndTrigger {
7966 children: Vec<WidgetId>,
7967 }
7968
7969 impl Widget for PageAndTrigger {
7970 fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
7971 proposal.resolve(0.0, 0.0).into()
7972 }
7973
7974 fn place_children(
7975 &self,
7976 bounds: Rect,
7977 _proposal: SizeProposal,
7978 children: &mut [crate::widget::WidgetPlacement],
7979 _ctx: &LayoutContext,
7980 ) {
7981 if let Some(page) = children.get_mut(0) {
7982 page.origin = bounds.origin();
7983 page.size = bounds.size();
7984 }
7985 if let Some(trigger) = children.get_mut(1) {
7986 trigger.origin = bounds.origin();
7987 trigger.size = Size::new(10.0, 10.0);
7988 }
7989 }
7990
7991 fn children(&self) -> Vec<WidgetId> {
7992 self.children.clone()
7993 }
7994 }
7995
7996 fn contact_id(n: u64) -> PointerId {
7997 PointerIdAllocator::global().begin(BackendDeviceKey::new(0x0FA2), n)
7998 }
7999
8000 fn touch(id: PointerId, phase: PointerPhase, at: Point) -> PointerSample {
8001 PointerSample {
8002 pointer: PointerInfo::touch(id, EventTime::ZERO),
8003 phase,
8004 position: at,
8005 button: None,
8006 modifiers: Modifiers::NONE,
8007 coalesced: Vec::new(),
8008 }
8009 }
8010
8011 fn page_with_a_menu() -> (WidgetTree, WidgetId, Rc<Cell<u32>>) {
8018 let mut tree = WidgetTree::new();
8019 let taps = Rc::new(Cell::new(0u32));
8020 let counter = taps.clone();
8021 let page = tree.add(
8022 FillWidget::new()
8023 .focusable()
8024 .on_tap(move |_e, _c| counter.set(counter.get() + 1)),
8025 );
8026 let trigger = tree.add(Panel(Size::new(10.0, 10.0)));
8029 let _root = tree.add(PageAndTrigger {
8030 children: vec![page, trigger],
8031 });
8032 let menu = tree.add(Panel(Size::new(200.0, 200.0)));
8033 tree.show_overlay(OverlayRequest {
8034 content_id: menu,
8035 anchor: trigger,
8036 placement: OverlayPlacement::AtPointer(Point::new(100.0, 100.0)),
8037 dismiss: DismissBehavior::ClickOutside,
8038 layer: OverlayLayer::InTree,
8039 parent_overlay: None,
8040 on_dismiss: None,
8041 fade_duration: None,
8042 });
8043 tree.layout(SizeProposal::exact(800.0, 600.0));
8044 assert_eq!(tree.active_overlays().len(), 1, "the menu is open");
8045 (tree, page, taps)
8046 }
8047
8048 fn outside() -> Point {
8049 Point::new(600.0, 500.0)
8050 }
8051
8052 fn inside_menu() -> Point {
8053 Point::new(150.0, 150.0)
8054 }
8055
8056 #[test]
8063 fn the_suppressed_down_leaves_nothing_pressed_or_captured_beneath() {
8064 let (mut tree, page, taps) = page_with_a_menu();
8065 let finger = contact_id(1);
8066 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8067
8068 assert_eq!(
8069 tree.active_overlays().len(),
8070 1,
8071 "the press does not close the menu; the release does"
8072 );
8073 assert_eq!(tree.pressed_by(page), None, "nothing beneath is pressed");
8074 let entry = tree.pointers.get(finger).expect("the contact is live");
8075 assert_eq!(entry.captured_by, None, "nothing beneath captured it");
8076 assert!(entry.sequence.is_none(), "no arbitration was opened");
8077 assert_eq!(taps.get(), 0);
8078 }
8079
8080 #[test]
8082 fn a_touch_tap_outside_a_menu_closes_it_and_actuates_nothing() {
8083 let (mut tree, page, taps) = page_with_a_menu();
8084 let finger = contact_id(2);
8085 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8086 tree.dispatch_pointer(touch(finger, PointerPhase::Up, outside()));
8087
8088 assert!(tree.active_overlays().is_empty(), "the menu closed");
8089 assert_eq!(taps.get(), 0, "the page beneath was never tapped");
8090 assert_eq!(tree.pressed_by(page), None);
8091 tree.assert_no_leaked_pointer_state();
8092 }
8093
8094 #[test]
8097 fn a_cancelled_press_aborts_the_arm() {
8098 let (mut tree, page, taps) = page_with_a_menu();
8099 let finger = contact_id(3);
8100 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8101 tree.dispatch_pointer(touch(finger, PointerPhase::Cancel, outside()));
8102
8103 assert_eq!(tree.active_overlays().len(), 1, "the menu survives");
8104 assert_eq!(taps.get(), 0);
8105 assert_eq!(tree.pressed_by(page), None);
8106 tree.assert_no_leaked_pointer_state();
8107 }
8108
8109 #[test]
8113 fn a_press_slid_onto_the_menu_dismisses_nothing() {
8114 let (mut tree, _page, taps) = page_with_a_menu();
8115 let finger = contact_id(4);
8116 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8117 tree.dispatch_pointer(touch(finger, PointerPhase::Move, inside_menu()));
8118 tree.dispatch_pointer(touch(finger, PointerPhase::Up, inside_menu()));
8119
8120 assert_eq!(tree.active_overlays().len(), 1, "the menu survives");
8121 assert_eq!(taps.get(), 0);
8122 tree.assert_no_leaked_pointer_state();
8123 }
8124
8125 #[test]
8128 fn a_second_contact_cannot_dismiss_what_the_first_is_manipulating() {
8129 let (mut tree, _page, _taps) = page_with_a_menu();
8130 let first = contact_id(5);
8131 let second = contact_id(6);
8132
8133 tree.dispatch_pointer(touch(first, PointerPhase::Down, inside_menu()));
8134 assert!(
8135 tree.pointers
8136 .get(first)
8137 .is_some_and(|e| e.sequence.is_some()),
8138 "the first contact holds a live press inside the menu"
8139 );
8140
8141 tree.dispatch_pointer(touch(second, PointerPhase::Down, outside()));
8142 tree.dispatch_pointer(touch(second, PointerPhase::Up, outside()));
8143 assert_eq!(
8144 tree.active_overlays().len(),
8145 1,
8146 "the menu the first finger is holding must not close under it"
8147 );
8148
8149 tree.dispatch_pointer(touch(first, PointerPhase::Up, inside_menu()));
8152 let third = contact_id(7);
8153 tree.dispatch_pointer(touch(third, PointerPhase::Down, outside()));
8154 tree.dispatch_pointer(touch(third, PointerPhase::Up, outside()));
8155 assert!(tree.active_overlays().is_empty());
8156 tree.assert_no_leaked_pointer_state();
8157 }
8158
8159 #[test]
8163 fn an_arm_is_not_itself_a_press_that_blocks_another_contact() {
8164 let (mut tree, _page, _taps) = page_with_a_menu();
8165 let first = contact_id(8);
8166 let second = contact_id(9);
8167
8168 tree.dispatch_pointer(touch(first, PointerPhase::Down, outside()));
8169 assert!(
8170 tree.overlay_manager().has_armed_dismiss(first),
8171 "the first contact armed"
8172 );
8173 assert!(tree.busy_press_points(second).is_empty());
8175
8176 tree.dispatch_pointer(touch(second, PointerPhase::Down, outside()));
8177 assert!(tree.overlay_manager().has_armed_dismiss(second));
8178 tree.dispatch_pointer(touch(second, PointerPhase::Up, outside()));
8179 assert!(tree.active_overlays().is_empty());
8180
8181 tree.dispatch_pointer(touch(first, PointerPhase::Up, outside()));
8184 tree.assert_no_leaked_pointer_state();
8185 }
8186
8187 #[test]
8190 fn a_touch_inside_the_menu_arms_nothing() {
8191 let (mut tree, _page, _taps) = page_with_a_menu();
8192 let finger = contact_id(10);
8193 tree.dispatch_pointer(touch(finger, PointerPhase::Down, inside_menu()));
8194 assert!(!tree.overlay_manager().has_armed_dismiss(finger));
8195 tree.dispatch_pointer(touch(finger, PointerPhase::Up, inside_menu()));
8196 assert_eq!(tree.active_overlays().len(), 1);
8197 tree.assert_no_leaked_pointer_state();
8198 }
8199
8200 #[test]
8210 fn a_contact_with_no_press_still_ends_when_the_platform_revokes_it() {
8211 let mut tree = WidgetTree::new();
8212 tree.layout(SizeProposal::exact(800.0, 600.0));
8213 let finger = contact_id(99);
8214 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8215 tree.dispatch_pointer(touch(finger, PointerPhase::Cancel, outside()));
8216 tree.assert_no_leaked_pointer_state();
8217 }
8218
8219 #[test]
8226 fn a_mouse_click_outside_a_menu_dismisses_on_the_press_and_falls_through() {
8227 let (mut tree, _page, taps) = page_with_a_menu();
8228 tree.dispatch_event(WidgetEvent::pointer_down(
8229 outside(),
8230 PointerButton::Primary,
8231 Modifiers::NONE,
8232 ));
8233 assert!(
8234 tree.active_overlays().is_empty(),
8235 "the mouse still dismisses on the press"
8236 );
8237 assert!(
8238 tree.pointers
8239 .get(PointerId::MOUSE)
8240 .is_some_and(|e| e.sequence.is_some()),
8241 "and the press still reaches the page beneath"
8242 );
8243 tree.dispatch_event(WidgetEvent::pointer_up(
8244 outside(),
8245 PointerButton::Primary,
8246 Modifiers::NONE,
8247 ));
8248 assert_eq!(taps.get(), 1, "the control beneath activated");
8249 assert!(
8250 !tree.overlay_manager().has_armed_dismiss(PointerId::MOUSE),
8251 "a mouse never arms"
8252 );
8253 tree.assert_no_leaked_pointer_state();
8254 }
8255
8256 #[test]
8263 fn a_context_menu_avoids_the_contact_that_opened_it() {
8264 fn menu_bounds(pointer: PointerInfo, at: Point) -> Rect {
8265 let mut tree = WidgetTree::new();
8266 let page = tree.add(
8267 FillWidget::new()
8268 .focusable()
8269 .context_menu(|_p, _ctx| Some(Box::new(Panel(Size::new(200.0, 160.0))))),
8270 );
8271 let _ = page;
8272 tree.layout(SizeProposal::exact(800.0, 600.0));
8273 tree.dispatch_pointer(PointerSample {
8274 pointer,
8275 phase: PointerPhase::Down,
8276 position: at,
8277 button: Some(PointerButton::Secondary),
8278 modifiers: Modifiers::NONE,
8279 coalesced: Vec::new(),
8280 });
8281 tree.layout(SizeProposal::exact(800.0, 600.0));
8282 let id = *tree
8283 .active_overlays()
8284 .first()
8285 .expect("the context menu opened");
8286 tree.overlay_manager().bounds_for(id).expect("bounds")
8287 }
8288
8289 let at = Point::new(400.0, 300.0);
8290 let finger = menu_bounds(PointerInfo::touch(contact_id(11), EventTime::ZERO), at);
8291 let contact = crate::overlay::rect_centred_on(at, crate::overlay::ASSUMED_CONTACT_PATCH);
8292 assert!(
8293 finger.x < contact.x && finger.right() <= contact.x,
8294 "a touch menu clears the contact patch: {finger:?} vs {contact:?}"
8295 );
8296
8297 let mouse = menu_bounds(PointerInfo::mouse(EventTime::ZERO), at);
8298 assert_eq!(
8299 (mouse.x, mouse.y),
8300 (at.x, at.y),
8301 "a mouse menu still opens with its corner on the pointer"
8302 );
8303 }
8304}
8305
8306#[cfg(test)]
8322mod pass_through_overlay_tests {
8323 use teksilo_canvas::{Point, Rect, SizeProposal};
8324
8325 use crate::WidgetId;
8326 use crate::overlay::{DismissBehavior, OverlayLayer, OverlayPlacement, OverlayRequest};
8327 use crate::test_widgets::FillWidget;
8328 use crate::widget::{LayoutContext, LayoutResponse, Widget};
8329 use crate::widget_builder::WidgetBuilder;
8330 use crate::widget_tree::WidgetTree;
8331
8332 #[derive(Debug)]
8335 struct HandleLayer {
8336 handle: WidgetId,
8337 at: Rect,
8338 }
8339
8340 impl Widget for HandleLayer {
8341 fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
8342 proposal.resolve(0.0, 0.0).into()
8343 }
8344
8345 fn place_children(
8346 &self,
8347 _bounds: Rect,
8348 _proposal: SizeProposal,
8349 children: &mut [crate::widget::WidgetPlacement],
8350 _ctx: &LayoutContext,
8351 ) {
8352 if let Some(child) = children.get_mut(0) {
8353 child.origin = self.at.origin();
8354 child.size = self.at.size();
8355 }
8356 }
8357
8358 fn children(&self) -> Vec<WidgetId> {
8359 vec![self.handle]
8360 }
8361 }
8362
8363 const HANDLE: Rect = Rect {
8364 x: 10.0,
8365 y: 10.0,
8366 width: 20.0,
8367 height: 20.0,
8368 };
8369
8370 #[derive(Clone, Copy, PartialEq)]
8372 enum RootFlag {
8373 PassThrough,
8375 HitTransparent,
8378 }
8379
8380 fn field_under_a_layer(flag: RootFlag) -> (WidgetTree, WidgetId, WidgetId) {
8386 let mut tree = WidgetTree::new();
8387 let field = tree.add(FillWidget::new());
8388 let handle = tree.add(FillWidget::new());
8389 let layer = HandleLayer { handle, at: HANDLE };
8390 let layer = match flag {
8391 RootFlag::PassThrough => tree.add(layer.event_pass_through(true)),
8392 RootFlag::HitTransparent => tree.add(layer.hit_transparent(true)),
8393 };
8394 tree.show_overlay(OverlayRequest {
8395 content_id: layer,
8396 anchor: field,
8397 placement: OverlayPlacement::FullViewport,
8398 dismiss: DismissBehavior::Manual,
8399 layer: OverlayLayer::InTree,
8400 parent_overlay: None,
8401 on_dismiss: None,
8402 fade_duration: None,
8403 });
8404 tree.layout(SizeProposal::exact(400.0, 200.0));
8405 assert_eq!(tree.active_overlays().len(), 1, "the layer is up");
8406 (tree, field, handle)
8407 }
8408
8409 #[test]
8411 fn a_pass_through_layer_hands_back_what_it_did_not_claim() {
8412 let (tree, field, handle) = field_under_a_layer(RootFlag::PassThrough);
8413
8414 assert_eq!(
8415 tree.hit_test(Point::new(15.0, 15.0)),
8416 Some(handle),
8417 "the layer must still win the point its own child covers"
8418 );
8419 assert_eq!(
8420 tree.hit_test(Point::new(200.0, 100.0)),
8421 Some(field),
8422 "a press the layer did not claim must reach the surface beneath it"
8423 );
8424 }
8425
8426 #[test]
8437 fn a_pass_through_root_still_hears_a_press_that_landed_on_its_child() {
8438 use std::cell::Cell;
8439 use std::rc::Rc;
8440
8441 let seen = Rc::new(Cell::new(0u32));
8442 let counter = seen.clone();
8443 let mut tree = WidgetTree::new();
8444 let field = tree.add(FillWidget::new());
8445 let handle = tree.add(FillWidget::new().on_tap(|_e, _c| {}));
8446 let layer = tree.add(
8447 HandleLayer { handle, at: HANDLE }
8448 .event_pass_through(true)
8449 .on_pointer_event(move |_event, _ctx| {
8450 counter.set(counter.get() + 1);
8451 crate::event::EventResponse::Ignored
8452 }),
8453 );
8454 tree.show_overlay(OverlayRequest {
8455 content_id: layer,
8456 anchor: field,
8457 placement: OverlayPlacement::FullViewport,
8458 dismiss: DismissBehavior::Manual,
8459 layer: OverlayLayer::InTree,
8460 parent_overlay: None,
8461 on_dismiss: None,
8462 fade_duration: None,
8463 });
8464 tree.layout(SizeProposal::exact(400.0, 200.0));
8465
8466 tree.pointer_down_button(Point::new(15.0, 15.0), crate::event::PointerButton::Primary);
8467 assert!(
8468 seen.get() > 0,
8469 "a pass-through root heard nothing about a press on its own child"
8470 );
8471
8472 let after_child = seen.get();
8473 tree.pointer_up_button(Point::new(15.0, 15.0), crate::event::PointerButton::Primary);
8474 tree.pointer_down_button(
8477 Point::new(200.0, 100.0),
8478 crate::event::PointerButton::Primary,
8479 );
8480 assert_eq!(
8481 seen.get(),
8482 after_child + 1,
8483 "the press-and-lift on the child accounts for the count, and the \
8484 press on the surface beneath added nothing"
8485 );
8486 }
8487
8488 #[test]
8502 fn a_hit_transparent_root_does_not_get_the_fall_through() {
8503 let (tree, field, _handle) = field_under_a_layer(RootFlag::HitTransparent);
8504
8505 assert_eq!(
8507 tree.hit_test(Point::new(15.0, 15.0)),
8508 None,
8509 "hit_transparent must exclude the subtree, or this fixture is not \
8510 testing the gate"
8511 );
8512 assert_eq!(
8513 tree.hit_test(Point::new(200.0, 100.0)),
8514 None,
8515 "the fall-through reached past an overlay that did not ask for it"
8516 );
8517 let _ = field;
8518 }
8519}