1use super::*;
9
10use crate::event::ButtonMask;
11use crate::gesture::{GestureEvent, RawPointerEvent, TapEvent};
12
13fn fire_event_handler_both(
17 external: &mut Option<Box<dyn FnMut(&WidgetEvent, &mut EventContext) -> EventResponse>>,
18 own: &mut Option<Box<dyn FnMut(&WidgetEvent, &mut EventContext) -> EventResponse>>,
19 event: &WidgetEvent,
20 ctx: &mut EventContext,
21) -> EventResponse {
22 let r1 = external
23 .as_mut()
24 .map(|h| h(event, ctx))
25 .unwrap_or(EventResponse::Ignored);
26 let r2 = own
27 .as_mut()
28 .map(|h| h(event, ctx))
29 .unwrap_or(EventResponse::Ignored);
30 if r1 == EventResponse::Handled || r2 == EventResponse::Handled {
31 EventResponse::Handled
32 } else {
33 EventResponse::Ignored
34 }
35}
36
37pub(super) enum QueuedDispatch {
44 Event {
51 event: WidgetEvent,
52 snapshot: Box<crate::pointer::InputSnapshot>,
53 },
54 Cancel {
65 pointer: crate::pointer::PointerId,
66 reason: crate::pointer::CancelReason,
67 recipient: Option<WidgetId>,
75 },
76}
77
78fn pointer_event_position(event: &WidgetEvent) -> Option<Point> {
84 match event {
85 WidgetEvent::PointerDown { position, .. }
86 | WidgetEvent::PointerUp { position, .. }
87 | WidgetEvent::PointerMove { position, .. } => Some(*position),
88 _ => None,
92 }
93}
94
95#[derive(Copy, Clone)]
100struct BubbleGates {
101 fire_on_pointer_event: bool,
109 arena_blocked: bool,
114}
115
116impl WidgetTree {
117 fn scope_distance_from_focus(&self, focus: WidgetId, scope_id: WidgetId) -> Option<usize> {
129 let mut hops = 0usize;
130 let mut current = Some(focus);
131 while let Some(id) = current {
132 if id == scope_id {
133 return Some(hops);
134 }
135 current = self.arena.parent(id);
136 hops += 1;
137 }
138 None
139 }
140
141 pub(crate) fn focus_or_modal(&self) -> Option<WidgetId> {
147 self.focused
148 .filter(|id| self.arena.is_active(*id))
149 .or_else(|| {
150 self.overlay_manager
151 .topmost_modal()
152 .map(|overlay| overlay.content_id)
153 })
154 }
155
156 fn select_shortcut_for_focus(
167 &self,
168 candidates: &[(&'static str, crate::shortcut::ShortcutScope, bool)],
169 ) -> Option<(&'static str, crate::shortcut::ShortcutScope, bool)> {
170 use crate::shortcut::ShortcutScope;
171 let mut best: Option<(usize, (&'static str, ShortcutScope, bool))> = None;
172 let focus = self.focus_or_modal();
179 let modal = focus.and_then(|f| self.intent_walk(f, &[]).2);
180 for &(id, scope, propagate) in candidates {
181 let score = match scope {
186 ShortcutScope::Global => {
187 let owners = self.shortcut_registry.owners(id);
188 (owners.is_empty()
189 || owners
190 .iter()
191 .any(|&owner| self.global_applies(owner, modal)))
192 .then_some(0usize)
193 }
194 ShortcutScope::Scoped(scope_id) => focus
195 .and_then(|f| self.scope_distance_from_focus(f, scope_id))
196 .map(|hops| usize::MAX - hops),
197 };
198 let Some(score) = score else { continue };
199 if best.as_ref().is_none_or(|(b, _)| score > *b) {
202 best = Some((score, (id, scope, propagate)));
203 }
204 }
205 best.map(|(_, c)| c)
206 }
207
208 pub fn dispatch_event_with_ops(
222 &mut self,
223 event: WidgetEvent,
224 ops: &mut dyn crate::window::WindowOps,
225 ) {
226 let mut snapshot = crate::pointer::InputSnapshot::from_event(&event);
229 if snapshot.pointer.time == crate::pointer::EventTime::ZERO {
234 snapshot.pointer.time = self.input_now();
235 }
236 self.dispatch_with_input_snapshot(event, snapshot, ops)
237 }
238
239 pub fn dispatch_event(&mut self, event: WidgetEvent) {
245 let mut noop = crate::window::NoopWindowOps;
246 self.dispatch_event_with_ops(event, &mut noop);
247 }
248
249 pub fn dispatch_pointer(&mut self, sample: crate::pointer::PointerSample) {
271 let mut noop = crate::window::NoopWindowOps;
272 self.dispatch_pointer_with_ops(sample, &mut noop);
273 }
274
275 pub fn dispatch_pointer_with_ops(
279 &mut self,
280 sample: crate::pointer::PointerSample,
281 ops: &mut dyn crate::window::WindowOps,
282 ) {
283 use crate::pointer::PointerPhase;
284
285 crate::trace_input!(
286 Samples,
287 "{:?} {:?} at {:?} buttons={:?} t={:?}",
288 sample.phase,
289 sample.pointer.id,
290 sample.position,
291 sample.pointer.buttons,
292 sample.pointer.time
293 );
294
295 let button = sample
299 .button
300 .unwrap_or(crate::event::PointerButton::Primary);
301 let event = match sample.phase {
302 PointerPhase::Down => WidgetEvent::PointerDown {
303 position: sample.position,
304 button,
305 modifiers: sample.modifiers,
306 pointer: sample.pointer,
307 },
308 PointerPhase::Move => WidgetEvent::PointerMove {
309 position: sample.position,
310 modifiers: sample.modifiers,
311 pointer: sample.pointer,
312 },
313 PointerPhase::Up => WidgetEvent::PointerUp {
314 position: sample.position,
315 button,
316 modifiers: sample.modifiers,
317 pointer: sample.pointer,
318 },
319 PointerPhase::Cancel => {
325 self.overlay_manager.abort_dismiss(sample.pointer.id);
330 self.cancel_pointer(
331 sample.pointer.id,
332 crate::pointer::CancelReason::Platform,
333 ops,
334 );
335 if !sample.pointer.kind.hovers() {
342 self.pointers.end(sample.pointer.id);
343 }
344 return;
345 }
346 };
347
348 if !self.pointers.would_admit(&sample.pointer) {
354 return;
355 }
356 let ends_pointer = matches!(sample.phase, PointerPhase::Up | PointerPhase::Cancel)
360 && !sample.pointer.kind.hovers();
361 let pointer_id = sample.pointer.id;
362
363 let snapshot = crate::pointer::InputSnapshot::from_pointer_sample(&sample);
364 self.dispatch_with_input_snapshot(event, snapshot, ops);
365
366 if ends_pointer {
367 self.pointers.end(pointer_id);
368 }
369 }
370
371 pub fn dispatch_scroll(&mut self, sample: crate::pointer::ScrollSample) {
379 let mut noop = crate::window::NoopWindowOps;
380 self.dispatch_scroll_with_ops(sample, &mut noop);
381 }
382
383 pub fn dispatch_scroll_with_ops(
386 &mut self,
387 sample: crate::pointer::ScrollSample,
388 ops: &mut dyn crate::window::WindowOps,
389 ) {
390 crate::trace_input!(
391 Samples,
392 "scroll {:?} {:?}/{:?} at {:?}",
393 sample.delta,
394 sample.phase,
395 sample.source,
396 sample.position
397 );
398
399 let event = WidgetEvent::Scroll {
400 delta: sample.delta,
401 modifiers: sample.modifiers,
402 window_position: sample.position,
403 phase: sample.phase,
404 pointer: sample.pointer,
405 };
406 let snapshot = crate::pointer::InputSnapshot::from_scroll_sample(&sample);
407 self.dispatch_with_input_snapshot(event, snapshot, ops);
408 }
409
410 pub fn pointer_left_window(&mut self, ops: &mut dyn crate::window::WindowOps) {
435 if self
439 .pointers
440 .hover_owner_id()
441 .and_then(|id| self.captured_by(id))
442 .is_some()
443 {
444 return;
445 }
446 let Some(hovered) = self.hovered_id() else {
447 return;
448 };
449 let leave = WidgetEvent::PointerLeave {
452 pointer: self.hover_transition_pointer(),
453 };
454 self.dispatch_to_widget(hovered, &leave, &mut *ops);
455 self.tooltip_pointer_leave(hovered, &mut *ops);
456 self.set_hovered(None);
457 self.update_hover_within_signals(Some(hovered), None);
458 }
459
460 pub(super) fn hover_transition_pointer(&self) -> crate::pointer::PointerInfo {
477 self.pointers
478 .hover_owner()
479 .map(|entry| entry.info)
480 .unwrap_or_else(|| crate::pointer::PointerInfo::mouse(self.input_now()))
481 }
482
483 fn dispatch_with_input_snapshot(
490 &mut self,
491 event: WidgetEvent,
492 snapshot: crate::pointer::InputSnapshot,
493 ops: &mut dyn crate::window::WindowOps,
494 ) {
495 self.pending_dispatch.push_back(QueuedDispatch::Event {
505 event,
506 snapshot: Box::new(snapshot),
507 });
508 if self.dispatch_depth > 0 {
509 return;
510 }
511 self.drain_pending_dispatch(&mut *ops);
512 }
513
514 pub(super) fn drain_pending_dispatch(&mut self, ops: &mut dyn crate::window::WindowOps) {
520 while let Some(queued) = self.pending_dispatch.pop_front() {
521 match queued {
522 QueuedDispatch::Event { event, snapshot } => {
523 self.run_one_dispatch(event, *snapshot, &mut *ops);
524 }
525 QueuedDispatch::Cancel {
526 pointer,
527 reason,
528 recipient,
529 } => {
530 self.run_one_cancel(pointer, reason, recipient, &mut *ops);
531 }
532 }
533 }
534 }
535
536 fn run_one_dispatch(
543 &mut self,
544 event: WidgetEvent,
545 snapshot: crate::pointer::InputSnapshot,
546 ops: &mut dyn crate::window::WindowOps,
547 ) {
548 let previous = std::mem::replace(&mut self.current_input, snapshot);
549 self.dispatch_depth += 1;
550 self.dispatch_event_impl(event, ops);
551 self.dispatch_depth -= 1;
552 self.current_input = previous;
553 }
554
555 fn dismiss_top_on_escape(&mut self, ops: &mut dyn crate::window::WindowOps) -> bool {
559 let Some((_id, content_ids, focus_restore)) =
560 self.overlay_manager.try_dismiss_top_on_escape()
561 else {
562 return false;
563 };
564 self.dormant_dismissed_content(&content_ids, ops);
565 if let Some(restore_id) = focus_restore
566 && self.arena.is_active(restore_id)
567 {
568 self.focus_ops(restore_id, ops);
569 }
570 true
571 }
572
573 #[doc(hidden)]
587 pub fn answer_close_request_with_modal(
588 &mut self,
589 ops: &mut dyn crate::window::WindowOps,
590 ) -> bool {
591 if !self.overlay_manager.has_open_modal() {
592 return false;
593 }
594 self.tooltip_escape_pressed();
597 self.inert_overlays_escape_pressed(ops);
598 self.dismiss_top_on_escape(ops);
599 true
600 }
601
602 fn dispatch_event_impl(&mut self, event: WidgetEvent, ops: &mut dyn crate::window::WindowOps) {
603 if let Some(position) = pointer_event_position(&event) {
609 let is_down = matches!(event, WidgetEvent::PointerDown { .. });
610 let is_move = matches!(event, WidgetEvent::PointerMove { .. });
611 if !self.admit_current_pointer(position, is_down, is_move) {
612 return;
613 }
614 if self.current_input.pointer.kind.hovers() {
618 self.claim_hover_owner_for_current(&mut *ops);
619 }
620 }
621
622 match &event {
626 WidgetEvent::KeyDown { .. } if !self.focus_visible.get() => {
627 self.focus_visible.set(true);
628 }
629 WidgetEvent::PointerDown { .. } if self.focus_visible.get() => {
630 self.focus_visible.set(false);
631 }
632 _ => {}
633 }
634
635 let overlay_back_key = match self.layout_direction {
641 crate::environment::LayoutDirection::RightToLeft => Key::ArrowRight,
642 crate::environment::LayoutDirection::LeftToRight => Key::ArrowLeft,
643 };
644 if let WidgetEvent::KeyDown { key, .. } = &event
645 && *key == overlay_back_key
646 {
647 let nested_menu_overlays = {
677 let ids: Vec<_> = self
678 .overlay_manager
679 .stack
680 .iter()
681 .filter(|o| o.band == crate::overlay::OverlayBand::Standard && !o.inert)
682 .map(|o| o.id)
683 .collect();
684 ids.into_iter()
685 .filter(|&id| !self.overlay_is_host_surface(id))
686 .count()
687 };
688 let top_id = self
695 .overlay_manager
696 .stack
697 .iter()
698 .rev()
699 .find(|o| !o.inert)
700 .map(|o| o.id);
701 let top_is_back_navigable = top_id.is_some_and(|id| !self.overlay_is_host_surface(id));
702 if nested_menu_overlays > 1
703 && top_is_back_navigable
704 && let Some(top_id) = top_id
705 {
706 let (content_ids, focus_restore) =
707 self.overlay_manager.dismiss_with_focus_restore_because(
708 top_id,
709 crate::overlay::DismissReason::Escape,
710 );
711 self.dormant_dismissed_content(&content_ids, &mut *ops);
712 if let Some(restore_id) = focus_restore
713 && self.arena.is_active(restore_id)
714 {
715 self.focus_ops(restore_id, &mut *ops);
716 }
717 return;
718 }
719 }
720
721 if let WidgetEvent::KeyDown {
727 key: Key::Escape, ..
728 } = &event
729 {
730 self.tooltip_escape_pressed();
731 self.inert_overlays_escape_pressed(&mut *ops);
734 }
735
736 if let WidgetEvent::KeyDown {
737 key: Key::Escape, ..
738 } = &event
739 && !self.overlay_manager.is_empty()
740 && self.dismiss_top_on_escape(&mut *ops)
741 {
742 return;
743 }
744
745 match &event {
750 WidgetEvent::PointerDown {
751 position, button, ..
752 } => {
753 if self.arm_outside_press_dismissal(*position, *button, &mut *ops) {
754 return;
755 }
756 }
757 WidgetEvent::PointerUp { position, .. } => {
758 if self.commit_outside_press_dismissal(*position, &mut *ops) {
759 return;
760 }
761 }
762 WidgetEvent::PointerCancel { pointer, .. } => {
763 self.overlay_manager.abort_dismiss(pointer.id);
767 }
768 _ => {}
769 }
770
771 if let WidgetEvent::KeyDown { key, modifiers, .. } = &event
782 && let Some(callback) = self.take_key_capture()
783 {
784 let keystroke = crate::shortcut::KeyStroke::new(*key, *modifiers);
785 let mut cap_ctx = self.make_event_context(&mut *ops);
786 callback(keystroke, self.shortcut_registry_mut(), &mut cap_ctx);
787 let anchor = self.focused.or_else(|| self.intent_anchor());
790 if let Some(anchor_id) = anchor {
791 self.collect_from_ctx(cap_ctx, anchor_id);
792 self.drain_pending_intents(&mut *ops);
793 }
794 return;
795 }
796
797 let focus_captures_keys = matches!(&event, WidgetEvent::KeyDown { .. })
805 && self.focused.is_some_and(|f| self.is_keyboard_capture(f));
806
807 if !focus_captures_keys && let WidgetEvent::KeyDown { key, modifiers, .. } = &event {
823 let keystroke = crate::shortcut::KeyStroke::new(*key, *modifiers);
824 let candidates: Vec<(&'static str, crate::shortcut::ShortcutScope, bool)> = self
833 .shortcut_registry
834 .matches_by_keystroke(keystroke)
835 .map(|eff| {
836 (
837 eff.shortcut.id,
838 eff.shortcut.scope,
839 eff.shortcut.propagate_when_disabled,
840 )
841 })
842 .collect();
843 let lookup = self.select_shortcut_for_focus(&candidates);
844 if let Some((id, scope, propagate_when_disabled)) = lookup {
845 let anchor = match scope {
846 crate::shortcut::ShortcutScope::Global => {
852 self.focus_or_modal().or_else(|| self.intent_anchor())
853 }
854 crate::shortcut::ShortcutScope::Scoped(scope_id) => self
855 .focus_or_modal()
856 .filter(|f| self.scope_distance_from_focus(*f, scope_id).is_some()),
857 };
858 if let Some(anchor_id) = anchor {
859 let mut act_ctx = self.make_event_context(&mut *ops);
860 if let Some(intent) =
861 self.shortcut_registry
862 .invoke_on_activate(id, keystroke, &mut act_ctx)
863 {
864 self.collect_from_ctx(act_ctx, anchor_id);
865 self.drain_pending_intents(&mut *ops);
869 let intent = self.sent_here(
873 intent.with_source(crate::telemetry::IntentSource::Shortcut),
874 );
875 let keep = self.keeps_unclaimed(&intent).then(|| intent.clone());
876 let consumed = self.dispatch_intent(
877 anchor_id,
878 intent,
879 propagate_when_disabled,
880 &mut *ops,
881 );
882 self.drain_pending_intents(&mut *ops);
883 if consumed {
884 return;
885 }
886 if let Some(intent) = keep {
889 self.keep_unclaimed(intent, &mut *ops);
890 return;
891 }
892 }
899 }
900 }
905 }
908
909 if self.outbound_drag_source.is_some()
915 && let WidgetEvent::KeyDown {
916 key: Key::Escape, ..
917 } = &event
918 {
919 ops.cancel_os_drag();
920 }
925
926 if self.active_drag.is_some() {
928 match &event {
929 WidgetEvent::PointerMove { position, .. } => {
930 self.handle_drag_move(*position, &mut *ops);
931 return;
932 }
933 WidgetEvent::PointerUp { position, .. } => {
934 self.handle_drag_drop(*position, &mut *ops);
935 return;
936 }
937 WidgetEvent::KeyDown {
938 key: Key::Escape, ..
939 } => {
940 self.cancel_active_drag(&mut *ops);
941 return;
942 }
943 WidgetEvent::Scroll { .. } => {
944 let target_and_pos = self
950 .active_drag
951 .as_ref()
952 .and_then(|d| d.current_target.map(|t| (t, d.current_position)));
953 if let Some((target, _pos)) = target_and_pos {
954 self.dispatch_to_widget(target, &event, &mut *ops);
955 }
956 if let Some((_, pos)) = target_and_pos
957 && self.active_drag.is_some()
958 {
959 self.handle_drag_move(pos, &mut *ops);
960 }
961 return;
962 }
963 _ => {}
964 }
965 }
966
967 if let WidgetEvent::KeyDown { key, modifiers, .. } = &event
976 && is_context_menu_chord(*key, *modifiers)
977 && self.open_context_menu_from_keyboard(&mut *ops)
978 {
979 return;
980 }
981
982 match &event {
983 WidgetEvent::PointerMove { position, .. } => {
984 self.note_sequence_position(*position);
988 self.revalidate_sequence(&mut *ops);
989 self.tick_sequence_timers();
992 self.note_palm_sample(*position);
997 self.feed_pinch(super::pan_arbiter::PinchFeed::Move, *position, &mut *ops);
998 if let Some(captured) = self.current_pointer_capture() {
999 self.dispatch_to_widget(captured, &event, &mut *ops);
1000 if self.active_drag.is_none() {
1005 self.advance_sequence(&event, &mut *ops);
1006 }
1007 } else {
1008 self.handle_pointer_move(&event, *position, &mut *ops);
1009 if self.active_drag.is_none() {
1014 self.advance_sequence(&event, &mut *ops);
1015 }
1016 }
1017 self.advance_pan(*position, &mut *ops);
1020 self.update_press(*position);
1023 self.touch_route_moved(self.current_pointer_id());
1026 if self.pointers.hover_owner_id() == Some(self.current_pointer_id()) {
1036 self.update_pointer_leave_overlays(*position, &mut *ops);
1037 }
1038 }
1039 WidgetEvent::PointerDown {
1040 position, button, ..
1041 } => {
1042 let pressed = self.current_pointer_id();
1045 self.cancelled_pointers.retain(|p| *p != pressed);
1046 self.tooltip_pointer_press(Some(*position));
1053 let pressing = self.current_input.pointer;
1057 if let Some(target) = self.hit_test_for(*position, &pressing) {
1058 if *button == PointerButton::Secondary
1059 && self.show_context_menu_for(
1060 target,
1061 *position,
1062 crate::widget_builder::ContextMenuTrigger::Pointer,
1063 &mut *ops,
1064 )
1065 {
1066 return;
1067 }
1068 self.begin_sequence(target, *position);
1074 let modifiers = match &event {
1079 WidgetEvent::PointerDown { modifiers, .. } => *modifiers,
1080 _ => crate::event::Modifiers::NONE,
1081 };
1082 self.begin_pan(target, *position, modifiers);
1083 self.begin_palm_watch(*position);
1084 self.feed_pinch(super::pan_arbiter::PinchFeed::Down, *position, &mut *ops);
1085 let focusable = self.find_focusable_at_or_above(target);
1089 self.begin_press(focusable);
1090 if !pressing.kind.is_direct()
1100 && let Some(focusable) = focusable
1101 && self.click_may_focus(focusable)
1102 {
1103 self.focus_with_origin_ops(
1104 focusable,
1105 crate::focus::FocusOrigin::Pointer(pressing.kind),
1106 &mut *ops,
1107 );
1108 }
1109 self.dispatch_to_widget(target, &event, &mut *ops);
1110 if self.active_drag.is_none() {
1115 self.enrol_sequence_members(&event, &mut *ops);
1116 }
1117 self.adopt_press_owner(*button);
1121 self.arm_touch_route(target, *position);
1126 }
1127 }
1128 WidgetEvent::PointerUp { position, .. } => {
1129 let released = self.current_pointer_id();
1135 self.cancel_touch_route(released);
1139 if let Some(index) = self.cancelled_pointers.iter().position(|p| *p == released) {
1140 self.cancelled_pointers.swap_remove(index);
1141 crate::trace_input!(
1142 Samples,
1143 "swallowing the Up for {released:?}: its press was cancelled"
1144 );
1145 return;
1146 }
1147 self.feed_pinch(super::pan_arbiter::PinchFeed::Up, *position, &mut *ops);
1154 if self.take_palm_verdict(released) {
1155 crate::trace_input!(
1156 Samples,
1157 "{released:?} released as a palm: large, and it never moved"
1158 );
1159 self.cancel_pointer(
1160 released,
1161 crate::pointer::CancelReason::PalmRejected,
1162 &mut *ops,
1163 );
1164 return;
1165 }
1166 self.end_pan(*position, &mut *ops);
1169 self.note_sequence_position(*position);
1176 self.end_sequence(&event, &mut *ops);
1177 self.focus_on_release(*position, &mut *ops);
1182 if let Some(captured) = self.current_pointer_capture() {
1183 self.dispatch_to_widget(captured, &event, &mut *ops);
1184 self.set_current_pointer_capture(None);
1187 } else {
1188 let releasing = self.current_input.pointer;
1189 if let Some(target) = self.hit_test_for(*position, &releasing) {
1190 self.dispatch_to_widget(target, &event, &mut *ops);
1191 }
1192 }
1193 let released = self.current_pointer_id();
1196 self.release_arenas_following(released);
1197 self.end_press(released);
1201 }
1202 WidgetEvent::PointerCancel {
1203 reason, pointer, ..
1204 } => {
1205 let (reason, pointer) = (*reason, pointer.id);
1210 self.cancel_pointer(pointer, reason, &mut *ops);
1211 }
1212 WidgetEvent::Scroll {
1213 window_position: position,
1214 ..
1215 } => {
1216 use super::pan_arbiter::ScrollDelivery;
1217 match ScrollDelivery::for_source(self.current_input.scroll_source) {
1218 ScrollDelivery::ClaimantChain => {
1223 let position = *position;
1224 self.route_scroll_along_chain(&event, position, &mut *ops);
1225 }
1226 ScrollDelivery::Bubble => {
1232 let scrolling = self.current_input.pointer;
1233 let target = match position {
1234 Some(p) => self.hit_test_for(*p, &scrolling),
1235 None => self.hovered_id().or(self.focused),
1236 };
1237 if let Some(target) = target {
1238 self.dispatch_to_widget(target, &event, &mut *ops);
1239 }
1240 }
1241 }
1242 }
1243 WidgetEvent::KeyDown { key, modifiers, .. } => {
1244 if *key == Key::Tab {
1245 let captured_focus = self
1260 .focused
1261 .is_some_and(|focused| self.is_keyboard_capture(focused));
1262 if captured_focus && modifiers.ctrl() {
1263 self.cycle_focus(modifiers.shift(), &mut *ops);
1264 return;
1265 }
1266 let handled = self
1273 .focused
1274 .map(|focused| {
1275 self.dispatch_to_widget_returning_handled(focused, &event, &mut *ops)
1276 })
1277 .unwrap_or(false);
1278 if !handled {
1279 self.cycle_focus(modifiers.shift(), &mut *ops);
1280 }
1281 } else if let Some(focused) = self.focused {
1282 self.dispatch_to_widget(focused, &event, &mut *ops);
1283 }
1284 }
1285 WidgetEvent::KeyUp { .. }
1286 | WidgetEvent::ImeComposition { .. }
1287 | WidgetEvent::ImeCommit { .. } => {
1288 if let Some(focused) = self.focused {
1289 self.dispatch_to_widget(focused, &event, &mut *ops);
1290 }
1291 }
1292 WidgetEvent::AccessAction { target, action, .. } => {
1293 if let Some(id) =
1311 target.filter(|id| self.arena.is_active(*id) && !self.is_behind_modal(*id))
1312 {
1313 if *action == accesskit::Action::Focus {
1314 if self.advertises_focus_action(id) {
1343 let target = self
1344 .focusable_composite_behind(id)
1345 .or_else(|| self.first_focusable_descendant(id))
1346 .unwrap_or(id);
1347 self.focus_with_origin_ops(
1353 target,
1354 crate::focus::FocusOrigin::Accessibility,
1355 &mut *ops,
1356 );
1357 self.access_action_handled = self.focused == Some(target);
1360 } else {
1361 self.access_action_handled = false;
1362 }
1363 } else if *action == accesskit::Action::ShowContextMenu {
1364 self.access_action_handled =
1380 if self.dispatch_to_widget_returning_handled(id, &event, &mut *ops) {
1381 true
1382 } else {
1383 let position = self.arena.bounds(id).center();
1384 self.show_context_menu_for(
1385 id,
1386 position,
1387 crate::widget_builder::ContextMenuTrigger::Accessibility,
1388 &mut *ops,
1389 )
1390 };
1391 } else {
1392 if *action == accesskit::Action::Click
1397 && self
1398 .find_focusable_at_or_above(id)
1399 .is_some_and(|focusable| !self.click_may_focus(focusable))
1400 {
1401 self.commit_pending_edit(&mut *ops);
1402 }
1403 self.access_action_handled =
1404 self.dispatch_to_widget_returning_handled(id, &event, &mut *ops);
1405 }
1406 }
1407 }
1408 WidgetEvent::Gesture { .. } => {
1409 if let Some(target) = self.hovered_id().or(self.focused) {
1410 self.dispatch_to_widget(target, &event, &mut *ops);
1411 }
1412 }
1413 WidgetEvent::ScrollIntoView { .. }
1414 | WidgetEvent::PointerEnter { .. }
1415 | WidgetEvent::PointerLeave { .. }
1416 | WidgetEvent::FocusGained { .. }
1417 | WidgetEvent::FocusLost => {}
1418 }
1419 self.drain_pending_intents(&mut *ops);
1424 }
1425
1426 fn open_context_menu_from_keyboard(&mut self, ops: &mut dyn crate::window::WindowOps) -> bool {
1439 let Some(focused) = self.focused else {
1440 return false;
1441 };
1442 let target = self
1443 .arena
1444 .get(focused)
1445 .and_then(|node| node.widget.context_menu_key_target())
1446 .filter(|id| self.arena.is_active(*id))
1447 .unwrap_or(focused);
1448
1449 let bounds = self.bounds(target);
1453 let anchor = Point {
1454 x: bounds.x + bounds.width / 2.0,
1455 y: bounds.y + bounds.height / 2.0,
1456 };
1457 self.show_context_menu_for(
1458 target,
1459 anchor,
1460 crate::widget_builder::ContextMenuTrigger::Keyboard,
1461 ops,
1462 )
1463 }
1464
1465 pub(super) fn show_context_menu_for(
1474 &mut self,
1475 target: WidgetId,
1476 position: Point,
1477 trigger: crate::widget_builder::ContextMenuTrigger,
1478 ops: &mut dyn crate::window::WindowOps,
1479 ) -> bool {
1480 let mut ctx = self.make_event_context(&mut *ops);
1486 ctx.context_menu_trigger = Some(trigger);
1487 let mut walker = Some(target);
1488 let menu_decision: Option<(WidgetId, Box<dyn Widget>)> = loop {
1489 let owner_id = {
1492 let mut probe = walker;
1493 loop {
1494 match probe {
1495 None => break None,
1496 Some(id) => {
1497 if self
1498 .arena
1499 .get(id)
1500 .is_some_and(|node| node.context_menu_factory.is_some())
1501 {
1502 break Some(id);
1503 }
1504 probe = self.arena.get(id).and_then(|node| node.parent);
1505 }
1506 }
1507 }
1508 };
1509 let Some(owner_id) = owner_id else {
1510 break None;
1511 };
1512 let outcome: Option<Box<dyn Widget>> = {
1519 let node = self
1520 .arena
1521 .get(owner_id)
1522 .expect("owner_id from active arena walk");
1523 let factory = node
1524 .context_menu_factory
1525 .as_ref()
1526 .expect("owner_id only set when factory present");
1527 factory(position, &mut ctx)
1528 };
1529 match outcome {
1530 Some(menu) => break Some((owner_id, menu)),
1531 None => {
1532 walker = self.arena.get(owner_id).and_then(|n| n.parent);
1534 }
1535 }
1536 };
1537
1538 let drain_anchor = menu_decision.as_ref().map(|(id, _)| *id).unwrap_or(target);
1544 self.collect_from_ctx(ctx, drain_anchor);
1545
1546 let Some((owner_id, menu_widget)) = menu_decision else {
1547 return false;
1548 };
1549
1550 let keep: std::collections::HashSet<WidgetId> = self
1556 .overlay_manager
1557 .stack
1558 .iter()
1559 .map(|o| o.content_id)
1560 .filter(|&content_id| self.is_descendant_of(owner_id, content_id))
1561 .collect();
1562 let dismissed = self.overlay_manager.dismiss_except(&keep);
1563 self.dormant_dismissed_content(&dismissed, &mut *ops);
1564
1565 let content_id = self.add_boxed(menu_widget);
1566 let prev_focus = self.focused;
1567 let placement =
1574 crate::overlay::OverlayPlacement::at_pointer_for(position, &self.current_input.pointer);
1575 self.overlay_manager
1576 .show(crate::overlay::OverlayRequest::new(
1577 content_id,
1578 owner_id,
1579 placement,
1580 crate::overlay::DismissBehavior::EscapeOrClickOutside,
1581 ));
1582 if let Some(focus_id) = prev_focus {
1583 self.overlay_manager.set_top_focus_restore(focus_id);
1584 }
1585 self.focus_ops(content_id, &mut *ops);
1586 self.drain_pending_intents(&mut *ops);
1591 true
1592 }
1593
1594 fn arm_outside_press_dismissal(
1615 &mut self,
1616 position: Point,
1617 button: PointerButton,
1618 ops: &mut dyn crate::window::WindowOps,
1619 ) -> bool {
1620 self.prune_stale_dismiss_arms();
1621 let pressing = self.current_input.pointer;
1622 let busy = self.busy_press_points(pressing.id);
1623
1624 if pressing.kind.is_direct() {
1625 return self
1626 .overlay_manager
1627 .arm_dismiss(pressing.id, position, &busy)
1628 .suppress_beneath;
1629 }
1630
1631 let (dismissed, focus_restore, toggle_anchors) = self
1632 .overlay_manager
1633 .dismiss_outside_press_toggles(position, &busy);
1634 if dismissed.is_empty() {
1635 return false;
1636 }
1637 self.dormant_dismissed_content(&dismissed, &mut *ops);
1638 if let Some(restore_id) = focus_restore
1639 && self.arena.is_active(restore_id)
1640 {
1641 self.focus_ops(restore_id, &mut *ops);
1642 }
1643 toggle_anchors
1656 .iter()
1657 .any(|&toggle| self.press_reopens(toggle, position, button, &pressing))
1658 }
1659
1660 fn press_reopens(
1672 &self,
1673 toggle: crate::overlay::ToggleAnchor,
1674 position: Point,
1675 button: PointerButton,
1676 pointer: &crate::pointer::PointerInfo,
1677 ) -> bool {
1678 let anchor = toggle.anchor;
1679 if !self.arena.is_active(anchor) || !self.arena.bounds(anchor).contains(position) {
1680 return false;
1681 }
1682 if toggle.modal {
1683 return self.first_tapper(position, button, pointer) == Some(anchor);
1684 }
1685 button == PointerButton::Primary
1686 || self
1687 .anchor_tap_buttons(anchor, position, pointer)
1688 .contains(button)
1689 }
1690
1691 fn first_tapper(
1694 &self,
1695 position: Point,
1696 button: PointerButton,
1697 pointer: &crate::pointer::PointerInfo,
1698 ) -> Option<WidgetId> {
1699 let mut node = self.hit_test_for(position, pointer);
1700 while let Some(id) = node {
1701 if self.tap_buttons(id).contains(button) {
1702 return Some(id);
1703 }
1704 node = self.arena.parent(id);
1705 }
1706 None
1707 }
1708
1709 fn anchor_tap_buttons(
1719 &self,
1720 anchor: WidgetId,
1721 position: Point,
1722 pointer: &crate::pointer::PointerInfo,
1723 ) -> ButtonMask {
1724 let mut mask = ButtonMask::NONE;
1725 let mut node = self.hit_test_for(position, pointer);
1726 while let Some(id) = node {
1727 mask = mask.union(self.tap_buttons(id));
1728 if id == anchor {
1729 return mask;
1730 }
1731 node = self.arena.parent(id);
1732 }
1733 ButtonMask::NONE
1734 }
1735
1736 fn commit_outside_press_dismissal(
1743 &mut self,
1744 position: Point,
1745 ops: &mut dyn crate::window::WindowOps,
1746 ) -> bool {
1747 let pointer = self.current_pointer_id();
1748 if !self.overlay_manager.has_armed_dismiss(pointer) {
1749 return false;
1750 }
1751 let (dismissed, focus_restore, _anchors) =
1752 self.overlay_manager.commit_dismiss(pointer, position);
1753 if !dismissed.is_empty() {
1754 self.dormant_dismissed_content(&dismissed, &mut *ops);
1755 if let Some(restore_id) = focus_restore
1756 && self.arena.is_active(restore_id)
1757 {
1758 self.focus_ops(restore_id, &mut *ops);
1759 }
1760 }
1761 true
1762 }
1763
1764 fn busy_press_points(&self, exclude: crate::pointer::PointerId) -> Vec<Point> {
1781 self.pointers
1782 .iter()
1783 .filter(|entry| entry.info.id != exclude)
1784 .filter(|entry| self.press_is_revocable(entry.info.id))
1785 .map(|entry| entry.down_position)
1786 .collect()
1787 }
1788
1789 fn prune_stale_dismiss_arms(&mut self) {
1796 let stale: Vec<crate::pointer::PointerId> = self
1797 .overlay_manager
1798 .armed_pointers()
1799 .into_iter()
1800 .filter(|id| self.pointers.get(*id).is_none())
1801 .collect();
1802 for id in stale {
1803 self.overlay_manager.abort_dismiss(id);
1804 }
1805 }
1806
1807 fn begin_press(&mut self, focusable: Option<WidgetId>) {
1828 let pointer = self.current_pointer_id();
1829 let now = self.sequence_now();
1830 let delay = self
1831 .pan_session_open(pointer)
1832 .then(|| self.current_profile().press_feedback_delay)
1833 .filter(|d| !d.is_zero());
1834 self.presses.press(pointer, focusable, now, delay);
1835 }
1836
1837 fn adopt_press_owner(&mut self, button: PointerButton) {
1860 let pointer = self.current_pointer_id();
1861 let Some(owner) = self.current_sequence().and_then(|s| s.pressed_owner()) else {
1862 return;
1863 };
1864 if !self.press_buttons(owner).contains(button) {
1865 crate::trace_input!(
1866 Samples,
1867 "no press visual for {owner:?}: {button:?} is not one of its accepted buttons"
1868 );
1869 return;
1870 }
1871 self.presses.set_owner(pointer, owner);
1872 self.publish_pressed(owner);
1873 }
1874
1875 fn update_press(&mut self, position: Point) {
1888 let pointer = self.current_pointer_id();
1889 if self.presses.get(pointer).is_none() {
1890 return;
1891 }
1892 let claimed_elsewhere = self.current_sequence().is_some_and(|sequence| {
1895 sequence
1896 .winner()
1897 .is_some_and(|winner| Some(winner) != sequence.pressed_owner())
1898 });
1899 if claimed_elsewhere {
1900 self.end_press(pointer);
1901 return;
1902 }
1903 let profile = self.current_profile();
1904 let origin = self.current_sequence().map(|s| s.press_origin());
1905 let owner = self.presses.get(pointer).and_then(|p| p.owner);
1906 let inside = match (origin, owner) {
1907 (Some(origin), Some(owner)) => {
1908 let bounds = self
1909 .arena
1910 .is_active(owner)
1911 .then(|| self.arena.bounds(owner));
1912 !crate::gesture::TapBoundary::for_pointer(&self.current_input.pointer, &profile)
1913 .left(origin, position, bounds, &profile)
1914 }
1915 _ => true,
1918 };
1919 let Some(press) = self.presses.get_mut(pointer) else {
1920 return;
1921 };
1922 if press.inside == inside {
1923 return;
1924 }
1925 press.inside = inside;
1926 if let Some(owner) = owner {
1927 self.publish_pressed(owner);
1928 }
1929 }
1930
1931 pub(crate) fn end_press(&mut self, pointer: crate::pointer::PointerId) {
1937 if let Some(owner) = self.presses.release(pointer) {
1938 self.publish_pressed(owner);
1939 }
1940 }
1941
1942 pub(crate) fn resolve_press_delays(&mut self, now: crate::pointer::EventTime) {
1945 if self.presses.is_empty() {
1946 return;
1947 }
1948 for id in self.presses.resolve_delays(now) {
1949 self.publish_pressed(id);
1950 self.arena.mark_needs_paint(id);
1951 }
1952 }
1953
1954 pub(crate) fn next_press_deadline(&self) -> Option<std::time::Instant> {
1957 self.presses.next_deadline().map(|t| self.instant_for(t))
1958 }
1959
1960 pub(crate) fn commit_pending_edit(&mut self, ops: &mut dyn crate::window::WindowOps) {
1975 let Some(focus) = self.focused.filter(|id| self.arena.is_active(*id)) else {
1976 return;
1977 };
1978 let (chain, _, _) = self.intent_walk(focus, &[]);
1979 for id in chain {
1980 for external in [true, false] {
1981 let handler = self.arena.get_mut(id).and_then(|node| {
1982 let handlers = if external {
1983 &mut node.external_handlers
1984 } else {
1985 &mut node.handlers
1986 };
1987 handlers.on_commit_edit.take()
1988 });
1989 let Some(mut handler) = handler else { continue };
1990 let mut ctx = self.make_event_context(&mut *ops).with_dispatch_node(id);
1991 handler(&mut ctx);
1992 if let Some(node) = self.arena.get_mut(id) {
1993 let handlers = if external {
1994 &mut node.external_handlers
1995 } else {
1996 &mut node.handlers
1997 };
1998 if handlers.on_commit_edit.is_none() {
2000 handlers.on_commit_edit = Some(handler);
2001 }
2002 }
2003 self.collect_from_ctx(ctx, id);
2004 }
2005 }
2006 }
2007
2008 fn click_may_focus(&self, focusable: WidgetId) -> bool {
2016 self.arena
2017 .get(focusable)
2018 .is_none_or(|node| node.focus_on_click)
2019 }
2020
2021 fn focus_on_release(&mut self, position: Point, ops: &mut dyn crate::window::WindowOps) {
2037 let releasing = self.current_input.pointer;
2038 let pointer = self.current_pointer_id();
2039 let Some(pressed) = self.presses.get(pointer).and_then(|p| p.focusable) else {
2040 return;
2041 };
2042 if !releasing.kind.is_direct() && self.click_may_focus(pressed) {
2043 return;
2044 }
2045 let Some(target) = self.hit_test_for(position, &releasing) else {
2046 return;
2047 };
2048 if self.find_focusable_at_or_above(target) != Some(pressed) {
2049 return;
2050 }
2051 if !self.click_may_focus(pressed) {
2052 self.commit_pending_edit(&mut *ops);
2053 } else {
2054 self.focus_with_origin_ops(
2055 pressed,
2056 crate::focus::FocusOrigin::Pointer(releasing.kind),
2057 &mut *ops,
2058 );
2059 }
2060 }
2061
2062 fn handle_pointer_move(
2082 &mut self,
2083 event: &WidgetEvent,
2084 position: Point,
2085 ops: &mut dyn crate::window::WindowOps,
2086 ) {
2087 let moving = self.current_input.pointer;
2092 if self.pointers.hover_owner_id() != Some(self.current_pointer_id()) {
2093 if let Some(target) = self.hit_test_for(position, &moving) {
2094 self.dispatch_to_widget(target, event, &mut *ops);
2095 }
2096 return;
2097 }
2098 let target = self.hit_test_for(position, &moving);
2099
2100 if target != self.hovered_id() {
2101 let hover = self.hover_transition_pointer();
2103 let previously_hovered = self.hovered_id();
2104 if let Some(old) = previously_hovered {
2105 self.dispatch_to_widget(
2106 old,
2107 &WidgetEvent::PointerLeave { pointer: hover },
2108 &mut *ops,
2109 );
2110 self.tooltip_pointer_leave(old, &mut *ops);
2111 }
2112 if let Some(new) = target {
2113 self.dispatch_to_widget(
2114 new,
2115 &WidgetEvent::PointerEnter { pointer: hover },
2116 &mut *ops,
2117 );
2118 self.tooltip_pointer_enter(new);
2119 }
2120 self.set_hovered(target);
2121 self.update_hover_within_signals(previously_hovered, target);
2122 } else if let Some(target) = target {
2123 self.tooltip_pointer_moved(target, position);
2126 }
2127
2128 if let Some(target) = target {
2129 self.dispatch_to_widget(target, event, &mut *ops);
2130 }
2131 }
2132
2133 pub(super) fn dispatch_to_widget(
2134 &mut self,
2135 target: WidgetId,
2136 event: &WidgetEvent,
2137 ops: &mut dyn crate::window::WindowOps,
2138 ) {
2139 self.dispatch_to_widget_returning_handled(target, event, ops);
2140 }
2141
2142 pub(super) fn localize_event(
2158 frame: &crate::arena::LocalFrame,
2159 event: &WidgetEvent,
2160 ) -> Option<WidgetEvent> {
2161 match event {
2162 WidgetEvent::PointerDown {
2163 position,
2164 button,
2165 modifiers,
2166 pointer,
2167 } => Some(WidgetEvent::PointerDown {
2168 position: frame.to_local(*position),
2169 button: *button,
2170 modifiers: *modifiers,
2171 pointer: *pointer,
2172 }),
2173 WidgetEvent::PointerUp {
2174 position,
2175 button,
2176 modifiers,
2177 pointer,
2178 } => Some(WidgetEvent::PointerUp {
2179 position: frame.to_local(*position),
2180 button: *button,
2181 modifiers: *modifiers,
2182 pointer: *pointer,
2183 }),
2184 WidgetEvent::PointerMove {
2185 position,
2186 modifiers,
2187 pointer,
2188 } => Some(WidgetEvent::PointerMove {
2189 position: frame.to_local(*position),
2190 modifiers: *modifiers,
2191 pointer: *pointer,
2192 }),
2193 WidgetEvent::Gesture { gesture } => Some(WidgetEvent::Gesture {
2194 gesture: Self::localize_gesture(frame, gesture),
2195 }),
2196 _ => None,
2202 }
2203 }
2204
2205 fn localize_gesture(frame: &crate::arena::LocalFrame, gesture: &GestureEvent) -> GestureEvent {
2212 let loc = |p: teksilo_canvas::Point| frame.to_local(p);
2213 let tap = |t: &TapEvent| {
2214 TapEvent::new(loc(t.position), t.button, t.modifiers).with_pointer(t.pointer)
2215 };
2216 match gesture {
2217 GestureEvent::Tap(t) => GestureEvent::Tap(tap(t)),
2218 GestureEvent::DoubleTap(t) => GestureEvent::DoubleTap(tap(t)),
2219 GestureEvent::TripleTap(t) => GestureEvent::TripleTap(tap(t)),
2220 GestureEvent::LongPress(t) => GestureEvent::LongPress(tap(t)),
2221 GestureEvent::DragStarted {
2222 position,
2223 button,
2224 pointer,
2225 } => GestureEvent::DragStarted {
2226 position: loc(*position),
2227 button: *button,
2228 pointer: *pointer,
2229 },
2230 GestureEvent::DragMoved {
2231 position,
2232 delta,
2233 pointer,
2234 } => GestureEvent::DragMoved {
2235 position: loc(*position),
2236 delta: *delta,
2237 pointer: *pointer,
2238 },
2239 GestureEvent::DragEnded { position, pointer } => GestureEvent::DragEnded {
2240 position: loc(*position),
2241 pointer: *pointer,
2242 },
2243 GestureEvent::DragCancelled {
2244 position,
2245 pointer,
2246 reason,
2247 } => GestureEvent::DragCancelled {
2248 position: loc(*position),
2249 pointer: *pointer,
2250 reason: *reason,
2251 },
2252 GestureEvent::PinchStarted { center } => GestureEvent::PinchStarted {
2253 center: loc(*center),
2254 },
2255 GestureEvent::PinchChanged {
2256 center,
2257 scale,
2258 rotation,
2259 } => GestureEvent::PinchChanged {
2260 center: loc(*center),
2261 scale: *scale,
2262 rotation: *rotation,
2263 },
2264 GestureEvent::PinchEnded => GestureEvent::PinchEnded,
2265 GestureEvent::PinchCancelled { reason } => {
2266 GestureEvent::PinchCancelled { reason: *reason }
2267 }
2268 GestureEvent::Swipe {
2269 direction,
2270 velocity,
2271 } => GestureEvent::Swipe {
2272 direction: *direction,
2273 velocity: *velocity,
2274 },
2275 }
2276 }
2277
2278 pub(super) fn dispatch_to_widget_returning_handled(
2284 &mut self,
2285 target: WidgetId,
2286 event: &WidgetEvent,
2287 ops: &mut dyn crate::window::WindowOps,
2288 ) -> bool {
2289 if !self.arena.is_enabled(target) {
2290 return false;
2291 }
2292
2293 let mut ancestors = Vec::new();
2294 let mut current = self.arena.parent(target);
2295 while let Some(id) = current {
2296 ancestors.push(id);
2297 current = self.arena.parent(id);
2298 }
2299 ancestors.reverse();
2300
2301 let tap_owner: Option<WidgetId> = if matches!(
2319 event,
2320 WidgetEvent::PointerDown { .. } | WidgetEvent::PointerUp { .. }
2321 ) {
2322 let mut owner = None;
2323 let mut cur = Some(target);
2324 while let Some(id) = cur {
2325 if self.arena.get(id).is_some_and(|n| {
2326 n.any_handler(|h| h.on_tap.is_some() || h.on_long_press.is_some())
2327 }) {
2328 owner = Some(id);
2329 break;
2330 }
2331 cur = self.arena.parent(id);
2332 }
2333 owner
2334 } else {
2335 None
2336 };
2337
2338 for &id in &ancestors {
2339 let frame = self.arena.local_frame(id);
2340 let mut ctx = self
2341 .make_event_context(&mut *ops)
2342 .with_dispatch_node(id)
2343 .with_dispatch_target(target)
2344 .with_local_frame(frame);
2345 ctx.press_claimed_by_interactive_child =
2346 tap_owner.is_some_and(|owner| owner != id && self.is_descendant_of(owner, id));
2347 let localized = Self::localize_event(&frame, event);
2350 let event = localized.as_ref().unwrap_or(event);
2351 let response = if let Some(node) = self.arena.get_mut(id) {
2352 Self::try_handler_preview(node, event, &mut ctx).unwrap_or(EventResponse::Ignored)
2353 } else {
2354 EventResponse::Ignored
2355 };
2356 self.collect_from_ctx(ctx, id);
2357 if response == EventResponse::Handled {
2358 self.arena.mark_needs_paint(id);
2359 if matches!(event, WidgetEvent::PointerDown { .. }) {
2367 self.note_preview_claim(id);
2368 }
2369 return true;
2370 }
2371 }
2372
2373 let needs_layout_on_handle = matches!(
2374 event,
2375 WidgetEvent::Scroll { .. } | WidgetEvent::ScrollIntoView { .. }
2376 );
2377 let mut current = Some(target);
2378 let mut is_target = true;
2379 while let Some(id) = current {
2380 let frame = self.arena.local_frame(id);
2381 let mut ctx = self
2382 .make_event_context(&mut *ops)
2383 .with_dispatch_node(id)
2384 .with_dispatch_target(target)
2385 .with_local_frame(frame);
2386 ctx.press_claimed_by_interactive_child =
2387 tap_owner.is_some_and(|owner| owner != id && self.is_descendant_of(owner, id));
2388 let localized = Self::localize_event(&frame, event);
2391 let gesture_cx = self.recognizer_context(id);
2392 let arena_blocked = self.sequence_blocks_arena(id);
2395 let WidgetTree {
2396 arena,
2397 gesture_owners,
2398 ..
2399 } = self;
2400 let event = localized.as_ref().unwrap_or(event);
2401 let response = if let Some(node) = arena.get_mut(id) {
2402 Self::try_handler_bubble(
2403 node,
2404 event,
2405 &mut ctx,
2406 BubbleGates {
2407 fire_on_pointer_event: is_target,
2408 arena_blocked,
2409 },
2410 id,
2411 gesture_owners,
2412 gesture_cx,
2413 )
2414 .unwrap_or(EventResponse::Ignored)
2415 } else {
2416 EventResponse::Ignored
2417 };
2418 self.collect_from_ctx(ctx, id);
2419 if response == EventResponse::Handled {
2420 if needs_layout_on_handle {
2421 self.arena.mark_needs_layout(id);
2422 } else {
2423 self.arena.mark_needs_paint(id);
2424 }
2425 self.note_pointer_acceptance(id, event);
2426 if !matches!(
2441 event,
2442 WidgetEvent::PointerEnter { .. } | WidgetEvent::PointerLeave { .. }
2443 ) {
2444 return true;
2445 }
2446 }
2447 is_target = false;
2448 current = self.arena.parent(id);
2449 }
2450 false
2451 }
2452
2453 pub(super) fn dispatch_to_widget_direct(
2454 &mut self,
2455 target: WidgetId,
2456 event: &WidgetEvent,
2457 ops: &mut dyn crate::window::WindowOps,
2458 ) {
2459 self.dispatch_to_widget_direct_returning_handled(target, event, ops);
2460 }
2461
2462 pub(super) fn dispatch_to_widget_direct_returning_handled(
2472 &mut self,
2473 target: WidgetId,
2474 event: &WidgetEvent,
2475 ops: &mut dyn crate::window::WindowOps,
2476 ) -> bool {
2477 if !self.arena.is_enabled(target) {
2478 return false;
2479 }
2480
2481 let frame = self.arena.local_frame(target);
2482 let mut ctx = self
2483 .make_event_context(&mut *ops)
2484 .with_dispatch_node(target)
2485 .with_dispatch_target(target)
2486 .with_local_frame(frame);
2487 let gesture_cx = self.recognizer_context(target);
2488 let arena_blocked = self.sequence_blocks_arena(target);
2489 let WidgetTree {
2490 arena,
2491 gesture_owners,
2492 ..
2493 } = self;
2494 let response = if let Some(node) = arena.get_mut(target) {
2495 Self::try_handler_bubble(
2496 node,
2497 event,
2498 &mut ctx,
2499 BubbleGates {
2500 fire_on_pointer_event: true,
2501 arena_blocked,
2502 },
2503 target,
2504 gesture_owners,
2505 gesture_cx,
2506 )
2507 .unwrap_or(EventResponse::Ignored)
2508 } else {
2509 EventResponse::Ignored
2510 };
2511 self.collect_from_ctx(ctx, target);
2512
2513 if response == EventResponse::Handled {
2514 if matches!(
2517 event,
2518 WidgetEvent::Scroll { .. } | WidgetEvent::ScrollIntoView { .. }
2519 ) {
2520 self.arena.mark_needs_layout(target);
2521 } else {
2522 self.arena.mark_needs_paint(target);
2523 }
2524 self.note_pointer_acceptance(target, event);
2525 }
2526 response == EventResponse::Handled
2527 }
2528
2529 pub(super) fn note_pointer_acceptance(&mut self, target: WidgetId, event: &WidgetEvent) {
2538 if !matches!(
2539 event,
2540 WidgetEvent::PointerDown { .. }
2541 | WidgetEvent::PointerMove { .. }
2542 | WidgetEvent::PointerUp { .. }
2543 ) {
2544 return;
2545 }
2546 let pointer = self.current_pointer_id();
2547 if let Some(entry) = self.pointers.get_mut(pointer) {
2548 entry.last_accepted = Some(target);
2549 }
2550 }
2551
2552 fn try_handler_preview(
2553 node: &mut crate::arena::WidgetNode,
2554 event: &WidgetEvent,
2555 ctx: &mut EventContext,
2556 ) -> Option<EventResponse> {
2557 match event {
2558 WidgetEvent::KeyDown { .. }
2566 | WidgetEvent::KeyUp { .. }
2567 | WidgetEvent::ImeComposition { .. }
2568 | WidgetEvent::ImeCommit { .. } => {
2569 let has = node.external_handlers.on_key_preview.is_some()
2570 || node.handlers.on_key_preview.is_some();
2571 if !has {
2572 return None;
2573 }
2574 Some(fire_event_handler_both(
2575 &mut node.external_handlers.on_key_preview,
2576 &mut node.handlers.on_key_preview,
2577 event,
2578 ctx,
2579 ))
2580 }
2581 WidgetEvent::PointerEnter { .. } | WidgetEvent::PointerLeave { .. } => None,
2593 _ => {
2594 let has = node.external_handlers.on_pointer_event.is_some()
2595 || node.handlers.on_pointer_event.is_some();
2596 if !has {
2597 return None;
2598 }
2599 Some(fire_event_handler_both(
2600 &mut node.external_handlers.on_pointer_event,
2601 &mut node.handlers.on_pointer_event,
2602 event,
2603 ctx,
2604 ))
2605 }
2606 }
2607 }
2608
2609 fn try_handler_bubble(
2610 node: &mut crate::arena::WidgetNode,
2611 event: &WidgetEvent,
2612 ctx: &mut EventContext,
2613 gates: BubbleGates,
2614 node_id: WidgetId,
2615 gesture_owners: &mut std::collections::HashSet<WidgetId>,
2616 gesture_cx: crate::gesture::RecognizerContext<'_>,
2617 ) -> Option<EventResponse> {
2618 let BubbleGates {
2619 fire_on_pointer_event,
2620 arena_blocked,
2621 } = gates;
2622 match event {
2623 WidgetEvent::PointerEnter { .. } => {
2624 if let Some(cursor) = node.node_cursor {
2625 ctx.declared_cursor_request = Some(cursor);
2630 }
2631 let mut fired = false;
2632 if let Some(h) = node.external_handlers.on_hover.as_mut() {
2633 h(true, ctx);
2634 fired = true;
2635 }
2636 if let Some(h) = node.handlers.on_hover.as_mut() {
2637 h(true, ctx);
2638 fired = true;
2639 }
2640 if fired {
2641 Some(EventResponse::Handled)
2642 } else {
2643 node.node_cursor.map(|_| EventResponse::Handled)
2644 }
2645 }
2646 WidgetEvent::PointerLeave { .. } => {
2647 if node.node_cursor.is_some() {
2648 ctx.declared_cursor_request = Some(crate::widget::CursorIcon::Default);
2649 }
2650 let mut fired = false;
2651 if let Some(h) = node.external_handlers.on_hover.as_mut() {
2652 h(false, ctx);
2653 fired = true;
2654 }
2655 if let Some(h) = node.handlers.on_hover.as_mut() {
2656 h(false, ctx);
2657 fired = true;
2658 }
2659 if fired {
2660 Some(EventResponse::Handled)
2661 } else {
2662 node.node_cursor.map(|_| EventResponse::Handled)
2663 }
2664 }
2665 WidgetEvent::FocusGained { origin } => {
2666 let previous_origin = ctx.focus_origin.replace(*origin);
2670 let mut fired = false;
2671 if let Some(h) = node.external_handlers.on_focus.as_mut() {
2672 h(true, ctx);
2673 fired = true;
2674 }
2675 if let Some(h) = node.handlers.on_focus.as_mut() {
2676 h(true, ctx);
2677 fired = true;
2678 }
2679 ctx.focus_origin = previous_origin;
2680 fired.then_some(EventResponse::Handled)
2681 }
2682 WidgetEvent::FocusLost => {
2683 let mut fired = false;
2684 if let Some(h) = node.external_handlers.on_focus.as_mut() {
2685 h(false, ctx);
2686 fired = true;
2687 }
2688 if let Some(h) = node.handlers.on_focus.as_mut() {
2689 h(false, ctx);
2690 fired = true;
2691 }
2692 fired.then_some(EventResponse::Handled)
2693 }
2694 WidgetEvent::KeyDown { .. }
2695 | WidgetEvent::KeyUp { .. }
2696 | WidgetEvent::ImeComposition { .. }
2697 | WidgetEvent::ImeCommit { .. } => {
2698 if node.external_handlers.on_key.is_some() || node.handlers.on_key.is_some() {
2699 Some(fire_event_handler_both(
2700 &mut node.external_handlers.on_key,
2701 &mut node.handlers.on_key,
2702 event,
2703 ctx,
2704 ))
2705 } else {
2706 None
2707 }
2708 }
2709 WidgetEvent::Scroll { .. } | WidgetEvent::ScrollIntoView { .. } => {
2710 if node.external_handlers.on_scroll.is_some() || node.handlers.on_scroll.is_some() {
2711 Some(fire_event_handler_both(
2712 &mut node.external_handlers.on_scroll,
2713 &mut node.handlers.on_scroll,
2714 event,
2715 ctx,
2716 ))
2717 } else {
2718 None
2719 }
2720 }
2721 WidgetEvent::AccessAction {
2722 action,
2723 target_node,
2724 data,
2725 ..
2726 } => {
2727 let saved_a11y_source = ctx
2747 .current_source
2748 .replace(crate::telemetry::IntentSource::Accessibility);
2749 let mut any_slot = false;
2750 let mut any_handled = false;
2751 if let Some(h) = node.external_handlers.on_access_action_request.as_mut() {
2752 any_slot = true;
2753 any_handled |=
2754 h(*action, *target_node, data.clone(), ctx) == EventResponse::Handled;
2755 }
2756 if let Some(h) = node.handlers.on_access_action_request.as_mut() {
2757 any_slot = true;
2758 any_handled |=
2759 h(*action, *target_node, data.clone(), ctx) == EventResponse::Handled;
2760 }
2761 if let Some(h) = node.external_handlers.on_access_action.as_mut() {
2762 any_slot = true;
2763 any_handled |= h(*action, ctx) == EventResponse::Handled;
2764 }
2765 if let Some(h) = node.handlers.on_access_action.as_mut() {
2766 any_slot = true;
2767 any_handled |= h(*action, ctx) == EventResponse::Handled;
2768 }
2769 let user_handled = any_slot.then_some(if any_handled {
2770 EventResponse::Handled
2771 } else {
2772 EventResponse::Ignored
2773 });
2774
2775 let mut override_handled = false;
2781 if let Some(ov) = node.access_overrides.as_deref_mut() {
2782 if matches!(action, accesskit::Action::CustomAction) {
2783 if let Some(accesskit::ActionData::CustomAction(idx)) = data
2784 && let Some((_, cb)) = ov.custom_actions.get_mut(*idx as usize)
2785 {
2786 cb(ctx);
2787 override_handled = true;
2788 }
2789 } else {
2790 for (a, cb) in ov.actions.iter_mut() {
2791 if *a == *action {
2792 cb(ctx);
2793 override_handled = true;
2794 }
2795 }
2796 }
2797 }
2798
2799 ctx.current_source = saved_a11y_source;
2800 match (user_handled, override_handled) {
2801 (Some(EventResponse::Handled), _) | (_, true) => Some(EventResponse::Handled),
2802 (Some(EventResponse::Ignored), false) => Some(EventResponse::Ignored),
2803 (None, false) => None,
2804 }
2805 }
2806 WidgetEvent::Gesture { gesture } => {
2807 let matched = matches!(
2811 gesture,
2812 GestureEvent::PinchStarted { .. }
2813 | GestureEvent::PinchChanged { .. }
2814 | GestureEvent::PinchEnded
2815 | GestureEvent::Swipe { .. }
2816 | GestureEvent::DoubleTap { .. }
2817 | GestureEvent::TripleTap { .. }
2818 ) && {
2819 let has_handler = match gesture {
2820 GestureEvent::PinchStarted { .. }
2821 | GestureEvent::PinchChanged { .. }
2822 | GestureEvent::PinchEnded => node.any_handler(|h| h.on_pinch.is_some()),
2823 GestureEvent::Swipe { .. } => node.any_handler(|h| h.on_swipe.is_some()),
2824 GestureEvent::DoubleTap { .. } => {
2825 node.any_handler(|h| h.on_double_tap.is_some())
2826 }
2827 GestureEvent::TripleTap { .. } => {
2828 node.any_handler(|h| h.on_triple_tap.is_some())
2829 }
2830 _ => false,
2831 };
2832 if has_handler {
2833 Self::dispatch_recognized_gesture(node, *gesture, ctx);
2834 }
2835 has_handler
2836 };
2837 if matched {
2838 Some(EventResponse::Handled)
2839 } else {
2840 None
2841 }
2842 }
2843 WidgetEvent::PointerDown {
2844 position,
2845 button,
2846 modifiers,
2847 ..
2848 } => {
2849 if fire_on_pointer_event {
2856 let r = fire_event_handler_both(
2857 &mut node.external_handlers.on_pointer_event,
2858 &mut node.handlers.on_pointer_event,
2859 event,
2860 ctx,
2861 );
2862 if r == EventResponse::Handled {
2863 return Some(EventResponse::Handled);
2864 }
2865 }
2866 if arena_blocked {
2867 return None;
2871 }
2872 Self::ensure_gesture_arena(node, node_id, gesture_owners);
2873 if let Some(arena) = node.handlers.gesture_arena.as_mut() {
2874 let cx = gesture_cx;
2875 ctx.capture_pointer_implicit();
2890 let result = arena.process(
2891 &RawPointerEvent::Down {
2892 position: *position,
2893 button: *button,
2894 modifiers: *modifiers,
2895 pointer: cx.pointer,
2896 time: cx.now,
2897 },
2898 &cx,
2899 );
2900 if let Some(gesture) = result {
2901 Self::dispatch_recognized_gesture(node, gesture, ctx);
2902 }
2903 return Some(EventResponse::Handled);
2904 }
2905 None
2906 }
2907 WidgetEvent::PointerUp {
2908 position,
2909 button,
2910 modifiers,
2911 ..
2912 } => {
2913 if fire_on_pointer_event {
2914 let r = fire_event_handler_both(
2915 &mut node.external_handlers.on_pointer_event,
2916 &mut node.handlers.on_pointer_event,
2917 event,
2918 ctx,
2919 );
2920 if r == EventResponse::Handled {
2921 return Some(EventResponse::Handled);
2922 }
2923 }
2924 if arena_blocked {
2925 return None;
2926 }
2927 if let Some(arena) = node.handlers.gesture_arena.as_mut() {
2928 let cx = gesture_cx;
2929 let result = arena.process(
2930 &RawPointerEvent::Up {
2931 position: *position,
2932 button: *button,
2933 modifiers: *modifiers,
2934 pointer: cx.pointer,
2935 time: cx.now,
2936 },
2937 &cx,
2938 );
2939 if let Some(gesture) = result {
2940 Self::dispatch_recognized_gesture(node, gesture, ctx);
2941 }
2942 return Some(EventResponse::Handled);
2943 }
2944 None
2945 }
2946 WidgetEvent::PointerMove { position, .. } => {
2947 if fire_on_pointer_event {
2948 let r = fire_event_handler_both(
2949 &mut node.external_handlers.on_pointer_event,
2950 &mut node.handlers.on_pointer_event,
2951 event,
2952 ctx,
2953 );
2954 if r == EventResponse::Handled {
2955 return Some(EventResponse::Handled);
2956 }
2957 }
2958 if arena_blocked {
2959 return None;
2960 }
2961 if let Some(arena) = node.handlers.gesture_arena.as_mut() {
2962 let cx = gesture_cx;
2963 let result = arena.process(
2964 &RawPointerEvent::Move {
2965 position: *position,
2966 pointer: cx.pointer,
2967 time: cx.now,
2968 },
2969 &cx,
2970 );
2971 if let Some(gesture) = result {
2972 Self::dispatch_recognized_gesture(node, gesture, ctx);
2973 return Some(EventResponse::Handled);
2985 }
2986 return Some(EventResponse::Ignored);
2987 }
2988 None
2989 }
2990 WidgetEvent::PointerCancel {
2991 reason, pointer, ..
2992 } => {
2993 for slot in [
3001 &mut node.external_handlers.on_pointer_cancel,
3002 &mut node.handlers.on_pointer_cancel,
3003 ] {
3004 if let Some(handler) = slot.as_mut() {
3005 handler(pointer, *reason, ctx);
3006 }
3007 }
3008 if fire_on_pointer_event {
3009 let r = fire_event_handler_both(
3010 &mut node.external_handlers.on_pointer_event,
3011 &mut node.handlers.on_pointer_event,
3012 event,
3013 ctx,
3014 );
3015 if r == EventResponse::Handled {
3016 return Some(EventResponse::Handled);
3017 }
3018 }
3019 None
3020 }
3021 }
3022 }
3023
3024 pub(super) fn collect_from_ctx<'ops>(
3025 &mut self,
3026 mut ctx: EventContext<'ops>,
3027 source_widget: WidgetId,
3028 ) {
3029 let local_ops = ctx.window_ops.take();
3035 let mut noop = crate::window::NoopWindowOps;
3036 let ops: &mut dyn crate::window::WindowOps = match local_ops {
3037 Some(o) => o,
3038 None => &mut noop,
3039 };
3040 if ctx.frame_requested {
3041 self.request_frame();
3042 }
3043 if let Some(declared) = ctx.declared_cursor_request {
3047 self.node_declared_cursor = declared;
3048 self.current_cursor = declared;
3049 }
3050 match ctx.cursor_request {
3051 Some(crate::widget::CursorRequest::Set(cursor)) => {
3052 self.current_cursor = cursor;
3053 }
3054 Some(crate::widget::CursorRequest::Release) => {
3058 self.current_cursor = self.node_declared_cursor;
3059 }
3060 None => {}
3061 }
3062 for intent in ctx.pending_intents {
3068 self.enqueue_intent(source_widget, intent, true);
3069 }
3070 if ctx.cancel_key_capture {
3079 self.cancel_key_capture();
3080 }
3081 if let Some(slot) = ctx.pending_key_capture {
3082 self.key_capture = Some(slot);
3083 }
3084 for mutation in ctx.pending_shortcut_mutations {
3086 match mutation {
3087 crate::widget::ShortcutMutation::RebindPrimary { id, keystroke } => {
3088 self.shortcut_registry.rebind_primary(id, keystroke);
3089 }
3090 crate::widget::ShortcutMutation::RebindSecondary { id, keystroke } => {
3091 self.shortcut_registry.rebind_secondary(id, keystroke);
3092 }
3093 crate::widget::ShortcutMutation::ClearOverride { id } => {
3094 self.shortcut_registry.clear_override(&id);
3095 }
3096 }
3097 }
3098 if ctx.close_request_to_modal {
3099 self.answer_close_request_with_modal(&mut *ops);
3100 }
3101 if ctx.close_window_requested {
3102 self.close_window_requested = true;
3103 }
3104 if ctx.force_close_requested {
3105 self.force_close_requested = true;
3106 }
3107 self.pending_modal_requests
3108 .extend(ctx.modal_requests.into_iter().map(|request| {
3109 crate::modal::QueuedModalRequest {
3110 source_widget,
3111 request,
3112 }
3113 }));
3114 let focus_intents = std::mem::take(&mut ctx.pending_focus_intents);
3120 let (fallback_crossings, fallback_modal) = if focus_intents.is_empty() {
3121 (Vec::new(), None)
3122 } else {
3123 let modal = self.intent_walk(source_widget, &[]).2;
3124 (self.intent_route(source_widget, &[], true).crossings, modal)
3125 };
3126 if ctx.dismiss_modal && !self.dismiss_modal_for_source(source_widget, &mut *ops) {
3127 self.pending_modal_dismissal = true;
3128 }
3129 for callback in ctx.idle_callbacks {
3130 self.idle_queue.push_boxed(callback);
3131 }
3132 match ctx.dismiss_scope {
3133 Some(crate::widget::DismissScope::All) => {
3134 let dismissed = self.overlay_manager.dismiss_all();
3135 self.dormant_dismissed_content(&dismissed, &mut *ops);
3136 }
3137 Some(crate::widget::DismissScope::AllExceptHosts) => {
3138 self.dismiss_all_overlays_except_hosts(&mut *ops);
3139 }
3140 Some(crate::widget::DismissScope::SelfChain) => {
3141 self.dismiss_self_overlay_chain_for_source(source_widget, &mut *ops);
3142 }
3143 Some(crate::widget::DismissScope::Top) => {
3144 if let Some((_id, content_ids, focus_restore)) = self.overlay_manager.dismiss_top()
3145 {
3146 self.dormant_dismissed_content(&content_ids, &mut *ops);
3147 if let Some(restore_id) = focus_restore
3148 && self.arena.is_active(restore_id)
3149 {
3150 self.focus_ops(restore_id, &mut *ops);
3151 }
3152 }
3153 }
3154 None => {
3155 for id in ctx.overlay_dismissals {
3156 let dismissed = self.overlay_manager.dismiss(id);
3157 self.dormant_dismissed_content(&dismissed, &mut *ops);
3158 }
3159 }
3160 }
3161 for content_id in ctx.overlay_content_dismissals {
3168 if let Some(overlay_id) = self.overlay_manager.find_by_content(content_id) {
3169 let dismissed = self.overlay_manager.dismiss(overlay_id);
3170 self.dormant_dismissed_content(&dismissed, &mut *ops);
3171 }
3172 }
3173 for (id, pause) in ctx.overlay_pause_requests {
3180 if pause {
3181 self.overlay_manager.pause_auto_dismiss(id);
3182 } else {
3183 self.overlay_manager.resume_auto_dismiss(id);
3184 }
3185 }
3186 for preserve_content in ctx.dismiss_descendant_overlays {
3187 self.dismiss_child_overlays_for_source(source_widget, preserve_content, &mut *ops);
3188 }
3189 if !focus_intents.is_empty() {
3198 let modal_closed = fallback_modal
3199 .is_some_and(|content| self.overlay_manager.find_by_content(content).is_none());
3200 let start = match self.focused.filter(|id| self.arena.is_active(*id)) {
3201 Some(focus) => Some((focus, None)),
3202 None if modal_closed => self.intent_anchor().map(|root| (root, None)),
3203 None if fallback_modal.is_none()
3207 && self.overlay_manager.topmost_modal().is_some() =>
3208 {
3209 self.focus_or_modal().map(|modal| (modal, None))
3210 }
3211 None => Some((source_widget, Some(fallback_crossings))),
3212 };
3213 if let Some((start, crossings)) = start {
3214 for intent in focus_intents {
3215 match &crossings {
3216 None => self.enqueue_intent(start, intent, true),
3217 Some(crossings) => {
3218 self.enqueue_intent_crossing(start, intent, true, crossings.clone())
3219 }
3220 }
3221 }
3222 }
3223 }
3224 let deferred_row_activations =
3225 self.apply_tree_mutations(std::mem::take(&mut ctx.tree_mutations));
3226 if ctx.request_a11y_update {
3227 self.a11y_dirty = true;
3228 }
3229 if let Some(visible) = ctx.soft_keyboard_request.take() {
3230 self.request_soft_keyboard(visible);
3231 }
3232 for (message, politeness) in std::mem::take(&mut ctx.announcements) {
3235 self.announce_with(message, politeness);
3236 }
3237 for mut req in ctx.overlay_requests {
3238 if req.parent_overlay.is_none() {
3239 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3240 }
3241 if self
3242 .overlay_manager
3243 .find_by_content(req.content_id)
3244 .is_some()
3245 {
3246 continue;
3247 }
3248 let current_focus = self.focused;
3249 self.overlay_manager.show(req);
3250 self.a11y_dirty = true;
3254 if let Some(focus_id) = current_focus {
3255 self.overlay_manager.set_top_focus_restore(focus_id);
3256 }
3257 }
3258 for (mut req, band) in ctx.overlay_band_requests {
3259 if req.parent_overlay.is_none() {
3260 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3261 }
3262 if self
3263 .overlay_manager
3264 .find_by_content(req.content_id)
3265 .is_some()
3266 {
3267 continue;
3268 }
3269 let content_id = req.content_id;
3270 self.overlay_manager.show_in_band(req, band);
3271 self.arena.activate(content_id);
3272 self.a11y_dirty = true;
3273 }
3277 for (content_id, placement) in ctx.overlay_placement_updates {
3280 if let Some(overlay_id) = self.overlay_manager.find_by_content(content_id) {
3281 self.overlay_manager.update_placement(overlay_id, placement);
3282 }
3283 }
3284 for (content_id, source) in ctx.overlay_placement_sources {
3285 if let Some(overlay_id) = self.overlay_manager.find_by_content(content_id) {
3286 self.overlay_manager
3287 .set_placement_source(overlay_id, source);
3288 }
3289 }
3290 for (mut req, duration) in ctx.timed_overlay_requests {
3291 if req.parent_overlay.is_none() {
3292 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3293 }
3294 if self
3295 .overlay_manager
3296 .find_by_content(req.content_id)
3297 .is_some()
3298 {
3299 continue;
3300 }
3301 let current_focus = self.focused;
3302 let overlay_id = self.overlay_manager.show_for(req, duration);
3303 self.overlay_manager
3304 .set_shown_at_sim(overlay_id, self.sim_clock);
3305 self.a11y_dirty = true;
3306 if let Some(focus_id) = current_focus {
3307 self.overlay_manager.set_top_focus_restore(focus_id);
3308 }
3309 }
3310 for (mut req, progress, duration) in ctx.reveal_overlay_requests {
3311 if req.parent_overlay.is_none() {
3312 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3313 }
3314 if self
3315 .overlay_manager
3316 .find_by_content(req.content_id)
3317 .is_some()
3318 {
3319 continue;
3320 }
3321 let content_id = req.content_id;
3322 let current_focus = self.focused;
3323 let overlay_id = self.overlay_manager.show(req);
3324 self.overlay_manager
3325 .set_shown_at_sim(overlay_id, self.sim_clock);
3326 self.a11y_dirty = true;
3327 if let Some(focus_id) = current_focus {
3328 self.overlay_manager.set_top_focus_restore(focus_id);
3329 }
3330 self.register_animated_signal(&progress, content_id);
3336 let _ = progress.try_animate_with_options(crate::animation::AnimationRequest {
3337 target: 1.0,
3338 duration,
3339 easing: teksilo_tokens::Easing::EaseOut,
3340 frame_interval: None,
3341 looping: false,
3342 epsilon: 0.0,
3343 max_duration: None,
3344 });
3345 self.overlay_manager
3346 .attach_fade(overlay_id, progress, duration);
3347 }
3348 if let Some((pointer, capture)) = ctx.pointer_capture {
3349 let named = pointer;
3353 let pointer = pointer.unwrap_or_else(|| self.current_pointer_id());
3354 self.set_pointer_capture(pointer, capture.then_some(source_widget));
3355 if capture && ctx.explicit_capture && named.is_none() {
3359 self.note_explicit_capture(source_widget);
3360 }
3361 }
3362 if let Some(activation) = ctx.drag_activation_override.take() {
3363 self.note_drag_activation_override(source_widget, activation);
3367 }
3368 if ctx.recognized_owning_gesture {
3369 self.note_gesture_recognized(source_widget);
3372 }
3373 if !ctx.gesture_acts.is_empty() {
3374 let acts = std::mem::take(&mut ctx.gesture_acts);
3375 self.apply_gesture_acts(&acts, source_widget);
3376 }
3377 if let Some(reason) = ctx.cancel_pointer_request {
3378 let pointer = self.current_pointer_id();
3379 self.cancel_pointer(pointer, reason, &mut *ops);
3380 }
3381 for (mut request, delay, focus_target, replace_siblings) in ctx.delayed_overlay_requests {
3382 if request.parent_overlay.is_none() {
3383 request.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3384 }
3385 if self
3386 .overlay_manager
3387 .find_by_content(request.content_id)
3388 .is_some()
3389 {
3390 continue;
3391 }
3392 let content_id = request.content_id;
3393 self.pending_delayed_overlays
3394 .retain(|pending| pending.request.content_id != content_id);
3395 self.pending_delayed_overlays.push(PendingDelayedOverlay {
3396 request,
3397 delay,
3398 focus_target,
3399 replace_siblings,
3400 real_requested_at: std::time::Instant::now(),
3401 sim_requested_at: self.sim_clock,
3402 });
3403 self.arena.mark_needs_paint(source_widget);
3404 }
3405 for content_id in ctx.cancel_delayed_overlays {
3406 self.pending_delayed_overlays
3407 .retain(|pending| pending.request.content_id != content_id);
3408 }
3409 for content_id in ctx.safe_region_arm_requests {
3416 if let Some(apex) = self
3417 .previous_pointer_position
3418 .or_else(|| self.hover_owner_position())
3419 {
3420 self.overlay_manager.arm_safe_region(
3421 content_id,
3422 apex,
3423 std::time::Instant::now(),
3424 self.sim_clock,
3425 );
3426 }
3427 }
3428 for id in ctx.repaint_requests {
3429 self.arena.mark_needs_paint(id);
3430 }
3431 for id in ctx.synthetic_clicks {
3432 self.synthesise_tap_with_ops(id, &mut *ops);
3437 }
3438 if ctx.clear_focus_requested {
3441 self.clear_focus_ops(&mut *ops);
3442 }
3443 if let Some(&id) = ctx.focus_requests.last() {
3448 self.request_focus_with_ops(id, super::FocusRequestMode::SelfOrDescendant, &mut *ops);
3458 }
3459 if let Some(&id) = ctx.focus_into_requests.last() {
3460 self.request_focus_with_ops(id, super::FocusRequestMode::Into, &mut *ops);
3467 }
3468
3469 for req in ctx.scroll_into_view_requests {
3478 self.scroll_rect_into_view(
3479 req.from.unwrap_or(source_widget),
3482 req.rect,
3483 req.margin,
3484 req.align,
3485 req.motion,
3486 &mut *ops,
3487 );
3488 }
3489 for (id, margin) in ctx.scroll_widget_into_view_requests {
3496 if self.arena.get(id).is_some() {
3497 let bounds = self.arena.bounds(id);
3498 self.scroll_rect_into_view(
3499 id,
3500 bounds,
3501 margin,
3502 crate::event::ScrollAlign::Minimal,
3503 crate::event::ScrollMotion::Instant,
3504 &mut *ops,
3505 );
3506 }
3507 }
3508
3509 if let Some(&id) = ctx.highlight_tooltip_requests.last() {
3514 self.show_highlight_tooltip(id, &mut *ops);
3515 }
3516
3517 if let Some((source_widget, payload, preview_widget)) = ctx.drag_start_request {
3519 let (preview_content_id, preview_overlay_id) = if let Some(preview) = preview_widget {
3520 let content_id = self.add_boxed(preview);
3527 let overlay_id = self
3528 .overlay_manager
3529 .show(crate::overlay::OverlayRequest::new(
3530 content_id,
3531 source_widget,
3532 crate::overlay::OverlayPlacement::AtPointer(teksilo_canvas::Point::ZERO),
3533 crate::overlay::DismissBehavior::Manual,
3534 ));
3535 self.arena.mark_needs_layout(content_id);
3539 (Some(content_id), Some(overlay_id))
3540 } else {
3541 (None, None)
3542 };
3543 self.active_drag = Some(crate::drag_state::DragSession {
3544 payload,
3545 pointer: self.current_input.pointer,
3551 source_widget: Some(source_widget),
3552 is_external: false,
3553 current_position: teksilo_canvas::Point::ZERO,
3554 current_target: None,
3555 feedback: crate::drag_state::DropFeedback::NoFeedback,
3556 preview_content_id,
3557 preview_overlay_id,
3558 });
3559 self.set_current_pointer_capture(Some(source_widget));
3560 self.current_cursor = crate::widget::CursorIcon::Grabbing;
3563 }
3564 if ctx.cancel_drag {
3565 self.cancel_active_drag(&mut *ops);
3566 }
3567
3568 if let Some(theme) = ctx.theme_request {
3570 self.pending_theme_request = Some(theme);
3576 }
3577 if ctx.follow_system_request {
3578 self.pending_follow_system_request = true;
3582 }
3583 if let Some(locale) = ctx.locale_request {
3584 self.pending_locale_request = Some(locale);
3590 }
3591 if let Some(scale) = ctx.text_scale_request {
3592 self.pending_text_scale_request = Some(scale);
3597 }
3598 if !deferred_row_activations.is_empty() {
3605 self.run_with_event_context(&mut *ops, move |ctx| {
3606 for action in deferred_row_activations {
3607 action(ctx);
3608 }
3609 });
3610 }
3611 }
3612
3613 #[must_use]
3619 fn apply_tree_mutations(
3620 &mut self,
3621 mutations: Vec<crate::widget::TreeMutation>,
3622 ) -> Vec<std::rc::Rc<dyn Fn(&mut crate::widget::EventContext)>> {
3623 let mut deferred_row_activations = Vec::new();
3624 use crate::binding::BindingLevel;
3625 use crate::widget::TreeMutation;
3626
3627 for mutation in mutations {
3628 match mutation {
3629 TreeMutation::SetDormant(id) => {
3630 self.park_subtree(id);
3631 }
3632 TreeMutation::Activate(id) => self.arena.activate(id),
3633 TreeMutation::Destroy(id) => {
3634 if let Some(overlay_id) = self.overlay_manager().find_by_content(id) {
3647 self.dismiss_overlay(overlay_id);
3648 }
3649 self.destroy_subtree(id);
3650 }
3651 TreeMutation::MaterializeNow(id) => {
3655 if self.arena.get(id).is_some() {
3656 self.rebuild_single_widget(id);
3657 }
3658 }
3659 TreeMutation::RowSpaceActivate { row, fallback } => {
3660 deferred_row_activations.push(self.keyboard_toggle_in(row).unwrap_or(fallback));
3670 }
3671 TreeMutation::WithWidgetMut { id, dirty, apply } => {
3672 let existed = if let Some(any) =
3677 self.arena.get_mut(id).and_then(|n| n.widget.as_any_mut())
3678 {
3679 apply(any);
3680 true
3681 } else {
3682 false
3683 };
3684 if existed {
3685 match dirty {
3686 BindingLevel::RepaintOnly => self.arena.mark_needs_paint(id),
3687 BindingLevel::SubtreeRepaint => self.arena.mark_subtree_needs_paint(id),
3688 BindingLevel::Relayout => {
3689 self.arena.mark_needs_layout(id);
3690 self.arena.mark_ancestors_need_layout(id);
3691 }
3692 BindingLevel::Rebuild => {
3693 self.arena.mark_needs_rebuild(id);
3694 self.arena.mark_ancestors_need_layout(id);
3695 }
3696 BindingLevel::AccessibilityOnly => self.a11y_dirty = true,
3697 }
3698 }
3699 }
3700 }
3701 }
3702 deferred_row_activations
3703 }
3704
3705 pub fn hit_test(&self, point: Point) -> Option<WidgetId> {
3714 self.hit_test_excluding_overlay_and_widget(point, None, None)
3715 }
3716
3717 pub fn hit_test_for(
3728 &self,
3729 point: Point,
3730 pointer: &crate::pointer::PointerInfo,
3731 ) -> Option<WidgetId> {
3732 self.hit_test_for_excluding(point, pointer, None, None)
3733 }
3734
3735 pub fn hit_test_for_excluding(
3738 &self,
3739 point: Point,
3740 pointer: &crate::pointer::PointerInfo,
3741 exclude_overlay: Option<crate::overlay::OverlayId>,
3742 exclude_widget: Option<WidgetId>,
3743 ) -> Option<WidgetId> {
3744 let surfaces = self.text_surfaces();
3745 let read_only = |id: WidgetId| surfaces.is_read_only(id);
3746 let hit =
3747 crate::pointer::hit_slop::HitContext::new(pointer.kind, &self.effective_theme.input)
3748 .direction(self.layout_direction)
3749 .read_only_probe(&read_only);
3750 self.hit_test_with(point, exclude_overlay, exclude_widget, &hit)
3751 }
3752
3753 pub fn hit_test_excluding_overlay_and_widget(
3760 &self,
3761 point: Point,
3762 exclude_overlay: Option<crate::overlay::OverlayId>,
3763 exclude_widget: Option<WidgetId>,
3764 ) -> Option<WidgetId> {
3765 self.hit_test_with(
3766 point,
3767 exclude_overlay,
3768 exclude_widget,
3769 &crate::pointer::hit_slop::HitContext::mouse(),
3770 )
3771 }
3772
3773 pub(crate) fn hit_test_with(
3776 &self,
3777 point: Point,
3778 exclude_overlay: Option<crate::overlay::OverlayId>,
3779 exclude_widget: Option<WidgetId>,
3780 hit: &crate::pointer::hit_slop::HitContext<'_>,
3781 ) -> Option<WidgetId> {
3782 if let Some(overlay_id) = self.overlay_manager.hit_test(point) {
3783 if Some(overlay_id) == exclude_overlay {
3784 } else if let Some(overlay) = self.overlay_manager.overlay(overlay_id) {
3786 if self.overlay_manager.is_modal_scrim(overlay)
3788 && let Some(chrome) = self.window_chrome_under_modal(point, exclude_widget, hit)
3789 {
3790 return Some(chrome);
3791 }
3792 let content_id = overlay.content_id;
3796 if let Some(found) =
3797 self.arena
3798 .hit_test_in_subtree_with_slop(content_id, point, exclude_widget, hit)
3799 {
3800 return Some(found);
3801 }
3802 if !self
3820 .arena
3821 .get(content_id)
3822 .is_some_and(|node| node.event_pass_through)
3823 {
3824 return None;
3825 }
3826 }
3827 }
3828
3829 if self.overlay_manager.topmost_modal().is_some() {
3830 return self.window_chrome_under_modal(point, exclude_widget, hit);
3831 }
3832
3833 let mut roots = self.arena.roots();
3839 let inert: Vec<WidgetId> = self
3840 .overlay_manager
3841 .stack
3842 .iter()
3843 .filter(|overlay| overlay.inert)
3844 .map(|overlay| overlay.content_id)
3845 .collect();
3846 if !inert.is_empty() {
3847 roots.retain(|root| !inert.contains(root));
3848 }
3849 self.arena
3850 .hit_test_roots_with_slop(roots, point, exclude_widget, hit)
3851 }
3852}
3853
3854impl WidgetTree {
3855 fn window_chrome_under_modal(
3870 &self,
3871 point: Point,
3872 exclude_widget: Option<WidgetId>,
3873 hit: &crate::pointer::hit_slop::HitContext<'_>,
3874 ) -> Option<WidgetId> {
3875 let overlays: Vec<WidgetId> = self
3876 .overlay_manager
3877 .stack
3878 .iter()
3879 .map(|overlay| overlay.content_id)
3880 .collect();
3881 let mut roots = self.arena.roots();
3882 roots.retain(|root| !overlays.contains(root));
3883 let found = self
3884 .arena
3885 .hit_test_roots_with_slop(roots, point, exclude_widget, hit)?;
3886 let mut node = Some(found);
3887 while let Some(id) = node {
3888 let current = self.arena.get(id)?;
3889 if current.window_chrome {
3890 return Some(id);
3891 }
3892 if current.gesture_dead_zone {
3893 return None;
3894 }
3895 node = self.arena.parent(id);
3896 }
3897 None
3898 }
3899}
3900
3901fn is_context_menu_chord(key: Key, modifiers: Modifiers) -> bool {
3919 match key {
3920 Key::ContextMenu => modifiers == Modifiers::NONE,
3921 Key::F10 => modifiers == Modifiers::SHIFT,
3922 #[cfg(target_os = "macos")]
3923 Key::M => modifiers == Modifiers::CTRL | Modifiers::SHIFT,
3924 _ => false,
3925 }
3926}
3927
3928#[cfg(test)]
3929mod tests {
3930 use super::*;
3931 use crate::test_widgets::FillWidget;
3932 use crate::widget::CursorIcon;
3933 use crate::widget_builder::WidgetBuilder;
3934
3935 #[test]
3936 fn pointer_enter_leave_synthesized() {
3937 let mut tree = WidgetTree::new();
3938 let widget = tree.add(FillWidget::new());
3939 tree.layout(SizeProposal::exact(100.0, 50.0));
3940 tree.pointer_move(Point::new(50.0, 25.0));
3941 assert_eq!(tree.hovered(), Some(widget));
3942 tree.pointer_move(Point::new(200.0, 200.0));
3943 assert_eq!(tree.hovered(), None);
3944 }
3945
3946 #[test]
3947 fn pointer_hover_updates_current_cursor() {
3948 let mut tree = WidgetTree::new();
3949 tree.add(FillWidget::new().cursor(CursorIcon::ColResize));
3950 tree.layout(SizeProposal::exact(100.0, 50.0));
3951
3952 tree.pointer_move(Point::new(50.0, 25.0));
3953 assert_eq!(tree.current_cursor(), CursorIcon::ColResize);
3954
3955 tree.pointer_move(Point::new(200.0, 200.0));
3956 assert_eq!(tree.current_cursor(), CursorIcon::Default);
3957 }
3958
3959 #[derive(Debug)]
3962 struct SelfDeclared(CursorIcon);
3963
3964 impl crate::widget::Widget for SelfDeclared {
3965 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
3966 ctx.apply_self_handlers(crate::widget_builder::HandlerSet::new().cursor(self.0));
3967 Vec::new()
3968 }
3969
3970 fn layout_response(
3971 &self,
3972 proposal: SizeProposal,
3973 _ctx: &crate::widget::LayoutContext,
3974 ) -> crate::widget::LayoutResponse {
3975 proposal.resolve(100.0, 50.0).into()
3976 }
3977 }
3978
3979 #[test]
3985 fn declared_cursor_reads_what_a_node_asks_for_and_current_cursor_follows_the_hover() {
3986 let over = Point::new(50.0, 25.0);
3987 let away = Point::new(200.0, 200.0);
3988
3989 let mut tree = WidgetTree::new();
3991 let built = tree.add(FillWidget::new().cursor(CursorIcon::ColResize));
3992 tree.layout(SizeProposal::exact(100.0, 50.0));
3993 assert_eq!(tree.declared_cursor(built), Some(CursorIcon::ColResize));
3994 assert_eq!(
3995 tree.current_cursor(),
3996 CursorIcon::Default,
3997 "not hovered yet"
3998 );
3999 tree.pointer_move(over);
4000 assert_eq!(tree.current_cursor(), CursorIcon::ColResize);
4001 assert_eq!(tree.declared_cursor(built), Some(CursorIcon::ColResize));
4002 tree.pointer_move(away);
4003 assert_eq!(tree.current_cursor(), CursorIcon::Default);
4004 assert_eq!(
4005 tree.declared_cursor(built),
4006 Some(CursorIcon::ColResize),
4007 "the declaration does not depend on the pointer",
4008 );
4009
4010 let mut tree = WidgetTree::new();
4012 let own = tree.add(SelfDeclared(CursorIcon::Crosshair));
4013 tree.layout(SizeProposal::exact(100.0, 50.0));
4014 assert_eq!(tree.declared_cursor(own), Some(CursorIcon::Crosshair));
4015 tree.pointer_move(over);
4016 assert_eq!(tree.current_cursor(), CursorIcon::Crosshair);
4017 tree.pointer_move(away);
4018 assert_eq!(tree.current_cursor(), CursorIcon::Default);
4019
4020 let mut tree = WidgetTree::new();
4022 let plain = tree.add(FillWidget::new());
4023 tree.layout(SizeProposal::exact(100.0, 50.0));
4024 assert_eq!(tree.declared_cursor(plain), None);
4025 let mut tree = WidgetTree::new();
4026 let explicit = tree.add(FillWidget::new().cursor(CursorIcon::Default));
4027 tree.layout(SizeProposal::exact(100.0, 50.0));
4028 assert_eq!(
4029 tree.declared_cursor(explicit),
4030 Some(CursorIcon::Default),
4031 "declaring the default cursor is not declaring nothing",
4032 );
4033 }
4034
4035 #[test]
4045 fn release_cursor_hands_the_cursor_back_to_the_node_that_declared_it() {
4046 let mut tree = WidgetTree::new();
4050 tree.add(
4051 FillWidget::new()
4052 .cursor(CursorIcon::ColResize)
4053 .on_pointer_event(|event, ctx| {
4054 if let WidgetEvent::PointerMove { position, .. } = event {
4055 if position.x < 30.0 {
4056 ctx.set_cursor(CursorIcon::Crosshair);
4057 } else {
4058 ctx.release_cursor();
4059 }
4060 }
4061 EventResponse::Ignored
4062 }),
4063 );
4064 tree.layout(SizeProposal::exact(100.0, 50.0));
4065
4066 tree.pointer_move(Point::new(10.0, 25.0));
4067 assert_eq!(
4068 tree.current_cursor(),
4069 CursorIcon::Crosshair,
4070 "the handler outranks the node it is attached to",
4071 );
4072 tree.pointer_move(Point::new(60.0, 25.0));
4073 assert_eq!(
4074 tree.current_cursor(),
4075 CursorIcon::ColResize,
4076 "and hands it back to the node, not to Default — no enter/leave \
4077 fires on a move within one node, so nothing else could",
4078 );
4079 tree.pointer_move(Point::new(10.0, 25.0));
4080 assert_eq!(
4081 tree.current_cursor(),
4082 CursorIcon::Crosshair,
4083 "and can take it again"
4084 );
4085 tree.pointer_move(Point::new(200.0, 200.0));
4086 assert_eq!(
4087 tree.current_cursor(),
4088 CursorIcon::Default,
4089 "leaving the node clears both the override and the declaration",
4090 );
4091 }
4092
4093 #[test]
4097 fn release_cursor_is_a_no_op_with_nothing_to_undo() {
4098 let mut tree = WidgetTree::new();
4099 tree.add(
4100 FillWidget::new()
4101 .cursor(CursorIcon::ColResize)
4102 .on_pointer_event(|event, ctx| {
4103 if matches!(event, WidgetEvent::PointerMove { .. }) {
4104 ctx.release_cursor();
4105 }
4106 EventResponse::Ignored
4107 }),
4108 );
4109 tree.layout(SizeProposal::exact(100.0, 50.0));
4110 tree.pointer_move(Point::new(50.0, 25.0));
4111 assert_eq!(tree.current_cursor(), CursorIcon::ColResize);
4112
4113 let mut tree = WidgetTree::new();
4115 tree.add(FillWidget::new().on_pointer_event(|event, ctx| {
4116 if let WidgetEvent::PointerMove { position, .. } = event {
4117 if position.x < 30.0 {
4118 ctx.set_cursor(CursorIcon::Crosshair);
4119 } else {
4120 ctx.release_cursor();
4121 }
4122 }
4123 EventResponse::Ignored
4124 }));
4125 tree.layout(SizeProposal::exact(100.0, 50.0));
4126 tree.pointer_move(Point::new(10.0, 25.0));
4127 assert_eq!(tree.current_cursor(), CursorIcon::Crosshair);
4128 tree.pointer_move(Point::new(60.0, 25.0));
4129 assert_eq!(
4130 tree.current_cursor(),
4131 CursorIcon::Default,
4132 "nothing declared a cursor for this chain, so the hand-back \
4133 resolves to Default",
4134 );
4135 }
4136
4137 #[test]
4145 fn an_on_hover_override_does_not_erase_the_declaration_it_outranks() {
4146 let mut tree = WidgetTree::new();
4147 tree.add(
4148 FillWidget::new()
4149 .cursor(CursorIcon::ColResize)
4150 .on_hover(|entered, ctx| {
4151 if entered {
4152 ctx.set_cursor(CursorIcon::Crosshair);
4153 }
4154 })
4155 .on_pointer_event(|event, ctx| {
4156 if let WidgetEvent::PointerMove { position, .. } = event
4157 && position.x >= 30.0
4158 {
4159 ctx.release_cursor();
4160 }
4161 EventResponse::Ignored
4162 }),
4163 );
4164 tree.layout(SizeProposal::exact(100.0, 50.0));
4165
4166 tree.pointer_move(Point::new(10.0, 25.0));
4167 assert_eq!(
4168 tree.current_cursor(),
4169 CursorIcon::Crosshair,
4170 "on_hover runs after the node cursor is applied, so it wins",
4171 );
4172 tree.pointer_move(Point::new(60.0, 25.0));
4173 assert_eq!(
4174 tree.current_cursor(),
4175 CursorIcon::ColResize,
4176 "and the node's declaration survived being overridden",
4177 );
4178 }
4179
4180 #[derive(Debug)]
4183 struct Bumpable {
4184 value: i32,
4185 }
4186
4187 impl crate::widget::Widget for Bumpable {
4188 fn layout_response(
4189 &self,
4190 proposal: SizeProposal,
4191 _ctx: &crate::widget::LayoutContext,
4192 ) -> crate::widget::LayoutResponse {
4193 proposal.resolve(10.0, 10.0).into()
4194 }
4195 fn as_any(&self) -> Option<&dyn std::any::Any> {
4196 Some(self)
4197 }
4198 fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
4199 Some(self)
4200 }
4201 }
4202
4203 #[test]
4204 fn with_widget_mut_applies_and_dirty_marks() {
4205 let mut tree = WidgetTree::new();
4206 let id = tree.add(Bumpable { value: 0 });
4207 tree.layout(SizeProposal::exact(100.0, 100.0));
4208
4209 let mut ctx = EventContext::new();
4210 ctx.with_widget_mut::<Bumpable>(id, crate::binding::BindingLevel::Relayout, |b| {
4211 b.value = 42;
4212 });
4213 tree.collect_from_ctx(ctx, id);
4214
4215 let value = tree
4216 .widget_as_any(id)
4217 .and_then(|a| a.downcast_ref::<Bumpable>())
4218 .map(|b| b.value);
4219 assert_eq!(
4220 value,
4221 Some(42),
4222 "the deferred closure must mutate the live widget"
4223 );
4224 assert!(
4225 tree.needs_layout(),
4226 "Relayout dirty level must mark the tree for relayout"
4227 );
4228 }
4229
4230 #[test]
4231 #[cfg(debug_assertions)]
4232 #[should_panic(expected = "not the requested type")]
4233 fn with_widget_mut_wrong_type_panics_in_debug() {
4234 struct Other;
4235 let mut tree = WidgetTree::new();
4236 let id = tree.add(Bumpable { value: 0 });
4237 let mut ctx = EventContext::new();
4238 ctx.with_widget_mut::<Other>(
4239 id,
4240 crate::binding::BindingLevel::RepaintOnly,
4241 |_o: &mut Other| {},
4242 );
4243 tree.collect_from_ctx(ctx, id);
4246 }
4247
4248 #[test]
4249 fn with_widget_mut_closure_may_fire_observed_signals() {
4250 use crate::signal::Signal;
4258 let mut tree = WidgetTree::new();
4259 let id = tree.add(Bumpable { value: 0 });
4260 tree.layout(SizeProposal::exact(100.0, 100.0));
4261
4262 let trigger = Signal::new(0_u64);
4263 let echo = Signal::new(0_u64);
4264 let echo_for_obs = echo.clone();
4265 let _obs = trigger.observe(move |v| echo_for_obs.set(*v));
4266
4267 let trigger_in = trigger.clone();
4268 let mut ctx = EventContext::new();
4269 ctx.with_widget_mut::<Bumpable>(id, crate::binding::BindingLevel::RepaintOnly, move |b| {
4270 b.value = 7;
4271 trigger_in.set(99);
4273 });
4274 tree.collect_from_ctx(ctx, id); assert_eq!(
4277 echo.get(),
4278 99,
4279 "the observer ran during the deferred mutation"
4280 );
4281 let value = tree
4282 .widget_as_any(id)
4283 .and_then(|a| a.downcast_ref::<Bumpable>())
4284 .map(|b| b.value);
4285 assert_eq!(value, Some(7));
4286 }
4287
4288 #[test]
4289 fn request_accessibility_update_forces_rewalk() {
4290 let mut tree = WidgetTree::new();
4291 let id = tree.add(Bumpable { value: 0 });
4292 tree.layout(SizeProposal::exact(100.0, 100.0));
4293 let _ = tree.sync_accessibility(); assert!(
4295 !tree.a11y_dirty,
4296 "sync_accessibility should clear the dirty flag"
4297 );
4298
4299 let mut ctx = EventContext::new();
4300 ctx.request_accessibility_update();
4301 tree.collect_from_ctx(ctx, id);
4302 assert!(
4303 tree.a11y_dirty,
4304 "request_accessibility_update must force an AT re-walk"
4305 );
4306 }
4307
4308 #[test]
4309 fn rebuild_dirties_accessibility_tree() {
4310 let mut tree = WidgetTree::new();
4317 let id = tree.add(Bumpable { value: 0 });
4318 tree.layout(SizeProposal::exact(100.0, 100.0));
4319 let _ = tree.sync_accessibility(); assert!(
4321 !tree.a11y_dirty,
4322 "sync_accessibility should clear the dirty flag"
4323 );
4324
4325 tree.arena_mark_needs_rebuild_for_testing(id);
4328 tree.layout(SizeProposal::exact(100.0, 100.0));
4329 assert!(
4330 tree.a11y_dirty,
4331 "a rebuild must dirty the AT tree so the next sync re-walks"
4332 );
4333 }
4334
4335 #[test]
4336 fn bound_access_label_change_dirties_accessibility_tree() {
4337 use crate::signal::Signal;
4338 use crate::test_widgets::FillWidget;
4339 use crate::widget_builder::WidgetBuilder;
4340
4341 let label = Signal::new("first".to_string());
4347 let mut tree = WidgetTree::new();
4348 let _id = tree.add(FillWidget::new().access_label(label.clone()));
4349 tree.layout(SizeProposal::exact(100.0, 100.0));
4350 let _ = tree.sync_accessibility(); assert!(!tree.a11y_dirty, "sync_accessibility should clear the flag");
4352
4353 label.set("second".to_string());
4354 tree.layout(SizeProposal::exact(100.0, 100.0));
4355 assert!(
4356 tree.a11y_dirty,
4357 "changing a bound access_label must dirty the AT tree"
4358 );
4359 }
4360
4361 #[test]
4362 fn disabled_ancestor_blocks_event_to_descendant() {
4363 use crate::signal::Signal;
4364 use crate::test_widgets::StackWidget;
4365 use std::cell::Cell;
4366 use std::rc::Rc;
4367
4368 let tapped = Rc::new(Cell::new(false));
4369 let flag = tapped.clone();
4370 let enabled = Signal::new(true);
4371
4372 let mut tree = WidgetTree::new();
4373 let child = tree.add(FillWidget::new().on_tap(move |_pos, _ctx| {
4374 flag.set(true);
4375 }));
4376 let parent = tree.add(StackWidget::new().child(child));
4377 tree.enabled_when(parent, enabled.clone());
4378 tree.layout(SizeProposal::exact(100.0, 50.0));
4379
4380 enabled.set(false);
4381 tree.click(child);
4382 assert!(
4383 !tapped.get(),
4384 "disabled ancestor should block descendant tap"
4385 );
4386
4387 enabled.set(true);
4388 tree.click(child);
4389 assert!(tapped.get(), "re-enabling should restore dispatch");
4390 }
4391
4392 #[test]
4393 fn pointer_positions_are_widget_local_at_nonzero_origin() {
4394 use crate::event::{Modifiers, PointerButton};
4395 use crate::test_widgets::InsetWidget;
4396 use std::cell::Cell;
4397 use std::rc::Rc;
4398
4399 let tap_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
4401 let down_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
4402 let drag_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
4403 let (tp, dp, gp) = (tap_pos.clone(), down_pos.clone(), drag_pos.clone());
4404
4405 let mut tree = WidgetTree::new();
4406 let child = tree.add(
4407 FillWidget::new()
4408 .on_tap(move |ev, _ctx| tp.set(Some(ev.position)))
4409 .on_pointer_event(move |ev, _ctx| {
4410 if let WidgetEvent::PointerDown { position, .. } = ev {
4411 dp.set(Some(*position));
4412 }
4413 crate::event::EventResponse::Ignored
4414 })
4415 .on_drag(move |phase, _ctx| {
4416 use crate::gesture::DragPhase;
4417 match phase {
4418 DragPhase::Started { position, .. }
4419 | DragPhase::Moved { position, .. }
4420 | DragPhase::Ended { position, .. } => gp.set(Some(position)),
4421 _ => {}
4422 }
4423 }),
4424 );
4425 let inset = tree.add(InsetWidget::new(20.0).set_child(child));
4426 let _ = inset;
4427 tree.layout(SizeProposal::exact(200.0, 200.0));
4428 assert_eq!(tree.bounds(child).origin(), Point::new(20.0, 20.0));
4429
4430 tree.dispatch_event(WidgetEvent::pointer_down(
4432 Point::new(50.0, 40.0),
4433 PointerButton::Primary,
4434 Modifiers::NONE,
4435 ));
4436 assert_eq!(
4437 down_pos.get(),
4438 Some(Point::new(30.0, 20.0)),
4439 "on_pointer_event PointerDown must be widget-local"
4440 );
4441 tree.dispatch_event(WidgetEvent::pointer_up(
4442 Point::new(50.0, 40.0),
4443 PointerButton::Primary,
4444 Modifiers::NONE,
4445 ));
4446 assert_eq!(
4447 tap_pos.get(),
4448 Some(Point::new(30.0, 20.0)),
4449 "on_tap position must be widget-local"
4450 );
4451
4452 tree.dispatch_event(WidgetEvent::pointer_down(
4455 Point::new(50.0, 40.0),
4456 PointerButton::Primary,
4457 Modifiers::NONE,
4458 ));
4459 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(65.0, 55.0)));
4462 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(90.0, 70.0)));
4463 assert_eq!(
4464 drag_pos.get(),
4465 Some(Point::new(70.0, 50.0)),
4466 "on_drag position must be widget-local"
4467 );
4468 }
4469
4470 #[test]
4471 fn dormant_widget_not_hit_tested() {
4472 let mut tree = WidgetTree::new();
4473 let widget = tree.add(FillWidget::new());
4474 tree.layout(SizeProposal::exact(100.0, 50.0));
4475
4476 tree.pointer_move(Point::new(50.0, 25.0));
4477 assert_eq!(tree.hovered(), Some(widget));
4478
4479 tree.set_dormant(widget);
4480 tree.pointer_move(Point::new(200.0, 200.0));
4481 tree.pointer_move(Point::new(50.0, 25.0));
4482 assert_eq!(tree.hovered(), None);
4483 }
4484
4485 #[test]
4486 fn ancestor_pointer_handler_does_not_suppress_descendant_hover() {
4487 use crate::event::EventResponse;
4488 use crate::test_widgets::StackWidget;
4489 use std::cell::Cell;
4490 use std::rc::Rc;
4491
4492 let hovered = Rc::new(Cell::new(false));
4494 let h = hovered.clone();
4495
4496 let mut tree = WidgetTree::new();
4497 let child = tree.add(FillWidget::new().on_hover(move |entered, _ctx| h.set(entered)));
4498 tree.add(
4503 StackWidget::new()
4504 .child(child)
4505 .on_pointer_event(|_event, _ctx| EventResponse::Handled),
4506 );
4507 tree.layout(SizeProposal::exact(100.0, 50.0));
4508
4509 tree.pointer_move(Point::new(50.0, 25.0));
4510 assert!(
4511 hovered.get(),
4512 "a greedy ancestor on_pointer_event must NOT swallow the child's PointerEnter"
4513 );
4514
4515 tree.pointer_move(Point::new(500.0, 500.0));
4516 assert!(
4517 !hovered.get(),
4518 "PointerLeave must likewise reach the child despite the ancestor"
4519 );
4520 }
4521
4522 #[test]
4530 fn a_child_hover_handler_does_not_swallow_its_ancestors() {
4531 use crate::test_widgets::StackWidget;
4532 use std::cell::Cell;
4533 use std::rc::Rc;
4534
4535 let (row, button) = (Rc::new(Cell::new(false)), Rc::new(Cell::new(false)));
4536 let (r, b) = (row.clone(), button.clone());
4537
4538 let mut tree = WidgetTree::new();
4539 let child = tree.add(FillWidget::new().on_hover(move |entered, _ctx| b.set(entered)));
4540 tree.add(
4541 StackWidget::new()
4542 .child(child)
4543 .on_hover(move |entered, _ctx| r.set(entered)),
4544 );
4545 tree.layout(SizeProposal::exact(100.0, 50.0));
4546
4547 tree.pointer_move(Point::new(50.0, 25.0));
4548 assert!(button.get(), "the child is hovered");
4549 assert!(row.get(), "and so is the row it is inside");
4550
4551 tree.pointer_move(Point::new(500.0, 500.0));
4552 assert!(!button.get(), "the child heard the leave");
4553 assert!(
4554 !row.get(),
4555 "and so did the row — a container is not still hovered because the \
4556 pointer left it through a button"
4557 );
4558 }
4559
4560 #[test]
4567 fn key_preview_consumes_before_focused_on_key() {
4568 use crate::event::EventResponse;
4571 use crate::test_widgets::StackWidget;
4572 use std::cell::Cell;
4573 use std::rc::Rc;
4574
4575 let leaf_fired = Rc::new(Cell::new(false));
4576 let leaf_flag = leaf_fired.clone();
4577 let preview_fired = Rc::new(Cell::new(false));
4578 let preview_flag = preview_fired.clone();
4579
4580 let mut tree = WidgetTree::new();
4581 let leaf = tree.add(FillWidget::new().focusable().on_key(move |event, _c| {
4582 if matches!(event, WidgetEvent::KeyDown { .. }) {
4585 leaf_flag.set(true);
4586 }
4587 EventResponse::Handled
4588 }));
4589 let mid = tree.add(StackWidget::new().child(leaf));
4590 let _root = tree.add(
4591 StackWidget::new()
4592 .child(mid)
4593 .on_key_preview(move |event, _c| match event {
4594 WidgetEvent::KeyDown {
4595 key: Key::Enter, ..
4596 } => {
4597 preview_flag.set(true);
4598 EventResponse::Handled
4599 }
4600 _ => EventResponse::Ignored,
4601 }),
4602 );
4603
4604 tree.layout(SizeProposal::exact(100.0, 50.0));
4605 tree.focus(leaf);
4606 tree.press_key(Key::Enter, Modifiers::NONE);
4607
4608 assert!(
4609 preview_fired.get(),
4610 "ancestor on_key_preview must fire for KeyDown on a focused descendant"
4611 );
4612 assert!(
4613 !leaf_fired.get(),
4614 "consuming the event in preview must prevent the focused widget's on_key from running"
4615 );
4616 }
4617
4618 #[test]
4619 fn key_preview_falls_through_when_returning_ignored() {
4620 use crate::event::EventResponse;
4623 use crate::test_widgets::StackWidget;
4624 use std::cell::Cell;
4625 use std::rc::Rc;
4626
4627 let leaf_fired = Rc::new(Cell::new(false));
4628 let leaf_flag = leaf_fired.clone();
4629 let preview_fired = Rc::new(Cell::new(false));
4630 let preview_flag = preview_fired.clone();
4631
4632 let mut tree = WidgetTree::new();
4633 let leaf = tree.add(FillWidget::new().focusable().on_key(move |_e, _c| {
4634 leaf_flag.set(true);
4635 EventResponse::Handled
4636 }));
4637 let mid = tree.add(StackWidget::new().child(leaf));
4638 let _root = tree.add(
4639 StackWidget::new()
4640 .child(mid)
4641 .on_key_preview(move |_event, _c| {
4642 preview_flag.set(true);
4643 EventResponse::Ignored
4644 }),
4645 );
4646
4647 tree.layout(SizeProposal::exact(100.0, 50.0));
4648 tree.focus(leaf);
4649 tree.press_key(Key::Enter, Modifiers::NONE);
4650
4651 assert!(preview_fired.get(), "preview must always be invoked");
4652 assert!(
4653 leaf_fired.get(),
4654 "preview returning Ignored must not block the focused widget's on_key"
4655 );
4656 }
4657
4658 #[test]
4659 fn key_preview_excludes_focused_target_itself() {
4660 use crate::event::EventResponse;
4664 use std::cell::Cell;
4665 use std::rc::Rc;
4666
4667 let preview_on_target = Rc::new(Cell::new(false));
4668 let pf = preview_on_target.clone();
4669
4670 let mut tree = WidgetTree::new();
4671 let leaf = tree.add(FillWidget::new().focusable().on_key_preview(move |_e, _c| {
4672 pf.set(true);
4673 EventResponse::Handled
4674 }));
4675 tree.layout(SizeProposal::exact(100.0, 50.0));
4676 tree.focus(leaf);
4677 tree.press_key(Key::Enter, Modifiers::NONE);
4678
4679 assert!(
4680 !preview_on_target.get(),
4681 "the focused widget itself must not see its own on_key_preview"
4682 );
4683 }
4684
4685 #[test]
4686 fn key_preview_root_to_target_order() {
4687 use crate::event::EventResponse;
4691 use crate::test_widgets::StackWidget;
4692 use std::cell::RefCell;
4693 use std::rc::Rc;
4694
4695 let order: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
4696 let outer_log = order.clone();
4697 let inner_log = order.clone();
4698
4699 let mut tree = WidgetTree::new();
4700 let leaf = tree.add(FillWidget::new().focusable());
4701 let inner = tree.add(
4702 StackWidget::new()
4703 .child(leaf)
4704 .on_key_preview(move |event, _c| {
4705 if matches!(event, WidgetEvent::KeyDown { .. }) {
4706 inner_log.borrow_mut().push("inner");
4707 }
4708 EventResponse::Ignored
4709 }),
4710 );
4711 let _outer = tree.add(
4712 StackWidget::new()
4713 .child(inner)
4714 .on_key_preview(move |event, _c| {
4715 if matches!(event, WidgetEvent::KeyDown { .. }) {
4716 outer_log.borrow_mut().push("outer");
4717 }
4718 EventResponse::Ignored
4719 }),
4720 );
4721
4722 tree.layout(SizeProposal::exact(100.0, 50.0));
4723 tree.focus(leaf);
4724 tree.dispatch_event(WidgetEvent::KeyDown {
4725 key: Key::Enter,
4726 modifiers: Modifiers::NONE,
4727 text: None,
4728 });
4729
4730 assert_eq!(
4731 *order.borrow(),
4732 vec!["outer", "inner"],
4733 "preview must walk root → parent-of-target"
4734 );
4735 }
4736
4737 #[test]
4738 fn access_action_routes_to_cursored_target_not_focus() {
4739 use crate::signal::Signal;
4743 let a_fired = Signal::new(false);
4744 let b_fired = Signal::new(false);
4745 let a_cb = a_fired.clone();
4746 let b_cb = b_fired.clone();
4747
4748 let mut tree = WidgetTree::new();
4749 let a = tree.add(
4750 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| a_cb.set(true)),
4751 );
4752 let b = tree.add(
4753 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| b_cb.set(true)),
4754 );
4755 tree.layout(SizeProposal::exact(200.0, 100.0));
4756
4757 tree.focus(a);
4758 tree.dispatch_event(WidgetEvent::AccessAction {
4759 action: accesskit::Action::Click,
4760 target: Some(b),
4761 target_node: crate::accessibility::widget_id_to_node_id(b),
4762 data: None,
4763 });
4764
4765 assert!(b_fired.get(), "the cursored target must receive the action");
4766 assert!(
4767 !a_fired.get(),
4768 "the keyboard-focused widget must NOT receive an action targeting another node"
4769 );
4770 }
4771
4772 #[test]
4773 fn access_action_without_target_is_dropped_not_redirected_to_focus() {
4774 use crate::signal::Signal;
4777 let fired = Signal::new(false);
4778 let cb = fired.clone();
4779
4780 let mut tree = WidgetTree::new();
4781 let widget = tree.add(
4782 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| cb.set(true)),
4783 );
4784 tree.layout(SizeProposal::exact(200.0, 100.0));
4785
4786 tree.focus(widget);
4787 tree.dispatch_event(WidgetEvent::AccessAction {
4788 action: accesskit::Action::Click,
4789 target: None,
4790 target_node: crate::accessibility::root_node_id(),
4791 data: None,
4792 });
4793
4794 assert!(
4795 !fired.get(),
4796 "a target-less action must not be redirected to the focused widget"
4797 );
4798 }
4799
4800 #[test]
4807 fn shortcut_fires_matching_action_on_source_widget() {
4808 use crate::action::Action;
4809 use crate::shortcut::{KeyStroke, Shortcut};
4810 use std::cell::Cell;
4811 use std::rc::Rc;
4812
4813 let fired = Rc::new(Cell::new(false));
4814 let fired_flag = fired.clone();
4815
4816 let mut tree = WidgetTree::new();
4817 let widget = tree.add(FillWidget::new().focusable());
4818 tree.push_action(
4819 widget,
4820 Action::new("app.save").on_invoke(move |_intent, _ctx| {
4821 fired_flag.set(true);
4822 }),
4823 );
4824 tree.shortcut_registry_mut().register(
4825 Shortcut::new("app.save")
4826 .primary(KeyStroke::command(Key::S))
4827 .build(),
4828 );
4829
4830 tree.layout(SizeProposal::exact(100.0, 50.0));
4831 tree.focus(widget);
4832
4833 tree.press_key(Key::S, Modifiers::COMMAND);
4834 assert!(fired.get(), "matching action must fire on KeyDown");
4835 }
4836
4837 #[test]
4838 fn global_shortcut_fires_without_focused_widget() {
4839 use crate::action::Action;
4840 use crate::shortcut::{KeyStroke, Shortcut};
4841 use std::cell::Cell;
4842 use std::rc::Rc;
4843
4844 let fired = Rc::new(Cell::new(false));
4848 let fired_flag = fired.clone();
4849
4850 let mut tree = WidgetTree::new();
4851 let root = tree.add(FillWidget::new());
4852 tree.push_action(
4853 root,
4854 Action::new("app.save").on_invoke(move |_intent, _ctx| {
4855 fired_flag.set(true);
4856 }),
4857 );
4858 tree.shortcut_registry_mut().register(
4859 Shortcut::new("app.save")
4860 .primary(KeyStroke::command(Key::S))
4861 .build(),
4862 );
4863
4864 tree.layout(SizeProposal::exact(100.0, 50.0));
4865 tree.press_key(Key::S, Modifiers::COMMAND);
4868 assert!(
4869 fired.get(),
4870 "global shortcut must fire without a focused widget"
4871 );
4872 }
4873
4874 #[test]
4875 fn global_shortcut_fires_after_focused_widget_destroyed() {
4876 use crate::action::Action;
4877 use crate::shortcut::{KeyStroke, Shortcut};
4878 use std::cell::Cell;
4879 use std::rc::Rc;
4880
4881 let fired = Rc::new(Cell::new(false));
4886 let fired_flag = fired.clone();
4887
4888 let mut tree = WidgetTree::new();
4889 let root = tree.add(FillWidget::new());
4890 let focusable = tree.add_child(root, FillWidget::new().focusable());
4891 tree.push_action(
4892 root,
4893 Action::new("app.save").on_invoke(move |_intent, _ctx| {
4894 fired_flag.set(true);
4895 }),
4896 );
4897 tree.shortcut_registry_mut().register(
4898 Shortcut::new("app.save")
4899 .primary(KeyStroke::command(Key::S))
4900 .build(),
4901 );
4902
4903 tree.layout(SizeProposal::exact(100.0, 50.0));
4904 tree.focus(focusable);
4905 assert_eq!(tree.focused(), Some(focusable));
4906
4907 tree.destroy_subtree(focusable);
4910 assert_eq!(tree.focused(), None, "focus must clear when destroyed");
4911
4912 tree.press_key(Key::S, Modifiers::COMMAND);
4913 assert!(
4914 fired.get(),
4915 "global shortcut must still fire after the focused widget is destroyed"
4916 );
4917 }
4918
4919 #[test]
4920 fn scoped_shortcut_matches_only_when_focus_in_scope() {
4921 use crate::action::Action;
4922 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
4923 use std::cell::Cell;
4924 use std::rc::Rc;
4925
4926 let fired = Rc::new(Cell::new(0));
4927 let fired_flag = fired.clone();
4928
4929 let mut tree = WidgetTree::new();
4930 let scope_root = tree.add(FillWidget::new().focusable());
4931 let inside = tree.add_child(scope_root, FillWidget::new().focusable());
4932 let outside = tree.add(FillWidget::new().focusable());
4933
4934 tree.push_action(
4935 scope_root,
4936 Action::new("editor.find").on_invoke(move |_i, _c| {
4937 fired_flag.set(fired_flag.get() + 1);
4938 }),
4939 );
4940 tree.shortcut_registry_mut().register(
4941 Shortcut::new("editor.find")
4942 .primary(KeyStroke::command(Key::F))
4943 .scope(ShortcutScope::Scoped(scope_root))
4944 .build(),
4945 );
4946
4947 tree.layout(SizeProposal::exact(200.0, 100.0));
4948
4949 tree.focus(outside);
4951 tree.press_key(Key::F, Modifiers::COMMAND);
4952 assert_eq!(
4953 fired.get(),
4954 0,
4955 "scoped shortcut must not fire outside scope"
4956 );
4957
4958 tree.focus(inside);
4960 tree.press_key(Key::F, Modifiers::COMMAND);
4961 assert_eq!(
4962 fired.get(),
4963 1,
4964 "scoped shortcut must fire when focus in scope"
4965 );
4966 }
4967
4968 #[test]
4969 fn same_chord_scoped_first_falls_back_to_global_when_focus_outside() {
4970 use crate::action::Action;
4976 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
4977 use std::cell::Cell;
4978 use std::rc::Rc;
4979
4980 let scoped_fired = Rc::new(Cell::new(0));
4981 let global_fired = Rc::new(Cell::new(0));
4982 let sf = scoped_fired.clone();
4983 let gf = global_fired.clone();
4984
4985 let mut tree = WidgetTree::new();
4986 let root = tree.add(FillWidget::new());
4987 let editor = tree.add_child(root, FillWidget::new().focusable());
4988 let _editor_inner = tree.add_child(editor, FillWidget::new().focusable());
4989 let sidebar = tree.add_child(root, FillWidget::new().focusable());
4990
4991 tree.push_action(
4992 editor,
4993 Action::new("editor.saveBlock").on_invoke(move |_i, _c| sf.set(sf.get() + 1)),
4994 );
4995 tree.push_action(
4996 root,
4997 Action::new("zzz.global.save").on_invoke(move |_i, _c| gf.set(gf.get() + 1)),
4998 );
4999 tree.shortcut_registry_mut().register(
5000 Shortcut::new("editor.saveBlock")
5001 .primary(KeyStroke::command(Key::S))
5002 .scope(ShortcutScope::Scoped(editor))
5003 .build(),
5004 );
5005 tree.shortcut_registry_mut().register(
5006 Shortcut::new("zzz.global.save")
5007 .primary(KeyStroke::command(Key::S))
5008 .build(),
5009 );
5010
5011 tree.layout(SizeProposal::exact(200.0, 100.0));
5012 tree.focus(sidebar);
5013 tree.press_key(Key::S, Modifiers::COMMAND);
5014
5015 assert_eq!(global_fired.get(), 1, "applicable global must fire");
5016 assert_eq!(
5017 scoped_fired.get(),
5018 0,
5019 "inapplicable scoped binding must not eat the chord"
5020 );
5021 }
5022
5023 #[test]
5024 fn same_chord_global_first_yields_to_scoped_when_focus_inside() {
5025 use crate::action::Action;
5030 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
5031 use std::cell::Cell;
5032 use std::rc::Rc;
5033
5034 let scoped_fired = Rc::new(Cell::new(0));
5035 let global_fired = Rc::new(Cell::new(0));
5036 let sf = scoped_fired.clone();
5037 let gf = global_fired.clone();
5038
5039 let mut tree = WidgetTree::new();
5040 let root = tree.add(FillWidget::new());
5041 let editor = tree.add_child(root, FillWidget::new().focusable());
5042 let editor_inner = tree.add_child(editor, FillWidget::new().focusable());
5043 let sidebar = tree.add_child(root, FillWidget::new().focusable());
5044
5045 tree.push_action(
5046 editor,
5047 Action::new("editor.saveBlock").on_invoke(move |_i, _c| sf.set(sf.get() + 1)),
5048 );
5049 tree.push_action(
5050 root,
5051 Action::new("app.save").on_invoke(move |_i, _c| gf.set(gf.get() + 1)),
5052 );
5053 tree.shortcut_registry_mut().register(
5054 Shortcut::new("app.save")
5055 .primary(KeyStroke::command(Key::S))
5056 .build(),
5057 );
5058 tree.shortcut_registry_mut().register(
5059 Shortcut::new("editor.saveBlock")
5060 .primary(KeyStroke::command(Key::S))
5061 .scope(ShortcutScope::Scoped(editor))
5062 .build(),
5063 );
5064
5065 tree.layout(SizeProposal::exact(200.0, 100.0));
5066
5067 tree.focus(editor_inner);
5069 tree.press_key(Key::S, Modifiers::COMMAND);
5070 assert_eq!(
5071 scoped_fired.get(),
5072 1,
5073 "in-focus scoped must win over global"
5074 );
5075 assert_eq!(
5076 global_fired.get(),
5077 0,
5078 "global must yield to the scoped binding"
5079 );
5080
5081 tree.focus(sidebar);
5083 tree.press_key(Key::S, Modifiers::COMMAND);
5084 assert_eq!(scoped_fired.get(), 1, "scoped stays put outside its scope");
5085 assert_eq!(
5086 global_fired.get(),
5087 1,
5088 "global fires when focus leaves the scope"
5089 );
5090 }
5091
5092 #[test]
5093 fn propagated_action_lets_ancestor_handle() {
5094 use crate::action::Action;
5095 use crate::intent::IntentResponse;
5096 use crate::shortcut::{KeyStroke, Shortcut};
5097 use std::cell::Cell;
5098 use std::rc::Rc;
5099
5100 let inner_seen = Rc::new(Cell::new(false));
5101 let outer_seen = Rc::new(Cell::new(false));
5102 let inner_flag = inner_seen.clone();
5103 let outer_flag = outer_seen.clone();
5104
5105 let mut tree = WidgetTree::new();
5106 let outer = tree.add(FillWidget::new().focusable());
5107 let inner = tree.add_child(outer, FillWidget::new().focusable());
5108
5109 tree.push_action(
5111 inner,
5112 Action::new("app.save").on_invoke_with_response(move |_i, _c| {
5113 inner_flag.set(true);
5114 IntentResponse::Propagated
5115 }),
5116 );
5117 tree.push_action(
5118 outer,
5119 Action::new("app.save").on_invoke(move |_i, _c| {
5120 outer_flag.set(true);
5121 }),
5122 );
5123 tree.shortcut_registry_mut().register(
5124 Shortcut::new("app.save")
5125 .primary(KeyStroke::command(Key::S))
5126 .build(),
5127 );
5128
5129 tree.layout(SizeProposal::exact(100.0, 50.0));
5130 tree.focus(inner);
5131
5132 tree.press_key(Key::S, Modifiers::COMMAND);
5133 assert!(inner_seen.get(), "inner action observed the intent");
5134 assert!(outer_seen.get(), "outer action reached after Propagated");
5135 }
5136
5137 #[test]
5138 fn handled_action_stops_propagation() {
5139 use crate::action::Action;
5140 use crate::shortcut::{KeyStroke, Shortcut};
5141 use std::cell::Cell;
5142 use std::rc::Rc;
5143
5144 let inner_seen = Rc::new(Cell::new(false));
5145 let outer_seen = Rc::new(Cell::new(false));
5146 let inner_flag = inner_seen.clone();
5147 let outer_flag = outer_seen.clone();
5148
5149 let mut tree = WidgetTree::new();
5150 let outer = tree.add(FillWidget::new().focusable());
5151 let inner = tree.add_child(outer, FillWidget::new().focusable());
5152
5153 tree.push_action(
5154 inner,
5155 Action::new("app.save").on_invoke(move |_i, _c| {
5156 inner_flag.set(true);
5157 }),
5158 );
5159 tree.push_action(
5160 outer,
5161 Action::new("app.save").on_invoke(move |_i, _c| {
5162 outer_flag.set(true);
5163 }),
5164 );
5165 tree.shortcut_registry_mut().register(
5166 Shortcut::new("app.save")
5167 .primary(KeyStroke::command(Key::S))
5168 .build(),
5169 );
5170
5171 tree.layout(SizeProposal::exact(100.0, 50.0));
5172 tree.focus(inner);
5173
5174 tree.press_key(Key::S, Modifiers::COMMAND);
5175 assert!(inner_seen.get());
5176 assert!(!outer_seen.get(), "Handled at inner must stop propagation");
5177 }
5178
5179 #[test]
5180 fn disabled_action_propagates_by_default() {
5181 use crate::action::Action;
5182 use crate::shortcut::{KeyStroke, Shortcut};
5183 use crate::signal::Signal;
5184 use std::cell::Cell;
5185 use std::rc::Rc;
5186
5187 let inner_seen = Rc::new(Cell::new(false));
5188 let outer_seen = Rc::new(Cell::new(false));
5189 let inner_flag = inner_seen.clone();
5190 let outer_flag = outer_seen.clone();
5191
5192 let mut tree = WidgetTree::new();
5193 let outer = tree.add(FillWidget::new().focusable());
5194 let inner = tree.add_child(outer, FillWidget::new().focusable());
5195
5196 let enabled = Signal::new(false);
5197 tree.push_action(
5198 inner,
5199 Action::new("app.save")
5200 .enabled_when(enabled.clone())
5201 .on_invoke(move |_i, _c| {
5202 inner_flag.set(true);
5203 }),
5204 );
5205 tree.push_action(
5206 outer,
5207 Action::new("app.save").on_invoke(move |_i, _c| {
5208 outer_flag.set(true);
5209 }),
5210 );
5211 tree.shortcut_registry_mut().register(
5212 Shortcut::new("app.save")
5213 .primary(KeyStroke::command(Key::S))
5214 .build(),
5215 );
5216
5217 tree.layout(SizeProposal::exact(100.0, 50.0));
5218 tree.focus(inner);
5219
5220 tree.press_key(Key::S, Modifiers::COMMAND);
5221 assert!(!inner_seen.get(), "disabled inner must not run");
5222 assert!(
5223 outer_seen.get(),
5224 "intent must propagate past disabled inner"
5225 );
5226 }
5227
5228 #[test]
5229 fn disabled_action_with_non_propagating_shortcut_consumes() {
5230 use crate::action::Action;
5231 use crate::shortcut::{KeyStroke, Shortcut};
5232 use crate::signal::Signal;
5233 use std::cell::Cell;
5234 use std::rc::Rc;
5235
5236 let inner_seen = Rc::new(Cell::new(false));
5237 let outer_seen = Rc::new(Cell::new(false));
5238 let inner_flag = inner_seen.clone();
5239 let outer_flag = outer_seen.clone();
5240
5241 let mut tree = WidgetTree::new();
5242 let outer = tree.add(FillWidget::new().focusable());
5243 let inner = tree.add_child(outer, FillWidget::new().focusable());
5244
5245 let enabled = Signal::new(false);
5246 tree.push_action(
5247 inner,
5248 Action::new("app.save")
5249 .enabled_when(enabled.clone())
5250 .on_invoke(move |_i, _c| {
5251 inner_flag.set(true);
5252 }),
5253 );
5254 tree.push_action(
5255 outer,
5256 Action::new("app.save").on_invoke(move |_i, _c| {
5257 outer_flag.set(true);
5258 }),
5259 );
5260 tree.shortcut_registry_mut().register(
5261 Shortcut::new("app.save")
5262 .primary(KeyStroke::command(Key::S))
5263 .propagate_when_disabled(false)
5264 .build(),
5265 );
5266
5267 tree.layout(SizeProposal::exact(100.0, 50.0));
5268 tree.focus(inner);
5269
5270 tree.press_key(Key::S, Modifiers::COMMAND);
5271 assert!(!inner_seen.get(), "disabled inner still does not run");
5272 assert!(
5273 !outer_seen.get(),
5274 "intent must NOT propagate when shortcut disallows it"
5275 );
5276 }
5277
5278 #[test]
5279 fn send_intent_from_handler_reaches_ancestor_action() {
5280 use crate::action::Action;
5281 use crate::intent::Intent;
5282 use std::cell::Cell;
5283 use std::rc::Rc;
5284
5285 let save_seen = Rc::new(Cell::new(false));
5286 let save_flag = save_seen.clone();
5287
5288 let mut tree = WidgetTree::new();
5289 let root = tree.add(FillWidget::new());
5290 let button = tree.add_child(
5291 root,
5292 FillWidget::new().on_tap(|_pos, ctx| {
5293 ctx.send_intent(Intent::new("app.save"));
5294 }),
5295 );
5296 tree.push_action(
5297 root,
5298 Action::new("app.save").on_invoke(move |_i, _c| {
5299 save_flag.set(true);
5300 }),
5301 );
5302
5303 tree.layout(SizeProposal::exact(100.0, 50.0));
5304 tree.click(button);
5305 assert!(
5306 save_seen.get(),
5307 "ctx.send_intent must reach ancestor action"
5308 );
5309 }
5310
5311 #[test]
5312 fn widget_type_histogram_counts_distinct_types() {
5313 let mut tree = WidgetTree::new();
5318 let _ = tree.add(FillWidget::new());
5319 let _ = tree.add(FillWidget::new());
5320 let _ = tree.add(FillWidget::new());
5321 tree.layout(SizeProposal::exact(100.0, 100.0));
5322 let histogram = tree.widget_type_histogram();
5323 let total: u32 = histogram.values().sum();
5324 assert!(
5325 total >= 3,
5326 "expected at least 3 active widgets, got {total}: {histogram:?}"
5327 );
5328 let fillwidget_entries: u32 = histogram
5329 .iter()
5330 .filter(|(k, _)| k.contains("FillWidget"))
5331 .map(|(_, v)| *v)
5332 .sum();
5333 assert!(
5334 fillwidget_entries >= 3,
5335 "expected ≥3 FillWidget instances; histogram = {histogram:?}"
5336 );
5337 assert_eq!(tree.active_widget_count() as u32, total);
5338 }
5339
5340 #[test]
5341 fn intent_source_tagged_handler_for_tap_activation() {
5342 use crate::action::Action;
5346 use crate::intent::Intent;
5347 use crate::telemetry::IntentSource;
5348 use std::cell::Cell;
5349 use std::rc::Rc;
5350 let captured = Rc::new(Cell::new(IntentSource::Unknown));
5351 let captured_for_action = captured.clone();
5352
5353 let mut tree = WidgetTree::new();
5354 let root = tree.add(FillWidget::new());
5355 let button = tree.add_child(
5356 root,
5357 FillWidget::new().on_tap(|_pos, ctx| {
5358 ctx.send_intent(Intent::new("app.save"));
5359 }),
5360 );
5361 tree.push_action(
5362 root,
5363 Action::new("app.save").on_invoke(move |intent, _c| {
5364 captured_for_action.set(intent.source);
5365 }),
5366 );
5367
5368 tree.layout(SizeProposal::exact(100.0, 50.0));
5369 tree.click(button);
5370 assert_eq!(
5371 captured.get(),
5372 IntentSource::Handler,
5373 "tap-driven intent must tag IntentSource::Handler"
5374 );
5375 }
5376
5377 #[test]
5378 fn intent_source_programmatic_when_no_handler_active() {
5379 use crate::intent::Intent;
5380 use crate::telemetry::IntentSource;
5381 let intent = Intent::new("app.demo");
5382 assert_eq!(intent.source, IntentSource::Programmatic);
5383
5384 let mut ctx = EventContext::new();
5386 ctx.send_intent(Intent::new("app.demo"));
5387 let queued = ctx.pending_intents.first().expect("intent queued");
5388 assert_eq!(queued.source, IntentSource::Programmatic);
5389 }
5390
5391 #[test]
5392 fn with_intent_source_overrides_for_managed_widgets() {
5393 use crate::intent::Intent;
5394 use crate::telemetry::IntentSource;
5395 let mut ctx = EventContext::new();
5396 ctx.with_intent_source(IntentSource::Menu, |ctx| {
5397 ctx.send_intent(Intent::new("app.demo"));
5398 });
5399 let queued = ctx.pending_intents.first().expect("intent queued");
5400 assert_eq!(
5401 queued.source,
5402 IntentSource::Menu,
5403 "with_intent_source(Menu) must tag the dispatched intent"
5404 );
5405
5406 ctx.send_intent(Intent::new("app.next"));
5410 let next = ctx.pending_intents.last().expect("second intent");
5411 assert_eq!(next.source, IntentSource::Programmatic);
5412 }
5413
5414 #[test]
5415 fn disabled_shortcut_falls_through_to_focused_widget() {
5416 use crate::action::Action;
5417 use crate::shortcut::{KeyStroke, Shortcut};
5418 use crate::signal::Signal;
5419 use std::cell::Cell;
5420 use std::rc::Rc;
5421
5422 let action_fired = Rc::new(Cell::new(false));
5423 let on_key_fired = Rc::new(Cell::new(false));
5424 let af = action_fired.clone();
5425 let kf = on_key_fired.clone();
5426
5427 let enabled = Signal::new(false);
5428
5429 let mut tree = WidgetTree::new();
5430 let widget = tree.add(FillWidget::new().focusable().on_key(move |event, _ctx| {
5431 if matches!(
5432 event,
5433 WidgetEvent::KeyDown {
5434 key: Key::S,
5435 modifiers,
5436 ..
5437 } if modifiers.command()
5438 ) {
5439 kf.set(true);
5440 return EventResponse::Handled;
5441 }
5442 EventResponse::Ignored
5443 }));
5444 tree.push_action(
5445 widget,
5446 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
5447 );
5448 tree.shortcut_registry_mut().register(
5449 Shortcut::new("app.save")
5450 .primary(KeyStroke::command(Key::S))
5451 .enabled_when(enabled.clone())
5452 .build(),
5453 );
5454
5455 tree.layout(SizeProposal::exact(100.0, 50.0));
5456 tree.focus(widget);
5457
5458 tree.press_key(Key::S, Modifiers::COMMAND);
5460 assert!(
5461 !action_fired.get(),
5462 "disabled shortcut must not invoke its action"
5463 );
5464 assert!(
5465 on_key_fired.get(),
5466 "disabled shortcut must let KeyDown reach the focused widget"
5467 );
5468
5469 on_key_fired.set(false);
5471 enabled.set(true);
5472 tree.press_key(Key::S, Modifiers::COMMAND);
5473 assert!(action_fired.get(), "re-enabled shortcut must dispatch");
5474 assert!(
5475 !on_key_fired.get(),
5476 "enabled shortcut must consume the KeyDown"
5477 );
5478 }
5479
5480 #[test]
5481 fn keyboard_capture_bypasses_shortcut() {
5482 use crate::action::Action;
5483 use crate::shortcut::{KeyStroke, Shortcut};
5484 use std::cell::Cell;
5485 use std::rc::Rc;
5486
5487 fn run(capture: bool) -> (bool, bool) {
5493 let action_fired = Rc::new(Cell::new(false));
5494 let on_key_fired = Rc::new(Cell::new(false));
5495 let af = action_fired.clone();
5496 let kf = on_key_fired.clone();
5497
5498 let mut tree = WidgetTree::new();
5499 let widget = tree.add(
5500 FillWidget::new()
5501 .focusable()
5502 .keyboard_capture(capture)
5503 .on_key(move |event, _ctx| {
5504 if matches!(
5505 event,
5506 WidgetEvent::KeyDown { key: Key::S, modifiers, .. } if modifiers.command()
5507 ) {
5508 kf.set(true);
5509 return EventResponse::Handled;
5510 }
5511 EventResponse::Ignored
5512 }),
5513 );
5514 tree.push_action(
5515 widget,
5516 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
5517 );
5518 tree.shortcut_registry_mut().register(
5519 Shortcut::new("app.save")
5520 .primary(KeyStroke::command(Key::S))
5521 .build(),
5522 );
5523
5524 tree.layout(SizeProposal::exact(100.0, 50.0));
5525 tree.focus(widget);
5526 tree.press_key(Key::S, Modifiers::COMMAND);
5527 (action_fired.get(), on_key_fired.get())
5528 }
5529
5530 let (action, on_key) = run(true);
5532 assert!(
5533 !action,
5534 "keyboard_capture must suppress the shortcut action"
5535 );
5536 assert!(on_key, "keyboard_capture must deliver the raw KeyDown");
5537
5538 let (action, on_key) = run(false);
5540 assert!(action, "without capture the shortcut must fire");
5541 assert!(!on_key, "without capture the widget must not see the key");
5542 }
5543
5544 fn key_recorder(
5546 tree: &mut WidgetTree,
5547 ) -> (WidgetId, std::rc::Rc<std::cell::RefCell<Vec<Key>>>) {
5548 let seen = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
5549 let log = seen.clone();
5550 let widget = tree.add(FillWidget::new().focusable().on_key(move |event, _ctx| {
5551 if let WidgetEvent::KeyDown { key, .. } = event {
5552 log.borrow_mut().push(*key);
5553 }
5554 EventResponse::Ignored
5555 }));
5556 tree.layout(SizeProposal::exact(100.0, 50.0));
5557 tree.focus(widget);
5558 (widget, seen)
5559 }
5560
5561 #[test]
5562 fn a_shortcut_no_action_takes_leaves_its_key_to_the_focused_widget() {
5563 use crate::action::Action;
5564 use crate::intent::IntentResponse;
5565 use crate::shortcut::{KeyStroke, Shortcut};
5566
5567 let mut tree = WidgetTree::new();
5568 let (widget, seen) = key_recorder(&mut tree);
5569 tree.shortcut_registry_mut().register(
5570 Shortcut::new("app.save")
5571 .primary(KeyStroke::command(Key::S))
5572 .build(),
5573 );
5574
5575 tree.press_key(Key::S, Modifiers::COMMAND);
5577 assert_eq!(
5578 *seen.borrow(),
5579 vec![Key::S],
5580 "an unhandled intent must not eat the key"
5581 );
5582
5583 tree.push_action(
5585 widget,
5586 Action::new("app.save").on_invoke_with_response(|_i, _c| IntentResponse::Propagated),
5587 );
5588 tree.press_key(Key::S, Modifiers::COMMAND);
5589 assert_eq!(
5590 seen.borrow().len(),
5591 2,
5592 "a propagated intent must leave the key too"
5593 );
5594
5595 let mut tree = WidgetTree::new();
5598 let (widget, seen) = key_recorder(&mut tree);
5599 tree.shortcut_registry_mut().register(
5600 Shortcut::new("app.save")
5601 .primary(KeyStroke::command(Key::S))
5602 .build(),
5603 );
5604 tree.push_action(widget, Action::new("app.save").on_invoke(|_i, _c| {}));
5605 tree.press_key(Key::S, Modifiers::COMMAND);
5606 assert!(
5607 seen.borrow().is_empty(),
5608 "a handled shortcut consumes its key"
5609 );
5610 }
5611
5612 #[test]
5613 fn standard_edit_chords_follow_a_rebind() {
5614 use crate::action::Action;
5615 use crate::shortcut::{KeyStroke, StandardEdit};
5616 use std::cell::Cell;
5617 use std::rc::Rc;
5618
5619 let mut tree = WidgetTree::new();
5620 let (widget, seen) = key_recorder(&mut tree);
5621 let copies = Rc::new(Cell::new(0));
5622 let count = copies.clone();
5623 tree.push_action(
5624 widget,
5625 Action::new(StandardEdit::Copy.id())
5626 .on_invoke(move |_i, _c| count.set(count.get() + 1)),
5627 );
5628
5629 tree.press_key(Key::C, Modifiers::COMMAND);
5631 assert_eq!(copies.get(), 1, "Ctrl+C reaches the edit.copy action");
5632 assert!(seen.borrow().is_empty());
5633
5634 let elsewhere = KeyStroke::new(Key::Character('à'), Modifiers::CTRL);
5637 tree.shortcut_registry_mut()
5638 .rebind_primary(StandardEdit::Copy.id(), Some(elsewhere));
5639 tree.press_key(Key::C, Modifiers::COMMAND);
5640 assert_eq!(copies.get(), 1, "the replaced chord must not copy");
5641 assert_eq!(*seen.borrow(), vec![Key::C]);
5642 tree.press_key(Key::Character('à'), Modifiers::CTRL);
5643 assert_eq!(copies.get(), 2, "the new chord copies");
5644 }
5645
5646 #[test]
5647 fn ctrl_tab_always_escapes_a_keyboard_capture_surface() {
5648 use std::cell::Cell;
5649 use std::rc::Rc;
5650
5651 let saw_key = Rc::new(Cell::new(false));
5657 let sk = saw_key.clone();
5658
5659 let mut tree = WidgetTree::new();
5660 let capture = tree.add(
5661 FillWidget::new()
5662 .focusable()
5663 .keyboard_capture(true)
5664 .on_key(move |_event, _ctx| {
5666 sk.set(true);
5667 EventResponse::Handled
5668 }),
5669 );
5670 let neighbour = tree.add(FillWidget::new().focusable());
5671 tree.layout(SizeProposal::exact(100.0, 50.0));
5672
5673 tree.focus(capture);
5676 tree.press_key(Key::Tab, Modifiers::NONE);
5677 assert!(saw_key.get(), "plain Tab must reach the capture surface");
5678 assert_eq!(
5679 tree.focused(),
5680 Some(capture),
5681 "plain Tab must not move focus off a capture surface"
5682 );
5683
5684 saw_key.set(false);
5686 tree.press_key(Key::Tab, Modifiers::CTRL);
5687 assert!(
5688 !saw_key.get(),
5689 "Ctrl+Tab is reserved and must not reach the capture surface"
5690 );
5691 assert_eq!(
5692 tree.focused(),
5693 Some(neighbour),
5694 "Ctrl+Tab must move focus out of a capture surface"
5695 );
5696
5697 tree.focus(capture);
5699 tree.press_key(Key::Tab, Modifiers::CTRL | Modifiers::SHIFT);
5700 assert_eq!(
5701 tree.focused(),
5702 Some(neighbour),
5703 "Ctrl+Shift+Tab must move focus out of a capture surface"
5704 );
5705 }
5706
5707 #[test]
5708 fn scope_mismatch_does_not_invoke_on_activate() {
5709 use crate::intent::Intent;
5710 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
5711 use std::cell::Cell;
5712 use std::rc::Rc;
5713
5714 let activated = Rc::new(Cell::new(false));
5720 let activated_flag = activated.clone();
5721
5722 let mut tree = WidgetTree::new();
5723 let scope_root = tree.add(FillWidget::new().focusable());
5724 let outside = tree.add(FillWidget::new().focusable());
5725
5726 tree.shortcut_registry_mut().register(
5727 Shortcut::new("editor.find")
5728 .primary(KeyStroke::command(Key::F))
5729 .scope(ShortcutScope::Scoped(scope_root))
5730 .on_activate(move |_ks, _ctx| {
5731 activated_flag.set(true);
5732 Intent::new("editor.find")
5733 })
5734 .build(),
5735 );
5736
5737 tree.layout(SizeProposal::exact(200.0, 100.0));
5738 tree.focus(outside);
5739
5740 tree.press_key(Key::F, Modifiers::COMMAND);
5741 assert!(
5742 !activated.get(),
5743 "on_activate must not run when focus is outside the shortcut's scope"
5744 );
5745 }
5746
5747 #[test]
5748 fn key_capture_runs_callback_and_bypasses_registry() {
5749 use crate::action::Action;
5750 use crate::shortcut::{KeyStroke, Shortcut};
5751 use std::cell::Cell;
5752 use std::rc::Rc;
5753
5754 let action_fired = Rc::new(Cell::new(false));
5755 let af = action_fired.clone();
5756 let captured = Rc::new(Cell::new(None));
5757 let cf = captured.clone();
5758
5759 let mut tree = WidgetTree::new();
5760 let widget = tree.add(FillWidget::new().focusable());
5761 tree.push_action(
5762 widget,
5763 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
5764 );
5765 tree.shortcut_registry_mut().register(
5766 Shortcut::new("app.save")
5767 .primary(KeyStroke::command(Key::S))
5768 .build(),
5769 );
5770
5771 tree.layout(SizeProposal::exact(100.0, 50.0));
5772 tree.focus(widget);
5773
5774 let handle = tree.begin_key_capture(move |ks, _reg, _ctx| cf.set(Some(ks)));
5775 assert!(tree.is_capturing_keys());
5776
5777 tree.press_key(Key::S, Modifiers::COMMAND);
5778 assert_eq!(
5779 captured.get(),
5780 Some(KeyStroke::command(Key::S)),
5781 "capture callback must receive the chord"
5782 );
5783 assert!(
5784 !action_fired.get(),
5785 "shortcut action must not fire while capture is armed"
5786 );
5787 assert!(
5788 !tree.is_capturing_keys(),
5789 "capture is one-shot; next KeyDown flows normally"
5790 );
5791 drop(handle);
5792 }
5793
5794 #[test]
5795 fn key_capture_can_rebind_through_registry() {
5796 use crate::shortcut::{KeyStroke, Shortcut};
5797
5798 let mut tree = WidgetTree::new();
5799 let widget = tree.add(FillWidget::new().focusable());
5800 tree.shortcut_registry_mut().register(
5801 Shortcut::new("app.save")
5802 .primary(KeyStroke::command(Key::S))
5803 .build(),
5804 );
5805
5806 tree.layout(SizeProposal::exact(100.0, 50.0));
5807 tree.focus(widget);
5808
5809 let _h = tree.begin_key_capture(|ks, reg, _ctx| {
5811 reg.rebind_primary("app.save", Some(ks));
5812 });
5813
5814 tree.press_key(Key::B, Modifiers::COMMAND | Modifiers::SHIFT);
5815 assert_eq!(
5816 tree.shortcut_registry()
5817 .effective("app.save")
5818 .unwrap()
5819 .primary,
5820 Some(KeyStroke::command_shift(Key::B))
5821 );
5822 }
5823
5824 #[test]
5825 fn dropping_capture_handle_cancels_capture() {
5826 use crate::shortcut::{KeyStroke, Shortcut};
5827 use std::cell::Cell;
5828 use std::rc::Rc;
5829
5830 let action_fired = Rc::new(Cell::new(false));
5831 let af = action_fired.clone();
5832 let capture_fired = Rc::new(Cell::new(false));
5833 let cf = capture_fired.clone();
5834
5835 let mut tree = WidgetTree::new();
5836 let widget = tree.add(FillWidget::new().focusable());
5837 tree.push_action(
5838 widget,
5839 crate::action::Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
5840 );
5841 tree.shortcut_registry_mut().register(
5842 Shortcut::new("app.save")
5843 .primary(KeyStroke::command(Key::S))
5844 .build(),
5845 );
5846 tree.layout(SizeProposal::exact(100.0, 50.0));
5847 tree.focus(widget);
5848
5849 {
5853 let _h = tree.begin_key_capture(move |_ks, _reg, _ctx| cf.set(true));
5854 assert!(tree.is_capturing_keys());
5855 }
5857 assert!(
5858 !tree.is_capturing_keys(),
5859 "dropping the handle must cancel the capture"
5860 );
5861
5862 tree.press_key(Key::S, Modifiers::COMMAND);
5863 assert!(!capture_fired.get(), "cancelled capture must not fire");
5864 assert!(
5865 action_fired.get(),
5866 "shortcut action runs after capture was cancelled"
5867 );
5868 }
5869
5870 #[test]
5871 fn second_begin_key_capture_does_not_racecancel_first() {
5872 use std::cell::Cell;
5873 use std::rc::Rc;
5874
5875 let first = Rc::new(Cell::new(false));
5876 let second = Rc::new(Cell::new(false));
5877 let f = first.clone();
5878 let s = second.clone();
5879
5880 let mut tree = WidgetTree::new();
5881 let widget = tree.add(FillWidget::new().focusable());
5882 tree.layout(SizeProposal::exact(100.0, 50.0));
5883 tree.focus(widget);
5884
5885 let h1 = tree.begin_key_capture(move |_ks, _reg, _ctx| f.set(true));
5891 let _h2 = tree.begin_key_capture(move |_ks, _reg, _ctx| s.set(true));
5892 drop(h1);
5893
5894 assert!(
5895 tree.is_capturing_keys(),
5896 "dropping the older handle must not cancel the active capture"
5897 );
5898 tree.press_key(Key::K, Modifiers::COMMAND);
5899 assert!(!first.get());
5900 assert!(second.get(), "newest capture wins");
5901 }
5902
5903 #[test]
5904 fn capture_callback_can_send_intent() {
5905 use crate::action::Action;
5906 use crate::intent::Intent;
5907
5908 use std::cell::Cell;
5909 use std::rc::Rc;
5910
5911 let ran = Rc::new(Cell::new(false));
5912 let flag = ran.clone();
5913
5914 let mut tree = WidgetTree::new();
5915 let widget = tree.add(FillWidget::new().focusable());
5916 tree.push_action(
5917 widget,
5918 Action::new("app.save").on_invoke(move |_i, _c| flag.set(true)),
5919 );
5920 tree.layout(SizeProposal::exact(100.0, 50.0));
5921 tree.focus(widget);
5922
5923 let _h = tree.begin_key_capture(|_ks, _reg, ctx| {
5924 ctx.send_intent(Intent::new("app.save"));
5925 });
5926 tree.press_key(Key::X, Modifiers::COMMAND);
5927 assert!(
5928 ran.get(),
5929 "intent queued from capture callback must dispatch"
5930 );
5931 }
5932
5933 #[test]
5934 fn binding_registry_does_not_accumulate_across_rebuilds() {
5935 use crate::binding::BindingLevel;
5936 use crate::signal::Signal;
5937
5938 #[derive(Debug)]
5939 struct BoundLeaf {
5940 tick: Signal<u64>,
5941 }
5942 impl crate::widget::Widget for BoundLeaf {
5943 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
5944 self.tick.bind_to(
5945 ctx.self_id(),
5946 ctx.binding_registry(),
5947 BindingLevel::Relayout,
5948 );
5949 Vec::new()
5950 }
5951 fn layout_response(
5952 &self,
5953 proposal: SizeProposal,
5954 _ctx: &crate::widget::LayoutContext,
5955 ) -> crate::widget::LayoutResponse {
5956 proposal.resolve(10.0, 10.0).into()
5957 }
5958 }
5959
5960 let mut tree = WidgetTree::new();
5961 let tick = Signal::new(0_u64);
5962 let widget = tree.add(BoundLeaf { tick: tick.clone() });
5963 tree.layout(SizeProposal::exact(200.0, 200.0));
5964 let after_first_build = tree.binding_registry().len();
5965 assert!(after_first_build >= 1);
5966
5967 for _ in 0..5 {
5971 tree.arena.mark_needs_rebuild(widget);
5972 tree.layout(SizeProposal::exact(200.0, 200.0));
5973 }
5974 assert_eq!(
5975 tree.binding_registry().len(),
5976 after_first_build,
5977 "bindings must be cleared on rebuild"
5978 );
5979
5980 tree.destroy_subtree(widget);
5981 assert_eq!(
5982 tree.binding_registry().len(),
5983 0,
5984 "bindings must be cleared on destroy"
5985 );
5986 let _ = tick;
5988 }
5989
5990 #[test]
5991 fn ctx_destroy_cancels_animations_and_bindings_via_deferred_path() {
5992 use crate::binding::BindingLevel;
6000 use crate::signal::Signal;
6001 use std::time::{Duration, Instant};
6002 use teksilo_tokens::Easing;
6003
6004 #[derive(Debug)]
6005 struct BoundLeaf {
6006 tick: Signal<u64>,
6007 }
6008 impl crate::widget::Widget for BoundLeaf {
6009 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6010 self.tick.bind_to(
6011 ctx.self_id(),
6012 ctx.binding_registry(),
6013 BindingLevel::Relayout,
6014 );
6015 Vec::new()
6016 }
6017 fn layout_response(
6018 &self,
6019 proposal: SizeProposal,
6020 _ctx: &crate::widget::LayoutContext,
6021 ) -> crate::widget::LayoutResponse {
6022 proposal.resolve(10.0, 10.0).into()
6023 }
6024 }
6025
6026 let mut tree = WidgetTree::new();
6027 let widget = tree.add(BoundLeaf {
6028 tick: Signal::new(0_u64),
6029 });
6030 tree.layout(SizeProposal::exact(200.0, 200.0));
6031 assert!(tree.binding_registry().len() >= 1);
6032
6033 let anim = Signal::<f32>::new_animated(0.0);
6036 tree.animation_scheduler.animate(
6037 &anim,
6038 widget,
6039 1.0,
6040 Duration::from_secs(10),
6041 Easing::Linear,
6042 Instant::now(),
6043 );
6044 assert_eq!(tree.animation_scheduler.active_count(), 1);
6045
6046 let mut noop = crate::window::NoopWindowOps;
6048 tree.run_with_event_context(&mut noop, |ctx| ctx.destroy(widget));
6049
6050 assert_eq!(
6051 tree.animation_scheduler.active_count(),
6052 0,
6053 "ctx.destroy must cancel animations owned by the destroyed widget"
6054 );
6055 assert_eq!(
6056 tree.binding_registry().len(),
6057 0,
6058 "ctx.destroy must unregister the destroyed widget's bindings"
6059 );
6060 assert!(
6061 tree.arena.get(widget).is_none(),
6062 "node must be removed from the arena"
6063 );
6064 }
6065
6066 #[test]
6067 fn clear_shortcut_override_via_event_context_restores_default() {
6068 use crate::shortcut::{KeyStroke, Shortcut};
6069
6070 let mut tree = WidgetTree::new();
6071 tree.shortcut_registry_mut().register(
6072 Shortcut::new("app.save")
6073 .primary(KeyStroke::command(Key::S))
6074 .build(),
6075 );
6076 tree.shortcut_registry_mut()
6077 .rebind_primary("app.save", Some(KeyStroke::alt(Key::S)));
6078
6079 let source = tree.add(FillWidget::new());
6080 let mut ctx = EventContext::new();
6081 ctx.clear_shortcut_override("app.save");
6082 tree.collect_from_ctx(ctx, source);
6083
6084 assert_eq!(
6085 tree.shortcut_registry()
6086 .effective("app.save")
6087 .unwrap()
6088 .primary,
6089 Some(KeyStroke::command(Key::S))
6090 );
6091 }
6092
6093 #[test]
6094 fn rebind_shortcut_primary_via_event_context() {
6095 use crate::shortcut::{KeyStroke, Shortcut};
6096
6097 let mut tree = WidgetTree::new();
6098 tree.shortcut_registry_mut().register(
6099 Shortcut::new("app.save")
6100 .primary(KeyStroke::command(Key::S))
6101 .build(),
6102 );
6103 let source = tree.add(FillWidget::new());
6104
6105 let mut ctx = EventContext::new();
6106 ctx.rebind_shortcut_primary("app.save", Some(KeyStroke::alt(Key::S)));
6107 tree.collect_from_ctx(ctx, source);
6108
6109 assert_eq!(
6110 tree.shortcut_registry()
6111 .effective("app.save")
6112 .unwrap()
6113 .primary,
6114 Some(KeyStroke::alt(Key::S))
6115 );
6116 }
6117
6118 #[test]
6119 fn unregister_all_for_owner_called_on_destroy() {
6120 use crate::shortcut::{KeyStroke, Shortcut};
6121
6122 let mut tree = WidgetTree::new();
6123 let widget = tree.add(FillWidget::new());
6124 let widget_owner = widget;
6125 tree.shortcut_registry_mut().register_owned(
6126 Shortcut::new("scoped.thing")
6127 .primary(KeyStroke::command(Key::K))
6128 .build(),
6129 widget_owner,
6130 );
6131 assert!(
6132 tree.shortcut_registry()
6133 .get_default("scoped.thing")
6134 .is_some()
6135 );
6136
6137 tree.destroy_subtree(widget);
6138 assert!(
6139 tree.shortcut_registry()
6140 .get_default("scoped.thing")
6141 .is_none(),
6142 "destroying the owner must unregister its shortcut"
6143 );
6144 }
6145
6146 #[test]
6150 fn global_action_reached_from_unrelated_source() {
6151 use crate::action::Action;
6152 use crate::intent::Intent;
6153 use std::cell::Cell;
6154 use std::rc::Rc;
6155
6156 #[derive(Debug)]
6157 struct Registrar(Rc<Cell<bool>>);
6158 impl crate::widget::Widget for Registrar {
6159 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6160 let flag = self.0.clone();
6161 ctx.register_action_global(
6162 Action::new("test.global").on_invoke(move |_i, _c| flag.set(true)),
6163 );
6164 vec![]
6165 }
6166 fn layout_response(
6167 &self,
6168 _p: teksilo_canvas::SizeProposal,
6169 _c: &crate::widget::LayoutContext,
6170 ) -> crate::widget::LayoutResponse {
6171 teksilo_canvas::Size::new(0.0, 0.0).into()
6172 }
6173 }
6174
6175 let mut tree = WidgetTree::new();
6176 let fired = Rc::new(Cell::new(false));
6177 let registrar = tree.add(Registrar(fired.clone()));
6178 let source = tree.add(FillWidget::new()); let mut ops = crate::window::NoopWindowOps;
6180
6181 tree.dispatch_intent(source, Intent::new("test.global"), true, &mut ops);
6182 assert!(
6183 fired.get(),
6184 "global action must fire from an unrelated source"
6185 );
6186
6187 fired.set(false);
6189 tree.destroy_subtree(registrar);
6190 tree.dispatch_intent(source, Intent::new("test.global"), true, &mut ops);
6191 assert!(
6192 !fired.get(),
6193 "destroying the owner must remove its global action"
6194 );
6195 }
6196
6197 #[test]
6209 fn hit_test_through_translate_scope() {
6210 use crate::test_widgets::StackWidget;
6211 let mut tree = WidgetTree::new();
6212 let child = tree.add(FillWidget::new());
6213 let parent = tree.add(StackWidget::new().child(child));
6214 tree.set_transform(parent, teksilo_canvas::Transform2D::translate(100.0, 0.0));
6216 tree.layout(SizeProposal::exact(100.0, 50.0));
6217
6218 assert_eq!(
6222 tree.hit_test(Point::new(50.0, 25.0)),
6223 None,
6224 "pre-transform area is not visually populated and must not hit"
6225 );
6226 assert_eq!(
6228 tree.hit_test(Point::new(150.0, 25.0)),
6229 Some(child),
6230 "visually-rendered area must hit the child"
6231 );
6232 assert_eq!(tree.hit_test(Point::new(250.0, 25.0)), None);
6234 }
6235
6236 #[test]
6237 fn hit_test_through_scale_scope() {
6238 use crate::test_widgets::StackWidget;
6239 let mut tree = WidgetTree::new();
6240 let child = tree.add(FillWidget::new());
6241 let parent = tree.add(StackWidget::new().child(child));
6242 tree.set_transform(parent, teksilo_canvas::Transform2D::scale(0.5, 0.5));
6245 tree.layout(SizeProposal::exact(100.0, 50.0));
6246
6247 assert_eq!(tree.hit_test(Point::new(25.0, 12.0)), Some(child));
6249 assert_eq!(
6252 tree.hit_test(Point::new(75.0, 25.0)),
6253 None,
6254 "scaled-out region must not hit"
6255 );
6256 }
6257
6258 #[test]
6259 fn hit_test_through_nested_transforms_compose() {
6260 use crate::test_widgets::StackWidget;
6261 let mut tree = WidgetTree::new();
6262 let leaf = tree.add(FillWidget::new());
6263 let inner = tree.add(StackWidget::new().child(leaf));
6264 let outer = tree.add(StackWidget::new().child(inner));
6265 tree.set_transform(outer, teksilo_canvas::Transform2D::translate(100.0, 0.0));
6269 tree.set_transform(inner, teksilo_canvas::Transform2D::scale(2.0, 2.0));
6270 tree.layout(SizeProposal::exact(50.0, 25.0));
6271
6272 assert_eq!(tree.hit_test(Point::new(150.0, 25.0)), Some(leaf));
6276 assert_eq!(tree.hit_test(Point::new(50.0, 25.0)), None);
6277 assert_eq!(tree.hit_test(Point::new(250.0, 25.0)), None);
6278 }
6279
6280 #[test]
6281 fn hit_test_identity_transform_unchanged() {
6282 let mut tree = WidgetTree::new();
6286 let widget = tree.add(FillWidget::new());
6287 tree.set_transform(widget, teksilo_canvas::Transform2D::IDENTITY);
6288 tree.layout(SizeProposal::exact(100.0, 50.0));
6289 assert_eq!(tree.hit_test(Point::new(50.0, 25.0)), Some(widget));
6290 }
6291
6292 #[test]
6293 fn arena_effective_transform_composes_ancestors() {
6294 use crate::test_widgets::StackWidget;
6305 let mut tree = WidgetTree::new();
6306 let leaf = tree.add(FillWidget::new());
6307 let inner = tree.add(StackWidget::new().child(leaf));
6308 let outer = tree.add(StackWidget::new().child(inner));
6309 tree.set_transform(outer, teksilo_canvas::Transform2D::translate(100.0, 0.0));
6310 tree.set_transform(inner, teksilo_canvas::Transform2D::scale(2.0, 2.0));
6311 tree.layout(SizeProposal::exact(50.0, 25.0));
6312
6313 let eff = tree.arena.effective_transform(leaf);
6314 let expected = teksilo_canvas::Transform2D::scale(2.0, 2.0)
6315 .then(&teksilo_canvas::Transform2D::translate(100.0, 0.0));
6316 for (a, b) in eff.m.iter().zip(expected.m.iter()) {
6317 assert!(
6318 (a - b).abs() < 1e-5,
6319 "effective_transform mismatch: got {:?}, want {:?}",
6320 eff.m,
6321 expected.m
6322 );
6323 }
6324
6325 let screen_origin = eff.apply_point(Point::new(0.0, 0.0));
6331 assert!((screen_origin.x - 100.0).abs() < 1e-5);
6332 assert!((screen_origin.y - 0.0).abs() < 1e-5);
6333 let screen_corner = eff.apply_point(Point::new(50.0, 25.0));
6336 assert!((screen_corner.x - 200.0).abs() < 1e-5);
6337 assert!((screen_corner.y - 50.0).abs() < 1e-5);
6338 }
6339
6340 #[derive(Debug)]
6345 struct StubMenu;
6346 impl crate::widget::Widget for StubMenu {
6347 fn layout_response(
6348 &self,
6349 _proposal: SizeProposal,
6350 _ctx: &crate::widget::LayoutContext,
6351 ) -> crate::widget::LayoutResponse {
6352 teksilo_canvas::Size::new(100.0, 40.0).into()
6353 }
6354 }
6355
6356 #[derive(Debug)]
6368 struct NominatingWidget {
6369 row: std::cell::Cell<Option<WidgetId>>,
6370 }
6371
6372 impl crate::widget::Widget for NominatingWidget {
6373 fn layout_response(
6374 &self,
6375 proposal: SizeProposal,
6376 _ctx: &LayoutContext,
6377 ) -> crate::widget::LayoutResponse {
6378 proposal.resolve(50.0, 20.0).into()
6379 }
6380
6381 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6382 ctx.apply_self_handlers(crate::widget_builder::HandlerSet::new().focusable(true));
6383 Vec::new()
6384 }
6385
6386 fn context_menu_key_target(&self) -> Option<WidgetId> {
6387 self.row.get()
6388 }
6389 }
6390
6391 fn press(tree: &mut WidgetTree, key: Key, modifiers: Modifiers) {
6392 tree.dispatch_event(WidgetEvent::KeyDown {
6393 key,
6394 modifiers,
6395 text: None,
6396 });
6397 }
6398
6399 #[test]
6400 fn the_context_menu_key_opens_the_focused_widget_menu() {
6401 use std::cell::Cell;
6402 use std::rc::Rc;
6403
6404 let opened = Rc::new(Cell::new(false));
6405 let flag = opened.clone();
6406 let mut tree = WidgetTree::new();
6407 let widget = tree.add(
6408 FillWidget::new()
6409 .focusable()
6410 .context_menu(move |_pos, _ctx| {
6411 flag.set(true);
6412 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6413 }),
6414 );
6415 tree.layout(SizeProposal::exact(200.0, 100.0));
6416 tree.focus(widget);
6417
6418 press(&mut tree, Key::ContextMenu, Modifiers::NONE);
6419 assert!(opened.get(), "the dedicated Menu key must open the menu");
6420 }
6421
6422 #[test]
6425 fn shift_f10_opens_the_focused_widget_menu() {
6426 use std::cell::Cell;
6427 use std::rc::Rc;
6428
6429 let opened = Rc::new(Cell::new(false));
6430 let flag = opened.clone();
6431 let mut tree = WidgetTree::new();
6432 let widget = tree.add(
6433 FillWidget::new()
6434 .focusable()
6435 .context_menu(move |_pos, _ctx| {
6436 flag.set(true);
6437 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6438 }),
6439 );
6440 tree.layout(SizeProposal::exact(200.0, 100.0));
6441 tree.focus(widget);
6442
6443 press(&mut tree, Key::F10, Modifiers::SHIFT);
6444 assert!(opened.get(), "Shift+F10 must open the menu");
6445 }
6446
6447 #[test]
6450 fn a_near_miss_chord_is_not_a_context_menu_request() {
6451 use std::cell::Cell;
6452 use std::rc::Rc;
6453
6454 let opened = Rc::new(Cell::new(false));
6455 let flag = opened.clone();
6456 let mut tree = WidgetTree::new();
6457 let widget = tree.add(
6458 FillWidget::new()
6459 .focusable()
6460 .context_menu(move |_pos, _ctx| {
6461 flag.set(true);
6462 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6463 }),
6464 );
6465 tree.layout(SizeProposal::exact(200.0, 100.0));
6466 tree.focus(widget);
6467
6468 press(&mut tree, Key::F10, Modifiers::SHIFT | Modifiers::CTRL);
6469 press(&mut tree, Key::F10, Modifiers::NONE);
6470 assert!(!opened.get(), "only Shift+F10 exactly asks for a menu");
6471 }
6472
6473 #[test]
6477 fn the_keyboard_target_can_be_a_row_rather_than_the_focused_container() {
6478 use std::cell::Cell;
6479 use std::rc::Rc;
6480
6481 let menu_owner = Rc::new(Cell::new(None::<&'static str>));
6482
6483 let row_flag = menu_owner.clone();
6484 let container_flag = menu_owner.clone();
6485
6486 let mut tree = WidgetTree::new();
6487 let row = tree.add(FillWidget::new().context_menu(move |_pos, _ctx| {
6488 row_flag.set(Some("row"));
6489 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6490 }));
6491 let container = tree.add(
6492 crate::test_widgets::StackWidget::new()
6493 .child(row)
6494 .context_menu(move |_pos, _ctx| {
6495 container_flag.set(Some("container"));
6496 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6497 }),
6498 );
6499 tree.layout(SizeProposal::exact(200.0, 100.0));
6500
6501 let nominator = tree.add(NominatingWidget {
6503 row: std::cell::Cell::new(Some(row)),
6504 });
6505 tree.layout(SizeProposal::exact(200.0, 100.0));
6506 tree.focus(nominator);
6507 let _ = container;
6508
6509 press(&mut tree, Key::ContextMenu, Modifiers::NONE);
6510 assert_eq!(
6511 menu_owner.get(),
6512 Some("row"),
6513 "the nominated row's factory must be the one that runs"
6514 );
6515 }
6516
6517 #[test]
6520 fn the_chord_falls_through_when_there_is_no_menu_to_show() {
6521 use std::cell::Cell;
6522 use std::rc::Rc;
6523
6524 let saw_key = Rc::new(Cell::new(false));
6525 let flag = saw_key.clone();
6526 let mut tree = WidgetTree::new();
6527 let widget = tree.add(FillWidget::new().focusable().on_key(move |ev, _ctx| {
6528 if matches!(ev, WidgetEvent::KeyDown { key: Key::F10, .. }) {
6529 flag.set(true);
6530 }
6531 crate::event::EventResponse::Ignored
6532 }));
6533 tree.layout(SizeProposal::exact(200.0, 100.0));
6534 tree.focus(widget);
6535
6536 press(&mut tree, Key::F10, Modifiers::SHIFT);
6537 assert!(
6538 saw_key.get(),
6539 "with no factory anywhere, the key must reach the widget"
6540 );
6541 }
6542
6543 #[test]
6549 fn each_route_to_a_menu_tells_the_factory_what_asked_for_it() {
6550 use crate::widget_builder::ContextMenuTrigger;
6551 use std::cell::RefCell;
6552 use std::rc::Rc;
6553
6554 let seen = Rc::new(RefCell::new(Vec::new()));
6555 let log = seen.clone();
6556 let mut tree = WidgetTree::new();
6557 let widget = tree.add(
6558 FillWidget::new()
6559 .focusable()
6560 .context_menu(move |_pos, ctx| {
6561 log.borrow_mut().push(ctx.context_menu_trigger());
6562 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6563 }),
6564 );
6565 tree.layout(SizeProposal::exact(200.0, 100.0));
6566 tree.focus(widget);
6567 let inside = tree.bounds(widget).center();
6568
6569 press(&mut tree, Key::F10, Modifiers::SHIFT);
6570 press(&mut tree, Key::Escape, Modifiers::NONE);
6571 press(&mut tree, Key::ContextMenu, Modifiers::NONE);
6572 press(&mut tree, Key::Escape, Modifiers::NONE);
6573 tree.dispatch_event(WidgetEvent::pointer_down(
6574 inside,
6575 crate::event::PointerButton::Secondary,
6576 Modifiers::NONE,
6577 ));
6578 tree.dispatch_event(WidgetEvent::pointer_up(
6579 inside,
6580 crate::event::PointerButton::Secondary,
6581 Modifiers::NONE,
6582 ));
6583 press(&mut tree, Key::Escape, Modifiers::NONE);
6584 tree.dispatch_event(WidgetEvent::AccessAction {
6585 action: accesskit::Action::ShowContextMenu,
6586 target: Some(widget),
6587 target_node: crate::accessibility::widget_id_to_node_id(widget),
6588 data: None,
6589 });
6590 press(&mut tree, Key::Escape, Modifiers::NONE);
6591 tree.long_press_at(teksilo_tokens::PointerKind::Touch, inside);
6592
6593 assert_eq!(
6594 *seen.borrow(),
6595 vec![
6596 Some(ContextMenuTrigger::Keyboard),
6597 Some(ContextMenuTrigger::Keyboard),
6598 Some(ContextMenuTrigger::Pointer),
6599 Some(ContextMenuTrigger::Accessibility),
6600 Some(ContextMenuTrigger::Pointer),
6601 ],
6602 "Shift+F10, the Menu key, a right-click, the assistive action and a \
6603 hold, in that order"
6604 );
6605 }
6606
6607 #[test]
6608 fn context_menu_factory_receives_click_position() {
6609 use crate::event::{Modifiers, PointerButton};
6610 use std::cell::Cell;
6611 use std::rc::Rc;
6612
6613 let captured_position = Rc::new(Cell::new(None::<Point>));
6614 let cap = captured_position.clone();
6615 let mut tree = WidgetTree::new();
6616 let widget = tree.add(FillWidget::new().context_menu(move |pos, _ctx| {
6617 cap.set(Some(pos));
6618 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6619 }));
6620 tree.layout(SizeProposal::exact(200.0, 100.0));
6621
6622 let click = Point::new(73.0, 42.0);
6623 tree.dispatch_event(WidgetEvent::pointer_down(
6624 click,
6625 PointerButton::Secondary,
6626 Modifiers::NONE,
6627 ));
6628
6629 let got = captured_position.get();
6630 assert_eq!(
6631 got,
6632 Some(click),
6633 "factory must receive the click position; got {:?}",
6634 got
6635 );
6636 let _ = widget;
6637 }
6638
6639 #[test]
6640 fn context_menu_factory_returning_none_falls_through_to_parent() {
6641 use crate::event::{Modifiers, PointerButton};
6642 use crate::test_widgets::StackWidget;
6643 use std::cell::Cell;
6644 use std::rc::Rc;
6645
6646 let outer_called = Rc::new(Cell::new(0_u32));
6650 let outer_flag = outer_called.clone();
6651 let mut tree = WidgetTree::new();
6652 let inner = tree.add(FillWidget::new().context_menu(|_pos, _ctx| None));
6653 let _outer = tree.add(
6654 StackWidget::new()
6655 .child(inner)
6656 .context_menu(move |_pos, _ctx| {
6657 outer_flag.set(outer_flag.get() + 1);
6658 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6659 }),
6660 );
6661 tree.layout(SizeProposal::exact(200.0, 100.0));
6662
6663 tree.dispatch_event(WidgetEvent::pointer_down(
6664 Point::new(50.0, 25.0),
6665 PointerButton::Secondary,
6666 Modifiers::NONE,
6667 ));
6668
6669 assert_eq!(
6670 outer_called.get(),
6671 1,
6672 "inner returning None must fall through to the outer factory"
6673 );
6674 }
6675
6676 #[test]
6677 fn context_menu_factory_none_throughout_chain_does_not_show_overlay() {
6678 use crate::event::{Modifiers, PointerButton};
6679
6680 let mut tree = WidgetTree::new();
6682 tree.add(FillWidget::new().context_menu(|_pos, _ctx| None));
6683 tree.layout(SizeProposal::exact(200.0, 100.0));
6684
6685 let overlay_count_before = tree.overlay_manager.len();
6686 tree.dispatch_event(WidgetEvent::pointer_down(
6687 Point::new(50.0, 25.0),
6688 PointerButton::Secondary,
6689 Modifiers::NONE,
6690 ));
6691 let overlay_count_after = tree.overlay_manager.len();
6692 assert_eq!(
6693 overlay_count_before, overlay_count_after,
6694 "a factory returning None must not mount any overlay"
6695 );
6696 }
6697
6698 #[derive(Debug)]
6709 struct FreshChildHost {
6710 preserve: bool,
6711 }
6712 impl Widget for FreshChildHost {
6713 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6714 vec![ctx.add(FillWidget::new())]
6715 }
6716 fn layout_response(
6717 &self,
6718 p: SizeProposal,
6719 _c: &LayoutContext,
6720 ) -> crate::widget::LayoutResponse {
6721 p.resolve(10.0, 10.0).into()
6722 }
6723 fn preserves_children_on_rebuild(&self) -> bool {
6724 self.preserve
6725 }
6726 }
6727
6728 #[test]
6729 fn reconcile_reaps_dropped_children_no_leak() {
6730 for preserve in [false, true] {
6735 let mut tree = WidgetTree::new();
6736 let host = tree.add(FreshChildHost { preserve });
6737 tree.layout(SizeProposal::exact(100.0, 100.0));
6738 let total0 = tree.arena.len();
6739 let active0 = tree.active_widget_count();
6740 for _ in 0..5 {
6741 tree.arena_mark_needs_rebuild_for_testing(host);
6742 tree.layout(SizeProposal::exact(100.0, 100.0));
6743 }
6744 assert_eq!(
6745 tree.arena.len(),
6746 total0,
6747 "preserve={preserve}: dropped children must be reaped, not leaked"
6748 );
6749 assert_eq!(
6750 tree.active_widget_count(),
6751 active0,
6752 "preserve={preserve}: no stranded still-active orphans"
6753 );
6754 }
6755 }
6756
6757 #[derive(Debug)]
6763 struct DetachedContentHost {
6764 preserve: bool,
6765 }
6766 impl Widget for DetachedContentHost {
6767 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6768 let popup = ctx.add_detached(FillWidget::new());
6769 ctx.set_dormant(popup);
6770 vec![ctx.add(FillWidget::new())]
6771 }
6772 fn layout_response(
6773 &self,
6774 p: SizeProposal,
6775 _c: &LayoutContext,
6776 ) -> crate::widget::LayoutResponse {
6777 p.resolve(10.0, 10.0).into()
6778 }
6779 fn preserves_children_on_rebuild(&self) -> bool {
6780 self.preserve
6781 }
6782 }
6783
6784 #[test]
6785 fn rebuilding_reaps_detached_content_no_leak() {
6786 for preserve in [false, true] {
6792 let mut tree = WidgetTree::new();
6793 let host = tree.add(DetachedContentHost { preserve });
6794 tree.layout(SizeProposal::exact(100.0, 100.0));
6795 let total0 = tree.arena.len();
6796 for _ in 0..5 {
6797 tree.arena_mark_needs_rebuild_for_testing(host);
6798 tree.layout(SizeProposal::exact(100.0, 100.0));
6799 }
6800 assert_eq!(
6801 tree.arena.len(),
6802 total0,
6803 "preserve={preserve}: the previous build's detached content must be reaped"
6804 );
6805 }
6806 }
6807
6808 #[test]
6809 fn destroying_a_host_reaps_its_detached_content() {
6810 let mut tree = WidgetTree::new();
6811 let outer = tree.add(FillWidget::new());
6812 tree.layout(SizeProposal::exact(100.0, 100.0));
6813 let empty = tree.arena.len();
6814
6815 let host = tree.add_child(outer, DetachedContentHost { preserve: false });
6816 tree.layout(SizeProposal::exact(100.0, 100.0));
6817 assert!(tree.arena.len() > empty);
6818
6819 tree.destroy_subtree(host);
6820 assert_eq!(
6821 tree.arena.len(),
6822 empty,
6823 "the popup must die with the widget that built it"
6824 );
6825 }
6826
6827 #[derive(Debug)]
6829 struct StableChildHost {
6830 child: Option<WidgetId>,
6831 probe: std::rc::Rc<std::cell::Cell<Option<WidgetId>>>,
6832 }
6833 impl Widget for StableChildHost {
6834 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6835 let id = match self.child {
6836 Some(id) => id,
6837 None => {
6838 let id = ctx.add(FillWidget::new());
6839 self.child = Some(id);
6840 self.probe.set(Some(id));
6841 id
6842 }
6843 };
6844 vec![id]
6845 }
6846 fn layout_response(
6847 &self,
6848 p: SizeProposal,
6849 _c: &LayoutContext,
6850 ) -> crate::widget::LayoutResponse {
6851 p.resolve(10.0, 10.0).into()
6852 }
6853 fn preserves_children_on_rebuild(&self) -> bool {
6854 true
6855 }
6856 }
6857
6858 #[test]
6859 fn reconcile_preserves_reattached_child() {
6860 let probe = std::rc::Rc::new(std::cell::Cell::new(None));
6861 let mut tree = WidgetTree::new();
6862 let host = tree.add(StableChildHost {
6863 child: None,
6864 probe: probe.clone(),
6865 });
6866 tree.layout(SizeProposal::exact(100.0, 100.0));
6867 let child = probe.get().expect("child mounted");
6868 let total0 = tree.arena.len();
6869 for _ in 0..5 {
6870 tree.arena_mark_needs_rebuild_for_testing(host);
6871 tree.layout(SizeProposal::exact(100.0, 100.0));
6872 }
6873 assert!(
6874 tree.arena.is_active(child),
6875 "the re-attached child must survive every rebuild"
6876 );
6877 assert_eq!(tree.arena.len(), total0, "no growth — same child reused");
6878 }
6879
6880 #[derive(Debug)]
6882 struct Wrapper {
6883 child: WidgetId,
6884 }
6885 impl Widget for Wrapper {
6886 fn build(&mut self, _ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6887 vec![self.child]
6888 }
6889 fn layout_response(
6890 &self,
6891 p: SizeProposal,
6892 _c: &LayoutContext,
6893 ) -> crate::widget::LayoutResponse {
6894 p.resolve(10.0, 10.0).into()
6895 }
6896 }
6897
6898 #[derive(Debug)]
6902 struct ReparentHost {
6903 body: Option<WidgetId>,
6904 probe: std::rc::Rc<std::cell::Cell<Option<WidgetId>>>,
6905 }
6906 impl Widget for ReparentHost {
6907 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6908 let body = match self.body {
6909 Some(id) => id,
6910 None => {
6911 let id = ctx.add(FillWidget::new());
6912 self.body = Some(id);
6913 self.probe.set(Some(id));
6914 id
6915 }
6916 };
6917 vec![ctx.add(Wrapper { child: body })]
6918 }
6919 fn layout_response(
6920 &self,
6921 p: SizeProposal,
6922 _c: &LayoutContext,
6923 ) -> crate::widget::LayoutResponse {
6924 p.resolve(10.0, 10.0).into()
6925 }
6926 fn preserves_children_on_rebuild(&self) -> bool {
6927 true
6928 }
6929 }
6930
6931 #[test]
6932 fn reconcile_spares_reparented_survivor() {
6933 let probe = std::rc::Rc::new(std::cell::Cell::new(None));
6940 let mut tree = WidgetTree::new();
6941 let host = tree.add(ReparentHost {
6942 body: None,
6943 probe: probe.clone(),
6944 });
6945 tree.layout(SizeProposal::exact(100.0, 100.0));
6946 let body = probe.get().expect("body mounted");
6947 let total0 = tree.arena.len();
6948 for _ in 0..5 {
6949 tree.arena_mark_needs_rebuild_for_testing(host);
6950 tree.layout(SizeProposal::exact(100.0, 100.0));
6951 }
6952 assert!(
6953 tree.arena.is_active(body),
6954 "the re-parented body must survive — it was moved into the new tree, \
6955 not swept with the dropped wrapper"
6956 );
6957 assert_eq!(
6958 tree.arena.len(),
6959 total0,
6960 "dropped wrappers reaped — no per-rebuild leak"
6961 );
6962 }
6963
6964 #[derive(Debug)]
6974 struct BelowContainer {
6975 child: Option<WidgetId>,
6976 offset: f32,
6977 }
6978
6979 impl crate::widget::Widget for BelowContainer {
6980 fn layout_response(
6981 &self,
6982 proposal: SizeProposal,
6983 _ctx: &crate::widget::LayoutContext,
6984 ) -> crate::widget::LayoutResponse {
6985 proposal.resolve(0.0, 0.0).into()
6986 }
6987 fn place_children(
6988 &self,
6989 bounds: Rect,
6990 _proposal: SizeProposal,
6991 children: &mut [crate::widget::WidgetPlacement],
6992 _ctx: &crate::widget::LayoutContext,
6993 ) {
6994 for c in children.iter_mut() {
6995 c.origin = Point::new(bounds.x, bounds.y + self.offset);
6996 c.size = bounds.size();
6997 }
6998 }
6999 fn children(&self) -> Vec<WidgetId> {
7000 self.child.into_iter().collect()
7001 }
7002 }
7003
7004 fn recording_scroll_container(
7007 tree: &mut WidgetTree,
7008 child: WidgetId,
7009 recorded: std::rc::Rc<std::cell::Cell<Option<Rect>>>,
7010 ) -> WidgetId {
7011 use crate::test_widgets::StackWidget;
7012 tree.add(
7013 StackWidget::new()
7014 .child(child)
7015 .on_scroll(move |ev, _ctx| match ev {
7016 WidgetEvent::ScrollIntoView { target_bounds, .. } => {
7017 recorded.set(Some(*target_bounds));
7018 EventResponse::Handled
7019 }
7020 _ => EventResponse::Ignored,
7021 })
7022 .clips_children(true),
7023 )
7024 }
7025
7026 #[test]
7027 fn ensure_visible_dispatches_scroll_into_view_to_clipping_ancestor() {
7028 use std::cell::Cell;
7029 use std::rc::Rc;
7030 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
7031 let mut tree = WidgetTree::new();
7032 let actor = tree.add(FillWidget::new());
7033 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
7034 tree.layout(SizeProposal::exact(100.0, 100.0));
7035
7036 let target = Rect::new(10.0, 500.0, 20.0, 15.0);
7039 let mut ctx = EventContext::new();
7040 ctx.ensure_visible(target);
7041 tree.collect_from_ctx(ctx, actor);
7042
7043 assert_eq!(
7044 recorded.get(),
7045 Some(target),
7046 "ensure_visible(rect) must dispatch ScrollIntoView with the exact rect \
7047 to the clips_children ancestor"
7048 );
7049 }
7050
7051 #[test]
7052 fn ensure_visible_is_noop_when_rect_already_visible() {
7053 use std::cell::Cell;
7054 use std::rc::Rc;
7055 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
7056 let mut tree = WidgetTree::new();
7057 let actor = tree.add(FillWidget::new());
7058 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
7059 tree.layout(SizeProposal::exact(100.0, 100.0));
7060
7061 let mut ctx = EventContext::new();
7064 ctx.ensure_visible(Rect::new(10.0, 10.0, 20.0, 15.0));
7065 tree.collect_from_ctx(ctx, actor);
7066
7067 assert_eq!(
7068 recorded.get(),
7069 None,
7070 "a rect already inside the viewport must not trigger a scroll"
7071 );
7072 }
7073
7074 #[test]
7075 fn ensure_visible_margin_forces_scroll_near_edge() {
7076 use std::cell::Cell;
7077 use std::rc::Rc;
7078 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
7079 let mut tree = WidgetTree::new();
7080 let actor = tree.add(FillWidget::new());
7081 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
7082 tree.layout(SizeProposal::exact(100.0, 100.0));
7083
7084 let rect = Rect::new(10.0, 95.0, 20.0, 4.0);
7087 let mut ctx = EventContext::new();
7088 ctx.ensure_visible_with_margin(rect, 10.0);
7089 tree.collect_from_ctx(ctx, actor);
7090
7091 assert_eq!(
7092 recorded.get(),
7093 Some(rect),
7094 "the margin must widen the visibility test so a near-edge rect scrolls"
7095 );
7096 }
7097
7098 fn recording_align_container(
7101 tree: &mut WidgetTree,
7102 child: WidgetId,
7103 recorded: std::rc::Rc<
7104 std::cell::Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>,
7105 >,
7106 ) -> WidgetId {
7107 use crate::test_widgets::StackWidget;
7108 tree.add(
7109 StackWidget::new()
7110 .child(child)
7111 .on_scroll(move |ev, _ctx| match ev {
7112 WidgetEvent::ScrollIntoView { align, motion, .. } => {
7113 recorded.set(Some((*align, *motion)));
7114 EventResponse::Handled
7115 }
7116 _ => EventResponse::Ignored,
7117 })
7118 .clips_children(true),
7119 )
7120 }
7121
7122 #[test]
7123 fn ensure_visible_aligned_scrolls_even_when_already_visible() {
7124 use std::cell::Cell;
7125 use std::rc::Rc;
7126 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
7127 let mut tree = WidgetTree::new();
7128 let actor = tree.add(FillWidget::new());
7129 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
7130 tree.layout(SizeProposal::exact(100.0, 100.0));
7131
7132 let target = Rect::new(10.0, 10.0, 20.0, 15.0);
7137 let mut ctx = EventContext::new();
7138 ctx.ensure_visible_aligned(target, 0.5, crate::event::ScrollMotion::Instant);
7139 tree.collect_from_ctx(ctx, actor);
7140
7141 assert_eq!(
7142 recorded.get(),
7143 Some(target),
7144 "an aligned reveal must dispatch even when the rect is already visible"
7145 );
7146 }
7147
7148 #[test]
7149 fn ensure_visible_aligned_forwards_fraction_and_motion() {
7150 use std::cell::Cell;
7151 use std::rc::Rc;
7152 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
7153 Rc::new(Cell::new(None));
7154 let mut tree = WidgetTree::new();
7155 let actor = tree.add(FillWidget::new());
7156 let _container = recording_align_container(&mut tree, actor, recorded.clone());
7157 tree.layout(SizeProposal::exact(100.0, 100.0));
7158
7159 let mut ctx = EventContext::new();
7160 ctx.ensure_visible_aligned(
7161 Rect::new(10.0, 10.0, 20.0, 15.0),
7162 0.25,
7163 crate::event::ScrollMotion::Smooth,
7164 );
7165 tree.collect_from_ctx(ctx, actor);
7166
7167 assert_eq!(
7168 recorded.get(),
7169 Some((
7170 crate::event::ScrollAlign::Fraction(0.25),
7171 crate::event::ScrollMotion::Smooth
7172 )),
7173 "the container must receive the requested fraction and motion verbatim"
7174 );
7175 }
7176
7177 #[test]
7178 fn ensure_visible_aligned_clamps_the_fraction() {
7179 use std::cell::Cell;
7180 use std::rc::Rc;
7181 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
7182 Rc::new(Cell::new(None));
7183 let mut tree = WidgetTree::new();
7184 let actor = tree.add(FillWidget::new());
7185 let _container = recording_align_container(&mut tree, actor, recorded.clone());
7186 tree.layout(SizeProposal::exact(100.0, 100.0));
7187
7188 let mut ctx = EventContext::new();
7189 ctx.ensure_visible_aligned(
7190 Rect::new(10.0, 10.0, 20.0, 15.0),
7191 4.2,
7192 crate::event::ScrollMotion::Instant,
7193 );
7194 tree.collect_from_ctx(ctx, actor);
7195
7196 assert_eq!(
7197 recorded.get().map(|(a, _)| a),
7198 Some(crate::event::ScrollAlign::Fraction(1.0)),
7199 "an out-of-range fraction must clamp rather than aim the pin off-screen"
7200 );
7201 }
7202
7203 #[test]
7204 fn plain_ensure_visible_requests_minimal_alignment() {
7205 use std::cell::Cell;
7206 use std::rc::Rc;
7207 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
7208 Rc::new(Cell::new(None));
7209 let mut tree = WidgetTree::new();
7210 let actor = tree.add(FillWidget::new());
7211 let _container = recording_align_container(&mut tree, actor, recorded.clone());
7212 tree.layout(SizeProposal::exact(100.0, 100.0));
7213
7214 let mut ctx = EventContext::new();
7215 ctx.ensure_visible(Rect::new(10.0, 500.0, 20.0, 15.0));
7216 tree.collect_from_ctx(ctx, actor);
7217
7218 assert_eq!(
7219 recorded.get(),
7220 Some((
7221 crate::event::ScrollAlign::Minimal,
7222 crate::event::ScrollMotion::Instant
7223 )),
7224 "the pre-existing reveal API must keep its exact semantics"
7225 );
7226 }
7227
7228 #[test]
7229 fn only_the_innermost_container_aligns() {
7230 use std::cell::Cell;
7231 use std::rc::Rc;
7232 let inner_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
7233 Rc::new(Cell::new(None));
7234 let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
7235 Rc::new(Cell::new(None));
7236
7237 let mut tree = WidgetTree::new();
7238 let actor = tree.add(FillWidget::new());
7239 let inner = recording_align_container(&mut tree, actor, inner_rec.clone());
7240 let _outer = recording_align_container(&mut tree, inner, outer_rec.clone());
7241 tree.layout(SizeProposal::exact(100.0, 100.0));
7242
7243 let mut ctx = EventContext::new();
7246 ctx.ensure_visible_aligned(
7247 Rect::new(10.0, 500.0, 20.0, 15.0),
7248 0.5,
7249 crate::event::ScrollMotion::Instant,
7250 );
7251 tree.collect_from_ctx(ctx, actor);
7252
7253 assert_eq!(
7254 inner_rec.get().map(|(a, _)| a),
7255 Some(crate::event::ScrollAlign::Fraction(0.5)),
7256 "the innermost clipping ancestor owns the pin"
7257 );
7258 assert_eq!(
7259 outer_rec.get().map(|(a, _)| a),
7260 Some(crate::event::ScrollAlign::Minimal),
7261 "an outer container must only bring the inner viewport into view — a \
7262 fraction names a height in one viewport, not in every ancestor's"
7263 );
7264 }
7265
7266 #[test]
7267 fn a_clipper_that_does_not_scroll_passes_the_pin_on() {
7268 use crate::test_widgets::StackWidget;
7275 use std::cell::Cell;
7276 use std::rc::Rc;
7277 let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
7278 Rc::new(Cell::new(None));
7279
7280 let mut tree = WidgetTree::new();
7281 let actor = tree.add(FillWidget::new());
7282 let clipper = tree.add(StackWidget::new().child(actor).clips_children_on(true));
7283 let _scroller = recording_align_container(&mut tree, clipper, outer_rec.clone());
7284 tree.layout(SizeProposal::exact(100.0, 100.0));
7285
7286 let mut ctx = EventContext::new();
7289 ctx.ensure_visible_aligned(
7290 Rect::new(10.0, 10.0, 20.0, 15.0),
7291 0.5,
7292 crate::event::ScrollMotion::Instant,
7293 );
7294 tree.collect_from_ctx(ctx, actor);
7295
7296 assert_eq!(
7297 outer_rec.get().map(|(a, _)| a),
7298 Some(crate::event::ScrollAlign::Fraction(0.5)),
7299 "a clipping wrapper with no scroll handler must pass the pin through \
7300 to the scroll container above it"
7301 );
7302 }
7303
7304 #[test]
7305 fn a_scroller_that_declines_to_move_still_holds_the_pin() {
7306 use crate::test_widgets::StackWidget;
7311 use std::cell::Cell;
7312 use std::rc::Rc;
7313 let inner_rec: Rc<Cell<Option<crate::event::ScrollAlign>>> = Rc::new(Cell::new(None));
7314 let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
7315 Rc::new(Cell::new(None));
7316
7317 let mut tree = WidgetTree::new();
7318 let actor = tree.add(FillWidget::new());
7319 let rec = inner_rec.clone();
7320 let inner = tree.add(
7321 StackWidget::new()
7322 .child(actor)
7323 .on_scroll(move |ev, _ctx| match ev {
7324 WidgetEvent::ScrollIntoView { align, .. } => {
7325 rec.set(Some(*align));
7326 EventResponse::Ignored
7327 }
7328 _ => EventResponse::Ignored,
7329 })
7330 .clips_children(true),
7331 );
7332 let _outer = recording_align_container(&mut tree, inner, outer_rec.clone());
7333 tree.layout(SizeProposal::exact(100.0, 100.0));
7334
7335 let mut ctx = EventContext::new();
7337 ctx.ensure_visible_aligned(
7338 Rect::new(10.0, 500.0, 20.0, 15.0),
7339 0.5,
7340 crate::event::ScrollMotion::Instant,
7341 );
7342 tree.collect_from_ctx(ctx, actor);
7343
7344 assert_eq!(
7345 inner_rec.get(),
7346 Some(crate::event::ScrollAlign::Fraction(0.5)),
7347 "the innermost scroller is offered the pin"
7348 );
7349 assert_eq!(
7350 outer_rec.get().map(|(a, _)| a),
7351 Some(crate::event::ScrollAlign::Minimal),
7352 "declining to move must not pass the pin outward"
7353 );
7354 }
7355
7356 #[test]
7357 fn ensure_widget_visible_uses_target_arena_bounds() {
7358 use std::cell::Cell;
7359 use std::rc::Rc;
7360 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
7361 let mut tree = WidgetTree::new();
7362 let target = tree.add(FillWidget::new());
7364 let rec = recorded.clone();
7365 let container = tree.add(
7366 BelowContainer {
7367 child: Some(target),
7368 offset: 500.0,
7369 }
7370 .on_scroll(move |ev, _ctx| match ev {
7371 WidgetEvent::ScrollIntoView { target_bounds, .. } => {
7372 rec.set(Some(*target_bounds));
7373 EventResponse::Handled
7374 }
7375 _ => EventResponse::Ignored,
7376 })
7377 .clips_children(true),
7378 );
7379 tree.layout(SizeProposal::exact(100.0, 100.0));
7380
7381 let expected = tree.bounds(target);
7382 assert!(
7383 expected.y > 100.0,
7384 "fixture sanity: the target must sit below the viewport (y={})",
7385 expected.y
7386 );
7387
7388 let mut ctx = EventContext::new();
7391 ctx.ensure_widget_visible(target);
7392 tree.collect_from_ctx(ctx, container);
7393
7394 assert_eq!(
7395 recorded.get(),
7396 Some(expected),
7397 "ensure_widget_visible(id) must dispatch ScrollIntoView with the \
7398 target's current arena bounds"
7399 );
7400 }
7401
7402 #[test]
7403 fn ensure_widget_visible_ignores_missing_widget() {
7404 use std::cell::Cell;
7406 use std::rc::Rc;
7407 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
7408 let mut tree = WidgetTree::new();
7409 let actor = tree.add(FillWidget::new());
7410 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
7411 tree.layout(SizeProposal::exact(100.0, 100.0));
7412
7413 let mut ctx = EventContext::new();
7414 ctx.ensure_widget_visible(WidgetId::default());
7415 tree.collect_from_ctx(ctx, actor); assert_eq!(
7418 recorded.get(),
7419 None,
7420 "an unmounted id must not trigger a scroll"
7421 );
7422 }
7423
7424 #[test]
7425 fn context_menu_inside_a_modal_keeps_the_modal() {
7426 use crate::event::{Modifiers, PointerButton, WidgetEvent};
7430 use crate::overlay::{DismissBehavior, OverlayPlacement, OverlayRequest};
7431 use crate::test_widgets::{FillWidget, StackWidget};
7432
7433 let mut tree = WidgetTree::new();
7434 let modal_content = tree.add(StackWidget::new());
7437 let _editor = tree.add_child(
7438 modal_content,
7439 FillWidget::new()
7440 .context_menu(|_pos, _ctx| Some(Box::new(FillWidget::new()) as Box<dyn Widget>)),
7441 );
7442 tree.layout(SizeProposal::exact(200.0, 100.0));
7443
7444 let modal = tree.overlay_manager.show(OverlayRequest::new(
7445 modal_content,
7446 modal_content,
7447 OverlayPlacement::Centered,
7448 DismissBehavior::EscapeKey,
7449 ));
7450 tree.overlay_manager
7452 .stack
7453 .iter_mut()
7454 .find(|o| o.id == modal)
7455 .unwrap()
7456 .bounds = Rect::new(0.0, 0.0, 200.0, 100.0);
7457 assert_eq!(tree.overlay_manager.len(), 1);
7458
7459 tree.dispatch_event(WidgetEvent::pointer_down(
7461 Point::new(50.0, 25.0),
7462 PointerButton::Secondary,
7463 Modifiers::NONE,
7464 ));
7465
7466 assert!(
7467 tree.overlay_manager.active_ids().contains(&modal),
7468 "the modal must survive opening a context menu inside it"
7469 );
7470 assert_eq!(
7471 tree.overlay_manager.len(),
7472 2,
7473 "the context menu should now be open on top of the surviving modal"
7474 );
7475 }
7476}
7477
7478#[cfg(test)]
7481mod input_ingress_tests {
7482 use super::*;
7483 use crate::event::{EventResponse, Modifiers, PointerButton, ScrollDelta};
7484 use crate::pointer::{
7485 EventTime, PointerId, PointerInfo, PointerPhase, PointerSample, ScrollPhase, ScrollSample,
7486 ScrollSource,
7487 };
7488 use crate::test_widgets::FillWidget;
7489 use crate::widget::{LayoutContext, WidgetPlacement};
7490 use crate::widget_builder::WidgetBuilder;
7491 use std::cell::RefCell;
7492 use std::rc::Rc;
7493
7494 #[derive(Debug)]
7501 struct RowWidget {
7502 children: Vec<WidgetId>,
7503 }
7504
7505 impl crate::widget::Widget for RowWidget {
7506 fn layout_response(
7507 &self,
7508 proposal: SizeProposal,
7509 _ctx: &LayoutContext,
7510 ) -> crate::widget::LayoutResponse {
7511 proposal.resolve(0.0, 0.0).into()
7512 }
7513
7514 fn place_children(
7515 &self,
7516 bounds: Rect,
7517 _proposal: SizeProposal,
7518 children: &mut [WidgetPlacement],
7519 _ctx: &LayoutContext,
7520 ) {
7521 let n = children.len().max(1) as f32;
7522 let w = bounds.width / n;
7523 for (i, child) in children.iter_mut().enumerate() {
7524 child.origin = Point::new(bounds.x + w * i as f32, bounds.y);
7525 child.size = teksilo_canvas::Size::new(w, bounds.height);
7526 }
7527 }
7528
7529 fn children(&self) -> Vec<WidgetId> {
7530 self.children.clone()
7531 }
7532 }
7533
7534 fn record(drive: impl FnOnce(&mut WidgetTree)) -> Vec<String> {
7537 let log: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(Vec::new()));
7538
7539 let mut tree = WidgetTree::new();
7540 let pointer_log = log.clone();
7541 let hover_log = log.clone();
7542 tree.add(
7543 FillWidget::new()
7544 .on_pointer_event(move |event, _ctx| {
7545 pointer_log.borrow_mut().push(format!("{event:?}"));
7546 EventResponse::Ignored
7547 })
7548 .on_hover(move |entered, _ctx| {
7549 hover_log.borrow_mut().push(format!("hover({entered})"));
7550 }),
7551 );
7552 tree.layout(SizeProposal::exact(100.0, 100.0));
7553
7554 drive(&mut tree);
7555 log.borrow().clone()
7556 }
7557
7558 #[test]
7564 fn a_mouse_sample_reproduces_todays_event_stream() {
7565 let inside = Point::new(40.0, 40.0);
7566 let moved = Point::new(60.0, 55.0);
7567 let outside = Point::new(400.0, 400.0);
7568
7569 let legacy = record(|tree| {
7570 tree.dispatch_event(WidgetEvent::pointer_move(inside));
7571 tree.dispatch_event(WidgetEvent::pointer_down(
7572 inside,
7573 PointerButton::Primary,
7574 Modifiers::NONE,
7575 ));
7576 tree.dispatch_event(WidgetEvent::pointer_move(moved));
7577 tree.dispatch_event(WidgetEvent::pointer_up(
7578 moved,
7579 PointerButton::Primary,
7580 Modifiers::NONE,
7581 ));
7582 tree.dispatch_event(WidgetEvent::pointer_move(outside));
7583 });
7584
7585 let sampled = record(|tree| {
7586 let t = EventTime::ZERO;
7587 tree.dispatch_pointer(PointerSample::mouse(PointerPhase::Move, inside, t));
7588 tree.dispatch_pointer(
7589 PointerSample::mouse(PointerPhase::Down, inside, t)
7590 .with_button(PointerButton::Primary),
7591 );
7592 tree.dispatch_pointer(PointerSample::mouse(PointerPhase::Move, moved, t));
7593 tree.dispatch_pointer(
7594 PointerSample::mouse(PointerPhase::Up, moved, t)
7595 .with_button(PointerButton::Primary),
7596 );
7597 tree.dispatch_pointer(PointerSample::mouse(PointerPhase::Move, outside, t));
7598 });
7599
7600 assert_eq!(legacy, sampled);
7601 assert!(
7602 legacy.contains(&"hover(true)".to_string())
7603 && legacy.contains(&"hover(false)".to_string()),
7604 "the fixture must actually exercise enter and leave: {legacy:?}"
7605 );
7606 }
7607
7608 #[test]
7612 fn a_buttonless_press_lowers_to_primary() {
7613 let events = record(|tree| {
7614 tree.dispatch_pointer(PointerSample::mouse(
7615 PointerPhase::Down,
7616 Point::new(20.0, 20.0),
7617 EventTime::ZERO,
7618 ));
7619 });
7620 assert!(
7621 events.iter().any(|e| e.contains("button: Primary")),
7622 "{events:?}"
7623 );
7624 }
7625
7626 #[test]
7632 fn a_sampled_press_and_release_form_a_tap() {
7633 let taps = Rc::new(std::cell::Cell::new(0));
7634 let seen = taps.clone();
7635 let mut tree = WidgetTree::new();
7636 tree.add(FillWidget::new().on_tap(move |_event, _ctx| seen.set(seen.get() + 1)));
7637 tree.layout(SizeProposal::exact(100.0, 100.0));
7638
7639 let at = Point::new(50.0, 50.0);
7640 tree.dispatch_pointer(PointerSample::mouse(
7641 PointerPhase::Down,
7642 at,
7643 EventTime::from_millis(0),
7644 ));
7645 tree.dispatch_pointer(PointerSample::mouse(
7646 PointerPhase::Up,
7647 at,
7648 EventTime::from_millis(5),
7649 ));
7650
7651 assert_eq!(taps.get(), 1);
7652 }
7653
7654 fn scroll_fixture() -> (WidgetTree, Rc<RefCell<Vec<&'static str>>>) {
7659 let hits: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
7660 let mut tree = WidgetTree::new();
7661
7662 let leading_hits = hits.clone();
7663 let trailing_hits = hits.clone();
7664 let leading = tree.add(FillWidget::new().on_scroll(move |_event, _ctx| {
7665 leading_hits.borrow_mut().push("leading");
7666 EventResponse::Handled
7667 }));
7668 let trailing = tree.add(FillWidget::new().on_scroll(move |_event, _ctx| {
7669 trailing_hits.borrow_mut().push("trailing");
7670 EventResponse::Handled
7671 }));
7672 tree.add(RowWidget {
7673 children: vec![leading, trailing],
7674 });
7675 tree.layout(SizeProposal::exact(200.0, 100.0));
7676 (tree, hits)
7677 }
7678
7679 fn notch() -> ScrollDelta {
7680 ScrollDelta::Lines { x: 0.0, y: -1.0 }
7681 }
7682
7683 #[test]
7686 fn a_positionless_scroll_routes_by_hover() {
7687 let (mut tree, hits) = scroll_fixture();
7688
7689 tree.pointer_move(Point::new(150.0, 50.0)); tree.dispatch_event(WidgetEvent::scroll(notch(), Modifiers::NONE));
7691 assert_eq!(*hits.borrow(), vec!["trailing"]);
7692
7693 tree.pointer_move(Point::new(50.0, 50.0)); tree.dispatch_scroll(ScrollSample::wheel(
7695 notch(),
7696 Modifiers::NONE,
7697 EventTime::ZERO,
7698 ));
7699 assert_eq!(*hits.borrow(), vec!["trailing", "leading"]);
7700 }
7701
7702 #[test]
7707 fn a_positioned_scroll_routes_by_hit_test() {
7708 let (mut tree, hits) = scroll_fixture();
7709 tree.pointer_move(Point::new(150.0, 50.0)); tree.dispatch_event(WidgetEvent::scroll_at(
7712 notch(),
7713 Modifiers::NONE,
7714 Point::new(50.0, 50.0), ));
7716 assert_eq!(*hits.borrow(), vec!["leading"]);
7717
7718 tree.dispatch_scroll(
7719 ScrollSample::wheel(notch(), Modifiers::NONE, EventTime::ZERO)
7720 .at(Point::new(150.0, 50.0)),
7721 );
7722 assert_eq!(*hits.borrow(), vec!["leading", "trailing"]);
7723 }
7724
7725 #[test]
7729 fn a_positionless_scroll_with_no_hover_goes_nowhere() {
7730 let (mut tree, hits) = scroll_fixture();
7731 tree.dispatch_event(WidgetEvent::scroll(notch(), Modifiers::NONE));
7732 assert!(hits.borrow().is_empty());
7733 }
7734
7735 #[test]
7740 fn a_handler_sees_the_sample_it_is_serving() {
7741 let seen: Rc<RefCell<Vec<(ScrollPhase, ScrollSource, Option<Point>)>>> =
7742 Rc::new(RefCell::new(Vec::new()));
7743 let sink = seen.clone();
7744
7745 let mut tree = WidgetTree::new();
7746 tree.add(
7753 FillWidget::new()
7754 .scroll_container(crate::pointer::touch_action::PanAxes::BOTH)
7755 .on_scroll(move |_event, ctx| {
7756 sink.borrow_mut().push((
7757 ctx.scroll_phase(),
7758 ctx.scroll_source(),
7759 ctx.pointer_position(),
7760 ));
7761 EventResponse::Handled
7762 }),
7763 );
7764 tree.layout(SizeProposal::exact(100.0, 100.0));
7765
7766 tree.dispatch_scroll(ScrollSample {
7767 delta: notch(),
7768 position: Some(Point::new(50.0, 50.0)),
7769 phase: ScrollPhase::Momentum,
7770 source: ScrollSource::TouchPan,
7771 pointer: PointerInfo::mouse(EventTime::from_millis(12)),
7772 modifiers: Modifiers::NONE,
7773 });
7774
7775 assert_eq!(
7776 *seen.borrow(),
7777 vec![(
7778 ScrollPhase::Momentum,
7779 ScrollSource::TouchPan,
7780 Some(Point::new(50.0, 50.0))
7781 )]
7782 );
7783 }
7784
7785 #[test]
7788 fn a_legacy_event_reports_the_mouse_at_the_epoch() {
7789 let seen: Rc<RefCell<Option<(PointerId, ScrollPhase)>>> = Rc::new(RefCell::new(None));
7790 let sink = seen.clone();
7791
7792 let mut tree = WidgetTree::new();
7793 tree.add(FillWidget::new().on_pointer_event(move |_event, ctx| {
7794 *sink.borrow_mut() = Some((ctx.pointer().id, ctx.scroll_phase()));
7795 EventResponse::Ignored
7796 }));
7797 tree.layout(SizeProposal::exact(100.0, 100.0));
7798 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(50.0, 50.0)));
7799
7800 assert_eq!(
7801 *seen.borrow(),
7802 Some((PointerId::MOUSE, ScrollPhase::Discrete))
7803 );
7804 }
7805
7806 #[test]
7810 fn the_snapshot_is_restored_after_a_dispatch() {
7811 let mut tree = WidgetTree::new();
7812 tree.add(FillWidget::new());
7813 tree.layout(SizeProposal::exact(100.0, 100.0));
7814
7815 let before = tree.current_input.clone();
7816 tree.dispatch_scroll(
7817 ScrollSample::wheel(notch(), Modifiers::NONE, EventTime::ZERO)
7818 .at(Point::new(10.0, 10.0)),
7819 );
7820 assert_eq!(tree.current_input, before);
7821 }
7822}
7823
7824#[cfg(test)]
7825mod press_and_focus_tests {
7826 use std::cell::{Cell, RefCell};
7831 use std::rc::Rc;
7832 use teksilo_canvas::{Point, SizeProposal};
7833
7834 use crate::WidgetId;
7835 use crate::event::{EventResponse, Key, Modifiers, PointerButton, WidgetEvent};
7836 use crate::focus::FocusOrigin;
7837 use crate::pointer::clock::ManualClock;
7838 use crate::pointer::touch_action::PanClaim;
7839 use crate::pointer::{
7840 BackendDeviceKey, EventTime, PointerId, PointerIdAllocator, PointerInfo, PointerPhase,
7841 PointerSample,
7842 };
7843 use crate::test_widgets::{FillWidget, StackWidget};
7844 use crate::widget_builder::WidgetBuilder;
7845 use crate::widget_tree::WidgetTree;
7846
7847 fn contact_id() -> PointerId {
7853 use std::sync::atomic::{AtomicU64, Ordering};
7854 static NEXT: AtomicU64 = AtomicU64::new(1);
7855 PointerIdAllocator::global().begin(
7856 BackendDeviceKey::new(0x0B11),
7857 NEXT.fetch_add(1, Ordering::Relaxed),
7858 )
7859 }
7860
7861 fn contact(id: PointerId, phase: PointerPhase, at: Point, t: EventTime) -> PointerSample {
7862 PointerSample {
7863 pointer: PointerInfo::touch(id, t),
7864 phase,
7865 position: at,
7866 button: None,
7867 modifiers: Modifiers::NONE,
7868 coalesced: Vec::new(),
7869 }
7870 }
7871
7872 fn tree_on_a_clock() -> (WidgetTree, Rc<ManualClock>) {
7875 let mut tree = WidgetTree::new();
7876 let clock = Rc::new(ManualClock::new(EventTime::ZERO));
7877 tree.set_input_clock(clock.clone());
7878 (tree, clock)
7879 }
7880
7881 fn tappable(tree: &mut WidgetTree) -> (WidgetId, crate::signal::Signal<bool>) {
7888 let id = tree.add(FillWidget::new().focusable().on_tap(|_e, _c| {}));
7889 let signal = tree.pressed_signal(id);
7890 (id, signal)
7891 }
7892
7893 #[test]
7900 fn a_pointer_origin_names_its_device() {
7901 use teksilo_tokens::{PenKind, PointerKind};
7902
7903 assert!(FocusOrigin::Pointer(PointerKind::Touch).is_pointer());
7904 assert!(FocusOrigin::Pointer(PointerKind::Pen(PenKind::Pen)).is_pointer());
7905 assert!(FocusOrigin::POINTER.is_pointer());
7906 assert!(!FocusOrigin::Keyboard.is_pointer());
7907 assert!(!FocusOrigin::Programmatic.is_pointer());
7908 assert!(!FocusOrigin::Accessibility.is_pointer());
7909
7910 assert_eq!(
7911 FocusOrigin::Pointer(PointerKind::Touch).pointer_kind(),
7912 Some(PointerKind::Touch),
7913 );
7914 assert_eq!(FocusOrigin::Keyboard.pointer_kind(), None);
7915 assert_eq!(
7916 FocusOrigin::POINTER.pointer_kind(),
7917 Some(PointerKind::Unknown),
7918 "a widget deriving its own origin says so rather than naming a device it never saw",
7919 );
7920 }
7921
7922 #[test]
7926 fn only_a_real_navigation_declares_a_modality() {
7927 use teksilo_tokens::PointerKind;
7928
7929 assert_eq!(FocusOrigin::Keyboard.focus_visible(), Some(true));
7930 assert_eq!(FocusOrigin::Accessibility.focus_visible(), Some(true));
7931 assert_eq!(
7932 FocusOrigin::Pointer(PointerKind::Touch).focus_visible(),
7933 Some(false),
7934 );
7935 assert_eq!(FocusOrigin::Programmatic.focus_visible(), None);
7936 }
7937
7938 #[test]
7941 fn the_tier_three_configs_are_unchanged() {
7942 let origin: crate::signal::Signal<Option<FocusOrigin>> =
7943 crate::signal::Signal::new(Some(FocusOrigin::Keyboard));
7944 let slider: crate::signal::Signal<Option<FocusOrigin>> = origin.clone();
7945 let splitter: crate::signal::Signal<Option<FocusOrigin>> = origin.clone();
7946 let segmented: crate::signal::Signal<Option<FocusOrigin>> = origin.clone();
7947 for field in [slider, splitter, segmented] {
7948 assert_eq!(field.get(), Some(FocusOrigin::Keyboard));
7949 }
7950 origin.set(Some(FocusOrigin::POINTER));
7951 assert_ne!(
7952 origin.get(),
7953 Some(FocusOrigin::Keyboard),
7954 "the `== Some(Keyboard)` test every consumer makes still discriminates",
7955 );
7956 }
7957
7958 #[test]
7969 fn a_touch_tap_after_keyboard_focus_leaves_no_ring() {
7970 let (mut tree, _clock) = tree_on_a_clock();
7971 let a = tree.add(FillWidget::new().focusable());
7972 let b = tree.add(FillWidget::new().focusable());
7973 let root = tree.add(SideBySide {
7974 children: vec![a, b],
7975 });
7976 tree.layout(SizeProposal::exact(200.0, 100.0));
7977 let _ = root;
7978
7979 let visible = tree.focus_visible_signal();
7980 tree.press_key(Key::Tab, Modifiers::NONE);
7981 assert_eq!(tree.focused(), Some(a));
7982 assert!(visible.get(), "keyboard navigation reveals the ring");
7983
7984 let id = contact_id();
7985 let at = tree.bounds(b).center();
7986 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
7987 tree.dispatch_pointer(contact(
7988 id,
7989 PointerPhase::Up,
7990 at,
7991 EventTime::from_millis(30),
7992 ));
7993
7994 assert_eq!(tree.focused(), Some(b), "the release moved focus");
7995 assert!(!visible.get(), "and the ring did not come with it");
7996 assert_eq!(
7997 tree.focus_origin(),
7998 Some(FocusOrigin::Pointer(teksilo_tokens::PointerKind::Touch)),
7999 );
8000 }
8001
8002 #[test]
8007 fn an_assistive_focus_reveals_the_ring() {
8008 let mut tree = WidgetTree::new();
8009 let a = tree.add(FillWidget::new().focusable());
8010 let b = tree.add(FillWidget::new().focusable());
8011 let root = tree.add(SideBySide {
8012 children: vec![a, b],
8013 });
8014 tree.layout(SizeProposal::exact(200.0, 50.0));
8015 let _ = root;
8016 let visible = tree.focus_visible_signal();
8017
8018 tree.click(a);
8019 assert_eq!(tree.focused(), Some(a));
8020 assert!(!visible.get(), "the click hid it");
8021
8022 tree.dispatch_event(WidgetEvent::AccessAction {
8023 target: Some(b),
8024 action: accesskit::Action::Focus,
8025 target_node: crate::accessibility::widget_id_to_node_id(b),
8026 data: None,
8027 });
8028 assert_eq!(tree.focused(), Some(b));
8029 assert!(visible.get(), "an assistive move is a navigation");
8030 assert_eq!(tree.focus_origin(), Some(FocusOrigin::Accessibility));
8031 }
8032
8033 #[test]
8038 fn a_programmatic_focus_leaves_the_modality_alone() {
8039 let mut tree = WidgetTree::new();
8040 let a = tree.add(FillWidget::new().focusable());
8041 let b = tree.add(FillWidget::new().focusable());
8042 let root = tree.add(SideBySide {
8043 children: vec![a, b],
8044 });
8045 tree.layout(SizeProposal::exact(200.0, 100.0));
8046 let _ = root;
8047 let visible = tree.focus_visible_signal();
8048
8049 tree.press_key(Key::Tab, Modifiers::NONE);
8050 assert_eq!(tree.focused(), Some(a));
8051 assert!(visible.get());
8052 tree.focus(b);
8053 assert!(
8054 visible.get(),
8055 "a scripted focus does not hide a keyboard ring"
8056 );
8057
8058 tree.click(a);
8059 assert!(!visible.get());
8060 tree.focus(b);
8061 assert!(
8062 !visible.get(),
8063 "and does not reveal one after a click either",
8064 );
8065 }
8066
8067 #[test]
8073 fn a_mouse_still_focuses_on_press() {
8074 let mut tree = WidgetTree::new();
8075 let w = tree.add(FillWidget::new().focusable());
8076 tree.layout(SizeProposal::exact(100.0, 50.0));
8077 let center = tree.bounds(w).center();
8078
8079 tree.dispatch_event(WidgetEvent::pointer_down(
8080 center,
8081 PointerButton::Primary,
8082 Modifiers::NONE,
8083 ));
8084 assert_eq!(
8085 tree.focused(),
8086 Some(w),
8087 "focus lands on the press for an indirect pointer",
8088 );
8089 }
8090
8091 #[test]
8093 fn a_finger_focuses_on_release() {
8094 let (mut tree, _clock) = tree_on_a_clock();
8095 let w = tree.add(FillWidget::new().focusable());
8096 tree.layout(SizeProposal::exact(100.0, 50.0));
8097 let center = tree.bounds(w).center();
8098
8099 let id = contact_id();
8100 tree.dispatch_pointer(contact(id, PointerPhase::Down, center, EventTime::ZERO));
8101 assert_eq!(
8102 tree.focused(),
8103 None,
8104 "a finger that has only landed has chosen nothing yet",
8105 );
8106
8107 tree.dispatch_pointer(contact(
8108 id,
8109 PointerPhase::Up,
8110 center,
8111 EventTime::from_millis(40),
8112 ));
8113 assert_eq!(tree.focused(), Some(w));
8114 assert_eq!(
8115 tree.focus_origin().and_then(FocusOrigin::pointer_kind),
8116 Some(teksilo_tokens::PointerKind::Touch),
8117 "the origin names the device that delivered it",
8118 );
8119 }
8120
8121 #[test]
8125 fn a_release_on_a_different_focusable_moves_no_focus() {
8126 let (mut tree, _clock) = tree_on_a_clock();
8127 let a = tree.add(FillWidget::new().focusable());
8128 let b = tree.add(FillWidget::new().focusable());
8129 let root = tree.add(SideBySide {
8130 children: vec![a, b],
8131 });
8132 tree.layout(SizeProposal::exact(200.0, 50.0));
8133 let _ = root;
8134
8135 let on_a = tree.bounds(a).center();
8136 let on_b = tree.bounds(b).center();
8137 let id = contact_id();
8138 tree.dispatch_pointer(contact(id, PointerPhase::Down, on_a, EventTime::ZERO));
8139 tree.dispatch_pointer(contact(
8140 id,
8141 PointerPhase::Move,
8142 on_b,
8143 EventTime::from_millis(20),
8144 ));
8145 tree.dispatch_pointer(contact(
8146 id,
8147 PointerPhase::Up,
8148 on_b,
8149 EventTime::from_millis(40),
8150 ));
8151
8152 assert_eq!(
8153 tree.focused(),
8154 None,
8155 "the guard is `the release landed on the same focusable as the press`",
8156 );
8157 }
8158
8159 #[test]
8166 fn a_mouse_press_visual_is_unchanged() {
8167 let mut tree = WidgetTree::new();
8168 let (w, pressed) = tappable(&mut tree);
8169 tree.layout(SizeProposal::exact(100.0, 50.0));
8170 let center = tree.bounds(w).center();
8171
8172 assert!(!pressed.get(), "not pressed at rest");
8173 tree.dispatch_event(WidgetEvent::pointer_down(
8174 center,
8175 PointerButton::Primary,
8176 Modifiers::NONE,
8177 ));
8178 assert!(pressed.get(), "an indirect pointer lights up on the press");
8179 assert!(
8180 !tree.press_pending(w),
8181 "and never waits: a mouse opens no pan session, so there is no \
8182 ambiguity to wait out",
8183 );
8184 assert_eq!(tree.pressed_by(w), Some(PointerId::MOUSE));
8185
8186 tree.dispatch_event(WidgetEvent::pointer_up(
8187 center,
8188 PointerButton::Primary,
8189 Modifiers::NONE,
8190 ));
8191 assert!(!pressed.get(), "the release clears it");
8192 assert_eq!(tree.pressed_by(w), None);
8193 }
8194
8195 #[test]
8199 fn a_slide_off_clears_the_visual_and_re_entry_restores_it() {
8200 let (mut tree, _clock) = tree_on_a_clock();
8201 let (w, pressed) = tappable(&mut tree);
8202 tree.layout(SizeProposal::exact(100.0, 200.0));
8203 let inside = tree.bounds(w).center();
8204 let outside = Point::new(inside.x, inside.y + 400.0);
8205
8206 let id = contact_id();
8207 tree.dispatch_pointer(contact(id, PointerPhase::Down, inside, EventTime::ZERO));
8208 assert!(
8209 pressed.get(),
8210 "nothing here claims a pan, so no delay applies"
8211 );
8212
8213 tree.dispatch_pointer(contact(
8214 id,
8215 PointerPhase::Move,
8216 outside,
8217 EventTime::from_millis(20),
8218 ));
8219 assert!(!pressed.get(), "the press has left its target");
8220 assert!(!tree.press_is_inside(w));
8221 assert_eq!(
8222 tree.pressed_by(w),
8223 Some(id),
8224 "the contact still holds the press — it is the *visual* that is off",
8225 );
8226
8227 tree.dispatch_pointer(contact(
8228 id,
8229 PointerPhase::Move,
8230 inside,
8231 EventTime::from_millis(40),
8232 ));
8233 assert!(pressed.get(), "sliding back on restores it");
8234
8235 tree.dispatch_pointer(contact(
8236 id,
8237 PointerPhase::Up,
8238 inside,
8239 EventTime::from_millis(60),
8240 ));
8241 assert!(!pressed.get());
8242 }
8243
8244 #[test]
8247 fn a_second_contact_cannot_clear_the_first_visual() {
8248 let (mut tree, _clock) = tree_on_a_clock();
8249 let (w, pressed) = tappable(&mut tree);
8250 tree.layout(SizeProposal::exact(100.0, 50.0));
8251 let bounds = tree.bounds(w);
8252 let first_at = Point::new(bounds.x + 20.0, bounds.y + 25.0);
8253 let second_at = Point::new(bounds.x + 70.0, bounds.y + 25.0);
8254
8255 let first = contact_id();
8256 let second = contact_id();
8257 tree.dispatch_pointer(contact(
8258 first,
8259 PointerPhase::Down,
8260 first_at,
8261 EventTime::ZERO,
8262 ));
8263 assert!(pressed.get());
8264 assert_eq!(tree.pressed_by(w), Some(first));
8265
8266 tree.dispatch_pointer(contact(
8267 second,
8268 PointerPhase::Down,
8269 second_at,
8270 EventTime::from_millis(10),
8271 ));
8272 assert_eq!(
8273 tree.pressed_by(w),
8274 Some(first),
8275 "under `MultiContact::First` the second contact is terminated before \
8276 it reaches the arena at all",
8277 );
8278
8279 tree.dispatch_pointer(contact(
8280 second,
8281 PointerPhase::Up,
8282 second_at,
8283 EventTime::from_millis(20),
8284 ));
8285 assert!(
8286 pressed.get(),
8287 "so the second contact's release cannot clear a visual it never owned",
8288 );
8289 assert_eq!(tree.pressed_by(w), Some(first));
8290
8291 tree.dispatch_pointer(contact(
8292 first,
8293 PointerPhase::Up,
8294 first_at,
8295 EventTime::from_millis(30),
8296 ));
8297 assert!(!pressed.get(), "the owner's release does clear it");
8298 }
8299
8300 #[test]
8303 fn a_cancel_clears_the_visual() {
8304 let (mut tree, _clock) = tree_on_a_clock();
8305 let (w, pressed) = tappable(&mut tree);
8306 tree.layout(SizeProposal::exact(100.0, 50.0));
8307 let center = tree.bounds(w).center();
8308
8309 let id = contact_id();
8310 tree.dispatch_pointer(contact(id, PointerPhase::Down, center, EventTime::ZERO));
8311 assert!(pressed.get());
8312
8313 tree.dispatch_pointer(contact(
8314 id,
8315 PointerPhase::Cancel,
8316 center,
8317 EventTime::from_millis(20),
8318 ));
8319 assert!(!pressed.get(), "a press that was taken away is not painted");
8320 assert_eq!(tree.pressed_by(w), None);
8321 }
8322
8323 #[test]
8326 fn a_pan_claim_clears_the_visual() {
8327 let (mut tree, clock) = tree_on_a_clock();
8328 let (row, pressed) = tappable(&mut tree);
8329 let list = tree.add(
8330 StackWidget::new()
8331 .child(row)
8332 .pan_claim(PanClaim::vertical())
8333 .on_scroll(|_e, _c| EventResponse::Handled),
8334 );
8335 tree.layout(SizeProposal::exact(200.0, 400.0));
8336 let _ = list;
8337 let start = Point::new(100.0, 200.0);
8338
8339 let id = contact_id();
8340 tree.dispatch_pointer(contact(id, PointerPhase::Down, start, EventTime::ZERO));
8341 assert!(tree.press_pending(row));
8343 clock.set(EventTime::from_millis(100));
8344 tree.tick_gestures(std::time::Instant::now());
8345 assert!(pressed.get(), "…and appears once the delay has elapsed");
8346
8347 tree.dispatch_pointer(contact(
8349 id,
8350 PointerPhase::Move,
8351 Point::new(100.0, 260.0),
8352 EventTime::from_millis(120),
8353 ));
8354 assert!(
8355 !pressed.get(),
8356 "the row lost the press to the list and must stop advertising it",
8357 );
8358 assert_eq!(tree.pressed_by(row), None);
8359 }
8360
8361 #[test]
8370 fn a_button_the_control_cannot_act_on_raises_no_visual() {
8371 let taps = Rc::new(Cell::new(0u32));
8372 let count = taps.clone();
8373 let mut tree = WidgetTree::new();
8374 let w = tree.add(FillWidget::new().focusable().on_tap(move |_e, _c| {
8375 count.set(count.get() + 1);
8376 }));
8377 let pressed = tree.pressed_signal(w);
8378 tree.layout(SizeProposal::exact(100.0, 50.0));
8379 let center = tree.bounds(w).center();
8380
8381 for button in [
8382 PointerButton::Middle,
8383 PointerButton::Back,
8384 PointerButton::Forward,
8385 ] {
8386 tree.dispatch_event(WidgetEvent::pointer_down(center, button, Modifiers::NONE));
8387 assert!(
8388 !pressed.get(),
8389 "{button:?} cannot activate an `on_tap` node, so it must not light one up",
8390 );
8391 assert_eq!(tree.pressed_by(w), None, "and owns no visual to clear");
8392 tree.dispatch_event(WidgetEvent::pointer_up(center, button, Modifiers::NONE));
8393 assert_eq!(
8394 taps.get(),
8395 0,
8396 "the tap recognizer refuses {button:?} too — that is the point",
8397 );
8398 }
8399
8400 tree.dispatch_event(WidgetEvent::pointer_down(
8402 center,
8403 PointerButton::Primary,
8404 Modifiers::NONE,
8405 ));
8406 assert!(pressed.get(), "a primary press lights up as it always has");
8407 assert_eq!(tree.pressed_by(w), Some(PointerId::MOUSE));
8408 tree.dispatch_event(WidgetEvent::pointer_up(
8409 center,
8410 PointerButton::Primary,
8411 Modifiers::NONE,
8412 ));
8413 assert!(!pressed.get());
8414 assert_eq!(taps.get(), 1);
8415 }
8416
8417 #[test]
8421 fn a_secondary_press_with_no_context_menu_raises_no_visual() {
8422 let mut tree = WidgetTree::new();
8423 let (w, pressed) = tappable(&mut tree);
8424 tree.layout(SizeProposal::exact(100.0, 50.0));
8425 let center = tree.bounds(w).center();
8426
8427 tree.dispatch_event(WidgetEvent::pointer_down(
8428 center,
8429 PointerButton::Secondary,
8430 Modifiers::NONE,
8431 ));
8432 assert!(
8433 !pressed.get(),
8434 "nothing opened, and nothing may look pressed either",
8435 );
8436 assert_eq!(tree.pressed_by(w), None);
8437 assert!(!tree.press_is_inside(w));
8438 }
8439
8440 #[test]
8444 fn a_widened_mask_widens_the_visual_with_it() {
8445 use crate::event::ButtonMask;
8446
8447 let mut tree = WidgetTree::new();
8448 let w = tree.add(
8449 FillWidget::new()
8450 .on_tap(|_e, _c| {})
8451 .accept_tap_buttons(ButtonMask::PRIMARY | ButtonMask::MIDDLE),
8452 );
8453 let pressed = tree.pressed_signal(w);
8454 tree.layout(SizeProposal::exact(100.0, 50.0));
8455 let center = tree.bounds(w).center();
8456
8457 tree.dispatch_event(WidgetEvent::pointer_down(
8458 center,
8459 PointerButton::Middle,
8460 Modifiers::NONE,
8461 ));
8462 assert!(pressed.get(), "this node really does act on a middle-click");
8463
8464 tree.dispatch_event(WidgetEvent::pointer_up(
8465 center,
8466 PointerButton::Middle,
8467 Modifiers::NONE,
8468 ));
8469 assert!(!pressed.get());
8470 }
8471
8472 #[test]
8478 fn a_press_that_raises_no_visual_still_defers_its_focus() {
8479 use teksilo_tokens::{PenKind, PointerKind};
8480
8481 let (mut tree, _clock) = tree_on_a_clock();
8482 let (w, pressed) = tappable(&mut tree);
8483 tree.layout(SizeProposal::exact(100.0, 50.0));
8484 let center = tree.bounds(w).center();
8485
8486 let id = contact_id();
8487 let barrel = |phase, t| PointerSample {
8488 pointer: PointerInfo {
8489 kind: PointerKind::Pen(PenKind::Pen),
8490 ..PointerInfo::touch(id, t)
8491 },
8492 phase,
8493 position: center,
8494 button: Some(PointerButton::Secondary),
8495 modifiers: Modifiers::NONE,
8496 coalesced: Vec::new(),
8497 };
8498
8499 tree.dispatch_pointer(barrel(PointerPhase::Down, EventTime::ZERO));
8500 assert!(!pressed.get(), "the barrel button activates nothing here");
8501 assert_eq!(tree.pressed_by(w), None);
8502 assert_eq!(
8503 tree.focused(),
8504 None,
8505 "a direct pointer has chosen nothing until it lifts",
8506 );
8507
8508 tree.dispatch_pointer(barrel(PointerPhase::Up, EventTime::from_millis(40)));
8509 assert_eq!(
8510 tree.focused(),
8511 Some(w),
8512 "the deferral is not button-gated: the release landed where the press did",
8513 );
8514 assert_eq!(
8515 tree.focus_origin().and_then(FocusOrigin::pointer_kind),
8516 Some(PointerKind::Pen(PenKind::Pen)),
8517 );
8518 }
8519
8520 #[test]
8527 fn the_feedback_delay_applies_only_inside_a_claimant() {
8528 let (mut tree, _clock) = tree_on_a_clock();
8530 let (w, pressed) = tappable(&mut tree);
8531 tree.layout(SizeProposal::exact(200.0, 400.0));
8532 let id = contact_id();
8533 let at = tree.bounds(w).center();
8534 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8535 assert!(
8536 !tree.press_pending(w),
8537 "no claimant above it, so nothing to rule out",
8538 );
8539 assert!(pressed.get());
8540
8541 let (mut tree, clock) = tree_on_a_clock();
8543 let (row, pressed) = tappable(&mut tree);
8544 let list = tree.add(
8545 StackWidget::new()
8546 .child(row)
8547 .pan_claim(PanClaim::vertical())
8548 .on_scroll(|_e, _c| EventResponse::Handled),
8549 );
8550 tree.layout(SizeProposal::exact(200.0, 400.0));
8551 let _ = list;
8552 let id = contact_id();
8553 let at = Point::new(100.0, 200.0);
8554 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8555 assert!(tree.press_pending(row), "a finger might be about to scroll");
8556 assert!(!pressed.get(), "so the row does not flash");
8557 assert!(
8558 tree.press_is_inside(row),
8559 "the press is real; only its visual is being withheld",
8560 );
8561
8562 clock.set(EventTime::from_millis(99));
8563 tree.tick_gestures(std::time::Instant::now());
8564 assert!(!pressed.get(), "99 ms is under the 100 ms delay");
8565
8566 clock.set(EventTime::from_millis(100));
8567 tree.tick_gestures(std::time::Instant::now());
8568 assert!(pressed.get(), "and 100 ms is it");
8569 assert!(!tree.press_pending(row));
8570 }
8571
8572 #[test]
8575 fn a_mouse_inside_a_claimant_never_waits() {
8576 let mut tree = WidgetTree::new();
8577 let (row, pressed) = tappable(&mut tree);
8578 let list = tree.add(
8579 StackWidget::new()
8580 .child(row)
8581 .pan_claim(PanClaim::vertical())
8582 .on_scroll(|_e, _c| EventResponse::Handled),
8583 );
8584 tree.layout(SizeProposal::exact(200.0, 400.0));
8585 let _ = list;
8586
8587 tree.dispatch_event(WidgetEvent::pointer_down(
8588 Point::new(100.0, 200.0),
8589 PointerButton::Primary,
8590 Modifiers::NONE,
8591 ));
8592 assert!(!tree.press_pending(row));
8593 assert!(pressed.get(), "Compact with a mouse is exactly as it was");
8594 }
8595
8596 #[test]
8603 fn a_handler_can_read_the_press_it_is_inside() {
8604 let (mut tree, _clock) = tree_on_a_clock();
8605 let seen: Rc<RefCell<Vec<(bool, bool, bool)>>> = Rc::new(RefCell::new(Vec::new()));
8606 let log = seen.clone();
8607 let row = tree.add(FillWidget::new().on_tap(|_e, _c| {}).on_pointer_event(
8608 move |event, ctx| {
8609 if matches!(event, WidgetEvent::PointerUp { .. }) {
8610 log.borrow_mut().push((
8611 ctx.is_pressed(),
8612 ctx.press_is_inside(),
8613 ctx.press_pending(),
8614 ));
8615 }
8616 EventResponse::Ignored
8617 },
8618 ));
8619 let list = tree.add(
8620 StackWidget::new()
8621 .child(row)
8622 .pan_claim(PanClaim::vertical())
8623 .on_scroll(|_e, _c| EventResponse::Handled),
8624 );
8625 tree.layout(SizeProposal::exact(200.0, 400.0));
8626 let _ = list;
8627
8628 let id = contact_id();
8629 let at = Point::new(100.0, 200.0);
8630 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8631 tree.dispatch_pointer(contact(
8632 id,
8633 PointerPhase::Up,
8634 at,
8635 EventTime::from_millis(30),
8636 ));
8637
8638 assert_eq!(
8639 seen.borrow().as_slice(),
8640 &[(false, true, true)],
8641 "released inside the claimant before the delay elapsed: inside, \
8642 pending, and therefore not yet showing",
8643 );
8644 }
8645
8646 #[test]
8649 fn a_handler_outside_a_press_reads_nothing() {
8650 let mut tree = WidgetTree::new();
8651 let asked = Rc::new(Cell::new(false));
8652 let flag = asked.clone();
8653 let w = tree.add(FillWidget::new().focusable().on_key(move |_e, ctx| {
8654 flag.set(ctx.is_pressed() || ctx.press_is_inside() || ctx.press_pending());
8655 EventResponse::Ignored
8656 }));
8657 tree.layout(SizeProposal::exact(100.0, 50.0));
8658 tree.focus(w);
8659 tree.press_key(Key::ArrowDown, Modifiers::NONE);
8660 assert!(!asked.get());
8661 }
8662
8663 #[test]
8667 fn a_completed_press_leaves_the_detector_clean() {
8668 let (mut tree, _clock) = tree_on_a_clock();
8669 let (w, pressed) = tappable(&mut tree);
8670 tree.layout(SizeProposal::exact(100.0, 50.0));
8671 let at = tree.bounds(w).center();
8672
8673 let id = contact_id();
8674 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8675 assert!(pressed.get());
8676 tree.dispatch_pointer(contact(
8677 id,
8678 PointerPhase::Up,
8679 at,
8680 EventTime::from_millis(30),
8681 ));
8682 tree.assert_no_leaked_pointer_state();
8683 }
8684
8685 #[test]
8689 fn the_detector_reports_a_press_still_held() {
8690 let (mut tree, _clock) = tree_on_a_clock();
8691 let (w, _pressed) = tappable(&mut tree);
8692 tree.layout(SizeProposal::exact(100.0, 50.0));
8693 let at = tree.bounds(w).center();
8694
8695 let id = contact_id();
8696 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8697 let held = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
8698 tree.assert_no_leaked_pointer_state()
8699 }));
8700 let message = *held
8701 .expect_err("a live press is leaked state")
8702 .downcast::<String>()
8703 .expect("the detector panics with a message");
8704 assert!(
8705 message.contains("is still pressed by"),
8706 "the press is named in the report, got: {message}",
8707 );
8708 }
8709
8710 #[derive(Debug)]
8714 struct SideBySide {
8715 children: Vec<WidgetId>,
8716 }
8717
8718 impl crate::widget::Widget for SideBySide {
8719 fn layout_response(
8720 &self,
8721 proposal: SizeProposal,
8722 _ctx: &crate::widget::LayoutContext,
8723 ) -> crate::widget::LayoutResponse {
8724 proposal.resolve(0.0, 0.0).into()
8725 }
8726
8727 fn place_children(
8728 &self,
8729 bounds: teksilo_canvas::Rect,
8730 _proposal: SizeProposal,
8731 children: &mut [crate::widget::WidgetPlacement],
8732 _ctx: &crate::widget::LayoutContext,
8733 ) {
8734 let n = children.len().max(1) as f32;
8735 let w = bounds.width / n;
8736 for (i, child) in children.iter_mut().enumerate() {
8737 child.origin = Point::new(bounds.x + w * i as f32, bounds.y);
8738 child.size = teksilo_canvas::Size::new(w, bounds.height);
8739 }
8740 }
8741
8742 fn children(&self) -> Vec<WidgetId> {
8743 self.children.clone()
8744 }
8745 }
8746}
8747
8748#[cfg(test)]
8749mod overlay_release_dismissal_tests {
8750 use std::cell::Cell;
8760 use std::rc::Rc;
8761
8762 use teksilo_canvas::{Point, Rect, Size, SizeProposal};
8763
8764 use crate::WidgetId;
8765 use crate::event::{Modifiers, PointerButton, WidgetEvent};
8766 use crate::overlay::{DismissBehavior, OverlayPlacement, OverlayRequest};
8767 use crate::pointer::{
8768 BackendDeviceKey, EventTime, PointerId, PointerIdAllocator, PointerInfo, PointerPhase,
8769 PointerSample,
8770 };
8771 use crate::test_widgets::FillWidget;
8772 use crate::widget::{LayoutContext, LayoutResponse, Widget};
8773 use crate::widget_builder::WidgetBuilder;
8774 use crate::widget_tree::WidgetTree;
8775
8776 #[derive(Debug)]
8783 struct Panel(Size);
8784
8785 impl Widget for Panel {
8786 fn layout_response(&self, _proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
8787 self.0.into()
8788 }
8789 }
8790
8791 #[derive(Debug)]
8799 struct PageAndTrigger {
8800 children: Vec<WidgetId>,
8801 }
8802
8803 impl Widget for PageAndTrigger {
8804 fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
8805 proposal.resolve(0.0, 0.0).into()
8806 }
8807
8808 fn place_children(
8809 &self,
8810 bounds: Rect,
8811 _proposal: SizeProposal,
8812 children: &mut [crate::widget::WidgetPlacement],
8813 _ctx: &LayoutContext,
8814 ) {
8815 if let Some(page) = children.get_mut(0) {
8816 page.origin = bounds.origin();
8817 page.size = bounds.size();
8818 }
8819 if let Some(trigger) = children.get_mut(1) {
8820 trigger.origin = bounds.origin();
8821 trigger.size = Size::new(10.0, 10.0);
8822 }
8823 }
8824
8825 fn children(&self) -> Vec<WidgetId> {
8826 self.children.clone()
8827 }
8828 }
8829
8830 fn contact_id(n: u64) -> PointerId {
8831 PointerIdAllocator::global().begin(BackendDeviceKey::new(0x0FA2), n)
8832 }
8833
8834 fn touch(id: PointerId, phase: PointerPhase, at: Point) -> PointerSample {
8835 PointerSample {
8836 pointer: PointerInfo::touch(id, EventTime::ZERO),
8837 phase,
8838 position: at,
8839 button: None,
8840 modifiers: Modifiers::NONE,
8841 coalesced: Vec::new(),
8842 }
8843 }
8844
8845 fn page_with_a_menu() -> (WidgetTree, WidgetId, Rc<Cell<u32>>) {
8852 let mut tree = WidgetTree::new();
8853 let taps = Rc::new(Cell::new(0u32));
8854 let counter = taps.clone();
8855 let page = tree.add(
8856 FillWidget::new()
8857 .focusable()
8858 .on_tap(move |_e, _c| counter.set(counter.get() + 1)),
8859 );
8860 let trigger = tree.add(Panel(Size::new(10.0, 10.0)));
8863 let _root = tree.add(PageAndTrigger {
8864 children: vec![page, trigger],
8865 });
8866 let menu = tree.add(Panel(Size::new(200.0, 200.0)));
8867 tree.show_overlay(OverlayRequest::new(
8868 menu,
8869 trigger,
8870 OverlayPlacement::AtPointer(Point::new(100.0, 100.0)),
8871 DismissBehavior::ClickOutside,
8872 ));
8873 tree.layout(SizeProposal::exact(800.0, 600.0));
8874 assert_eq!(tree.active_overlays().len(), 1, "the menu is open");
8875 (tree, page, taps)
8876 }
8877
8878 fn outside() -> Point {
8879 Point::new(600.0, 500.0)
8880 }
8881
8882 fn inside_menu() -> Point {
8883 Point::new(150.0, 150.0)
8884 }
8885
8886 #[test]
8893 fn the_suppressed_down_leaves_nothing_pressed_or_captured_beneath() {
8894 let (mut tree, page, taps) = page_with_a_menu();
8895 let finger = contact_id(1);
8896 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8897
8898 assert_eq!(
8899 tree.active_overlays().len(),
8900 1,
8901 "the press does not close the menu; the release does"
8902 );
8903 assert_eq!(tree.pressed_by(page), None, "nothing beneath is pressed");
8904 let entry = tree.pointers.get(finger).expect("the contact is live");
8905 assert_eq!(entry.captured_by, None, "nothing beneath captured it");
8906 assert!(entry.sequence.is_none(), "no arbitration was opened");
8907 assert_eq!(taps.get(), 0);
8908 }
8909
8910 #[test]
8912 fn a_touch_tap_outside_a_menu_closes_it_and_actuates_nothing() {
8913 let (mut tree, page, taps) = page_with_a_menu();
8914 let finger = contact_id(2);
8915 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8916 tree.dispatch_pointer(touch(finger, PointerPhase::Up, outside()));
8917
8918 assert!(tree.active_overlays().is_empty(), "the menu closed");
8919 assert_eq!(taps.get(), 0, "the page beneath was never tapped");
8920 assert_eq!(tree.pressed_by(page), None);
8921 tree.assert_no_leaked_pointer_state();
8922 }
8923
8924 #[test]
8927 fn a_cancelled_press_aborts_the_arm() {
8928 let (mut tree, page, taps) = page_with_a_menu();
8929 let finger = contact_id(3);
8930 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8931 tree.dispatch_pointer(touch(finger, PointerPhase::Cancel, outside()));
8932
8933 assert_eq!(tree.active_overlays().len(), 1, "the menu survives");
8934 assert_eq!(taps.get(), 0);
8935 assert_eq!(tree.pressed_by(page), None);
8936 tree.assert_no_leaked_pointer_state();
8937 }
8938
8939 #[test]
8943 fn a_press_slid_onto_the_menu_dismisses_nothing() {
8944 let (mut tree, _page, taps) = page_with_a_menu();
8945 let finger = contact_id(4);
8946 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8947 tree.dispatch_pointer(touch(finger, PointerPhase::Move, inside_menu()));
8948 tree.dispatch_pointer(touch(finger, PointerPhase::Up, inside_menu()));
8949
8950 assert_eq!(tree.active_overlays().len(), 1, "the menu survives");
8951 assert_eq!(taps.get(), 0);
8952 tree.assert_no_leaked_pointer_state();
8953 }
8954
8955 #[test]
8958 fn a_second_contact_cannot_dismiss_what_the_first_is_manipulating() {
8959 let (mut tree, _page, _taps) = page_with_a_menu();
8960 let first = contact_id(5);
8961 let second = contact_id(6);
8962
8963 tree.dispatch_pointer(touch(first, PointerPhase::Down, inside_menu()));
8964 assert!(
8965 tree.pointers
8966 .get(first)
8967 .is_some_and(|e| e.sequence.is_some()),
8968 "the first contact holds a live press inside the menu"
8969 );
8970
8971 tree.dispatch_pointer(touch(second, PointerPhase::Down, outside()));
8972 tree.dispatch_pointer(touch(second, PointerPhase::Up, outside()));
8973 assert_eq!(
8974 tree.active_overlays().len(),
8975 1,
8976 "the menu the first finger is holding must not close under it"
8977 );
8978
8979 tree.dispatch_pointer(touch(first, PointerPhase::Up, inside_menu()));
8982 let third = contact_id(7);
8983 tree.dispatch_pointer(touch(third, PointerPhase::Down, outside()));
8984 tree.dispatch_pointer(touch(third, PointerPhase::Up, outside()));
8985 assert!(tree.active_overlays().is_empty());
8986 tree.assert_no_leaked_pointer_state();
8987 }
8988
8989 #[test]
8993 fn an_arm_is_not_itself_a_press_that_blocks_another_contact() {
8994 let (mut tree, _page, _taps) = page_with_a_menu();
8995 let first = contact_id(8);
8996 let second = contact_id(9);
8997
8998 tree.dispatch_pointer(touch(first, PointerPhase::Down, outside()));
8999 assert!(
9000 tree.overlay_manager().has_armed_dismiss(first),
9001 "the first contact armed"
9002 );
9003 assert!(tree.busy_press_points(second).is_empty());
9005
9006 tree.dispatch_pointer(touch(second, PointerPhase::Down, outside()));
9007 assert!(tree.overlay_manager().has_armed_dismiss(second));
9008 tree.dispatch_pointer(touch(second, PointerPhase::Up, outside()));
9009 assert!(tree.active_overlays().is_empty());
9010
9011 tree.dispatch_pointer(touch(first, PointerPhase::Up, outside()));
9014 tree.assert_no_leaked_pointer_state();
9015 }
9016
9017 #[test]
9020 fn a_touch_inside_the_menu_arms_nothing() {
9021 let (mut tree, _page, _taps) = page_with_a_menu();
9022 let finger = contact_id(10);
9023 tree.dispatch_pointer(touch(finger, PointerPhase::Down, inside_menu()));
9024 assert!(!tree.overlay_manager().has_armed_dismiss(finger));
9025 tree.dispatch_pointer(touch(finger, PointerPhase::Up, inside_menu()));
9026 assert_eq!(tree.active_overlays().len(), 1);
9027 tree.assert_no_leaked_pointer_state();
9028 }
9029
9030 #[test]
9040 fn a_contact_with_no_press_still_ends_when_the_platform_revokes_it() {
9041 let mut tree = WidgetTree::new();
9042 tree.layout(SizeProposal::exact(800.0, 600.0));
9043 let finger = contact_id(99);
9044 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
9045 tree.dispatch_pointer(touch(finger, PointerPhase::Cancel, outside()));
9046 tree.assert_no_leaked_pointer_state();
9047 }
9048
9049 #[test]
9056 fn a_mouse_click_outside_a_menu_dismisses_on_the_press_and_falls_through() {
9057 let (mut tree, _page, taps) = page_with_a_menu();
9058 tree.dispatch_event(WidgetEvent::pointer_down(
9059 outside(),
9060 PointerButton::Primary,
9061 Modifiers::NONE,
9062 ));
9063 assert!(
9064 tree.active_overlays().is_empty(),
9065 "the mouse still dismisses on the press"
9066 );
9067 assert!(
9068 tree.pointers
9069 .get(PointerId::MOUSE)
9070 .is_some_and(|e| e.sequence.is_some()),
9071 "and the press still reaches the page beneath"
9072 );
9073 tree.dispatch_event(WidgetEvent::pointer_up(
9074 outside(),
9075 PointerButton::Primary,
9076 Modifiers::NONE,
9077 ));
9078 assert_eq!(taps.get(), 1, "the control beneath activated");
9079 assert!(
9080 !tree.overlay_manager().has_armed_dismiss(PointerId::MOUSE),
9081 "a mouse never arms"
9082 );
9083 tree.assert_no_leaked_pointer_state();
9084 }
9085
9086 #[test]
9093 fn a_context_menu_avoids_the_contact_that_opened_it() {
9094 fn menu_bounds(pointer: PointerInfo, at: Point) -> Rect {
9095 let mut tree = WidgetTree::new();
9096 let page = tree.add(
9097 FillWidget::new()
9098 .focusable()
9099 .context_menu(|_p, _ctx| Some(Box::new(Panel(Size::new(200.0, 160.0))))),
9100 );
9101 let _ = page;
9102 tree.layout(SizeProposal::exact(800.0, 600.0));
9103 tree.dispatch_pointer(PointerSample {
9104 pointer,
9105 phase: PointerPhase::Down,
9106 position: at,
9107 button: Some(PointerButton::Secondary),
9108 modifiers: Modifiers::NONE,
9109 coalesced: Vec::new(),
9110 });
9111 tree.layout(SizeProposal::exact(800.0, 600.0));
9112 let id = *tree
9113 .active_overlays()
9114 .first()
9115 .expect("the context menu opened");
9116 tree.overlay_manager().bounds_for(id).expect("bounds")
9117 }
9118
9119 let at = Point::new(400.0, 300.0);
9120 let finger = menu_bounds(PointerInfo::touch(contact_id(11), EventTime::ZERO), at);
9121 let contact = crate::overlay::rect_centred_on(at, crate::overlay::ASSUMED_CONTACT_PATCH);
9122 assert!(
9123 finger.x < contact.x && finger.right() <= contact.x,
9124 "a touch menu clears the contact patch: {finger:?} vs {contact:?}"
9125 );
9126
9127 let mouse = menu_bounds(PointerInfo::mouse(EventTime::ZERO), at);
9128 assert_eq!(
9129 (mouse.x, mouse.y),
9130 (at.x, at.y),
9131 "a mouse menu still opens with its corner on the pointer"
9132 );
9133 }
9134}
9135
9136#[cfg(test)]
9152mod pass_through_overlay_tests {
9153 use teksilo_canvas::{Point, Rect, SizeProposal};
9154
9155 use crate::WidgetId;
9156 use crate::overlay::{DismissBehavior, OverlayPlacement, OverlayRequest};
9157 use crate::test_widgets::FillWidget;
9158 use crate::widget::{LayoutContext, LayoutResponse, Widget};
9159 use crate::widget_builder::WidgetBuilder;
9160 use crate::widget_tree::WidgetTree;
9161
9162 #[derive(Debug)]
9165 struct HandleLayer {
9166 handle: WidgetId,
9167 at: Rect,
9168 }
9169
9170 impl Widget for HandleLayer {
9171 fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
9172 proposal.resolve(0.0, 0.0).into()
9173 }
9174
9175 fn place_children(
9176 &self,
9177 _bounds: Rect,
9178 _proposal: SizeProposal,
9179 children: &mut [crate::widget::WidgetPlacement],
9180 _ctx: &LayoutContext,
9181 ) {
9182 if let Some(child) = children.get_mut(0) {
9183 child.origin = self.at.origin();
9184 child.size = self.at.size();
9185 }
9186 }
9187
9188 fn children(&self) -> Vec<WidgetId> {
9189 vec![self.handle]
9190 }
9191 }
9192
9193 const HANDLE: Rect = Rect {
9194 x: 10.0,
9195 y: 10.0,
9196 width: 20.0,
9197 height: 20.0,
9198 };
9199
9200 #[derive(Clone, Copy, PartialEq)]
9202 enum RootFlag {
9203 PassThrough,
9205 HitTransparent,
9208 }
9209
9210 fn field_under_a_layer(flag: RootFlag) -> (WidgetTree, WidgetId, WidgetId) {
9216 let mut tree = WidgetTree::new();
9217 let field = tree.add(FillWidget::new());
9218 let handle = tree.add(FillWidget::new());
9219 let layer = HandleLayer { handle, at: HANDLE };
9220 let layer = match flag {
9221 RootFlag::PassThrough => tree.add(layer.event_pass_through(true)),
9222 RootFlag::HitTransparent => tree.add(layer.hit_transparent(true)),
9223 };
9224 tree.show_overlay(OverlayRequest::new(
9225 layer,
9226 field,
9227 OverlayPlacement::FullViewport,
9228 DismissBehavior::Manual,
9229 ));
9230 tree.layout(SizeProposal::exact(400.0, 200.0));
9231 assert_eq!(tree.active_overlays().len(), 1, "the layer is up");
9232 (tree, field, handle)
9233 }
9234
9235 #[test]
9237 fn a_pass_through_layer_hands_back_what_it_did_not_claim() {
9238 let (tree, field, handle) = field_under_a_layer(RootFlag::PassThrough);
9239
9240 assert_eq!(
9241 tree.hit_test(Point::new(15.0, 15.0)),
9242 Some(handle),
9243 "the layer must still win the point its own child covers"
9244 );
9245 assert_eq!(
9246 tree.hit_test(Point::new(200.0, 100.0)),
9247 Some(field),
9248 "a press the layer did not claim must reach the surface beneath it"
9249 );
9250 }
9251
9252 #[test]
9263 fn a_pass_through_root_still_hears_a_press_that_landed_on_its_child() {
9264 use std::cell::Cell;
9265 use std::rc::Rc;
9266
9267 let seen = Rc::new(Cell::new(0u32));
9268 let counter = seen.clone();
9269 let mut tree = WidgetTree::new();
9270 let field = tree.add(FillWidget::new());
9271 let handle = tree.add(FillWidget::new().on_tap(|_e, _c| {}));
9272 let layer = tree.add(
9273 HandleLayer { handle, at: HANDLE }
9274 .event_pass_through(true)
9275 .on_pointer_event(move |_event, _ctx| {
9276 counter.set(counter.get() + 1);
9277 crate::event::EventResponse::Ignored
9278 }),
9279 );
9280 tree.show_overlay(OverlayRequest::new(
9281 layer,
9282 field,
9283 OverlayPlacement::FullViewport,
9284 DismissBehavior::Manual,
9285 ));
9286 tree.layout(SizeProposal::exact(400.0, 200.0));
9287
9288 tree.pointer_down_button(Point::new(15.0, 15.0), crate::event::PointerButton::Primary);
9289 assert!(
9290 seen.get() > 0,
9291 "a pass-through root heard nothing about a press on its own child"
9292 );
9293
9294 let after_child = seen.get();
9295 tree.pointer_up_button(Point::new(15.0, 15.0), crate::event::PointerButton::Primary);
9296 tree.pointer_down_button(
9299 Point::new(200.0, 100.0),
9300 crate::event::PointerButton::Primary,
9301 );
9302 assert_eq!(
9303 seen.get(),
9304 after_child + 1,
9305 "the press-and-lift on the child accounts for the count, and the \
9306 press on the surface beneath added nothing"
9307 );
9308 }
9309
9310 #[test]
9324 fn a_hit_transparent_root_does_not_get_the_fall_through() {
9325 let (tree, field, _handle) = field_under_a_layer(RootFlag::HitTransparent);
9326
9327 assert_eq!(
9329 tree.hit_test(Point::new(15.0, 15.0)),
9330 None,
9331 "hit_transparent must exclude the subtree, or this fixture is not \
9332 testing the gate"
9333 );
9334 assert_eq!(
9335 tree.hit_test(Point::new(200.0, 100.0)),
9336 None,
9337 "the fall-through reached past an overlay that did not ask for it"
9338 );
9339 let _ = field;
9340 }
9341}