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 = {
581 let ids: Vec<_> = self
582 .overlay_manager
583 .stack
584 .iter()
585 .filter(|o| o.band == crate::overlay::OverlayBand::Standard)
586 .map(|o| o.id)
587 .collect();
588 ids.into_iter()
589 .filter(|&id| !self.overlay_is_host_surface(id))
590 .count()
591 };
592 let top_id = self.overlay_manager.stack.last().map(|o| o.id);
598 let top_is_back_navigable = top_id.is_some_and(|id| !self.overlay_is_host_surface(id));
599 if nested_menu_overlays > 1 && top_is_back_navigable {
600 if let Some((_id, content_ids, focus_restore)) = self
601 .overlay_manager
602 .dismiss_top_because(crate::overlay::DismissReason::Escape)
603 {
604 self.dormant_dismissed_content(&content_ids, &mut *ops);
605 if let Some(restore_id) = focus_restore
606 && self.arena.is_active(restore_id)
607 {
608 self.focus_ops(restore_id, &mut *ops);
609 }
610 }
611 return;
612 }
613 }
614
615 if let WidgetEvent::KeyDown {
621 key: Key::Escape, ..
622 } = &event
623 {
624 self.tooltip_escape_pressed();
625 }
626
627 if let WidgetEvent::KeyDown {
628 key: Key::Escape, ..
629 } = &event
630 && !self.overlay_manager.is_empty()
631 && let Some((_id, content_ids, focus_restore)) =
632 self.overlay_manager.try_dismiss_top_on_escape()
633 {
634 self.dormant_dismissed_content(&content_ids, &mut *ops);
635 if let Some(restore_id) = focus_restore
636 && self.arena.is_active(restore_id)
637 {
638 self.focus_ops(restore_id, &mut *ops);
639 }
640 return;
641 }
642
643 match &event {
648 WidgetEvent::PointerDown {
649 position, button, ..
650 } => {
651 if self.arm_outside_press_dismissal(*position, *button, &mut *ops) {
652 return;
653 }
654 }
655 WidgetEvent::PointerUp { position, .. } => {
656 if self.commit_outside_press_dismissal(*position, &mut *ops) {
657 return;
658 }
659 }
660 WidgetEvent::PointerCancel { pointer, .. } => {
661 self.overlay_manager.abort_dismiss(pointer.id);
665 }
666 _ => {}
667 }
668
669 if let WidgetEvent::KeyDown { key, modifiers, .. } = &event
680 && let Some(callback) = self.take_key_capture()
681 {
682 let keystroke = crate::shortcut::KeyStroke::new(*key, *modifiers);
683 let mut cap_ctx = self.make_event_context(&mut *ops);
684 callback(keystroke, self.shortcut_registry_mut(), &mut cap_ctx);
685 let anchor = self.focused.or_else(|| self.arena.roots().first().copied());
688 if let Some(anchor_id) = anchor {
689 self.collect_from_ctx(cap_ctx, anchor_id);
690 self.drain_pending_intents(&mut *ops);
691 }
692 return;
693 }
694
695 let focus_captures_keys = matches!(&event, WidgetEvent::KeyDown { .. })
703 && self.focused.is_some_and(|f| self.is_keyboard_capture(f));
704
705 if !focus_captures_keys && let WidgetEvent::KeyDown { key, modifiers, .. } = &event {
720 let keystroke = crate::shortcut::KeyStroke::new(*key, *modifiers);
721 let candidates: Vec<(&'static str, crate::shortcut::ShortcutScope, bool)> = self
730 .shortcut_registry
731 .matches_by_keystroke(keystroke)
732 .map(|eff| {
733 (
734 eff.shortcut.id,
735 eff.shortcut.scope,
736 eff.shortcut.propagate_when_disabled,
737 )
738 })
739 .collect();
740 let lookup = self.select_shortcut_for_focus(&candidates);
741 if let Some((id, scope, propagate_when_disabled)) = lookup {
742 let anchor = match scope {
743 crate::shortcut::ShortcutScope::Global => {
748 self.focused.or_else(|| self.arena.roots().first().copied())
749 }
750 crate::shortcut::ShortcutScope::Scoped(scope_id) => {
751 self.focused.filter(|f| self.is_descendant_of(*f, scope_id))
752 }
753 };
754 if let Some(anchor_id) = anchor {
755 let mut act_ctx = self.make_event_context(&mut *ops);
756 if let Some(intent) =
757 self.shortcut_registry
758 .invoke_on_activate(id, keystroke, &mut act_ctx)
759 {
760 self.collect_from_ctx(act_ctx, anchor_id);
761 let intent = intent.with_source(crate::telemetry::IntentSource::Shortcut);
765 self.enqueue_intent(anchor_id, intent, propagate_when_disabled);
766 self.drain_pending_intents(&mut *ops);
767 return;
768 }
769 }
770 }
775 }
778
779 if self.outbound_drag_source.is_some()
785 && let WidgetEvent::KeyDown {
786 key: Key::Escape, ..
787 } = &event
788 {
789 ops.cancel_os_drag();
790 }
795
796 if self.active_drag.is_some() {
798 match &event {
799 WidgetEvent::PointerMove { position, .. } => {
800 self.handle_drag_move(*position, &mut *ops);
801 return;
802 }
803 WidgetEvent::PointerUp { position, .. } => {
804 self.handle_drag_drop(*position, &mut *ops);
805 return;
806 }
807 WidgetEvent::KeyDown {
808 key: Key::Escape, ..
809 } => {
810 self.cancel_active_drag(&mut *ops);
811 return;
812 }
813 WidgetEvent::Scroll { .. } => {
814 let target_and_pos = self
820 .active_drag
821 .as_ref()
822 .and_then(|d| d.current_target.map(|t| (t, d.current_position)));
823 if let Some((target, _pos)) = target_and_pos {
824 self.dispatch_to_widget(target, &event, &mut *ops);
825 }
826 if let Some((_, pos)) = target_and_pos
827 && self.active_drag.is_some()
828 {
829 self.handle_drag_move(pos, &mut *ops);
830 }
831 return;
832 }
833 _ => {}
834 }
835 }
836
837 if let WidgetEvent::KeyDown { key, modifiers, .. } = &event
846 && is_context_menu_chord(*key, *modifiers)
847 && self.open_context_menu_from_keyboard(&mut *ops)
848 {
849 return;
850 }
851
852 match &event {
853 WidgetEvent::PointerMove { position, .. } => {
854 self.note_sequence_position(*position);
858 self.revalidate_sequence(&mut *ops);
859 self.tick_sequence_timers();
862 self.note_palm_sample(*position);
867 self.feed_pinch(super::pan_arbiter::PinchFeed::Move, *position, &mut *ops);
868 if let Some(captured) = self.current_pointer_capture() {
869 self.dispatch_to_widget(captured, &event, &mut *ops);
870 if self.active_drag.is_none() {
875 self.advance_sequence(&event, &mut *ops);
876 }
877 } else {
878 self.handle_pointer_move(&event, *position, &mut *ops);
879 if self.active_drag.is_none() {
884 self.advance_sequence(&event, &mut *ops);
885 }
886 }
887 self.advance_pan(*position, &mut *ops);
890 self.update_press(*position);
893 self.touch_route_moved(self.current_pointer_id());
896 if self.pointers.hover_owner_id() == Some(self.current_pointer_id()) {
906 self.update_pointer_leave_overlays(*position, &mut *ops);
907 }
908 }
909 WidgetEvent::PointerDown {
910 position, button, ..
911 } => {
912 let pressed = self.current_pointer_id();
915 self.cancelled_pointers.retain(|p| *p != pressed);
916 self.tooltip_pointer_press(Some(*position));
923 let pressing = self.current_input.pointer;
927 if let Some(target) = self.hit_test_for(*position, &pressing) {
928 if *button == PointerButton::Secondary
929 && self.show_context_menu_for(
930 target,
931 *position,
932 crate::widget_builder::ContextMenuTrigger::Pointer,
933 &mut *ops,
934 )
935 {
936 return;
937 }
938 self.begin_sequence(target, *position);
944 let modifiers = match &event {
949 WidgetEvent::PointerDown { modifiers, .. } => *modifiers,
950 _ => crate::event::Modifiers::NONE,
951 };
952 self.begin_pan(target, *position, modifiers);
953 self.begin_palm_watch(*position);
954 self.feed_pinch(super::pan_arbiter::PinchFeed::Down, *position, &mut *ops);
955 let focusable = self.find_focusable_at_or_above(target);
959 self.begin_press(focusable);
960 if !pressing.kind.is_direct()
967 && let Some(focusable) = focusable
968 {
969 self.focus_with_origin_ops(
970 focusable,
971 crate::focus::FocusOrigin::Pointer(pressing.kind),
972 &mut *ops,
973 );
974 }
975 self.dispatch_to_widget(target, &event, &mut *ops);
976 if self.active_drag.is_none() {
981 self.enrol_sequence_members(&event, &mut *ops);
982 }
983 self.adopt_press_owner(*button);
987 self.arm_touch_route(target, *position);
992 }
993 }
994 WidgetEvent::PointerUp { position, .. } => {
995 let released = self.current_pointer_id();
1001 self.cancel_touch_route(released);
1005 if let Some(index) = self.cancelled_pointers.iter().position(|p| *p == released) {
1006 self.cancelled_pointers.swap_remove(index);
1007 crate::trace_input!(
1008 Samples,
1009 "swallowing the Up for {released:?}: its press was cancelled"
1010 );
1011 return;
1012 }
1013 self.feed_pinch(super::pan_arbiter::PinchFeed::Up, *position, &mut *ops);
1020 if self.take_palm_verdict(released) {
1021 crate::trace_input!(
1022 Samples,
1023 "{released:?} released as a palm: large, and it never moved"
1024 );
1025 self.cancel_pointer(
1026 released,
1027 crate::pointer::CancelReason::PalmRejected,
1028 &mut *ops,
1029 );
1030 return;
1031 }
1032 self.end_pan(*position, &mut *ops);
1035 self.note_sequence_position(*position);
1042 self.end_sequence(&event, &mut *ops);
1043 self.focus_on_release(*position, &mut *ops);
1047 if let Some(captured) = self.current_pointer_capture() {
1048 self.dispatch_to_widget(captured, &event, &mut *ops);
1049 self.set_current_pointer_capture(None);
1052 } else {
1053 let releasing = self.current_input.pointer;
1054 if let Some(target) = self.hit_test_for(*position, &releasing) {
1055 self.dispatch_to_widget(target, &event, &mut *ops);
1056 }
1057 }
1058 let released = self.current_pointer_id();
1061 self.release_arenas_following(released);
1062 self.end_press(released);
1066 }
1067 WidgetEvent::PointerCancel {
1068 reason, pointer, ..
1069 } => {
1070 let (reason, pointer) = (*reason, pointer.id);
1075 self.cancel_pointer(pointer, reason, &mut *ops);
1076 }
1077 WidgetEvent::Scroll {
1078 window_position: position,
1079 ..
1080 } => {
1081 use super::pan_arbiter::ScrollDelivery;
1082 match ScrollDelivery::for_source(self.current_input.scroll_source) {
1083 ScrollDelivery::ClaimantChain => {
1088 let position = *position;
1089 self.route_scroll_along_chain(&event, position, &mut *ops);
1090 }
1091 ScrollDelivery::Bubble => {
1097 let scrolling = self.current_input.pointer;
1098 let target = match position {
1099 Some(p) => self.hit_test_for(*p, &scrolling),
1100 None => self.hovered_id().or(self.focused),
1101 };
1102 if let Some(target) = target {
1103 self.dispatch_to_widget(target, &event, &mut *ops);
1104 }
1105 }
1106 }
1107 }
1108 WidgetEvent::KeyDown { key, modifiers, .. } => {
1109 if *key == Key::Tab {
1110 let captured_focus = self
1125 .focused
1126 .is_some_and(|focused| self.is_keyboard_capture(focused));
1127 if captured_focus && modifiers.ctrl() {
1128 self.cycle_focus(modifiers.shift(), &mut *ops);
1129 return;
1130 }
1131 let handled = self
1138 .focused
1139 .map(|focused| {
1140 self.dispatch_to_widget_returning_handled(focused, &event, &mut *ops)
1141 })
1142 .unwrap_or(false);
1143 if !handled {
1144 self.cycle_focus(modifiers.shift(), &mut *ops);
1145 }
1146 } else if let Some(focused) = self.focused {
1147 self.dispatch_to_widget(focused, &event, &mut *ops);
1148 }
1149 }
1150 WidgetEvent::KeyUp { .. }
1151 | WidgetEvent::ImeComposition { .. }
1152 | WidgetEvent::ImeCommit { .. } => {
1153 if let Some(focused) = self.focused {
1154 self.dispatch_to_widget(focused, &event, &mut *ops);
1155 }
1156 }
1157 WidgetEvent::AccessAction { target, action, .. } => {
1158 if let Some(id) =
1176 target.filter(|id| self.arena.is_active(*id) && !self.is_behind_modal(*id))
1177 {
1178 if *action == accesskit::Action::Focus {
1179 if self.advertises_focus_action(id) {
1208 let target = self
1209 .focusable_composite_behind(id)
1210 .or_else(|| self.first_focusable_descendant(id))
1211 .unwrap_or(id);
1212 self.focus_with_origin_ops(
1218 target,
1219 crate::focus::FocusOrigin::Accessibility,
1220 &mut *ops,
1221 );
1222 self.access_action_handled = self.focused == Some(target);
1225 } else {
1226 self.access_action_handled = false;
1227 }
1228 } else if *action == accesskit::Action::ShowContextMenu {
1229 self.access_action_handled =
1245 if self.dispatch_to_widget_returning_handled(id, &event, &mut *ops) {
1246 true
1247 } else {
1248 let position = self.arena.bounds(id).center();
1249 self.show_context_menu_for(
1250 id,
1251 position,
1252 crate::widget_builder::ContextMenuTrigger::Accessibility,
1253 &mut *ops,
1254 )
1255 };
1256 } else {
1257 self.access_action_handled =
1258 self.dispatch_to_widget_returning_handled(id, &event, &mut *ops);
1259 }
1260 }
1261 }
1262 WidgetEvent::Gesture { .. } => {
1263 if let Some(target) = self.hovered_id().or(self.focused) {
1264 self.dispatch_to_widget(target, &event, &mut *ops);
1265 }
1266 }
1267 WidgetEvent::ScrollIntoView { .. }
1268 | WidgetEvent::PointerEnter { .. }
1269 | WidgetEvent::PointerLeave { .. }
1270 | WidgetEvent::FocusGained { .. }
1271 | WidgetEvent::FocusLost => {}
1272 }
1273 self.drain_pending_intents(&mut *ops);
1278 }
1279
1280 fn open_context_menu_from_keyboard(&mut self, ops: &mut dyn crate::window::WindowOps) -> bool {
1293 let Some(focused) = self.focused else {
1294 return false;
1295 };
1296 let target = self
1297 .arena
1298 .get(focused)
1299 .and_then(|node| node.widget.context_menu_key_target())
1300 .filter(|id| self.arena.is_active(*id))
1301 .unwrap_or(focused);
1302
1303 let bounds = self.bounds(target);
1307 let anchor = Point {
1308 x: bounds.x + bounds.width / 2.0,
1309 y: bounds.y + bounds.height / 2.0,
1310 };
1311 self.show_context_menu_for(
1312 target,
1313 anchor,
1314 crate::widget_builder::ContextMenuTrigger::Keyboard,
1315 ops,
1316 )
1317 }
1318
1319 pub(super) fn show_context_menu_for(
1328 &mut self,
1329 target: WidgetId,
1330 position: Point,
1331 trigger: crate::widget_builder::ContextMenuTrigger,
1332 ops: &mut dyn crate::window::WindowOps,
1333 ) -> bool {
1334 let mut ctx = self.make_event_context(&mut *ops);
1340 ctx.context_menu_trigger = Some(trigger);
1341 let mut walker = Some(target);
1342 let menu_decision: Option<(WidgetId, Box<dyn Widget>)> = loop {
1343 let owner_id = {
1346 let mut probe = walker;
1347 loop {
1348 match probe {
1349 None => break None,
1350 Some(id) => {
1351 if self
1352 .arena
1353 .get(id)
1354 .is_some_and(|node| node.context_menu_factory.is_some())
1355 {
1356 break Some(id);
1357 }
1358 probe = self.arena.get(id).and_then(|node| node.parent);
1359 }
1360 }
1361 }
1362 };
1363 let Some(owner_id) = owner_id else {
1364 break None;
1365 };
1366 let outcome: Option<Box<dyn Widget>> = {
1373 let node = self
1374 .arena
1375 .get(owner_id)
1376 .expect("owner_id from active arena walk");
1377 let factory = node
1378 .context_menu_factory
1379 .as_ref()
1380 .expect("owner_id only set when factory present");
1381 factory(position, &mut ctx)
1382 };
1383 match outcome {
1384 Some(menu) => break Some((owner_id, menu)),
1385 None => {
1386 walker = self.arena.get(owner_id).and_then(|n| n.parent);
1388 }
1389 }
1390 };
1391
1392 let drain_anchor = menu_decision
1396 .as_ref()
1397 .map(|(id, _)| *id)
1398 .or_else(|| self.arena.roots().first().copied())
1399 .unwrap_or(target);
1400 self.collect_from_ctx(ctx, drain_anchor);
1401
1402 let Some((owner_id, menu_widget)) = menu_decision else {
1403 return false;
1404 };
1405
1406 let keep: std::collections::HashSet<WidgetId> = self
1412 .overlay_manager
1413 .stack
1414 .iter()
1415 .map(|o| o.content_id)
1416 .filter(|&content_id| self.is_descendant_of(owner_id, content_id))
1417 .collect();
1418 let dismissed = self.overlay_manager.dismiss_except(&keep);
1419 self.dormant_dismissed_content(&dismissed, &mut *ops);
1420
1421 let content_id = self.add_boxed(menu_widget);
1422 let prev_focus = self.focused;
1423 let placement =
1430 crate::overlay::OverlayPlacement::at_pointer_for(position, &self.current_input.pointer);
1431 self.overlay_manager.show(crate::overlay::OverlayRequest {
1432 content_id,
1433 anchor: owner_id,
1434 placement,
1435 dismiss: crate::overlay::DismissBehavior::EscapeOrClickOutside,
1436 layer: crate::overlay::OverlayLayer::InTree,
1437 parent_overlay: None,
1438 on_dismiss: None,
1439 fade_duration: None,
1440 });
1441 if let Some(focus_id) = prev_focus {
1442 self.overlay_manager.set_top_focus_restore(focus_id);
1443 }
1444 self.focus_ops(content_id, &mut *ops);
1445 self.drain_pending_intents(&mut *ops);
1450 true
1451 }
1452
1453 fn arm_outside_press_dismissal(
1474 &mut self,
1475 position: Point,
1476 button: PointerButton,
1477 ops: &mut dyn crate::window::WindowOps,
1478 ) -> bool {
1479 self.prune_stale_dismiss_arms();
1480 let pressing = self.current_input.pointer;
1481 let busy = self.busy_press_points(pressing.id);
1482
1483 if pressing.kind.is_direct() {
1484 return self
1485 .overlay_manager
1486 .arm_dismiss(pressing.id, position, &busy)
1487 .suppress_beneath;
1488 }
1489
1490 let (dismissed, focus_restore, toggle_anchors) =
1491 self.overlay_manager.dismiss_outside_press(position, &busy);
1492 if dismissed.is_empty() {
1493 return false;
1494 }
1495 self.dormant_dismissed_content(&dismissed, &mut *ops);
1496 if let Some(restore_id) = focus_restore
1497 && self.arena.is_active(restore_id)
1498 {
1499 self.focus_ops(restore_id, &mut *ops);
1500 }
1501 button == PointerButton::Primary
1511 && toggle_anchors.iter().any(|&anchor| {
1512 self.arena.is_active(anchor) && self.arena.bounds(anchor).contains(position)
1513 })
1514 }
1515
1516 fn commit_outside_press_dismissal(
1523 &mut self,
1524 position: Point,
1525 ops: &mut dyn crate::window::WindowOps,
1526 ) -> bool {
1527 let pointer = self.current_pointer_id();
1528 if !self.overlay_manager.has_armed_dismiss(pointer) {
1529 return false;
1530 }
1531 let (dismissed, focus_restore, _anchors) =
1532 self.overlay_manager.commit_dismiss(pointer, position);
1533 if !dismissed.is_empty() {
1534 self.dormant_dismissed_content(&dismissed, &mut *ops);
1535 if let Some(restore_id) = focus_restore
1536 && self.arena.is_active(restore_id)
1537 {
1538 self.focus_ops(restore_id, &mut *ops);
1539 }
1540 }
1541 true
1542 }
1543
1544 fn busy_press_points(&self, exclude: crate::pointer::PointerId) -> Vec<Point> {
1561 self.pointers
1562 .iter()
1563 .filter(|entry| entry.info.id != exclude)
1564 .filter(|entry| self.press_is_revocable(entry.info.id))
1565 .map(|entry| entry.down_position)
1566 .collect()
1567 }
1568
1569 fn prune_stale_dismiss_arms(&mut self) {
1576 let stale: Vec<crate::pointer::PointerId> = self
1577 .overlay_manager
1578 .armed_pointers()
1579 .into_iter()
1580 .filter(|id| self.pointers.get(*id).is_none())
1581 .collect();
1582 for id in stale {
1583 self.overlay_manager.abort_dismiss(id);
1584 }
1585 }
1586
1587 fn begin_press(&mut self, focusable: Option<WidgetId>) {
1608 let pointer = self.current_pointer_id();
1609 let now = self.sequence_now();
1610 let delay = self
1611 .pan_session_open(pointer)
1612 .then(|| self.current_profile().press_feedback_delay)
1613 .filter(|d| !d.is_zero());
1614 self.presses.press(pointer, focusable, now, delay);
1615 }
1616
1617 fn adopt_press_owner(&mut self, button: PointerButton) {
1640 let pointer = self.current_pointer_id();
1641 let Some(owner) = self.current_sequence().and_then(|s| s.pressed_owner()) else {
1642 return;
1643 };
1644 if !self.press_buttons(owner).contains(button) {
1645 crate::trace_input!(
1646 Samples,
1647 "no press visual for {owner:?}: {button:?} is not one of its accepted buttons"
1648 );
1649 return;
1650 }
1651 self.presses.set_owner(pointer, owner);
1652 self.publish_pressed(owner);
1653 }
1654
1655 fn update_press(&mut self, position: Point) {
1668 let pointer = self.current_pointer_id();
1669 if self.presses.get(pointer).is_none() {
1670 return;
1671 }
1672 let claimed_elsewhere = self.current_sequence().is_some_and(|sequence| {
1675 sequence
1676 .winner()
1677 .is_some_and(|winner| Some(winner) != sequence.pressed_owner())
1678 });
1679 if claimed_elsewhere {
1680 self.end_press(pointer);
1681 return;
1682 }
1683 let profile = self.current_profile();
1684 let origin = self.current_sequence().map(|s| s.press_origin());
1685 let owner = self.presses.get(pointer).and_then(|p| p.owner);
1686 let inside = match (origin, owner) {
1687 (Some(origin), Some(owner)) => {
1688 let bounds = self
1689 .arena
1690 .is_active(owner)
1691 .then(|| self.arena.bounds(owner));
1692 !crate::gesture::TapBoundary::for_pointer(&self.current_input.pointer, &profile)
1693 .left(origin, position, bounds, &profile)
1694 }
1695 _ => true,
1698 };
1699 let Some(press) = self.presses.get_mut(pointer) else {
1700 return;
1701 };
1702 if press.inside == inside {
1703 return;
1704 }
1705 press.inside = inside;
1706 if let Some(owner) = owner {
1707 self.publish_pressed(owner);
1708 }
1709 }
1710
1711 pub(crate) fn end_press(&mut self, pointer: crate::pointer::PointerId) {
1717 if let Some(owner) = self.presses.release(pointer) {
1718 self.publish_pressed(owner);
1719 }
1720 }
1721
1722 pub(crate) fn resolve_press_delays(&mut self, now: crate::pointer::EventTime) {
1725 if self.presses.is_empty() {
1726 return;
1727 }
1728 for id in self.presses.resolve_delays(now) {
1729 self.publish_pressed(id);
1730 self.arena.mark_needs_paint(id);
1731 }
1732 }
1733
1734 pub(crate) fn next_press_deadline(&self) -> Option<std::time::Instant> {
1737 self.presses.next_deadline().map(|t| self.instant_for(t))
1738 }
1739
1740 fn focus_on_release(&mut self, position: Point, ops: &mut dyn crate::window::WindowOps) {
1751 let releasing = self.current_input.pointer;
1752 if !releasing.kind.is_direct() {
1753 return;
1754 }
1755 let pointer = self.current_pointer_id();
1756 let Some(pressed) = self.presses.get(pointer).and_then(|p| p.focusable) else {
1757 return;
1758 };
1759 let released_on = self
1760 .hit_test_for(position, &releasing)
1761 .and_then(|target| self.find_focusable_at_or_above(target));
1762 if released_on == Some(pressed) {
1763 self.focus_with_origin_ops(
1764 pressed,
1765 crate::focus::FocusOrigin::Pointer(releasing.kind),
1766 &mut *ops,
1767 );
1768 }
1769 }
1770
1771 fn handle_pointer_move(
1791 &mut self,
1792 event: &WidgetEvent,
1793 position: Point,
1794 ops: &mut dyn crate::window::WindowOps,
1795 ) {
1796 let moving = self.current_input.pointer;
1801 if self.pointers.hover_owner_id() != Some(self.current_pointer_id()) {
1802 if let Some(target) = self.hit_test_for(position, &moving) {
1803 self.dispatch_to_widget(target, event, &mut *ops);
1804 }
1805 return;
1806 }
1807 let target = self.hit_test_for(position, &moving);
1808
1809 if target != self.hovered_id() {
1810 let hover = self.hover_transition_pointer();
1812 let previously_hovered = self.hovered_id();
1813 if let Some(old) = previously_hovered {
1814 self.dispatch_to_widget(
1815 old,
1816 &WidgetEvent::PointerLeave { pointer: hover },
1817 &mut *ops,
1818 );
1819 self.tooltip_pointer_leave(old, &mut *ops);
1820 }
1821 if let Some(new) = target {
1822 self.dispatch_to_widget(
1823 new,
1824 &WidgetEvent::PointerEnter { pointer: hover },
1825 &mut *ops,
1826 );
1827 self.tooltip_pointer_enter(new);
1828 }
1829 self.set_hovered(target);
1830 self.update_hover_within_signals(previously_hovered, target);
1831 } else if let Some(target) = target {
1832 self.tooltip_pointer_moved(target, position);
1835 }
1836
1837 if let Some(target) = target {
1838 self.dispatch_to_widget(target, event, &mut *ops);
1839 }
1840 }
1841
1842 pub(super) fn dispatch_to_widget(
1843 &mut self,
1844 target: WidgetId,
1845 event: &WidgetEvent,
1846 ops: &mut dyn crate::window::WindowOps,
1847 ) {
1848 self.dispatch_to_widget_returning_handled(target, event, ops);
1849 }
1850
1851 pub(super) fn localize_event(&self, id: WidgetId, event: &WidgetEvent) -> Option<WidgetEvent> {
1863 match event {
1864 WidgetEvent::PointerDown {
1865 position,
1866 button,
1867 modifiers,
1868 pointer,
1869 } => Some(WidgetEvent::PointerDown {
1870 position: self.arena.local_pointer_position(id, *position),
1871 button: *button,
1872 modifiers: *modifiers,
1873 pointer: *pointer,
1874 }),
1875 WidgetEvent::PointerUp {
1876 position,
1877 button,
1878 modifiers,
1879 pointer,
1880 } => Some(WidgetEvent::PointerUp {
1881 position: self.arena.local_pointer_position(id, *position),
1882 button: *button,
1883 modifiers: *modifiers,
1884 pointer: *pointer,
1885 }),
1886 WidgetEvent::PointerMove {
1887 position,
1888 modifiers,
1889 pointer,
1890 } => Some(WidgetEvent::PointerMove {
1891 position: self.arena.local_pointer_position(id, *position),
1892 modifiers: *modifiers,
1893 pointer: *pointer,
1894 }),
1895 WidgetEvent::Gesture { gesture } => Some(WidgetEvent::Gesture {
1896 gesture: self.localize_gesture(id, gesture),
1897 }),
1898 _ => None,
1904 }
1905 }
1906
1907 fn localize_gesture(&self, id: WidgetId, gesture: &GestureEvent) -> GestureEvent {
1914 let loc = |p: teksilo_canvas::Point| self.arena.local_pointer_position(id, p);
1915 let tap = |t: &TapEvent| {
1916 TapEvent::new(loc(t.position), t.button, t.modifiers).with_pointer(t.pointer)
1917 };
1918 match gesture {
1919 GestureEvent::Tap(t) => GestureEvent::Tap(tap(t)),
1920 GestureEvent::DoubleTap(t) => GestureEvent::DoubleTap(tap(t)),
1921 GestureEvent::TripleTap(t) => GestureEvent::TripleTap(tap(t)),
1922 GestureEvent::LongPress(t) => GestureEvent::LongPress(tap(t)),
1923 GestureEvent::DragStarted {
1924 position,
1925 button,
1926 pointer,
1927 } => GestureEvent::DragStarted {
1928 position: loc(*position),
1929 button: *button,
1930 pointer: *pointer,
1931 },
1932 GestureEvent::DragMoved {
1933 position,
1934 delta,
1935 pointer,
1936 } => GestureEvent::DragMoved {
1937 position: loc(*position),
1938 delta: *delta,
1939 pointer: *pointer,
1940 },
1941 GestureEvent::DragEnded { position, pointer } => GestureEvent::DragEnded {
1942 position: loc(*position),
1943 pointer: *pointer,
1944 },
1945 GestureEvent::DragCancelled {
1946 position,
1947 pointer,
1948 reason,
1949 } => GestureEvent::DragCancelled {
1950 position: loc(*position),
1951 pointer: *pointer,
1952 reason: *reason,
1953 },
1954 GestureEvent::PinchStarted { center } => GestureEvent::PinchStarted {
1955 center: loc(*center),
1956 },
1957 GestureEvent::PinchChanged {
1958 center,
1959 scale,
1960 rotation,
1961 } => GestureEvent::PinchChanged {
1962 center: loc(*center),
1963 scale: *scale,
1964 rotation: *rotation,
1965 },
1966 GestureEvent::PinchEnded => GestureEvent::PinchEnded,
1967 GestureEvent::PinchCancelled { reason } => {
1968 GestureEvent::PinchCancelled { reason: *reason }
1969 }
1970 GestureEvent::Swipe {
1971 direction,
1972 velocity,
1973 } => GestureEvent::Swipe {
1974 direction: *direction,
1975 velocity: *velocity,
1976 },
1977 }
1978 }
1979
1980 pub(super) fn dispatch_to_widget_returning_handled(
1986 &mut self,
1987 target: WidgetId,
1988 event: &WidgetEvent,
1989 ops: &mut dyn crate::window::WindowOps,
1990 ) -> bool {
1991 if !self.arena.is_enabled(target) {
1992 return false;
1993 }
1994
1995 let mut ancestors = Vec::new();
1996 let mut current = self.arena.parent(target);
1997 while let Some(id) = current {
1998 ancestors.push(id);
1999 current = self.arena.parent(id);
2000 }
2001 ancestors.reverse();
2002
2003 let tap_owner: Option<WidgetId> = if matches!(
2021 event,
2022 WidgetEvent::PointerDown { .. } | WidgetEvent::PointerUp { .. }
2023 ) {
2024 let mut owner = None;
2025 let mut cur = Some(target);
2026 while let Some(id) = cur {
2027 if self.arena.get(id).is_some_and(|n| {
2028 n.any_handler(|h| h.on_tap.is_some() || h.on_long_press.is_some())
2029 }) {
2030 owner = Some(id);
2031 break;
2032 }
2033 cur = self.arena.parent(id);
2034 }
2035 owner
2036 } else {
2037 None
2038 };
2039
2040 for &id in &ancestors {
2041 let mut ctx = self
2042 .make_event_context(&mut *ops)
2043 .with_dispatch_node(id)
2044 .with_dispatch_target(target);
2045 ctx.press_claimed_by_interactive_child =
2046 tap_owner.is_some_and(|owner| owner != id && self.is_descendant_of(owner, id));
2047 let localized = self.localize_event(id, event);
2050 let event = localized.as_ref().unwrap_or(event);
2051 let response = if let Some(node) = self.arena.get_mut(id) {
2052 Self::try_handler_preview(node, event, &mut ctx).unwrap_or(EventResponse::Ignored)
2053 } else {
2054 EventResponse::Ignored
2055 };
2056 self.collect_from_ctx(ctx, id);
2057 if response == EventResponse::Handled {
2058 self.arena.mark_needs_paint(id);
2059 if matches!(event, WidgetEvent::PointerDown { .. }) {
2067 self.note_preview_claim(id);
2068 }
2069 return true;
2070 }
2071 }
2072
2073 let needs_layout_on_handle = matches!(
2074 event,
2075 WidgetEvent::Scroll { .. } | WidgetEvent::ScrollIntoView { .. }
2076 );
2077 let mut current = Some(target);
2078 let mut is_target = true;
2079 while let Some(id) = current {
2080 let mut ctx = self
2081 .make_event_context(&mut *ops)
2082 .with_dispatch_node(id)
2083 .with_dispatch_target(target);
2084 ctx.press_claimed_by_interactive_child =
2085 tap_owner.is_some_and(|owner| owner != id && self.is_descendant_of(owner, id));
2086 let localized = self.localize_event(id, event);
2089 let gesture_cx = self.recognizer_context(id);
2090 let arena_blocked = self.sequence_blocks_arena(id);
2093 let WidgetTree {
2094 arena,
2095 gesture_owners,
2096 ..
2097 } = self;
2098 let event = localized.as_ref().unwrap_or(event);
2099 let response = if let Some(node) = arena.get_mut(id) {
2100 Self::try_handler_bubble(
2101 node,
2102 event,
2103 &mut ctx,
2104 BubbleGates {
2105 fire_on_pointer_event: is_target,
2106 arena_blocked,
2107 },
2108 id,
2109 gesture_owners,
2110 gesture_cx,
2111 )
2112 .unwrap_or(EventResponse::Ignored)
2113 } else {
2114 EventResponse::Ignored
2115 };
2116 self.collect_from_ctx(ctx, id);
2117 if response == EventResponse::Handled {
2118 if needs_layout_on_handle {
2119 self.arena.mark_needs_layout(id);
2120 } else {
2121 self.arena.mark_needs_paint(id);
2122 }
2123 self.note_pointer_acceptance(id, event);
2124 if !matches!(
2139 event,
2140 WidgetEvent::PointerEnter { .. } | WidgetEvent::PointerLeave { .. }
2141 ) {
2142 return true;
2143 }
2144 }
2145 is_target = false;
2146 current = self.arena.parent(id);
2147 }
2148 false
2149 }
2150
2151 pub(super) fn dispatch_to_widget_direct(
2152 &mut self,
2153 target: WidgetId,
2154 event: &WidgetEvent,
2155 ops: &mut dyn crate::window::WindowOps,
2156 ) {
2157 self.dispatch_to_widget_direct_returning_handled(target, event, ops);
2158 }
2159
2160 pub(super) fn dispatch_to_widget_direct_returning_handled(
2170 &mut self,
2171 target: WidgetId,
2172 event: &WidgetEvent,
2173 ops: &mut dyn crate::window::WindowOps,
2174 ) -> bool {
2175 if !self.arena.is_enabled(target) {
2176 return false;
2177 }
2178
2179 let mut ctx = self
2180 .make_event_context(&mut *ops)
2181 .with_dispatch_node(target)
2182 .with_dispatch_target(target);
2183 let gesture_cx = self.recognizer_context(target);
2184 let arena_blocked = self.sequence_blocks_arena(target);
2185 let WidgetTree {
2186 arena,
2187 gesture_owners,
2188 ..
2189 } = self;
2190 let response = if let Some(node) = arena.get_mut(target) {
2191 Self::try_handler_bubble(
2192 node,
2193 event,
2194 &mut ctx,
2195 BubbleGates {
2196 fire_on_pointer_event: true,
2197 arena_blocked,
2198 },
2199 target,
2200 gesture_owners,
2201 gesture_cx,
2202 )
2203 .unwrap_or(EventResponse::Ignored)
2204 } else {
2205 EventResponse::Ignored
2206 };
2207 self.collect_from_ctx(ctx, target);
2208
2209 if response == EventResponse::Handled {
2210 if matches!(
2213 event,
2214 WidgetEvent::Scroll { .. } | WidgetEvent::ScrollIntoView { .. }
2215 ) {
2216 self.arena.mark_needs_layout(target);
2217 } else {
2218 self.arena.mark_needs_paint(target);
2219 }
2220 self.note_pointer_acceptance(target, event);
2221 }
2222 response == EventResponse::Handled
2223 }
2224
2225 pub(super) fn note_pointer_acceptance(&mut self, target: WidgetId, event: &WidgetEvent) {
2234 if !matches!(
2235 event,
2236 WidgetEvent::PointerDown { .. }
2237 | WidgetEvent::PointerMove { .. }
2238 | WidgetEvent::PointerUp { .. }
2239 ) {
2240 return;
2241 }
2242 let pointer = self.current_pointer_id();
2243 if let Some(entry) = self.pointers.get_mut(pointer) {
2244 entry.last_accepted = Some(target);
2245 }
2246 }
2247
2248 fn try_handler_preview(
2249 node: &mut crate::arena::WidgetNode,
2250 event: &WidgetEvent,
2251 ctx: &mut EventContext,
2252 ) -> Option<EventResponse> {
2253 match event {
2254 WidgetEvent::KeyDown { .. }
2262 | WidgetEvent::KeyUp { .. }
2263 | WidgetEvent::ImeComposition { .. }
2264 | WidgetEvent::ImeCommit { .. } => {
2265 let has = node.external_handlers.on_key_preview.is_some()
2266 || node.handlers.on_key_preview.is_some();
2267 if !has {
2268 return None;
2269 }
2270 Some(fire_event_handler_both(
2271 &mut node.external_handlers.on_key_preview,
2272 &mut node.handlers.on_key_preview,
2273 event,
2274 ctx,
2275 ))
2276 }
2277 WidgetEvent::PointerEnter { .. } | WidgetEvent::PointerLeave { .. } => None,
2289 _ => {
2290 let has = node.external_handlers.on_pointer_event.is_some()
2291 || node.handlers.on_pointer_event.is_some();
2292 if !has {
2293 return None;
2294 }
2295 Some(fire_event_handler_both(
2296 &mut node.external_handlers.on_pointer_event,
2297 &mut node.handlers.on_pointer_event,
2298 event,
2299 ctx,
2300 ))
2301 }
2302 }
2303 }
2304
2305 fn try_handler_bubble(
2306 node: &mut crate::arena::WidgetNode,
2307 event: &WidgetEvent,
2308 ctx: &mut EventContext,
2309 gates: BubbleGates,
2310 node_id: WidgetId,
2311 gesture_owners: &mut std::collections::HashSet<WidgetId>,
2312 gesture_cx: crate::gesture::RecognizerContext<'_>,
2313 ) -> Option<EventResponse> {
2314 let BubbleGates {
2315 fire_on_pointer_event,
2316 arena_blocked,
2317 } = gates;
2318 match event {
2319 WidgetEvent::PointerEnter { .. } => {
2320 if let Some(cursor) = node.node_cursor {
2321 ctx.declared_cursor_request = Some(cursor);
2326 }
2327 let mut fired = false;
2328 if let Some(h) = node.external_handlers.on_hover.as_mut() {
2329 h(true, ctx);
2330 fired = true;
2331 }
2332 if let Some(h) = node.handlers.on_hover.as_mut() {
2333 h(true, ctx);
2334 fired = true;
2335 }
2336 if fired {
2337 Some(EventResponse::Handled)
2338 } else {
2339 node.node_cursor.map(|_| EventResponse::Handled)
2340 }
2341 }
2342 WidgetEvent::PointerLeave { .. } => {
2343 if node.node_cursor.is_some() {
2344 ctx.declared_cursor_request = Some(crate::widget::CursorIcon::Default);
2345 }
2346 let mut fired = false;
2347 if let Some(h) = node.external_handlers.on_hover.as_mut() {
2348 h(false, ctx);
2349 fired = true;
2350 }
2351 if let Some(h) = node.handlers.on_hover.as_mut() {
2352 h(false, ctx);
2353 fired = true;
2354 }
2355 if fired {
2356 Some(EventResponse::Handled)
2357 } else {
2358 node.node_cursor.map(|_| EventResponse::Handled)
2359 }
2360 }
2361 WidgetEvent::FocusGained { .. } => {
2362 let mut fired = false;
2363 if let Some(h) = node.external_handlers.on_focus.as_mut() {
2364 h(true, ctx);
2365 fired = true;
2366 }
2367 if let Some(h) = node.handlers.on_focus.as_mut() {
2368 h(true, ctx);
2369 fired = true;
2370 }
2371 fired.then_some(EventResponse::Handled)
2372 }
2373 WidgetEvent::FocusLost => {
2374 let mut fired = false;
2375 if let Some(h) = node.external_handlers.on_focus.as_mut() {
2376 h(false, ctx);
2377 fired = true;
2378 }
2379 if let Some(h) = node.handlers.on_focus.as_mut() {
2380 h(false, ctx);
2381 fired = true;
2382 }
2383 fired.then_some(EventResponse::Handled)
2384 }
2385 WidgetEvent::KeyDown { .. }
2386 | WidgetEvent::KeyUp { .. }
2387 | WidgetEvent::ImeComposition { .. }
2388 | WidgetEvent::ImeCommit { .. } => {
2389 if node.external_handlers.on_key.is_some() || node.handlers.on_key.is_some() {
2390 Some(fire_event_handler_both(
2391 &mut node.external_handlers.on_key,
2392 &mut node.handlers.on_key,
2393 event,
2394 ctx,
2395 ))
2396 } else {
2397 None
2398 }
2399 }
2400 WidgetEvent::Scroll { .. } | WidgetEvent::ScrollIntoView { .. } => {
2401 if node.external_handlers.on_scroll.is_some() || node.handlers.on_scroll.is_some() {
2402 Some(fire_event_handler_both(
2403 &mut node.external_handlers.on_scroll,
2404 &mut node.handlers.on_scroll,
2405 event,
2406 ctx,
2407 ))
2408 } else {
2409 None
2410 }
2411 }
2412 WidgetEvent::AccessAction {
2413 action,
2414 target_node,
2415 data,
2416 ..
2417 } => {
2418 let saved_a11y_source = ctx
2438 .current_source
2439 .replace(crate::telemetry::IntentSource::Accessibility);
2440 let mut any_slot = false;
2441 let mut any_handled = false;
2442 if let Some(h) = node.external_handlers.on_access_action_request.as_mut() {
2443 any_slot = true;
2444 any_handled |=
2445 h(*action, *target_node, data.clone(), ctx) == EventResponse::Handled;
2446 }
2447 if let Some(h) = node.handlers.on_access_action_request.as_mut() {
2448 any_slot = true;
2449 any_handled |=
2450 h(*action, *target_node, data.clone(), ctx) == EventResponse::Handled;
2451 }
2452 if let Some(h) = node.external_handlers.on_access_action.as_mut() {
2453 any_slot = true;
2454 any_handled |= h(*action, ctx) == EventResponse::Handled;
2455 }
2456 if let Some(h) = node.handlers.on_access_action.as_mut() {
2457 any_slot = true;
2458 any_handled |= h(*action, ctx) == EventResponse::Handled;
2459 }
2460 let user_handled = any_slot.then_some(if any_handled {
2461 EventResponse::Handled
2462 } else {
2463 EventResponse::Ignored
2464 });
2465
2466 let mut override_handled = false;
2472 if let Some(ov) = node.access_overrides.as_deref_mut() {
2473 if matches!(action, accesskit::Action::CustomAction) {
2474 if let Some(accesskit::ActionData::CustomAction(idx)) = data
2475 && let Some((_, cb)) = ov.custom_actions.get_mut(*idx as usize)
2476 {
2477 cb(ctx);
2478 override_handled = true;
2479 }
2480 } else {
2481 for (a, cb) in ov.actions.iter_mut() {
2482 if *a == *action {
2483 cb(ctx);
2484 override_handled = true;
2485 }
2486 }
2487 }
2488 }
2489
2490 ctx.current_source = saved_a11y_source;
2491 match (user_handled, override_handled) {
2492 (Some(EventResponse::Handled), _) | (_, true) => Some(EventResponse::Handled),
2493 (Some(EventResponse::Ignored), false) => Some(EventResponse::Ignored),
2494 (None, false) => None,
2495 }
2496 }
2497 WidgetEvent::Gesture { gesture } => {
2498 let matched = matches!(
2502 gesture,
2503 GestureEvent::PinchStarted { .. }
2504 | GestureEvent::PinchChanged { .. }
2505 | GestureEvent::PinchEnded
2506 | GestureEvent::Swipe { .. }
2507 | GestureEvent::DoubleTap { .. }
2508 | GestureEvent::TripleTap { .. }
2509 ) && {
2510 let has_handler = match gesture {
2511 GestureEvent::PinchStarted { .. }
2512 | GestureEvent::PinchChanged { .. }
2513 | GestureEvent::PinchEnded => node.any_handler(|h| h.on_pinch.is_some()),
2514 GestureEvent::Swipe { .. } => node.any_handler(|h| h.on_swipe.is_some()),
2515 GestureEvent::DoubleTap { .. } => {
2516 node.any_handler(|h| h.on_double_tap.is_some())
2517 }
2518 GestureEvent::TripleTap { .. } => {
2519 node.any_handler(|h| h.on_triple_tap.is_some())
2520 }
2521 _ => false,
2522 };
2523 if has_handler {
2524 Self::dispatch_recognized_gesture(node, *gesture, ctx);
2525 }
2526 has_handler
2527 };
2528 if matched {
2529 Some(EventResponse::Handled)
2530 } else {
2531 None
2532 }
2533 }
2534 WidgetEvent::PointerDown {
2535 position,
2536 button,
2537 modifiers,
2538 ..
2539 } => {
2540 if fire_on_pointer_event {
2547 let r = fire_event_handler_both(
2548 &mut node.external_handlers.on_pointer_event,
2549 &mut node.handlers.on_pointer_event,
2550 event,
2551 ctx,
2552 );
2553 if r == EventResponse::Handled {
2554 return Some(EventResponse::Handled);
2555 }
2556 }
2557 if arena_blocked {
2558 return None;
2562 }
2563 Self::ensure_gesture_arena(node, node_id, gesture_owners);
2564 if let Some(arena) = node.handlers.gesture_arena.as_mut() {
2565 let cx = gesture_cx;
2566 ctx.capture_pointer_implicit();
2581 let result = arena.process(
2582 &RawPointerEvent::Down {
2583 position: *position,
2584 button: *button,
2585 modifiers: *modifiers,
2586 pointer: cx.pointer,
2587 time: cx.now,
2588 },
2589 &cx,
2590 );
2591 if let Some(gesture) = result {
2592 Self::dispatch_recognized_gesture(node, gesture, ctx);
2593 }
2594 return Some(EventResponse::Handled);
2595 }
2596 None
2597 }
2598 WidgetEvent::PointerUp {
2599 position,
2600 button,
2601 modifiers,
2602 ..
2603 } => {
2604 if fire_on_pointer_event {
2605 let r = fire_event_handler_both(
2606 &mut node.external_handlers.on_pointer_event,
2607 &mut node.handlers.on_pointer_event,
2608 event,
2609 ctx,
2610 );
2611 if r == EventResponse::Handled {
2612 return Some(EventResponse::Handled);
2613 }
2614 }
2615 if arena_blocked {
2616 return None;
2617 }
2618 if let Some(arena) = node.handlers.gesture_arena.as_mut() {
2619 let cx = gesture_cx;
2620 let result = arena.process(
2621 &RawPointerEvent::Up {
2622 position: *position,
2623 button: *button,
2624 modifiers: *modifiers,
2625 pointer: cx.pointer,
2626 time: cx.now,
2627 },
2628 &cx,
2629 );
2630 if let Some(gesture) = result {
2631 Self::dispatch_recognized_gesture(node, gesture, ctx);
2632 }
2633 return Some(EventResponse::Handled);
2634 }
2635 None
2636 }
2637 WidgetEvent::PointerMove { position, .. } => {
2638 if fire_on_pointer_event {
2639 let r = fire_event_handler_both(
2640 &mut node.external_handlers.on_pointer_event,
2641 &mut node.handlers.on_pointer_event,
2642 event,
2643 ctx,
2644 );
2645 if r == EventResponse::Handled {
2646 return Some(EventResponse::Handled);
2647 }
2648 }
2649 if arena_blocked {
2650 return None;
2651 }
2652 if let Some(arena) = node.handlers.gesture_arena.as_mut() {
2653 let cx = gesture_cx;
2654 let result = arena.process(
2655 &RawPointerEvent::Move {
2656 position: *position,
2657 pointer: cx.pointer,
2658 time: cx.now,
2659 },
2660 &cx,
2661 );
2662 if let Some(gesture) = result {
2663 Self::dispatch_recognized_gesture(node, gesture, ctx);
2664 return Some(EventResponse::Handled);
2676 }
2677 return Some(EventResponse::Ignored);
2678 }
2679 None
2680 }
2681 WidgetEvent::PointerCancel {
2682 reason, pointer, ..
2683 } => {
2684 for slot in [
2692 &mut node.external_handlers.on_pointer_cancel,
2693 &mut node.handlers.on_pointer_cancel,
2694 ] {
2695 if let Some(handler) = slot.as_mut() {
2696 handler(pointer, *reason, ctx);
2697 }
2698 }
2699 if fire_on_pointer_event {
2700 let r = fire_event_handler_both(
2701 &mut node.external_handlers.on_pointer_event,
2702 &mut node.handlers.on_pointer_event,
2703 event,
2704 ctx,
2705 );
2706 if r == EventResponse::Handled {
2707 return Some(EventResponse::Handled);
2708 }
2709 }
2710 None
2711 }
2712 }
2713 }
2714
2715 pub(super) fn collect_from_ctx<'ops>(
2716 &mut self,
2717 mut ctx: EventContext<'ops>,
2718 source_widget: WidgetId,
2719 ) {
2720 let local_ops = ctx.window_ops.take();
2726 let mut noop = crate::window::NoopWindowOps;
2727 let ops: &mut dyn crate::window::WindowOps = match local_ops {
2728 Some(o) => o,
2729 None => &mut noop,
2730 };
2731 if ctx.frame_requested {
2732 self.request_frame();
2733 }
2734 if let Some(declared) = ctx.declared_cursor_request {
2738 self.node_declared_cursor = declared;
2739 self.current_cursor = declared;
2740 }
2741 match ctx.cursor_request {
2742 Some(crate::widget::CursorRequest::Set(cursor)) => {
2743 self.current_cursor = cursor;
2744 }
2745 Some(crate::widget::CursorRequest::Release) => {
2749 self.current_cursor = self.node_declared_cursor;
2750 }
2751 None => {}
2752 }
2753 for intent in ctx.pending_intents {
2759 self.enqueue_intent(source_widget, intent, true);
2760 }
2761 if ctx.cancel_key_capture {
2770 self.cancel_key_capture();
2771 }
2772 if let Some(slot) = ctx.pending_key_capture {
2773 self.key_capture = Some(slot);
2774 }
2775 for mutation in ctx.pending_shortcut_mutations {
2777 match mutation {
2778 crate::widget::ShortcutMutation::RebindPrimary { id, keystroke } => {
2779 self.shortcut_registry.rebind_primary(id, keystroke);
2780 }
2781 crate::widget::ShortcutMutation::RebindSecondary { id, keystroke } => {
2782 self.shortcut_registry.rebind_secondary(id, keystroke);
2783 }
2784 crate::widget::ShortcutMutation::ClearOverride { id } => {
2785 self.shortcut_registry.clear_override(&id);
2786 }
2787 }
2788 }
2789 if ctx.close_window_requested {
2790 self.close_window_requested = true;
2791 }
2792 if ctx.force_close_requested {
2793 self.force_close_requested = true;
2794 }
2795 self.pending_modal_requests
2796 .extend(ctx.modal_requests.into_iter().map(|request| {
2797 crate::modal::QueuedModalRequest {
2798 source_widget,
2799 request,
2800 }
2801 }));
2802 if ctx.dismiss_modal && !self.dismiss_modal_for_source(source_widget, &mut *ops) {
2803 self.pending_modal_dismissal = true;
2804 }
2805 for callback in ctx.idle_callbacks {
2806 self.idle_queue.push_boxed(callback);
2807 }
2808 match ctx.dismiss_scope {
2809 Some(crate::widget::DismissScope::All) => {
2810 let dismissed = self.overlay_manager.dismiss_all();
2811 self.dormant_dismissed_content(&dismissed, &mut *ops);
2812 }
2813 Some(crate::widget::DismissScope::AllExceptHosts) => {
2814 self.dismiss_all_overlays_except_hosts(&mut *ops);
2815 }
2816 Some(crate::widget::DismissScope::SelfChain) => {
2817 self.dismiss_self_overlay_chain_for_source(source_widget, &mut *ops);
2818 }
2819 Some(crate::widget::DismissScope::Top) => {
2820 if let Some((_id, content_ids, focus_restore)) = self.overlay_manager.dismiss_top()
2821 {
2822 self.dormant_dismissed_content(&content_ids, &mut *ops);
2823 if let Some(restore_id) = focus_restore
2824 && self.arena.is_active(restore_id)
2825 {
2826 self.focus_ops(restore_id, &mut *ops);
2827 }
2828 }
2829 }
2830 None => {
2831 for id in ctx.overlay_dismissals {
2832 let dismissed = self.overlay_manager.dismiss(id);
2833 self.dormant_dismissed_content(&dismissed, &mut *ops);
2834 }
2835 }
2836 }
2837 for content_id in ctx.overlay_content_dismissals {
2844 if let Some(overlay_id) = self.overlay_manager.find_by_content(content_id) {
2845 let dismissed = self.overlay_manager.dismiss(overlay_id);
2846 self.dormant_dismissed_content(&dismissed, &mut *ops);
2847 }
2848 }
2849 for (id, pause) in ctx.overlay_pause_requests {
2856 if pause {
2857 self.overlay_manager.pause_auto_dismiss(id);
2858 } else {
2859 self.overlay_manager.resume_auto_dismiss(id);
2860 }
2861 }
2862 for preserve_content in ctx.dismiss_descendant_overlays {
2863 self.dismiss_child_overlays_for_source(source_widget, preserve_content, &mut *ops);
2864 }
2865 let deferred_row_activations =
2866 self.apply_tree_mutations(std::mem::take(&mut ctx.tree_mutations));
2867 if ctx.request_a11y_update {
2868 self.a11y_dirty = true;
2869 }
2870 if let Some(visible) = ctx.soft_keyboard_request.take() {
2871 self.request_soft_keyboard(visible);
2872 }
2873 for (message, politeness) in std::mem::take(&mut ctx.announcements) {
2876 self.announce_with(message, politeness);
2877 }
2878 for mut req in ctx.overlay_requests {
2879 if req.parent_overlay.is_none() {
2880 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
2881 }
2882 if self
2883 .overlay_manager
2884 .find_by_content(req.content_id)
2885 .is_some()
2886 {
2887 continue;
2888 }
2889 let current_focus = self.focused;
2890 self.overlay_manager.show(req);
2891 self.a11y_dirty = true;
2895 if let Some(focus_id) = current_focus {
2896 self.overlay_manager.set_top_focus_restore(focus_id);
2897 }
2898 }
2899 for (mut req, band) in ctx.overlay_band_requests {
2900 if req.parent_overlay.is_none() {
2901 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
2902 }
2903 if self
2904 .overlay_manager
2905 .find_by_content(req.content_id)
2906 .is_some()
2907 {
2908 continue;
2909 }
2910 let content_id = req.content_id;
2911 self.overlay_manager.show_in_band(req, band);
2912 self.arena.activate(content_id);
2913 self.a11y_dirty = true;
2914 }
2918 for (content_id, placement) in ctx.overlay_placement_updates {
2921 if let Some(overlay_id) = self.overlay_manager.find_by_content(content_id) {
2922 self.overlay_manager.update_placement(overlay_id, placement);
2923 }
2924 }
2925 for (mut req, duration) in ctx.timed_overlay_requests {
2926 if req.parent_overlay.is_none() {
2927 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
2928 }
2929 if self
2930 .overlay_manager
2931 .find_by_content(req.content_id)
2932 .is_some()
2933 {
2934 continue;
2935 }
2936 let current_focus = self.focused;
2937 let overlay_id = self.overlay_manager.show_for(req, duration);
2938 self.overlay_manager
2939 .set_shown_at_sim(overlay_id, self.sim_clock);
2940 self.a11y_dirty = true;
2941 if let Some(focus_id) = current_focus {
2942 self.overlay_manager.set_top_focus_restore(focus_id);
2943 }
2944 }
2945 for (mut req, progress, duration) in ctx.reveal_overlay_requests {
2946 if req.parent_overlay.is_none() {
2947 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
2948 }
2949 if self
2950 .overlay_manager
2951 .find_by_content(req.content_id)
2952 .is_some()
2953 {
2954 continue;
2955 }
2956 let content_id = req.content_id;
2957 let current_focus = self.focused;
2958 let overlay_id = self.overlay_manager.show(req);
2959 self.overlay_manager
2960 .set_shown_at_sim(overlay_id, self.sim_clock);
2961 self.a11y_dirty = true;
2962 if let Some(focus_id) = current_focus {
2963 self.overlay_manager.set_top_focus_restore(focus_id);
2964 }
2965 self.register_animated_signal(&progress, content_id);
2971 let _ = progress.try_animate_with_options(crate::animation::AnimationRequest {
2972 target: 1.0,
2973 duration,
2974 easing: teksilo_tokens::Easing::EaseOut,
2975 frame_interval: None,
2976 looping: false,
2977 epsilon: 0.0,
2978 max_duration: None,
2979 });
2980 self.overlay_manager
2981 .attach_fade(overlay_id, progress, duration);
2982 }
2983 if let Some((pointer, capture)) = ctx.pointer_capture {
2984 let named = pointer;
2988 let pointer = pointer.unwrap_or_else(|| self.current_pointer_id());
2989 self.set_pointer_capture(pointer, capture.then_some(source_widget));
2990 if capture && ctx.explicit_capture && named.is_none() {
2994 self.note_explicit_capture(source_widget);
2995 }
2996 }
2997 if let Some(activation) = ctx.drag_activation_override.take() {
2998 self.note_drag_activation_override(source_widget, activation);
3002 }
3003 if ctx.recognized_owning_gesture {
3004 self.note_gesture_recognized(source_widget);
3007 }
3008 if !ctx.gesture_acts.is_empty() {
3009 let acts = std::mem::take(&mut ctx.gesture_acts);
3010 self.apply_gesture_acts(&acts, source_widget);
3011 }
3012 if let Some(reason) = ctx.cancel_pointer_request {
3013 let pointer = self.current_pointer_id();
3014 self.cancel_pointer(pointer, reason, &mut *ops);
3015 }
3016 for (mut request, delay, focus_target, replace_siblings) in ctx.delayed_overlay_requests {
3017 if request.parent_overlay.is_none() {
3018 request.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3019 }
3020 if self
3021 .overlay_manager
3022 .find_by_content(request.content_id)
3023 .is_some()
3024 {
3025 continue;
3026 }
3027 let content_id = request.content_id;
3028 self.pending_delayed_overlays
3029 .retain(|pending| pending.request.content_id != content_id);
3030 self.pending_delayed_overlays.push(PendingDelayedOverlay {
3031 request,
3032 delay,
3033 focus_target,
3034 replace_siblings,
3035 real_requested_at: std::time::Instant::now(),
3036 sim_requested_at: self.sim_clock,
3037 });
3038 self.arena.mark_needs_paint(source_widget);
3039 }
3040 for content_id in ctx.cancel_delayed_overlays {
3041 self.pending_delayed_overlays
3042 .retain(|pending| pending.request.content_id != content_id);
3043 }
3044 for content_id in ctx.safe_region_arm_requests {
3051 if let Some(apex) = self
3052 .previous_pointer_position
3053 .or_else(|| self.hover_owner_position())
3054 {
3055 self.overlay_manager.arm_safe_region(
3056 content_id,
3057 apex,
3058 std::time::Instant::now(),
3059 self.sim_clock,
3060 );
3061 }
3062 }
3063 for id in ctx.repaint_requests {
3064 self.arena.mark_needs_paint(id);
3065 }
3066 for id in ctx.synthetic_clicks {
3067 self.synthesise_tap_with_ops(id, &mut *ops);
3072 }
3073 if let Some(&id) = ctx.focus_requests.last() {
3074 let target = self.first_focusable_descendant(id).unwrap_or(id);
3084 self.focus_ops(target, &mut *ops);
3085 }
3086 if let Some(&id) = ctx.focus_into_requests.last() {
3087 if let Some(target) = self.first_focusable_descendant(id) {
3094 self.focus_ops(target, &mut *ops);
3095 }
3096 }
3097
3098 for req in ctx.scroll_into_view_requests {
3107 self.scroll_rect_into_view(
3108 req.from.unwrap_or(source_widget),
3111 req.rect,
3112 req.margin,
3113 req.align,
3114 req.motion,
3115 &mut *ops,
3116 );
3117 }
3118 for (id, margin) in ctx.scroll_widget_into_view_requests {
3125 if self.arena.get(id).is_some() {
3126 let bounds = self.arena.bounds(id);
3127 self.scroll_rect_into_view(
3128 id,
3129 bounds,
3130 margin,
3131 crate::event::ScrollAlign::Minimal,
3132 crate::event::ScrollMotion::Instant,
3133 &mut *ops,
3134 );
3135 }
3136 }
3137
3138 if let Some(&id) = ctx.highlight_tooltip_requests.last() {
3143 self.show_highlight_tooltip(id, &mut *ops);
3144 }
3145
3146 if let Some((source_widget, payload, preview_widget)) = ctx.drag_start_request {
3148 let (preview_content_id, preview_overlay_id) = if let Some(preview) = preview_widget {
3149 let content_id = self.add_boxed(preview);
3156 let overlay_id = self.overlay_manager.show(crate::overlay::OverlayRequest {
3157 content_id,
3158 anchor: source_widget,
3159 placement: crate::overlay::OverlayPlacement::AtPointer(
3160 teksilo_canvas::Point::ZERO,
3161 ),
3162 dismiss: crate::overlay::DismissBehavior::Manual,
3163 layer: crate::overlay::OverlayLayer::InTree,
3164 parent_overlay: None,
3165 on_dismiss: None,
3166 fade_duration: None,
3167 });
3168 self.arena.mark_needs_layout(content_id);
3172 (Some(content_id), Some(overlay_id))
3173 } else {
3174 (None, None)
3175 };
3176 self.active_drag = Some(crate::drag_state::DragSession {
3177 payload,
3178 pointer: self.current_input.pointer,
3184 source_widget: Some(source_widget),
3185 is_external: false,
3186 current_position: teksilo_canvas::Point::ZERO,
3187 current_target: None,
3188 feedback: crate::drag_state::DropFeedback::NoFeedback,
3189 preview_content_id,
3190 preview_overlay_id,
3191 });
3192 self.set_current_pointer_capture(Some(source_widget));
3193 self.current_cursor = crate::widget::CursorIcon::Grabbing;
3196 }
3197 if ctx.cancel_drag {
3198 self.cancel_active_drag(&mut *ops);
3199 }
3200
3201 if let Some(theme) = ctx.theme_request {
3203 self.pending_theme_request = Some(theme);
3209 }
3210 if ctx.follow_system_request {
3211 self.pending_follow_system_request = true;
3215 }
3216 if let Some(locale) = ctx.locale_request {
3217 self.pending_locale_request = Some(locale);
3223 }
3224 if let Some(scale) = ctx.text_scale_request {
3225 self.pending_text_scale_request = Some(scale);
3230 }
3231 if !deferred_row_activations.is_empty() {
3238 self.run_with_event_context(&mut *ops, move |ctx| {
3239 for action in deferred_row_activations {
3240 action(ctx);
3241 }
3242 });
3243 }
3244 }
3245
3246 #[must_use]
3252 fn apply_tree_mutations(
3253 &mut self,
3254 mutations: Vec<crate::widget::TreeMutation>,
3255 ) -> Vec<std::rc::Rc<dyn Fn(&mut crate::widget::EventContext)>> {
3256 let mut deferred_row_activations = Vec::new();
3257 use crate::binding::BindingLevel;
3258 use crate::widget::TreeMutation;
3259
3260 for mutation in mutations {
3261 match mutation {
3262 TreeMutation::SetDormant(id) => {
3263 self.park_subtree(id);
3264 }
3265 TreeMutation::Activate(id) => self.arena.activate(id),
3266 TreeMutation::Destroy(id) => {
3267 if let Some(overlay_id) = self.overlay_manager().find_by_content(id) {
3280 self.dismiss_overlay(overlay_id);
3281 }
3282 self.destroy_subtree(id);
3283 }
3284 TreeMutation::MaterializeNow(id) => {
3288 if self.arena.get(id).is_some() {
3289 self.rebuild_single_widget(id);
3290 }
3291 }
3292 TreeMutation::RowSpaceActivate { row, fallback } => {
3293 deferred_row_activations.push(self.keyboard_toggle_in(row).unwrap_or(fallback));
3303 }
3304 TreeMutation::WithWidgetMut { id, dirty, apply } => {
3305 let existed = if let Some(any) =
3310 self.arena.get_mut(id).and_then(|n| n.widget.as_any_mut())
3311 {
3312 apply(any);
3313 true
3314 } else {
3315 false
3316 };
3317 if existed {
3318 match dirty {
3319 BindingLevel::RepaintOnly => self.arena.mark_needs_paint(id),
3320 BindingLevel::SubtreeRepaint => self.arena.mark_subtree_needs_paint(id),
3321 BindingLevel::Relayout => {
3322 self.arena.mark_needs_layout(id);
3323 self.arena.mark_ancestors_need_layout(id);
3324 }
3325 BindingLevel::Rebuild => {
3326 self.arena.mark_needs_rebuild(id);
3327 self.arena.mark_ancestors_need_layout(id);
3328 }
3329 BindingLevel::AccessibilityOnly => self.a11y_dirty = true,
3330 }
3331 }
3332 }
3333 }
3334 }
3335 deferred_row_activations
3336 }
3337
3338 pub fn hit_test(&self, point: Point) -> Option<WidgetId> {
3347 self.hit_test_excluding_overlay_and_widget(point, None, None)
3348 }
3349
3350 pub fn hit_test_for(
3361 &self,
3362 point: Point,
3363 pointer: &crate::pointer::PointerInfo,
3364 ) -> Option<WidgetId> {
3365 self.hit_test_for_excluding(point, pointer, None, None)
3366 }
3367
3368 pub fn hit_test_for_excluding(
3371 &self,
3372 point: Point,
3373 pointer: &crate::pointer::PointerInfo,
3374 exclude_overlay: Option<crate::overlay::OverlayId>,
3375 exclude_widget: Option<WidgetId>,
3376 ) -> Option<WidgetId> {
3377 let surfaces = self.text_surfaces();
3378 let read_only = |id: WidgetId| surfaces.is_read_only(id);
3379 let hit =
3380 crate::pointer::hit_slop::HitContext::new(pointer.kind, &self.effective_theme.input)
3381 .direction(self.layout_direction)
3382 .read_only_probe(&read_only);
3383 self.hit_test_with(point, exclude_overlay, exclude_widget, &hit)
3384 }
3385
3386 pub fn hit_test_excluding_overlay_and_widget(
3393 &self,
3394 point: Point,
3395 exclude_overlay: Option<crate::overlay::OverlayId>,
3396 exclude_widget: Option<WidgetId>,
3397 ) -> Option<WidgetId> {
3398 self.hit_test_with(
3399 point,
3400 exclude_overlay,
3401 exclude_widget,
3402 &crate::pointer::hit_slop::HitContext::mouse(),
3403 )
3404 }
3405
3406 pub(crate) fn hit_test_with(
3409 &self,
3410 point: Point,
3411 exclude_overlay: Option<crate::overlay::OverlayId>,
3412 exclude_widget: Option<WidgetId>,
3413 hit: &crate::pointer::hit_slop::HitContext<'_>,
3414 ) -> Option<WidgetId> {
3415 if let Some(overlay_id) = self.overlay_manager.hit_test(point) {
3416 if Some(overlay_id) == exclude_overlay {
3417 } else if let Some(overlay) = self.overlay_manager.overlay(overlay_id) {
3419 let content_id = overlay.content_id;
3423 if let Some(found) =
3424 self.arena
3425 .hit_test_in_subtree_with_slop(content_id, point, exclude_widget, hit)
3426 {
3427 return Some(found);
3428 }
3429 if !self
3447 .arena
3448 .get(content_id)
3449 .is_some_and(|node| node.event_pass_through)
3450 {
3451 return None;
3452 }
3453 }
3454 }
3455
3456 if self.overlay_manager.topmost_centered().is_some() {
3457 return None;
3458 }
3459
3460 self.arena.hit_test_at_with_slop(point, exclude_widget, hit)
3463 }
3464}
3465
3466fn is_context_menu_chord(key: Key, modifiers: Modifiers) -> bool {
3484 match key {
3485 Key::ContextMenu => modifiers == Modifiers::NONE,
3486 Key::F10 => modifiers == Modifiers::SHIFT,
3487 #[cfg(target_os = "macos")]
3488 Key::M => modifiers == Modifiers::CTRL | Modifiers::SHIFT,
3489 _ => false,
3490 }
3491}
3492
3493#[cfg(test)]
3494mod tests {
3495 use super::*;
3496 use crate::test_widgets::FillWidget;
3497 use crate::widget::CursorIcon;
3498 use crate::widget_builder::WidgetBuilder;
3499
3500 #[test]
3501 fn pointer_enter_leave_synthesized() {
3502 let mut tree = WidgetTree::new();
3503 let widget = tree.add(FillWidget::new());
3504 tree.layout(SizeProposal::exact(100.0, 50.0));
3505 tree.pointer_move(Point::new(50.0, 25.0));
3506 assert_eq!(tree.hovered(), Some(widget));
3507 tree.pointer_move(Point::new(200.0, 200.0));
3508 assert_eq!(tree.hovered(), None);
3509 }
3510
3511 #[test]
3512 fn pointer_hover_updates_current_cursor() {
3513 let mut tree = WidgetTree::new();
3514 tree.add(FillWidget::new().cursor(CursorIcon::ColResize));
3515 tree.layout(SizeProposal::exact(100.0, 50.0));
3516
3517 tree.pointer_move(Point::new(50.0, 25.0));
3518 assert_eq!(tree.current_cursor(), CursorIcon::ColResize);
3519
3520 tree.pointer_move(Point::new(200.0, 200.0));
3521 assert_eq!(tree.current_cursor(), CursorIcon::Default);
3522 }
3523
3524 #[test]
3534 fn release_cursor_hands_the_cursor_back_to_the_node_that_declared_it() {
3535 let mut tree = WidgetTree::new();
3539 tree.add(
3540 FillWidget::new()
3541 .cursor(CursorIcon::ColResize)
3542 .on_pointer_event(|event, ctx| {
3543 if let WidgetEvent::PointerMove { position, .. } = event {
3544 if position.x < 30.0 {
3545 ctx.set_cursor(CursorIcon::Crosshair);
3546 } else {
3547 ctx.release_cursor();
3548 }
3549 }
3550 EventResponse::Ignored
3551 }),
3552 );
3553 tree.layout(SizeProposal::exact(100.0, 50.0));
3554
3555 tree.pointer_move(Point::new(10.0, 25.0));
3556 assert_eq!(
3557 tree.current_cursor(),
3558 CursorIcon::Crosshair,
3559 "the handler outranks the node it is attached to",
3560 );
3561 tree.pointer_move(Point::new(60.0, 25.0));
3562 assert_eq!(
3563 tree.current_cursor(),
3564 CursorIcon::ColResize,
3565 "and hands it back to the node, not to Default — no enter/leave \
3566 fires on a move within one node, so nothing else could",
3567 );
3568 tree.pointer_move(Point::new(10.0, 25.0));
3569 assert_eq!(
3570 tree.current_cursor(),
3571 CursorIcon::Crosshair,
3572 "and can take it again"
3573 );
3574 tree.pointer_move(Point::new(200.0, 200.0));
3575 assert_eq!(
3576 tree.current_cursor(),
3577 CursorIcon::Default,
3578 "leaving the node clears both the override and the declaration",
3579 );
3580 }
3581
3582 #[test]
3586 fn release_cursor_is_a_no_op_with_nothing_to_undo() {
3587 let mut tree = WidgetTree::new();
3588 tree.add(
3589 FillWidget::new()
3590 .cursor(CursorIcon::ColResize)
3591 .on_pointer_event(|event, ctx| {
3592 if matches!(event, WidgetEvent::PointerMove { .. }) {
3593 ctx.release_cursor();
3594 }
3595 EventResponse::Ignored
3596 }),
3597 );
3598 tree.layout(SizeProposal::exact(100.0, 50.0));
3599 tree.pointer_move(Point::new(50.0, 25.0));
3600 assert_eq!(tree.current_cursor(), CursorIcon::ColResize);
3601
3602 let mut tree = WidgetTree::new();
3604 tree.add(FillWidget::new().on_pointer_event(|event, ctx| {
3605 if let WidgetEvent::PointerMove { position, .. } = event {
3606 if position.x < 30.0 {
3607 ctx.set_cursor(CursorIcon::Crosshair);
3608 } else {
3609 ctx.release_cursor();
3610 }
3611 }
3612 EventResponse::Ignored
3613 }));
3614 tree.layout(SizeProposal::exact(100.0, 50.0));
3615 tree.pointer_move(Point::new(10.0, 25.0));
3616 assert_eq!(tree.current_cursor(), CursorIcon::Crosshair);
3617 tree.pointer_move(Point::new(60.0, 25.0));
3618 assert_eq!(
3619 tree.current_cursor(),
3620 CursorIcon::Default,
3621 "nothing declared a cursor for this chain, so the hand-back \
3622 resolves to Default",
3623 );
3624 }
3625
3626 #[test]
3634 fn an_on_hover_override_does_not_erase_the_declaration_it_outranks() {
3635 let mut tree = WidgetTree::new();
3636 tree.add(
3637 FillWidget::new()
3638 .cursor(CursorIcon::ColResize)
3639 .on_hover(|entered, ctx| {
3640 if entered {
3641 ctx.set_cursor(CursorIcon::Crosshair);
3642 }
3643 })
3644 .on_pointer_event(|event, ctx| {
3645 if let WidgetEvent::PointerMove { position, .. } = event
3646 && position.x >= 30.0
3647 {
3648 ctx.release_cursor();
3649 }
3650 EventResponse::Ignored
3651 }),
3652 );
3653 tree.layout(SizeProposal::exact(100.0, 50.0));
3654
3655 tree.pointer_move(Point::new(10.0, 25.0));
3656 assert_eq!(
3657 tree.current_cursor(),
3658 CursorIcon::Crosshair,
3659 "on_hover runs after the node cursor is applied, so it wins",
3660 );
3661 tree.pointer_move(Point::new(60.0, 25.0));
3662 assert_eq!(
3663 tree.current_cursor(),
3664 CursorIcon::ColResize,
3665 "and the node's declaration survived being overridden",
3666 );
3667 }
3668
3669 #[derive(Debug)]
3672 struct Bumpable {
3673 value: i32,
3674 }
3675
3676 impl crate::widget::Widget for Bumpable {
3677 fn layout_response(
3678 &self,
3679 proposal: SizeProposal,
3680 _ctx: &crate::widget::LayoutContext,
3681 ) -> crate::widget::LayoutResponse {
3682 proposal.resolve(10.0, 10.0).into()
3683 }
3684 fn as_any(&self) -> Option<&dyn std::any::Any> {
3685 Some(self)
3686 }
3687 fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
3688 Some(self)
3689 }
3690 }
3691
3692 #[test]
3693 fn with_widget_mut_applies_and_dirty_marks() {
3694 let mut tree = WidgetTree::new();
3695 let id = tree.add(Bumpable { value: 0 });
3696 tree.layout(SizeProposal::exact(100.0, 100.0));
3697
3698 let mut ctx = EventContext::new();
3699 ctx.with_widget_mut::<Bumpable>(id, crate::binding::BindingLevel::Relayout, |b| {
3700 b.value = 42;
3701 });
3702 tree.collect_from_ctx(ctx, id);
3703
3704 let value = tree
3705 .widget_as_any(id)
3706 .and_then(|a| a.downcast_ref::<Bumpable>())
3707 .map(|b| b.value);
3708 assert_eq!(
3709 value,
3710 Some(42),
3711 "the deferred closure must mutate the live widget"
3712 );
3713 assert!(
3714 tree.needs_layout(),
3715 "Relayout dirty level must mark the tree for relayout"
3716 );
3717 }
3718
3719 #[test]
3720 #[cfg(debug_assertions)]
3721 #[should_panic(expected = "not the requested type")]
3722 fn with_widget_mut_wrong_type_panics_in_debug() {
3723 struct Other;
3724 let mut tree = WidgetTree::new();
3725 let id = tree.add(Bumpable { value: 0 });
3726 let mut ctx = EventContext::new();
3727 ctx.with_widget_mut::<Other>(
3728 id,
3729 crate::binding::BindingLevel::RepaintOnly,
3730 |_o: &mut Other| {},
3731 );
3732 tree.collect_from_ctx(ctx, id);
3735 }
3736
3737 #[test]
3738 fn with_widget_mut_closure_may_fire_observed_signals() {
3739 use crate::signal::Signal;
3747 let mut tree = WidgetTree::new();
3748 let id = tree.add(Bumpable { value: 0 });
3749 tree.layout(SizeProposal::exact(100.0, 100.0));
3750
3751 let trigger = Signal::new(0_u64);
3752 let echo = Signal::new(0_u64);
3753 let echo_for_obs = echo.clone();
3754 let _obs = trigger.observe(move |v| echo_for_obs.set(*v));
3755
3756 let trigger_in = trigger.clone();
3757 let mut ctx = EventContext::new();
3758 ctx.with_widget_mut::<Bumpable>(id, crate::binding::BindingLevel::RepaintOnly, move |b| {
3759 b.value = 7;
3760 trigger_in.set(99);
3762 });
3763 tree.collect_from_ctx(ctx, id); assert_eq!(
3766 echo.get(),
3767 99,
3768 "the observer ran during the deferred mutation"
3769 );
3770 let value = tree
3771 .widget_as_any(id)
3772 .and_then(|a| a.downcast_ref::<Bumpable>())
3773 .map(|b| b.value);
3774 assert_eq!(value, Some(7));
3775 }
3776
3777 #[test]
3778 fn request_accessibility_update_forces_rewalk() {
3779 let mut tree = WidgetTree::new();
3780 let id = tree.add(Bumpable { value: 0 });
3781 tree.layout(SizeProposal::exact(100.0, 100.0));
3782 let _ = tree.sync_accessibility(); assert!(
3784 !tree.a11y_dirty,
3785 "sync_accessibility should clear the dirty flag"
3786 );
3787
3788 let mut ctx = EventContext::new();
3789 ctx.request_accessibility_update();
3790 tree.collect_from_ctx(ctx, id);
3791 assert!(
3792 tree.a11y_dirty,
3793 "request_accessibility_update must force an AT re-walk"
3794 );
3795 }
3796
3797 #[test]
3798 fn rebuild_dirties_accessibility_tree() {
3799 let mut tree = WidgetTree::new();
3806 let id = tree.add(Bumpable { value: 0 });
3807 tree.layout(SizeProposal::exact(100.0, 100.0));
3808 let _ = tree.sync_accessibility(); assert!(
3810 !tree.a11y_dirty,
3811 "sync_accessibility should clear the dirty flag"
3812 );
3813
3814 tree.arena_mark_needs_rebuild_for_testing(id);
3817 tree.layout(SizeProposal::exact(100.0, 100.0));
3818 assert!(
3819 tree.a11y_dirty,
3820 "a rebuild must dirty the AT tree so the next sync re-walks"
3821 );
3822 }
3823
3824 #[test]
3825 fn bound_access_label_change_dirties_accessibility_tree() {
3826 use crate::signal::Signal;
3827 use crate::test_widgets::FillWidget;
3828 use crate::widget_builder::WidgetBuilder;
3829
3830 let label = Signal::new("first".to_string());
3836 let mut tree = WidgetTree::new();
3837 let _id = tree.add(FillWidget::new().access_label(label.clone()));
3838 tree.layout(SizeProposal::exact(100.0, 100.0));
3839 let _ = tree.sync_accessibility(); assert!(!tree.a11y_dirty, "sync_accessibility should clear the flag");
3841
3842 label.set("second".to_string());
3843 tree.layout(SizeProposal::exact(100.0, 100.0));
3844 assert!(
3845 tree.a11y_dirty,
3846 "changing a bound access_label must dirty the AT tree"
3847 );
3848 }
3849
3850 #[test]
3851 fn disabled_ancestor_blocks_event_to_descendant() {
3852 use crate::signal::Signal;
3853 use crate::test_widgets::StackWidget;
3854 use std::cell::Cell;
3855 use std::rc::Rc;
3856
3857 let tapped = Rc::new(Cell::new(false));
3858 let flag = tapped.clone();
3859 let enabled = Signal::new(true);
3860
3861 let mut tree = WidgetTree::new();
3862 let child = tree.add(FillWidget::new().on_tap(move |_pos, _ctx| {
3863 flag.set(true);
3864 }));
3865 let parent = tree.add(StackWidget::new().child(child));
3866 tree.enabled_when(parent, enabled.clone());
3867 tree.layout(SizeProposal::exact(100.0, 50.0));
3868
3869 enabled.set(false);
3870 tree.click(child);
3871 assert!(
3872 !tapped.get(),
3873 "disabled ancestor should block descendant tap"
3874 );
3875
3876 enabled.set(true);
3877 tree.click(child);
3878 assert!(tapped.get(), "re-enabling should restore dispatch");
3879 }
3880
3881 #[test]
3882 fn pointer_positions_are_widget_local_at_nonzero_origin() {
3883 use crate::event::{Modifiers, PointerButton};
3884 use crate::test_widgets::InsetWidget;
3885 use std::cell::Cell;
3886 use std::rc::Rc;
3887
3888 let tap_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
3890 let down_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
3891 let drag_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
3892 let (tp, dp, gp) = (tap_pos.clone(), down_pos.clone(), drag_pos.clone());
3893
3894 let mut tree = WidgetTree::new();
3895 let child = tree.add(
3896 FillWidget::new()
3897 .on_tap(move |ev, _ctx| tp.set(Some(ev.position)))
3898 .on_pointer_event(move |ev, _ctx| {
3899 if let WidgetEvent::PointerDown { position, .. } = ev {
3900 dp.set(Some(*position));
3901 }
3902 crate::event::EventResponse::Ignored
3903 })
3904 .on_drag(move |phase, _ctx| {
3905 use crate::gesture::DragPhase;
3906 match phase {
3907 DragPhase::Started { position, .. }
3908 | DragPhase::Moved { position, .. }
3909 | DragPhase::Ended { position, .. } => gp.set(Some(position)),
3910 _ => {}
3911 }
3912 }),
3913 );
3914 let inset = tree.add(InsetWidget::new(20.0).set_child(child));
3915 let _ = inset;
3916 tree.layout(SizeProposal::exact(200.0, 200.0));
3917 assert_eq!(tree.bounds(child).origin(), Point::new(20.0, 20.0));
3918
3919 tree.dispatch_event(WidgetEvent::pointer_down(
3921 Point::new(50.0, 40.0),
3922 PointerButton::Primary,
3923 Modifiers::NONE,
3924 ));
3925 assert_eq!(
3926 down_pos.get(),
3927 Some(Point::new(30.0, 20.0)),
3928 "on_pointer_event PointerDown must be widget-local"
3929 );
3930 tree.dispatch_event(WidgetEvent::pointer_up(
3931 Point::new(50.0, 40.0),
3932 PointerButton::Primary,
3933 Modifiers::NONE,
3934 ));
3935 assert_eq!(
3936 tap_pos.get(),
3937 Some(Point::new(30.0, 20.0)),
3938 "on_tap position must be widget-local"
3939 );
3940
3941 tree.dispatch_event(WidgetEvent::pointer_down(
3944 Point::new(50.0, 40.0),
3945 PointerButton::Primary,
3946 Modifiers::NONE,
3947 ));
3948 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(65.0, 55.0)));
3951 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(90.0, 70.0)));
3952 assert_eq!(
3953 drag_pos.get(),
3954 Some(Point::new(70.0, 50.0)),
3955 "on_drag position must be widget-local"
3956 );
3957 }
3958
3959 #[test]
3960 fn dormant_widget_not_hit_tested() {
3961 let mut tree = WidgetTree::new();
3962 let widget = tree.add(FillWidget::new());
3963 tree.layout(SizeProposal::exact(100.0, 50.0));
3964
3965 tree.pointer_move(Point::new(50.0, 25.0));
3966 assert_eq!(tree.hovered(), Some(widget));
3967
3968 tree.set_dormant(widget);
3969 tree.pointer_move(Point::new(200.0, 200.0));
3970 tree.pointer_move(Point::new(50.0, 25.0));
3971 assert_eq!(tree.hovered(), None);
3972 }
3973
3974 #[test]
3975 fn ancestor_pointer_handler_does_not_suppress_descendant_hover() {
3976 use crate::event::EventResponse;
3977 use crate::test_widgets::StackWidget;
3978 use std::cell::Cell;
3979 use std::rc::Rc;
3980
3981 let hovered = Rc::new(Cell::new(false));
3983 let h = hovered.clone();
3984
3985 let mut tree = WidgetTree::new();
3986 let child = tree.add(FillWidget::new().on_hover(move |entered, _ctx| h.set(entered)));
3987 tree.add(
3992 StackWidget::new()
3993 .child(child)
3994 .on_pointer_event(|_event, _ctx| EventResponse::Handled),
3995 );
3996 tree.layout(SizeProposal::exact(100.0, 50.0));
3997
3998 tree.pointer_move(Point::new(50.0, 25.0));
3999 assert!(
4000 hovered.get(),
4001 "a greedy ancestor on_pointer_event must NOT swallow the child's PointerEnter"
4002 );
4003
4004 tree.pointer_move(Point::new(500.0, 500.0));
4005 assert!(
4006 !hovered.get(),
4007 "PointerLeave must likewise reach the child despite the ancestor"
4008 );
4009 }
4010
4011 #[test]
4019 fn a_child_hover_handler_does_not_swallow_its_ancestors() {
4020 use crate::test_widgets::StackWidget;
4021 use std::cell::Cell;
4022 use std::rc::Rc;
4023
4024 let (row, button) = (Rc::new(Cell::new(false)), Rc::new(Cell::new(false)));
4025 let (r, b) = (row.clone(), button.clone());
4026
4027 let mut tree = WidgetTree::new();
4028 let child = tree.add(FillWidget::new().on_hover(move |entered, _ctx| b.set(entered)));
4029 tree.add(
4030 StackWidget::new()
4031 .child(child)
4032 .on_hover(move |entered, _ctx| r.set(entered)),
4033 );
4034 tree.layout(SizeProposal::exact(100.0, 50.0));
4035
4036 tree.pointer_move(Point::new(50.0, 25.0));
4037 assert!(button.get(), "the child is hovered");
4038 assert!(row.get(), "and so is the row it is inside");
4039
4040 tree.pointer_move(Point::new(500.0, 500.0));
4041 assert!(!button.get(), "the child heard the leave");
4042 assert!(
4043 !row.get(),
4044 "and so did the row — a container is not still hovered because the \
4045 pointer left it through a button"
4046 );
4047 }
4048
4049 #[test]
4056 fn key_preview_consumes_before_focused_on_key() {
4057 use crate::event::EventResponse;
4060 use crate::test_widgets::StackWidget;
4061 use std::cell::Cell;
4062 use std::rc::Rc;
4063
4064 let leaf_fired = Rc::new(Cell::new(false));
4065 let leaf_flag = leaf_fired.clone();
4066 let preview_fired = Rc::new(Cell::new(false));
4067 let preview_flag = preview_fired.clone();
4068
4069 let mut tree = WidgetTree::new();
4070 let leaf = tree.add(FillWidget::new().focusable().on_key(move |event, _c| {
4071 if matches!(event, WidgetEvent::KeyDown { .. }) {
4074 leaf_flag.set(true);
4075 }
4076 EventResponse::Handled
4077 }));
4078 let mid = tree.add(StackWidget::new().child(leaf));
4079 let _root = tree.add(
4080 StackWidget::new()
4081 .child(mid)
4082 .on_key_preview(move |event, _c| match event {
4083 WidgetEvent::KeyDown {
4084 key: Key::Enter, ..
4085 } => {
4086 preview_flag.set(true);
4087 EventResponse::Handled
4088 }
4089 _ => EventResponse::Ignored,
4090 }),
4091 );
4092
4093 tree.layout(SizeProposal::exact(100.0, 50.0));
4094 tree.focus(leaf);
4095 tree.press_key(Key::Enter, Modifiers::NONE);
4096
4097 assert!(
4098 preview_fired.get(),
4099 "ancestor on_key_preview must fire for KeyDown on a focused descendant"
4100 );
4101 assert!(
4102 !leaf_fired.get(),
4103 "consuming the event in preview must prevent the focused widget's on_key from running"
4104 );
4105 }
4106
4107 #[test]
4108 fn key_preview_falls_through_when_returning_ignored() {
4109 use crate::event::EventResponse;
4112 use crate::test_widgets::StackWidget;
4113 use std::cell::Cell;
4114 use std::rc::Rc;
4115
4116 let leaf_fired = Rc::new(Cell::new(false));
4117 let leaf_flag = leaf_fired.clone();
4118 let preview_fired = Rc::new(Cell::new(false));
4119 let preview_flag = preview_fired.clone();
4120
4121 let mut tree = WidgetTree::new();
4122 let leaf = tree.add(FillWidget::new().focusable().on_key(move |_e, _c| {
4123 leaf_flag.set(true);
4124 EventResponse::Handled
4125 }));
4126 let mid = tree.add(StackWidget::new().child(leaf));
4127 let _root = tree.add(
4128 StackWidget::new()
4129 .child(mid)
4130 .on_key_preview(move |_event, _c| {
4131 preview_flag.set(true);
4132 EventResponse::Ignored
4133 }),
4134 );
4135
4136 tree.layout(SizeProposal::exact(100.0, 50.0));
4137 tree.focus(leaf);
4138 tree.press_key(Key::Enter, Modifiers::NONE);
4139
4140 assert!(preview_fired.get(), "preview must always be invoked");
4141 assert!(
4142 leaf_fired.get(),
4143 "preview returning Ignored must not block the focused widget's on_key"
4144 );
4145 }
4146
4147 #[test]
4148 fn key_preview_excludes_focused_target_itself() {
4149 use crate::event::EventResponse;
4153 use std::cell::Cell;
4154 use std::rc::Rc;
4155
4156 let preview_on_target = Rc::new(Cell::new(false));
4157 let pf = preview_on_target.clone();
4158
4159 let mut tree = WidgetTree::new();
4160 let leaf = tree.add(FillWidget::new().focusable().on_key_preview(move |_e, _c| {
4161 pf.set(true);
4162 EventResponse::Handled
4163 }));
4164 tree.layout(SizeProposal::exact(100.0, 50.0));
4165 tree.focus(leaf);
4166 tree.press_key(Key::Enter, Modifiers::NONE);
4167
4168 assert!(
4169 !preview_on_target.get(),
4170 "the focused widget itself must not see its own on_key_preview"
4171 );
4172 }
4173
4174 #[test]
4175 fn key_preview_root_to_target_order() {
4176 use crate::event::EventResponse;
4180 use crate::test_widgets::StackWidget;
4181 use std::cell::RefCell;
4182 use std::rc::Rc;
4183
4184 let order: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
4185 let outer_log = order.clone();
4186 let inner_log = order.clone();
4187
4188 let mut tree = WidgetTree::new();
4189 let leaf = tree.add(FillWidget::new().focusable());
4190 let inner = tree.add(
4191 StackWidget::new()
4192 .child(leaf)
4193 .on_key_preview(move |event, _c| {
4194 if matches!(event, WidgetEvent::KeyDown { .. }) {
4195 inner_log.borrow_mut().push("inner");
4196 }
4197 EventResponse::Ignored
4198 }),
4199 );
4200 let _outer = tree.add(
4201 StackWidget::new()
4202 .child(inner)
4203 .on_key_preview(move |event, _c| {
4204 if matches!(event, WidgetEvent::KeyDown { .. }) {
4205 outer_log.borrow_mut().push("outer");
4206 }
4207 EventResponse::Ignored
4208 }),
4209 );
4210
4211 tree.layout(SizeProposal::exact(100.0, 50.0));
4212 tree.focus(leaf);
4213 tree.dispatch_event(WidgetEvent::KeyDown {
4214 key: Key::Enter,
4215 modifiers: Modifiers::NONE,
4216 text: None,
4217 });
4218
4219 assert_eq!(
4220 *order.borrow(),
4221 vec!["outer", "inner"],
4222 "preview must walk root → parent-of-target"
4223 );
4224 }
4225
4226 #[test]
4227 fn access_action_routes_to_cursored_target_not_focus() {
4228 use crate::signal::Signal;
4232 let a_fired = Signal::new(false);
4233 let b_fired = Signal::new(false);
4234 let a_cb = a_fired.clone();
4235 let b_cb = b_fired.clone();
4236
4237 let mut tree = WidgetTree::new();
4238 let a = tree.add(
4239 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| a_cb.set(true)),
4240 );
4241 let b = tree.add(
4242 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| b_cb.set(true)),
4243 );
4244 tree.layout(SizeProposal::exact(200.0, 100.0));
4245
4246 tree.focus(a);
4247 tree.dispatch_event(WidgetEvent::AccessAction {
4248 action: accesskit::Action::Click,
4249 target: Some(b),
4250 target_node: crate::accessibility::widget_id_to_node_id(b),
4251 data: None,
4252 });
4253
4254 assert!(b_fired.get(), "the cursored target must receive the action");
4255 assert!(
4256 !a_fired.get(),
4257 "the keyboard-focused widget must NOT receive an action targeting another node"
4258 );
4259 }
4260
4261 #[test]
4262 fn access_action_without_target_is_dropped_not_redirected_to_focus() {
4263 use crate::signal::Signal;
4266 let fired = Signal::new(false);
4267 let cb = fired.clone();
4268
4269 let mut tree = WidgetTree::new();
4270 let widget = tree.add(
4271 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| cb.set(true)),
4272 );
4273 tree.layout(SizeProposal::exact(200.0, 100.0));
4274
4275 tree.focus(widget);
4276 tree.dispatch_event(WidgetEvent::AccessAction {
4277 action: accesskit::Action::Click,
4278 target: None,
4279 target_node: crate::accessibility::root_node_id(),
4280 data: None,
4281 });
4282
4283 assert!(
4284 !fired.get(),
4285 "a target-less action must not be redirected to the focused widget"
4286 );
4287 }
4288
4289 #[test]
4296 fn shortcut_fires_matching_action_on_source_widget() {
4297 use crate::action::Action;
4298 use crate::shortcut::{KeyStroke, Shortcut};
4299 use std::cell::Cell;
4300 use std::rc::Rc;
4301
4302 let fired = Rc::new(Cell::new(false));
4303 let fired_flag = fired.clone();
4304
4305 let mut tree = WidgetTree::new();
4306 let widget = tree.add(FillWidget::new().focusable());
4307 tree.push_action(
4308 widget,
4309 Action::new("app.save").on_invoke(move |_intent, _ctx| {
4310 fired_flag.set(true);
4311 }),
4312 );
4313 tree.shortcut_registry_mut().register(
4314 Shortcut::new("app.save")
4315 .primary(KeyStroke::command(Key::S))
4316 .build(),
4317 );
4318
4319 tree.layout(SizeProposal::exact(100.0, 50.0));
4320 tree.focus(widget);
4321
4322 tree.press_key(Key::S, Modifiers::COMMAND);
4323 assert!(fired.get(), "matching action must fire on KeyDown");
4324 }
4325
4326 #[test]
4327 fn global_shortcut_fires_without_focused_widget() {
4328 use crate::action::Action;
4329 use crate::shortcut::{KeyStroke, Shortcut};
4330 use std::cell::Cell;
4331 use std::rc::Rc;
4332
4333 let fired = Rc::new(Cell::new(false));
4337 let fired_flag = fired.clone();
4338
4339 let mut tree = WidgetTree::new();
4340 let root = tree.add(FillWidget::new());
4341 tree.push_action(
4342 root,
4343 Action::new("app.save").on_invoke(move |_intent, _ctx| {
4344 fired_flag.set(true);
4345 }),
4346 );
4347 tree.shortcut_registry_mut().register(
4348 Shortcut::new("app.save")
4349 .primary(KeyStroke::command(Key::S))
4350 .build(),
4351 );
4352
4353 tree.layout(SizeProposal::exact(100.0, 50.0));
4354 tree.press_key(Key::S, Modifiers::COMMAND);
4357 assert!(
4358 fired.get(),
4359 "global shortcut must fire without a focused widget"
4360 );
4361 }
4362
4363 #[test]
4364 fn global_shortcut_fires_after_focused_widget_destroyed() {
4365 use crate::action::Action;
4366 use crate::shortcut::{KeyStroke, Shortcut};
4367 use std::cell::Cell;
4368 use std::rc::Rc;
4369
4370 let fired = Rc::new(Cell::new(false));
4375 let fired_flag = fired.clone();
4376
4377 let mut tree = WidgetTree::new();
4378 let root = tree.add(FillWidget::new());
4379 let focusable = tree.add_child(root, FillWidget::new().focusable());
4380 tree.push_action(
4381 root,
4382 Action::new("app.save").on_invoke(move |_intent, _ctx| {
4383 fired_flag.set(true);
4384 }),
4385 );
4386 tree.shortcut_registry_mut().register(
4387 Shortcut::new("app.save")
4388 .primary(KeyStroke::command(Key::S))
4389 .build(),
4390 );
4391
4392 tree.layout(SizeProposal::exact(100.0, 50.0));
4393 tree.focus(focusable);
4394 assert_eq!(tree.focused(), Some(focusable));
4395
4396 tree.destroy_subtree(focusable);
4399 assert_eq!(tree.focused(), None, "focus must clear when destroyed");
4400
4401 tree.press_key(Key::S, Modifiers::COMMAND);
4402 assert!(
4403 fired.get(),
4404 "global shortcut must still fire after the focused widget is destroyed"
4405 );
4406 }
4407
4408 #[test]
4409 fn scoped_shortcut_matches_only_when_focus_in_scope() {
4410 use crate::action::Action;
4411 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
4412 use std::cell::Cell;
4413 use std::rc::Rc;
4414
4415 let fired = Rc::new(Cell::new(0));
4416 let fired_flag = fired.clone();
4417
4418 let mut tree = WidgetTree::new();
4419 let scope_root = tree.add(FillWidget::new().focusable());
4420 let inside = tree.add_child(scope_root, FillWidget::new().focusable());
4421 let outside = tree.add(FillWidget::new().focusable());
4422
4423 tree.push_action(
4424 scope_root,
4425 Action::new("editor.find").on_invoke(move |_i, _c| {
4426 fired_flag.set(fired_flag.get() + 1);
4427 }),
4428 );
4429 tree.shortcut_registry_mut().register(
4430 Shortcut::new("editor.find")
4431 .primary(KeyStroke::command(Key::F))
4432 .scope(ShortcutScope::Scoped(scope_root))
4433 .build(),
4434 );
4435
4436 tree.layout(SizeProposal::exact(200.0, 100.0));
4437
4438 tree.focus(outside);
4440 tree.press_key(Key::F, Modifiers::COMMAND);
4441 assert_eq!(
4442 fired.get(),
4443 0,
4444 "scoped shortcut must not fire outside scope"
4445 );
4446
4447 tree.focus(inside);
4449 tree.press_key(Key::F, Modifiers::COMMAND);
4450 assert_eq!(
4451 fired.get(),
4452 1,
4453 "scoped shortcut must fire when focus in scope"
4454 );
4455 }
4456
4457 #[test]
4458 fn same_chord_scoped_first_falls_back_to_global_when_focus_outside() {
4459 use crate::action::Action;
4465 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
4466 use std::cell::Cell;
4467 use std::rc::Rc;
4468
4469 let scoped_fired = Rc::new(Cell::new(0));
4470 let global_fired = Rc::new(Cell::new(0));
4471 let sf = scoped_fired.clone();
4472 let gf = global_fired.clone();
4473
4474 let mut tree = WidgetTree::new();
4475 let root = tree.add(FillWidget::new());
4476 let editor = tree.add_child(root, FillWidget::new().focusable());
4477 let _editor_inner = tree.add_child(editor, FillWidget::new().focusable());
4478 let sidebar = tree.add_child(root, FillWidget::new().focusable());
4479
4480 tree.push_action(
4481 editor,
4482 Action::new("editor.saveBlock").on_invoke(move |_i, _c| sf.set(sf.get() + 1)),
4483 );
4484 tree.push_action(
4485 root,
4486 Action::new("zzz.global.save").on_invoke(move |_i, _c| gf.set(gf.get() + 1)),
4487 );
4488 tree.shortcut_registry_mut().register(
4489 Shortcut::new("editor.saveBlock")
4490 .primary(KeyStroke::command(Key::S))
4491 .scope(ShortcutScope::Scoped(editor))
4492 .build(),
4493 );
4494 tree.shortcut_registry_mut().register(
4495 Shortcut::new("zzz.global.save")
4496 .primary(KeyStroke::command(Key::S))
4497 .build(),
4498 );
4499
4500 tree.layout(SizeProposal::exact(200.0, 100.0));
4501 tree.focus(sidebar);
4502 tree.press_key(Key::S, Modifiers::COMMAND);
4503
4504 assert_eq!(global_fired.get(), 1, "applicable global must fire");
4505 assert_eq!(
4506 scoped_fired.get(),
4507 0,
4508 "inapplicable scoped binding must not eat the chord"
4509 );
4510 }
4511
4512 #[test]
4513 fn same_chord_global_first_yields_to_scoped_when_focus_inside() {
4514 use crate::action::Action;
4519 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
4520 use std::cell::Cell;
4521 use std::rc::Rc;
4522
4523 let scoped_fired = Rc::new(Cell::new(0));
4524 let global_fired = Rc::new(Cell::new(0));
4525 let sf = scoped_fired.clone();
4526 let gf = global_fired.clone();
4527
4528 let mut tree = WidgetTree::new();
4529 let root = tree.add(FillWidget::new());
4530 let editor = tree.add_child(root, FillWidget::new().focusable());
4531 let editor_inner = tree.add_child(editor, FillWidget::new().focusable());
4532 let sidebar = tree.add_child(root, FillWidget::new().focusable());
4533
4534 tree.push_action(
4535 editor,
4536 Action::new("editor.saveBlock").on_invoke(move |_i, _c| sf.set(sf.get() + 1)),
4537 );
4538 tree.push_action(
4539 root,
4540 Action::new("app.save").on_invoke(move |_i, _c| gf.set(gf.get() + 1)),
4541 );
4542 tree.shortcut_registry_mut().register(
4543 Shortcut::new("app.save")
4544 .primary(KeyStroke::command(Key::S))
4545 .build(),
4546 );
4547 tree.shortcut_registry_mut().register(
4548 Shortcut::new("editor.saveBlock")
4549 .primary(KeyStroke::command(Key::S))
4550 .scope(ShortcutScope::Scoped(editor))
4551 .build(),
4552 );
4553
4554 tree.layout(SizeProposal::exact(200.0, 100.0));
4555
4556 tree.focus(editor_inner);
4558 tree.press_key(Key::S, Modifiers::COMMAND);
4559 assert_eq!(
4560 scoped_fired.get(),
4561 1,
4562 "in-focus scoped must win over global"
4563 );
4564 assert_eq!(
4565 global_fired.get(),
4566 0,
4567 "global must yield to the scoped binding"
4568 );
4569
4570 tree.focus(sidebar);
4572 tree.press_key(Key::S, Modifiers::COMMAND);
4573 assert_eq!(scoped_fired.get(), 1, "scoped stays put outside its scope");
4574 assert_eq!(
4575 global_fired.get(),
4576 1,
4577 "global fires when focus leaves the scope"
4578 );
4579 }
4580
4581 #[test]
4582 fn propagated_action_lets_ancestor_handle() {
4583 use crate::action::Action;
4584 use crate::intent::IntentResponse;
4585 use crate::shortcut::{KeyStroke, Shortcut};
4586 use std::cell::Cell;
4587 use std::rc::Rc;
4588
4589 let inner_seen = Rc::new(Cell::new(false));
4590 let outer_seen = Rc::new(Cell::new(false));
4591 let inner_flag = inner_seen.clone();
4592 let outer_flag = outer_seen.clone();
4593
4594 let mut tree = WidgetTree::new();
4595 let outer = tree.add(FillWidget::new().focusable());
4596 let inner = tree.add_child(outer, FillWidget::new().focusable());
4597
4598 tree.push_action(
4600 inner,
4601 Action::new("app.save").on_invoke_with_response(move |_i, _c| {
4602 inner_flag.set(true);
4603 IntentResponse::Propagated
4604 }),
4605 );
4606 tree.push_action(
4607 outer,
4608 Action::new("app.save").on_invoke(move |_i, _c| {
4609 outer_flag.set(true);
4610 }),
4611 );
4612 tree.shortcut_registry_mut().register(
4613 Shortcut::new("app.save")
4614 .primary(KeyStroke::command(Key::S))
4615 .build(),
4616 );
4617
4618 tree.layout(SizeProposal::exact(100.0, 50.0));
4619 tree.focus(inner);
4620
4621 tree.press_key(Key::S, Modifiers::COMMAND);
4622 assert!(inner_seen.get(), "inner action observed the intent");
4623 assert!(outer_seen.get(), "outer action reached after Propagated");
4624 }
4625
4626 #[test]
4627 fn handled_action_stops_propagation() {
4628 use crate::action::Action;
4629 use crate::shortcut::{KeyStroke, Shortcut};
4630 use std::cell::Cell;
4631 use std::rc::Rc;
4632
4633 let inner_seen = Rc::new(Cell::new(false));
4634 let outer_seen = Rc::new(Cell::new(false));
4635 let inner_flag = inner_seen.clone();
4636 let outer_flag = outer_seen.clone();
4637
4638 let mut tree = WidgetTree::new();
4639 let outer = tree.add(FillWidget::new().focusable());
4640 let inner = tree.add_child(outer, FillWidget::new().focusable());
4641
4642 tree.push_action(
4643 inner,
4644 Action::new("app.save").on_invoke(move |_i, _c| {
4645 inner_flag.set(true);
4646 }),
4647 );
4648 tree.push_action(
4649 outer,
4650 Action::new("app.save").on_invoke(move |_i, _c| {
4651 outer_flag.set(true);
4652 }),
4653 );
4654 tree.shortcut_registry_mut().register(
4655 Shortcut::new("app.save")
4656 .primary(KeyStroke::command(Key::S))
4657 .build(),
4658 );
4659
4660 tree.layout(SizeProposal::exact(100.0, 50.0));
4661 tree.focus(inner);
4662
4663 tree.press_key(Key::S, Modifiers::COMMAND);
4664 assert!(inner_seen.get());
4665 assert!(!outer_seen.get(), "Handled at inner must stop propagation");
4666 }
4667
4668 #[test]
4669 fn disabled_action_propagates_by_default() {
4670 use crate::action::Action;
4671 use crate::shortcut::{KeyStroke, Shortcut};
4672 use crate::signal::Signal;
4673 use std::cell::Cell;
4674 use std::rc::Rc;
4675
4676 let inner_seen = Rc::new(Cell::new(false));
4677 let outer_seen = Rc::new(Cell::new(false));
4678 let inner_flag = inner_seen.clone();
4679 let outer_flag = outer_seen.clone();
4680
4681 let mut tree = WidgetTree::new();
4682 let outer = tree.add(FillWidget::new().focusable());
4683 let inner = tree.add_child(outer, FillWidget::new().focusable());
4684
4685 let enabled = Signal::new(false);
4686 tree.push_action(
4687 inner,
4688 Action::new("app.save")
4689 .enabled_when(enabled.clone())
4690 .on_invoke(move |_i, _c| {
4691 inner_flag.set(true);
4692 }),
4693 );
4694 tree.push_action(
4695 outer,
4696 Action::new("app.save").on_invoke(move |_i, _c| {
4697 outer_flag.set(true);
4698 }),
4699 );
4700 tree.shortcut_registry_mut().register(
4701 Shortcut::new("app.save")
4702 .primary(KeyStroke::command(Key::S))
4703 .build(),
4704 );
4705
4706 tree.layout(SizeProposal::exact(100.0, 50.0));
4707 tree.focus(inner);
4708
4709 tree.press_key(Key::S, Modifiers::COMMAND);
4710 assert!(!inner_seen.get(), "disabled inner must not run");
4711 assert!(
4712 outer_seen.get(),
4713 "intent must propagate past disabled inner"
4714 );
4715 }
4716
4717 #[test]
4718 fn disabled_action_with_non_propagating_shortcut_consumes() {
4719 use crate::action::Action;
4720 use crate::shortcut::{KeyStroke, Shortcut};
4721 use crate::signal::Signal;
4722 use std::cell::Cell;
4723 use std::rc::Rc;
4724
4725 let inner_seen = Rc::new(Cell::new(false));
4726 let outer_seen = Rc::new(Cell::new(false));
4727 let inner_flag = inner_seen.clone();
4728 let outer_flag = outer_seen.clone();
4729
4730 let mut tree = WidgetTree::new();
4731 let outer = tree.add(FillWidget::new().focusable());
4732 let inner = tree.add_child(outer, FillWidget::new().focusable());
4733
4734 let enabled = Signal::new(false);
4735 tree.push_action(
4736 inner,
4737 Action::new("app.save")
4738 .enabled_when(enabled.clone())
4739 .on_invoke(move |_i, _c| {
4740 inner_flag.set(true);
4741 }),
4742 );
4743 tree.push_action(
4744 outer,
4745 Action::new("app.save").on_invoke(move |_i, _c| {
4746 outer_flag.set(true);
4747 }),
4748 );
4749 tree.shortcut_registry_mut().register(
4750 Shortcut::new("app.save")
4751 .primary(KeyStroke::command(Key::S))
4752 .propagate_when_disabled(false)
4753 .build(),
4754 );
4755
4756 tree.layout(SizeProposal::exact(100.0, 50.0));
4757 tree.focus(inner);
4758
4759 tree.press_key(Key::S, Modifiers::COMMAND);
4760 assert!(!inner_seen.get(), "disabled inner still does not run");
4761 assert!(
4762 !outer_seen.get(),
4763 "intent must NOT propagate when shortcut disallows it"
4764 );
4765 }
4766
4767 #[test]
4768 fn send_intent_from_handler_reaches_ancestor_action() {
4769 use crate::action::Action;
4770 use crate::intent::Intent;
4771 use std::cell::Cell;
4772 use std::rc::Rc;
4773
4774 let save_seen = Rc::new(Cell::new(false));
4775 let save_flag = save_seen.clone();
4776
4777 let mut tree = WidgetTree::new();
4778 let root = tree.add(FillWidget::new());
4779 let button = tree.add_child(
4780 root,
4781 FillWidget::new().on_tap(|_pos, ctx| {
4782 ctx.send_intent(Intent::new("app.save"));
4783 }),
4784 );
4785 tree.push_action(
4786 root,
4787 Action::new("app.save").on_invoke(move |_i, _c| {
4788 save_flag.set(true);
4789 }),
4790 );
4791
4792 tree.layout(SizeProposal::exact(100.0, 50.0));
4793 tree.click(button);
4794 assert!(
4795 save_seen.get(),
4796 "ctx.send_intent must reach ancestor action"
4797 );
4798 }
4799
4800 #[test]
4801 fn widget_type_histogram_counts_distinct_types() {
4802 let mut tree = WidgetTree::new();
4807 let _ = tree.add(FillWidget::new());
4808 let _ = tree.add(FillWidget::new());
4809 let _ = tree.add(FillWidget::new());
4810 tree.layout(SizeProposal::exact(100.0, 100.0));
4811 let histogram = tree.widget_type_histogram();
4812 let total: u32 = histogram.values().sum();
4813 assert!(
4814 total >= 3,
4815 "expected at least 3 active widgets, got {total}: {histogram:?}"
4816 );
4817 let fillwidget_entries: u32 = histogram
4818 .iter()
4819 .filter(|(k, _)| k.contains("FillWidget"))
4820 .map(|(_, v)| *v)
4821 .sum();
4822 assert!(
4823 fillwidget_entries >= 3,
4824 "expected ≥3 FillWidget instances; histogram = {histogram:?}"
4825 );
4826 assert_eq!(tree.active_widget_count() as u32, total);
4827 }
4828
4829 #[test]
4830 fn intent_source_tagged_handler_for_tap_activation() {
4831 use crate::action::Action;
4835 use crate::intent::Intent;
4836 use crate::telemetry::IntentSource;
4837 use std::cell::Cell;
4838 use std::rc::Rc;
4839 let captured = Rc::new(Cell::new(IntentSource::Unknown));
4840 let captured_for_action = captured.clone();
4841
4842 let mut tree = WidgetTree::new();
4843 let root = tree.add(FillWidget::new());
4844 let button = tree.add_child(
4845 root,
4846 FillWidget::new().on_tap(|_pos, ctx| {
4847 ctx.send_intent(Intent::new("app.save"));
4848 }),
4849 );
4850 tree.push_action(
4851 root,
4852 Action::new("app.save").on_invoke(move |intent, _c| {
4853 captured_for_action.set(intent.source);
4854 }),
4855 );
4856
4857 tree.layout(SizeProposal::exact(100.0, 50.0));
4858 tree.click(button);
4859 assert_eq!(
4860 captured.get(),
4861 IntentSource::Handler,
4862 "tap-driven intent must tag IntentSource::Handler"
4863 );
4864 }
4865
4866 #[test]
4867 fn intent_source_programmatic_when_no_handler_active() {
4868 use crate::intent::Intent;
4869 use crate::telemetry::IntentSource;
4870 let intent = Intent::new("app.demo");
4871 assert_eq!(intent.source, IntentSource::Programmatic);
4872
4873 let mut ctx = EventContext::new();
4875 ctx.send_intent(Intent::new("app.demo"));
4876 let queued = ctx.pending_intents.first().expect("intent queued");
4877 assert_eq!(queued.source, IntentSource::Programmatic);
4878 }
4879
4880 #[test]
4881 fn with_intent_source_overrides_for_managed_widgets() {
4882 use crate::intent::Intent;
4883 use crate::telemetry::IntentSource;
4884 let mut ctx = EventContext::new();
4885 ctx.with_intent_source(IntentSource::Menu, |ctx| {
4886 ctx.send_intent(Intent::new("app.demo"));
4887 });
4888 let queued = ctx.pending_intents.first().expect("intent queued");
4889 assert_eq!(
4890 queued.source,
4891 IntentSource::Menu,
4892 "with_intent_source(Menu) must tag the dispatched intent"
4893 );
4894
4895 ctx.send_intent(Intent::new("app.next"));
4899 let next = ctx.pending_intents.last().expect("second intent");
4900 assert_eq!(next.source, IntentSource::Programmatic);
4901 }
4902
4903 #[test]
4904 fn disabled_shortcut_falls_through_to_focused_widget() {
4905 use crate::action::Action;
4906 use crate::shortcut::{KeyStroke, Shortcut};
4907 use crate::signal::Signal;
4908 use std::cell::Cell;
4909 use std::rc::Rc;
4910
4911 let action_fired = Rc::new(Cell::new(false));
4912 let on_key_fired = Rc::new(Cell::new(false));
4913 let af = action_fired.clone();
4914 let kf = on_key_fired.clone();
4915
4916 let enabled = Signal::new(false);
4917
4918 let mut tree = WidgetTree::new();
4919 let widget = tree.add(FillWidget::new().focusable().on_key(move |event, _ctx| {
4920 if matches!(
4921 event,
4922 WidgetEvent::KeyDown {
4923 key: Key::S,
4924 modifiers,
4925 ..
4926 } if modifiers.command()
4927 ) {
4928 kf.set(true);
4929 return EventResponse::Handled;
4930 }
4931 EventResponse::Ignored
4932 }));
4933 tree.push_action(
4934 widget,
4935 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
4936 );
4937 tree.shortcut_registry_mut().register(
4938 Shortcut::new("app.save")
4939 .primary(KeyStroke::command(Key::S))
4940 .enabled_when(enabled.clone())
4941 .build(),
4942 );
4943
4944 tree.layout(SizeProposal::exact(100.0, 50.0));
4945 tree.focus(widget);
4946
4947 tree.press_key(Key::S, Modifiers::COMMAND);
4949 assert!(
4950 !action_fired.get(),
4951 "disabled shortcut must not invoke its action"
4952 );
4953 assert!(
4954 on_key_fired.get(),
4955 "disabled shortcut must let KeyDown reach the focused widget"
4956 );
4957
4958 on_key_fired.set(false);
4960 enabled.set(true);
4961 tree.press_key(Key::S, Modifiers::COMMAND);
4962 assert!(action_fired.get(), "re-enabled shortcut must dispatch");
4963 assert!(
4964 !on_key_fired.get(),
4965 "enabled shortcut must consume the KeyDown"
4966 );
4967 }
4968
4969 #[test]
4970 fn keyboard_capture_bypasses_shortcut() {
4971 use crate::action::Action;
4972 use crate::shortcut::{KeyStroke, Shortcut};
4973 use std::cell::Cell;
4974 use std::rc::Rc;
4975
4976 fn run(capture: bool) -> (bool, bool) {
4982 let action_fired = Rc::new(Cell::new(false));
4983 let on_key_fired = Rc::new(Cell::new(false));
4984 let af = action_fired.clone();
4985 let kf = on_key_fired.clone();
4986
4987 let mut tree = WidgetTree::new();
4988 let widget = tree.add(
4989 FillWidget::new()
4990 .focusable()
4991 .keyboard_capture(capture)
4992 .on_key(move |event, _ctx| {
4993 if matches!(
4994 event,
4995 WidgetEvent::KeyDown { key: Key::S, modifiers, .. } if modifiers.command()
4996 ) {
4997 kf.set(true);
4998 return EventResponse::Handled;
4999 }
5000 EventResponse::Ignored
5001 }),
5002 );
5003 tree.push_action(
5004 widget,
5005 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
5006 );
5007 tree.shortcut_registry_mut().register(
5008 Shortcut::new("app.save")
5009 .primary(KeyStroke::command(Key::S))
5010 .build(),
5011 );
5012
5013 tree.layout(SizeProposal::exact(100.0, 50.0));
5014 tree.focus(widget);
5015 tree.press_key(Key::S, Modifiers::COMMAND);
5016 (action_fired.get(), on_key_fired.get())
5017 }
5018
5019 let (action, on_key) = run(true);
5021 assert!(
5022 !action,
5023 "keyboard_capture must suppress the shortcut action"
5024 );
5025 assert!(on_key, "keyboard_capture must deliver the raw KeyDown");
5026
5027 let (action, on_key) = run(false);
5029 assert!(action, "without capture the shortcut must fire");
5030 assert!(!on_key, "without capture the widget must not see the key");
5031 }
5032
5033 #[test]
5034 fn ctrl_tab_always_escapes_a_keyboard_capture_surface() {
5035 use std::cell::Cell;
5036 use std::rc::Rc;
5037
5038 let saw_key = Rc::new(Cell::new(false));
5044 let sk = saw_key.clone();
5045
5046 let mut tree = WidgetTree::new();
5047 let capture = tree.add(
5048 FillWidget::new()
5049 .focusable()
5050 .keyboard_capture(true)
5051 .on_key(move |_event, _ctx| {
5053 sk.set(true);
5054 EventResponse::Handled
5055 }),
5056 );
5057 let neighbour = tree.add(FillWidget::new().focusable());
5058 tree.layout(SizeProposal::exact(100.0, 50.0));
5059
5060 tree.focus(capture);
5063 tree.press_key(Key::Tab, Modifiers::NONE);
5064 assert!(saw_key.get(), "plain Tab must reach the capture surface");
5065 assert_eq!(
5066 tree.focused(),
5067 Some(capture),
5068 "plain Tab must not move focus off a capture surface"
5069 );
5070
5071 saw_key.set(false);
5073 tree.press_key(Key::Tab, Modifiers::CTRL);
5074 assert!(
5075 !saw_key.get(),
5076 "Ctrl+Tab is reserved and must not reach the capture surface"
5077 );
5078 assert_eq!(
5079 tree.focused(),
5080 Some(neighbour),
5081 "Ctrl+Tab must move focus out of a capture surface"
5082 );
5083
5084 tree.focus(capture);
5086 tree.press_key(Key::Tab, Modifiers::CTRL | Modifiers::SHIFT);
5087 assert_eq!(
5088 tree.focused(),
5089 Some(neighbour),
5090 "Ctrl+Shift+Tab must move focus out of a capture surface"
5091 );
5092 }
5093
5094 #[test]
5095 fn scope_mismatch_does_not_invoke_on_activate() {
5096 use crate::intent::Intent;
5097 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
5098 use std::cell::Cell;
5099 use std::rc::Rc;
5100
5101 let activated = Rc::new(Cell::new(false));
5107 let activated_flag = activated.clone();
5108
5109 let mut tree = WidgetTree::new();
5110 let scope_root = tree.add(FillWidget::new().focusable());
5111 let outside = tree.add(FillWidget::new().focusable());
5112
5113 tree.shortcut_registry_mut().register(
5114 Shortcut::new("editor.find")
5115 .primary(KeyStroke::command(Key::F))
5116 .scope(ShortcutScope::Scoped(scope_root))
5117 .on_activate(move |_ks, _ctx| {
5118 activated_flag.set(true);
5119 Intent::new("editor.find")
5120 })
5121 .build(),
5122 );
5123
5124 tree.layout(SizeProposal::exact(200.0, 100.0));
5125 tree.focus(outside);
5126
5127 tree.press_key(Key::F, Modifiers::COMMAND);
5128 assert!(
5129 !activated.get(),
5130 "on_activate must not run when focus is outside the shortcut's scope"
5131 );
5132 }
5133
5134 #[test]
5135 fn key_capture_runs_callback_and_bypasses_registry() {
5136 use crate::action::Action;
5137 use crate::shortcut::{KeyStroke, Shortcut};
5138 use std::cell::Cell;
5139 use std::rc::Rc;
5140
5141 let action_fired = Rc::new(Cell::new(false));
5142 let af = action_fired.clone();
5143 let captured = Rc::new(Cell::new(None));
5144 let cf = captured.clone();
5145
5146 let mut tree = WidgetTree::new();
5147 let widget = tree.add(FillWidget::new().focusable());
5148 tree.push_action(
5149 widget,
5150 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
5151 );
5152 tree.shortcut_registry_mut().register(
5153 Shortcut::new("app.save")
5154 .primary(KeyStroke::command(Key::S))
5155 .build(),
5156 );
5157
5158 tree.layout(SizeProposal::exact(100.0, 50.0));
5159 tree.focus(widget);
5160
5161 let handle = tree.begin_key_capture(move |ks, _reg, _ctx| cf.set(Some(ks)));
5162 assert!(tree.is_capturing_keys());
5163
5164 tree.press_key(Key::S, Modifiers::COMMAND);
5165 assert_eq!(
5166 captured.get(),
5167 Some(KeyStroke::command(Key::S)),
5168 "capture callback must receive the chord"
5169 );
5170 assert!(
5171 !action_fired.get(),
5172 "shortcut action must not fire while capture is armed"
5173 );
5174 assert!(
5175 !tree.is_capturing_keys(),
5176 "capture is one-shot; next KeyDown flows normally"
5177 );
5178 drop(handle);
5179 }
5180
5181 #[test]
5182 fn key_capture_can_rebind_through_registry() {
5183 use crate::shortcut::{KeyStroke, Shortcut};
5184
5185 let mut tree = WidgetTree::new();
5186 let widget = tree.add(FillWidget::new().focusable());
5187 tree.shortcut_registry_mut().register(
5188 Shortcut::new("app.save")
5189 .primary(KeyStroke::command(Key::S))
5190 .build(),
5191 );
5192
5193 tree.layout(SizeProposal::exact(100.0, 50.0));
5194 tree.focus(widget);
5195
5196 let _h = tree.begin_key_capture(|ks, reg, _ctx| {
5198 reg.rebind_primary("app.save", Some(ks));
5199 });
5200
5201 tree.press_key(Key::B, Modifiers::COMMAND | Modifiers::SHIFT);
5202 assert_eq!(
5203 tree.shortcut_registry()
5204 .effective("app.save")
5205 .unwrap()
5206 .primary,
5207 Some(KeyStroke::command_shift(Key::B))
5208 );
5209 }
5210
5211 #[test]
5212 fn dropping_capture_handle_cancels_capture() {
5213 use crate::shortcut::{KeyStroke, Shortcut};
5214 use std::cell::Cell;
5215 use std::rc::Rc;
5216
5217 let action_fired = Rc::new(Cell::new(false));
5218 let af = action_fired.clone();
5219 let capture_fired = Rc::new(Cell::new(false));
5220 let cf = capture_fired.clone();
5221
5222 let mut tree = WidgetTree::new();
5223 let widget = tree.add(FillWidget::new().focusable());
5224 tree.push_action(
5225 widget,
5226 crate::action::Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
5227 );
5228 tree.shortcut_registry_mut().register(
5229 Shortcut::new("app.save")
5230 .primary(KeyStroke::command(Key::S))
5231 .build(),
5232 );
5233 tree.layout(SizeProposal::exact(100.0, 50.0));
5234 tree.focus(widget);
5235
5236 {
5240 let _h = tree.begin_key_capture(move |_ks, _reg, _ctx| cf.set(true));
5241 assert!(tree.is_capturing_keys());
5242 }
5244 assert!(
5245 !tree.is_capturing_keys(),
5246 "dropping the handle must cancel the capture"
5247 );
5248
5249 tree.press_key(Key::S, Modifiers::COMMAND);
5250 assert!(!capture_fired.get(), "cancelled capture must not fire");
5251 assert!(
5252 action_fired.get(),
5253 "shortcut action runs after capture was cancelled"
5254 );
5255 }
5256
5257 #[test]
5258 fn second_begin_key_capture_does_not_racecancel_first() {
5259 use std::cell::Cell;
5260 use std::rc::Rc;
5261
5262 let first = Rc::new(Cell::new(false));
5263 let second = Rc::new(Cell::new(false));
5264 let f = first.clone();
5265 let s = second.clone();
5266
5267 let mut tree = WidgetTree::new();
5268 let widget = tree.add(FillWidget::new().focusable());
5269 tree.layout(SizeProposal::exact(100.0, 50.0));
5270 tree.focus(widget);
5271
5272 let h1 = tree.begin_key_capture(move |_ks, _reg, _ctx| f.set(true));
5278 let _h2 = tree.begin_key_capture(move |_ks, _reg, _ctx| s.set(true));
5279 drop(h1);
5280
5281 assert!(
5282 tree.is_capturing_keys(),
5283 "dropping the older handle must not cancel the active capture"
5284 );
5285 tree.press_key(Key::K, Modifiers::COMMAND);
5286 assert!(!first.get());
5287 assert!(second.get(), "newest capture wins");
5288 }
5289
5290 #[test]
5291 fn capture_callback_can_send_intent() {
5292 use crate::action::Action;
5293 use crate::intent::Intent;
5294
5295 use std::cell::Cell;
5296 use std::rc::Rc;
5297
5298 let ran = Rc::new(Cell::new(false));
5299 let flag = ran.clone();
5300
5301 let mut tree = WidgetTree::new();
5302 let widget = tree.add(FillWidget::new().focusable());
5303 tree.push_action(
5304 widget,
5305 Action::new("app.save").on_invoke(move |_i, _c| flag.set(true)),
5306 );
5307 tree.layout(SizeProposal::exact(100.0, 50.0));
5308 tree.focus(widget);
5309
5310 let _h = tree.begin_key_capture(|_ks, _reg, ctx| {
5311 ctx.send_intent(Intent::new("app.save"));
5312 });
5313 tree.press_key(Key::X, Modifiers::COMMAND);
5314 assert!(
5315 ran.get(),
5316 "intent queued from capture callback must dispatch"
5317 );
5318 }
5319
5320 #[test]
5321 fn binding_registry_does_not_accumulate_across_rebuilds() {
5322 use crate::binding::BindingLevel;
5323 use crate::signal::Signal;
5324
5325 #[derive(Debug)]
5326 struct BoundLeaf {
5327 tick: Signal<u64>,
5328 }
5329 impl crate::widget::Widget for BoundLeaf {
5330 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
5331 self.tick.bind_to(
5332 ctx.self_id(),
5333 ctx.binding_registry(),
5334 BindingLevel::Relayout,
5335 );
5336 Vec::new()
5337 }
5338 fn layout_response(
5339 &self,
5340 proposal: SizeProposal,
5341 _ctx: &crate::widget::LayoutContext,
5342 ) -> crate::widget::LayoutResponse {
5343 proposal.resolve(10.0, 10.0).into()
5344 }
5345 }
5346
5347 let mut tree = WidgetTree::new();
5348 let tick = Signal::new(0_u64);
5349 let widget = tree.add(BoundLeaf { tick: tick.clone() });
5350 tree.layout(SizeProposal::exact(200.0, 200.0));
5351 let after_first_build = tree.binding_registry().len();
5352 assert!(after_first_build >= 1);
5353
5354 for _ in 0..5 {
5358 tree.arena.mark_needs_rebuild(widget);
5359 tree.layout(SizeProposal::exact(200.0, 200.0));
5360 }
5361 assert_eq!(
5362 tree.binding_registry().len(),
5363 after_first_build,
5364 "bindings must be cleared on rebuild"
5365 );
5366
5367 tree.destroy_subtree(widget);
5368 assert_eq!(
5369 tree.binding_registry().len(),
5370 0,
5371 "bindings must be cleared on destroy"
5372 );
5373 let _ = tick;
5375 }
5376
5377 #[test]
5378 fn ctx_destroy_cancels_animations_and_bindings_via_deferred_path() {
5379 use crate::binding::BindingLevel;
5387 use crate::signal::Signal;
5388 use std::time::{Duration, Instant};
5389 use teksilo_tokens::Easing;
5390
5391 #[derive(Debug)]
5392 struct BoundLeaf {
5393 tick: Signal<u64>,
5394 }
5395 impl crate::widget::Widget for BoundLeaf {
5396 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
5397 self.tick.bind_to(
5398 ctx.self_id(),
5399 ctx.binding_registry(),
5400 BindingLevel::Relayout,
5401 );
5402 Vec::new()
5403 }
5404 fn layout_response(
5405 &self,
5406 proposal: SizeProposal,
5407 _ctx: &crate::widget::LayoutContext,
5408 ) -> crate::widget::LayoutResponse {
5409 proposal.resolve(10.0, 10.0).into()
5410 }
5411 }
5412
5413 let mut tree = WidgetTree::new();
5414 let widget = tree.add(BoundLeaf {
5415 tick: Signal::new(0_u64),
5416 });
5417 tree.layout(SizeProposal::exact(200.0, 200.0));
5418 assert!(tree.binding_registry().len() >= 1);
5419
5420 let anim = Signal::<f32>::new_animated(0.0);
5423 tree.animation_scheduler.animate(
5424 &anim,
5425 widget,
5426 1.0,
5427 Duration::from_secs(10),
5428 Easing::Linear,
5429 Instant::now(),
5430 );
5431 assert_eq!(tree.animation_scheduler.active_count(), 1);
5432
5433 let mut noop = crate::window::NoopWindowOps;
5435 tree.run_with_event_context(&mut noop, |ctx| ctx.destroy(widget));
5436
5437 assert_eq!(
5438 tree.animation_scheduler.active_count(),
5439 0,
5440 "ctx.destroy must cancel animations owned by the destroyed widget"
5441 );
5442 assert_eq!(
5443 tree.binding_registry().len(),
5444 0,
5445 "ctx.destroy must unregister the destroyed widget's bindings"
5446 );
5447 assert!(
5448 tree.arena.get(widget).is_none(),
5449 "node must be removed from the arena"
5450 );
5451 }
5452
5453 #[test]
5454 fn clear_shortcut_override_via_event_context_restores_default() {
5455 use crate::shortcut::{KeyStroke, Shortcut};
5456
5457 let mut tree = WidgetTree::new();
5458 tree.shortcut_registry_mut().register(
5459 Shortcut::new("app.save")
5460 .primary(KeyStroke::command(Key::S))
5461 .build(),
5462 );
5463 tree.shortcut_registry_mut()
5464 .rebind_primary("app.save", Some(KeyStroke::alt(Key::S)));
5465
5466 let source = tree.add(FillWidget::new());
5467 let mut ctx = EventContext::new();
5468 ctx.clear_shortcut_override("app.save");
5469 tree.collect_from_ctx(ctx, source);
5470
5471 assert_eq!(
5472 tree.shortcut_registry()
5473 .effective("app.save")
5474 .unwrap()
5475 .primary,
5476 Some(KeyStroke::command(Key::S))
5477 );
5478 }
5479
5480 #[test]
5481 fn rebind_shortcut_primary_via_event_context() {
5482 use crate::shortcut::{KeyStroke, Shortcut};
5483
5484 let mut tree = WidgetTree::new();
5485 tree.shortcut_registry_mut().register(
5486 Shortcut::new("app.save")
5487 .primary(KeyStroke::command(Key::S))
5488 .build(),
5489 );
5490 let source = tree.add(FillWidget::new());
5491
5492 let mut ctx = EventContext::new();
5493 ctx.rebind_shortcut_primary("app.save", Some(KeyStroke::alt(Key::S)));
5494 tree.collect_from_ctx(ctx, source);
5495
5496 assert_eq!(
5497 tree.shortcut_registry()
5498 .effective("app.save")
5499 .unwrap()
5500 .primary,
5501 Some(KeyStroke::alt(Key::S))
5502 );
5503 }
5504
5505 #[test]
5506 fn unregister_all_for_owner_called_on_destroy() {
5507 use crate::shortcut::{KeyStroke, Shortcut};
5508
5509 let mut tree = WidgetTree::new();
5510 let widget = tree.add(FillWidget::new());
5511 let widget_owner = widget;
5512 tree.shortcut_registry_mut().register_owned(
5513 Shortcut::new("scoped.thing")
5514 .primary(KeyStroke::command(Key::K))
5515 .build(),
5516 widget_owner,
5517 );
5518 assert!(
5519 tree.shortcut_registry()
5520 .get_default("scoped.thing")
5521 .is_some()
5522 );
5523
5524 tree.destroy_subtree(widget);
5525 assert!(
5526 tree.shortcut_registry()
5527 .get_default("scoped.thing")
5528 .is_none(),
5529 "destroying the owner must unregister its shortcut"
5530 );
5531 }
5532
5533 #[test]
5537 fn global_action_reached_from_unrelated_source() {
5538 use crate::action::Action;
5539 use crate::intent::Intent;
5540 use std::cell::Cell;
5541 use std::rc::Rc;
5542
5543 #[derive(Debug)]
5544 struct Registrar(Rc<Cell<bool>>);
5545 impl crate::widget::Widget for Registrar {
5546 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
5547 let flag = self.0.clone();
5548 ctx.register_action_global(
5549 Action::new("test.global").on_invoke(move |_i, _c| flag.set(true)),
5550 );
5551 vec![]
5552 }
5553 fn layout_response(
5554 &self,
5555 _p: teksilo_canvas::SizeProposal,
5556 _c: &crate::widget::LayoutContext,
5557 ) -> crate::widget::LayoutResponse {
5558 teksilo_canvas::Size::new(0.0, 0.0).into()
5559 }
5560 }
5561
5562 let mut tree = WidgetTree::new();
5563 let fired = Rc::new(Cell::new(false));
5564 let registrar = tree.add(Registrar(fired.clone()));
5565 let source = tree.add(FillWidget::new()); let mut ops = crate::window::NoopWindowOps;
5567
5568 tree.dispatch_intent(source, Intent::new("test.global"), true, &mut ops);
5569 assert!(
5570 fired.get(),
5571 "global action must fire from an unrelated source"
5572 );
5573
5574 fired.set(false);
5576 tree.destroy_subtree(registrar);
5577 tree.dispatch_intent(source, Intent::new("test.global"), true, &mut ops);
5578 assert!(
5579 !fired.get(),
5580 "destroying the owner must remove its global action"
5581 );
5582 }
5583
5584 #[test]
5596 fn hit_test_through_translate_scope() {
5597 use crate::test_widgets::StackWidget;
5598 let mut tree = WidgetTree::new();
5599 let child = tree.add(FillWidget::new());
5600 let parent = tree.add(StackWidget::new().child(child));
5601 tree.set_transform(parent, teksilo_canvas::Transform2D::translate(100.0, 0.0));
5603 tree.layout(SizeProposal::exact(100.0, 50.0));
5604
5605 assert_eq!(
5609 tree.hit_test(Point::new(50.0, 25.0)),
5610 None,
5611 "pre-transform area is not visually populated and must not hit"
5612 );
5613 assert_eq!(
5615 tree.hit_test(Point::new(150.0, 25.0)),
5616 Some(child),
5617 "visually-rendered area must hit the child"
5618 );
5619 assert_eq!(tree.hit_test(Point::new(250.0, 25.0)), None);
5621 }
5622
5623 #[test]
5624 fn hit_test_through_scale_scope() {
5625 use crate::test_widgets::StackWidget;
5626 let mut tree = WidgetTree::new();
5627 let child = tree.add(FillWidget::new());
5628 let parent = tree.add(StackWidget::new().child(child));
5629 tree.set_transform(parent, teksilo_canvas::Transform2D::scale(0.5, 0.5));
5632 tree.layout(SizeProposal::exact(100.0, 50.0));
5633
5634 assert_eq!(tree.hit_test(Point::new(25.0, 12.0)), Some(child));
5636 assert_eq!(
5639 tree.hit_test(Point::new(75.0, 25.0)),
5640 None,
5641 "scaled-out region must not hit"
5642 );
5643 }
5644
5645 #[test]
5646 fn hit_test_through_nested_transforms_compose() {
5647 use crate::test_widgets::StackWidget;
5648 let mut tree = WidgetTree::new();
5649 let leaf = tree.add(FillWidget::new());
5650 let inner = tree.add(StackWidget::new().child(leaf));
5651 let outer = tree.add(StackWidget::new().child(inner));
5652 tree.set_transform(outer, teksilo_canvas::Transform2D::translate(100.0, 0.0));
5656 tree.set_transform(inner, teksilo_canvas::Transform2D::scale(2.0, 2.0));
5657 tree.layout(SizeProposal::exact(50.0, 25.0));
5658
5659 assert_eq!(tree.hit_test(Point::new(150.0, 25.0)), Some(leaf));
5663 assert_eq!(tree.hit_test(Point::new(50.0, 25.0)), None);
5664 assert_eq!(tree.hit_test(Point::new(250.0, 25.0)), None);
5665 }
5666
5667 #[test]
5668 fn hit_test_identity_transform_unchanged() {
5669 let mut tree = WidgetTree::new();
5673 let widget = tree.add(FillWidget::new());
5674 tree.set_transform(widget, teksilo_canvas::Transform2D::IDENTITY);
5675 tree.layout(SizeProposal::exact(100.0, 50.0));
5676 assert_eq!(tree.hit_test(Point::new(50.0, 25.0)), Some(widget));
5677 }
5678
5679 #[test]
5680 fn arena_effective_transform_composes_ancestors() {
5681 use crate::test_widgets::StackWidget;
5692 let mut tree = WidgetTree::new();
5693 let leaf = tree.add(FillWidget::new());
5694 let inner = tree.add(StackWidget::new().child(leaf));
5695 let outer = tree.add(StackWidget::new().child(inner));
5696 tree.set_transform(outer, teksilo_canvas::Transform2D::translate(100.0, 0.0));
5697 tree.set_transform(inner, teksilo_canvas::Transform2D::scale(2.0, 2.0));
5698 tree.layout(SizeProposal::exact(50.0, 25.0));
5699
5700 let eff = tree.arena.effective_transform(leaf);
5701 let expected = teksilo_canvas::Transform2D::scale(2.0, 2.0)
5702 .then(&teksilo_canvas::Transform2D::translate(100.0, 0.0));
5703 for (a, b) in eff.m.iter().zip(expected.m.iter()) {
5704 assert!(
5705 (a - b).abs() < 1e-5,
5706 "effective_transform mismatch: got {:?}, want {:?}",
5707 eff.m,
5708 expected.m
5709 );
5710 }
5711
5712 let screen_origin = eff.apply_point(Point::new(0.0, 0.0));
5718 assert!((screen_origin.x - 100.0).abs() < 1e-5);
5719 assert!((screen_origin.y - 0.0).abs() < 1e-5);
5720 let screen_corner = eff.apply_point(Point::new(50.0, 25.0));
5723 assert!((screen_corner.x - 200.0).abs() < 1e-5);
5724 assert!((screen_corner.y - 50.0).abs() < 1e-5);
5725 }
5726
5727 #[derive(Debug)]
5732 struct StubMenu;
5733 impl crate::widget::Widget for StubMenu {
5734 fn layout_response(
5735 &self,
5736 _proposal: SizeProposal,
5737 _ctx: &crate::widget::LayoutContext,
5738 ) -> crate::widget::LayoutResponse {
5739 teksilo_canvas::Size::new(100.0, 40.0).into()
5740 }
5741 }
5742
5743 #[derive(Debug)]
5755 struct NominatingWidget {
5756 row: std::cell::Cell<Option<WidgetId>>,
5757 }
5758
5759 impl crate::widget::Widget for NominatingWidget {
5760 fn layout_response(
5761 &self,
5762 proposal: SizeProposal,
5763 _ctx: &LayoutContext,
5764 ) -> crate::widget::LayoutResponse {
5765 proposal.resolve(50.0, 20.0).into()
5766 }
5767
5768 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
5769 ctx.apply_self_handlers(crate::widget_builder::HandlerSet::new().focusable(true));
5770 Vec::new()
5771 }
5772
5773 fn context_menu_key_target(&self) -> Option<WidgetId> {
5774 self.row.get()
5775 }
5776 }
5777
5778 fn press(tree: &mut WidgetTree, key: Key, modifiers: Modifiers) {
5779 tree.dispatch_event(WidgetEvent::KeyDown {
5780 key,
5781 modifiers,
5782 text: None,
5783 });
5784 }
5785
5786 #[test]
5787 fn the_context_menu_key_opens_the_focused_widget_menu() {
5788 use std::cell::Cell;
5789 use std::rc::Rc;
5790
5791 let opened = Rc::new(Cell::new(false));
5792 let flag = opened.clone();
5793 let mut tree = WidgetTree::new();
5794 let widget = tree.add(
5795 FillWidget::new()
5796 .focusable()
5797 .context_menu(move |_pos, _ctx| {
5798 flag.set(true);
5799 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
5800 }),
5801 );
5802 tree.layout(SizeProposal::exact(200.0, 100.0));
5803 tree.focus(widget);
5804
5805 press(&mut tree, Key::ContextMenu, Modifiers::NONE);
5806 assert!(opened.get(), "the dedicated Menu key must open the menu");
5807 }
5808
5809 #[test]
5812 fn shift_f10_opens_the_focused_widget_menu() {
5813 use std::cell::Cell;
5814 use std::rc::Rc;
5815
5816 let opened = Rc::new(Cell::new(false));
5817 let flag = opened.clone();
5818 let mut tree = WidgetTree::new();
5819 let widget = tree.add(
5820 FillWidget::new()
5821 .focusable()
5822 .context_menu(move |_pos, _ctx| {
5823 flag.set(true);
5824 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
5825 }),
5826 );
5827 tree.layout(SizeProposal::exact(200.0, 100.0));
5828 tree.focus(widget);
5829
5830 press(&mut tree, Key::F10, Modifiers::SHIFT);
5831 assert!(opened.get(), "Shift+F10 must open the menu");
5832 }
5833
5834 #[test]
5837 fn a_near_miss_chord_is_not_a_context_menu_request() {
5838 use std::cell::Cell;
5839 use std::rc::Rc;
5840
5841 let opened = Rc::new(Cell::new(false));
5842 let flag = opened.clone();
5843 let mut tree = WidgetTree::new();
5844 let widget = tree.add(
5845 FillWidget::new()
5846 .focusable()
5847 .context_menu(move |_pos, _ctx| {
5848 flag.set(true);
5849 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
5850 }),
5851 );
5852 tree.layout(SizeProposal::exact(200.0, 100.0));
5853 tree.focus(widget);
5854
5855 press(&mut tree, Key::F10, Modifiers::SHIFT | Modifiers::CTRL);
5856 press(&mut tree, Key::F10, Modifiers::NONE);
5857 assert!(!opened.get(), "only Shift+F10 exactly asks for a menu");
5858 }
5859
5860 #[test]
5864 fn the_keyboard_target_can_be_a_row_rather_than_the_focused_container() {
5865 use std::cell::Cell;
5866 use std::rc::Rc;
5867
5868 let menu_owner = Rc::new(Cell::new(None::<&'static str>));
5869
5870 let row_flag = menu_owner.clone();
5871 let container_flag = menu_owner.clone();
5872
5873 let mut tree = WidgetTree::new();
5874 let row = tree.add(FillWidget::new().context_menu(move |_pos, _ctx| {
5875 row_flag.set(Some("row"));
5876 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
5877 }));
5878 let container = tree.add(
5879 crate::test_widgets::StackWidget::new()
5880 .child(row)
5881 .context_menu(move |_pos, _ctx| {
5882 container_flag.set(Some("container"));
5883 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
5884 }),
5885 );
5886 tree.layout(SizeProposal::exact(200.0, 100.0));
5887
5888 let nominator = tree.add(NominatingWidget {
5890 row: std::cell::Cell::new(Some(row)),
5891 });
5892 tree.layout(SizeProposal::exact(200.0, 100.0));
5893 tree.focus(nominator);
5894 let _ = container;
5895
5896 press(&mut tree, Key::ContextMenu, Modifiers::NONE);
5897 assert_eq!(
5898 menu_owner.get(),
5899 Some("row"),
5900 "the nominated row's factory must be the one that runs"
5901 );
5902 }
5903
5904 #[test]
5907 fn the_chord_falls_through_when_there_is_no_menu_to_show() {
5908 use std::cell::Cell;
5909 use std::rc::Rc;
5910
5911 let saw_key = Rc::new(Cell::new(false));
5912 let flag = saw_key.clone();
5913 let mut tree = WidgetTree::new();
5914 let widget = tree.add(FillWidget::new().focusable().on_key(move |ev, _ctx| {
5915 if matches!(ev, WidgetEvent::KeyDown { key: Key::F10, .. }) {
5916 flag.set(true);
5917 }
5918 crate::event::EventResponse::Ignored
5919 }));
5920 tree.layout(SizeProposal::exact(200.0, 100.0));
5921 tree.focus(widget);
5922
5923 press(&mut tree, Key::F10, Modifiers::SHIFT);
5924 assert!(
5925 saw_key.get(),
5926 "with no factory anywhere, the key must reach the widget"
5927 );
5928 }
5929
5930 #[test]
5936 fn each_route_to_a_menu_tells_the_factory_what_asked_for_it() {
5937 use crate::widget_builder::ContextMenuTrigger;
5938 use std::cell::RefCell;
5939 use std::rc::Rc;
5940
5941 let seen = Rc::new(RefCell::new(Vec::new()));
5942 let log = seen.clone();
5943 let mut tree = WidgetTree::new();
5944 let widget = tree.add(
5945 FillWidget::new()
5946 .focusable()
5947 .context_menu(move |_pos, ctx| {
5948 log.borrow_mut().push(ctx.context_menu_trigger());
5949 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
5950 }),
5951 );
5952 tree.layout(SizeProposal::exact(200.0, 100.0));
5953 tree.focus(widget);
5954 let inside = tree.bounds(widget).center();
5955
5956 press(&mut tree, Key::F10, Modifiers::SHIFT);
5957 press(&mut tree, Key::Escape, Modifiers::NONE);
5958 press(&mut tree, Key::ContextMenu, Modifiers::NONE);
5959 press(&mut tree, Key::Escape, Modifiers::NONE);
5960 tree.dispatch_event(WidgetEvent::pointer_down(
5961 inside,
5962 crate::event::PointerButton::Secondary,
5963 Modifiers::NONE,
5964 ));
5965 tree.dispatch_event(WidgetEvent::pointer_up(
5966 inside,
5967 crate::event::PointerButton::Secondary,
5968 Modifiers::NONE,
5969 ));
5970 press(&mut tree, Key::Escape, Modifiers::NONE);
5971 tree.dispatch_event(WidgetEvent::AccessAction {
5972 action: accesskit::Action::ShowContextMenu,
5973 target: Some(widget),
5974 target_node: crate::accessibility::widget_id_to_node_id(widget),
5975 data: None,
5976 });
5977 press(&mut tree, Key::Escape, Modifiers::NONE);
5978 tree.long_press_at(teksilo_tokens::PointerKind::Touch, inside);
5979
5980 assert_eq!(
5981 *seen.borrow(),
5982 vec![
5983 Some(ContextMenuTrigger::Keyboard),
5984 Some(ContextMenuTrigger::Keyboard),
5985 Some(ContextMenuTrigger::Pointer),
5986 Some(ContextMenuTrigger::Accessibility),
5987 Some(ContextMenuTrigger::Pointer),
5988 ],
5989 "Shift+F10, the Menu key, a right-click, the assistive action and a \
5990 hold, in that order"
5991 );
5992 }
5993
5994 #[test]
5995 fn context_menu_factory_receives_click_position() {
5996 use crate::event::{Modifiers, PointerButton};
5997 use std::cell::Cell;
5998 use std::rc::Rc;
5999
6000 let captured_position = Rc::new(Cell::new(None::<Point>));
6001 let cap = captured_position.clone();
6002 let mut tree = WidgetTree::new();
6003 let widget = tree.add(FillWidget::new().context_menu(move |pos, _ctx| {
6004 cap.set(Some(pos));
6005 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6006 }));
6007 tree.layout(SizeProposal::exact(200.0, 100.0));
6008
6009 let click = Point::new(73.0, 42.0);
6010 tree.dispatch_event(WidgetEvent::pointer_down(
6011 click,
6012 PointerButton::Secondary,
6013 Modifiers::NONE,
6014 ));
6015
6016 let got = captured_position.get();
6017 assert_eq!(
6018 got,
6019 Some(click),
6020 "factory must receive the click position; got {:?}",
6021 got
6022 );
6023 let _ = widget;
6024 }
6025
6026 #[test]
6027 fn context_menu_factory_returning_none_falls_through_to_parent() {
6028 use crate::event::{Modifiers, PointerButton};
6029 use crate::test_widgets::StackWidget;
6030 use std::cell::Cell;
6031 use std::rc::Rc;
6032
6033 let outer_called = Rc::new(Cell::new(0_u32));
6037 let outer_flag = outer_called.clone();
6038 let mut tree = WidgetTree::new();
6039 let inner = tree.add(FillWidget::new().context_menu(|_pos, _ctx| None));
6040 let _outer = tree.add(
6041 StackWidget::new()
6042 .child(inner)
6043 .context_menu(move |_pos, _ctx| {
6044 outer_flag.set(outer_flag.get() + 1);
6045 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6046 }),
6047 );
6048 tree.layout(SizeProposal::exact(200.0, 100.0));
6049
6050 tree.dispatch_event(WidgetEvent::pointer_down(
6051 Point::new(50.0, 25.0),
6052 PointerButton::Secondary,
6053 Modifiers::NONE,
6054 ));
6055
6056 assert_eq!(
6057 outer_called.get(),
6058 1,
6059 "inner returning None must fall through to the outer factory"
6060 );
6061 }
6062
6063 #[test]
6064 fn context_menu_factory_none_throughout_chain_does_not_show_overlay() {
6065 use crate::event::{Modifiers, PointerButton};
6066
6067 let mut tree = WidgetTree::new();
6069 tree.add(FillWidget::new().context_menu(|_pos, _ctx| None));
6070 tree.layout(SizeProposal::exact(200.0, 100.0));
6071
6072 let overlay_count_before = tree.overlay_manager.len();
6073 tree.dispatch_event(WidgetEvent::pointer_down(
6074 Point::new(50.0, 25.0),
6075 PointerButton::Secondary,
6076 Modifiers::NONE,
6077 ));
6078 let overlay_count_after = tree.overlay_manager.len();
6079 assert_eq!(
6080 overlay_count_before, overlay_count_after,
6081 "a factory returning None must not mount any overlay"
6082 );
6083 }
6084
6085 #[derive(Debug)]
6096 struct FreshChildHost {
6097 preserve: bool,
6098 }
6099 impl Widget for FreshChildHost {
6100 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6101 vec![ctx.add(FillWidget::new())]
6102 }
6103 fn layout_response(
6104 &self,
6105 p: SizeProposal,
6106 _c: &LayoutContext,
6107 ) -> crate::widget::LayoutResponse {
6108 p.resolve(10.0, 10.0).into()
6109 }
6110 fn preserves_children_on_rebuild(&self) -> bool {
6111 self.preserve
6112 }
6113 }
6114
6115 #[test]
6116 fn reconcile_reaps_dropped_children_no_leak() {
6117 for preserve in [false, true] {
6122 let mut tree = WidgetTree::new();
6123 let host = tree.add(FreshChildHost { preserve });
6124 tree.layout(SizeProposal::exact(100.0, 100.0));
6125 let total0 = tree.arena.len();
6126 let active0 = tree.active_widget_count();
6127 for _ in 0..5 {
6128 tree.arena_mark_needs_rebuild_for_testing(host);
6129 tree.layout(SizeProposal::exact(100.0, 100.0));
6130 }
6131 assert_eq!(
6132 tree.arena.len(),
6133 total0,
6134 "preserve={preserve}: dropped children must be reaped, not leaked"
6135 );
6136 assert_eq!(
6137 tree.active_widget_count(),
6138 active0,
6139 "preserve={preserve}: no stranded still-active orphans"
6140 );
6141 }
6142 }
6143
6144 #[derive(Debug)]
6150 struct DetachedContentHost {
6151 preserve: bool,
6152 }
6153 impl Widget for DetachedContentHost {
6154 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6155 let popup = ctx.add_detached(FillWidget::new());
6156 ctx.set_dormant(popup);
6157 vec![ctx.add(FillWidget::new())]
6158 }
6159 fn layout_response(
6160 &self,
6161 p: SizeProposal,
6162 _c: &LayoutContext,
6163 ) -> crate::widget::LayoutResponse {
6164 p.resolve(10.0, 10.0).into()
6165 }
6166 fn preserves_children_on_rebuild(&self) -> bool {
6167 self.preserve
6168 }
6169 }
6170
6171 #[test]
6172 fn rebuilding_reaps_detached_content_no_leak() {
6173 for preserve in [false, true] {
6179 let mut tree = WidgetTree::new();
6180 let host = tree.add(DetachedContentHost { preserve });
6181 tree.layout(SizeProposal::exact(100.0, 100.0));
6182 let total0 = tree.arena.len();
6183 for _ in 0..5 {
6184 tree.arena_mark_needs_rebuild_for_testing(host);
6185 tree.layout(SizeProposal::exact(100.0, 100.0));
6186 }
6187 assert_eq!(
6188 tree.arena.len(),
6189 total0,
6190 "preserve={preserve}: the previous build's detached content must be reaped"
6191 );
6192 }
6193 }
6194
6195 #[test]
6196 fn destroying_a_host_reaps_its_detached_content() {
6197 let mut tree = WidgetTree::new();
6198 let outer = tree.add(FillWidget::new());
6199 tree.layout(SizeProposal::exact(100.0, 100.0));
6200 let empty = tree.arena.len();
6201
6202 let host = tree.add_child(outer, DetachedContentHost { preserve: false });
6203 tree.layout(SizeProposal::exact(100.0, 100.0));
6204 assert!(tree.arena.len() > empty);
6205
6206 tree.destroy_subtree(host);
6207 assert_eq!(
6208 tree.arena.len(),
6209 empty,
6210 "the popup must die with the widget that built it"
6211 );
6212 }
6213
6214 #[derive(Debug)]
6216 struct StableChildHost {
6217 child: Option<WidgetId>,
6218 probe: std::rc::Rc<std::cell::Cell<Option<WidgetId>>>,
6219 }
6220 impl Widget for StableChildHost {
6221 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6222 let id = match self.child {
6223 Some(id) => id,
6224 None => {
6225 let id = ctx.add(FillWidget::new());
6226 self.child = Some(id);
6227 self.probe.set(Some(id));
6228 id
6229 }
6230 };
6231 vec![id]
6232 }
6233 fn layout_response(
6234 &self,
6235 p: SizeProposal,
6236 _c: &LayoutContext,
6237 ) -> crate::widget::LayoutResponse {
6238 p.resolve(10.0, 10.0).into()
6239 }
6240 fn preserves_children_on_rebuild(&self) -> bool {
6241 true
6242 }
6243 }
6244
6245 #[test]
6246 fn reconcile_preserves_reattached_child() {
6247 let probe = std::rc::Rc::new(std::cell::Cell::new(None));
6248 let mut tree = WidgetTree::new();
6249 let host = tree.add(StableChildHost {
6250 child: None,
6251 probe: probe.clone(),
6252 });
6253 tree.layout(SizeProposal::exact(100.0, 100.0));
6254 let child = probe.get().expect("child mounted");
6255 let total0 = tree.arena.len();
6256 for _ in 0..5 {
6257 tree.arena_mark_needs_rebuild_for_testing(host);
6258 tree.layout(SizeProposal::exact(100.0, 100.0));
6259 }
6260 assert!(
6261 tree.arena.is_active(child),
6262 "the re-attached child must survive every rebuild"
6263 );
6264 assert_eq!(tree.arena.len(), total0, "no growth — same child reused");
6265 }
6266
6267 #[derive(Debug)]
6269 struct Wrapper {
6270 child: WidgetId,
6271 }
6272 impl Widget for Wrapper {
6273 fn build(&mut self, _ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6274 vec![self.child]
6275 }
6276 fn layout_response(
6277 &self,
6278 p: SizeProposal,
6279 _c: &LayoutContext,
6280 ) -> crate::widget::LayoutResponse {
6281 p.resolve(10.0, 10.0).into()
6282 }
6283 }
6284
6285 #[derive(Debug)]
6289 struct ReparentHost {
6290 body: Option<WidgetId>,
6291 probe: std::rc::Rc<std::cell::Cell<Option<WidgetId>>>,
6292 }
6293 impl Widget for ReparentHost {
6294 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6295 let body = match self.body {
6296 Some(id) => id,
6297 None => {
6298 let id = ctx.add(FillWidget::new());
6299 self.body = Some(id);
6300 self.probe.set(Some(id));
6301 id
6302 }
6303 };
6304 vec![ctx.add(Wrapper { child: body })]
6305 }
6306 fn layout_response(
6307 &self,
6308 p: SizeProposal,
6309 _c: &LayoutContext,
6310 ) -> crate::widget::LayoutResponse {
6311 p.resolve(10.0, 10.0).into()
6312 }
6313 fn preserves_children_on_rebuild(&self) -> bool {
6314 true
6315 }
6316 }
6317
6318 #[test]
6319 fn reconcile_spares_reparented_survivor() {
6320 let probe = std::rc::Rc::new(std::cell::Cell::new(None));
6327 let mut tree = WidgetTree::new();
6328 let host = tree.add(ReparentHost {
6329 body: None,
6330 probe: probe.clone(),
6331 });
6332 tree.layout(SizeProposal::exact(100.0, 100.0));
6333 let body = probe.get().expect("body mounted");
6334 let total0 = tree.arena.len();
6335 for _ in 0..5 {
6336 tree.arena_mark_needs_rebuild_for_testing(host);
6337 tree.layout(SizeProposal::exact(100.0, 100.0));
6338 }
6339 assert!(
6340 tree.arena.is_active(body),
6341 "the re-parented body must survive — it was moved into the new tree, \
6342 not swept with the dropped wrapper"
6343 );
6344 assert_eq!(
6345 tree.arena.len(),
6346 total0,
6347 "dropped wrappers reaped — no per-rebuild leak"
6348 );
6349 }
6350
6351 #[derive(Debug)]
6361 struct BelowContainer {
6362 child: Option<WidgetId>,
6363 offset: f32,
6364 }
6365
6366 impl crate::widget::Widget for BelowContainer {
6367 fn layout_response(
6368 &self,
6369 proposal: SizeProposal,
6370 _ctx: &crate::widget::LayoutContext,
6371 ) -> crate::widget::LayoutResponse {
6372 proposal.resolve(0.0, 0.0).into()
6373 }
6374 fn place_children(
6375 &self,
6376 bounds: Rect,
6377 _proposal: SizeProposal,
6378 children: &mut [crate::widget::WidgetPlacement],
6379 _ctx: &crate::widget::LayoutContext,
6380 ) {
6381 for c in children.iter_mut() {
6382 c.origin = Point::new(bounds.x, bounds.y + self.offset);
6383 c.size = bounds.size();
6384 }
6385 }
6386 fn children(&self) -> Vec<WidgetId> {
6387 self.child.into_iter().collect()
6388 }
6389 }
6390
6391 fn recording_scroll_container(
6394 tree: &mut WidgetTree,
6395 child: WidgetId,
6396 recorded: std::rc::Rc<std::cell::Cell<Option<Rect>>>,
6397 ) -> WidgetId {
6398 use crate::test_widgets::StackWidget;
6399 tree.add(
6400 StackWidget::new()
6401 .child(child)
6402 .on_scroll(move |ev, _ctx| match ev {
6403 WidgetEvent::ScrollIntoView { target_bounds, .. } => {
6404 recorded.set(Some(*target_bounds));
6405 EventResponse::Handled
6406 }
6407 _ => EventResponse::Ignored,
6408 })
6409 .clips_children(true),
6410 )
6411 }
6412
6413 #[test]
6414 fn ensure_visible_dispatches_scroll_into_view_to_clipping_ancestor() {
6415 use std::cell::Cell;
6416 use std::rc::Rc;
6417 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6418 let mut tree = WidgetTree::new();
6419 let actor = tree.add(FillWidget::new());
6420 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
6421 tree.layout(SizeProposal::exact(100.0, 100.0));
6422
6423 let target = Rect::new(10.0, 500.0, 20.0, 15.0);
6426 let mut ctx = EventContext::new();
6427 ctx.ensure_visible(target);
6428 tree.collect_from_ctx(ctx, actor);
6429
6430 assert_eq!(
6431 recorded.get(),
6432 Some(target),
6433 "ensure_visible(rect) must dispatch ScrollIntoView with the exact rect \
6434 to the clips_children ancestor"
6435 );
6436 }
6437
6438 #[test]
6439 fn ensure_visible_is_noop_when_rect_already_visible() {
6440 use std::cell::Cell;
6441 use std::rc::Rc;
6442 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6443 let mut tree = WidgetTree::new();
6444 let actor = tree.add(FillWidget::new());
6445 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
6446 tree.layout(SizeProposal::exact(100.0, 100.0));
6447
6448 let mut ctx = EventContext::new();
6451 ctx.ensure_visible(Rect::new(10.0, 10.0, 20.0, 15.0));
6452 tree.collect_from_ctx(ctx, actor);
6453
6454 assert_eq!(
6455 recorded.get(),
6456 None,
6457 "a rect already inside the viewport must not trigger a scroll"
6458 );
6459 }
6460
6461 #[test]
6462 fn ensure_visible_margin_forces_scroll_near_edge() {
6463 use std::cell::Cell;
6464 use std::rc::Rc;
6465 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6466 let mut tree = WidgetTree::new();
6467 let actor = tree.add(FillWidget::new());
6468 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
6469 tree.layout(SizeProposal::exact(100.0, 100.0));
6470
6471 let rect = Rect::new(10.0, 95.0, 20.0, 4.0);
6474 let mut ctx = EventContext::new();
6475 ctx.ensure_visible_with_margin(rect, 10.0);
6476 tree.collect_from_ctx(ctx, actor);
6477
6478 assert_eq!(
6479 recorded.get(),
6480 Some(rect),
6481 "the margin must widen the visibility test so a near-edge rect scrolls"
6482 );
6483 }
6484
6485 fn recording_align_container(
6488 tree: &mut WidgetTree,
6489 child: WidgetId,
6490 recorded: std::rc::Rc<
6491 std::cell::Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>,
6492 >,
6493 ) -> WidgetId {
6494 use crate::test_widgets::StackWidget;
6495 tree.add(
6496 StackWidget::new()
6497 .child(child)
6498 .on_scroll(move |ev, _ctx| match ev {
6499 WidgetEvent::ScrollIntoView { align, motion, .. } => {
6500 recorded.set(Some((*align, *motion)));
6501 EventResponse::Handled
6502 }
6503 _ => EventResponse::Ignored,
6504 })
6505 .clips_children(true),
6506 )
6507 }
6508
6509 #[test]
6510 fn ensure_visible_aligned_scrolls_even_when_already_visible() {
6511 use std::cell::Cell;
6512 use std::rc::Rc;
6513 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6514 let mut tree = WidgetTree::new();
6515 let actor = tree.add(FillWidget::new());
6516 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
6517 tree.layout(SizeProposal::exact(100.0, 100.0));
6518
6519 let target = Rect::new(10.0, 10.0, 20.0, 15.0);
6524 let mut ctx = EventContext::new();
6525 ctx.ensure_visible_aligned(target, 0.5, crate::event::ScrollMotion::Instant);
6526 tree.collect_from_ctx(ctx, actor);
6527
6528 assert_eq!(
6529 recorded.get(),
6530 Some(target),
6531 "an aligned reveal must dispatch even when the rect is already visible"
6532 );
6533 }
6534
6535 #[test]
6536 fn ensure_visible_aligned_forwards_fraction_and_motion() {
6537 use std::cell::Cell;
6538 use std::rc::Rc;
6539 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6540 Rc::new(Cell::new(None));
6541 let mut tree = WidgetTree::new();
6542 let actor = tree.add(FillWidget::new());
6543 let _container = recording_align_container(&mut tree, actor, recorded.clone());
6544 tree.layout(SizeProposal::exact(100.0, 100.0));
6545
6546 let mut ctx = EventContext::new();
6547 ctx.ensure_visible_aligned(
6548 Rect::new(10.0, 10.0, 20.0, 15.0),
6549 0.25,
6550 crate::event::ScrollMotion::Smooth,
6551 );
6552 tree.collect_from_ctx(ctx, actor);
6553
6554 assert_eq!(
6555 recorded.get(),
6556 Some((
6557 crate::event::ScrollAlign::Fraction(0.25),
6558 crate::event::ScrollMotion::Smooth
6559 )),
6560 "the container must receive the requested fraction and motion verbatim"
6561 );
6562 }
6563
6564 #[test]
6565 fn ensure_visible_aligned_clamps_the_fraction() {
6566 use std::cell::Cell;
6567 use std::rc::Rc;
6568 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6569 Rc::new(Cell::new(None));
6570 let mut tree = WidgetTree::new();
6571 let actor = tree.add(FillWidget::new());
6572 let _container = recording_align_container(&mut tree, actor, recorded.clone());
6573 tree.layout(SizeProposal::exact(100.0, 100.0));
6574
6575 let mut ctx = EventContext::new();
6576 ctx.ensure_visible_aligned(
6577 Rect::new(10.0, 10.0, 20.0, 15.0),
6578 4.2,
6579 crate::event::ScrollMotion::Instant,
6580 );
6581 tree.collect_from_ctx(ctx, actor);
6582
6583 assert_eq!(
6584 recorded.get().map(|(a, _)| a),
6585 Some(crate::event::ScrollAlign::Fraction(1.0)),
6586 "an out-of-range fraction must clamp rather than aim the pin off-screen"
6587 );
6588 }
6589
6590 #[test]
6591 fn plain_ensure_visible_requests_minimal_alignment() {
6592 use std::cell::Cell;
6593 use std::rc::Rc;
6594 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6595 Rc::new(Cell::new(None));
6596 let mut tree = WidgetTree::new();
6597 let actor = tree.add(FillWidget::new());
6598 let _container = recording_align_container(&mut tree, actor, recorded.clone());
6599 tree.layout(SizeProposal::exact(100.0, 100.0));
6600
6601 let mut ctx = EventContext::new();
6602 ctx.ensure_visible(Rect::new(10.0, 500.0, 20.0, 15.0));
6603 tree.collect_from_ctx(ctx, actor);
6604
6605 assert_eq!(
6606 recorded.get(),
6607 Some((
6608 crate::event::ScrollAlign::Minimal,
6609 crate::event::ScrollMotion::Instant
6610 )),
6611 "the pre-existing reveal API must keep its exact semantics"
6612 );
6613 }
6614
6615 #[test]
6616 fn only_the_innermost_container_aligns() {
6617 use std::cell::Cell;
6618 use std::rc::Rc;
6619 let inner_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6620 Rc::new(Cell::new(None));
6621 let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6622 Rc::new(Cell::new(None));
6623
6624 let mut tree = WidgetTree::new();
6625 let actor = tree.add(FillWidget::new());
6626 let inner = recording_align_container(&mut tree, actor, inner_rec.clone());
6627 let _outer = recording_align_container(&mut tree, inner, outer_rec.clone());
6628 tree.layout(SizeProposal::exact(100.0, 100.0));
6629
6630 let mut ctx = EventContext::new();
6633 ctx.ensure_visible_aligned(
6634 Rect::new(10.0, 500.0, 20.0, 15.0),
6635 0.5,
6636 crate::event::ScrollMotion::Instant,
6637 );
6638 tree.collect_from_ctx(ctx, actor);
6639
6640 assert_eq!(
6641 inner_rec.get().map(|(a, _)| a),
6642 Some(crate::event::ScrollAlign::Fraction(0.5)),
6643 "the innermost clipping ancestor owns the pin"
6644 );
6645 assert_eq!(
6646 outer_rec.get().map(|(a, _)| a),
6647 Some(crate::event::ScrollAlign::Minimal),
6648 "an outer container must only bring the inner viewport into view — a \
6649 fraction names a height in one viewport, not in every ancestor's"
6650 );
6651 }
6652
6653 #[test]
6654 fn a_clipper_that_does_not_scroll_passes_the_pin_on() {
6655 use crate::test_widgets::StackWidget;
6662 use std::cell::Cell;
6663 use std::rc::Rc;
6664 let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6665 Rc::new(Cell::new(None));
6666
6667 let mut tree = WidgetTree::new();
6668 let actor = tree.add(FillWidget::new());
6669 let clipper = tree.add(StackWidget::new().child(actor).clips_children_on(true));
6670 let _scroller = recording_align_container(&mut tree, clipper, outer_rec.clone());
6671 tree.layout(SizeProposal::exact(100.0, 100.0));
6672
6673 let mut ctx = EventContext::new();
6676 ctx.ensure_visible_aligned(
6677 Rect::new(10.0, 10.0, 20.0, 15.0),
6678 0.5,
6679 crate::event::ScrollMotion::Instant,
6680 );
6681 tree.collect_from_ctx(ctx, actor);
6682
6683 assert_eq!(
6684 outer_rec.get().map(|(a, _)| a),
6685 Some(crate::event::ScrollAlign::Fraction(0.5)),
6686 "a clipping wrapper with no scroll handler must pass the pin through \
6687 to the scroll container above it"
6688 );
6689 }
6690
6691 #[test]
6692 fn a_scroller_that_declines_to_move_still_holds_the_pin() {
6693 use crate::test_widgets::StackWidget;
6698 use std::cell::Cell;
6699 use std::rc::Rc;
6700 let inner_rec: Rc<Cell<Option<crate::event::ScrollAlign>>> = Rc::new(Cell::new(None));
6701 let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6702 Rc::new(Cell::new(None));
6703
6704 let mut tree = WidgetTree::new();
6705 let actor = tree.add(FillWidget::new());
6706 let rec = inner_rec.clone();
6707 let inner = tree.add(
6708 StackWidget::new()
6709 .child(actor)
6710 .on_scroll(move |ev, _ctx| match ev {
6711 WidgetEvent::ScrollIntoView { align, .. } => {
6712 rec.set(Some(*align));
6713 EventResponse::Ignored
6714 }
6715 _ => EventResponse::Ignored,
6716 })
6717 .clips_children(true),
6718 );
6719 let _outer = recording_align_container(&mut tree, inner, outer_rec.clone());
6720 tree.layout(SizeProposal::exact(100.0, 100.0));
6721
6722 let mut ctx = EventContext::new();
6724 ctx.ensure_visible_aligned(
6725 Rect::new(10.0, 500.0, 20.0, 15.0),
6726 0.5,
6727 crate::event::ScrollMotion::Instant,
6728 );
6729 tree.collect_from_ctx(ctx, actor);
6730
6731 assert_eq!(
6732 inner_rec.get(),
6733 Some(crate::event::ScrollAlign::Fraction(0.5)),
6734 "the innermost scroller is offered the pin"
6735 );
6736 assert_eq!(
6737 outer_rec.get().map(|(a, _)| a),
6738 Some(crate::event::ScrollAlign::Minimal),
6739 "declining to move must not pass the pin outward"
6740 );
6741 }
6742
6743 #[test]
6744 fn ensure_widget_visible_uses_target_arena_bounds() {
6745 use std::cell::Cell;
6746 use std::rc::Rc;
6747 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6748 let mut tree = WidgetTree::new();
6749 let target = tree.add(FillWidget::new());
6751 let rec = recorded.clone();
6752 let container = tree.add(
6753 BelowContainer {
6754 child: Some(target),
6755 offset: 500.0,
6756 }
6757 .on_scroll(move |ev, _ctx| match ev {
6758 WidgetEvent::ScrollIntoView { target_bounds, .. } => {
6759 rec.set(Some(*target_bounds));
6760 EventResponse::Handled
6761 }
6762 _ => EventResponse::Ignored,
6763 })
6764 .clips_children(true),
6765 );
6766 tree.layout(SizeProposal::exact(100.0, 100.0));
6767
6768 let expected = tree.bounds(target);
6769 assert!(
6770 expected.y > 100.0,
6771 "fixture sanity: the target must sit below the viewport (y={})",
6772 expected.y
6773 );
6774
6775 let mut ctx = EventContext::new();
6778 ctx.ensure_widget_visible(target);
6779 tree.collect_from_ctx(ctx, container);
6780
6781 assert_eq!(
6782 recorded.get(),
6783 Some(expected),
6784 "ensure_widget_visible(id) must dispatch ScrollIntoView with the \
6785 target's current arena bounds"
6786 );
6787 }
6788
6789 #[test]
6790 fn ensure_widget_visible_ignores_missing_widget() {
6791 use std::cell::Cell;
6793 use std::rc::Rc;
6794 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6795 let mut tree = WidgetTree::new();
6796 let actor = tree.add(FillWidget::new());
6797 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
6798 tree.layout(SizeProposal::exact(100.0, 100.0));
6799
6800 let mut ctx = EventContext::new();
6801 ctx.ensure_widget_visible(WidgetId::default());
6802 tree.collect_from_ctx(ctx, actor); assert_eq!(
6805 recorded.get(),
6806 None,
6807 "an unmounted id must not trigger a scroll"
6808 );
6809 }
6810
6811 #[test]
6812 fn context_menu_inside_a_modal_keeps_the_modal() {
6813 use crate::event::{Modifiers, PointerButton, WidgetEvent};
6817 use crate::overlay::{DismissBehavior, OverlayLayer, OverlayPlacement, OverlayRequest};
6818 use crate::test_widgets::{FillWidget, StackWidget};
6819
6820 let mut tree = WidgetTree::new();
6821 let modal_content = tree.add(StackWidget::new());
6824 let _editor = tree.add_child(
6825 modal_content,
6826 FillWidget::new()
6827 .context_menu(|_pos, _ctx| Some(Box::new(FillWidget::new()) as Box<dyn Widget>)),
6828 );
6829 tree.layout(SizeProposal::exact(200.0, 100.0));
6830
6831 let modal = tree.overlay_manager.show(OverlayRequest {
6832 content_id: modal_content,
6833 anchor: modal_content,
6834 placement: OverlayPlacement::Centered,
6835 dismiss: DismissBehavior::EscapeKey,
6836 layer: OverlayLayer::InTree,
6837 parent_overlay: None,
6838 on_dismiss: None,
6839 fade_duration: None,
6840 });
6841 tree.overlay_manager
6843 .stack
6844 .iter_mut()
6845 .find(|o| o.id == modal)
6846 .unwrap()
6847 .bounds = Rect::new(0.0, 0.0, 200.0, 100.0);
6848 assert_eq!(tree.overlay_manager.len(), 1);
6849
6850 tree.dispatch_event(WidgetEvent::pointer_down(
6852 Point::new(50.0, 25.0),
6853 PointerButton::Secondary,
6854 Modifiers::NONE,
6855 ));
6856
6857 assert!(
6858 tree.overlay_manager.active_ids().contains(&modal),
6859 "the modal must survive opening a context menu inside it"
6860 );
6861 assert_eq!(
6862 tree.overlay_manager.len(),
6863 2,
6864 "the context menu should now be open on top of the surviving modal"
6865 );
6866 }
6867}
6868
6869#[cfg(test)]
6872mod input_ingress_tests {
6873 use super::*;
6874 use crate::event::{EventResponse, Modifiers, PointerButton, ScrollDelta};
6875 use crate::pointer::{
6876 EventTime, PointerId, PointerInfo, PointerPhase, PointerSample, ScrollPhase, ScrollSample,
6877 ScrollSource,
6878 };
6879 use crate::test_widgets::FillWidget;
6880 use crate::widget::{LayoutContext, WidgetPlacement};
6881 use crate::widget_builder::WidgetBuilder;
6882 use std::cell::RefCell;
6883 use std::rc::Rc;
6884
6885 #[derive(Debug)]
6892 struct RowWidget {
6893 children: Vec<WidgetId>,
6894 }
6895
6896 impl crate::widget::Widget for RowWidget {
6897 fn layout_response(
6898 &self,
6899 proposal: SizeProposal,
6900 _ctx: &LayoutContext,
6901 ) -> crate::widget::LayoutResponse {
6902 proposal.resolve(0.0, 0.0).into()
6903 }
6904
6905 fn place_children(
6906 &self,
6907 bounds: Rect,
6908 _proposal: SizeProposal,
6909 children: &mut [WidgetPlacement],
6910 _ctx: &LayoutContext,
6911 ) {
6912 let n = children.len().max(1) as f32;
6913 let w = bounds.width / n;
6914 for (i, child) in children.iter_mut().enumerate() {
6915 child.origin = Point::new(bounds.x + w * i as f32, bounds.y);
6916 child.size = teksilo_canvas::Size::new(w, bounds.height);
6917 }
6918 }
6919
6920 fn children(&self) -> Vec<WidgetId> {
6921 self.children.clone()
6922 }
6923 }
6924
6925 fn record(drive: impl FnOnce(&mut WidgetTree)) -> Vec<String> {
6928 let log: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(Vec::new()));
6929
6930 let mut tree = WidgetTree::new();
6931 let pointer_log = log.clone();
6932 let hover_log = log.clone();
6933 tree.add(
6934 FillWidget::new()
6935 .on_pointer_event(move |event, _ctx| {
6936 pointer_log.borrow_mut().push(format!("{event:?}"));
6937 EventResponse::Ignored
6938 })
6939 .on_hover(move |entered, _ctx| {
6940 hover_log.borrow_mut().push(format!("hover({entered})"));
6941 }),
6942 );
6943 tree.layout(SizeProposal::exact(100.0, 100.0));
6944
6945 drive(&mut tree);
6946 log.borrow().clone()
6947 }
6948
6949 #[test]
6955 fn a_mouse_sample_reproduces_todays_event_stream() {
6956 let inside = Point::new(40.0, 40.0);
6957 let moved = Point::new(60.0, 55.0);
6958 let outside = Point::new(400.0, 400.0);
6959
6960 let legacy = record(|tree| {
6961 tree.dispatch_event(WidgetEvent::pointer_move(inside));
6962 tree.dispatch_event(WidgetEvent::pointer_down(
6963 inside,
6964 PointerButton::Primary,
6965 Modifiers::NONE,
6966 ));
6967 tree.dispatch_event(WidgetEvent::pointer_move(moved));
6968 tree.dispatch_event(WidgetEvent::pointer_up(
6969 moved,
6970 PointerButton::Primary,
6971 Modifiers::NONE,
6972 ));
6973 tree.dispatch_event(WidgetEvent::pointer_move(outside));
6974 });
6975
6976 let sampled = record(|tree| {
6977 let t = EventTime::ZERO;
6978 tree.dispatch_pointer(PointerSample::mouse(PointerPhase::Move, inside, t));
6979 tree.dispatch_pointer(
6980 PointerSample::mouse(PointerPhase::Down, inside, t)
6981 .with_button(PointerButton::Primary),
6982 );
6983 tree.dispatch_pointer(PointerSample::mouse(PointerPhase::Move, moved, t));
6984 tree.dispatch_pointer(
6985 PointerSample::mouse(PointerPhase::Up, moved, t)
6986 .with_button(PointerButton::Primary),
6987 );
6988 tree.dispatch_pointer(PointerSample::mouse(PointerPhase::Move, outside, t));
6989 });
6990
6991 assert_eq!(legacy, sampled);
6992 assert!(
6993 legacy.contains(&"hover(true)".to_string())
6994 && legacy.contains(&"hover(false)".to_string()),
6995 "the fixture must actually exercise enter and leave: {legacy:?}"
6996 );
6997 }
6998
6999 #[test]
7003 fn a_buttonless_press_lowers_to_primary() {
7004 let events = record(|tree| {
7005 tree.dispatch_pointer(PointerSample::mouse(
7006 PointerPhase::Down,
7007 Point::new(20.0, 20.0),
7008 EventTime::ZERO,
7009 ));
7010 });
7011 assert!(
7012 events.iter().any(|e| e.contains("button: Primary")),
7013 "{events:?}"
7014 );
7015 }
7016
7017 fn scroll_fixture() -> (WidgetTree, Rc<RefCell<Vec<&'static str>>>) {
7022 let hits: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
7023 let mut tree = WidgetTree::new();
7024
7025 let leading_hits = hits.clone();
7026 let trailing_hits = hits.clone();
7027 let leading = tree.add(FillWidget::new().on_scroll(move |_event, _ctx| {
7028 leading_hits.borrow_mut().push("leading");
7029 EventResponse::Handled
7030 }));
7031 let trailing = tree.add(FillWidget::new().on_scroll(move |_event, _ctx| {
7032 trailing_hits.borrow_mut().push("trailing");
7033 EventResponse::Handled
7034 }));
7035 tree.add(RowWidget {
7036 children: vec![leading, trailing],
7037 });
7038 tree.layout(SizeProposal::exact(200.0, 100.0));
7039 (tree, hits)
7040 }
7041
7042 fn notch() -> ScrollDelta {
7043 ScrollDelta::Lines { x: 0.0, y: -1.0 }
7044 }
7045
7046 #[test]
7049 fn a_positionless_scroll_routes_by_hover() {
7050 let (mut tree, hits) = scroll_fixture();
7051
7052 tree.pointer_move(Point::new(150.0, 50.0)); tree.dispatch_event(WidgetEvent::scroll(notch(), Modifiers::NONE));
7054 assert_eq!(*hits.borrow(), vec!["trailing"]);
7055
7056 tree.pointer_move(Point::new(50.0, 50.0)); tree.dispatch_scroll(ScrollSample::wheel(
7058 notch(),
7059 Modifiers::NONE,
7060 EventTime::ZERO,
7061 ));
7062 assert_eq!(*hits.borrow(), vec!["trailing", "leading"]);
7063 }
7064
7065 #[test]
7070 fn a_positioned_scroll_routes_by_hit_test() {
7071 let (mut tree, hits) = scroll_fixture();
7072 tree.pointer_move(Point::new(150.0, 50.0)); tree.dispatch_event(WidgetEvent::scroll_at(
7075 notch(),
7076 Modifiers::NONE,
7077 Point::new(50.0, 50.0), ));
7079 assert_eq!(*hits.borrow(), vec!["leading"]);
7080
7081 tree.dispatch_scroll(
7082 ScrollSample::wheel(notch(), Modifiers::NONE, EventTime::ZERO)
7083 .at(Point::new(150.0, 50.0)),
7084 );
7085 assert_eq!(*hits.borrow(), vec!["leading", "trailing"]);
7086 }
7087
7088 #[test]
7092 fn a_positionless_scroll_with_no_hover_goes_nowhere() {
7093 let (mut tree, hits) = scroll_fixture();
7094 tree.dispatch_event(WidgetEvent::scroll(notch(), Modifiers::NONE));
7095 assert!(hits.borrow().is_empty());
7096 }
7097
7098 #[test]
7103 fn a_handler_sees_the_sample_it_is_serving() {
7104 let seen: Rc<RefCell<Vec<(ScrollPhase, ScrollSource, Option<Point>)>>> =
7105 Rc::new(RefCell::new(Vec::new()));
7106 let sink = seen.clone();
7107
7108 let mut tree = WidgetTree::new();
7109 tree.add(
7116 FillWidget::new()
7117 .scroll_container(crate::pointer::touch_action::PanAxes::BOTH)
7118 .on_scroll(move |_event, ctx| {
7119 sink.borrow_mut().push((
7120 ctx.scroll_phase(),
7121 ctx.scroll_source(),
7122 ctx.pointer_position(),
7123 ));
7124 EventResponse::Handled
7125 }),
7126 );
7127 tree.layout(SizeProposal::exact(100.0, 100.0));
7128
7129 tree.dispatch_scroll(ScrollSample {
7130 delta: notch(),
7131 position: Some(Point::new(50.0, 50.0)),
7132 phase: ScrollPhase::Momentum,
7133 source: ScrollSource::TouchPan,
7134 pointer: PointerInfo::mouse(EventTime::from_millis(12)),
7135 modifiers: Modifiers::NONE,
7136 });
7137
7138 assert_eq!(
7139 *seen.borrow(),
7140 vec![(
7141 ScrollPhase::Momentum,
7142 ScrollSource::TouchPan,
7143 Some(Point::new(50.0, 50.0))
7144 )]
7145 );
7146 }
7147
7148 #[test]
7151 fn a_legacy_event_reports_the_mouse_at_the_epoch() {
7152 let seen: Rc<RefCell<Option<(PointerId, ScrollPhase)>>> = Rc::new(RefCell::new(None));
7153 let sink = seen.clone();
7154
7155 let mut tree = WidgetTree::new();
7156 tree.add(FillWidget::new().on_pointer_event(move |_event, ctx| {
7157 *sink.borrow_mut() = Some((ctx.pointer().id, ctx.scroll_phase()));
7158 EventResponse::Ignored
7159 }));
7160 tree.layout(SizeProposal::exact(100.0, 100.0));
7161 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(50.0, 50.0)));
7162
7163 assert_eq!(
7164 *seen.borrow(),
7165 Some((PointerId::MOUSE, ScrollPhase::Discrete))
7166 );
7167 }
7168
7169 #[test]
7173 fn the_snapshot_is_restored_after_a_dispatch() {
7174 let mut tree = WidgetTree::new();
7175 tree.add(FillWidget::new());
7176 tree.layout(SizeProposal::exact(100.0, 100.0));
7177
7178 let before = tree.current_input.clone();
7179 tree.dispatch_scroll(
7180 ScrollSample::wheel(notch(), Modifiers::NONE, EventTime::ZERO)
7181 .at(Point::new(10.0, 10.0)),
7182 );
7183 assert_eq!(tree.current_input, before);
7184 }
7185}
7186
7187#[cfg(test)]
7188mod press_and_focus_tests {
7189 use std::cell::{Cell, RefCell};
7194 use std::rc::Rc;
7195 use teksilo_canvas::{Point, SizeProposal};
7196
7197 use crate::WidgetId;
7198 use crate::event::{EventResponse, Key, Modifiers, PointerButton, WidgetEvent};
7199 use crate::focus::FocusOrigin;
7200 use crate::pointer::clock::ManualClock;
7201 use crate::pointer::touch_action::PanClaim;
7202 use crate::pointer::{
7203 BackendDeviceKey, EventTime, PointerId, PointerIdAllocator, PointerInfo, PointerPhase,
7204 PointerSample,
7205 };
7206 use crate::test_widgets::{FillWidget, StackWidget};
7207 use crate::widget_builder::WidgetBuilder;
7208 use crate::widget_tree::WidgetTree;
7209
7210 fn contact_id() -> PointerId {
7216 use std::sync::atomic::{AtomicU64, Ordering};
7217 static NEXT: AtomicU64 = AtomicU64::new(1);
7218 PointerIdAllocator::global().begin(
7219 BackendDeviceKey::new(0x0B11),
7220 NEXT.fetch_add(1, Ordering::Relaxed),
7221 )
7222 }
7223
7224 fn contact(id: PointerId, phase: PointerPhase, at: Point, t: EventTime) -> PointerSample {
7225 PointerSample {
7226 pointer: PointerInfo::touch(id, t),
7227 phase,
7228 position: at,
7229 button: None,
7230 modifiers: Modifiers::NONE,
7231 coalesced: Vec::new(),
7232 }
7233 }
7234
7235 fn tree_on_a_clock() -> (WidgetTree, Rc<ManualClock>) {
7238 let mut tree = WidgetTree::new();
7239 let clock = Rc::new(ManualClock::new(EventTime::ZERO));
7240 tree.set_input_clock(clock.clone());
7241 (tree, clock)
7242 }
7243
7244 fn tappable(tree: &mut WidgetTree) -> (WidgetId, crate::signal::Signal<bool>) {
7251 let id = tree.add(FillWidget::new().focusable().on_tap(|_e, _c| {}));
7252 let signal = tree.pressed_signal(id);
7253 (id, signal)
7254 }
7255
7256 #[test]
7263 fn a_pointer_origin_names_its_device() {
7264 use teksilo_tokens::{PenKind, PointerKind};
7265
7266 assert!(FocusOrigin::Pointer(PointerKind::Touch).is_pointer());
7267 assert!(FocusOrigin::Pointer(PointerKind::Pen(PenKind::Pen)).is_pointer());
7268 assert!(FocusOrigin::POINTER.is_pointer());
7269 assert!(!FocusOrigin::Keyboard.is_pointer());
7270 assert!(!FocusOrigin::Programmatic.is_pointer());
7271 assert!(!FocusOrigin::Accessibility.is_pointer());
7272
7273 assert_eq!(
7274 FocusOrigin::Pointer(PointerKind::Touch).pointer_kind(),
7275 Some(PointerKind::Touch),
7276 );
7277 assert_eq!(FocusOrigin::Keyboard.pointer_kind(), None);
7278 assert_eq!(
7279 FocusOrigin::POINTER.pointer_kind(),
7280 Some(PointerKind::Unknown),
7281 "a widget deriving its own origin says so rather than naming a device it never saw",
7282 );
7283 }
7284
7285 #[test]
7289 fn only_a_real_navigation_declares_a_modality() {
7290 use teksilo_tokens::PointerKind;
7291
7292 assert_eq!(FocusOrigin::Keyboard.focus_visible(), Some(true));
7293 assert_eq!(FocusOrigin::Accessibility.focus_visible(), Some(true));
7294 assert_eq!(
7295 FocusOrigin::Pointer(PointerKind::Touch).focus_visible(),
7296 Some(false),
7297 );
7298 assert_eq!(FocusOrigin::Programmatic.focus_visible(), None);
7299 }
7300
7301 #[test]
7304 fn the_tier_three_configs_are_unchanged() {
7305 let origin: crate::signal::Signal<Option<FocusOrigin>> =
7306 crate::signal::Signal::new(Some(FocusOrigin::Keyboard));
7307 let slider: crate::signal::Signal<Option<FocusOrigin>> = origin.clone();
7308 let splitter: crate::signal::Signal<Option<FocusOrigin>> = origin.clone();
7309 let segmented: crate::signal::Signal<Option<FocusOrigin>> = origin.clone();
7310 for field in [slider, splitter, segmented] {
7311 assert_eq!(field.get(), Some(FocusOrigin::Keyboard));
7312 }
7313 origin.set(Some(FocusOrigin::POINTER));
7314 assert_ne!(
7315 origin.get(),
7316 Some(FocusOrigin::Keyboard),
7317 "the `== Some(Keyboard)` test every consumer makes still discriminates",
7318 );
7319 }
7320
7321 #[test]
7332 fn a_touch_tap_after_keyboard_focus_leaves_no_ring() {
7333 let (mut tree, _clock) = tree_on_a_clock();
7334 let a = tree.add(FillWidget::new().focusable());
7335 let b = tree.add(FillWidget::new().focusable());
7336 let root = tree.add(SideBySide {
7337 children: vec![a, b],
7338 });
7339 tree.layout(SizeProposal::exact(200.0, 100.0));
7340 let _ = root;
7341
7342 let visible = tree.focus_visible_signal();
7343 tree.press_key(Key::Tab, Modifiers::NONE);
7344 assert_eq!(tree.focused(), Some(a));
7345 assert!(visible.get(), "keyboard navigation reveals the ring");
7346
7347 let id = contact_id();
7348 let at = tree.bounds(b).center();
7349 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
7350 tree.dispatch_pointer(contact(
7351 id,
7352 PointerPhase::Up,
7353 at,
7354 EventTime::from_millis(30),
7355 ));
7356
7357 assert_eq!(tree.focused(), Some(b), "the release moved focus");
7358 assert!(!visible.get(), "and the ring did not come with it");
7359 assert_eq!(
7360 tree.focus_origin(),
7361 Some(FocusOrigin::Pointer(teksilo_tokens::PointerKind::Touch)),
7362 );
7363 }
7364
7365 #[test]
7370 fn an_assistive_focus_reveals_the_ring() {
7371 let mut tree = WidgetTree::new();
7372 let a = tree.add(FillWidget::new().focusable());
7373 let b = tree.add(FillWidget::new().focusable());
7374 let root = tree.add(SideBySide {
7375 children: vec![a, b],
7376 });
7377 tree.layout(SizeProposal::exact(200.0, 50.0));
7378 let _ = root;
7379 let visible = tree.focus_visible_signal();
7380
7381 tree.click(a);
7382 assert_eq!(tree.focused(), Some(a));
7383 assert!(!visible.get(), "the click hid it");
7384
7385 tree.dispatch_event(WidgetEvent::AccessAction {
7386 target: Some(b),
7387 action: accesskit::Action::Focus,
7388 target_node: crate::accessibility::widget_id_to_node_id(b),
7389 data: None,
7390 });
7391 assert_eq!(tree.focused(), Some(b));
7392 assert!(visible.get(), "an assistive move is a navigation");
7393 assert_eq!(tree.focus_origin(), Some(FocusOrigin::Accessibility));
7394 }
7395
7396 #[test]
7401 fn a_programmatic_focus_leaves_the_modality_alone() {
7402 let mut tree = WidgetTree::new();
7403 let a = tree.add(FillWidget::new().focusable());
7404 let b = tree.add(FillWidget::new().focusable());
7405 let root = tree.add(SideBySide {
7406 children: vec![a, b],
7407 });
7408 tree.layout(SizeProposal::exact(200.0, 100.0));
7409 let _ = root;
7410 let visible = tree.focus_visible_signal();
7411
7412 tree.press_key(Key::Tab, Modifiers::NONE);
7413 assert_eq!(tree.focused(), Some(a));
7414 assert!(visible.get());
7415 tree.focus(b);
7416 assert!(
7417 visible.get(),
7418 "a scripted focus does not hide a keyboard ring"
7419 );
7420
7421 tree.click(a);
7422 assert!(!visible.get());
7423 tree.focus(b);
7424 assert!(
7425 !visible.get(),
7426 "and does not reveal one after a click either",
7427 );
7428 }
7429
7430 #[test]
7436 fn a_mouse_still_focuses_on_press() {
7437 let mut tree = WidgetTree::new();
7438 let w = tree.add(FillWidget::new().focusable());
7439 tree.layout(SizeProposal::exact(100.0, 50.0));
7440 let center = tree.bounds(w).center();
7441
7442 tree.dispatch_event(WidgetEvent::pointer_down(
7443 center,
7444 PointerButton::Primary,
7445 Modifiers::NONE,
7446 ));
7447 assert_eq!(
7448 tree.focused(),
7449 Some(w),
7450 "focus lands on the press for an indirect pointer",
7451 );
7452 }
7453
7454 #[test]
7456 fn a_finger_focuses_on_release() {
7457 let (mut tree, _clock) = tree_on_a_clock();
7458 let w = tree.add(FillWidget::new().focusable());
7459 tree.layout(SizeProposal::exact(100.0, 50.0));
7460 let center = tree.bounds(w).center();
7461
7462 let id = contact_id();
7463 tree.dispatch_pointer(contact(id, PointerPhase::Down, center, EventTime::ZERO));
7464 assert_eq!(
7465 tree.focused(),
7466 None,
7467 "a finger that has only landed has chosen nothing yet",
7468 );
7469
7470 tree.dispatch_pointer(contact(
7471 id,
7472 PointerPhase::Up,
7473 center,
7474 EventTime::from_millis(40),
7475 ));
7476 assert_eq!(tree.focused(), Some(w));
7477 assert_eq!(
7478 tree.focus_origin().and_then(FocusOrigin::pointer_kind),
7479 Some(teksilo_tokens::PointerKind::Touch),
7480 "the origin names the device that delivered it",
7481 );
7482 }
7483
7484 #[test]
7488 fn a_release_on_a_different_focusable_moves_no_focus() {
7489 let (mut tree, _clock) = tree_on_a_clock();
7490 let a = tree.add(FillWidget::new().focusable());
7491 let b = tree.add(FillWidget::new().focusable());
7492 let root = tree.add(SideBySide {
7493 children: vec![a, b],
7494 });
7495 tree.layout(SizeProposal::exact(200.0, 50.0));
7496 let _ = root;
7497
7498 let on_a = tree.bounds(a).center();
7499 let on_b = tree.bounds(b).center();
7500 let id = contact_id();
7501 tree.dispatch_pointer(contact(id, PointerPhase::Down, on_a, EventTime::ZERO));
7502 tree.dispatch_pointer(contact(
7503 id,
7504 PointerPhase::Move,
7505 on_b,
7506 EventTime::from_millis(20),
7507 ));
7508 tree.dispatch_pointer(contact(
7509 id,
7510 PointerPhase::Up,
7511 on_b,
7512 EventTime::from_millis(40),
7513 ));
7514
7515 assert_eq!(
7516 tree.focused(),
7517 None,
7518 "the guard is `the release landed on the same focusable as the press`",
7519 );
7520 }
7521
7522 #[test]
7529 fn a_mouse_press_visual_is_unchanged() {
7530 let mut tree = WidgetTree::new();
7531 let (w, pressed) = tappable(&mut tree);
7532 tree.layout(SizeProposal::exact(100.0, 50.0));
7533 let center = tree.bounds(w).center();
7534
7535 assert!(!pressed.get(), "not pressed at rest");
7536 tree.dispatch_event(WidgetEvent::pointer_down(
7537 center,
7538 PointerButton::Primary,
7539 Modifiers::NONE,
7540 ));
7541 assert!(pressed.get(), "an indirect pointer lights up on the press");
7542 assert!(
7543 !tree.press_pending(w),
7544 "and never waits: a mouse opens no pan session, so there is no \
7545 ambiguity to wait out",
7546 );
7547 assert_eq!(tree.pressed_by(w), Some(PointerId::MOUSE));
7548
7549 tree.dispatch_event(WidgetEvent::pointer_up(
7550 center,
7551 PointerButton::Primary,
7552 Modifiers::NONE,
7553 ));
7554 assert!(!pressed.get(), "the release clears it");
7555 assert_eq!(tree.pressed_by(w), None);
7556 }
7557
7558 #[test]
7562 fn a_slide_off_clears_the_visual_and_re_entry_restores_it() {
7563 let (mut tree, _clock) = tree_on_a_clock();
7564 let (w, pressed) = tappable(&mut tree);
7565 tree.layout(SizeProposal::exact(100.0, 200.0));
7566 let inside = tree.bounds(w).center();
7567 let outside = Point::new(inside.x, inside.y + 400.0);
7568
7569 let id = contact_id();
7570 tree.dispatch_pointer(contact(id, PointerPhase::Down, inside, EventTime::ZERO));
7571 assert!(
7572 pressed.get(),
7573 "nothing here claims a pan, so no delay applies"
7574 );
7575
7576 tree.dispatch_pointer(contact(
7577 id,
7578 PointerPhase::Move,
7579 outside,
7580 EventTime::from_millis(20),
7581 ));
7582 assert!(!pressed.get(), "the press has left its target");
7583 assert!(!tree.press_is_inside(w));
7584 assert_eq!(
7585 tree.pressed_by(w),
7586 Some(id),
7587 "the contact still holds the press — it is the *visual* that is off",
7588 );
7589
7590 tree.dispatch_pointer(contact(
7591 id,
7592 PointerPhase::Move,
7593 inside,
7594 EventTime::from_millis(40),
7595 ));
7596 assert!(pressed.get(), "sliding back on restores it");
7597
7598 tree.dispatch_pointer(contact(
7599 id,
7600 PointerPhase::Up,
7601 inside,
7602 EventTime::from_millis(60),
7603 ));
7604 assert!(!pressed.get());
7605 }
7606
7607 #[test]
7610 fn a_second_contact_cannot_clear_the_first_visual() {
7611 let (mut tree, _clock) = tree_on_a_clock();
7612 let (w, pressed) = tappable(&mut tree);
7613 tree.layout(SizeProposal::exact(100.0, 50.0));
7614 let bounds = tree.bounds(w);
7615 let first_at = Point::new(bounds.x + 20.0, bounds.y + 25.0);
7616 let second_at = Point::new(bounds.x + 70.0, bounds.y + 25.0);
7617
7618 let first = contact_id();
7619 let second = contact_id();
7620 tree.dispatch_pointer(contact(
7621 first,
7622 PointerPhase::Down,
7623 first_at,
7624 EventTime::ZERO,
7625 ));
7626 assert!(pressed.get());
7627 assert_eq!(tree.pressed_by(w), Some(first));
7628
7629 tree.dispatch_pointer(contact(
7630 second,
7631 PointerPhase::Down,
7632 second_at,
7633 EventTime::from_millis(10),
7634 ));
7635 assert_eq!(
7636 tree.pressed_by(w),
7637 Some(first),
7638 "under `MultiContact::First` the second contact is terminated before \
7639 it reaches the arena at all",
7640 );
7641
7642 tree.dispatch_pointer(contact(
7643 second,
7644 PointerPhase::Up,
7645 second_at,
7646 EventTime::from_millis(20),
7647 ));
7648 assert!(
7649 pressed.get(),
7650 "so the second contact's release cannot clear a visual it never owned",
7651 );
7652 assert_eq!(tree.pressed_by(w), Some(first));
7653
7654 tree.dispatch_pointer(contact(
7655 first,
7656 PointerPhase::Up,
7657 first_at,
7658 EventTime::from_millis(30),
7659 ));
7660 assert!(!pressed.get(), "the owner's release does clear it");
7661 }
7662
7663 #[test]
7666 fn a_cancel_clears_the_visual() {
7667 let (mut tree, _clock) = tree_on_a_clock();
7668 let (w, pressed) = tappable(&mut tree);
7669 tree.layout(SizeProposal::exact(100.0, 50.0));
7670 let center = tree.bounds(w).center();
7671
7672 let id = contact_id();
7673 tree.dispatch_pointer(contact(id, PointerPhase::Down, center, EventTime::ZERO));
7674 assert!(pressed.get());
7675
7676 tree.dispatch_pointer(contact(
7677 id,
7678 PointerPhase::Cancel,
7679 center,
7680 EventTime::from_millis(20),
7681 ));
7682 assert!(!pressed.get(), "a press that was taken away is not painted");
7683 assert_eq!(tree.pressed_by(w), None);
7684 }
7685
7686 #[test]
7689 fn a_pan_claim_clears_the_visual() {
7690 let (mut tree, clock) = tree_on_a_clock();
7691 let (row, pressed) = tappable(&mut tree);
7692 let list = tree.add(
7693 StackWidget::new()
7694 .child(row)
7695 .pan_claim(PanClaim::vertical())
7696 .on_scroll(|_e, _c| EventResponse::Handled),
7697 );
7698 tree.layout(SizeProposal::exact(200.0, 400.0));
7699 let _ = list;
7700 let start = Point::new(100.0, 200.0);
7701
7702 let id = contact_id();
7703 tree.dispatch_pointer(contact(id, PointerPhase::Down, start, EventTime::ZERO));
7704 assert!(tree.press_pending(row));
7706 clock.set(EventTime::from_millis(100));
7707 tree.tick_gestures(std::time::Instant::now());
7708 assert!(pressed.get(), "…and appears once the delay has elapsed");
7709
7710 tree.dispatch_pointer(contact(
7712 id,
7713 PointerPhase::Move,
7714 Point::new(100.0, 260.0),
7715 EventTime::from_millis(120),
7716 ));
7717 assert!(
7718 !pressed.get(),
7719 "the row lost the press to the list and must stop advertising it",
7720 );
7721 assert_eq!(tree.pressed_by(row), None);
7722 }
7723
7724 #[test]
7733 fn a_button_the_control_cannot_act_on_raises_no_visual() {
7734 let taps = Rc::new(Cell::new(0u32));
7735 let count = taps.clone();
7736 let mut tree = WidgetTree::new();
7737 let w = tree.add(FillWidget::new().focusable().on_tap(move |_e, _c| {
7738 count.set(count.get() + 1);
7739 }));
7740 let pressed = tree.pressed_signal(w);
7741 tree.layout(SizeProposal::exact(100.0, 50.0));
7742 let center = tree.bounds(w).center();
7743
7744 for button in [
7745 PointerButton::Middle,
7746 PointerButton::Back,
7747 PointerButton::Forward,
7748 ] {
7749 tree.dispatch_event(WidgetEvent::pointer_down(center, button, Modifiers::NONE));
7750 assert!(
7751 !pressed.get(),
7752 "{button:?} cannot activate an `on_tap` node, so it must not light one up",
7753 );
7754 assert_eq!(tree.pressed_by(w), None, "and owns no visual to clear");
7755 tree.dispatch_event(WidgetEvent::pointer_up(center, button, Modifiers::NONE));
7756 assert_eq!(
7757 taps.get(),
7758 0,
7759 "the tap recognizer refuses {button:?} too — that is the point",
7760 );
7761 }
7762
7763 tree.dispatch_event(WidgetEvent::pointer_down(
7765 center,
7766 PointerButton::Primary,
7767 Modifiers::NONE,
7768 ));
7769 assert!(pressed.get(), "a primary press lights up as it always has");
7770 assert_eq!(tree.pressed_by(w), Some(PointerId::MOUSE));
7771 tree.dispatch_event(WidgetEvent::pointer_up(
7772 center,
7773 PointerButton::Primary,
7774 Modifiers::NONE,
7775 ));
7776 assert!(!pressed.get());
7777 assert_eq!(taps.get(), 1);
7778 }
7779
7780 #[test]
7784 fn a_secondary_press_with_no_context_menu_raises_no_visual() {
7785 let mut tree = WidgetTree::new();
7786 let (w, pressed) = tappable(&mut tree);
7787 tree.layout(SizeProposal::exact(100.0, 50.0));
7788 let center = tree.bounds(w).center();
7789
7790 tree.dispatch_event(WidgetEvent::pointer_down(
7791 center,
7792 PointerButton::Secondary,
7793 Modifiers::NONE,
7794 ));
7795 assert!(
7796 !pressed.get(),
7797 "nothing opened, and nothing may look pressed either",
7798 );
7799 assert_eq!(tree.pressed_by(w), None);
7800 assert!(!tree.press_is_inside(w));
7801 }
7802
7803 #[test]
7807 fn a_widened_mask_widens_the_visual_with_it() {
7808 use crate::event::ButtonMask;
7809
7810 let mut tree = WidgetTree::new();
7811 let w = tree.add(
7812 FillWidget::new()
7813 .on_tap(|_e, _c| {})
7814 .accept_tap_buttons(ButtonMask::PRIMARY | ButtonMask::MIDDLE),
7815 );
7816 let pressed = tree.pressed_signal(w);
7817 tree.layout(SizeProposal::exact(100.0, 50.0));
7818 let center = tree.bounds(w).center();
7819
7820 tree.dispatch_event(WidgetEvent::pointer_down(
7821 center,
7822 PointerButton::Middle,
7823 Modifiers::NONE,
7824 ));
7825 assert!(pressed.get(), "this node really does act on a middle-click");
7826
7827 tree.dispatch_event(WidgetEvent::pointer_up(
7828 center,
7829 PointerButton::Middle,
7830 Modifiers::NONE,
7831 ));
7832 assert!(!pressed.get());
7833 }
7834
7835 #[test]
7841 fn a_press_that_raises_no_visual_still_defers_its_focus() {
7842 use teksilo_tokens::{PenKind, PointerKind};
7843
7844 let (mut tree, _clock) = tree_on_a_clock();
7845 let (w, pressed) = tappable(&mut tree);
7846 tree.layout(SizeProposal::exact(100.0, 50.0));
7847 let center = tree.bounds(w).center();
7848
7849 let id = contact_id();
7850 let barrel = |phase, t| PointerSample {
7851 pointer: PointerInfo {
7852 kind: PointerKind::Pen(PenKind::Pen),
7853 ..PointerInfo::touch(id, t)
7854 },
7855 phase,
7856 position: center,
7857 button: Some(PointerButton::Secondary),
7858 modifiers: Modifiers::NONE,
7859 coalesced: Vec::new(),
7860 };
7861
7862 tree.dispatch_pointer(barrel(PointerPhase::Down, EventTime::ZERO));
7863 assert!(!pressed.get(), "the barrel button activates nothing here");
7864 assert_eq!(tree.pressed_by(w), None);
7865 assert_eq!(
7866 tree.focused(),
7867 None,
7868 "a direct pointer has chosen nothing until it lifts",
7869 );
7870
7871 tree.dispatch_pointer(barrel(PointerPhase::Up, EventTime::from_millis(40)));
7872 assert_eq!(
7873 tree.focused(),
7874 Some(w),
7875 "the deferral is not button-gated: the release landed where the press did",
7876 );
7877 assert_eq!(
7878 tree.focus_origin().and_then(FocusOrigin::pointer_kind),
7879 Some(PointerKind::Pen(PenKind::Pen)),
7880 );
7881 }
7882
7883 #[test]
7890 fn the_feedback_delay_applies_only_inside_a_claimant() {
7891 let (mut tree, _clock) = tree_on_a_clock();
7893 let (w, pressed) = tappable(&mut tree);
7894 tree.layout(SizeProposal::exact(200.0, 400.0));
7895 let id = contact_id();
7896 let at = tree.bounds(w).center();
7897 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
7898 assert!(
7899 !tree.press_pending(w),
7900 "no claimant above it, so nothing to rule out",
7901 );
7902 assert!(pressed.get());
7903
7904 let (mut tree, clock) = tree_on_a_clock();
7906 let (row, pressed) = tappable(&mut tree);
7907 let list = tree.add(
7908 StackWidget::new()
7909 .child(row)
7910 .pan_claim(PanClaim::vertical())
7911 .on_scroll(|_e, _c| EventResponse::Handled),
7912 );
7913 tree.layout(SizeProposal::exact(200.0, 400.0));
7914 let _ = list;
7915 let id = contact_id();
7916 let at = Point::new(100.0, 200.0);
7917 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
7918 assert!(tree.press_pending(row), "a finger might be about to scroll");
7919 assert!(!pressed.get(), "so the row does not flash");
7920 assert!(
7921 tree.press_is_inside(row),
7922 "the press is real; only its visual is being withheld",
7923 );
7924
7925 clock.set(EventTime::from_millis(99));
7926 tree.tick_gestures(std::time::Instant::now());
7927 assert!(!pressed.get(), "99 ms is under the 100 ms delay");
7928
7929 clock.set(EventTime::from_millis(100));
7930 tree.tick_gestures(std::time::Instant::now());
7931 assert!(pressed.get(), "and 100 ms is it");
7932 assert!(!tree.press_pending(row));
7933 }
7934
7935 #[test]
7938 fn a_mouse_inside_a_claimant_never_waits() {
7939 let mut tree = WidgetTree::new();
7940 let (row, pressed) = tappable(&mut tree);
7941 let list = tree.add(
7942 StackWidget::new()
7943 .child(row)
7944 .pan_claim(PanClaim::vertical())
7945 .on_scroll(|_e, _c| EventResponse::Handled),
7946 );
7947 tree.layout(SizeProposal::exact(200.0, 400.0));
7948 let _ = list;
7949
7950 tree.dispatch_event(WidgetEvent::pointer_down(
7951 Point::new(100.0, 200.0),
7952 PointerButton::Primary,
7953 Modifiers::NONE,
7954 ));
7955 assert!(!tree.press_pending(row));
7956 assert!(pressed.get(), "Compact with a mouse is exactly as it was");
7957 }
7958
7959 #[test]
7966 fn a_handler_can_read_the_press_it_is_inside() {
7967 let (mut tree, _clock) = tree_on_a_clock();
7968 let seen: Rc<RefCell<Vec<(bool, bool, bool)>>> = Rc::new(RefCell::new(Vec::new()));
7969 let log = seen.clone();
7970 let row = tree.add(FillWidget::new().on_tap(|_e, _c| {}).on_pointer_event(
7971 move |event, ctx| {
7972 if matches!(event, WidgetEvent::PointerUp { .. }) {
7973 log.borrow_mut().push((
7974 ctx.is_pressed(),
7975 ctx.press_is_inside(),
7976 ctx.press_pending(),
7977 ));
7978 }
7979 EventResponse::Ignored
7980 },
7981 ));
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 let id = contact_id();
7992 let at = Point::new(100.0, 200.0);
7993 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
7994 tree.dispatch_pointer(contact(
7995 id,
7996 PointerPhase::Up,
7997 at,
7998 EventTime::from_millis(30),
7999 ));
8000
8001 assert_eq!(
8002 seen.borrow().as_slice(),
8003 &[(false, true, true)],
8004 "released inside the claimant before the delay elapsed: inside, \
8005 pending, and therefore not yet showing",
8006 );
8007 }
8008
8009 #[test]
8012 fn a_handler_outside_a_press_reads_nothing() {
8013 let mut tree = WidgetTree::new();
8014 let asked = Rc::new(Cell::new(false));
8015 let flag = asked.clone();
8016 let w = tree.add(FillWidget::new().focusable().on_key(move |_e, ctx| {
8017 flag.set(ctx.is_pressed() || ctx.press_is_inside() || ctx.press_pending());
8018 EventResponse::Ignored
8019 }));
8020 tree.layout(SizeProposal::exact(100.0, 50.0));
8021 tree.focus(w);
8022 tree.press_key(Key::ArrowDown, Modifiers::NONE);
8023 assert!(!asked.get());
8024 }
8025
8026 #[test]
8030 fn a_completed_press_leaves_the_detector_clean() {
8031 let (mut tree, _clock) = tree_on_a_clock();
8032 let (w, pressed) = tappable(&mut tree);
8033 tree.layout(SizeProposal::exact(100.0, 50.0));
8034 let at = tree.bounds(w).center();
8035
8036 let id = contact_id();
8037 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8038 assert!(pressed.get());
8039 tree.dispatch_pointer(contact(
8040 id,
8041 PointerPhase::Up,
8042 at,
8043 EventTime::from_millis(30),
8044 ));
8045 tree.assert_no_leaked_pointer_state();
8046 }
8047
8048 #[test]
8052 fn the_detector_reports_a_press_still_held() {
8053 let (mut tree, _clock) = tree_on_a_clock();
8054 let (w, _pressed) = tappable(&mut tree);
8055 tree.layout(SizeProposal::exact(100.0, 50.0));
8056 let at = tree.bounds(w).center();
8057
8058 let id = contact_id();
8059 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8060 let held = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
8061 tree.assert_no_leaked_pointer_state()
8062 }));
8063 let message = *held
8064 .expect_err("a live press is leaked state")
8065 .downcast::<String>()
8066 .expect("the detector panics with a message");
8067 assert!(
8068 message.contains("is still pressed by"),
8069 "the press is named in the report, got: {message}",
8070 );
8071 }
8072
8073 #[derive(Debug)]
8077 struct SideBySide {
8078 children: Vec<WidgetId>,
8079 }
8080
8081 impl crate::widget::Widget for SideBySide {
8082 fn layout_response(
8083 &self,
8084 proposal: SizeProposal,
8085 _ctx: &crate::widget::LayoutContext,
8086 ) -> crate::widget::LayoutResponse {
8087 proposal.resolve(0.0, 0.0).into()
8088 }
8089
8090 fn place_children(
8091 &self,
8092 bounds: teksilo_canvas::Rect,
8093 _proposal: SizeProposal,
8094 children: &mut [crate::widget::WidgetPlacement],
8095 _ctx: &crate::widget::LayoutContext,
8096 ) {
8097 let n = children.len().max(1) as f32;
8098 let w = bounds.width / n;
8099 for (i, child) in children.iter_mut().enumerate() {
8100 child.origin = Point::new(bounds.x + w * i as f32, bounds.y);
8101 child.size = teksilo_canvas::Size::new(w, bounds.height);
8102 }
8103 }
8104
8105 fn children(&self) -> Vec<WidgetId> {
8106 self.children.clone()
8107 }
8108 }
8109}
8110
8111#[cfg(test)]
8112mod overlay_release_dismissal_tests {
8113 use std::cell::Cell;
8123 use std::rc::Rc;
8124
8125 use teksilo_canvas::{Point, Rect, Size, SizeProposal};
8126
8127 use crate::WidgetId;
8128 use crate::event::{Modifiers, PointerButton, WidgetEvent};
8129 use crate::overlay::{DismissBehavior, OverlayLayer, OverlayPlacement, OverlayRequest};
8130 use crate::pointer::{
8131 BackendDeviceKey, EventTime, PointerId, PointerIdAllocator, PointerInfo, PointerPhase,
8132 PointerSample,
8133 };
8134 use crate::test_widgets::FillWidget;
8135 use crate::widget::{LayoutContext, LayoutResponse, Widget};
8136 use crate::widget_builder::WidgetBuilder;
8137 use crate::widget_tree::WidgetTree;
8138
8139 #[derive(Debug)]
8146 struct Panel(Size);
8147
8148 impl Widget for Panel {
8149 fn layout_response(&self, _proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
8150 self.0.into()
8151 }
8152 }
8153
8154 #[derive(Debug)]
8162 struct PageAndTrigger {
8163 children: Vec<WidgetId>,
8164 }
8165
8166 impl Widget for PageAndTrigger {
8167 fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
8168 proposal.resolve(0.0, 0.0).into()
8169 }
8170
8171 fn place_children(
8172 &self,
8173 bounds: Rect,
8174 _proposal: SizeProposal,
8175 children: &mut [crate::widget::WidgetPlacement],
8176 _ctx: &LayoutContext,
8177 ) {
8178 if let Some(page) = children.get_mut(0) {
8179 page.origin = bounds.origin();
8180 page.size = bounds.size();
8181 }
8182 if let Some(trigger) = children.get_mut(1) {
8183 trigger.origin = bounds.origin();
8184 trigger.size = Size::new(10.0, 10.0);
8185 }
8186 }
8187
8188 fn children(&self) -> Vec<WidgetId> {
8189 self.children.clone()
8190 }
8191 }
8192
8193 fn contact_id(n: u64) -> PointerId {
8194 PointerIdAllocator::global().begin(BackendDeviceKey::new(0x0FA2), n)
8195 }
8196
8197 fn touch(id: PointerId, phase: PointerPhase, at: Point) -> PointerSample {
8198 PointerSample {
8199 pointer: PointerInfo::touch(id, EventTime::ZERO),
8200 phase,
8201 position: at,
8202 button: None,
8203 modifiers: Modifiers::NONE,
8204 coalesced: Vec::new(),
8205 }
8206 }
8207
8208 fn page_with_a_menu() -> (WidgetTree, WidgetId, Rc<Cell<u32>>) {
8215 let mut tree = WidgetTree::new();
8216 let taps = Rc::new(Cell::new(0u32));
8217 let counter = taps.clone();
8218 let page = tree.add(
8219 FillWidget::new()
8220 .focusable()
8221 .on_tap(move |_e, _c| counter.set(counter.get() + 1)),
8222 );
8223 let trigger = tree.add(Panel(Size::new(10.0, 10.0)));
8226 let _root = tree.add(PageAndTrigger {
8227 children: vec![page, trigger],
8228 });
8229 let menu = tree.add(Panel(Size::new(200.0, 200.0)));
8230 tree.show_overlay(OverlayRequest {
8231 content_id: menu,
8232 anchor: trigger,
8233 placement: OverlayPlacement::AtPointer(Point::new(100.0, 100.0)),
8234 dismiss: DismissBehavior::ClickOutside,
8235 layer: OverlayLayer::InTree,
8236 parent_overlay: None,
8237 on_dismiss: None,
8238 fade_duration: None,
8239 });
8240 tree.layout(SizeProposal::exact(800.0, 600.0));
8241 assert_eq!(tree.active_overlays().len(), 1, "the menu is open");
8242 (tree, page, taps)
8243 }
8244
8245 fn outside() -> Point {
8246 Point::new(600.0, 500.0)
8247 }
8248
8249 fn inside_menu() -> Point {
8250 Point::new(150.0, 150.0)
8251 }
8252
8253 #[test]
8260 fn the_suppressed_down_leaves_nothing_pressed_or_captured_beneath() {
8261 let (mut tree, page, taps) = page_with_a_menu();
8262 let finger = contact_id(1);
8263 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8264
8265 assert_eq!(
8266 tree.active_overlays().len(),
8267 1,
8268 "the press does not close the menu; the release does"
8269 );
8270 assert_eq!(tree.pressed_by(page), None, "nothing beneath is pressed");
8271 let entry = tree.pointers.get(finger).expect("the contact is live");
8272 assert_eq!(entry.captured_by, None, "nothing beneath captured it");
8273 assert!(entry.sequence.is_none(), "no arbitration was opened");
8274 assert_eq!(taps.get(), 0);
8275 }
8276
8277 #[test]
8279 fn a_touch_tap_outside_a_menu_closes_it_and_actuates_nothing() {
8280 let (mut tree, page, taps) = page_with_a_menu();
8281 let finger = contact_id(2);
8282 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8283 tree.dispatch_pointer(touch(finger, PointerPhase::Up, outside()));
8284
8285 assert!(tree.active_overlays().is_empty(), "the menu closed");
8286 assert_eq!(taps.get(), 0, "the page beneath was never tapped");
8287 assert_eq!(tree.pressed_by(page), None);
8288 tree.assert_no_leaked_pointer_state();
8289 }
8290
8291 #[test]
8294 fn a_cancelled_press_aborts_the_arm() {
8295 let (mut tree, page, taps) = page_with_a_menu();
8296 let finger = contact_id(3);
8297 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8298 tree.dispatch_pointer(touch(finger, PointerPhase::Cancel, outside()));
8299
8300 assert_eq!(tree.active_overlays().len(), 1, "the menu survives");
8301 assert_eq!(taps.get(), 0);
8302 assert_eq!(tree.pressed_by(page), None);
8303 tree.assert_no_leaked_pointer_state();
8304 }
8305
8306 #[test]
8310 fn a_press_slid_onto_the_menu_dismisses_nothing() {
8311 let (mut tree, _page, taps) = page_with_a_menu();
8312 let finger = contact_id(4);
8313 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8314 tree.dispatch_pointer(touch(finger, PointerPhase::Move, inside_menu()));
8315 tree.dispatch_pointer(touch(finger, PointerPhase::Up, inside_menu()));
8316
8317 assert_eq!(tree.active_overlays().len(), 1, "the menu survives");
8318 assert_eq!(taps.get(), 0);
8319 tree.assert_no_leaked_pointer_state();
8320 }
8321
8322 #[test]
8325 fn a_second_contact_cannot_dismiss_what_the_first_is_manipulating() {
8326 let (mut tree, _page, _taps) = page_with_a_menu();
8327 let first = contact_id(5);
8328 let second = contact_id(6);
8329
8330 tree.dispatch_pointer(touch(first, PointerPhase::Down, inside_menu()));
8331 assert!(
8332 tree.pointers
8333 .get(first)
8334 .is_some_and(|e| e.sequence.is_some()),
8335 "the first contact holds a live press inside the menu"
8336 );
8337
8338 tree.dispatch_pointer(touch(second, PointerPhase::Down, outside()));
8339 tree.dispatch_pointer(touch(second, PointerPhase::Up, outside()));
8340 assert_eq!(
8341 tree.active_overlays().len(),
8342 1,
8343 "the menu the first finger is holding must not close under it"
8344 );
8345
8346 tree.dispatch_pointer(touch(first, PointerPhase::Up, inside_menu()));
8349 let third = contact_id(7);
8350 tree.dispatch_pointer(touch(third, PointerPhase::Down, outside()));
8351 tree.dispatch_pointer(touch(third, PointerPhase::Up, outside()));
8352 assert!(tree.active_overlays().is_empty());
8353 tree.assert_no_leaked_pointer_state();
8354 }
8355
8356 #[test]
8360 fn an_arm_is_not_itself_a_press_that_blocks_another_contact() {
8361 let (mut tree, _page, _taps) = page_with_a_menu();
8362 let first = contact_id(8);
8363 let second = contact_id(9);
8364
8365 tree.dispatch_pointer(touch(first, PointerPhase::Down, outside()));
8366 assert!(
8367 tree.overlay_manager().has_armed_dismiss(first),
8368 "the first contact armed"
8369 );
8370 assert!(tree.busy_press_points(second).is_empty());
8372
8373 tree.dispatch_pointer(touch(second, PointerPhase::Down, outside()));
8374 assert!(tree.overlay_manager().has_armed_dismiss(second));
8375 tree.dispatch_pointer(touch(second, PointerPhase::Up, outside()));
8376 assert!(tree.active_overlays().is_empty());
8377
8378 tree.dispatch_pointer(touch(first, PointerPhase::Up, outside()));
8381 tree.assert_no_leaked_pointer_state();
8382 }
8383
8384 #[test]
8387 fn a_touch_inside_the_menu_arms_nothing() {
8388 let (mut tree, _page, _taps) = page_with_a_menu();
8389 let finger = contact_id(10);
8390 tree.dispatch_pointer(touch(finger, PointerPhase::Down, inside_menu()));
8391 assert!(!tree.overlay_manager().has_armed_dismiss(finger));
8392 tree.dispatch_pointer(touch(finger, PointerPhase::Up, inside_menu()));
8393 assert_eq!(tree.active_overlays().len(), 1);
8394 tree.assert_no_leaked_pointer_state();
8395 }
8396
8397 #[test]
8407 fn a_contact_with_no_press_still_ends_when_the_platform_revokes_it() {
8408 let mut tree = WidgetTree::new();
8409 tree.layout(SizeProposal::exact(800.0, 600.0));
8410 let finger = contact_id(99);
8411 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8412 tree.dispatch_pointer(touch(finger, PointerPhase::Cancel, outside()));
8413 tree.assert_no_leaked_pointer_state();
8414 }
8415
8416 #[test]
8423 fn a_mouse_click_outside_a_menu_dismisses_on_the_press_and_falls_through() {
8424 let (mut tree, _page, taps) = page_with_a_menu();
8425 tree.dispatch_event(WidgetEvent::pointer_down(
8426 outside(),
8427 PointerButton::Primary,
8428 Modifiers::NONE,
8429 ));
8430 assert!(
8431 tree.active_overlays().is_empty(),
8432 "the mouse still dismisses on the press"
8433 );
8434 assert!(
8435 tree.pointers
8436 .get(PointerId::MOUSE)
8437 .is_some_and(|e| e.sequence.is_some()),
8438 "and the press still reaches the page beneath"
8439 );
8440 tree.dispatch_event(WidgetEvent::pointer_up(
8441 outside(),
8442 PointerButton::Primary,
8443 Modifiers::NONE,
8444 ));
8445 assert_eq!(taps.get(), 1, "the control beneath activated");
8446 assert!(
8447 !tree.overlay_manager().has_armed_dismiss(PointerId::MOUSE),
8448 "a mouse never arms"
8449 );
8450 tree.assert_no_leaked_pointer_state();
8451 }
8452
8453 #[test]
8460 fn a_context_menu_avoids_the_contact_that_opened_it() {
8461 fn menu_bounds(pointer: PointerInfo, at: Point) -> Rect {
8462 let mut tree = WidgetTree::new();
8463 let page = tree.add(
8464 FillWidget::new()
8465 .focusable()
8466 .context_menu(|_p, _ctx| Some(Box::new(Panel(Size::new(200.0, 160.0))))),
8467 );
8468 let _ = page;
8469 tree.layout(SizeProposal::exact(800.0, 600.0));
8470 tree.dispatch_pointer(PointerSample {
8471 pointer,
8472 phase: PointerPhase::Down,
8473 position: at,
8474 button: Some(PointerButton::Secondary),
8475 modifiers: Modifiers::NONE,
8476 coalesced: Vec::new(),
8477 });
8478 tree.layout(SizeProposal::exact(800.0, 600.0));
8479 let id = *tree
8480 .active_overlays()
8481 .first()
8482 .expect("the context menu opened");
8483 tree.overlay_manager().bounds_for(id).expect("bounds")
8484 }
8485
8486 let at = Point::new(400.0, 300.0);
8487 let finger = menu_bounds(PointerInfo::touch(contact_id(11), EventTime::ZERO), at);
8488 let contact = crate::overlay::rect_centred_on(at, crate::overlay::ASSUMED_CONTACT_PATCH);
8489 assert!(
8490 finger.x < contact.x && finger.right() <= contact.x,
8491 "a touch menu clears the contact patch: {finger:?} vs {contact:?}"
8492 );
8493
8494 let mouse = menu_bounds(PointerInfo::mouse(EventTime::ZERO), at);
8495 assert_eq!(
8496 (mouse.x, mouse.y),
8497 (at.x, at.y),
8498 "a mouse menu still opens with its corner on the pointer"
8499 );
8500 }
8501}
8502
8503#[cfg(test)]
8519mod pass_through_overlay_tests {
8520 use teksilo_canvas::{Point, Rect, SizeProposal};
8521
8522 use crate::WidgetId;
8523 use crate::overlay::{DismissBehavior, OverlayLayer, OverlayPlacement, OverlayRequest};
8524 use crate::test_widgets::FillWidget;
8525 use crate::widget::{LayoutContext, LayoutResponse, Widget};
8526 use crate::widget_builder::WidgetBuilder;
8527 use crate::widget_tree::WidgetTree;
8528
8529 #[derive(Debug)]
8532 struct HandleLayer {
8533 handle: WidgetId,
8534 at: Rect,
8535 }
8536
8537 impl Widget for HandleLayer {
8538 fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
8539 proposal.resolve(0.0, 0.0).into()
8540 }
8541
8542 fn place_children(
8543 &self,
8544 _bounds: Rect,
8545 _proposal: SizeProposal,
8546 children: &mut [crate::widget::WidgetPlacement],
8547 _ctx: &LayoutContext,
8548 ) {
8549 if let Some(child) = children.get_mut(0) {
8550 child.origin = self.at.origin();
8551 child.size = self.at.size();
8552 }
8553 }
8554
8555 fn children(&self) -> Vec<WidgetId> {
8556 vec![self.handle]
8557 }
8558 }
8559
8560 const HANDLE: Rect = Rect {
8561 x: 10.0,
8562 y: 10.0,
8563 width: 20.0,
8564 height: 20.0,
8565 };
8566
8567 #[derive(Clone, Copy, PartialEq)]
8569 enum RootFlag {
8570 PassThrough,
8572 HitTransparent,
8575 }
8576
8577 fn field_under_a_layer(flag: RootFlag) -> (WidgetTree, WidgetId, WidgetId) {
8583 let mut tree = WidgetTree::new();
8584 let field = tree.add(FillWidget::new());
8585 let handle = tree.add(FillWidget::new());
8586 let layer = HandleLayer { handle, at: HANDLE };
8587 let layer = match flag {
8588 RootFlag::PassThrough => tree.add(layer.event_pass_through(true)),
8589 RootFlag::HitTransparent => tree.add(layer.hit_transparent(true)),
8590 };
8591 tree.show_overlay(OverlayRequest {
8592 content_id: layer,
8593 anchor: field,
8594 placement: OverlayPlacement::FullViewport,
8595 dismiss: DismissBehavior::Manual,
8596 layer: OverlayLayer::InTree,
8597 parent_overlay: None,
8598 on_dismiss: None,
8599 fade_duration: None,
8600 });
8601 tree.layout(SizeProposal::exact(400.0, 200.0));
8602 assert_eq!(tree.active_overlays().len(), 1, "the layer is up");
8603 (tree, field, handle)
8604 }
8605
8606 #[test]
8608 fn a_pass_through_layer_hands_back_what_it_did_not_claim() {
8609 let (tree, field, handle) = field_under_a_layer(RootFlag::PassThrough);
8610
8611 assert_eq!(
8612 tree.hit_test(Point::new(15.0, 15.0)),
8613 Some(handle),
8614 "the layer must still win the point its own child covers"
8615 );
8616 assert_eq!(
8617 tree.hit_test(Point::new(200.0, 100.0)),
8618 Some(field),
8619 "a press the layer did not claim must reach the surface beneath it"
8620 );
8621 }
8622
8623 #[test]
8634 fn a_pass_through_root_still_hears_a_press_that_landed_on_its_child() {
8635 use std::cell::Cell;
8636 use std::rc::Rc;
8637
8638 let seen = Rc::new(Cell::new(0u32));
8639 let counter = seen.clone();
8640 let mut tree = WidgetTree::new();
8641 let field = tree.add(FillWidget::new());
8642 let handle = tree.add(FillWidget::new().on_tap(|_e, _c| {}));
8643 let layer = tree.add(
8644 HandleLayer { handle, at: HANDLE }
8645 .event_pass_through(true)
8646 .on_pointer_event(move |_event, _ctx| {
8647 counter.set(counter.get() + 1);
8648 crate::event::EventResponse::Ignored
8649 }),
8650 );
8651 tree.show_overlay(OverlayRequest {
8652 content_id: layer,
8653 anchor: field,
8654 placement: OverlayPlacement::FullViewport,
8655 dismiss: DismissBehavior::Manual,
8656 layer: OverlayLayer::InTree,
8657 parent_overlay: None,
8658 on_dismiss: None,
8659 fade_duration: None,
8660 });
8661 tree.layout(SizeProposal::exact(400.0, 200.0));
8662
8663 tree.pointer_down_button(Point::new(15.0, 15.0), crate::event::PointerButton::Primary);
8664 assert!(
8665 seen.get() > 0,
8666 "a pass-through root heard nothing about a press on its own child"
8667 );
8668
8669 let after_child = seen.get();
8670 tree.pointer_up_button(Point::new(15.0, 15.0), crate::event::PointerButton::Primary);
8671 tree.pointer_down_button(
8674 Point::new(200.0, 100.0),
8675 crate::event::PointerButton::Primary,
8676 );
8677 assert_eq!(
8678 seen.get(),
8679 after_child + 1,
8680 "the press-and-lift on the child accounts for the count, and the \
8681 press on the surface beneath added nothing"
8682 );
8683 }
8684
8685 #[test]
8699 fn a_hit_transparent_root_does_not_get_the_fall_through() {
8700 let (tree, field, _handle) = field_under_a_layer(RootFlag::HitTransparent);
8701
8702 assert_eq!(
8704 tree.hit_test(Point::new(15.0, 15.0)),
8705 None,
8706 "hit_transparent must exclude the subtree, or this fixture is not \
8707 testing the gate"
8708 );
8709 assert_eq!(
8710 tree.hit_test(Point::new(200.0, 100.0)),
8711 None,
8712 "the fall-through reached past an overlay that did not ask for it"
8713 );
8714 let _ = field;
8715 }
8716}