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) {
227 let mut noop = crate::window::NoopWindowOps;
228 self.dispatch_pointer_with_ops(sample, &mut noop);
229 }
230
231 pub fn dispatch_pointer_with_ops(
235 &mut self,
236 sample: crate::pointer::PointerSample,
237 ops: &mut dyn crate::window::WindowOps,
238 ) {
239 use crate::pointer::PointerPhase;
240
241 crate::trace_input!(
242 Samples,
243 "{:?} {:?} at {:?} buttons={:?} t={:?}",
244 sample.phase,
245 sample.pointer.id,
246 sample.position,
247 sample.pointer.buttons,
248 sample.pointer.time
249 );
250
251 let button = sample
255 .button
256 .unwrap_or(crate::event::PointerButton::Primary);
257 let event = match sample.phase {
258 PointerPhase::Down => WidgetEvent::PointerDown {
259 position: sample.position,
260 button,
261 modifiers: sample.modifiers,
262 pointer: sample.pointer,
263 },
264 PointerPhase::Move => WidgetEvent::PointerMove {
265 position: sample.position,
266 modifiers: sample.modifiers,
267 pointer: sample.pointer,
268 },
269 PointerPhase::Up => WidgetEvent::PointerUp {
270 position: sample.position,
271 button,
272 modifiers: sample.modifiers,
273 pointer: sample.pointer,
274 },
275 PointerPhase::Cancel => {
281 self.overlay_manager.abort_dismiss(sample.pointer.id);
286 self.cancel_pointer(
287 sample.pointer.id,
288 crate::pointer::CancelReason::Platform,
289 ops,
290 );
291 if !sample.pointer.kind.hovers() {
298 self.pointers.end(sample.pointer.id);
299 }
300 return;
301 }
302 };
303
304 if !self.pointers.would_admit(&sample.pointer) {
310 return;
311 }
312 let ends_pointer = matches!(sample.phase, PointerPhase::Up | PointerPhase::Cancel)
316 && !sample.pointer.kind.hovers();
317 let pointer_id = sample.pointer.id;
318
319 let snapshot = crate::pointer::InputSnapshot::from_pointer_sample(&sample);
320 self.dispatch_with_input_snapshot(event, snapshot, ops);
321
322 if ends_pointer {
323 self.pointers.end(pointer_id);
324 }
325 }
326
327 pub fn dispatch_scroll(&mut self, sample: crate::pointer::ScrollSample) {
335 let mut noop = crate::window::NoopWindowOps;
336 self.dispatch_scroll_with_ops(sample, &mut noop);
337 }
338
339 pub fn dispatch_scroll_with_ops(
342 &mut self,
343 sample: crate::pointer::ScrollSample,
344 ops: &mut dyn crate::window::WindowOps,
345 ) {
346 crate::trace_input!(
347 Samples,
348 "scroll {:?} {:?}/{:?} at {:?}",
349 sample.delta,
350 sample.phase,
351 sample.source,
352 sample.position
353 );
354
355 let event = WidgetEvent::Scroll {
356 delta: sample.delta,
357 modifiers: sample.modifiers,
358 window_position: sample.position,
359 phase: sample.phase,
360 pointer: sample.pointer,
361 };
362 let snapshot = crate::pointer::InputSnapshot::from_scroll_sample(&sample);
363 self.dispatch_with_input_snapshot(event, snapshot, ops);
364 }
365
366 pub fn pointer_left_window(&mut self, ops: &mut dyn crate::window::WindowOps) {
391 if self
395 .pointers
396 .hover_owner_id()
397 .and_then(|id| self.captured_by(id))
398 .is_some()
399 {
400 return;
401 }
402 let Some(hovered) = self.hovered_id() else {
403 return;
404 };
405 let leave = WidgetEvent::PointerLeave {
408 pointer: self.hover_transition_pointer(),
409 };
410 self.dispatch_to_widget(hovered, &leave, &mut *ops);
411 self.tooltip_pointer_leave(hovered, &mut *ops);
412 self.set_hovered(None);
413 self.update_hover_within_signals(Some(hovered), None);
414 }
415
416 pub(super) fn hover_transition_pointer(&self) -> crate::pointer::PointerInfo {
433 self.pointers
434 .hover_owner()
435 .map(|entry| entry.info)
436 .unwrap_or_else(|| crate::pointer::PointerInfo::mouse(self.input_now()))
437 }
438
439 fn dispatch_with_input_snapshot(
446 &mut self,
447 event: WidgetEvent,
448 snapshot: crate::pointer::InputSnapshot,
449 ops: &mut dyn crate::window::WindowOps,
450 ) {
451 self.pending_dispatch.push_back(QueuedDispatch::Event {
461 event,
462 snapshot: Box::new(snapshot),
463 });
464 if self.dispatch_depth > 0 {
465 return;
466 }
467 self.drain_pending_dispatch(&mut *ops);
468 }
469
470 pub(super) fn drain_pending_dispatch(&mut self, ops: &mut dyn crate::window::WindowOps) {
476 while let Some(queued) = self.pending_dispatch.pop_front() {
477 match queued {
478 QueuedDispatch::Event { event, snapshot } => {
479 self.run_one_dispatch(event, *snapshot, &mut *ops);
480 }
481 QueuedDispatch::Cancel {
482 pointer,
483 reason,
484 recipient,
485 } => {
486 self.run_one_cancel(pointer, reason, recipient, &mut *ops);
487 }
488 }
489 }
490 }
491
492 fn run_one_dispatch(
499 &mut self,
500 event: WidgetEvent,
501 snapshot: crate::pointer::InputSnapshot,
502 ops: &mut dyn crate::window::WindowOps,
503 ) {
504 let previous = std::mem::replace(&mut self.current_input, snapshot);
505 self.dispatch_depth += 1;
506 self.dispatch_event_impl(event, ops);
507 self.dispatch_depth -= 1;
508 self.current_input = previous;
509 }
510
511 fn dispatch_event_impl(&mut self, event: WidgetEvent, ops: &mut dyn crate::window::WindowOps) {
512 if let Some(position) = pointer_event_position(&event) {
518 let is_down = matches!(event, WidgetEvent::PointerDown { .. });
519 let is_move = matches!(event, WidgetEvent::PointerMove { .. });
520 if !self.admit_current_pointer(position, is_down, is_move) {
521 return;
522 }
523 if self.current_input.pointer.kind.hovers() {
527 self.claim_hover_owner_for_current(&mut *ops);
528 }
529 }
530
531 match &event {
535 WidgetEvent::KeyDown { .. } if !self.focus_visible.get() => {
536 self.focus_visible.set(true);
537 }
538 WidgetEvent::PointerDown { .. } if self.focus_visible.get() => {
539 self.focus_visible.set(false);
540 }
541 _ => {}
542 }
543
544 let overlay_back_key = match self.layout_direction {
550 crate::environment::LayoutDirection::RightToLeft => Key::ArrowRight,
551 crate::environment::LayoutDirection::LeftToRight => Key::ArrowLeft,
552 };
553 if let WidgetEvent::KeyDown { key, .. } = &event
554 && *key == overlay_back_key
555 {
556 let nested_menu_overlays = {
586 let ids: Vec<_> = self
587 .overlay_manager
588 .stack
589 .iter()
590 .filter(|o| o.band == crate::overlay::OverlayBand::Standard && !o.inert)
591 .map(|o| o.id)
592 .collect();
593 ids.into_iter()
594 .filter(|&id| !self.overlay_is_host_surface(id))
595 .count()
596 };
597 let top_id = self
604 .overlay_manager
605 .stack
606 .iter()
607 .rev()
608 .find(|o| !o.inert)
609 .map(|o| o.id);
610 let top_is_back_navigable = top_id.is_some_and(|id| !self.overlay_is_host_surface(id));
611 if nested_menu_overlays > 1
612 && top_is_back_navigable
613 && let Some(top_id) = top_id
614 {
615 let (content_ids, focus_restore) =
616 self.overlay_manager.dismiss_with_focus_restore_because(
617 top_id,
618 crate::overlay::DismissReason::Escape,
619 );
620 self.dormant_dismissed_content(&content_ids, &mut *ops);
621 if let Some(restore_id) = focus_restore
622 && self.arena.is_active(restore_id)
623 {
624 self.focus_ops(restore_id, &mut *ops);
625 }
626 return;
627 }
628 }
629
630 if let WidgetEvent::KeyDown {
636 key: Key::Escape, ..
637 } = &event
638 {
639 self.tooltip_escape_pressed();
640 self.inert_overlays_escape_pressed(&mut *ops);
643 }
644
645 if let WidgetEvent::KeyDown {
646 key: Key::Escape, ..
647 } = &event
648 && !self.overlay_manager.is_empty()
649 && let Some((_id, content_ids, focus_restore)) =
650 self.overlay_manager.try_dismiss_top_on_escape()
651 {
652 self.dormant_dismissed_content(&content_ids, &mut *ops);
653 if let Some(restore_id) = focus_restore
654 && self.arena.is_active(restore_id)
655 {
656 self.focus_ops(restore_id, &mut *ops);
657 }
658 return;
659 }
660
661 match &event {
666 WidgetEvent::PointerDown {
667 position, button, ..
668 } => {
669 if self.arm_outside_press_dismissal(*position, *button, &mut *ops) {
670 return;
671 }
672 }
673 WidgetEvent::PointerUp { position, .. } => {
674 if self.commit_outside_press_dismissal(*position, &mut *ops) {
675 return;
676 }
677 }
678 WidgetEvent::PointerCancel { pointer, .. } => {
679 self.overlay_manager.abort_dismiss(pointer.id);
683 }
684 _ => {}
685 }
686
687 if let WidgetEvent::KeyDown { key, modifiers, .. } = &event
698 && let Some(callback) = self.take_key_capture()
699 {
700 let keystroke = crate::shortcut::KeyStroke::new(*key, *modifiers);
701 let mut cap_ctx = self.make_event_context(&mut *ops);
702 callback(keystroke, self.shortcut_registry_mut(), &mut cap_ctx);
703 let anchor = self.focused.or_else(|| self.arena.roots().first().copied());
706 if let Some(anchor_id) = anchor {
707 self.collect_from_ctx(cap_ctx, anchor_id);
708 self.drain_pending_intents(&mut *ops);
709 }
710 return;
711 }
712
713 let focus_captures_keys = matches!(&event, WidgetEvent::KeyDown { .. })
721 && self.focused.is_some_and(|f| self.is_keyboard_capture(f));
722
723 if !focus_captures_keys && let WidgetEvent::KeyDown { key, modifiers, .. } = &event {
738 let keystroke = crate::shortcut::KeyStroke::new(*key, *modifiers);
739 let candidates: Vec<(&'static str, crate::shortcut::ShortcutScope, bool)> = self
748 .shortcut_registry
749 .matches_by_keystroke(keystroke)
750 .map(|eff| {
751 (
752 eff.shortcut.id,
753 eff.shortcut.scope,
754 eff.shortcut.propagate_when_disabled,
755 )
756 })
757 .collect();
758 let lookup = self.select_shortcut_for_focus(&candidates);
759 if let Some((id, scope, propagate_when_disabled)) = lookup {
760 let anchor = match scope {
761 crate::shortcut::ShortcutScope::Global => {
766 self.focused.or_else(|| self.arena.roots().first().copied())
767 }
768 crate::shortcut::ShortcutScope::Scoped(scope_id) => {
769 self.focused.filter(|f| self.is_descendant_of(*f, scope_id))
770 }
771 };
772 if let Some(anchor_id) = anchor {
773 let mut act_ctx = self.make_event_context(&mut *ops);
774 if let Some(intent) =
775 self.shortcut_registry
776 .invoke_on_activate(id, keystroke, &mut act_ctx)
777 {
778 self.collect_from_ctx(act_ctx, anchor_id);
779 let intent = intent.with_source(crate::telemetry::IntentSource::Shortcut);
783 self.enqueue_intent(anchor_id, intent, propagate_when_disabled);
784 self.drain_pending_intents(&mut *ops);
785 return;
786 }
787 }
788 }
793 }
796
797 if self.outbound_drag_source.is_some()
803 && let WidgetEvent::KeyDown {
804 key: Key::Escape, ..
805 } = &event
806 {
807 ops.cancel_os_drag();
808 }
813
814 if self.active_drag.is_some() {
816 match &event {
817 WidgetEvent::PointerMove { position, .. } => {
818 self.handle_drag_move(*position, &mut *ops);
819 return;
820 }
821 WidgetEvent::PointerUp { position, .. } => {
822 self.handle_drag_drop(*position, &mut *ops);
823 return;
824 }
825 WidgetEvent::KeyDown {
826 key: Key::Escape, ..
827 } => {
828 self.cancel_active_drag(&mut *ops);
829 return;
830 }
831 WidgetEvent::Scroll { .. } => {
832 let target_and_pos = self
838 .active_drag
839 .as_ref()
840 .and_then(|d| d.current_target.map(|t| (t, d.current_position)));
841 if let Some((target, _pos)) = target_and_pos {
842 self.dispatch_to_widget(target, &event, &mut *ops);
843 }
844 if let Some((_, pos)) = target_and_pos
845 && self.active_drag.is_some()
846 {
847 self.handle_drag_move(pos, &mut *ops);
848 }
849 return;
850 }
851 _ => {}
852 }
853 }
854
855 if let WidgetEvent::KeyDown { key, modifiers, .. } = &event
864 && is_context_menu_chord(*key, *modifiers)
865 && self.open_context_menu_from_keyboard(&mut *ops)
866 {
867 return;
868 }
869
870 match &event {
871 WidgetEvent::PointerMove { position, .. } => {
872 self.note_sequence_position(*position);
876 self.revalidate_sequence(&mut *ops);
877 self.tick_sequence_timers();
880 self.note_palm_sample(*position);
885 self.feed_pinch(super::pan_arbiter::PinchFeed::Move, *position, &mut *ops);
886 if let Some(captured) = self.current_pointer_capture() {
887 self.dispatch_to_widget(captured, &event, &mut *ops);
888 if self.active_drag.is_none() {
893 self.advance_sequence(&event, &mut *ops);
894 }
895 } else {
896 self.handle_pointer_move(&event, *position, &mut *ops);
897 if self.active_drag.is_none() {
902 self.advance_sequence(&event, &mut *ops);
903 }
904 }
905 self.advance_pan(*position, &mut *ops);
908 self.update_press(*position);
911 self.touch_route_moved(self.current_pointer_id());
914 if self.pointers.hover_owner_id() == Some(self.current_pointer_id()) {
924 self.update_pointer_leave_overlays(*position, &mut *ops);
925 }
926 }
927 WidgetEvent::PointerDown {
928 position, button, ..
929 } => {
930 let pressed = self.current_pointer_id();
933 self.cancelled_pointers.retain(|p| *p != pressed);
934 self.tooltip_pointer_press(Some(*position));
941 let pressing = self.current_input.pointer;
945 if let Some(target) = self.hit_test_for(*position, &pressing) {
946 if *button == PointerButton::Secondary
947 && self.show_context_menu_for(
948 target,
949 *position,
950 crate::widget_builder::ContextMenuTrigger::Pointer,
951 &mut *ops,
952 )
953 {
954 return;
955 }
956 self.begin_sequence(target, *position);
962 let modifiers = match &event {
967 WidgetEvent::PointerDown { modifiers, .. } => *modifiers,
968 _ => crate::event::Modifiers::NONE,
969 };
970 self.begin_pan(target, *position, modifiers);
971 self.begin_palm_watch(*position);
972 self.feed_pinch(super::pan_arbiter::PinchFeed::Down, *position, &mut *ops);
973 let focusable = self.find_focusable_at_or_above(target);
977 self.begin_press(focusable);
978 if !pressing.kind.is_direct()
985 && let Some(focusable) = focusable
986 {
987 self.focus_with_origin_ops(
988 focusable,
989 crate::focus::FocusOrigin::Pointer(pressing.kind),
990 &mut *ops,
991 );
992 }
993 self.dispatch_to_widget(target, &event, &mut *ops);
994 if self.active_drag.is_none() {
999 self.enrol_sequence_members(&event, &mut *ops);
1000 }
1001 self.adopt_press_owner(*button);
1005 self.arm_touch_route(target, *position);
1010 }
1011 }
1012 WidgetEvent::PointerUp { position, .. } => {
1013 let released = self.current_pointer_id();
1019 self.cancel_touch_route(released);
1023 if let Some(index) = self.cancelled_pointers.iter().position(|p| *p == released) {
1024 self.cancelled_pointers.swap_remove(index);
1025 crate::trace_input!(
1026 Samples,
1027 "swallowing the Up for {released:?}: its press was cancelled"
1028 );
1029 return;
1030 }
1031 self.feed_pinch(super::pan_arbiter::PinchFeed::Up, *position, &mut *ops);
1038 if self.take_palm_verdict(released) {
1039 crate::trace_input!(
1040 Samples,
1041 "{released:?} released as a palm: large, and it never moved"
1042 );
1043 self.cancel_pointer(
1044 released,
1045 crate::pointer::CancelReason::PalmRejected,
1046 &mut *ops,
1047 );
1048 return;
1049 }
1050 self.end_pan(*position, &mut *ops);
1053 self.note_sequence_position(*position);
1060 self.end_sequence(&event, &mut *ops);
1061 self.focus_on_release(*position, &mut *ops);
1065 if let Some(captured) = self.current_pointer_capture() {
1066 self.dispatch_to_widget(captured, &event, &mut *ops);
1067 self.set_current_pointer_capture(None);
1070 } else {
1071 let releasing = self.current_input.pointer;
1072 if let Some(target) = self.hit_test_for(*position, &releasing) {
1073 self.dispatch_to_widget(target, &event, &mut *ops);
1074 }
1075 }
1076 let released = self.current_pointer_id();
1079 self.release_arenas_following(released);
1080 self.end_press(released);
1084 }
1085 WidgetEvent::PointerCancel {
1086 reason, pointer, ..
1087 } => {
1088 let (reason, pointer) = (*reason, pointer.id);
1093 self.cancel_pointer(pointer, reason, &mut *ops);
1094 }
1095 WidgetEvent::Scroll {
1096 window_position: position,
1097 ..
1098 } => {
1099 use super::pan_arbiter::ScrollDelivery;
1100 match ScrollDelivery::for_source(self.current_input.scroll_source) {
1101 ScrollDelivery::ClaimantChain => {
1106 let position = *position;
1107 self.route_scroll_along_chain(&event, position, &mut *ops);
1108 }
1109 ScrollDelivery::Bubble => {
1115 let scrolling = self.current_input.pointer;
1116 let target = match position {
1117 Some(p) => self.hit_test_for(*p, &scrolling),
1118 None => self.hovered_id().or(self.focused),
1119 };
1120 if let Some(target) = target {
1121 self.dispatch_to_widget(target, &event, &mut *ops);
1122 }
1123 }
1124 }
1125 }
1126 WidgetEvent::KeyDown { key, modifiers, .. } => {
1127 if *key == Key::Tab {
1128 let captured_focus = self
1143 .focused
1144 .is_some_and(|focused| self.is_keyboard_capture(focused));
1145 if captured_focus && modifiers.ctrl() {
1146 self.cycle_focus(modifiers.shift(), &mut *ops);
1147 return;
1148 }
1149 let handled = self
1156 .focused
1157 .map(|focused| {
1158 self.dispatch_to_widget_returning_handled(focused, &event, &mut *ops)
1159 })
1160 .unwrap_or(false);
1161 if !handled {
1162 self.cycle_focus(modifiers.shift(), &mut *ops);
1163 }
1164 } else if let Some(focused) = self.focused {
1165 self.dispatch_to_widget(focused, &event, &mut *ops);
1166 }
1167 }
1168 WidgetEvent::KeyUp { .. }
1169 | WidgetEvent::ImeComposition { .. }
1170 | WidgetEvent::ImeCommit { .. } => {
1171 if let Some(focused) = self.focused {
1172 self.dispatch_to_widget(focused, &event, &mut *ops);
1173 }
1174 }
1175 WidgetEvent::AccessAction { target, action, .. } => {
1176 if let Some(id) =
1194 target.filter(|id| self.arena.is_active(*id) && !self.is_behind_modal(*id))
1195 {
1196 if *action == accesskit::Action::Focus {
1197 if self.advertises_focus_action(id) {
1226 let target = self
1227 .focusable_composite_behind(id)
1228 .or_else(|| self.first_focusable_descendant(id))
1229 .unwrap_or(id);
1230 self.focus_with_origin_ops(
1236 target,
1237 crate::focus::FocusOrigin::Accessibility,
1238 &mut *ops,
1239 );
1240 self.access_action_handled = self.focused == Some(target);
1243 } else {
1244 self.access_action_handled = false;
1245 }
1246 } else if *action == accesskit::Action::ShowContextMenu {
1247 self.access_action_handled =
1263 if self.dispatch_to_widget_returning_handled(id, &event, &mut *ops) {
1264 true
1265 } else {
1266 let position = self.arena.bounds(id).center();
1267 self.show_context_menu_for(
1268 id,
1269 position,
1270 crate::widget_builder::ContextMenuTrigger::Accessibility,
1271 &mut *ops,
1272 )
1273 };
1274 } else {
1275 self.access_action_handled =
1276 self.dispatch_to_widget_returning_handled(id, &event, &mut *ops);
1277 }
1278 }
1279 }
1280 WidgetEvent::Gesture { .. } => {
1281 if let Some(target) = self.hovered_id().or(self.focused) {
1282 self.dispatch_to_widget(target, &event, &mut *ops);
1283 }
1284 }
1285 WidgetEvent::ScrollIntoView { .. }
1286 | WidgetEvent::PointerEnter { .. }
1287 | WidgetEvent::PointerLeave { .. }
1288 | WidgetEvent::FocusGained { .. }
1289 | WidgetEvent::FocusLost => {}
1290 }
1291 self.drain_pending_intents(&mut *ops);
1296 }
1297
1298 fn open_context_menu_from_keyboard(&mut self, ops: &mut dyn crate::window::WindowOps) -> bool {
1311 let Some(focused) = self.focused else {
1312 return false;
1313 };
1314 let target = self
1315 .arena
1316 .get(focused)
1317 .and_then(|node| node.widget.context_menu_key_target())
1318 .filter(|id| self.arena.is_active(*id))
1319 .unwrap_or(focused);
1320
1321 let bounds = self.bounds(target);
1325 let anchor = Point {
1326 x: bounds.x + bounds.width / 2.0,
1327 y: bounds.y + bounds.height / 2.0,
1328 };
1329 self.show_context_menu_for(
1330 target,
1331 anchor,
1332 crate::widget_builder::ContextMenuTrigger::Keyboard,
1333 ops,
1334 )
1335 }
1336
1337 pub(super) fn show_context_menu_for(
1346 &mut self,
1347 target: WidgetId,
1348 position: Point,
1349 trigger: crate::widget_builder::ContextMenuTrigger,
1350 ops: &mut dyn crate::window::WindowOps,
1351 ) -> bool {
1352 let mut ctx = self.make_event_context(&mut *ops);
1358 ctx.context_menu_trigger = Some(trigger);
1359 let mut walker = Some(target);
1360 let menu_decision: Option<(WidgetId, Box<dyn Widget>)> = loop {
1361 let owner_id = {
1364 let mut probe = walker;
1365 loop {
1366 match probe {
1367 None => break None,
1368 Some(id) => {
1369 if self
1370 .arena
1371 .get(id)
1372 .is_some_and(|node| node.context_menu_factory.is_some())
1373 {
1374 break Some(id);
1375 }
1376 probe = self.arena.get(id).and_then(|node| node.parent);
1377 }
1378 }
1379 }
1380 };
1381 let Some(owner_id) = owner_id else {
1382 break None;
1383 };
1384 let outcome: Option<Box<dyn Widget>> = {
1391 let node = self
1392 .arena
1393 .get(owner_id)
1394 .expect("owner_id from active arena walk");
1395 let factory = node
1396 .context_menu_factory
1397 .as_ref()
1398 .expect("owner_id only set when factory present");
1399 factory(position, &mut ctx)
1400 };
1401 match outcome {
1402 Some(menu) => break Some((owner_id, menu)),
1403 None => {
1404 walker = self.arena.get(owner_id).and_then(|n| n.parent);
1406 }
1407 }
1408 };
1409
1410 let drain_anchor = menu_decision
1414 .as_ref()
1415 .map(|(id, _)| *id)
1416 .or_else(|| self.arena.roots().first().copied())
1417 .unwrap_or(target);
1418 self.collect_from_ctx(ctx, drain_anchor);
1419
1420 let Some((owner_id, menu_widget)) = menu_decision else {
1421 return false;
1422 };
1423
1424 let keep: std::collections::HashSet<WidgetId> = self
1430 .overlay_manager
1431 .stack
1432 .iter()
1433 .map(|o| o.content_id)
1434 .filter(|&content_id| self.is_descendant_of(owner_id, content_id))
1435 .collect();
1436 let dismissed = self.overlay_manager.dismiss_except(&keep);
1437 self.dormant_dismissed_content(&dismissed, &mut *ops);
1438
1439 let content_id = self.add_boxed(menu_widget);
1440 let prev_focus = self.focused;
1441 let placement =
1448 crate::overlay::OverlayPlacement::at_pointer_for(position, &self.current_input.pointer);
1449 self.overlay_manager
1450 .show(crate::overlay::OverlayRequest::new(
1451 content_id,
1452 owner_id,
1453 placement,
1454 crate::overlay::DismissBehavior::EscapeOrClickOutside,
1455 ));
1456 if let Some(focus_id) = prev_focus {
1457 self.overlay_manager.set_top_focus_restore(focus_id);
1458 }
1459 self.focus_ops(content_id, &mut *ops);
1460 self.drain_pending_intents(&mut *ops);
1465 true
1466 }
1467
1468 fn arm_outside_press_dismissal(
1489 &mut self,
1490 position: Point,
1491 button: PointerButton,
1492 ops: &mut dyn crate::window::WindowOps,
1493 ) -> bool {
1494 self.prune_stale_dismiss_arms();
1495 let pressing = self.current_input.pointer;
1496 let busy = self.busy_press_points(pressing.id);
1497
1498 if pressing.kind.is_direct() {
1499 return self
1500 .overlay_manager
1501 .arm_dismiss(pressing.id, position, &busy)
1502 .suppress_beneath;
1503 }
1504
1505 let (dismissed, focus_restore, toggle_anchors) =
1506 self.overlay_manager.dismiss_outside_press(position, &busy);
1507 if dismissed.is_empty() {
1508 return false;
1509 }
1510 self.dormant_dismissed_content(&dismissed, &mut *ops);
1511 if let Some(restore_id) = focus_restore
1512 && self.arena.is_active(restore_id)
1513 {
1514 self.focus_ops(restore_id, &mut *ops);
1515 }
1516 button == PointerButton::Primary
1526 && toggle_anchors.iter().any(|&anchor| {
1527 self.arena.is_active(anchor) && self.arena.bounds(anchor).contains(position)
1528 })
1529 }
1530
1531 fn commit_outside_press_dismissal(
1538 &mut self,
1539 position: Point,
1540 ops: &mut dyn crate::window::WindowOps,
1541 ) -> bool {
1542 let pointer = self.current_pointer_id();
1543 if !self.overlay_manager.has_armed_dismiss(pointer) {
1544 return false;
1545 }
1546 let (dismissed, focus_restore, _anchors) =
1547 self.overlay_manager.commit_dismiss(pointer, position);
1548 if !dismissed.is_empty() {
1549 self.dormant_dismissed_content(&dismissed, &mut *ops);
1550 if let Some(restore_id) = focus_restore
1551 && self.arena.is_active(restore_id)
1552 {
1553 self.focus_ops(restore_id, &mut *ops);
1554 }
1555 }
1556 true
1557 }
1558
1559 fn busy_press_points(&self, exclude: crate::pointer::PointerId) -> Vec<Point> {
1576 self.pointers
1577 .iter()
1578 .filter(|entry| entry.info.id != exclude)
1579 .filter(|entry| self.press_is_revocable(entry.info.id))
1580 .map(|entry| entry.down_position)
1581 .collect()
1582 }
1583
1584 fn prune_stale_dismiss_arms(&mut self) {
1591 let stale: Vec<crate::pointer::PointerId> = self
1592 .overlay_manager
1593 .armed_pointers()
1594 .into_iter()
1595 .filter(|id| self.pointers.get(*id).is_none())
1596 .collect();
1597 for id in stale {
1598 self.overlay_manager.abort_dismiss(id);
1599 }
1600 }
1601
1602 fn begin_press(&mut self, focusable: Option<WidgetId>) {
1623 let pointer = self.current_pointer_id();
1624 let now = self.sequence_now();
1625 let delay = self
1626 .pan_session_open(pointer)
1627 .then(|| self.current_profile().press_feedback_delay)
1628 .filter(|d| !d.is_zero());
1629 self.presses.press(pointer, focusable, now, delay);
1630 }
1631
1632 fn adopt_press_owner(&mut self, button: PointerButton) {
1655 let pointer = self.current_pointer_id();
1656 let Some(owner) = self.current_sequence().and_then(|s| s.pressed_owner()) else {
1657 return;
1658 };
1659 if !self.press_buttons(owner).contains(button) {
1660 crate::trace_input!(
1661 Samples,
1662 "no press visual for {owner:?}: {button:?} is not one of its accepted buttons"
1663 );
1664 return;
1665 }
1666 self.presses.set_owner(pointer, owner);
1667 self.publish_pressed(owner);
1668 }
1669
1670 fn update_press(&mut self, position: Point) {
1683 let pointer = self.current_pointer_id();
1684 if self.presses.get(pointer).is_none() {
1685 return;
1686 }
1687 let claimed_elsewhere = self.current_sequence().is_some_and(|sequence| {
1690 sequence
1691 .winner()
1692 .is_some_and(|winner| Some(winner) != sequence.pressed_owner())
1693 });
1694 if claimed_elsewhere {
1695 self.end_press(pointer);
1696 return;
1697 }
1698 let profile = self.current_profile();
1699 let origin = self.current_sequence().map(|s| s.press_origin());
1700 let owner = self.presses.get(pointer).and_then(|p| p.owner);
1701 let inside = match (origin, owner) {
1702 (Some(origin), Some(owner)) => {
1703 let bounds = self
1704 .arena
1705 .is_active(owner)
1706 .then(|| self.arena.bounds(owner));
1707 !crate::gesture::TapBoundary::for_pointer(&self.current_input.pointer, &profile)
1708 .left(origin, position, bounds, &profile)
1709 }
1710 _ => true,
1713 };
1714 let Some(press) = self.presses.get_mut(pointer) else {
1715 return;
1716 };
1717 if press.inside == inside {
1718 return;
1719 }
1720 press.inside = inside;
1721 if let Some(owner) = owner {
1722 self.publish_pressed(owner);
1723 }
1724 }
1725
1726 pub(crate) fn end_press(&mut self, pointer: crate::pointer::PointerId) {
1732 if let Some(owner) = self.presses.release(pointer) {
1733 self.publish_pressed(owner);
1734 }
1735 }
1736
1737 pub(crate) fn resolve_press_delays(&mut self, now: crate::pointer::EventTime) {
1740 if self.presses.is_empty() {
1741 return;
1742 }
1743 for id in self.presses.resolve_delays(now) {
1744 self.publish_pressed(id);
1745 self.arena.mark_needs_paint(id);
1746 }
1747 }
1748
1749 pub(crate) fn next_press_deadline(&self) -> Option<std::time::Instant> {
1752 self.presses.next_deadline().map(|t| self.instant_for(t))
1753 }
1754
1755 fn focus_on_release(&mut self, position: Point, ops: &mut dyn crate::window::WindowOps) {
1766 let releasing = self.current_input.pointer;
1767 if !releasing.kind.is_direct() {
1768 return;
1769 }
1770 let pointer = self.current_pointer_id();
1771 let Some(pressed) = self.presses.get(pointer).and_then(|p| p.focusable) else {
1772 return;
1773 };
1774 let released_on = self
1775 .hit_test_for(position, &releasing)
1776 .and_then(|target| self.find_focusable_at_or_above(target));
1777 if released_on == Some(pressed) {
1778 self.focus_with_origin_ops(
1779 pressed,
1780 crate::focus::FocusOrigin::Pointer(releasing.kind),
1781 &mut *ops,
1782 );
1783 }
1784 }
1785
1786 fn handle_pointer_move(
1806 &mut self,
1807 event: &WidgetEvent,
1808 position: Point,
1809 ops: &mut dyn crate::window::WindowOps,
1810 ) {
1811 let moving = self.current_input.pointer;
1816 if self.pointers.hover_owner_id() != Some(self.current_pointer_id()) {
1817 if let Some(target) = self.hit_test_for(position, &moving) {
1818 self.dispatch_to_widget(target, event, &mut *ops);
1819 }
1820 return;
1821 }
1822 let target = self.hit_test_for(position, &moving);
1823
1824 if target != self.hovered_id() {
1825 let hover = self.hover_transition_pointer();
1827 let previously_hovered = self.hovered_id();
1828 if let Some(old) = previously_hovered {
1829 self.dispatch_to_widget(
1830 old,
1831 &WidgetEvent::PointerLeave { pointer: hover },
1832 &mut *ops,
1833 );
1834 self.tooltip_pointer_leave(old, &mut *ops);
1835 }
1836 if let Some(new) = target {
1837 self.dispatch_to_widget(
1838 new,
1839 &WidgetEvent::PointerEnter { pointer: hover },
1840 &mut *ops,
1841 );
1842 self.tooltip_pointer_enter(new);
1843 }
1844 self.set_hovered(target);
1845 self.update_hover_within_signals(previously_hovered, target);
1846 } else if let Some(target) = target {
1847 self.tooltip_pointer_moved(target, position);
1850 }
1851
1852 if let Some(target) = target {
1853 self.dispatch_to_widget(target, event, &mut *ops);
1854 }
1855 }
1856
1857 pub(super) fn dispatch_to_widget(
1858 &mut self,
1859 target: WidgetId,
1860 event: &WidgetEvent,
1861 ops: &mut dyn crate::window::WindowOps,
1862 ) {
1863 self.dispatch_to_widget_returning_handled(target, event, ops);
1864 }
1865
1866 pub(super) fn localize_event(
1882 frame: &crate::arena::LocalFrame,
1883 event: &WidgetEvent,
1884 ) -> Option<WidgetEvent> {
1885 match event {
1886 WidgetEvent::PointerDown {
1887 position,
1888 button,
1889 modifiers,
1890 pointer,
1891 } => Some(WidgetEvent::PointerDown {
1892 position: frame.to_local(*position),
1893 button: *button,
1894 modifiers: *modifiers,
1895 pointer: *pointer,
1896 }),
1897 WidgetEvent::PointerUp {
1898 position,
1899 button,
1900 modifiers,
1901 pointer,
1902 } => Some(WidgetEvent::PointerUp {
1903 position: frame.to_local(*position),
1904 button: *button,
1905 modifiers: *modifiers,
1906 pointer: *pointer,
1907 }),
1908 WidgetEvent::PointerMove {
1909 position,
1910 modifiers,
1911 pointer,
1912 } => Some(WidgetEvent::PointerMove {
1913 position: frame.to_local(*position),
1914 modifiers: *modifiers,
1915 pointer: *pointer,
1916 }),
1917 WidgetEvent::Gesture { gesture } => Some(WidgetEvent::Gesture {
1918 gesture: Self::localize_gesture(frame, gesture),
1919 }),
1920 _ => None,
1926 }
1927 }
1928
1929 fn localize_gesture(frame: &crate::arena::LocalFrame, gesture: &GestureEvent) -> GestureEvent {
1936 let loc = |p: teksilo_canvas::Point| frame.to_local(p);
1937 let tap = |t: &TapEvent| {
1938 TapEvent::new(loc(t.position), t.button, t.modifiers).with_pointer(t.pointer)
1939 };
1940 match gesture {
1941 GestureEvent::Tap(t) => GestureEvent::Tap(tap(t)),
1942 GestureEvent::DoubleTap(t) => GestureEvent::DoubleTap(tap(t)),
1943 GestureEvent::TripleTap(t) => GestureEvent::TripleTap(tap(t)),
1944 GestureEvent::LongPress(t) => GestureEvent::LongPress(tap(t)),
1945 GestureEvent::DragStarted {
1946 position,
1947 button,
1948 pointer,
1949 } => GestureEvent::DragStarted {
1950 position: loc(*position),
1951 button: *button,
1952 pointer: *pointer,
1953 },
1954 GestureEvent::DragMoved {
1955 position,
1956 delta,
1957 pointer,
1958 } => GestureEvent::DragMoved {
1959 position: loc(*position),
1960 delta: *delta,
1961 pointer: *pointer,
1962 },
1963 GestureEvent::DragEnded { position, pointer } => GestureEvent::DragEnded {
1964 position: loc(*position),
1965 pointer: *pointer,
1966 },
1967 GestureEvent::DragCancelled {
1968 position,
1969 pointer,
1970 reason,
1971 } => GestureEvent::DragCancelled {
1972 position: loc(*position),
1973 pointer: *pointer,
1974 reason: *reason,
1975 },
1976 GestureEvent::PinchStarted { center } => GestureEvent::PinchStarted {
1977 center: loc(*center),
1978 },
1979 GestureEvent::PinchChanged {
1980 center,
1981 scale,
1982 rotation,
1983 } => GestureEvent::PinchChanged {
1984 center: loc(*center),
1985 scale: *scale,
1986 rotation: *rotation,
1987 },
1988 GestureEvent::PinchEnded => GestureEvent::PinchEnded,
1989 GestureEvent::PinchCancelled { reason } => {
1990 GestureEvent::PinchCancelled { reason: *reason }
1991 }
1992 GestureEvent::Swipe {
1993 direction,
1994 velocity,
1995 } => GestureEvent::Swipe {
1996 direction: *direction,
1997 velocity: *velocity,
1998 },
1999 }
2000 }
2001
2002 pub(super) fn dispatch_to_widget_returning_handled(
2008 &mut self,
2009 target: WidgetId,
2010 event: &WidgetEvent,
2011 ops: &mut dyn crate::window::WindowOps,
2012 ) -> bool {
2013 if !self.arena.is_enabled(target) {
2014 return false;
2015 }
2016
2017 let mut ancestors = Vec::new();
2018 let mut current = self.arena.parent(target);
2019 while let Some(id) = current {
2020 ancestors.push(id);
2021 current = self.arena.parent(id);
2022 }
2023 ancestors.reverse();
2024
2025 let tap_owner: Option<WidgetId> = if matches!(
2043 event,
2044 WidgetEvent::PointerDown { .. } | WidgetEvent::PointerUp { .. }
2045 ) {
2046 let mut owner = None;
2047 let mut cur = Some(target);
2048 while let Some(id) = cur {
2049 if self.arena.get(id).is_some_and(|n| {
2050 n.any_handler(|h| h.on_tap.is_some() || h.on_long_press.is_some())
2051 }) {
2052 owner = Some(id);
2053 break;
2054 }
2055 cur = self.arena.parent(id);
2056 }
2057 owner
2058 } else {
2059 None
2060 };
2061
2062 for &id in &ancestors {
2063 let frame = self.arena.local_frame(id);
2064 let mut ctx = self
2065 .make_event_context(&mut *ops)
2066 .with_dispatch_node(id)
2067 .with_dispatch_target(target)
2068 .with_local_frame(frame);
2069 ctx.press_claimed_by_interactive_child =
2070 tap_owner.is_some_and(|owner| owner != id && self.is_descendant_of(owner, id));
2071 let localized = Self::localize_event(&frame, event);
2074 let event = localized.as_ref().unwrap_or(event);
2075 let response = if let Some(node) = self.arena.get_mut(id) {
2076 Self::try_handler_preview(node, event, &mut ctx).unwrap_or(EventResponse::Ignored)
2077 } else {
2078 EventResponse::Ignored
2079 };
2080 self.collect_from_ctx(ctx, id);
2081 if response == EventResponse::Handled {
2082 self.arena.mark_needs_paint(id);
2083 if matches!(event, WidgetEvent::PointerDown { .. }) {
2091 self.note_preview_claim(id);
2092 }
2093 return true;
2094 }
2095 }
2096
2097 let needs_layout_on_handle = matches!(
2098 event,
2099 WidgetEvent::Scroll { .. } | WidgetEvent::ScrollIntoView { .. }
2100 );
2101 let mut current = Some(target);
2102 let mut is_target = true;
2103 while let Some(id) = current {
2104 let frame = self.arena.local_frame(id);
2105 let mut ctx = self
2106 .make_event_context(&mut *ops)
2107 .with_dispatch_node(id)
2108 .with_dispatch_target(target)
2109 .with_local_frame(frame);
2110 ctx.press_claimed_by_interactive_child =
2111 tap_owner.is_some_and(|owner| owner != id && self.is_descendant_of(owner, id));
2112 let localized = Self::localize_event(&frame, event);
2115 let gesture_cx = self.recognizer_context(id);
2116 let arena_blocked = self.sequence_blocks_arena(id);
2119 let WidgetTree {
2120 arena,
2121 gesture_owners,
2122 ..
2123 } = self;
2124 let event = localized.as_ref().unwrap_or(event);
2125 let response = if let Some(node) = arena.get_mut(id) {
2126 Self::try_handler_bubble(
2127 node,
2128 event,
2129 &mut ctx,
2130 BubbleGates {
2131 fire_on_pointer_event: is_target,
2132 arena_blocked,
2133 },
2134 id,
2135 gesture_owners,
2136 gesture_cx,
2137 )
2138 .unwrap_or(EventResponse::Ignored)
2139 } else {
2140 EventResponse::Ignored
2141 };
2142 self.collect_from_ctx(ctx, id);
2143 if response == EventResponse::Handled {
2144 if needs_layout_on_handle {
2145 self.arena.mark_needs_layout(id);
2146 } else {
2147 self.arena.mark_needs_paint(id);
2148 }
2149 self.note_pointer_acceptance(id, event);
2150 if !matches!(
2165 event,
2166 WidgetEvent::PointerEnter { .. } | WidgetEvent::PointerLeave { .. }
2167 ) {
2168 return true;
2169 }
2170 }
2171 is_target = false;
2172 current = self.arena.parent(id);
2173 }
2174 false
2175 }
2176
2177 pub(super) fn dispatch_to_widget_direct(
2178 &mut self,
2179 target: WidgetId,
2180 event: &WidgetEvent,
2181 ops: &mut dyn crate::window::WindowOps,
2182 ) {
2183 self.dispatch_to_widget_direct_returning_handled(target, event, ops);
2184 }
2185
2186 pub(super) fn dispatch_to_widget_direct_returning_handled(
2196 &mut self,
2197 target: WidgetId,
2198 event: &WidgetEvent,
2199 ops: &mut dyn crate::window::WindowOps,
2200 ) -> bool {
2201 if !self.arena.is_enabled(target) {
2202 return false;
2203 }
2204
2205 let frame = self.arena.local_frame(target);
2206 let mut ctx = self
2207 .make_event_context(&mut *ops)
2208 .with_dispatch_node(target)
2209 .with_dispatch_target(target)
2210 .with_local_frame(frame);
2211 let gesture_cx = self.recognizer_context(target);
2212 let arena_blocked = self.sequence_blocks_arena(target);
2213 let WidgetTree {
2214 arena,
2215 gesture_owners,
2216 ..
2217 } = self;
2218 let response = if let Some(node) = arena.get_mut(target) {
2219 Self::try_handler_bubble(
2220 node,
2221 event,
2222 &mut ctx,
2223 BubbleGates {
2224 fire_on_pointer_event: true,
2225 arena_blocked,
2226 },
2227 target,
2228 gesture_owners,
2229 gesture_cx,
2230 )
2231 .unwrap_or(EventResponse::Ignored)
2232 } else {
2233 EventResponse::Ignored
2234 };
2235 self.collect_from_ctx(ctx, target);
2236
2237 if response == EventResponse::Handled {
2238 if matches!(
2241 event,
2242 WidgetEvent::Scroll { .. } | WidgetEvent::ScrollIntoView { .. }
2243 ) {
2244 self.arena.mark_needs_layout(target);
2245 } else {
2246 self.arena.mark_needs_paint(target);
2247 }
2248 self.note_pointer_acceptance(target, event);
2249 }
2250 response == EventResponse::Handled
2251 }
2252
2253 pub(super) fn note_pointer_acceptance(&mut self, target: WidgetId, event: &WidgetEvent) {
2262 if !matches!(
2263 event,
2264 WidgetEvent::PointerDown { .. }
2265 | WidgetEvent::PointerMove { .. }
2266 | WidgetEvent::PointerUp { .. }
2267 ) {
2268 return;
2269 }
2270 let pointer = self.current_pointer_id();
2271 if let Some(entry) = self.pointers.get_mut(pointer) {
2272 entry.last_accepted = Some(target);
2273 }
2274 }
2275
2276 fn try_handler_preview(
2277 node: &mut crate::arena::WidgetNode,
2278 event: &WidgetEvent,
2279 ctx: &mut EventContext,
2280 ) -> Option<EventResponse> {
2281 match event {
2282 WidgetEvent::KeyDown { .. }
2290 | WidgetEvent::KeyUp { .. }
2291 | WidgetEvent::ImeComposition { .. }
2292 | WidgetEvent::ImeCommit { .. } => {
2293 let has = node.external_handlers.on_key_preview.is_some()
2294 || node.handlers.on_key_preview.is_some();
2295 if !has {
2296 return None;
2297 }
2298 Some(fire_event_handler_both(
2299 &mut node.external_handlers.on_key_preview,
2300 &mut node.handlers.on_key_preview,
2301 event,
2302 ctx,
2303 ))
2304 }
2305 WidgetEvent::PointerEnter { .. } | WidgetEvent::PointerLeave { .. } => None,
2317 _ => {
2318 let has = node.external_handlers.on_pointer_event.is_some()
2319 || node.handlers.on_pointer_event.is_some();
2320 if !has {
2321 return None;
2322 }
2323 Some(fire_event_handler_both(
2324 &mut node.external_handlers.on_pointer_event,
2325 &mut node.handlers.on_pointer_event,
2326 event,
2327 ctx,
2328 ))
2329 }
2330 }
2331 }
2332
2333 fn try_handler_bubble(
2334 node: &mut crate::arena::WidgetNode,
2335 event: &WidgetEvent,
2336 ctx: &mut EventContext,
2337 gates: BubbleGates,
2338 node_id: WidgetId,
2339 gesture_owners: &mut std::collections::HashSet<WidgetId>,
2340 gesture_cx: crate::gesture::RecognizerContext<'_>,
2341 ) -> Option<EventResponse> {
2342 let BubbleGates {
2343 fire_on_pointer_event,
2344 arena_blocked,
2345 } = gates;
2346 match event {
2347 WidgetEvent::PointerEnter { .. } => {
2348 if let Some(cursor) = node.node_cursor {
2349 ctx.declared_cursor_request = Some(cursor);
2354 }
2355 let mut fired = false;
2356 if let Some(h) = node.external_handlers.on_hover.as_mut() {
2357 h(true, ctx);
2358 fired = true;
2359 }
2360 if let Some(h) = node.handlers.on_hover.as_mut() {
2361 h(true, ctx);
2362 fired = true;
2363 }
2364 if fired {
2365 Some(EventResponse::Handled)
2366 } else {
2367 node.node_cursor.map(|_| EventResponse::Handled)
2368 }
2369 }
2370 WidgetEvent::PointerLeave { .. } => {
2371 if node.node_cursor.is_some() {
2372 ctx.declared_cursor_request = Some(crate::widget::CursorIcon::Default);
2373 }
2374 let mut fired = false;
2375 if let Some(h) = node.external_handlers.on_hover.as_mut() {
2376 h(false, ctx);
2377 fired = true;
2378 }
2379 if let Some(h) = node.handlers.on_hover.as_mut() {
2380 h(false, ctx);
2381 fired = true;
2382 }
2383 if fired {
2384 Some(EventResponse::Handled)
2385 } else {
2386 node.node_cursor.map(|_| EventResponse::Handled)
2387 }
2388 }
2389 WidgetEvent::FocusGained { .. } => {
2390 let mut fired = false;
2391 if let Some(h) = node.external_handlers.on_focus.as_mut() {
2392 h(true, ctx);
2393 fired = true;
2394 }
2395 if let Some(h) = node.handlers.on_focus.as_mut() {
2396 h(true, ctx);
2397 fired = true;
2398 }
2399 fired.then_some(EventResponse::Handled)
2400 }
2401 WidgetEvent::FocusLost => {
2402 let mut fired = false;
2403 if let Some(h) = node.external_handlers.on_focus.as_mut() {
2404 h(false, ctx);
2405 fired = true;
2406 }
2407 if let Some(h) = node.handlers.on_focus.as_mut() {
2408 h(false, ctx);
2409 fired = true;
2410 }
2411 fired.then_some(EventResponse::Handled)
2412 }
2413 WidgetEvent::KeyDown { .. }
2414 | WidgetEvent::KeyUp { .. }
2415 | WidgetEvent::ImeComposition { .. }
2416 | WidgetEvent::ImeCommit { .. } => {
2417 if node.external_handlers.on_key.is_some() || node.handlers.on_key.is_some() {
2418 Some(fire_event_handler_both(
2419 &mut node.external_handlers.on_key,
2420 &mut node.handlers.on_key,
2421 event,
2422 ctx,
2423 ))
2424 } else {
2425 None
2426 }
2427 }
2428 WidgetEvent::Scroll { .. } | WidgetEvent::ScrollIntoView { .. } => {
2429 if node.external_handlers.on_scroll.is_some() || node.handlers.on_scroll.is_some() {
2430 Some(fire_event_handler_both(
2431 &mut node.external_handlers.on_scroll,
2432 &mut node.handlers.on_scroll,
2433 event,
2434 ctx,
2435 ))
2436 } else {
2437 None
2438 }
2439 }
2440 WidgetEvent::AccessAction {
2441 action,
2442 target_node,
2443 data,
2444 ..
2445 } => {
2446 let saved_a11y_source = ctx
2466 .current_source
2467 .replace(crate::telemetry::IntentSource::Accessibility);
2468 let mut any_slot = false;
2469 let mut any_handled = false;
2470 if let Some(h) = node.external_handlers.on_access_action_request.as_mut() {
2471 any_slot = true;
2472 any_handled |=
2473 h(*action, *target_node, data.clone(), ctx) == EventResponse::Handled;
2474 }
2475 if let Some(h) = node.handlers.on_access_action_request.as_mut() {
2476 any_slot = true;
2477 any_handled |=
2478 h(*action, *target_node, data.clone(), ctx) == EventResponse::Handled;
2479 }
2480 if let Some(h) = node.external_handlers.on_access_action.as_mut() {
2481 any_slot = true;
2482 any_handled |= h(*action, ctx) == EventResponse::Handled;
2483 }
2484 if let Some(h) = node.handlers.on_access_action.as_mut() {
2485 any_slot = true;
2486 any_handled |= h(*action, ctx) == EventResponse::Handled;
2487 }
2488 let user_handled = any_slot.then_some(if any_handled {
2489 EventResponse::Handled
2490 } else {
2491 EventResponse::Ignored
2492 });
2493
2494 let mut override_handled = false;
2500 if let Some(ov) = node.access_overrides.as_deref_mut() {
2501 if matches!(action, accesskit::Action::CustomAction) {
2502 if let Some(accesskit::ActionData::CustomAction(idx)) = data
2503 && let Some((_, cb)) = ov.custom_actions.get_mut(*idx as usize)
2504 {
2505 cb(ctx);
2506 override_handled = true;
2507 }
2508 } else {
2509 for (a, cb) in ov.actions.iter_mut() {
2510 if *a == *action {
2511 cb(ctx);
2512 override_handled = true;
2513 }
2514 }
2515 }
2516 }
2517
2518 ctx.current_source = saved_a11y_source;
2519 match (user_handled, override_handled) {
2520 (Some(EventResponse::Handled), _) | (_, true) => Some(EventResponse::Handled),
2521 (Some(EventResponse::Ignored), false) => Some(EventResponse::Ignored),
2522 (None, false) => None,
2523 }
2524 }
2525 WidgetEvent::Gesture { gesture } => {
2526 let matched = matches!(
2530 gesture,
2531 GestureEvent::PinchStarted { .. }
2532 | GestureEvent::PinchChanged { .. }
2533 | GestureEvent::PinchEnded
2534 | GestureEvent::Swipe { .. }
2535 | GestureEvent::DoubleTap { .. }
2536 | GestureEvent::TripleTap { .. }
2537 ) && {
2538 let has_handler = match gesture {
2539 GestureEvent::PinchStarted { .. }
2540 | GestureEvent::PinchChanged { .. }
2541 | GestureEvent::PinchEnded => node.any_handler(|h| h.on_pinch.is_some()),
2542 GestureEvent::Swipe { .. } => node.any_handler(|h| h.on_swipe.is_some()),
2543 GestureEvent::DoubleTap { .. } => {
2544 node.any_handler(|h| h.on_double_tap.is_some())
2545 }
2546 GestureEvent::TripleTap { .. } => {
2547 node.any_handler(|h| h.on_triple_tap.is_some())
2548 }
2549 _ => false,
2550 };
2551 if has_handler {
2552 Self::dispatch_recognized_gesture(node, *gesture, ctx);
2553 }
2554 has_handler
2555 };
2556 if matched {
2557 Some(EventResponse::Handled)
2558 } else {
2559 None
2560 }
2561 }
2562 WidgetEvent::PointerDown {
2563 position,
2564 button,
2565 modifiers,
2566 ..
2567 } => {
2568 if fire_on_pointer_event {
2575 let r = fire_event_handler_both(
2576 &mut node.external_handlers.on_pointer_event,
2577 &mut node.handlers.on_pointer_event,
2578 event,
2579 ctx,
2580 );
2581 if r == EventResponse::Handled {
2582 return Some(EventResponse::Handled);
2583 }
2584 }
2585 if arena_blocked {
2586 return None;
2590 }
2591 Self::ensure_gesture_arena(node, node_id, gesture_owners);
2592 if let Some(arena) = node.handlers.gesture_arena.as_mut() {
2593 let cx = gesture_cx;
2594 ctx.capture_pointer_implicit();
2609 let result = arena.process(
2610 &RawPointerEvent::Down {
2611 position: *position,
2612 button: *button,
2613 modifiers: *modifiers,
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 }
2622 return Some(EventResponse::Handled);
2623 }
2624 None
2625 }
2626 WidgetEvent::PointerUp {
2627 position,
2628 button,
2629 modifiers,
2630 ..
2631 } => {
2632 if fire_on_pointer_event {
2633 let r = fire_event_handler_both(
2634 &mut node.external_handlers.on_pointer_event,
2635 &mut node.handlers.on_pointer_event,
2636 event,
2637 ctx,
2638 );
2639 if r == EventResponse::Handled {
2640 return Some(EventResponse::Handled);
2641 }
2642 }
2643 if arena_blocked {
2644 return None;
2645 }
2646 if let Some(arena) = node.handlers.gesture_arena.as_mut() {
2647 let cx = gesture_cx;
2648 let result = arena.process(
2649 &RawPointerEvent::Up {
2650 position: *position,
2651 button: *button,
2652 modifiers: *modifiers,
2653 pointer: cx.pointer,
2654 time: cx.now,
2655 },
2656 &cx,
2657 );
2658 if let Some(gesture) = result {
2659 Self::dispatch_recognized_gesture(node, gesture, ctx);
2660 }
2661 return Some(EventResponse::Handled);
2662 }
2663 None
2664 }
2665 WidgetEvent::PointerMove { position, .. } => {
2666 if fire_on_pointer_event {
2667 let r = fire_event_handler_both(
2668 &mut node.external_handlers.on_pointer_event,
2669 &mut node.handlers.on_pointer_event,
2670 event,
2671 ctx,
2672 );
2673 if r == EventResponse::Handled {
2674 return Some(EventResponse::Handled);
2675 }
2676 }
2677 if arena_blocked {
2678 return None;
2679 }
2680 if let Some(arena) = node.handlers.gesture_arena.as_mut() {
2681 let cx = gesture_cx;
2682 let result = arena.process(
2683 &RawPointerEvent::Move {
2684 position: *position,
2685 pointer: cx.pointer,
2686 time: cx.now,
2687 },
2688 &cx,
2689 );
2690 if let Some(gesture) = result {
2691 Self::dispatch_recognized_gesture(node, gesture, ctx);
2692 return Some(EventResponse::Handled);
2704 }
2705 return Some(EventResponse::Ignored);
2706 }
2707 None
2708 }
2709 WidgetEvent::PointerCancel {
2710 reason, pointer, ..
2711 } => {
2712 for slot in [
2720 &mut node.external_handlers.on_pointer_cancel,
2721 &mut node.handlers.on_pointer_cancel,
2722 ] {
2723 if let Some(handler) = slot.as_mut() {
2724 handler(pointer, *reason, ctx);
2725 }
2726 }
2727 if fire_on_pointer_event {
2728 let r = fire_event_handler_both(
2729 &mut node.external_handlers.on_pointer_event,
2730 &mut node.handlers.on_pointer_event,
2731 event,
2732 ctx,
2733 );
2734 if r == EventResponse::Handled {
2735 return Some(EventResponse::Handled);
2736 }
2737 }
2738 None
2739 }
2740 }
2741 }
2742
2743 pub(super) fn collect_from_ctx<'ops>(
2744 &mut self,
2745 mut ctx: EventContext<'ops>,
2746 source_widget: WidgetId,
2747 ) {
2748 let local_ops = ctx.window_ops.take();
2754 let mut noop = crate::window::NoopWindowOps;
2755 let ops: &mut dyn crate::window::WindowOps = match local_ops {
2756 Some(o) => o,
2757 None => &mut noop,
2758 };
2759 if ctx.frame_requested {
2760 self.request_frame();
2761 }
2762 if let Some(declared) = ctx.declared_cursor_request {
2766 self.node_declared_cursor = declared;
2767 self.current_cursor = declared;
2768 }
2769 match ctx.cursor_request {
2770 Some(crate::widget::CursorRequest::Set(cursor)) => {
2771 self.current_cursor = cursor;
2772 }
2773 Some(crate::widget::CursorRequest::Release) => {
2777 self.current_cursor = self.node_declared_cursor;
2778 }
2779 None => {}
2780 }
2781 for intent in ctx.pending_intents {
2787 self.enqueue_intent(source_widget, intent, true);
2788 }
2789 if ctx.cancel_key_capture {
2798 self.cancel_key_capture();
2799 }
2800 if let Some(slot) = ctx.pending_key_capture {
2801 self.key_capture = Some(slot);
2802 }
2803 for mutation in ctx.pending_shortcut_mutations {
2805 match mutation {
2806 crate::widget::ShortcutMutation::RebindPrimary { id, keystroke } => {
2807 self.shortcut_registry.rebind_primary(id, keystroke);
2808 }
2809 crate::widget::ShortcutMutation::RebindSecondary { id, keystroke } => {
2810 self.shortcut_registry.rebind_secondary(id, keystroke);
2811 }
2812 crate::widget::ShortcutMutation::ClearOverride { id } => {
2813 self.shortcut_registry.clear_override(&id);
2814 }
2815 }
2816 }
2817 if ctx.close_window_requested {
2818 self.close_window_requested = true;
2819 }
2820 if ctx.force_close_requested {
2821 self.force_close_requested = true;
2822 }
2823 self.pending_modal_requests
2824 .extend(ctx.modal_requests.into_iter().map(|request| {
2825 crate::modal::QueuedModalRequest {
2826 source_widget,
2827 request,
2828 }
2829 }));
2830 if ctx.dismiss_modal && !self.dismiss_modal_for_source(source_widget, &mut *ops) {
2831 self.pending_modal_dismissal = true;
2832 }
2833 for callback in ctx.idle_callbacks {
2834 self.idle_queue.push_boxed(callback);
2835 }
2836 match ctx.dismiss_scope {
2837 Some(crate::widget::DismissScope::All) => {
2838 let dismissed = self.overlay_manager.dismiss_all();
2839 self.dormant_dismissed_content(&dismissed, &mut *ops);
2840 }
2841 Some(crate::widget::DismissScope::AllExceptHosts) => {
2842 self.dismiss_all_overlays_except_hosts(&mut *ops);
2843 }
2844 Some(crate::widget::DismissScope::SelfChain) => {
2845 self.dismiss_self_overlay_chain_for_source(source_widget, &mut *ops);
2846 }
2847 Some(crate::widget::DismissScope::Top) => {
2848 if let Some((_id, content_ids, focus_restore)) = self.overlay_manager.dismiss_top()
2849 {
2850 self.dormant_dismissed_content(&content_ids, &mut *ops);
2851 if let Some(restore_id) = focus_restore
2852 && self.arena.is_active(restore_id)
2853 {
2854 self.focus_ops(restore_id, &mut *ops);
2855 }
2856 }
2857 }
2858 None => {
2859 for id in ctx.overlay_dismissals {
2860 let dismissed = self.overlay_manager.dismiss(id);
2861 self.dormant_dismissed_content(&dismissed, &mut *ops);
2862 }
2863 }
2864 }
2865 for content_id in ctx.overlay_content_dismissals {
2872 if let Some(overlay_id) = self.overlay_manager.find_by_content(content_id) {
2873 let dismissed = self.overlay_manager.dismiss(overlay_id);
2874 self.dormant_dismissed_content(&dismissed, &mut *ops);
2875 }
2876 }
2877 for (id, pause) in ctx.overlay_pause_requests {
2884 if pause {
2885 self.overlay_manager.pause_auto_dismiss(id);
2886 } else {
2887 self.overlay_manager.resume_auto_dismiss(id);
2888 }
2889 }
2890 for preserve_content in ctx.dismiss_descendant_overlays {
2891 self.dismiss_child_overlays_for_source(source_widget, preserve_content, &mut *ops);
2892 }
2893 let deferred_row_activations =
2894 self.apply_tree_mutations(std::mem::take(&mut ctx.tree_mutations));
2895 if ctx.request_a11y_update {
2896 self.a11y_dirty = true;
2897 }
2898 if let Some(visible) = ctx.soft_keyboard_request.take() {
2899 self.request_soft_keyboard(visible);
2900 }
2901 for (message, politeness) in std::mem::take(&mut ctx.announcements) {
2904 self.announce_with(message, politeness);
2905 }
2906 for mut req in ctx.overlay_requests {
2907 if req.parent_overlay.is_none() {
2908 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
2909 }
2910 if self
2911 .overlay_manager
2912 .find_by_content(req.content_id)
2913 .is_some()
2914 {
2915 continue;
2916 }
2917 let current_focus = self.focused;
2918 self.overlay_manager.show(req);
2919 self.a11y_dirty = true;
2923 if let Some(focus_id) = current_focus {
2924 self.overlay_manager.set_top_focus_restore(focus_id);
2925 }
2926 }
2927 for (mut req, band) in ctx.overlay_band_requests {
2928 if req.parent_overlay.is_none() {
2929 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
2930 }
2931 if self
2932 .overlay_manager
2933 .find_by_content(req.content_id)
2934 .is_some()
2935 {
2936 continue;
2937 }
2938 let content_id = req.content_id;
2939 self.overlay_manager.show_in_band(req, band);
2940 self.arena.activate(content_id);
2941 self.a11y_dirty = true;
2942 }
2946 for (content_id, placement) in ctx.overlay_placement_updates {
2949 if let Some(overlay_id) = self.overlay_manager.find_by_content(content_id) {
2950 self.overlay_manager.update_placement(overlay_id, placement);
2951 }
2952 }
2953 for (content_id, source) in ctx.overlay_placement_sources {
2954 if let Some(overlay_id) = self.overlay_manager.find_by_content(content_id) {
2955 self.overlay_manager
2956 .set_placement_source(overlay_id, source);
2957 }
2958 }
2959 for (mut req, duration) in ctx.timed_overlay_requests {
2960 if req.parent_overlay.is_none() {
2961 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
2962 }
2963 if self
2964 .overlay_manager
2965 .find_by_content(req.content_id)
2966 .is_some()
2967 {
2968 continue;
2969 }
2970 let current_focus = self.focused;
2971 let overlay_id = self.overlay_manager.show_for(req, duration);
2972 self.overlay_manager
2973 .set_shown_at_sim(overlay_id, self.sim_clock);
2974 self.a11y_dirty = true;
2975 if let Some(focus_id) = current_focus {
2976 self.overlay_manager.set_top_focus_restore(focus_id);
2977 }
2978 }
2979 for (mut req, progress, duration) in ctx.reveal_overlay_requests {
2980 if req.parent_overlay.is_none() {
2981 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
2982 }
2983 if self
2984 .overlay_manager
2985 .find_by_content(req.content_id)
2986 .is_some()
2987 {
2988 continue;
2989 }
2990 let content_id = req.content_id;
2991 let current_focus = self.focused;
2992 let overlay_id = self.overlay_manager.show(req);
2993 self.overlay_manager
2994 .set_shown_at_sim(overlay_id, self.sim_clock);
2995 self.a11y_dirty = true;
2996 if let Some(focus_id) = current_focus {
2997 self.overlay_manager.set_top_focus_restore(focus_id);
2998 }
2999 self.register_animated_signal(&progress, content_id);
3005 let _ = progress.try_animate_with_options(crate::animation::AnimationRequest {
3006 target: 1.0,
3007 duration,
3008 easing: teksilo_tokens::Easing::EaseOut,
3009 frame_interval: None,
3010 looping: false,
3011 epsilon: 0.0,
3012 max_duration: None,
3013 });
3014 self.overlay_manager
3015 .attach_fade(overlay_id, progress, duration);
3016 }
3017 if let Some((pointer, capture)) = ctx.pointer_capture {
3018 let named = pointer;
3022 let pointer = pointer.unwrap_or_else(|| self.current_pointer_id());
3023 self.set_pointer_capture(pointer, capture.then_some(source_widget));
3024 if capture && ctx.explicit_capture && named.is_none() {
3028 self.note_explicit_capture(source_widget);
3029 }
3030 }
3031 if let Some(activation) = ctx.drag_activation_override.take() {
3032 self.note_drag_activation_override(source_widget, activation);
3036 }
3037 if ctx.recognized_owning_gesture {
3038 self.note_gesture_recognized(source_widget);
3041 }
3042 if !ctx.gesture_acts.is_empty() {
3043 let acts = std::mem::take(&mut ctx.gesture_acts);
3044 self.apply_gesture_acts(&acts, source_widget);
3045 }
3046 if let Some(reason) = ctx.cancel_pointer_request {
3047 let pointer = self.current_pointer_id();
3048 self.cancel_pointer(pointer, reason, &mut *ops);
3049 }
3050 for (mut request, delay, focus_target, replace_siblings) in ctx.delayed_overlay_requests {
3051 if request.parent_overlay.is_none() {
3052 request.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3053 }
3054 if self
3055 .overlay_manager
3056 .find_by_content(request.content_id)
3057 .is_some()
3058 {
3059 continue;
3060 }
3061 let content_id = request.content_id;
3062 self.pending_delayed_overlays
3063 .retain(|pending| pending.request.content_id != content_id);
3064 self.pending_delayed_overlays.push(PendingDelayedOverlay {
3065 request,
3066 delay,
3067 focus_target,
3068 replace_siblings,
3069 real_requested_at: std::time::Instant::now(),
3070 sim_requested_at: self.sim_clock,
3071 });
3072 self.arena.mark_needs_paint(source_widget);
3073 }
3074 for content_id in ctx.cancel_delayed_overlays {
3075 self.pending_delayed_overlays
3076 .retain(|pending| pending.request.content_id != content_id);
3077 }
3078 for content_id in ctx.safe_region_arm_requests {
3085 if let Some(apex) = self
3086 .previous_pointer_position
3087 .or_else(|| self.hover_owner_position())
3088 {
3089 self.overlay_manager.arm_safe_region(
3090 content_id,
3091 apex,
3092 std::time::Instant::now(),
3093 self.sim_clock,
3094 );
3095 }
3096 }
3097 for id in ctx.repaint_requests {
3098 self.arena.mark_needs_paint(id);
3099 }
3100 for id in ctx.synthetic_clicks {
3101 self.synthesise_tap_with_ops(id, &mut *ops);
3106 }
3107 if let Some(&id) = ctx.focus_requests.last() {
3108 let target = self.first_focusable_descendant(id).unwrap_or(id);
3118 self.focus_ops(target, &mut *ops);
3119 }
3120 if let Some(&id) = ctx.focus_into_requests.last() {
3121 if let Some(target) = self.first_focusable_descendant(id) {
3128 self.focus_ops(target, &mut *ops);
3129 }
3130 }
3131
3132 for req in ctx.scroll_into_view_requests {
3141 self.scroll_rect_into_view(
3142 req.from.unwrap_or(source_widget),
3145 req.rect,
3146 req.margin,
3147 req.align,
3148 req.motion,
3149 &mut *ops,
3150 );
3151 }
3152 for (id, margin) in ctx.scroll_widget_into_view_requests {
3159 if self.arena.get(id).is_some() {
3160 let bounds = self.arena.bounds(id);
3161 self.scroll_rect_into_view(
3162 id,
3163 bounds,
3164 margin,
3165 crate::event::ScrollAlign::Minimal,
3166 crate::event::ScrollMotion::Instant,
3167 &mut *ops,
3168 );
3169 }
3170 }
3171
3172 if let Some(&id) = ctx.highlight_tooltip_requests.last() {
3177 self.show_highlight_tooltip(id, &mut *ops);
3178 }
3179
3180 if let Some((source_widget, payload, preview_widget)) = ctx.drag_start_request {
3182 let (preview_content_id, preview_overlay_id) = if let Some(preview) = preview_widget {
3183 let content_id = self.add_boxed(preview);
3190 let overlay_id = self
3191 .overlay_manager
3192 .show(crate::overlay::OverlayRequest::new(
3193 content_id,
3194 source_widget,
3195 crate::overlay::OverlayPlacement::AtPointer(teksilo_canvas::Point::ZERO),
3196 crate::overlay::DismissBehavior::Manual,
3197 ));
3198 self.arena.mark_needs_layout(content_id);
3202 (Some(content_id), Some(overlay_id))
3203 } else {
3204 (None, None)
3205 };
3206 self.active_drag = Some(crate::drag_state::DragSession {
3207 payload,
3208 pointer: self.current_input.pointer,
3214 source_widget: Some(source_widget),
3215 is_external: false,
3216 current_position: teksilo_canvas::Point::ZERO,
3217 current_target: None,
3218 feedback: crate::drag_state::DropFeedback::NoFeedback,
3219 preview_content_id,
3220 preview_overlay_id,
3221 });
3222 self.set_current_pointer_capture(Some(source_widget));
3223 self.current_cursor = crate::widget::CursorIcon::Grabbing;
3226 }
3227 if ctx.cancel_drag {
3228 self.cancel_active_drag(&mut *ops);
3229 }
3230
3231 if let Some(theme) = ctx.theme_request {
3233 self.pending_theme_request = Some(theme);
3239 }
3240 if ctx.follow_system_request {
3241 self.pending_follow_system_request = true;
3245 }
3246 if let Some(locale) = ctx.locale_request {
3247 self.pending_locale_request = Some(locale);
3253 }
3254 if let Some(scale) = ctx.text_scale_request {
3255 self.pending_text_scale_request = Some(scale);
3260 }
3261 if !deferred_row_activations.is_empty() {
3268 self.run_with_event_context(&mut *ops, move |ctx| {
3269 for action in deferred_row_activations {
3270 action(ctx);
3271 }
3272 });
3273 }
3274 }
3275
3276 #[must_use]
3282 fn apply_tree_mutations(
3283 &mut self,
3284 mutations: Vec<crate::widget::TreeMutation>,
3285 ) -> Vec<std::rc::Rc<dyn Fn(&mut crate::widget::EventContext)>> {
3286 let mut deferred_row_activations = Vec::new();
3287 use crate::binding::BindingLevel;
3288 use crate::widget::TreeMutation;
3289
3290 for mutation in mutations {
3291 match mutation {
3292 TreeMutation::SetDormant(id) => {
3293 self.park_subtree(id);
3294 }
3295 TreeMutation::Activate(id) => self.arena.activate(id),
3296 TreeMutation::Destroy(id) => {
3297 if let Some(overlay_id) = self.overlay_manager().find_by_content(id) {
3310 self.dismiss_overlay(overlay_id);
3311 }
3312 self.destroy_subtree(id);
3313 }
3314 TreeMutation::MaterializeNow(id) => {
3318 if self.arena.get(id).is_some() {
3319 self.rebuild_single_widget(id);
3320 }
3321 }
3322 TreeMutation::RowSpaceActivate { row, fallback } => {
3323 deferred_row_activations.push(self.keyboard_toggle_in(row).unwrap_or(fallback));
3333 }
3334 TreeMutation::WithWidgetMut { id, dirty, apply } => {
3335 let existed = if let Some(any) =
3340 self.arena.get_mut(id).and_then(|n| n.widget.as_any_mut())
3341 {
3342 apply(any);
3343 true
3344 } else {
3345 false
3346 };
3347 if existed {
3348 match dirty {
3349 BindingLevel::RepaintOnly => self.arena.mark_needs_paint(id),
3350 BindingLevel::SubtreeRepaint => self.arena.mark_subtree_needs_paint(id),
3351 BindingLevel::Relayout => {
3352 self.arena.mark_needs_layout(id);
3353 self.arena.mark_ancestors_need_layout(id);
3354 }
3355 BindingLevel::Rebuild => {
3356 self.arena.mark_needs_rebuild(id);
3357 self.arena.mark_ancestors_need_layout(id);
3358 }
3359 BindingLevel::AccessibilityOnly => self.a11y_dirty = true,
3360 }
3361 }
3362 }
3363 }
3364 }
3365 deferred_row_activations
3366 }
3367
3368 pub fn hit_test(&self, point: Point) -> Option<WidgetId> {
3377 self.hit_test_excluding_overlay_and_widget(point, None, None)
3378 }
3379
3380 pub fn hit_test_for(
3391 &self,
3392 point: Point,
3393 pointer: &crate::pointer::PointerInfo,
3394 ) -> Option<WidgetId> {
3395 self.hit_test_for_excluding(point, pointer, None, None)
3396 }
3397
3398 pub fn hit_test_for_excluding(
3401 &self,
3402 point: Point,
3403 pointer: &crate::pointer::PointerInfo,
3404 exclude_overlay: Option<crate::overlay::OverlayId>,
3405 exclude_widget: Option<WidgetId>,
3406 ) -> Option<WidgetId> {
3407 let surfaces = self.text_surfaces();
3408 let read_only = |id: WidgetId| surfaces.is_read_only(id);
3409 let hit =
3410 crate::pointer::hit_slop::HitContext::new(pointer.kind, &self.effective_theme.input)
3411 .direction(self.layout_direction)
3412 .read_only_probe(&read_only);
3413 self.hit_test_with(point, exclude_overlay, exclude_widget, &hit)
3414 }
3415
3416 pub fn hit_test_excluding_overlay_and_widget(
3423 &self,
3424 point: Point,
3425 exclude_overlay: Option<crate::overlay::OverlayId>,
3426 exclude_widget: Option<WidgetId>,
3427 ) -> Option<WidgetId> {
3428 self.hit_test_with(
3429 point,
3430 exclude_overlay,
3431 exclude_widget,
3432 &crate::pointer::hit_slop::HitContext::mouse(),
3433 )
3434 }
3435
3436 pub(crate) fn hit_test_with(
3439 &self,
3440 point: Point,
3441 exclude_overlay: Option<crate::overlay::OverlayId>,
3442 exclude_widget: Option<WidgetId>,
3443 hit: &crate::pointer::hit_slop::HitContext<'_>,
3444 ) -> Option<WidgetId> {
3445 if let Some(overlay_id) = self.overlay_manager.hit_test(point) {
3446 if Some(overlay_id) == exclude_overlay {
3447 } else if let Some(overlay) = self.overlay_manager.overlay(overlay_id) {
3449 let content_id = overlay.content_id;
3453 if let Some(found) =
3454 self.arena
3455 .hit_test_in_subtree_with_slop(content_id, point, exclude_widget, hit)
3456 {
3457 return Some(found);
3458 }
3459 if !self
3477 .arena
3478 .get(content_id)
3479 .is_some_and(|node| node.event_pass_through)
3480 {
3481 return None;
3482 }
3483 }
3484 }
3485
3486 if self.overlay_manager.topmost_centered().is_some() {
3487 return None;
3488 }
3489
3490 let mut roots = self.arena.roots();
3496 let inert: Vec<WidgetId> = self
3497 .overlay_manager
3498 .stack
3499 .iter()
3500 .filter(|overlay| overlay.inert)
3501 .map(|overlay| overlay.content_id)
3502 .collect();
3503 if !inert.is_empty() {
3504 roots.retain(|root| !inert.contains(root));
3505 }
3506 self.arena
3507 .hit_test_roots_with_slop(roots, point, exclude_widget, hit)
3508 }
3509}
3510
3511fn is_context_menu_chord(key: Key, modifiers: Modifiers) -> bool {
3529 match key {
3530 Key::ContextMenu => modifiers == Modifiers::NONE,
3531 Key::F10 => modifiers == Modifiers::SHIFT,
3532 #[cfg(target_os = "macos")]
3533 Key::M => modifiers == Modifiers::CTRL | Modifiers::SHIFT,
3534 _ => false,
3535 }
3536}
3537
3538#[cfg(test)]
3539mod tests {
3540 use super::*;
3541 use crate::test_widgets::FillWidget;
3542 use crate::widget::CursorIcon;
3543 use crate::widget_builder::WidgetBuilder;
3544
3545 #[test]
3546 fn pointer_enter_leave_synthesized() {
3547 let mut tree = WidgetTree::new();
3548 let widget = tree.add(FillWidget::new());
3549 tree.layout(SizeProposal::exact(100.0, 50.0));
3550 tree.pointer_move(Point::new(50.0, 25.0));
3551 assert_eq!(tree.hovered(), Some(widget));
3552 tree.pointer_move(Point::new(200.0, 200.0));
3553 assert_eq!(tree.hovered(), None);
3554 }
3555
3556 #[test]
3557 fn pointer_hover_updates_current_cursor() {
3558 let mut tree = WidgetTree::new();
3559 tree.add(FillWidget::new().cursor(CursorIcon::ColResize));
3560 tree.layout(SizeProposal::exact(100.0, 50.0));
3561
3562 tree.pointer_move(Point::new(50.0, 25.0));
3563 assert_eq!(tree.current_cursor(), CursorIcon::ColResize);
3564
3565 tree.pointer_move(Point::new(200.0, 200.0));
3566 assert_eq!(tree.current_cursor(), CursorIcon::Default);
3567 }
3568
3569 #[test]
3579 fn release_cursor_hands_the_cursor_back_to_the_node_that_declared_it() {
3580 let mut tree = WidgetTree::new();
3584 tree.add(
3585 FillWidget::new()
3586 .cursor(CursorIcon::ColResize)
3587 .on_pointer_event(|event, ctx| {
3588 if let WidgetEvent::PointerMove { position, .. } = event {
3589 if position.x < 30.0 {
3590 ctx.set_cursor(CursorIcon::Crosshair);
3591 } else {
3592 ctx.release_cursor();
3593 }
3594 }
3595 EventResponse::Ignored
3596 }),
3597 );
3598 tree.layout(SizeProposal::exact(100.0, 50.0));
3599
3600 tree.pointer_move(Point::new(10.0, 25.0));
3601 assert_eq!(
3602 tree.current_cursor(),
3603 CursorIcon::Crosshair,
3604 "the handler outranks the node it is attached to",
3605 );
3606 tree.pointer_move(Point::new(60.0, 25.0));
3607 assert_eq!(
3608 tree.current_cursor(),
3609 CursorIcon::ColResize,
3610 "and hands it back to the node, not to Default — no enter/leave \
3611 fires on a move within one node, so nothing else could",
3612 );
3613 tree.pointer_move(Point::new(10.0, 25.0));
3614 assert_eq!(
3615 tree.current_cursor(),
3616 CursorIcon::Crosshair,
3617 "and can take it again"
3618 );
3619 tree.pointer_move(Point::new(200.0, 200.0));
3620 assert_eq!(
3621 tree.current_cursor(),
3622 CursorIcon::Default,
3623 "leaving the node clears both the override and the declaration",
3624 );
3625 }
3626
3627 #[test]
3631 fn release_cursor_is_a_no_op_with_nothing_to_undo() {
3632 let mut tree = WidgetTree::new();
3633 tree.add(
3634 FillWidget::new()
3635 .cursor(CursorIcon::ColResize)
3636 .on_pointer_event(|event, ctx| {
3637 if matches!(event, WidgetEvent::PointerMove { .. }) {
3638 ctx.release_cursor();
3639 }
3640 EventResponse::Ignored
3641 }),
3642 );
3643 tree.layout(SizeProposal::exact(100.0, 50.0));
3644 tree.pointer_move(Point::new(50.0, 25.0));
3645 assert_eq!(tree.current_cursor(), CursorIcon::ColResize);
3646
3647 let mut tree = WidgetTree::new();
3649 tree.add(FillWidget::new().on_pointer_event(|event, ctx| {
3650 if let WidgetEvent::PointerMove { position, .. } = event {
3651 if position.x < 30.0 {
3652 ctx.set_cursor(CursorIcon::Crosshair);
3653 } else {
3654 ctx.release_cursor();
3655 }
3656 }
3657 EventResponse::Ignored
3658 }));
3659 tree.layout(SizeProposal::exact(100.0, 50.0));
3660 tree.pointer_move(Point::new(10.0, 25.0));
3661 assert_eq!(tree.current_cursor(), CursorIcon::Crosshair);
3662 tree.pointer_move(Point::new(60.0, 25.0));
3663 assert_eq!(
3664 tree.current_cursor(),
3665 CursorIcon::Default,
3666 "nothing declared a cursor for this chain, so the hand-back \
3667 resolves to Default",
3668 );
3669 }
3670
3671 #[test]
3679 fn an_on_hover_override_does_not_erase_the_declaration_it_outranks() {
3680 let mut tree = WidgetTree::new();
3681 tree.add(
3682 FillWidget::new()
3683 .cursor(CursorIcon::ColResize)
3684 .on_hover(|entered, ctx| {
3685 if entered {
3686 ctx.set_cursor(CursorIcon::Crosshair);
3687 }
3688 })
3689 .on_pointer_event(|event, ctx| {
3690 if let WidgetEvent::PointerMove { position, .. } = event
3691 && position.x >= 30.0
3692 {
3693 ctx.release_cursor();
3694 }
3695 EventResponse::Ignored
3696 }),
3697 );
3698 tree.layout(SizeProposal::exact(100.0, 50.0));
3699
3700 tree.pointer_move(Point::new(10.0, 25.0));
3701 assert_eq!(
3702 tree.current_cursor(),
3703 CursorIcon::Crosshair,
3704 "on_hover runs after the node cursor is applied, so it wins",
3705 );
3706 tree.pointer_move(Point::new(60.0, 25.0));
3707 assert_eq!(
3708 tree.current_cursor(),
3709 CursorIcon::ColResize,
3710 "and the node's declaration survived being overridden",
3711 );
3712 }
3713
3714 #[derive(Debug)]
3717 struct Bumpable {
3718 value: i32,
3719 }
3720
3721 impl crate::widget::Widget for Bumpable {
3722 fn layout_response(
3723 &self,
3724 proposal: SizeProposal,
3725 _ctx: &crate::widget::LayoutContext,
3726 ) -> crate::widget::LayoutResponse {
3727 proposal.resolve(10.0, 10.0).into()
3728 }
3729 fn as_any(&self) -> Option<&dyn std::any::Any> {
3730 Some(self)
3731 }
3732 fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
3733 Some(self)
3734 }
3735 }
3736
3737 #[test]
3738 fn with_widget_mut_applies_and_dirty_marks() {
3739 let mut tree = WidgetTree::new();
3740 let id = tree.add(Bumpable { value: 0 });
3741 tree.layout(SizeProposal::exact(100.0, 100.0));
3742
3743 let mut ctx = EventContext::new();
3744 ctx.with_widget_mut::<Bumpable>(id, crate::binding::BindingLevel::Relayout, |b| {
3745 b.value = 42;
3746 });
3747 tree.collect_from_ctx(ctx, id);
3748
3749 let value = tree
3750 .widget_as_any(id)
3751 .and_then(|a| a.downcast_ref::<Bumpable>())
3752 .map(|b| b.value);
3753 assert_eq!(
3754 value,
3755 Some(42),
3756 "the deferred closure must mutate the live widget"
3757 );
3758 assert!(
3759 tree.needs_layout(),
3760 "Relayout dirty level must mark the tree for relayout"
3761 );
3762 }
3763
3764 #[test]
3765 #[cfg(debug_assertions)]
3766 #[should_panic(expected = "not the requested type")]
3767 fn with_widget_mut_wrong_type_panics_in_debug() {
3768 struct Other;
3769 let mut tree = WidgetTree::new();
3770 let id = tree.add(Bumpable { value: 0 });
3771 let mut ctx = EventContext::new();
3772 ctx.with_widget_mut::<Other>(
3773 id,
3774 crate::binding::BindingLevel::RepaintOnly,
3775 |_o: &mut Other| {},
3776 );
3777 tree.collect_from_ctx(ctx, id);
3780 }
3781
3782 #[test]
3783 fn with_widget_mut_closure_may_fire_observed_signals() {
3784 use crate::signal::Signal;
3792 let mut tree = WidgetTree::new();
3793 let id = tree.add(Bumpable { value: 0 });
3794 tree.layout(SizeProposal::exact(100.0, 100.0));
3795
3796 let trigger = Signal::new(0_u64);
3797 let echo = Signal::new(0_u64);
3798 let echo_for_obs = echo.clone();
3799 let _obs = trigger.observe(move |v| echo_for_obs.set(*v));
3800
3801 let trigger_in = trigger.clone();
3802 let mut ctx = EventContext::new();
3803 ctx.with_widget_mut::<Bumpable>(id, crate::binding::BindingLevel::RepaintOnly, move |b| {
3804 b.value = 7;
3805 trigger_in.set(99);
3807 });
3808 tree.collect_from_ctx(ctx, id); assert_eq!(
3811 echo.get(),
3812 99,
3813 "the observer ran during the deferred mutation"
3814 );
3815 let value = tree
3816 .widget_as_any(id)
3817 .and_then(|a| a.downcast_ref::<Bumpable>())
3818 .map(|b| b.value);
3819 assert_eq!(value, Some(7));
3820 }
3821
3822 #[test]
3823 fn request_accessibility_update_forces_rewalk() {
3824 let mut tree = WidgetTree::new();
3825 let id = tree.add(Bumpable { value: 0 });
3826 tree.layout(SizeProposal::exact(100.0, 100.0));
3827 let _ = tree.sync_accessibility(); assert!(
3829 !tree.a11y_dirty,
3830 "sync_accessibility should clear the dirty flag"
3831 );
3832
3833 let mut ctx = EventContext::new();
3834 ctx.request_accessibility_update();
3835 tree.collect_from_ctx(ctx, id);
3836 assert!(
3837 tree.a11y_dirty,
3838 "request_accessibility_update must force an AT re-walk"
3839 );
3840 }
3841
3842 #[test]
3843 fn rebuild_dirties_accessibility_tree() {
3844 let mut tree = WidgetTree::new();
3851 let id = tree.add(Bumpable { value: 0 });
3852 tree.layout(SizeProposal::exact(100.0, 100.0));
3853 let _ = tree.sync_accessibility(); assert!(
3855 !tree.a11y_dirty,
3856 "sync_accessibility should clear the dirty flag"
3857 );
3858
3859 tree.arena_mark_needs_rebuild_for_testing(id);
3862 tree.layout(SizeProposal::exact(100.0, 100.0));
3863 assert!(
3864 tree.a11y_dirty,
3865 "a rebuild must dirty the AT tree so the next sync re-walks"
3866 );
3867 }
3868
3869 #[test]
3870 fn bound_access_label_change_dirties_accessibility_tree() {
3871 use crate::signal::Signal;
3872 use crate::test_widgets::FillWidget;
3873 use crate::widget_builder::WidgetBuilder;
3874
3875 let label = Signal::new("first".to_string());
3881 let mut tree = WidgetTree::new();
3882 let _id = tree.add(FillWidget::new().access_label(label.clone()));
3883 tree.layout(SizeProposal::exact(100.0, 100.0));
3884 let _ = tree.sync_accessibility(); assert!(!tree.a11y_dirty, "sync_accessibility should clear the flag");
3886
3887 label.set("second".to_string());
3888 tree.layout(SizeProposal::exact(100.0, 100.0));
3889 assert!(
3890 tree.a11y_dirty,
3891 "changing a bound access_label must dirty the AT tree"
3892 );
3893 }
3894
3895 #[test]
3896 fn disabled_ancestor_blocks_event_to_descendant() {
3897 use crate::signal::Signal;
3898 use crate::test_widgets::StackWidget;
3899 use std::cell::Cell;
3900 use std::rc::Rc;
3901
3902 let tapped = Rc::new(Cell::new(false));
3903 let flag = tapped.clone();
3904 let enabled = Signal::new(true);
3905
3906 let mut tree = WidgetTree::new();
3907 let child = tree.add(FillWidget::new().on_tap(move |_pos, _ctx| {
3908 flag.set(true);
3909 }));
3910 let parent = tree.add(StackWidget::new().child(child));
3911 tree.enabled_when(parent, enabled.clone());
3912 tree.layout(SizeProposal::exact(100.0, 50.0));
3913
3914 enabled.set(false);
3915 tree.click(child);
3916 assert!(
3917 !tapped.get(),
3918 "disabled ancestor should block descendant tap"
3919 );
3920
3921 enabled.set(true);
3922 tree.click(child);
3923 assert!(tapped.get(), "re-enabling should restore dispatch");
3924 }
3925
3926 #[test]
3927 fn pointer_positions_are_widget_local_at_nonzero_origin() {
3928 use crate::event::{Modifiers, PointerButton};
3929 use crate::test_widgets::InsetWidget;
3930 use std::cell::Cell;
3931 use std::rc::Rc;
3932
3933 let tap_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
3935 let down_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
3936 let drag_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
3937 let (tp, dp, gp) = (tap_pos.clone(), down_pos.clone(), drag_pos.clone());
3938
3939 let mut tree = WidgetTree::new();
3940 let child = tree.add(
3941 FillWidget::new()
3942 .on_tap(move |ev, _ctx| tp.set(Some(ev.position)))
3943 .on_pointer_event(move |ev, _ctx| {
3944 if let WidgetEvent::PointerDown { position, .. } = ev {
3945 dp.set(Some(*position));
3946 }
3947 crate::event::EventResponse::Ignored
3948 })
3949 .on_drag(move |phase, _ctx| {
3950 use crate::gesture::DragPhase;
3951 match phase {
3952 DragPhase::Started { position, .. }
3953 | DragPhase::Moved { position, .. }
3954 | DragPhase::Ended { position, .. } => gp.set(Some(position)),
3955 _ => {}
3956 }
3957 }),
3958 );
3959 let inset = tree.add(InsetWidget::new(20.0).set_child(child));
3960 let _ = inset;
3961 tree.layout(SizeProposal::exact(200.0, 200.0));
3962 assert_eq!(tree.bounds(child).origin(), Point::new(20.0, 20.0));
3963
3964 tree.dispatch_event(WidgetEvent::pointer_down(
3966 Point::new(50.0, 40.0),
3967 PointerButton::Primary,
3968 Modifiers::NONE,
3969 ));
3970 assert_eq!(
3971 down_pos.get(),
3972 Some(Point::new(30.0, 20.0)),
3973 "on_pointer_event PointerDown must be widget-local"
3974 );
3975 tree.dispatch_event(WidgetEvent::pointer_up(
3976 Point::new(50.0, 40.0),
3977 PointerButton::Primary,
3978 Modifiers::NONE,
3979 ));
3980 assert_eq!(
3981 tap_pos.get(),
3982 Some(Point::new(30.0, 20.0)),
3983 "on_tap position must be widget-local"
3984 );
3985
3986 tree.dispatch_event(WidgetEvent::pointer_down(
3989 Point::new(50.0, 40.0),
3990 PointerButton::Primary,
3991 Modifiers::NONE,
3992 ));
3993 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(65.0, 55.0)));
3996 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(90.0, 70.0)));
3997 assert_eq!(
3998 drag_pos.get(),
3999 Some(Point::new(70.0, 50.0)),
4000 "on_drag position must be widget-local"
4001 );
4002 }
4003
4004 #[test]
4005 fn dormant_widget_not_hit_tested() {
4006 let mut tree = WidgetTree::new();
4007 let widget = tree.add(FillWidget::new());
4008 tree.layout(SizeProposal::exact(100.0, 50.0));
4009
4010 tree.pointer_move(Point::new(50.0, 25.0));
4011 assert_eq!(tree.hovered(), Some(widget));
4012
4013 tree.set_dormant(widget);
4014 tree.pointer_move(Point::new(200.0, 200.0));
4015 tree.pointer_move(Point::new(50.0, 25.0));
4016 assert_eq!(tree.hovered(), None);
4017 }
4018
4019 #[test]
4020 fn ancestor_pointer_handler_does_not_suppress_descendant_hover() {
4021 use crate::event::EventResponse;
4022 use crate::test_widgets::StackWidget;
4023 use std::cell::Cell;
4024 use std::rc::Rc;
4025
4026 let hovered = Rc::new(Cell::new(false));
4028 let h = hovered.clone();
4029
4030 let mut tree = WidgetTree::new();
4031 let child = tree.add(FillWidget::new().on_hover(move |entered, _ctx| h.set(entered)));
4032 tree.add(
4037 StackWidget::new()
4038 .child(child)
4039 .on_pointer_event(|_event, _ctx| EventResponse::Handled),
4040 );
4041 tree.layout(SizeProposal::exact(100.0, 50.0));
4042
4043 tree.pointer_move(Point::new(50.0, 25.0));
4044 assert!(
4045 hovered.get(),
4046 "a greedy ancestor on_pointer_event must NOT swallow the child's PointerEnter"
4047 );
4048
4049 tree.pointer_move(Point::new(500.0, 500.0));
4050 assert!(
4051 !hovered.get(),
4052 "PointerLeave must likewise reach the child despite the ancestor"
4053 );
4054 }
4055
4056 #[test]
4064 fn a_child_hover_handler_does_not_swallow_its_ancestors() {
4065 use crate::test_widgets::StackWidget;
4066 use std::cell::Cell;
4067 use std::rc::Rc;
4068
4069 let (row, button) = (Rc::new(Cell::new(false)), Rc::new(Cell::new(false)));
4070 let (r, b) = (row.clone(), button.clone());
4071
4072 let mut tree = WidgetTree::new();
4073 let child = tree.add(FillWidget::new().on_hover(move |entered, _ctx| b.set(entered)));
4074 tree.add(
4075 StackWidget::new()
4076 .child(child)
4077 .on_hover(move |entered, _ctx| r.set(entered)),
4078 );
4079 tree.layout(SizeProposal::exact(100.0, 50.0));
4080
4081 tree.pointer_move(Point::new(50.0, 25.0));
4082 assert!(button.get(), "the child is hovered");
4083 assert!(row.get(), "and so is the row it is inside");
4084
4085 tree.pointer_move(Point::new(500.0, 500.0));
4086 assert!(!button.get(), "the child heard the leave");
4087 assert!(
4088 !row.get(),
4089 "and so did the row — a container is not still hovered because the \
4090 pointer left it through a button"
4091 );
4092 }
4093
4094 #[test]
4101 fn key_preview_consumes_before_focused_on_key() {
4102 use crate::event::EventResponse;
4105 use crate::test_widgets::StackWidget;
4106 use std::cell::Cell;
4107 use std::rc::Rc;
4108
4109 let leaf_fired = Rc::new(Cell::new(false));
4110 let leaf_flag = leaf_fired.clone();
4111 let preview_fired = Rc::new(Cell::new(false));
4112 let preview_flag = preview_fired.clone();
4113
4114 let mut tree = WidgetTree::new();
4115 let leaf = tree.add(FillWidget::new().focusable().on_key(move |event, _c| {
4116 if matches!(event, WidgetEvent::KeyDown { .. }) {
4119 leaf_flag.set(true);
4120 }
4121 EventResponse::Handled
4122 }));
4123 let mid = tree.add(StackWidget::new().child(leaf));
4124 let _root = tree.add(
4125 StackWidget::new()
4126 .child(mid)
4127 .on_key_preview(move |event, _c| match event {
4128 WidgetEvent::KeyDown {
4129 key: Key::Enter, ..
4130 } => {
4131 preview_flag.set(true);
4132 EventResponse::Handled
4133 }
4134 _ => EventResponse::Ignored,
4135 }),
4136 );
4137
4138 tree.layout(SizeProposal::exact(100.0, 50.0));
4139 tree.focus(leaf);
4140 tree.press_key(Key::Enter, Modifiers::NONE);
4141
4142 assert!(
4143 preview_fired.get(),
4144 "ancestor on_key_preview must fire for KeyDown on a focused descendant"
4145 );
4146 assert!(
4147 !leaf_fired.get(),
4148 "consuming the event in preview must prevent the focused widget's on_key from running"
4149 );
4150 }
4151
4152 #[test]
4153 fn key_preview_falls_through_when_returning_ignored() {
4154 use crate::event::EventResponse;
4157 use crate::test_widgets::StackWidget;
4158 use std::cell::Cell;
4159 use std::rc::Rc;
4160
4161 let leaf_fired = Rc::new(Cell::new(false));
4162 let leaf_flag = leaf_fired.clone();
4163 let preview_fired = Rc::new(Cell::new(false));
4164 let preview_flag = preview_fired.clone();
4165
4166 let mut tree = WidgetTree::new();
4167 let leaf = tree.add(FillWidget::new().focusable().on_key(move |_e, _c| {
4168 leaf_flag.set(true);
4169 EventResponse::Handled
4170 }));
4171 let mid = tree.add(StackWidget::new().child(leaf));
4172 let _root = tree.add(
4173 StackWidget::new()
4174 .child(mid)
4175 .on_key_preview(move |_event, _c| {
4176 preview_flag.set(true);
4177 EventResponse::Ignored
4178 }),
4179 );
4180
4181 tree.layout(SizeProposal::exact(100.0, 50.0));
4182 tree.focus(leaf);
4183 tree.press_key(Key::Enter, Modifiers::NONE);
4184
4185 assert!(preview_fired.get(), "preview must always be invoked");
4186 assert!(
4187 leaf_fired.get(),
4188 "preview returning Ignored must not block the focused widget's on_key"
4189 );
4190 }
4191
4192 #[test]
4193 fn key_preview_excludes_focused_target_itself() {
4194 use crate::event::EventResponse;
4198 use std::cell::Cell;
4199 use std::rc::Rc;
4200
4201 let preview_on_target = Rc::new(Cell::new(false));
4202 let pf = preview_on_target.clone();
4203
4204 let mut tree = WidgetTree::new();
4205 let leaf = tree.add(FillWidget::new().focusable().on_key_preview(move |_e, _c| {
4206 pf.set(true);
4207 EventResponse::Handled
4208 }));
4209 tree.layout(SizeProposal::exact(100.0, 50.0));
4210 tree.focus(leaf);
4211 tree.press_key(Key::Enter, Modifiers::NONE);
4212
4213 assert!(
4214 !preview_on_target.get(),
4215 "the focused widget itself must not see its own on_key_preview"
4216 );
4217 }
4218
4219 #[test]
4220 fn key_preview_root_to_target_order() {
4221 use crate::event::EventResponse;
4225 use crate::test_widgets::StackWidget;
4226 use std::cell::RefCell;
4227 use std::rc::Rc;
4228
4229 let order: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
4230 let outer_log = order.clone();
4231 let inner_log = order.clone();
4232
4233 let mut tree = WidgetTree::new();
4234 let leaf = tree.add(FillWidget::new().focusable());
4235 let inner = tree.add(
4236 StackWidget::new()
4237 .child(leaf)
4238 .on_key_preview(move |event, _c| {
4239 if matches!(event, WidgetEvent::KeyDown { .. }) {
4240 inner_log.borrow_mut().push("inner");
4241 }
4242 EventResponse::Ignored
4243 }),
4244 );
4245 let _outer = tree.add(
4246 StackWidget::new()
4247 .child(inner)
4248 .on_key_preview(move |event, _c| {
4249 if matches!(event, WidgetEvent::KeyDown { .. }) {
4250 outer_log.borrow_mut().push("outer");
4251 }
4252 EventResponse::Ignored
4253 }),
4254 );
4255
4256 tree.layout(SizeProposal::exact(100.0, 50.0));
4257 tree.focus(leaf);
4258 tree.dispatch_event(WidgetEvent::KeyDown {
4259 key: Key::Enter,
4260 modifiers: Modifiers::NONE,
4261 text: None,
4262 });
4263
4264 assert_eq!(
4265 *order.borrow(),
4266 vec!["outer", "inner"],
4267 "preview must walk root → parent-of-target"
4268 );
4269 }
4270
4271 #[test]
4272 fn access_action_routes_to_cursored_target_not_focus() {
4273 use crate::signal::Signal;
4277 let a_fired = Signal::new(false);
4278 let b_fired = Signal::new(false);
4279 let a_cb = a_fired.clone();
4280 let b_cb = b_fired.clone();
4281
4282 let mut tree = WidgetTree::new();
4283 let a = tree.add(
4284 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| a_cb.set(true)),
4285 );
4286 let b = tree.add(
4287 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| b_cb.set(true)),
4288 );
4289 tree.layout(SizeProposal::exact(200.0, 100.0));
4290
4291 tree.focus(a);
4292 tree.dispatch_event(WidgetEvent::AccessAction {
4293 action: accesskit::Action::Click,
4294 target: Some(b),
4295 target_node: crate::accessibility::widget_id_to_node_id(b),
4296 data: None,
4297 });
4298
4299 assert!(b_fired.get(), "the cursored target must receive the action");
4300 assert!(
4301 !a_fired.get(),
4302 "the keyboard-focused widget must NOT receive an action targeting another node"
4303 );
4304 }
4305
4306 #[test]
4307 fn access_action_without_target_is_dropped_not_redirected_to_focus() {
4308 use crate::signal::Signal;
4311 let fired = Signal::new(false);
4312 let cb = fired.clone();
4313
4314 let mut tree = WidgetTree::new();
4315 let widget = tree.add(
4316 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| cb.set(true)),
4317 );
4318 tree.layout(SizeProposal::exact(200.0, 100.0));
4319
4320 tree.focus(widget);
4321 tree.dispatch_event(WidgetEvent::AccessAction {
4322 action: accesskit::Action::Click,
4323 target: None,
4324 target_node: crate::accessibility::root_node_id(),
4325 data: None,
4326 });
4327
4328 assert!(
4329 !fired.get(),
4330 "a target-less action must not be redirected to the focused widget"
4331 );
4332 }
4333
4334 #[test]
4341 fn shortcut_fires_matching_action_on_source_widget() {
4342 use crate::action::Action;
4343 use crate::shortcut::{KeyStroke, Shortcut};
4344 use std::cell::Cell;
4345 use std::rc::Rc;
4346
4347 let fired = Rc::new(Cell::new(false));
4348 let fired_flag = fired.clone();
4349
4350 let mut tree = WidgetTree::new();
4351 let widget = tree.add(FillWidget::new().focusable());
4352 tree.push_action(
4353 widget,
4354 Action::new("app.save").on_invoke(move |_intent, _ctx| {
4355 fired_flag.set(true);
4356 }),
4357 );
4358 tree.shortcut_registry_mut().register(
4359 Shortcut::new("app.save")
4360 .primary(KeyStroke::command(Key::S))
4361 .build(),
4362 );
4363
4364 tree.layout(SizeProposal::exact(100.0, 50.0));
4365 tree.focus(widget);
4366
4367 tree.press_key(Key::S, Modifiers::COMMAND);
4368 assert!(fired.get(), "matching action must fire on KeyDown");
4369 }
4370
4371 #[test]
4372 fn global_shortcut_fires_without_focused_widget() {
4373 use crate::action::Action;
4374 use crate::shortcut::{KeyStroke, Shortcut};
4375 use std::cell::Cell;
4376 use std::rc::Rc;
4377
4378 let fired = Rc::new(Cell::new(false));
4382 let fired_flag = fired.clone();
4383
4384 let mut tree = WidgetTree::new();
4385 let root = tree.add(FillWidget::new());
4386 tree.push_action(
4387 root,
4388 Action::new("app.save").on_invoke(move |_intent, _ctx| {
4389 fired_flag.set(true);
4390 }),
4391 );
4392 tree.shortcut_registry_mut().register(
4393 Shortcut::new("app.save")
4394 .primary(KeyStroke::command(Key::S))
4395 .build(),
4396 );
4397
4398 tree.layout(SizeProposal::exact(100.0, 50.0));
4399 tree.press_key(Key::S, Modifiers::COMMAND);
4402 assert!(
4403 fired.get(),
4404 "global shortcut must fire without a focused widget"
4405 );
4406 }
4407
4408 #[test]
4409 fn global_shortcut_fires_after_focused_widget_destroyed() {
4410 use crate::action::Action;
4411 use crate::shortcut::{KeyStroke, Shortcut};
4412 use std::cell::Cell;
4413 use std::rc::Rc;
4414
4415 let fired = Rc::new(Cell::new(false));
4420 let fired_flag = fired.clone();
4421
4422 let mut tree = WidgetTree::new();
4423 let root = tree.add(FillWidget::new());
4424 let focusable = tree.add_child(root, FillWidget::new().focusable());
4425 tree.push_action(
4426 root,
4427 Action::new("app.save").on_invoke(move |_intent, _ctx| {
4428 fired_flag.set(true);
4429 }),
4430 );
4431 tree.shortcut_registry_mut().register(
4432 Shortcut::new("app.save")
4433 .primary(KeyStroke::command(Key::S))
4434 .build(),
4435 );
4436
4437 tree.layout(SizeProposal::exact(100.0, 50.0));
4438 tree.focus(focusable);
4439 assert_eq!(tree.focused(), Some(focusable));
4440
4441 tree.destroy_subtree(focusable);
4444 assert_eq!(tree.focused(), None, "focus must clear when destroyed");
4445
4446 tree.press_key(Key::S, Modifiers::COMMAND);
4447 assert!(
4448 fired.get(),
4449 "global shortcut must still fire after the focused widget is destroyed"
4450 );
4451 }
4452
4453 #[test]
4454 fn scoped_shortcut_matches_only_when_focus_in_scope() {
4455 use crate::action::Action;
4456 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
4457 use std::cell::Cell;
4458 use std::rc::Rc;
4459
4460 let fired = Rc::new(Cell::new(0));
4461 let fired_flag = fired.clone();
4462
4463 let mut tree = WidgetTree::new();
4464 let scope_root = tree.add(FillWidget::new().focusable());
4465 let inside = tree.add_child(scope_root, FillWidget::new().focusable());
4466 let outside = tree.add(FillWidget::new().focusable());
4467
4468 tree.push_action(
4469 scope_root,
4470 Action::new("editor.find").on_invoke(move |_i, _c| {
4471 fired_flag.set(fired_flag.get() + 1);
4472 }),
4473 );
4474 tree.shortcut_registry_mut().register(
4475 Shortcut::new("editor.find")
4476 .primary(KeyStroke::command(Key::F))
4477 .scope(ShortcutScope::Scoped(scope_root))
4478 .build(),
4479 );
4480
4481 tree.layout(SizeProposal::exact(200.0, 100.0));
4482
4483 tree.focus(outside);
4485 tree.press_key(Key::F, Modifiers::COMMAND);
4486 assert_eq!(
4487 fired.get(),
4488 0,
4489 "scoped shortcut must not fire outside scope"
4490 );
4491
4492 tree.focus(inside);
4494 tree.press_key(Key::F, Modifiers::COMMAND);
4495 assert_eq!(
4496 fired.get(),
4497 1,
4498 "scoped shortcut must fire when focus in scope"
4499 );
4500 }
4501
4502 #[test]
4503 fn same_chord_scoped_first_falls_back_to_global_when_focus_outside() {
4504 use crate::action::Action;
4510 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
4511 use std::cell::Cell;
4512 use std::rc::Rc;
4513
4514 let scoped_fired = Rc::new(Cell::new(0));
4515 let global_fired = Rc::new(Cell::new(0));
4516 let sf = scoped_fired.clone();
4517 let gf = global_fired.clone();
4518
4519 let mut tree = WidgetTree::new();
4520 let root = tree.add(FillWidget::new());
4521 let editor = tree.add_child(root, FillWidget::new().focusable());
4522 let _editor_inner = tree.add_child(editor, FillWidget::new().focusable());
4523 let sidebar = tree.add_child(root, FillWidget::new().focusable());
4524
4525 tree.push_action(
4526 editor,
4527 Action::new("editor.saveBlock").on_invoke(move |_i, _c| sf.set(sf.get() + 1)),
4528 );
4529 tree.push_action(
4530 root,
4531 Action::new("zzz.global.save").on_invoke(move |_i, _c| gf.set(gf.get() + 1)),
4532 );
4533 tree.shortcut_registry_mut().register(
4534 Shortcut::new("editor.saveBlock")
4535 .primary(KeyStroke::command(Key::S))
4536 .scope(ShortcutScope::Scoped(editor))
4537 .build(),
4538 );
4539 tree.shortcut_registry_mut().register(
4540 Shortcut::new("zzz.global.save")
4541 .primary(KeyStroke::command(Key::S))
4542 .build(),
4543 );
4544
4545 tree.layout(SizeProposal::exact(200.0, 100.0));
4546 tree.focus(sidebar);
4547 tree.press_key(Key::S, Modifiers::COMMAND);
4548
4549 assert_eq!(global_fired.get(), 1, "applicable global must fire");
4550 assert_eq!(
4551 scoped_fired.get(),
4552 0,
4553 "inapplicable scoped binding must not eat the chord"
4554 );
4555 }
4556
4557 #[test]
4558 fn same_chord_global_first_yields_to_scoped_when_focus_inside() {
4559 use crate::action::Action;
4564 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
4565 use std::cell::Cell;
4566 use std::rc::Rc;
4567
4568 let scoped_fired = Rc::new(Cell::new(0));
4569 let global_fired = Rc::new(Cell::new(0));
4570 let sf = scoped_fired.clone();
4571 let gf = global_fired.clone();
4572
4573 let mut tree = WidgetTree::new();
4574 let root = tree.add(FillWidget::new());
4575 let editor = tree.add_child(root, FillWidget::new().focusable());
4576 let editor_inner = tree.add_child(editor, FillWidget::new().focusable());
4577 let sidebar = tree.add_child(root, FillWidget::new().focusable());
4578
4579 tree.push_action(
4580 editor,
4581 Action::new("editor.saveBlock").on_invoke(move |_i, _c| sf.set(sf.get() + 1)),
4582 );
4583 tree.push_action(
4584 root,
4585 Action::new("app.save").on_invoke(move |_i, _c| gf.set(gf.get() + 1)),
4586 );
4587 tree.shortcut_registry_mut().register(
4588 Shortcut::new("app.save")
4589 .primary(KeyStroke::command(Key::S))
4590 .build(),
4591 );
4592 tree.shortcut_registry_mut().register(
4593 Shortcut::new("editor.saveBlock")
4594 .primary(KeyStroke::command(Key::S))
4595 .scope(ShortcutScope::Scoped(editor))
4596 .build(),
4597 );
4598
4599 tree.layout(SizeProposal::exact(200.0, 100.0));
4600
4601 tree.focus(editor_inner);
4603 tree.press_key(Key::S, Modifiers::COMMAND);
4604 assert_eq!(
4605 scoped_fired.get(),
4606 1,
4607 "in-focus scoped must win over global"
4608 );
4609 assert_eq!(
4610 global_fired.get(),
4611 0,
4612 "global must yield to the scoped binding"
4613 );
4614
4615 tree.focus(sidebar);
4617 tree.press_key(Key::S, Modifiers::COMMAND);
4618 assert_eq!(scoped_fired.get(), 1, "scoped stays put outside its scope");
4619 assert_eq!(
4620 global_fired.get(),
4621 1,
4622 "global fires when focus leaves the scope"
4623 );
4624 }
4625
4626 #[test]
4627 fn propagated_action_lets_ancestor_handle() {
4628 use crate::action::Action;
4629 use crate::intent::IntentResponse;
4630 use crate::shortcut::{KeyStroke, Shortcut};
4631 use std::cell::Cell;
4632 use std::rc::Rc;
4633
4634 let inner_seen = Rc::new(Cell::new(false));
4635 let outer_seen = Rc::new(Cell::new(false));
4636 let inner_flag = inner_seen.clone();
4637 let outer_flag = outer_seen.clone();
4638
4639 let mut tree = WidgetTree::new();
4640 let outer = tree.add(FillWidget::new().focusable());
4641 let inner = tree.add_child(outer, FillWidget::new().focusable());
4642
4643 tree.push_action(
4645 inner,
4646 Action::new("app.save").on_invoke_with_response(move |_i, _c| {
4647 inner_flag.set(true);
4648 IntentResponse::Propagated
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(), "inner action observed the intent");
4668 assert!(outer_seen.get(), "outer action reached after Propagated");
4669 }
4670
4671 #[test]
4672 fn handled_action_stops_propagation() {
4673 use crate::action::Action;
4674 use crate::shortcut::{KeyStroke, Shortcut};
4675 use std::cell::Cell;
4676 use std::rc::Rc;
4677
4678 let inner_seen = Rc::new(Cell::new(false));
4679 let outer_seen = Rc::new(Cell::new(false));
4680 let inner_flag = inner_seen.clone();
4681 let outer_flag = outer_seen.clone();
4682
4683 let mut tree = WidgetTree::new();
4684 let outer = tree.add(FillWidget::new().focusable());
4685 let inner = tree.add_child(outer, FillWidget::new().focusable());
4686
4687 tree.push_action(
4688 inner,
4689 Action::new("app.save").on_invoke(move |_i, _c| {
4690 inner_flag.set(true);
4691 }),
4692 );
4693 tree.push_action(
4694 outer,
4695 Action::new("app.save").on_invoke(move |_i, _c| {
4696 outer_flag.set(true);
4697 }),
4698 );
4699 tree.shortcut_registry_mut().register(
4700 Shortcut::new("app.save")
4701 .primary(KeyStroke::command(Key::S))
4702 .build(),
4703 );
4704
4705 tree.layout(SizeProposal::exact(100.0, 50.0));
4706 tree.focus(inner);
4707
4708 tree.press_key(Key::S, Modifiers::COMMAND);
4709 assert!(inner_seen.get());
4710 assert!(!outer_seen.get(), "Handled at inner must stop propagation");
4711 }
4712
4713 #[test]
4714 fn disabled_action_propagates_by_default() {
4715 use crate::action::Action;
4716 use crate::shortcut::{KeyStroke, Shortcut};
4717 use crate::signal::Signal;
4718 use std::cell::Cell;
4719 use std::rc::Rc;
4720
4721 let inner_seen = Rc::new(Cell::new(false));
4722 let outer_seen = Rc::new(Cell::new(false));
4723 let inner_flag = inner_seen.clone();
4724 let outer_flag = outer_seen.clone();
4725
4726 let mut tree = WidgetTree::new();
4727 let outer = tree.add(FillWidget::new().focusable());
4728 let inner = tree.add_child(outer, FillWidget::new().focusable());
4729
4730 let enabled = Signal::new(false);
4731 tree.push_action(
4732 inner,
4733 Action::new("app.save")
4734 .enabled_when(enabled.clone())
4735 .on_invoke(move |_i, _c| {
4736 inner_flag.set(true);
4737 }),
4738 );
4739 tree.push_action(
4740 outer,
4741 Action::new("app.save").on_invoke(move |_i, _c| {
4742 outer_flag.set(true);
4743 }),
4744 );
4745 tree.shortcut_registry_mut().register(
4746 Shortcut::new("app.save")
4747 .primary(KeyStroke::command(Key::S))
4748 .build(),
4749 );
4750
4751 tree.layout(SizeProposal::exact(100.0, 50.0));
4752 tree.focus(inner);
4753
4754 tree.press_key(Key::S, Modifiers::COMMAND);
4755 assert!(!inner_seen.get(), "disabled inner must not run");
4756 assert!(
4757 outer_seen.get(),
4758 "intent must propagate past disabled inner"
4759 );
4760 }
4761
4762 #[test]
4763 fn disabled_action_with_non_propagating_shortcut_consumes() {
4764 use crate::action::Action;
4765 use crate::shortcut::{KeyStroke, Shortcut};
4766 use crate::signal::Signal;
4767 use std::cell::Cell;
4768 use std::rc::Rc;
4769
4770 let inner_seen = Rc::new(Cell::new(false));
4771 let outer_seen = Rc::new(Cell::new(false));
4772 let inner_flag = inner_seen.clone();
4773 let outer_flag = outer_seen.clone();
4774
4775 let mut tree = WidgetTree::new();
4776 let outer = tree.add(FillWidget::new().focusable());
4777 let inner = tree.add_child(outer, FillWidget::new().focusable());
4778
4779 let enabled = Signal::new(false);
4780 tree.push_action(
4781 inner,
4782 Action::new("app.save")
4783 .enabled_when(enabled.clone())
4784 .on_invoke(move |_i, _c| {
4785 inner_flag.set(true);
4786 }),
4787 );
4788 tree.push_action(
4789 outer,
4790 Action::new("app.save").on_invoke(move |_i, _c| {
4791 outer_flag.set(true);
4792 }),
4793 );
4794 tree.shortcut_registry_mut().register(
4795 Shortcut::new("app.save")
4796 .primary(KeyStroke::command(Key::S))
4797 .propagate_when_disabled(false)
4798 .build(),
4799 );
4800
4801 tree.layout(SizeProposal::exact(100.0, 50.0));
4802 tree.focus(inner);
4803
4804 tree.press_key(Key::S, Modifiers::COMMAND);
4805 assert!(!inner_seen.get(), "disabled inner still does not run");
4806 assert!(
4807 !outer_seen.get(),
4808 "intent must NOT propagate when shortcut disallows it"
4809 );
4810 }
4811
4812 #[test]
4813 fn send_intent_from_handler_reaches_ancestor_action() {
4814 use crate::action::Action;
4815 use crate::intent::Intent;
4816 use std::cell::Cell;
4817 use std::rc::Rc;
4818
4819 let save_seen = Rc::new(Cell::new(false));
4820 let save_flag = save_seen.clone();
4821
4822 let mut tree = WidgetTree::new();
4823 let root = tree.add(FillWidget::new());
4824 let button = tree.add_child(
4825 root,
4826 FillWidget::new().on_tap(|_pos, ctx| {
4827 ctx.send_intent(Intent::new("app.save"));
4828 }),
4829 );
4830 tree.push_action(
4831 root,
4832 Action::new("app.save").on_invoke(move |_i, _c| {
4833 save_flag.set(true);
4834 }),
4835 );
4836
4837 tree.layout(SizeProposal::exact(100.0, 50.0));
4838 tree.click(button);
4839 assert!(
4840 save_seen.get(),
4841 "ctx.send_intent must reach ancestor action"
4842 );
4843 }
4844
4845 #[test]
4846 fn widget_type_histogram_counts_distinct_types() {
4847 let mut tree = WidgetTree::new();
4852 let _ = tree.add(FillWidget::new());
4853 let _ = tree.add(FillWidget::new());
4854 let _ = tree.add(FillWidget::new());
4855 tree.layout(SizeProposal::exact(100.0, 100.0));
4856 let histogram = tree.widget_type_histogram();
4857 let total: u32 = histogram.values().sum();
4858 assert!(
4859 total >= 3,
4860 "expected at least 3 active widgets, got {total}: {histogram:?}"
4861 );
4862 let fillwidget_entries: u32 = histogram
4863 .iter()
4864 .filter(|(k, _)| k.contains("FillWidget"))
4865 .map(|(_, v)| *v)
4866 .sum();
4867 assert!(
4868 fillwidget_entries >= 3,
4869 "expected ≥3 FillWidget instances; histogram = {histogram:?}"
4870 );
4871 assert_eq!(tree.active_widget_count() as u32, total);
4872 }
4873
4874 #[test]
4875 fn intent_source_tagged_handler_for_tap_activation() {
4876 use crate::action::Action;
4880 use crate::intent::Intent;
4881 use crate::telemetry::IntentSource;
4882 use std::cell::Cell;
4883 use std::rc::Rc;
4884 let captured = Rc::new(Cell::new(IntentSource::Unknown));
4885 let captured_for_action = captured.clone();
4886
4887 let mut tree = WidgetTree::new();
4888 let root = tree.add(FillWidget::new());
4889 let button = tree.add_child(
4890 root,
4891 FillWidget::new().on_tap(|_pos, ctx| {
4892 ctx.send_intent(Intent::new("app.save"));
4893 }),
4894 );
4895 tree.push_action(
4896 root,
4897 Action::new("app.save").on_invoke(move |intent, _c| {
4898 captured_for_action.set(intent.source);
4899 }),
4900 );
4901
4902 tree.layout(SizeProposal::exact(100.0, 50.0));
4903 tree.click(button);
4904 assert_eq!(
4905 captured.get(),
4906 IntentSource::Handler,
4907 "tap-driven intent must tag IntentSource::Handler"
4908 );
4909 }
4910
4911 #[test]
4912 fn intent_source_programmatic_when_no_handler_active() {
4913 use crate::intent::Intent;
4914 use crate::telemetry::IntentSource;
4915 let intent = Intent::new("app.demo");
4916 assert_eq!(intent.source, IntentSource::Programmatic);
4917
4918 let mut ctx = EventContext::new();
4920 ctx.send_intent(Intent::new("app.demo"));
4921 let queued = ctx.pending_intents.first().expect("intent queued");
4922 assert_eq!(queued.source, IntentSource::Programmatic);
4923 }
4924
4925 #[test]
4926 fn with_intent_source_overrides_for_managed_widgets() {
4927 use crate::intent::Intent;
4928 use crate::telemetry::IntentSource;
4929 let mut ctx = EventContext::new();
4930 ctx.with_intent_source(IntentSource::Menu, |ctx| {
4931 ctx.send_intent(Intent::new("app.demo"));
4932 });
4933 let queued = ctx.pending_intents.first().expect("intent queued");
4934 assert_eq!(
4935 queued.source,
4936 IntentSource::Menu,
4937 "with_intent_source(Menu) must tag the dispatched intent"
4938 );
4939
4940 ctx.send_intent(Intent::new("app.next"));
4944 let next = ctx.pending_intents.last().expect("second intent");
4945 assert_eq!(next.source, IntentSource::Programmatic);
4946 }
4947
4948 #[test]
4949 fn disabled_shortcut_falls_through_to_focused_widget() {
4950 use crate::action::Action;
4951 use crate::shortcut::{KeyStroke, Shortcut};
4952 use crate::signal::Signal;
4953 use std::cell::Cell;
4954 use std::rc::Rc;
4955
4956 let action_fired = Rc::new(Cell::new(false));
4957 let on_key_fired = Rc::new(Cell::new(false));
4958 let af = action_fired.clone();
4959 let kf = on_key_fired.clone();
4960
4961 let enabled = Signal::new(false);
4962
4963 let mut tree = WidgetTree::new();
4964 let widget = tree.add(FillWidget::new().focusable().on_key(move |event, _ctx| {
4965 if matches!(
4966 event,
4967 WidgetEvent::KeyDown {
4968 key: Key::S,
4969 modifiers,
4970 ..
4971 } if modifiers.command()
4972 ) {
4973 kf.set(true);
4974 return EventResponse::Handled;
4975 }
4976 EventResponse::Ignored
4977 }));
4978 tree.push_action(
4979 widget,
4980 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
4981 );
4982 tree.shortcut_registry_mut().register(
4983 Shortcut::new("app.save")
4984 .primary(KeyStroke::command(Key::S))
4985 .enabled_when(enabled.clone())
4986 .build(),
4987 );
4988
4989 tree.layout(SizeProposal::exact(100.0, 50.0));
4990 tree.focus(widget);
4991
4992 tree.press_key(Key::S, Modifiers::COMMAND);
4994 assert!(
4995 !action_fired.get(),
4996 "disabled shortcut must not invoke its action"
4997 );
4998 assert!(
4999 on_key_fired.get(),
5000 "disabled shortcut must let KeyDown reach the focused widget"
5001 );
5002
5003 on_key_fired.set(false);
5005 enabled.set(true);
5006 tree.press_key(Key::S, Modifiers::COMMAND);
5007 assert!(action_fired.get(), "re-enabled shortcut must dispatch");
5008 assert!(
5009 !on_key_fired.get(),
5010 "enabled shortcut must consume the KeyDown"
5011 );
5012 }
5013
5014 #[test]
5015 fn keyboard_capture_bypasses_shortcut() {
5016 use crate::action::Action;
5017 use crate::shortcut::{KeyStroke, Shortcut};
5018 use std::cell::Cell;
5019 use std::rc::Rc;
5020
5021 fn run(capture: bool) -> (bool, bool) {
5027 let action_fired = Rc::new(Cell::new(false));
5028 let on_key_fired = Rc::new(Cell::new(false));
5029 let af = action_fired.clone();
5030 let kf = on_key_fired.clone();
5031
5032 let mut tree = WidgetTree::new();
5033 let widget = tree.add(
5034 FillWidget::new()
5035 .focusable()
5036 .keyboard_capture(capture)
5037 .on_key(move |event, _ctx| {
5038 if matches!(
5039 event,
5040 WidgetEvent::KeyDown { key: Key::S, modifiers, .. } if modifiers.command()
5041 ) {
5042 kf.set(true);
5043 return EventResponse::Handled;
5044 }
5045 EventResponse::Ignored
5046 }),
5047 );
5048 tree.push_action(
5049 widget,
5050 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
5051 );
5052 tree.shortcut_registry_mut().register(
5053 Shortcut::new("app.save")
5054 .primary(KeyStroke::command(Key::S))
5055 .build(),
5056 );
5057
5058 tree.layout(SizeProposal::exact(100.0, 50.0));
5059 tree.focus(widget);
5060 tree.press_key(Key::S, Modifiers::COMMAND);
5061 (action_fired.get(), on_key_fired.get())
5062 }
5063
5064 let (action, on_key) = run(true);
5066 assert!(
5067 !action,
5068 "keyboard_capture must suppress the shortcut action"
5069 );
5070 assert!(on_key, "keyboard_capture must deliver the raw KeyDown");
5071
5072 let (action, on_key) = run(false);
5074 assert!(action, "without capture the shortcut must fire");
5075 assert!(!on_key, "without capture the widget must not see the key");
5076 }
5077
5078 #[test]
5079 fn ctrl_tab_always_escapes_a_keyboard_capture_surface() {
5080 use std::cell::Cell;
5081 use std::rc::Rc;
5082
5083 let saw_key = Rc::new(Cell::new(false));
5089 let sk = saw_key.clone();
5090
5091 let mut tree = WidgetTree::new();
5092 let capture = tree.add(
5093 FillWidget::new()
5094 .focusable()
5095 .keyboard_capture(true)
5096 .on_key(move |_event, _ctx| {
5098 sk.set(true);
5099 EventResponse::Handled
5100 }),
5101 );
5102 let neighbour = tree.add(FillWidget::new().focusable());
5103 tree.layout(SizeProposal::exact(100.0, 50.0));
5104
5105 tree.focus(capture);
5108 tree.press_key(Key::Tab, Modifiers::NONE);
5109 assert!(saw_key.get(), "plain Tab must reach the capture surface");
5110 assert_eq!(
5111 tree.focused(),
5112 Some(capture),
5113 "plain Tab must not move focus off a capture surface"
5114 );
5115
5116 saw_key.set(false);
5118 tree.press_key(Key::Tab, Modifiers::CTRL);
5119 assert!(
5120 !saw_key.get(),
5121 "Ctrl+Tab is reserved and must not reach the capture surface"
5122 );
5123 assert_eq!(
5124 tree.focused(),
5125 Some(neighbour),
5126 "Ctrl+Tab must move focus out of a capture surface"
5127 );
5128
5129 tree.focus(capture);
5131 tree.press_key(Key::Tab, Modifiers::CTRL | Modifiers::SHIFT);
5132 assert_eq!(
5133 tree.focused(),
5134 Some(neighbour),
5135 "Ctrl+Shift+Tab must move focus out of a capture surface"
5136 );
5137 }
5138
5139 #[test]
5140 fn scope_mismatch_does_not_invoke_on_activate() {
5141 use crate::intent::Intent;
5142 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
5143 use std::cell::Cell;
5144 use std::rc::Rc;
5145
5146 let activated = Rc::new(Cell::new(false));
5152 let activated_flag = activated.clone();
5153
5154 let mut tree = WidgetTree::new();
5155 let scope_root = tree.add(FillWidget::new().focusable());
5156 let outside = tree.add(FillWidget::new().focusable());
5157
5158 tree.shortcut_registry_mut().register(
5159 Shortcut::new("editor.find")
5160 .primary(KeyStroke::command(Key::F))
5161 .scope(ShortcutScope::Scoped(scope_root))
5162 .on_activate(move |_ks, _ctx| {
5163 activated_flag.set(true);
5164 Intent::new("editor.find")
5165 })
5166 .build(),
5167 );
5168
5169 tree.layout(SizeProposal::exact(200.0, 100.0));
5170 tree.focus(outside);
5171
5172 tree.press_key(Key::F, Modifiers::COMMAND);
5173 assert!(
5174 !activated.get(),
5175 "on_activate must not run when focus is outside the shortcut's scope"
5176 );
5177 }
5178
5179 #[test]
5180 fn key_capture_runs_callback_and_bypasses_registry() {
5181 use crate::action::Action;
5182 use crate::shortcut::{KeyStroke, Shortcut};
5183 use std::cell::Cell;
5184 use std::rc::Rc;
5185
5186 let action_fired = Rc::new(Cell::new(false));
5187 let af = action_fired.clone();
5188 let captured = Rc::new(Cell::new(None));
5189 let cf = captured.clone();
5190
5191 let mut tree = WidgetTree::new();
5192 let widget = tree.add(FillWidget::new().focusable());
5193 tree.push_action(
5194 widget,
5195 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
5196 );
5197 tree.shortcut_registry_mut().register(
5198 Shortcut::new("app.save")
5199 .primary(KeyStroke::command(Key::S))
5200 .build(),
5201 );
5202
5203 tree.layout(SizeProposal::exact(100.0, 50.0));
5204 tree.focus(widget);
5205
5206 let handle = tree.begin_key_capture(move |ks, _reg, _ctx| cf.set(Some(ks)));
5207 assert!(tree.is_capturing_keys());
5208
5209 tree.press_key(Key::S, Modifiers::COMMAND);
5210 assert_eq!(
5211 captured.get(),
5212 Some(KeyStroke::command(Key::S)),
5213 "capture callback must receive the chord"
5214 );
5215 assert!(
5216 !action_fired.get(),
5217 "shortcut action must not fire while capture is armed"
5218 );
5219 assert!(
5220 !tree.is_capturing_keys(),
5221 "capture is one-shot; next KeyDown flows normally"
5222 );
5223 drop(handle);
5224 }
5225
5226 #[test]
5227 fn key_capture_can_rebind_through_registry() {
5228 use crate::shortcut::{KeyStroke, Shortcut};
5229
5230 let mut tree = WidgetTree::new();
5231 let widget = tree.add(FillWidget::new().focusable());
5232 tree.shortcut_registry_mut().register(
5233 Shortcut::new("app.save")
5234 .primary(KeyStroke::command(Key::S))
5235 .build(),
5236 );
5237
5238 tree.layout(SizeProposal::exact(100.0, 50.0));
5239 tree.focus(widget);
5240
5241 let _h = tree.begin_key_capture(|ks, reg, _ctx| {
5243 reg.rebind_primary("app.save", Some(ks));
5244 });
5245
5246 tree.press_key(Key::B, Modifiers::COMMAND | Modifiers::SHIFT);
5247 assert_eq!(
5248 tree.shortcut_registry()
5249 .effective("app.save")
5250 .unwrap()
5251 .primary,
5252 Some(KeyStroke::command_shift(Key::B))
5253 );
5254 }
5255
5256 #[test]
5257 fn dropping_capture_handle_cancels_capture() {
5258 use crate::shortcut::{KeyStroke, Shortcut};
5259 use std::cell::Cell;
5260 use std::rc::Rc;
5261
5262 let action_fired = Rc::new(Cell::new(false));
5263 let af = action_fired.clone();
5264 let capture_fired = Rc::new(Cell::new(false));
5265 let cf = capture_fired.clone();
5266
5267 let mut tree = WidgetTree::new();
5268 let widget = tree.add(FillWidget::new().focusable());
5269 tree.push_action(
5270 widget,
5271 crate::action::Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
5272 );
5273 tree.shortcut_registry_mut().register(
5274 Shortcut::new("app.save")
5275 .primary(KeyStroke::command(Key::S))
5276 .build(),
5277 );
5278 tree.layout(SizeProposal::exact(100.0, 50.0));
5279 tree.focus(widget);
5280
5281 {
5285 let _h = tree.begin_key_capture(move |_ks, _reg, _ctx| cf.set(true));
5286 assert!(tree.is_capturing_keys());
5287 }
5289 assert!(
5290 !tree.is_capturing_keys(),
5291 "dropping the handle must cancel the capture"
5292 );
5293
5294 tree.press_key(Key::S, Modifiers::COMMAND);
5295 assert!(!capture_fired.get(), "cancelled capture must not fire");
5296 assert!(
5297 action_fired.get(),
5298 "shortcut action runs after capture was cancelled"
5299 );
5300 }
5301
5302 #[test]
5303 fn second_begin_key_capture_does_not_racecancel_first() {
5304 use std::cell::Cell;
5305 use std::rc::Rc;
5306
5307 let first = Rc::new(Cell::new(false));
5308 let second = Rc::new(Cell::new(false));
5309 let f = first.clone();
5310 let s = second.clone();
5311
5312 let mut tree = WidgetTree::new();
5313 let widget = tree.add(FillWidget::new().focusable());
5314 tree.layout(SizeProposal::exact(100.0, 50.0));
5315 tree.focus(widget);
5316
5317 let h1 = tree.begin_key_capture(move |_ks, _reg, _ctx| f.set(true));
5323 let _h2 = tree.begin_key_capture(move |_ks, _reg, _ctx| s.set(true));
5324 drop(h1);
5325
5326 assert!(
5327 tree.is_capturing_keys(),
5328 "dropping the older handle must not cancel the active capture"
5329 );
5330 tree.press_key(Key::K, Modifiers::COMMAND);
5331 assert!(!first.get());
5332 assert!(second.get(), "newest capture wins");
5333 }
5334
5335 #[test]
5336 fn capture_callback_can_send_intent() {
5337 use crate::action::Action;
5338 use crate::intent::Intent;
5339
5340 use std::cell::Cell;
5341 use std::rc::Rc;
5342
5343 let ran = Rc::new(Cell::new(false));
5344 let flag = ran.clone();
5345
5346 let mut tree = WidgetTree::new();
5347 let widget = tree.add(FillWidget::new().focusable());
5348 tree.push_action(
5349 widget,
5350 Action::new("app.save").on_invoke(move |_i, _c| flag.set(true)),
5351 );
5352 tree.layout(SizeProposal::exact(100.0, 50.0));
5353 tree.focus(widget);
5354
5355 let _h = tree.begin_key_capture(|_ks, _reg, ctx| {
5356 ctx.send_intent(Intent::new("app.save"));
5357 });
5358 tree.press_key(Key::X, Modifiers::COMMAND);
5359 assert!(
5360 ran.get(),
5361 "intent queued from capture callback must dispatch"
5362 );
5363 }
5364
5365 #[test]
5366 fn binding_registry_does_not_accumulate_across_rebuilds() {
5367 use crate::binding::BindingLevel;
5368 use crate::signal::Signal;
5369
5370 #[derive(Debug)]
5371 struct BoundLeaf {
5372 tick: Signal<u64>,
5373 }
5374 impl crate::widget::Widget for BoundLeaf {
5375 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
5376 self.tick.bind_to(
5377 ctx.self_id(),
5378 ctx.binding_registry(),
5379 BindingLevel::Relayout,
5380 );
5381 Vec::new()
5382 }
5383 fn layout_response(
5384 &self,
5385 proposal: SizeProposal,
5386 _ctx: &crate::widget::LayoutContext,
5387 ) -> crate::widget::LayoutResponse {
5388 proposal.resolve(10.0, 10.0).into()
5389 }
5390 }
5391
5392 let mut tree = WidgetTree::new();
5393 let tick = Signal::new(0_u64);
5394 let widget = tree.add(BoundLeaf { tick: tick.clone() });
5395 tree.layout(SizeProposal::exact(200.0, 200.0));
5396 let after_first_build = tree.binding_registry().len();
5397 assert!(after_first_build >= 1);
5398
5399 for _ in 0..5 {
5403 tree.arena.mark_needs_rebuild(widget);
5404 tree.layout(SizeProposal::exact(200.0, 200.0));
5405 }
5406 assert_eq!(
5407 tree.binding_registry().len(),
5408 after_first_build,
5409 "bindings must be cleared on rebuild"
5410 );
5411
5412 tree.destroy_subtree(widget);
5413 assert_eq!(
5414 tree.binding_registry().len(),
5415 0,
5416 "bindings must be cleared on destroy"
5417 );
5418 let _ = tick;
5420 }
5421
5422 #[test]
5423 fn ctx_destroy_cancels_animations_and_bindings_via_deferred_path() {
5424 use crate::binding::BindingLevel;
5432 use crate::signal::Signal;
5433 use std::time::{Duration, Instant};
5434 use teksilo_tokens::Easing;
5435
5436 #[derive(Debug)]
5437 struct BoundLeaf {
5438 tick: Signal<u64>,
5439 }
5440 impl crate::widget::Widget for BoundLeaf {
5441 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
5442 self.tick.bind_to(
5443 ctx.self_id(),
5444 ctx.binding_registry(),
5445 BindingLevel::Relayout,
5446 );
5447 Vec::new()
5448 }
5449 fn layout_response(
5450 &self,
5451 proposal: SizeProposal,
5452 _ctx: &crate::widget::LayoutContext,
5453 ) -> crate::widget::LayoutResponse {
5454 proposal.resolve(10.0, 10.0).into()
5455 }
5456 }
5457
5458 let mut tree = WidgetTree::new();
5459 let widget = tree.add(BoundLeaf {
5460 tick: Signal::new(0_u64),
5461 });
5462 tree.layout(SizeProposal::exact(200.0, 200.0));
5463 assert!(tree.binding_registry().len() >= 1);
5464
5465 let anim = Signal::<f32>::new_animated(0.0);
5468 tree.animation_scheduler.animate(
5469 &anim,
5470 widget,
5471 1.0,
5472 Duration::from_secs(10),
5473 Easing::Linear,
5474 Instant::now(),
5475 );
5476 assert_eq!(tree.animation_scheduler.active_count(), 1);
5477
5478 let mut noop = crate::window::NoopWindowOps;
5480 tree.run_with_event_context(&mut noop, |ctx| ctx.destroy(widget));
5481
5482 assert_eq!(
5483 tree.animation_scheduler.active_count(),
5484 0,
5485 "ctx.destroy must cancel animations owned by the destroyed widget"
5486 );
5487 assert_eq!(
5488 tree.binding_registry().len(),
5489 0,
5490 "ctx.destroy must unregister the destroyed widget's bindings"
5491 );
5492 assert!(
5493 tree.arena.get(widget).is_none(),
5494 "node must be removed from the arena"
5495 );
5496 }
5497
5498 #[test]
5499 fn clear_shortcut_override_via_event_context_restores_default() {
5500 use crate::shortcut::{KeyStroke, Shortcut};
5501
5502 let mut tree = WidgetTree::new();
5503 tree.shortcut_registry_mut().register(
5504 Shortcut::new("app.save")
5505 .primary(KeyStroke::command(Key::S))
5506 .build(),
5507 );
5508 tree.shortcut_registry_mut()
5509 .rebind_primary("app.save", Some(KeyStroke::alt(Key::S)));
5510
5511 let source = tree.add(FillWidget::new());
5512 let mut ctx = EventContext::new();
5513 ctx.clear_shortcut_override("app.save");
5514 tree.collect_from_ctx(ctx, source);
5515
5516 assert_eq!(
5517 tree.shortcut_registry()
5518 .effective("app.save")
5519 .unwrap()
5520 .primary,
5521 Some(KeyStroke::command(Key::S))
5522 );
5523 }
5524
5525 #[test]
5526 fn rebind_shortcut_primary_via_event_context() {
5527 use crate::shortcut::{KeyStroke, Shortcut};
5528
5529 let mut tree = WidgetTree::new();
5530 tree.shortcut_registry_mut().register(
5531 Shortcut::new("app.save")
5532 .primary(KeyStroke::command(Key::S))
5533 .build(),
5534 );
5535 let source = tree.add(FillWidget::new());
5536
5537 let mut ctx = EventContext::new();
5538 ctx.rebind_shortcut_primary("app.save", Some(KeyStroke::alt(Key::S)));
5539 tree.collect_from_ctx(ctx, source);
5540
5541 assert_eq!(
5542 tree.shortcut_registry()
5543 .effective("app.save")
5544 .unwrap()
5545 .primary,
5546 Some(KeyStroke::alt(Key::S))
5547 );
5548 }
5549
5550 #[test]
5551 fn unregister_all_for_owner_called_on_destroy() {
5552 use crate::shortcut::{KeyStroke, Shortcut};
5553
5554 let mut tree = WidgetTree::new();
5555 let widget = tree.add(FillWidget::new());
5556 let widget_owner = widget;
5557 tree.shortcut_registry_mut().register_owned(
5558 Shortcut::new("scoped.thing")
5559 .primary(KeyStroke::command(Key::K))
5560 .build(),
5561 widget_owner,
5562 );
5563 assert!(
5564 tree.shortcut_registry()
5565 .get_default("scoped.thing")
5566 .is_some()
5567 );
5568
5569 tree.destroy_subtree(widget);
5570 assert!(
5571 tree.shortcut_registry()
5572 .get_default("scoped.thing")
5573 .is_none(),
5574 "destroying the owner must unregister its shortcut"
5575 );
5576 }
5577
5578 #[test]
5582 fn global_action_reached_from_unrelated_source() {
5583 use crate::action::Action;
5584 use crate::intent::Intent;
5585 use std::cell::Cell;
5586 use std::rc::Rc;
5587
5588 #[derive(Debug)]
5589 struct Registrar(Rc<Cell<bool>>);
5590 impl crate::widget::Widget for Registrar {
5591 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
5592 let flag = self.0.clone();
5593 ctx.register_action_global(
5594 Action::new("test.global").on_invoke(move |_i, _c| flag.set(true)),
5595 );
5596 vec![]
5597 }
5598 fn layout_response(
5599 &self,
5600 _p: teksilo_canvas::SizeProposal,
5601 _c: &crate::widget::LayoutContext,
5602 ) -> crate::widget::LayoutResponse {
5603 teksilo_canvas::Size::new(0.0, 0.0).into()
5604 }
5605 }
5606
5607 let mut tree = WidgetTree::new();
5608 let fired = Rc::new(Cell::new(false));
5609 let registrar = tree.add(Registrar(fired.clone()));
5610 let source = tree.add(FillWidget::new()); let mut ops = crate::window::NoopWindowOps;
5612
5613 tree.dispatch_intent(source, Intent::new("test.global"), true, &mut ops);
5614 assert!(
5615 fired.get(),
5616 "global action must fire from an unrelated source"
5617 );
5618
5619 fired.set(false);
5621 tree.destroy_subtree(registrar);
5622 tree.dispatch_intent(source, Intent::new("test.global"), true, &mut ops);
5623 assert!(
5624 !fired.get(),
5625 "destroying the owner must remove its global action"
5626 );
5627 }
5628
5629 #[test]
5641 fn hit_test_through_translate_scope() {
5642 use crate::test_widgets::StackWidget;
5643 let mut tree = WidgetTree::new();
5644 let child = tree.add(FillWidget::new());
5645 let parent = tree.add(StackWidget::new().child(child));
5646 tree.set_transform(parent, teksilo_canvas::Transform2D::translate(100.0, 0.0));
5648 tree.layout(SizeProposal::exact(100.0, 50.0));
5649
5650 assert_eq!(
5654 tree.hit_test(Point::new(50.0, 25.0)),
5655 None,
5656 "pre-transform area is not visually populated and must not hit"
5657 );
5658 assert_eq!(
5660 tree.hit_test(Point::new(150.0, 25.0)),
5661 Some(child),
5662 "visually-rendered area must hit the child"
5663 );
5664 assert_eq!(tree.hit_test(Point::new(250.0, 25.0)), None);
5666 }
5667
5668 #[test]
5669 fn hit_test_through_scale_scope() {
5670 use crate::test_widgets::StackWidget;
5671 let mut tree = WidgetTree::new();
5672 let child = tree.add(FillWidget::new());
5673 let parent = tree.add(StackWidget::new().child(child));
5674 tree.set_transform(parent, teksilo_canvas::Transform2D::scale(0.5, 0.5));
5677 tree.layout(SizeProposal::exact(100.0, 50.0));
5678
5679 assert_eq!(tree.hit_test(Point::new(25.0, 12.0)), Some(child));
5681 assert_eq!(
5684 tree.hit_test(Point::new(75.0, 25.0)),
5685 None,
5686 "scaled-out region must not hit"
5687 );
5688 }
5689
5690 #[test]
5691 fn hit_test_through_nested_transforms_compose() {
5692 use crate::test_widgets::StackWidget;
5693 let mut tree = WidgetTree::new();
5694 let leaf = tree.add(FillWidget::new());
5695 let inner = tree.add(StackWidget::new().child(leaf));
5696 let outer = tree.add(StackWidget::new().child(inner));
5697 tree.set_transform(outer, teksilo_canvas::Transform2D::translate(100.0, 0.0));
5701 tree.set_transform(inner, teksilo_canvas::Transform2D::scale(2.0, 2.0));
5702 tree.layout(SizeProposal::exact(50.0, 25.0));
5703
5704 assert_eq!(tree.hit_test(Point::new(150.0, 25.0)), Some(leaf));
5708 assert_eq!(tree.hit_test(Point::new(50.0, 25.0)), None);
5709 assert_eq!(tree.hit_test(Point::new(250.0, 25.0)), None);
5710 }
5711
5712 #[test]
5713 fn hit_test_identity_transform_unchanged() {
5714 let mut tree = WidgetTree::new();
5718 let widget = tree.add(FillWidget::new());
5719 tree.set_transform(widget, teksilo_canvas::Transform2D::IDENTITY);
5720 tree.layout(SizeProposal::exact(100.0, 50.0));
5721 assert_eq!(tree.hit_test(Point::new(50.0, 25.0)), Some(widget));
5722 }
5723
5724 #[test]
5725 fn arena_effective_transform_composes_ancestors() {
5726 use crate::test_widgets::StackWidget;
5737 let mut tree = WidgetTree::new();
5738 let leaf = tree.add(FillWidget::new());
5739 let inner = tree.add(StackWidget::new().child(leaf));
5740 let outer = tree.add(StackWidget::new().child(inner));
5741 tree.set_transform(outer, teksilo_canvas::Transform2D::translate(100.0, 0.0));
5742 tree.set_transform(inner, teksilo_canvas::Transform2D::scale(2.0, 2.0));
5743 tree.layout(SizeProposal::exact(50.0, 25.0));
5744
5745 let eff = tree.arena.effective_transform(leaf);
5746 let expected = teksilo_canvas::Transform2D::scale(2.0, 2.0)
5747 .then(&teksilo_canvas::Transform2D::translate(100.0, 0.0));
5748 for (a, b) in eff.m.iter().zip(expected.m.iter()) {
5749 assert!(
5750 (a - b).abs() < 1e-5,
5751 "effective_transform mismatch: got {:?}, want {:?}",
5752 eff.m,
5753 expected.m
5754 );
5755 }
5756
5757 let screen_origin = eff.apply_point(Point::new(0.0, 0.0));
5763 assert!((screen_origin.x - 100.0).abs() < 1e-5);
5764 assert!((screen_origin.y - 0.0).abs() < 1e-5);
5765 let screen_corner = eff.apply_point(Point::new(50.0, 25.0));
5768 assert!((screen_corner.x - 200.0).abs() < 1e-5);
5769 assert!((screen_corner.y - 50.0).abs() < 1e-5);
5770 }
5771
5772 #[derive(Debug)]
5777 struct StubMenu;
5778 impl crate::widget::Widget for StubMenu {
5779 fn layout_response(
5780 &self,
5781 _proposal: SizeProposal,
5782 _ctx: &crate::widget::LayoutContext,
5783 ) -> crate::widget::LayoutResponse {
5784 teksilo_canvas::Size::new(100.0, 40.0).into()
5785 }
5786 }
5787
5788 #[derive(Debug)]
5800 struct NominatingWidget {
5801 row: std::cell::Cell<Option<WidgetId>>,
5802 }
5803
5804 impl crate::widget::Widget for NominatingWidget {
5805 fn layout_response(
5806 &self,
5807 proposal: SizeProposal,
5808 _ctx: &LayoutContext,
5809 ) -> crate::widget::LayoutResponse {
5810 proposal.resolve(50.0, 20.0).into()
5811 }
5812
5813 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
5814 ctx.apply_self_handlers(crate::widget_builder::HandlerSet::new().focusable(true));
5815 Vec::new()
5816 }
5817
5818 fn context_menu_key_target(&self) -> Option<WidgetId> {
5819 self.row.get()
5820 }
5821 }
5822
5823 fn press(tree: &mut WidgetTree, key: Key, modifiers: Modifiers) {
5824 tree.dispatch_event(WidgetEvent::KeyDown {
5825 key,
5826 modifiers,
5827 text: None,
5828 });
5829 }
5830
5831 #[test]
5832 fn the_context_menu_key_opens_the_focused_widget_menu() {
5833 use std::cell::Cell;
5834 use std::rc::Rc;
5835
5836 let opened = Rc::new(Cell::new(false));
5837 let flag = opened.clone();
5838 let mut tree = WidgetTree::new();
5839 let widget = tree.add(
5840 FillWidget::new()
5841 .focusable()
5842 .context_menu(move |_pos, _ctx| {
5843 flag.set(true);
5844 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
5845 }),
5846 );
5847 tree.layout(SizeProposal::exact(200.0, 100.0));
5848 tree.focus(widget);
5849
5850 press(&mut tree, Key::ContextMenu, Modifiers::NONE);
5851 assert!(opened.get(), "the dedicated Menu key must open the menu");
5852 }
5853
5854 #[test]
5857 fn shift_f10_opens_the_focused_widget_menu() {
5858 use std::cell::Cell;
5859 use std::rc::Rc;
5860
5861 let opened = Rc::new(Cell::new(false));
5862 let flag = opened.clone();
5863 let mut tree = WidgetTree::new();
5864 let widget = tree.add(
5865 FillWidget::new()
5866 .focusable()
5867 .context_menu(move |_pos, _ctx| {
5868 flag.set(true);
5869 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
5870 }),
5871 );
5872 tree.layout(SizeProposal::exact(200.0, 100.0));
5873 tree.focus(widget);
5874
5875 press(&mut tree, Key::F10, Modifiers::SHIFT);
5876 assert!(opened.get(), "Shift+F10 must open the menu");
5877 }
5878
5879 #[test]
5882 fn a_near_miss_chord_is_not_a_context_menu_request() {
5883 use std::cell::Cell;
5884 use std::rc::Rc;
5885
5886 let opened = Rc::new(Cell::new(false));
5887 let flag = opened.clone();
5888 let mut tree = WidgetTree::new();
5889 let widget = tree.add(
5890 FillWidget::new()
5891 .focusable()
5892 .context_menu(move |_pos, _ctx| {
5893 flag.set(true);
5894 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
5895 }),
5896 );
5897 tree.layout(SizeProposal::exact(200.0, 100.0));
5898 tree.focus(widget);
5899
5900 press(&mut tree, Key::F10, Modifiers::SHIFT | Modifiers::CTRL);
5901 press(&mut tree, Key::F10, Modifiers::NONE);
5902 assert!(!opened.get(), "only Shift+F10 exactly asks for a menu");
5903 }
5904
5905 #[test]
5909 fn the_keyboard_target_can_be_a_row_rather_than_the_focused_container() {
5910 use std::cell::Cell;
5911 use std::rc::Rc;
5912
5913 let menu_owner = Rc::new(Cell::new(None::<&'static str>));
5914
5915 let row_flag = menu_owner.clone();
5916 let container_flag = menu_owner.clone();
5917
5918 let mut tree = WidgetTree::new();
5919 let row = tree.add(FillWidget::new().context_menu(move |_pos, _ctx| {
5920 row_flag.set(Some("row"));
5921 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
5922 }));
5923 let container = tree.add(
5924 crate::test_widgets::StackWidget::new()
5925 .child(row)
5926 .context_menu(move |_pos, _ctx| {
5927 container_flag.set(Some("container"));
5928 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
5929 }),
5930 );
5931 tree.layout(SizeProposal::exact(200.0, 100.0));
5932
5933 let nominator = tree.add(NominatingWidget {
5935 row: std::cell::Cell::new(Some(row)),
5936 });
5937 tree.layout(SizeProposal::exact(200.0, 100.0));
5938 tree.focus(nominator);
5939 let _ = container;
5940
5941 press(&mut tree, Key::ContextMenu, Modifiers::NONE);
5942 assert_eq!(
5943 menu_owner.get(),
5944 Some("row"),
5945 "the nominated row's factory must be the one that runs"
5946 );
5947 }
5948
5949 #[test]
5952 fn the_chord_falls_through_when_there_is_no_menu_to_show() {
5953 use std::cell::Cell;
5954 use std::rc::Rc;
5955
5956 let saw_key = Rc::new(Cell::new(false));
5957 let flag = saw_key.clone();
5958 let mut tree = WidgetTree::new();
5959 let widget = tree.add(FillWidget::new().focusable().on_key(move |ev, _ctx| {
5960 if matches!(ev, WidgetEvent::KeyDown { key: Key::F10, .. }) {
5961 flag.set(true);
5962 }
5963 crate::event::EventResponse::Ignored
5964 }));
5965 tree.layout(SizeProposal::exact(200.0, 100.0));
5966 tree.focus(widget);
5967
5968 press(&mut tree, Key::F10, Modifiers::SHIFT);
5969 assert!(
5970 saw_key.get(),
5971 "with no factory anywhere, the key must reach the widget"
5972 );
5973 }
5974
5975 #[test]
5981 fn each_route_to_a_menu_tells_the_factory_what_asked_for_it() {
5982 use crate::widget_builder::ContextMenuTrigger;
5983 use std::cell::RefCell;
5984 use std::rc::Rc;
5985
5986 let seen = Rc::new(RefCell::new(Vec::new()));
5987 let log = seen.clone();
5988 let mut tree = WidgetTree::new();
5989 let widget = tree.add(
5990 FillWidget::new()
5991 .focusable()
5992 .context_menu(move |_pos, ctx| {
5993 log.borrow_mut().push(ctx.context_menu_trigger());
5994 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
5995 }),
5996 );
5997 tree.layout(SizeProposal::exact(200.0, 100.0));
5998 tree.focus(widget);
5999 let inside = tree.bounds(widget).center();
6000
6001 press(&mut tree, Key::F10, Modifiers::SHIFT);
6002 press(&mut tree, Key::Escape, Modifiers::NONE);
6003 press(&mut tree, Key::ContextMenu, Modifiers::NONE);
6004 press(&mut tree, Key::Escape, Modifiers::NONE);
6005 tree.dispatch_event(WidgetEvent::pointer_down(
6006 inside,
6007 crate::event::PointerButton::Secondary,
6008 Modifiers::NONE,
6009 ));
6010 tree.dispatch_event(WidgetEvent::pointer_up(
6011 inside,
6012 crate::event::PointerButton::Secondary,
6013 Modifiers::NONE,
6014 ));
6015 press(&mut tree, Key::Escape, Modifiers::NONE);
6016 tree.dispatch_event(WidgetEvent::AccessAction {
6017 action: accesskit::Action::ShowContextMenu,
6018 target: Some(widget),
6019 target_node: crate::accessibility::widget_id_to_node_id(widget),
6020 data: None,
6021 });
6022 press(&mut tree, Key::Escape, Modifiers::NONE);
6023 tree.long_press_at(teksilo_tokens::PointerKind::Touch, inside);
6024
6025 assert_eq!(
6026 *seen.borrow(),
6027 vec![
6028 Some(ContextMenuTrigger::Keyboard),
6029 Some(ContextMenuTrigger::Keyboard),
6030 Some(ContextMenuTrigger::Pointer),
6031 Some(ContextMenuTrigger::Accessibility),
6032 Some(ContextMenuTrigger::Pointer),
6033 ],
6034 "Shift+F10, the Menu key, a right-click, the assistive action and a \
6035 hold, in that order"
6036 );
6037 }
6038
6039 #[test]
6040 fn context_menu_factory_receives_click_position() {
6041 use crate::event::{Modifiers, PointerButton};
6042 use std::cell::Cell;
6043 use std::rc::Rc;
6044
6045 let captured_position = Rc::new(Cell::new(None::<Point>));
6046 let cap = captured_position.clone();
6047 let mut tree = WidgetTree::new();
6048 let widget = tree.add(FillWidget::new().context_menu(move |pos, _ctx| {
6049 cap.set(Some(pos));
6050 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6051 }));
6052 tree.layout(SizeProposal::exact(200.0, 100.0));
6053
6054 let click = Point::new(73.0, 42.0);
6055 tree.dispatch_event(WidgetEvent::pointer_down(
6056 click,
6057 PointerButton::Secondary,
6058 Modifiers::NONE,
6059 ));
6060
6061 let got = captured_position.get();
6062 assert_eq!(
6063 got,
6064 Some(click),
6065 "factory must receive the click position; got {:?}",
6066 got
6067 );
6068 let _ = widget;
6069 }
6070
6071 #[test]
6072 fn context_menu_factory_returning_none_falls_through_to_parent() {
6073 use crate::event::{Modifiers, PointerButton};
6074 use crate::test_widgets::StackWidget;
6075 use std::cell::Cell;
6076 use std::rc::Rc;
6077
6078 let outer_called = Rc::new(Cell::new(0_u32));
6082 let outer_flag = outer_called.clone();
6083 let mut tree = WidgetTree::new();
6084 let inner = tree.add(FillWidget::new().context_menu(|_pos, _ctx| None));
6085 let _outer = tree.add(
6086 StackWidget::new()
6087 .child(inner)
6088 .context_menu(move |_pos, _ctx| {
6089 outer_flag.set(outer_flag.get() + 1);
6090 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6091 }),
6092 );
6093 tree.layout(SizeProposal::exact(200.0, 100.0));
6094
6095 tree.dispatch_event(WidgetEvent::pointer_down(
6096 Point::new(50.0, 25.0),
6097 PointerButton::Secondary,
6098 Modifiers::NONE,
6099 ));
6100
6101 assert_eq!(
6102 outer_called.get(),
6103 1,
6104 "inner returning None must fall through to the outer factory"
6105 );
6106 }
6107
6108 #[test]
6109 fn context_menu_factory_none_throughout_chain_does_not_show_overlay() {
6110 use crate::event::{Modifiers, PointerButton};
6111
6112 let mut tree = WidgetTree::new();
6114 tree.add(FillWidget::new().context_menu(|_pos, _ctx| None));
6115 tree.layout(SizeProposal::exact(200.0, 100.0));
6116
6117 let overlay_count_before = tree.overlay_manager.len();
6118 tree.dispatch_event(WidgetEvent::pointer_down(
6119 Point::new(50.0, 25.0),
6120 PointerButton::Secondary,
6121 Modifiers::NONE,
6122 ));
6123 let overlay_count_after = tree.overlay_manager.len();
6124 assert_eq!(
6125 overlay_count_before, overlay_count_after,
6126 "a factory returning None must not mount any overlay"
6127 );
6128 }
6129
6130 #[derive(Debug)]
6141 struct FreshChildHost {
6142 preserve: bool,
6143 }
6144 impl Widget for FreshChildHost {
6145 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6146 vec![ctx.add(FillWidget::new())]
6147 }
6148 fn layout_response(
6149 &self,
6150 p: SizeProposal,
6151 _c: &LayoutContext,
6152 ) -> crate::widget::LayoutResponse {
6153 p.resolve(10.0, 10.0).into()
6154 }
6155 fn preserves_children_on_rebuild(&self) -> bool {
6156 self.preserve
6157 }
6158 }
6159
6160 #[test]
6161 fn reconcile_reaps_dropped_children_no_leak() {
6162 for preserve in [false, true] {
6167 let mut tree = WidgetTree::new();
6168 let host = tree.add(FreshChildHost { preserve });
6169 tree.layout(SizeProposal::exact(100.0, 100.0));
6170 let total0 = tree.arena.len();
6171 let active0 = tree.active_widget_count();
6172 for _ in 0..5 {
6173 tree.arena_mark_needs_rebuild_for_testing(host);
6174 tree.layout(SizeProposal::exact(100.0, 100.0));
6175 }
6176 assert_eq!(
6177 tree.arena.len(),
6178 total0,
6179 "preserve={preserve}: dropped children must be reaped, not leaked"
6180 );
6181 assert_eq!(
6182 tree.active_widget_count(),
6183 active0,
6184 "preserve={preserve}: no stranded still-active orphans"
6185 );
6186 }
6187 }
6188
6189 #[derive(Debug)]
6195 struct DetachedContentHost {
6196 preserve: bool,
6197 }
6198 impl Widget for DetachedContentHost {
6199 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6200 let popup = ctx.add_detached(FillWidget::new());
6201 ctx.set_dormant(popup);
6202 vec![ctx.add(FillWidget::new())]
6203 }
6204 fn layout_response(
6205 &self,
6206 p: SizeProposal,
6207 _c: &LayoutContext,
6208 ) -> crate::widget::LayoutResponse {
6209 p.resolve(10.0, 10.0).into()
6210 }
6211 fn preserves_children_on_rebuild(&self) -> bool {
6212 self.preserve
6213 }
6214 }
6215
6216 #[test]
6217 fn rebuilding_reaps_detached_content_no_leak() {
6218 for preserve in [false, true] {
6224 let mut tree = WidgetTree::new();
6225 let host = tree.add(DetachedContentHost { preserve });
6226 tree.layout(SizeProposal::exact(100.0, 100.0));
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_eq!(
6233 tree.arena.len(),
6234 total0,
6235 "preserve={preserve}: the previous build's detached content must be reaped"
6236 );
6237 }
6238 }
6239
6240 #[test]
6241 fn destroying_a_host_reaps_its_detached_content() {
6242 let mut tree = WidgetTree::new();
6243 let outer = tree.add(FillWidget::new());
6244 tree.layout(SizeProposal::exact(100.0, 100.0));
6245 let empty = tree.arena.len();
6246
6247 let host = tree.add_child(outer, DetachedContentHost { preserve: false });
6248 tree.layout(SizeProposal::exact(100.0, 100.0));
6249 assert!(tree.arena.len() > empty);
6250
6251 tree.destroy_subtree(host);
6252 assert_eq!(
6253 tree.arena.len(),
6254 empty,
6255 "the popup must die with the widget that built it"
6256 );
6257 }
6258
6259 #[derive(Debug)]
6261 struct StableChildHost {
6262 child: Option<WidgetId>,
6263 probe: std::rc::Rc<std::cell::Cell<Option<WidgetId>>>,
6264 }
6265 impl Widget for StableChildHost {
6266 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6267 let id = match self.child {
6268 Some(id) => id,
6269 None => {
6270 let id = ctx.add(FillWidget::new());
6271 self.child = Some(id);
6272 self.probe.set(Some(id));
6273 id
6274 }
6275 };
6276 vec![id]
6277 }
6278 fn layout_response(
6279 &self,
6280 p: SizeProposal,
6281 _c: &LayoutContext,
6282 ) -> crate::widget::LayoutResponse {
6283 p.resolve(10.0, 10.0).into()
6284 }
6285 fn preserves_children_on_rebuild(&self) -> bool {
6286 true
6287 }
6288 }
6289
6290 #[test]
6291 fn reconcile_preserves_reattached_child() {
6292 let probe = std::rc::Rc::new(std::cell::Cell::new(None));
6293 let mut tree = WidgetTree::new();
6294 let host = tree.add(StableChildHost {
6295 child: None,
6296 probe: probe.clone(),
6297 });
6298 tree.layout(SizeProposal::exact(100.0, 100.0));
6299 let child = probe.get().expect("child mounted");
6300 let total0 = tree.arena.len();
6301 for _ in 0..5 {
6302 tree.arena_mark_needs_rebuild_for_testing(host);
6303 tree.layout(SizeProposal::exact(100.0, 100.0));
6304 }
6305 assert!(
6306 tree.arena.is_active(child),
6307 "the re-attached child must survive every rebuild"
6308 );
6309 assert_eq!(tree.arena.len(), total0, "no growth — same child reused");
6310 }
6311
6312 #[derive(Debug)]
6314 struct Wrapper {
6315 child: WidgetId,
6316 }
6317 impl Widget for Wrapper {
6318 fn build(&mut self, _ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6319 vec![self.child]
6320 }
6321 fn layout_response(
6322 &self,
6323 p: SizeProposal,
6324 _c: &LayoutContext,
6325 ) -> crate::widget::LayoutResponse {
6326 p.resolve(10.0, 10.0).into()
6327 }
6328 }
6329
6330 #[derive(Debug)]
6334 struct ReparentHost {
6335 body: Option<WidgetId>,
6336 probe: std::rc::Rc<std::cell::Cell<Option<WidgetId>>>,
6337 }
6338 impl Widget for ReparentHost {
6339 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6340 let body = match self.body {
6341 Some(id) => id,
6342 None => {
6343 let id = ctx.add(FillWidget::new());
6344 self.body = Some(id);
6345 self.probe.set(Some(id));
6346 id
6347 }
6348 };
6349 vec![ctx.add(Wrapper { child: body })]
6350 }
6351 fn layout_response(
6352 &self,
6353 p: SizeProposal,
6354 _c: &LayoutContext,
6355 ) -> crate::widget::LayoutResponse {
6356 p.resolve(10.0, 10.0).into()
6357 }
6358 fn preserves_children_on_rebuild(&self) -> bool {
6359 true
6360 }
6361 }
6362
6363 #[test]
6364 fn reconcile_spares_reparented_survivor() {
6365 let probe = std::rc::Rc::new(std::cell::Cell::new(None));
6372 let mut tree = WidgetTree::new();
6373 let host = tree.add(ReparentHost {
6374 body: None,
6375 probe: probe.clone(),
6376 });
6377 tree.layout(SizeProposal::exact(100.0, 100.0));
6378 let body = probe.get().expect("body mounted");
6379 let total0 = tree.arena.len();
6380 for _ in 0..5 {
6381 tree.arena_mark_needs_rebuild_for_testing(host);
6382 tree.layout(SizeProposal::exact(100.0, 100.0));
6383 }
6384 assert!(
6385 tree.arena.is_active(body),
6386 "the re-parented body must survive — it was moved into the new tree, \
6387 not swept with the dropped wrapper"
6388 );
6389 assert_eq!(
6390 tree.arena.len(),
6391 total0,
6392 "dropped wrappers reaped — no per-rebuild leak"
6393 );
6394 }
6395
6396 #[derive(Debug)]
6406 struct BelowContainer {
6407 child: Option<WidgetId>,
6408 offset: f32,
6409 }
6410
6411 impl crate::widget::Widget for BelowContainer {
6412 fn layout_response(
6413 &self,
6414 proposal: SizeProposal,
6415 _ctx: &crate::widget::LayoutContext,
6416 ) -> crate::widget::LayoutResponse {
6417 proposal.resolve(0.0, 0.0).into()
6418 }
6419 fn place_children(
6420 &self,
6421 bounds: Rect,
6422 _proposal: SizeProposal,
6423 children: &mut [crate::widget::WidgetPlacement],
6424 _ctx: &crate::widget::LayoutContext,
6425 ) {
6426 for c in children.iter_mut() {
6427 c.origin = Point::new(bounds.x, bounds.y + self.offset);
6428 c.size = bounds.size();
6429 }
6430 }
6431 fn children(&self) -> Vec<WidgetId> {
6432 self.child.into_iter().collect()
6433 }
6434 }
6435
6436 fn recording_scroll_container(
6439 tree: &mut WidgetTree,
6440 child: WidgetId,
6441 recorded: std::rc::Rc<std::cell::Cell<Option<Rect>>>,
6442 ) -> WidgetId {
6443 use crate::test_widgets::StackWidget;
6444 tree.add(
6445 StackWidget::new()
6446 .child(child)
6447 .on_scroll(move |ev, _ctx| match ev {
6448 WidgetEvent::ScrollIntoView { target_bounds, .. } => {
6449 recorded.set(Some(*target_bounds));
6450 EventResponse::Handled
6451 }
6452 _ => EventResponse::Ignored,
6453 })
6454 .clips_children(true),
6455 )
6456 }
6457
6458 #[test]
6459 fn ensure_visible_dispatches_scroll_into_view_to_clipping_ancestor() {
6460 use std::cell::Cell;
6461 use std::rc::Rc;
6462 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6463 let mut tree = WidgetTree::new();
6464 let actor = tree.add(FillWidget::new());
6465 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
6466 tree.layout(SizeProposal::exact(100.0, 100.0));
6467
6468 let target = Rect::new(10.0, 500.0, 20.0, 15.0);
6471 let mut ctx = EventContext::new();
6472 ctx.ensure_visible(target);
6473 tree.collect_from_ctx(ctx, actor);
6474
6475 assert_eq!(
6476 recorded.get(),
6477 Some(target),
6478 "ensure_visible(rect) must dispatch ScrollIntoView with the exact rect \
6479 to the clips_children ancestor"
6480 );
6481 }
6482
6483 #[test]
6484 fn ensure_visible_is_noop_when_rect_already_visible() {
6485 use std::cell::Cell;
6486 use std::rc::Rc;
6487 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6488 let mut tree = WidgetTree::new();
6489 let actor = tree.add(FillWidget::new());
6490 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
6491 tree.layout(SizeProposal::exact(100.0, 100.0));
6492
6493 let mut ctx = EventContext::new();
6496 ctx.ensure_visible(Rect::new(10.0, 10.0, 20.0, 15.0));
6497 tree.collect_from_ctx(ctx, actor);
6498
6499 assert_eq!(
6500 recorded.get(),
6501 None,
6502 "a rect already inside the viewport must not trigger a scroll"
6503 );
6504 }
6505
6506 #[test]
6507 fn ensure_visible_margin_forces_scroll_near_edge() {
6508 use std::cell::Cell;
6509 use std::rc::Rc;
6510 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6511 let mut tree = WidgetTree::new();
6512 let actor = tree.add(FillWidget::new());
6513 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
6514 tree.layout(SizeProposal::exact(100.0, 100.0));
6515
6516 let rect = Rect::new(10.0, 95.0, 20.0, 4.0);
6519 let mut ctx = EventContext::new();
6520 ctx.ensure_visible_with_margin(rect, 10.0);
6521 tree.collect_from_ctx(ctx, actor);
6522
6523 assert_eq!(
6524 recorded.get(),
6525 Some(rect),
6526 "the margin must widen the visibility test so a near-edge rect scrolls"
6527 );
6528 }
6529
6530 fn recording_align_container(
6533 tree: &mut WidgetTree,
6534 child: WidgetId,
6535 recorded: std::rc::Rc<
6536 std::cell::Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>,
6537 >,
6538 ) -> WidgetId {
6539 use crate::test_widgets::StackWidget;
6540 tree.add(
6541 StackWidget::new()
6542 .child(child)
6543 .on_scroll(move |ev, _ctx| match ev {
6544 WidgetEvent::ScrollIntoView { align, motion, .. } => {
6545 recorded.set(Some((*align, *motion)));
6546 EventResponse::Handled
6547 }
6548 _ => EventResponse::Ignored,
6549 })
6550 .clips_children(true),
6551 )
6552 }
6553
6554 #[test]
6555 fn ensure_visible_aligned_scrolls_even_when_already_visible() {
6556 use std::cell::Cell;
6557 use std::rc::Rc;
6558 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6559 let mut tree = WidgetTree::new();
6560 let actor = tree.add(FillWidget::new());
6561 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
6562 tree.layout(SizeProposal::exact(100.0, 100.0));
6563
6564 let target = Rect::new(10.0, 10.0, 20.0, 15.0);
6569 let mut ctx = EventContext::new();
6570 ctx.ensure_visible_aligned(target, 0.5, crate::event::ScrollMotion::Instant);
6571 tree.collect_from_ctx(ctx, actor);
6572
6573 assert_eq!(
6574 recorded.get(),
6575 Some(target),
6576 "an aligned reveal must dispatch even when the rect is already visible"
6577 );
6578 }
6579
6580 #[test]
6581 fn ensure_visible_aligned_forwards_fraction_and_motion() {
6582 use std::cell::Cell;
6583 use std::rc::Rc;
6584 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6585 Rc::new(Cell::new(None));
6586 let mut tree = WidgetTree::new();
6587 let actor = tree.add(FillWidget::new());
6588 let _container = recording_align_container(&mut tree, actor, recorded.clone());
6589 tree.layout(SizeProposal::exact(100.0, 100.0));
6590
6591 let mut ctx = EventContext::new();
6592 ctx.ensure_visible_aligned(
6593 Rect::new(10.0, 10.0, 20.0, 15.0),
6594 0.25,
6595 crate::event::ScrollMotion::Smooth,
6596 );
6597 tree.collect_from_ctx(ctx, actor);
6598
6599 assert_eq!(
6600 recorded.get(),
6601 Some((
6602 crate::event::ScrollAlign::Fraction(0.25),
6603 crate::event::ScrollMotion::Smooth
6604 )),
6605 "the container must receive the requested fraction and motion verbatim"
6606 );
6607 }
6608
6609 #[test]
6610 fn ensure_visible_aligned_clamps_the_fraction() {
6611 use std::cell::Cell;
6612 use std::rc::Rc;
6613 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6614 Rc::new(Cell::new(None));
6615 let mut tree = WidgetTree::new();
6616 let actor = tree.add(FillWidget::new());
6617 let _container = recording_align_container(&mut tree, actor, recorded.clone());
6618 tree.layout(SizeProposal::exact(100.0, 100.0));
6619
6620 let mut ctx = EventContext::new();
6621 ctx.ensure_visible_aligned(
6622 Rect::new(10.0, 10.0, 20.0, 15.0),
6623 4.2,
6624 crate::event::ScrollMotion::Instant,
6625 );
6626 tree.collect_from_ctx(ctx, actor);
6627
6628 assert_eq!(
6629 recorded.get().map(|(a, _)| a),
6630 Some(crate::event::ScrollAlign::Fraction(1.0)),
6631 "an out-of-range fraction must clamp rather than aim the pin off-screen"
6632 );
6633 }
6634
6635 #[test]
6636 fn plain_ensure_visible_requests_minimal_alignment() {
6637 use std::cell::Cell;
6638 use std::rc::Rc;
6639 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6640 Rc::new(Cell::new(None));
6641 let mut tree = WidgetTree::new();
6642 let actor = tree.add(FillWidget::new());
6643 let _container = recording_align_container(&mut tree, actor, recorded.clone());
6644 tree.layout(SizeProposal::exact(100.0, 100.0));
6645
6646 let mut ctx = EventContext::new();
6647 ctx.ensure_visible(Rect::new(10.0, 500.0, 20.0, 15.0));
6648 tree.collect_from_ctx(ctx, actor);
6649
6650 assert_eq!(
6651 recorded.get(),
6652 Some((
6653 crate::event::ScrollAlign::Minimal,
6654 crate::event::ScrollMotion::Instant
6655 )),
6656 "the pre-existing reveal API must keep its exact semantics"
6657 );
6658 }
6659
6660 #[test]
6661 fn only_the_innermost_container_aligns() {
6662 use std::cell::Cell;
6663 use std::rc::Rc;
6664 let inner_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6665 Rc::new(Cell::new(None));
6666 let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6667 Rc::new(Cell::new(None));
6668
6669 let mut tree = WidgetTree::new();
6670 let actor = tree.add(FillWidget::new());
6671 let inner = recording_align_container(&mut tree, actor, inner_rec.clone());
6672 let _outer = recording_align_container(&mut tree, inner, outer_rec.clone());
6673 tree.layout(SizeProposal::exact(100.0, 100.0));
6674
6675 let mut ctx = EventContext::new();
6678 ctx.ensure_visible_aligned(
6679 Rect::new(10.0, 500.0, 20.0, 15.0),
6680 0.5,
6681 crate::event::ScrollMotion::Instant,
6682 );
6683 tree.collect_from_ctx(ctx, actor);
6684
6685 assert_eq!(
6686 inner_rec.get().map(|(a, _)| a),
6687 Some(crate::event::ScrollAlign::Fraction(0.5)),
6688 "the innermost clipping ancestor owns the pin"
6689 );
6690 assert_eq!(
6691 outer_rec.get().map(|(a, _)| a),
6692 Some(crate::event::ScrollAlign::Minimal),
6693 "an outer container must only bring the inner viewport into view — a \
6694 fraction names a height in one viewport, not in every ancestor's"
6695 );
6696 }
6697
6698 #[test]
6699 fn a_clipper_that_does_not_scroll_passes_the_pin_on() {
6700 use crate::test_widgets::StackWidget;
6707 use std::cell::Cell;
6708 use std::rc::Rc;
6709 let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6710 Rc::new(Cell::new(None));
6711
6712 let mut tree = WidgetTree::new();
6713 let actor = tree.add(FillWidget::new());
6714 let clipper = tree.add(StackWidget::new().child(actor).clips_children_on(true));
6715 let _scroller = recording_align_container(&mut tree, clipper, outer_rec.clone());
6716 tree.layout(SizeProposal::exact(100.0, 100.0));
6717
6718 let mut ctx = EventContext::new();
6721 ctx.ensure_visible_aligned(
6722 Rect::new(10.0, 10.0, 20.0, 15.0),
6723 0.5,
6724 crate::event::ScrollMotion::Instant,
6725 );
6726 tree.collect_from_ctx(ctx, actor);
6727
6728 assert_eq!(
6729 outer_rec.get().map(|(a, _)| a),
6730 Some(crate::event::ScrollAlign::Fraction(0.5)),
6731 "a clipping wrapper with no scroll handler must pass the pin through \
6732 to the scroll container above it"
6733 );
6734 }
6735
6736 #[test]
6737 fn a_scroller_that_declines_to_move_still_holds_the_pin() {
6738 use crate::test_widgets::StackWidget;
6743 use std::cell::Cell;
6744 use std::rc::Rc;
6745 let inner_rec: Rc<Cell<Option<crate::event::ScrollAlign>>> = Rc::new(Cell::new(None));
6746 let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6747 Rc::new(Cell::new(None));
6748
6749 let mut tree = WidgetTree::new();
6750 let actor = tree.add(FillWidget::new());
6751 let rec = inner_rec.clone();
6752 let inner = tree.add(
6753 StackWidget::new()
6754 .child(actor)
6755 .on_scroll(move |ev, _ctx| match ev {
6756 WidgetEvent::ScrollIntoView { align, .. } => {
6757 rec.set(Some(*align));
6758 EventResponse::Ignored
6759 }
6760 _ => EventResponse::Ignored,
6761 })
6762 .clips_children(true),
6763 );
6764 let _outer = recording_align_container(&mut tree, inner, outer_rec.clone());
6765 tree.layout(SizeProposal::exact(100.0, 100.0));
6766
6767 let mut ctx = EventContext::new();
6769 ctx.ensure_visible_aligned(
6770 Rect::new(10.0, 500.0, 20.0, 15.0),
6771 0.5,
6772 crate::event::ScrollMotion::Instant,
6773 );
6774 tree.collect_from_ctx(ctx, actor);
6775
6776 assert_eq!(
6777 inner_rec.get(),
6778 Some(crate::event::ScrollAlign::Fraction(0.5)),
6779 "the innermost scroller is offered the pin"
6780 );
6781 assert_eq!(
6782 outer_rec.get().map(|(a, _)| a),
6783 Some(crate::event::ScrollAlign::Minimal),
6784 "declining to move must not pass the pin outward"
6785 );
6786 }
6787
6788 #[test]
6789 fn ensure_widget_visible_uses_target_arena_bounds() {
6790 use std::cell::Cell;
6791 use std::rc::Rc;
6792 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6793 let mut tree = WidgetTree::new();
6794 let target = tree.add(FillWidget::new());
6796 let rec = recorded.clone();
6797 let container = tree.add(
6798 BelowContainer {
6799 child: Some(target),
6800 offset: 500.0,
6801 }
6802 .on_scroll(move |ev, _ctx| match ev {
6803 WidgetEvent::ScrollIntoView { target_bounds, .. } => {
6804 rec.set(Some(*target_bounds));
6805 EventResponse::Handled
6806 }
6807 _ => EventResponse::Ignored,
6808 })
6809 .clips_children(true),
6810 );
6811 tree.layout(SizeProposal::exact(100.0, 100.0));
6812
6813 let expected = tree.bounds(target);
6814 assert!(
6815 expected.y > 100.0,
6816 "fixture sanity: the target must sit below the viewport (y={})",
6817 expected.y
6818 );
6819
6820 let mut ctx = EventContext::new();
6823 ctx.ensure_widget_visible(target);
6824 tree.collect_from_ctx(ctx, container);
6825
6826 assert_eq!(
6827 recorded.get(),
6828 Some(expected),
6829 "ensure_widget_visible(id) must dispatch ScrollIntoView with the \
6830 target's current arena bounds"
6831 );
6832 }
6833
6834 #[test]
6835 fn ensure_widget_visible_ignores_missing_widget() {
6836 use std::cell::Cell;
6838 use std::rc::Rc;
6839 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6840 let mut tree = WidgetTree::new();
6841 let actor = tree.add(FillWidget::new());
6842 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
6843 tree.layout(SizeProposal::exact(100.0, 100.0));
6844
6845 let mut ctx = EventContext::new();
6846 ctx.ensure_widget_visible(WidgetId::default());
6847 tree.collect_from_ctx(ctx, actor); assert_eq!(
6850 recorded.get(),
6851 None,
6852 "an unmounted id must not trigger a scroll"
6853 );
6854 }
6855
6856 #[test]
6857 fn context_menu_inside_a_modal_keeps_the_modal() {
6858 use crate::event::{Modifiers, PointerButton, WidgetEvent};
6862 use crate::overlay::{DismissBehavior, OverlayPlacement, OverlayRequest};
6863 use crate::test_widgets::{FillWidget, StackWidget};
6864
6865 let mut tree = WidgetTree::new();
6866 let modal_content = tree.add(StackWidget::new());
6869 let _editor = tree.add_child(
6870 modal_content,
6871 FillWidget::new()
6872 .context_menu(|_pos, _ctx| Some(Box::new(FillWidget::new()) as Box<dyn Widget>)),
6873 );
6874 tree.layout(SizeProposal::exact(200.0, 100.0));
6875
6876 let modal = tree.overlay_manager.show(OverlayRequest::new(
6877 modal_content,
6878 modal_content,
6879 OverlayPlacement::Centered,
6880 DismissBehavior::EscapeKey,
6881 ));
6882 tree.overlay_manager
6884 .stack
6885 .iter_mut()
6886 .find(|o| o.id == modal)
6887 .unwrap()
6888 .bounds = Rect::new(0.0, 0.0, 200.0, 100.0);
6889 assert_eq!(tree.overlay_manager.len(), 1);
6890
6891 tree.dispatch_event(WidgetEvent::pointer_down(
6893 Point::new(50.0, 25.0),
6894 PointerButton::Secondary,
6895 Modifiers::NONE,
6896 ));
6897
6898 assert!(
6899 tree.overlay_manager.active_ids().contains(&modal),
6900 "the modal must survive opening a context menu inside it"
6901 );
6902 assert_eq!(
6903 tree.overlay_manager.len(),
6904 2,
6905 "the context menu should now be open on top of the surviving modal"
6906 );
6907 }
6908}
6909
6910#[cfg(test)]
6913mod input_ingress_tests {
6914 use super::*;
6915 use crate::event::{EventResponse, Modifiers, PointerButton, ScrollDelta};
6916 use crate::pointer::{
6917 EventTime, PointerId, PointerInfo, PointerPhase, PointerSample, ScrollPhase, ScrollSample,
6918 ScrollSource,
6919 };
6920 use crate::test_widgets::FillWidget;
6921 use crate::widget::{LayoutContext, WidgetPlacement};
6922 use crate::widget_builder::WidgetBuilder;
6923 use std::cell::RefCell;
6924 use std::rc::Rc;
6925
6926 #[derive(Debug)]
6933 struct RowWidget {
6934 children: Vec<WidgetId>,
6935 }
6936
6937 impl crate::widget::Widget for RowWidget {
6938 fn layout_response(
6939 &self,
6940 proposal: SizeProposal,
6941 _ctx: &LayoutContext,
6942 ) -> crate::widget::LayoutResponse {
6943 proposal.resolve(0.0, 0.0).into()
6944 }
6945
6946 fn place_children(
6947 &self,
6948 bounds: Rect,
6949 _proposal: SizeProposal,
6950 children: &mut [WidgetPlacement],
6951 _ctx: &LayoutContext,
6952 ) {
6953 let n = children.len().max(1) as f32;
6954 let w = bounds.width / n;
6955 for (i, child) in children.iter_mut().enumerate() {
6956 child.origin = Point::new(bounds.x + w * i as f32, bounds.y);
6957 child.size = teksilo_canvas::Size::new(w, bounds.height);
6958 }
6959 }
6960
6961 fn children(&self) -> Vec<WidgetId> {
6962 self.children.clone()
6963 }
6964 }
6965
6966 fn record(drive: impl FnOnce(&mut WidgetTree)) -> Vec<String> {
6969 let log: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(Vec::new()));
6970
6971 let mut tree = WidgetTree::new();
6972 let pointer_log = log.clone();
6973 let hover_log = log.clone();
6974 tree.add(
6975 FillWidget::new()
6976 .on_pointer_event(move |event, _ctx| {
6977 pointer_log.borrow_mut().push(format!("{event:?}"));
6978 EventResponse::Ignored
6979 })
6980 .on_hover(move |entered, _ctx| {
6981 hover_log.borrow_mut().push(format!("hover({entered})"));
6982 }),
6983 );
6984 tree.layout(SizeProposal::exact(100.0, 100.0));
6985
6986 drive(&mut tree);
6987 log.borrow().clone()
6988 }
6989
6990 #[test]
6996 fn a_mouse_sample_reproduces_todays_event_stream() {
6997 let inside = Point::new(40.0, 40.0);
6998 let moved = Point::new(60.0, 55.0);
6999 let outside = Point::new(400.0, 400.0);
7000
7001 let legacy = record(|tree| {
7002 tree.dispatch_event(WidgetEvent::pointer_move(inside));
7003 tree.dispatch_event(WidgetEvent::pointer_down(
7004 inside,
7005 PointerButton::Primary,
7006 Modifiers::NONE,
7007 ));
7008 tree.dispatch_event(WidgetEvent::pointer_move(moved));
7009 tree.dispatch_event(WidgetEvent::pointer_up(
7010 moved,
7011 PointerButton::Primary,
7012 Modifiers::NONE,
7013 ));
7014 tree.dispatch_event(WidgetEvent::pointer_move(outside));
7015 });
7016
7017 let sampled = record(|tree| {
7018 let t = EventTime::ZERO;
7019 tree.dispatch_pointer(PointerSample::mouse(PointerPhase::Move, inside, t));
7020 tree.dispatch_pointer(
7021 PointerSample::mouse(PointerPhase::Down, inside, t)
7022 .with_button(PointerButton::Primary),
7023 );
7024 tree.dispatch_pointer(PointerSample::mouse(PointerPhase::Move, moved, t));
7025 tree.dispatch_pointer(
7026 PointerSample::mouse(PointerPhase::Up, moved, t)
7027 .with_button(PointerButton::Primary),
7028 );
7029 tree.dispatch_pointer(PointerSample::mouse(PointerPhase::Move, outside, t));
7030 });
7031
7032 assert_eq!(legacy, sampled);
7033 assert!(
7034 legacy.contains(&"hover(true)".to_string())
7035 && legacy.contains(&"hover(false)".to_string()),
7036 "the fixture must actually exercise enter and leave: {legacy:?}"
7037 );
7038 }
7039
7040 #[test]
7044 fn a_buttonless_press_lowers_to_primary() {
7045 let events = record(|tree| {
7046 tree.dispatch_pointer(PointerSample::mouse(
7047 PointerPhase::Down,
7048 Point::new(20.0, 20.0),
7049 EventTime::ZERO,
7050 ));
7051 });
7052 assert!(
7053 events.iter().any(|e| e.contains("button: Primary")),
7054 "{events:?}"
7055 );
7056 }
7057
7058 fn scroll_fixture() -> (WidgetTree, Rc<RefCell<Vec<&'static str>>>) {
7063 let hits: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
7064 let mut tree = WidgetTree::new();
7065
7066 let leading_hits = hits.clone();
7067 let trailing_hits = hits.clone();
7068 let leading = tree.add(FillWidget::new().on_scroll(move |_event, _ctx| {
7069 leading_hits.borrow_mut().push("leading");
7070 EventResponse::Handled
7071 }));
7072 let trailing = tree.add(FillWidget::new().on_scroll(move |_event, _ctx| {
7073 trailing_hits.borrow_mut().push("trailing");
7074 EventResponse::Handled
7075 }));
7076 tree.add(RowWidget {
7077 children: vec![leading, trailing],
7078 });
7079 tree.layout(SizeProposal::exact(200.0, 100.0));
7080 (tree, hits)
7081 }
7082
7083 fn notch() -> ScrollDelta {
7084 ScrollDelta::Lines { x: 0.0, y: -1.0 }
7085 }
7086
7087 #[test]
7090 fn a_positionless_scroll_routes_by_hover() {
7091 let (mut tree, hits) = scroll_fixture();
7092
7093 tree.pointer_move(Point::new(150.0, 50.0)); tree.dispatch_event(WidgetEvent::scroll(notch(), Modifiers::NONE));
7095 assert_eq!(*hits.borrow(), vec!["trailing"]);
7096
7097 tree.pointer_move(Point::new(50.0, 50.0)); tree.dispatch_scroll(ScrollSample::wheel(
7099 notch(),
7100 Modifiers::NONE,
7101 EventTime::ZERO,
7102 ));
7103 assert_eq!(*hits.borrow(), vec!["trailing", "leading"]);
7104 }
7105
7106 #[test]
7111 fn a_positioned_scroll_routes_by_hit_test() {
7112 let (mut tree, hits) = scroll_fixture();
7113 tree.pointer_move(Point::new(150.0, 50.0)); tree.dispatch_event(WidgetEvent::scroll_at(
7116 notch(),
7117 Modifiers::NONE,
7118 Point::new(50.0, 50.0), ));
7120 assert_eq!(*hits.borrow(), vec!["leading"]);
7121
7122 tree.dispatch_scroll(
7123 ScrollSample::wheel(notch(), Modifiers::NONE, EventTime::ZERO)
7124 .at(Point::new(150.0, 50.0)),
7125 );
7126 assert_eq!(*hits.borrow(), vec!["leading", "trailing"]);
7127 }
7128
7129 #[test]
7133 fn a_positionless_scroll_with_no_hover_goes_nowhere() {
7134 let (mut tree, hits) = scroll_fixture();
7135 tree.dispatch_event(WidgetEvent::scroll(notch(), Modifiers::NONE));
7136 assert!(hits.borrow().is_empty());
7137 }
7138
7139 #[test]
7144 fn a_handler_sees_the_sample_it_is_serving() {
7145 let seen: Rc<RefCell<Vec<(ScrollPhase, ScrollSource, Option<Point>)>>> =
7146 Rc::new(RefCell::new(Vec::new()));
7147 let sink = seen.clone();
7148
7149 let mut tree = WidgetTree::new();
7150 tree.add(
7157 FillWidget::new()
7158 .scroll_container(crate::pointer::touch_action::PanAxes::BOTH)
7159 .on_scroll(move |_event, ctx| {
7160 sink.borrow_mut().push((
7161 ctx.scroll_phase(),
7162 ctx.scroll_source(),
7163 ctx.pointer_position(),
7164 ));
7165 EventResponse::Handled
7166 }),
7167 );
7168 tree.layout(SizeProposal::exact(100.0, 100.0));
7169
7170 tree.dispatch_scroll(ScrollSample {
7171 delta: notch(),
7172 position: Some(Point::new(50.0, 50.0)),
7173 phase: ScrollPhase::Momentum,
7174 source: ScrollSource::TouchPan,
7175 pointer: PointerInfo::mouse(EventTime::from_millis(12)),
7176 modifiers: Modifiers::NONE,
7177 });
7178
7179 assert_eq!(
7180 *seen.borrow(),
7181 vec![(
7182 ScrollPhase::Momentum,
7183 ScrollSource::TouchPan,
7184 Some(Point::new(50.0, 50.0))
7185 )]
7186 );
7187 }
7188
7189 #[test]
7192 fn a_legacy_event_reports_the_mouse_at_the_epoch() {
7193 let seen: Rc<RefCell<Option<(PointerId, ScrollPhase)>>> = Rc::new(RefCell::new(None));
7194 let sink = seen.clone();
7195
7196 let mut tree = WidgetTree::new();
7197 tree.add(FillWidget::new().on_pointer_event(move |_event, ctx| {
7198 *sink.borrow_mut() = Some((ctx.pointer().id, ctx.scroll_phase()));
7199 EventResponse::Ignored
7200 }));
7201 tree.layout(SizeProposal::exact(100.0, 100.0));
7202 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(50.0, 50.0)));
7203
7204 assert_eq!(
7205 *seen.borrow(),
7206 Some((PointerId::MOUSE, ScrollPhase::Discrete))
7207 );
7208 }
7209
7210 #[test]
7214 fn the_snapshot_is_restored_after_a_dispatch() {
7215 let mut tree = WidgetTree::new();
7216 tree.add(FillWidget::new());
7217 tree.layout(SizeProposal::exact(100.0, 100.0));
7218
7219 let before = tree.current_input.clone();
7220 tree.dispatch_scroll(
7221 ScrollSample::wheel(notch(), Modifiers::NONE, EventTime::ZERO)
7222 .at(Point::new(10.0, 10.0)),
7223 );
7224 assert_eq!(tree.current_input, before);
7225 }
7226}
7227
7228#[cfg(test)]
7229mod press_and_focus_tests {
7230 use std::cell::{Cell, RefCell};
7235 use std::rc::Rc;
7236 use teksilo_canvas::{Point, SizeProposal};
7237
7238 use crate::WidgetId;
7239 use crate::event::{EventResponse, Key, Modifiers, PointerButton, WidgetEvent};
7240 use crate::focus::FocusOrigin;
7241 use crate::pointer::clock::ManualClock;
7242 use crate::pointer::touch_action::PanClaim;
7243 use crate::pointer::{
7244 BackendDeviceKey, EventTime, PointerId, PointerIdAllocator, PointerInfo, PointerPhase,
7245 PointerSample,
7246 };
7247 use crate::test_widgets::{FillWidget, StackWidget};
7248 use crate::widget_builder::WidgetBuilder;
7249 use crate::widget_tree::WidgetTree;
7250
7251 fn contact_id() -> PointerId {
7257 use std::sync::atomic::{AtomicU64, Ordering};
7258 static NEXT: AtomicU64 = AtomicU64::new(1);
7259 PointerIdAllocator::global().begin(
7260 BackendDeviceKey::new(0x0B11),
7261 NEXT.fetch_add(1, Ordering::Relaxed),
7262 )
7263 }
7264
7265 fn contact(id: PointerId, phase: PointerPhase, at: Point, t: EventTime) -> PointerSample {
7266 PointerSample {
7267 pointer: PointerInfo::touch(id, t),
7268 phase,
7269 position: at,
7270 button: None,
7271 modifiers: Modifiers::NONE,
7272 coalesced: Vec::new(),
7273 }
7274 }
7275
7276 fn tree_on_a_clock() -> (WidgetTree, Rc<ManualClock>) {
7279 let mut tree = WidgetTree::new();
7280 let clock = Rc::new(ManualClock::new(EventTime::ZERO));
7281 tree.set_input_clock(clock.clone());
7282 (tree, clock)
7283 }
7284
7285 fn tappable(tree: &mut WidgetTree) -> (WidgetId, crate::signal::Signal<bool>) {
7292 let id = tree.add(FillWidget::new().focusable().on_tap(|_e, _c| {}));
7293 let signal = tree.pressed_signal(id);
7294 (id, signal)
7295 }
7296
7297 #[test]
7304 fn a_pointer_origin_names_its_device() {
7305 use teksilo_tokens::{PenKind, PointerKind};
7306
7307 assert!(FocusOrigin::Pointer(PointerKind::Touch).is_pointer());
7308 assert!(FocusOrigin::Pointer(PointerKind::Pen(PenKind::Pen)).is_pointer());
7309 assert!(FocusOrigin::POINTER.is_pointer());
7310 assert!(!FocusOrigin::Keyboard.is_pointer());
7311 assert!(!FocusOrigin::Programmatic.is_pointer());
7312 assert!(!FocusOrigin::Accessibility.is_pointer());
7313
7314 assert_eq!(
7315 FocusOrigin::Pointer(PointerKind::Touch).pointer_kind(),
7316 Some(PointerKind::Touch),
7317 );
7318 assert_eq!(FocusOrigin::Keyboard.pointer_kind(), None);
7319 assert_eq!(
7320 FocusOrigin::POINTER.pointer_kind(),
7321 Some(PointerKind::Unknown),
7322 "a widget deriving its own origin says so rather than naming a device it never saw",
7323 );
7324 }
7325
7326 #[test]
7330 fn only_a_real_navigation_declares_a_modality() {
7331 use teksilo_tokens::PointerKind;
7332
7333 assert_eq!(FocusOrigin::Keyboard.focus_visible(), Some(true));
7334 assert_eq!(FocusOrigin::Accessibility.focus_visible(), Some(true));
7335 assert_eq!(
7336 FocusOrigin::Pointer(PointerKind::Touch).focus_visible(),
7337 Some(false),
7338 );
7339 assert_eq!(FocusOrigin::Programmatic.focus_visible(), None);
7340 }
7341
7342 #[test]
7345 fn the_tier_three_configs_are_unchanged() {
7346 let origin: crate::signal::Signal<Option<FocusOrigin>> =
7347 crate::signal::Signal::new(Some(FocusOrigin::Keyboard));
7348 let slider: crate::signal::Signal<Option<FocusOrigin>> = origin.clone();
7349 let splitter: crate::signal::Signal<Option<FocusOrigin>> = origin.clone();
7350 let segmented: crate::signal::Signal<Option<FocusOrigin>> = origin.clone();
7351 for field in [slider, splitter, segmented] {
7352 assert_eq!(field.get(), Some(FocusOrigin::Keyboard));
7353 }
7354 origin.set(Some(FocusOrigin::POINTER));
7355 assert_ne!(
7356 origin.get(),
7357 Some(FocusOrigin::Keyboard),
7358 "the `== Some(Keyboard)` test every consumer makes still discriminates",
7359 );
7360 }
7361
7362 #[test]
7373 fn a_touch_tap_after_keyboard_focus_leaves_no_ring() {
7374 let (mut tree, _clock) = tree_on_a_clock();
7375 let a = tree.add(FillWidget::new().focusable());
7376 let b = tree.add(FillWidget::new().focusable());
7377 let root = tree.add(SideBySide {
7378 children: vec![a, b],
7379 });
7380 tree.layout(SizeProposal::exact(200.0, 100.0));
7381 let _ = root;
7382
7383 let visible = tree.focus_visible_signal();
7384 tree.press_key(Key::Tab, Modifiers::NONE);
7385 assert_eq!(tree.focused(), Some(a));
7386 assert!(visible.get(), "keyboard navigation reveals the ring");
7387
7388 let id = contact_id();
7389 let at = tree.bounds(b).center();
7390 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
7391 tree.dispatch_pointer(contact(
7392 id,
7393 PointerPhase::Up,
7394 at,
7395 EventTime::from_millis(30),
7396 ));
7397
7398 assert_eq!(tree.focused(), Some(b), "the release moved focus");
7399 assert!(!visible.get(), "and the ring did not come with it");
7400 assert_eq!(
7401 tree.focus_origin(),
7402 Some(FocusOrigin::Pointer(teksilo_tokens::PointerKind::Touch)),
7403 );
7404 }
7405
7406 #[test]
7411 fn an_assistive_focus_reveals_the_ring() {
7412 let mut tree = WidgetTree::new();
7413 let a = tree.add(FillWidget::new().focusable());
7414 let b = tree.add(FillWidget::new().focusable());
7415 let root = tree.add(SideBySide {
7416 children: vec![a, b],
7417 });
7418 tree.layout(SizeProposal::exact(200.0, 50.0));
7419 let _ = root;
7420 let visible = tree.focus_visible_signal();
7421
7422 tree.click(a);
7423 assert_eq!(tree.focused(), Some(a));
7424 assert!(!visible.get(), "the click hid it");
7425
7426 tree.dispatch_event(WidgetEvent::AccessAction {
7427 target: Some(b),
7428 action: accesskit::Action::Focus,
7429 target_node: crate::accessibility::widget_id_to_node_id(b),
7430 data: None,
7431 });
7432 assert_eq!(tree.focused(), Some(b));
7433 assert!(visible.get(), "an assistive move is a navigation");
7434 assert_eq!(tree.focus_origin(), Some(FocusOrigin::Accessibility));
7435 }
7436
7437 #[test]
7442 fn a_programmatic_focus_leaves_the_modality_alone() {
7443 let mut tree = WidgetTree::new();
7444 let a = tree.add(FillWidget::new().focusable());
7445 let b = tree.add(FillWidget::new().focusable());
7446 let root = tree.add(SideBySide {
7447 children: vec![a, b],
7448 });
7449 tree.layout(SizeProposal::exact(200.0, 100.0));
7450 let _ = root;
7451 let visible = tree.focus_visible_signal();
7452
7453 tree.press_key(Key::Tab, Modifiers::NONE);
7454 assert_eq!(tree.focused(), Some(a));
7455 assert!(visible.get());
7456 tree.focus(b);
7457 assert!(
7458 visible.get(),
7459 "a scripted focus does not hide a keyboard ring"
7460 );
7461
7462 tree.click(a);
7463 assert!(!visible.get());
7464 tree.focus(b);
7465 assert!(
7466 !visible.get(),
7467 "and does not reveal one after a click either",
7468 );
7469 }
7470
7471 #[test]
7477 fn a_mouse_still_focuses_on_press() {
7478 let mut tree = WidgetTree::new();
7479 let w = tree.add(FillWidget::new().focusable());
7480 tree.layout(SizeProposal::exact(100.0, 50.0));
7481 let center = tree.bounds(w).center();
7482
7483 tree.dispatch_event(WidgetEvent::pointer_down(
7484 center,
7485 PointerButton::Primary,
7486 Modifiers::NONE,
7487 ));
7488 assert_eq!(
7489 tree.focused(),
7490 Some(w),
7491 "focus lands on the press for an indirect pointer",
7492 );
7493 }
7494
7495 #[test]
7497 fn a_finger_focuses_on_release() {
7498 let (mut tree, _clock) = tree_on_a_clock();
7499 let w = tree.add(FillWidget::new().focusable());
7500 tree.layout(SizeProposal::exact(100.0, 50.0));
7501 let center = tree.bounds(w).center();
7502
7503 let id = contact_id();
7504 tree.dispatch_pointer(contact(id, PointerPhase::Down, center, EventTime::ZERO));
7505 assert_eq!(
7506 tree.focused(),
7507 None,
7508 "a finger that has only landed has chosen nothing yet",
7509 );
7510
7511 tree.dispatch_pointer(contact(
7512 id,
7513 PointerPhase::Up,
7514 center,
7515 EventTime::from_millis(40),
7516 ));
7517 assert_eq!(tree.focused(), Some(w));
7518 assert_eq!(
7519 tree.focus_origin().and_then(FocusOrigin::pointer_kind),
7520 Some(teksilo_tokens::PointerKind::Touch),
7521 "the origin names the device that delivered it",
7522 );
7523 }
7524
7525 #[test]
7529 fn a_release_on_a_different_focusable_moves_no_focus() {
7530 let (mut tree, _clock) = tree_on_a_clock();
7531 let a = tree.add(FillWidget::new().focusable());
7532 let b = tree.add(FillWidget::new().focusable());
7533 let root = tree.add(SideBySide {
7534 children: vec![a, b],
7535 });
7536 tree.layout(SizeProposal::exact(200.0, 50.0));
7537 let _ = root;
7538
7539 let on_a = tree.bounds(a).center();
7540 let on_b = tree.bounds(b).center();
7541 let id = contact_id();
7542 tree.dispatch_pointer(contact(id, PointerPhase::Down, on_a, EventTime::ZERO));
7543 tree.dispatch_pointer(contact(
7544 id,
7545 PointerPhase::Move,
7546 on_b,
7547 EventTime::from_millis(20),
7548 ));
7549 tree.dispatch_pointer(contact(
7550 id,
7551 PointerPhase::Up,
7552 on_b,
7553 EventTime::from_millis(40),
7554 ));
7555
7556 assert_eq!(
7557 tree.focused(),
7558 None,
7559 "the guard is `the release landed on the same focusable as the press`",
7560 );
7561 }
7562
7563 #[test]
7570 fn a_mouse_press_visual_is_unchanged() {
7571 let mut tree = WidgetTree::new();
7572 let (w, pressed) = tappable(&mut tree);
7573 tree.layout(SizeProposal::exact(100.0, 50.0));
7574 let center = tree.bounds(w).center();
7575
7576 assert!(!pressed.get(), "not pressed at rest");
7577 tree.dispatch_event(WidgetEvent::pointer_down(
7578 center,
7579 PointerButton::Primary,
7580 Modifiers::NONE,
7581 ));
7582 assert!(pressed.get(), "an indirect pointer lights up on the press");
7583 assert!(
7584 !tree.press_pending(w),
7585 "and never waits: a mouse opens no pan session, so there is no \
7586 ambiguity to wait out",
7587 );
7588 assert_eq!(tree.pressed_by(w), Some(PointerId::MOUSE));
7589
7590 tree.dispatch_event(WidgetEvent::pointer_up(
7591 center,
7592 PointerButton::Primary,
7593 Modifiers::NONE,
7594 ));
7595 assert!(!pressed.get(), "the release clears it");
7596 assert_eq!(tree.pressed_by(w), None);
7597 }
7598
7599 #[test]
7603 fn a_slide_off_clears_the_visual_and_re_entry_restores_it() {
7604 let (mut tree, _clock) = tree_on_a_clock();
7605 let (w, pressed) = tappable(&mut tree);
7606 tree.layout(SizeProposal::exact(100.0, 200.0));
7607 let inside = tree.bounds(w).center();
7608 let outside = Point::new(inside.x, inside.y + 400.0);
7609
7610 let id = contact_id();
7611 tree.dispatch_pointer(contact(id, PointerPhase::Down, inside, EventTime::ZERO));
7612 assert!(
7613 pressed.get(),
7614 "nothing here claims a pan, so no delay applies"
7615 );
7616
7617 tree.dispatch_pointer(contact(
7618 id,
7619 PointerPhase::Move,
7620 outside,
7621 EventTime::from_millis(20),
7622 ));
7623 assert!(!pressed.get(), "the press has left its target");
7624 assert!(!tree.press_is_inside(w));
7625 assert_eq!(
7626 tree.pressed_by(w),
7627 Some(id),
7628 "the contact still holds the press — it is the *visual* that is off",
7629 );
7630
7631 tree.dispatch_pointer(contact(
7632 id,
7633 PointerPhase::Move,
7634 inside,
7635 EventTime::from_millis(40),
7636 ));
7637 assert!(pressed.get(), "sliding back on restores it");
7638
7639 tree.dispatch_pointer(contact(
7640 id,
7641 PointerPhase::Up,
7642 inside,
7643 EventTime::from_millis(60),
7644 ));
7645 assert!(!pressed.get());
7646 }
7647
7648 #[test]
7651 fn a_second_contact_cannot_clear_the_first_visual() {
7652 let (mut tree, _clock) = tree_on_a_clock();
7653 let (w, pressed) = tappable(&mut tree);
7654 tree.layout(SizeProposal::exact(100.0, 50.0));
7655 let bounds = tree.bounds(w);
7656 let first_at = Point::new(bounds.x + 20.0, bounds.y + 25.0);
7657 let second_at = Point::new(bounds.x + 70.0, bounds.y + 25.0);
7658
7659 let first = contact_id();
7660 let second = contact_id();
7661 tree.dispatch_pointer(contact(
7662 first,
7663 PointerPhase::Down,
7664 first_at,
7665 EventTime::ZERO,
7666 ));
7667 assert!(pressed.get());
7668 assert_eq!(tree.pressed_by(w), Some(first));
7669
7670 tree.dispatch_pointer(contact(
7671 second,
7672 PointerPhase::Down,
7673 second_at,
7674 EventTime::from_millis(10),
7675 ));
7676 assert_eq!(
7677 tree.pressed_by(w),
7678 Some(first),
7679 "under `MultiContact::First` the second contact is terminated before \
7680 it reaches the arena at all",
7681 );
7682
7683 tree.dispatch_pointer(contact(
7684 second,
7685 PointerPhase::Up,
7686 second_at,
7687 EventTime::from_millis(20),
7688 ));
7689 assert!(
7690 pressed.get(),
7691 "so the second contact's release cannot clear a visual it never owned",
7692 );
7693 assert_eq!(tree.pressed_by(w), Some(first));
7694
7695 tree.dispatch_pointer(contact(
7696 first,
7697 PointerPhase::Up,
7698 first_at,
7699 EventTime::from_millis(30),
7700 ));
7701 assert!(!pressed.get(), "the owner's release does clear it");
7702 }
7703
7704 #[test]
7707 fn a_cancel_clears_the_visual() {
7708 let (mut tree, _clock) = tree_on_a_clock();
7709 let (w, pressed) = tappable(&mut tree);
7710 tree.layout(SizeProposal::exact(100.0, 50.0));
7711 let center = tree.bounds(w).center();
7712
7713 let id = contact_id();
7714 tree.dispatch_pointer(contact(id, PointerPhase::Down, center, EventTime::ZERO));
7715 assert!(pressed.get());
7716
7717 tree.dispatch_pointer(contact(
7718 id,
7719 PointerPhase::Cancel,
7720 center,
7721 EventTime::from_millis(20),
7722 ));
7723 assert!(!pressed.get(), "a press that was taken away is not painted");
7724 assert_eq!(tree.pressed_by(w), None);
7725 }
7726
7727 #[test]
7730 fn a_pan_claim_clears_the_visual() {
7731 let (mut tree, clock) = tree_on_a_clock();
7732 let (row, pressed) = tappable(&mut tree);
7733 let list = tree.add(
7734 StackWidget::new()
7735 .child(row)
7736 .pan_claim(PanClaim::vertical())
7737 .on_scroll(|_e, _c| EventResponse::Handled),
7738 );
7739 tree.layout(SizeProposal::exact(200.0, 400.0));
7740 let _ = list;
7741 let start = Point::new(100.0, 200.0);
7742
7743 let id = contact_id();
7744 tree.dispatch_pointer(contact(id, PointerPhase::Down, start, EventTime::ZERO));
7745 assert!(tree.press_pending(row));
7747 clock.set(EventTime::from_millis(100));
7748 tree.tick_gestures(std::time::Instant::now());
7749 assert!(pressed.get(), "…and appears once the delay has elapsed");
7750
7751 tree.dispatch_pointer(contact(
7753 id,
7754 PointerPhase::Move,
7755 Point::new(100.0, 260.0),
7756 EventTime::from_millis(120),
7757 ));
7758 assert!(
7759 !pressed.get(),
7760 "the row lost the press to the list and must stop advertising it",
7761 );
7762 assert_eq!(tree.pressed_by(row), None);
7763 }
7764
7765 #[test]
7774 fn a_button_the_control_cannot_act_on_raises_no_visual() {
7775 let taps = Rc::new(Cell::new(0u32));
7776 let count = taps.clone();
7777 let mut tree = WidgetTree::new();
7778 let w = tree.add(FillWidget::new().focusable().on_tap(move |_e, _c| {
7779 count.set(count.get() + 1);
7780 }));
7781 let pressed = tree.pressed_signal(w);
7782 tree.layout(SizeProposal::exact(100.0, 50.0));
7783 let center = tree.bounds(w).center();
7784
7785 for button in [
7786 PointerButton::Middle,
7787 PointerButton::Back,
7788 PointerButton::Forward,
7789 ] {
7790 tree.dispatch_event(WidgetEvent::pointer_down(center, button, Modifiers::NONE));
7791 assert!(
7792 !pressed.get(),
7793 "{button:?} cannot activate an `on_tap` node, so it must not light one up",
7794 );
7795 assert_eq!(tree.pressed_by(w), None, "and owns no visual to clear");
7796 tree.dispatch_event(WidgetEvent::pointer_up(center, button, Modifiers::NONE));
7797 assert_eq!(
7798 taps.get(),
7799 0,
7800 "the tap recognizer refuses {button:?} too — that is the point",
7801 );
7802 }
7803
7804 tree.dispatch_event(WidgetEvent::pointer_down(
7806 center,
7807 PointerButton::Primary,
7808 Modifiers::NONE,
7809 ));
7810 assert!(pressed.get(), "a primary press lights up as it always has");
7811 assert_eq!(tree.pressed_by(w), Some(PointerId::MOUSE));
7812 tree.dispatch_event(WidgetEvent::pointer_up(
7813 center,
7814 PointerButton::Primary,
7815 Modifiers::NONE,
7816 ));
7817 assert!(!pressed.get());
7818 assert_eq!(taps.get(), 1);
7819 }
7820
7821 #[test]
7825 fn a_secondary_press_with_no_context_menu_raises_no_visual() {
7826 let mut tree = WidgetTree::new();
7827 let (w, pressed) = tappable(&mut tree);
7828 tree.layout(SizeProposal::exact(100.0, 50.0));
7829 let center = tree.bounds(w).center();
7830
7831 tree.dispatch_event(WidgetEvent::pointer_down(
7832 center,
7833 PointerButton::Secondary,
7834 Modifiers::NONE,
7835 ));
7836 assert!(
7837 !pressed.get(),
7838 "nothing opened, and nothing may look pressed either",
7839 );
7840 assert_eq!(tree.pressed_by(w), None);
7841 assert!(!tree.press_is_inside(w));
7842 }
7843
7844 #[test]
7848 fn a_widened_mask_widens_the_visual_with_it() {
7849 use crate::event::ButtonMask;
7850
7851 let mut tree = WidgetTree::new();
7852 let w = tree.add(
7853 FillWidget::new()
7854 .on_tap(|_e, _c| {})
7855 .accept_tap_buttons(ButtonMask::PRIMARY | ButtonMask::MIDDLE),
7856 );
7857 let pressed = tree.pressed_signal(w);
7858 tree.layout(SizeProposal::exact(100.0, 50.0));
7859 let center = tree.bounds(w).center();
7860
7861 tree.dispatch_event(WidgetEvent::pointer_down(
7862 center,
7863 PointerButton::Middle,
7864 Modifiers::NONE,
7865 ));
7866 assert!(pressed.get(), "this node really does act on a middle-click");
7867
7868 tree.dispatch_event(WidgetEvent::pointer_up(
7869 center,
7870 PointerButton::Middle,
7871 Modifiers::NONE,
7872 ));
7873 assert!(!pressed.get());
7874 }
7875
7876 #[test]
7882 fn a_press_that_raises_no_visual_still_defers_its_focus() {
7883 use teksilo_tokens::{PenKind, PointerKind};
7884
7885 let (mut tree, _clock) = tree_on_a_clock();
7886 let (w, pressed) = tappable(&mut tree);
7887 tree.layout(SizeProposal::exact(100.0, 50.0));
7888 let center = tree.bounds(w).center();
7889
7890 let id = contact_id();
7891 let barrel = |phase, t| PointerSample {
7892 pointer: PointerInfo {
7893 kind: PointerKind::Pen(PenKind::Pen),
7894 ..PointerInfo::touch(id, t)
7895 },
7896 phase,
7897 position: center,
7898 button: Some(PointerButton::Secondary),
7899 modifiers: Modifiers::NONE,
7900 coalesced: Vec::new(),
7901 };
7902
7903 tree.dispatch_pointer(barrel(PointerPhase::Down, EventTime::ZERO));
7904 assert!(!pressed.get(), "the barrel button activates nothing here");
7905 assert_eq!(tree.pressed_by(w), None);
7906 assert_eq!(
7907 tree.focused(),
7908 None,
7909 "a direct pointer has chosen nothing until it lifts",
7910 );
7911
7912 tree.dispatch_pointer(barrel(PointerPhase::Up, EventTime::from_millis(40)));
7913 assert_eq!(
7914 tree.focused(),
7915 Some(w),
7916 "the deferral is not button-gated: the release landed where the press did",
7917 );
7918 assert_eq!(
7919 tree.focus_origin().and_then(FocusOrigin::pointer_kind),
7920 Some(PointerKind::Pen(PenKind::Pen)),
7921 );
7922 }
7923
7924 #[test]
7931 fn the_feedback_delay_applies_only_inside_a_claimant() {
7932 let (mut tree, _clock) = tree_on_a_clock();
7934 let (w, pressed) = tappable(&mut tree);
7935 tree.layout(SizeProposal::exact(200.0, 400.0));
7936 let id = contact_id();
7937 let at = tree.bounds(w).center();
7938 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
7939 assert!(
7940 !tree.press_pending(w),
7941 "no claimant above it, so nothing to rule out",
7942 );
7943 assert!(pressed.get());
7944
7945 let (mut tree, clock) = tree_on_a_clock();
7947 let (row, pressed) = tappable(&mut tree);
7948 let list = tree.add(
7949 StackWidget::new()
7950 .child(row)
7951 .pan_claim(PanClaim::vertical())
7952 .on_scroll(|_e, _c| EventResponse::Handled),
7953 );
7954 tree.layout(SizeProposal::exact(200.0, 400.0));
7955 let _ = list;
7956 let id = contact_id();
7957 let at = Point::new(100.0, 200.0);
7958 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
7959 assert!(tree.press_pending(row), "a finger might be about to scroll");
7960 assert!(!pressed.get(), "so the row does not flash");
7961 assert!(
7962 tree.press_is_inside(row),
7963 "the press is real; only its visual is being withheld",
7964 );
7965
7966 clock.set(EventTime::from_millis(99));
7967 tree.tick_gestures(std::time::Instant::now());
7968 assert!(!pressed.get(), "99 ms is under the 100 ms delay");
7969
7970 clock.set(EventTime::from_millis(100));
7971 tree.tick_gestures(std::time::Instant::now());
7972 assert!(pressed.get(), "and 100 ms is it");
7973 assert!(!tree.press_pending(row));
7974 }
7975
7976 #[test]
7979 fn a_mouse_inside_a_claimant_never_waits() {
7980 let mut tree = WidgetTree::new();
7981 let (row, pressed) = tappable(&mut tree);
7982 let list = tree.add(
7983 StackWidget::new()
7984 .child(row)
7985 .pan_claim(PanClaim::vertical())
7986 .on_scroll(|_e, _c| EventResponse::Handled),
7987 );
7988 tree.layout(SizeProposal::exact(200.0, 400.0));
7989 let _ = list;
7990
7991 tree.dispatch_event(WidgetEvent::pointer_down(
7992 Point::new(100.0, 200.0),
7993 PointerButton::Primary,
7994 Modifiers::NONE,
7995 ));
7996 assert!(!tree.press_pending(row));
7997 assert!(pressed.get(), "Compact with a mouse is exactly as it was");
7998 }
7999
8000 #[test]
8007 fn a_handler_can_read_the_press_it_is_inside() {
8008 let (mut tree, _clock) = tree_on_a_clock();
8009 let seen: Rc<RefCell<Vec<(bool, bool, bool)>>> = Rc::new(RefCell::new(Vec::new()));
8010 let log = seen.clone();
8011 let row = tree.add(FillWidget::new().on_tap(|_e, _c| {}).on_pointer_event(
8012 move |event, ctx| {
8013 if matches!(event, WidgetEvent::PointerUp { .. }) {
8014 log.borrow_mut().push((
8015 ctx.is_pressed(),
8016 ctx.press_is_inside(),
8017 ctx.press_pending(),
8018 ));
8019 }
8020 EventResponse::Ignored
8021 },
8022 ));
8023 let list = tree.add(
8024 StackWidget::new()
8025 .child(row)
8026 .pan_claim(PanClaim::vertical())
8027 .on_scroll(|_e, _c| EventResponse::Handled),
8028 );
8029 tree.layout(SizeProposal::exact(200.0, 400.0));
8030 let _ = list;
8031
8032 let id = contact_id();
8033 let at = Point::new(100.0, 200.0);
8034 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8035 tree.dispatch_pointer(contact(
8036 id,
8037 PointerPhase::Up,
8038 at,
8039 EventTime::from_millis(30),
8040 ));
8041
8042 assert_eq!(
8043 seen.borrow().as_slice(),
8044 &[(false, true, true)],
8045 "released inside the claimant before the delay elapsed: inside, \
8046 pending, and therefore not yet showing",
8047 );
8048 }
8049
8050 #[test]
8053 fn a_handler_outside_a_press_reads_nothing() {
8054 let mut tree = WidgetTree::new();
8055 let asked = Rc::new(Cell::new(false));
8056 let flag = asked.clone();
8057 let w = tree.add(FillWidget::new().focusable().on_key(move |_e, ctx| {
8058 flag.set(ctx.is_pressed() || ctx.press_is_inside() || ctx.press_pending());
8059 EventResponse::Ignored
8060 }));
8061 tree.layout(SizeProposal::exact(100.0, 50.0));
8062 tree.focus(w);
8063 tree.press_key(Key::ArrowDown, Modifiers::NONE);
8064 assert!(!asked.get());
8065 }
8066
8067 #[test]
8071 fn a_completed_press_leaves_the_detector_clean() {
8072 let (mut tree, _clock) = tree_on_a_clock();
8073 let (w, pressed) = tappable(&mut tree);
8074 tree.layout(SizeProposal::exact(100.0, 50.0));
8075 let at = tree.bounds(w).center();
8076
8077 let id = contact_id();
8078 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8079 assert!(pressed.get());
8080 tree.dispatch_pointer(contact(
8081 id,
8082 PointerPhase::Up,
8083 at,
8084 EventTime::from_millis(30),
8085 ));
8086 tree.assert_no_leaked_pointer_state();
8087 }
8088
8089 #[test]
8093 fn the_detector_reports_a_press_still_held() {
8094 let (mut tree, _clock) = tree_on_a_clock();
8095 let (w, _pressed) = tappable(&mut tree);
8096 tree.layout(SizeProposal::exact(100.0, 50.0));
8097 let at = tree.bounds(w).center();
8098
8099 let id = contact_id();
8100 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8101 let held = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
8102 tree.assert_no_leaked_pointer_state()
8103 }));
8104 let message = *held
8105 .expect_err("a live press is leaked state")
8106 .downcast::<String>()
8107 .expect("the detector panics with a message");
8108 assert!(
8109 message.contains("is still pressed by"),
8110 "the press is named in the report, got: {message}",
8111 );
8112 }
8113
8114 #[derive(Debug)]
8118 struct SideBySide {
8119 children: Vec<WidgetId>,
8120 }
8121
8122 impl crate::widget::Widget for SideBySide {
8123 fn layout_response(
8124 &self,
8125 proposal: SizeProposal,
8126 _ctx: &crate::widget::LayoutContext,
8127 ) -> crate::widget::LayoutResponse {
8128 proposal.resolve(0.0, 0.0).into()
8129 }
8130
8131 fn place_children(
8132 &self,
8133 bounds: teksilo_canvas::Rect,
8134 _proposal: SizeProposal,
8135 children: &mut [crate::widget::WidgetPlacement],
8136 _ctx: &crate::widget::LayoutContext,
8137 ) {
8138 let n = children.len().max(1) as f32;
8139 let w = bounds.width / n;
8140 for (i, child) in children.iter_mut().enumerate() {
8141 child.origin = Point::new(bounds.x + w * i as f32, bounds.y);
8142 child.size = teksilo_canvas::Size::new(w, bounds.height);
8143 }
8144 }
8145
8146 fn children(&self) -> Vec<WidgetId> {
8147 self.children.clone()
8148 }
8149 }
8150}
8151
8152#[cfg(test)]
8153mod overlay_release_dismissal_tests {
8154 use std::cell::Cell;
8164 use std::rc::Rc;
8165
8166 use teksilo_canvas::{Point, Rect, Size, SizeProposal};
8167
8168 use crate::WidgetId;
8169 use crate::event::{Modifiers, PointerButton, WidgetEvent};
8170 use crate::overlay::{DismissBehavior, OverlayPlacement, OverlayRequest};
8171 use crate::pointer::{
8172 BackendDeviceKey, EventTime, PointerId, PointerIdAllocator, PointerInfo, PointerPhase,
8173 PointerSample,
8174 };
8175 use crate::test_widgets::FillWidget;
8176 use crate::widget::{LayoutContext, LayoutResponse, Widget};
8177 use crate::widget_builder::WidgetBuilder;
8178 use crate::widget_tree::WidgetTree;
8179
8180 #[derive(Debug)]
8187 struct Panel(Size);
8188
8189 impl Widget for Panel {
8190 fn layout_response(&self, _proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
8191 self.0.into()
8192 }
8193 }
8194
8195 #[derive(Debug)]
8203 struct PageAndTrigger {
8204 children: Vec<WidgetId>,
8205 }
8206
8207 impl Widget for PageAndTrigger {
8208 fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
8209 proposal.resolve(0.0, 0.0).into()
8210 }
8211
8212 fn place_children(
8213 &self,
8214 bounds: Rect,
8215 _proposal: SizeProposal,
8216 children: &mut [crate::widget::WidgetPlacement],
8217 _ctx: &LayoutContext,
8218 ) {
8219 if let Some(page) = children.get_mut(0) {
8220 page.origin = bounds.origin();
8221 page.size = bounds.size();
8222 }
8223 if let Some(trigger) = children.get_mut(1) {
8224 trigger.origin = bounds.origin();
8225 trigger.size = Size::new(10.0, 10.0);
8226 }
8227 }
8228
8229 fn children(&self) -> Vec<WidgetId> {
8230 self.children.clone()
8231 }
8232 }
8233
8234 fn contact_id(n: u64) -> PointerId {
8235 PointerIdAllocator::global().begin(BackendDeviceKey::new(0x0FA2), n)
8236 }
8237
8238 fn touch(id: PointerId, phase: PointerPhase, at: Point) -> PointerSample {
8239 PointerSample {
8240 pointer: PointerInfo::touch(id, EventTime::ZERO),
8241 phase,
8242 position: at,
8243 button: None,
8244 modifiers: Modifiers::NONE,
8245 coalesced: Vec::new(),
8246 }
8247 }
8248
8249 fn page_with_a_menu() -> (WidgetTree, WidgetId, Rc<Cell<u32>>) {
8256 let mut tree = WidgetTree::new();
8257 let taps = Rc::new(Cell::new(0u32));
8258 let counter = taps.clone();
8259 let page = tree.add(
8260 FillWidget::new()
8261 .focusable()
8262 .on_tap(move |_e, _c| counter.set(counter.get() + 1)),
8263 );
8264 let trigger = tree.add(Panel(Size::new(10.0, 10.0)));
8267 let _root = tree.add(PageAndTrigger {
8268 children: vec![page, trigger],
8269 });
8270 let menu = tree.add(Panel(Size::new(200.0, 200.0)));
8271 tree.show_overlay(OverlayRequest::new(
8272 menu,
8273 trigger,
8274 OverlayPlacement::AtPointer(Point::new(100.0, 100.0)),
8275 DismissBehavior::ClickOutside,
8276 ));
8277 tree.layout(SizeProposal::exact(800.0, 600.0));
8278 assert_eq!(tree.active_overlays().len(), 1, "the menu is open");
8279 (tree, page, taps)
8280 }
8281
8282 fn outside() -> Point {
8283 Point::new(600.0, 500.0)
8284 }
8285
8286 fn inside_menu() -> Point {
8287 Point::new(150.0, 150.0)
8288 }
8289
8290 #[test]
8297 fn the_suppressed_down_leaves_nothing_pressed_or_captured_beneath() {
8298 let (mut tree, page, taps) = page_with_a_menu();
8299 let finger = contact_id(1);
8300 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8301
8302 assert_eq!(
8303 tree.active_overlays().len(),
8304 1,
8305 "the press does not close the menu; the release does"
8306 );
8307 assert_eq!(tree.pressed_by(page), None, "nothing beneath is pressed");
8308 let entry = tree.pointers.get(finger).expect("the contact is live");
8309 assert_eq!(entry.captured_by, None, "nothing beneath captured it");
8310 assert!(entry.sequence.is_none(), "no arbitration was opened");
8311 assert_eq!(taps.get(), 0);
8312 }
8313
8314 #[test]
8316 fn a_touch_tap_outside_a_menu_closes_it_and_actuates_nothing() {
8317 let (mut tree, page, taps) = page_with_a_menu();
8318 let finger = contact_id(2);
8319 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8320 tree.dispatch_pointer(touch(finger, PointerPhase::Up, outside()));
8321
8322 assert!(tree.active_overlays().is_empty(), "the menu closed");
8323 assert_eq!(taps.get(), 0, "the page beneath was never tapped");
8324 assert_eq!(tree.pressed_by(page), None);
8325 tree.assert_no_leaked_pointer_state();
8326 }
8327
8328 #[test]
8331 fn a_cancelled_press_aborts_the_arm() {
8332 let (mut tree, page, taps) = page_with_a_menu();
8333 let finger = contact_id(3);
8334 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8335 tree.dispatch_pointer(touch(finger, PointerPhase::Cancel, outside()));
8336
8337 assert_eq!(tree.active_overlays().len(), 1, "the menu survives");
8338 assert_eq!(taps.get(), 0);
8339 assert_eq!(tree.pressed_by(page), None);
8340 tree.assert_no_leaked_pointer_state();
8341 }
8342
8343 #[test]
8347 fn a_press_slid_onto_the_menu_dismisses_nothing() {
8348 let (mut tree, _page, taps) = page_with_a_menu();
8349 let finger = contact_id(4);
8350 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8351 tree.dispatch_pointer(touch(finger, PointerPhase::Move, inside_menu()));
8352 tree.dispatch_pointer(touch(finger, PointerPhase::Up, inside_menu()));
8353
8354 assert_eq!(tree.active_overlays().len(), 1, "the menu survives");
8355 assert_eq!(taps.get(), 0);
8356 tree.assert_no_leaked_pointer_state();
8357 }
8358
8359 #[test]
8362 fn a_second_contact_cannot_dismiss_what_the_first_is_manipulating() {
8363 let (mut tree, _page, _taps) = page_with_a_menu();
8364 let first = contact_id(5);
8365 let second = contact_id(6);
8366
8367 tree.dispatch_pointer(touch(first, PointerPhase::Down, inside_menu()));
8368 assert!(
8369 tree.pointers
8370 .get(first)
8371 .is_some_and(|e| e.sequence.is_some()),
8372 "the first contact holds a live press inside the menu"
8373 );
8374
8375 tree.dispatch_pointer(touch(second, PointerPhase::Down, outside()));
8376 tree.dispatch_pointer(touch(second, PointerPhase::Up, outside()));
8377 assert_eq!(
8378 tree.active_overlays().len(),
8379 1,
8380 "the menu the first finger is holding must not close under it"
8381 );
8382
8383 tree.dispatch_pointer(touch(first, PointerPhase::Up, inside_menu()));
8386 let third = contact_id(7);
8387 tree.dispatch_pointer(touch(third, PointerPhase::Down, outside()));
8388 tree.dispatch_pointer(touch(third, PointerPhase::Up, outside()));
8389 assert!(tree.active_overlays().is_empty());
8390 tree.assert_no_leaked_pointer_state();
8391 }
8392
8393 #[test]
8397 fn an_arm_is_not_itself_a_press_that_blocks_another_contact() {
8398 let (mut tree, _page, _taps) = page_with_a_menu();
8399 let first = contact_id(8);
8400 let second = contact_id(9);
8401
8402 tree.dispatch_pointer(touch(first, PointerPhase::Down, outside()));
8403 assert!(
8404 tree.overlay_manager().has_armed_dismiss(first),
8405 "the first contact armed"
8406 );
8407 assert!(tree.busy_press_points(second).is_empty());
8409
8410 tree.dispatch_pointer(touch(second, PointerPhase::Down, outside()));
8411 assert!(tree.overlay_manager().has_armed_dismiss(second));
8412 tree.dispatch_pointer(touch(second, PointerPhase::Up, outside()));
8413 assert!(tree.active_overlays().is_empty());
8414
8415 tree.dispatch_pointer(touch(first, PointerPhase::Up, outside()));
8418 tree.assert_no_leaked_pointer_state();
8419 }
8420
8421 #[test]
8424 fn a_touch_inside_the_menu_arms_nothing() {
8425 let (mut tree, _page, _taps) = page_with_a_menu();
8426 let finger = contact_id(10);
8427 tree.dispatch_pointer(touch(finger, PointerPhase::Down, inside_menu()));
8428 assert!(!tree.overlay_manager().has_armed_dismiss(finger));
8429 tree.dispatch_pointer(touch(finger, PointerPhase::Up, inside_menu()));
8430 assert_eq!(tree.active_overlays().len(), 1);
8431 tree.assert_no_leaked_pointer_state();
8432 }
8433
8434 #[test]
8444 fn a_contact_with_no_press_still_ends_when_the_platform_revokes_it() {
8445 let mut tree = WidgetTree::new();
8446 tree.layout(SizeProposal::exact(800.0, 600.0));
8447 let finger = contact_id(99);
8448 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8449 tree.dispatch_pointer(touch(finger, PointerPhase::Cancel, outside()));
8450 tree.assert_no_leaked_pointer_state();
8451 }
8452
8453 #[test]
8460 fn a_mouse_click_outside_a_menu_dismisses_on_the_press_and_falls_through() {
8461 let (mut tree, _page, taps) = page_with_a_menu();
8462 tree.dispatch_event(WidgetEvent::pointer_down(
8463 outside(),
8464 PointerButton::Primary,
8465 Modifiers::NONE,
8466 ));
8467 assert!(
8468 tree.active_overlays().is_empty(),
8469 "the mouse still dismisses on the press"
8470 );
8471 assert!(
8472 tree.pointers
8473 .get(PointerId::MOUSE)
8474 .is_some_and(|e| e.sequence.is_some()),
8475 "and the press still reaches the page beneath"
8476 );
8477 tree.dispatch_event(WidgetEvent::pointer_up(
8478 outside(),
8479 PointerButton::Primary,
8480 Modifiers::NONE,
8481 ));
8482 assert_eq!(taps.get(), 1, "the control beneath activated");
8483 assert!(
8484 !tree.overlay_manager().has_armed_dismiss(PointerId::MOUSE),
8485 "a mouse never arms"
8486 );
8487 tree.assert_no_leaked_pointer_state();
8488 }
8489
8490 #[test]
8497 fn a_context_menu_avoids_the_contact_that_opened_it() {
8498 fn menu_bounds(pointer: PointerInfo, at: Point) -> Rect {
8499 let mut tree = WidgetTree::new();
8500 let page = tree.add(
8501 FillWidget::new()
8502 .focusable()
8503 .context_menu(|_p, _ctx| Some(Box::new(Panel(Size::new(200.0, 160.0))))),
8504 );
8505 let _ = page;
8506 tree.layout(SizeProposal::exact(800.0, 600.0));
8507 tree.dispatch_pointer(PointerSample {
8508 pointer,
8509 phase: PointerPhase::Down,
8510 position: at,
8511 button: Some(PointerButton::Secondary),
8512 modifiers: Modifiers::NONE,
8513 coalesced: Vec::new(),
8514 });
8515 tree.layout(SizeProposal::exact(800.0, 600.0));
8516 let id = *tree
8517 .active_overlays()
8518 .first()
8519 .expect("the context menu opened");
8520 tree.overlay_manager().bounds_for(id).expect("bounds")
8521 }
8522
8523 let at = Point::new(400.0, 300.0);
8524 let finger = menu_bounds(PointerInfo::touch(contact_id(11), EventTime::ZERO), at);
8525 let contact = crate::overlay::rect_centred_on(at, crate::overlay::ASSUMED_CONTACT_PATCH);
8526 assert!(
8527 finger.x < contact.x && finger.right() <= contact.x,
8528 "a touch menu clears the contact patch: {finger:?} vs {contact:?}"
8529 );
8530
8531 let mouse = menu_bounds(PointerInfo::mouse(EventTime::ZERO), at);
8532 assert_eq!(
8533 (mouse.x, mouse.y),
8534 (at.x, at.y),
8535 "a mouse menu still opens with its corner on the pointer"
8536 );
8537 }
8538}
8539
8540#[cfg(test)]
8556mod pass_through_overlay_tests {
8557 use teksilo_canvas::{Point, Rect, SizeProposal};
8558
8559 use crate::WidgetId;
8560 use crate::overlay::{DismissBehavior, OverlayPlacement, OverlayRequest};
8561 use crate::test_widgets::FillWidget;
8562 use crate::widget::{LayoutContext, LayoutResponse, Widget};
8563 use crate::widget_builder::WidgetBuilder;
8564 use crate::widget_tree::WidgetTree;
8565
8566 #[derive(Debug)]
8569 struct HandleLayer {
8570 handle: WidgetId,
8571 at: Rect,
8572 }
8573
8574 impl Widget for HandleLayer {
8575 fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
8576 proposal.resolve(0.0, 0.0).into()
8577 }
8578
8579 fn place_children(
8580 &self,
8581 _bounds: Rect,
8582 _proposal: SizeProposal,
8583 children: &mut [crate::widget::WidgetPlacement],
8584 _ctx: &LayoutContext,
8585 ) {
8586 if let Some(child) = children.get_mut(0) {
8587 child.origin = self.at.origin();
8588 child.size = self.at.size();
8589 }
8590 }
8591
8592 fn children(&self) -> Vec<WidgetId> {
8593 vec![self.handle]
8594 }
8595 }
8596
8597 const HANDLE: Rect = Rect {
8598 x: 10.0,
8599 y: 10.0,
8600 width: 20.0,
8601 height: 20.0,
8602 };
8603
8604 #[derive(Clone, Copy, PartialEq)]
8606 enum RootFlag {
8607 PassThrough,
8609 HitTransparent,
8612 }
8613
8614 fn field_under_a_layer(flag: RootFlag) -> (WidgetTree, WidgetId, WidgetId) {
8620 let mut tree = WidgetTree::new();
8621 let field = tree.add(FillWidget::new());
8622 let handle = tree.add(FillWidget::new());
8623 let layer = HandleLayer { handle, at: HANDLE };
8624 let layer = match flag {
8625 RootFlag::PassThrough => tree.add(layer.event_pass_through(true)),
8626 RootFlag::HitTransparent => tree.add(layer.hit_transparent(true)),
8627 };
8628 tree.show_overlay(OverlayRequest::new(
8629 layer,
8630 field,
8631 OverlayPlacement::FullViewport,
8632 DismissBehavior::Manual,
8633 ));
8634 tree.layout(SizeProposal::exact(400.0, 200.0));
8635 assert_eq!(tree.active_overlays().len(), 1, "the layer is up");
8636 (tree, field, handle)
8637 }
8638
8639 #[test]
8641 fn a_pass_through_layer_hands_back_what_it_did_not_claim() {
8642 let (tree, field, handle) = field_under_a_layer(RootFlag::PassThrough);
8643
8644 assert_eq!(
8645 tree.hit_test(Point::new(15.0, 15.0)),
8646 Some(handle),
8647 "the layer must still win the point its own child covers"
8648 );
8649 assert_eq!(
8650 tree.hit_test(Point::new(200.0, 100.0)),
8651 Some(field),
8652 "a press the layer did not claim must reach the surface beneath it"
8653 );
8654 }
8655
8656 #[test]
8667 fn a_pass_through_root_still_hears_a_press_that_landed_on_its_child() {
8668 use std::cell::Cell;
8669 use std::rc::Rc;
8670
8671 let seen = Rc::new(Cell::new(0u32));
8672 let counter = seen.clone();
8673 let mut tree = WidgetTree::new();
8674 let field = tree.add(FillWidget::new());
8675 let handle = tree.add(FillWidget::new().on_tap(|_e, _c| {}));
8676 let layer = tree.add(
8677 HandleLayer { handle, at: HANDLE }
8678 .event_pass_through(true)
8679 .on_pointer_event(move |_event, _ctx| {
8680 counter.set(counter.get() + 1);
8681 crate::event::EventResponse::Ignored
8682 }),
8683 );
8684 tree.show_overlay(OverlayRequest::new(
8685 layer,
8686 field,
8687 OverlayPlacement::FullViewport,
8688 DismissBehavior::Manual,
8689 ));
8690 tree.layout(SizeProposal::exact(400.0, 200.0));
8691
8692 tree.pointer_down_button(Point::new(15.0, 15.0), crate::event::PointerButton::Primary);
8693 assert!(
8694 seen.get() > 0,
8695 "a pass-through root heard nothing about a press on its own child"
8696 );
8697
8698 let after_child = seen.get();
8699 tree.pointer_up_button(Point::new(15.0, 15.0), crate::event::PointerButton::Primary);
8700 tree.pointer_down_button(
8703 Point::new(200.0, 100.0),
8704 crate::event::PointerButton::Primary,
8705 );
8706 assert_eq!(
8707 seen.get(),
8708 after_child + 1,
8709 "the press-and-lift on the child accounts for the count, and the \
8710 press on the surface beneath added nothing"
8711 );
8712 }
8713
8714 #[test]
8728 fn a_hit_transparent_root_does_not_get_the_fall_through() {
8729 let (tree, field, _handle) = field_under_a_layer(RootFlag::HitTransparent);
8730
8731 assert_eq!(
8733 tree.hit_test(Point::new(15.0, 15.0)),
8734 None,
8735 "hit_transparent must exclude the subtree, or this fixture is not \
8736 testing the gate"
8737 );
8738 assert_eq!(
8739 tree.hit_test(Point::new(200.0, 100.0)),
8740 None,
8741 "the fall-through reached past an overlay that did not ask for it"
8742 );
8743 let _ = field;
8744 }
8745}