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 dispatch_event_impl(&mut self, event: WidgetEvent, ops: &mut dyn crate::window::WindowOps) {
556 if let Some(position) = pointer_event_position(&event) {
562 let is_down = matches!(event, WidgetEvent::PointerDown { .. });
563 let is_move = matches!(event, WidgetEvent::PointerMove { .. });
564 if !self.admit_current_pointer(position, is_down, is_move) {
565 return;
566 }
567 if self.current_input.pointer.kind.hovers() {
571 self.claim_hover_owner_for_current(&mut *ops);
572 }
573 }
574
575 match &event {
579 WidgetEvent::KeyDown { .. } if !self.focus_visible.get() => {
580 self.focus_visible.set(true);
581 }
582 WidgetEvent::PointerDown { .. } if self.focus_visible.get() => {
583 self.focus_visible.set(false);
584 }
585 _ => {}
586 }
587
588 let overlay_back_key = match self.layout_direction {
594 crate::environment::LayoutDirection::RightToLeft => Key::ArrowRight,
595 crate::environment::LayoutDirection::LeftToRight => Key::ArrowLeft,
596 };
597 if let WidgetEvent::KeyDown { key, .. } = &event
598 && *key == overlay_back_key
599 {
600 let nested_menu_overlays = {
630 let ids: Vec<_> = self
631 .overlay_manager
632 .stack
633 .iter()
634 .filter(|o| o.band == crate::overlay::OverlayBand::Standard && !o.inert)
635 .map(|o| o.id)
636 .collect();
637 ids.into_iter()
638 .filter(|&id| !self.overlay_is_host_surface(id))
639 .count()
640 };
641 let top_id = self
648 .overlay_manager
649 .stack
650 .iter()
651 .rev()
652 .find(|o| !o.inert)
653 .map(|o| o.id);
654 let top_is_back_navigable = top_id.is_some_and(|id| !self.overlay_is_host_surface(id));
655 if nested_menu_overlays > 1
656 && top_is_back_navigable
657 && let Some(top_id) = top_id
658 {
659 let (content_ids, focus_restore) =
660 self.overlay_manager.dismiss_with_focus_restore_because(
661 top_id,
662 crate::overlay::DismissReason::Escape,
663 );
664 self.dormant_dismissed_content(&content_ids, &mut *ops);
665 if let Some(restore_id) = focus_restore
666 && self.arena.is_active(restore_id)
667 {
668 self.focus_ops(restore_id, &mut *ops);
669 }
670 return;
671 }
672 }
673
674 if let WidgetEvent::KeyDown {
680 key: Key::Escape, ..
681 } = &event
682 {
683 self.tooltip_escape_pressed();
684 self.inert_overlays_escape_pressed(&mut *ops);
687 }
688
689 if let WidgetEvent::KeyDown {
690 key: Key::Escape, ..
691 } = &event
692 && !self.overlay_manager.is_empty()
693 && let Some((_id, content_ids, focus_restore)) =
694 self.overlay_manager.try_dismiss_top_on_escape()
695 {
696 self.dormant_dismissed_content(&content_ids, &mut *ops);
697 if let Some(restore_id) = focus_restore
698 && self.arena.is_active(restore_id)
699 {
700 self.focus_ops(restore_id, &mut *ops);
701 }
702 return;
703 }
704
705 match &event {
710 WidgetEvent::PointerDown {
711 position, button, ..
712 } => {
713 if self.arm_outside_press_dismissal(*position, *button, &mut *ops) {
714 return;
715 }
716 }
717 WidgetEvent::PointerUp { position, .. } => {
718 if self.commit_outside_press_dismissal(*position, &mut *ops) {
719 return;
720 }
721 }
722 WidgetEvent::PointerCancel { pointer, .. } => {
723 self.overlay_manager.abort_dismiss(pointer.id);
727 }
728 _ => {}
729 }
730
731 if let WidgetEvent::KeyDown { key, modifiers, .. } = &event
742 && let Some(callback) = self.take_key_capture()
743 {
744 let keystroke = crate::shortcut::KeyStroke::new(*key, *modifiers);
745 let mut cap_ctx = self.make_event_context(&mut *ops);
746 callback(keystroke, self.shortcut_registry_mut(), &mut cap_ctx);
747 let anchor = self.focused.or_else(|| self.intent_anchor());
750 if let Some(anchor_id) = anchor {
751 self.collect_from_ctx(cap_ctx, anchor_id);
752 self.drain_pending_intents(&mut *ops);
753 }
754 return;
755 }
756
757 let focus_captures_keys = matches!(&event, WidgetEvent::KeyDown { .. })
765 && self.focused.is_some_and(|f| self.is_keyboard_capture(f));
766
767 if !focus_captures_keys && let WidgetEvent::KeyDown { key, modifiers, .. } = &event {
783 let keystroke = crate::shortcut::KeyStroke::new(*key, *modifiers);
784 let candidates: Vec<(&'static str, crate::shortcut::ShortcutScope, bool)> = self
793 .shortcut_registry
794 .matches_by_keystroke(keystroke)
795 .map(|eff| {
796 (
797 eff.shortcut.id,
798 eff.shortcut.scope,
799 eff.shortcut.propagate_when_disabled,
800 )
801 })
802 .collect();
803 let lookup = self.select_shortcut_for_focus(&candidates);
804 if let Some((id, scope, propagate_when_disabled)) = lookup {
805 let anchor = match scope {
806 crate::shortcut::ShortcutScope::Global => {
812 self.focus_or_modal().or_else(|| self.intent_anchor())
813 }
814 crate::shortcut::ShortcutScope::Scoped(scope_id) => self
815 .focus_or_modal()
816 .filter(|f| self.scope_distance_from_focus(*f, scope_id).is_some()),
817 };
818 if let Some(anchor_id) = anchor {
819 let mut act_ctx = self.make_event_context(&mut *ops);
820 if let Some(intent) =
821 self.shortcut_registry
822 .invoke_on_activate(id, keystroke, &mut act_ctx)
823 {
824 self.collect_from_ctx(act_ctx, anchor_id);
825 self.drain_pending_intents(&mut *ops);
829 let intent = self.sent_here(
833 intent.with_source(crate::telemetry::IntentSource::Shortcut),
834 );
835 let keep = self.keeps_unclaimed(&intent).then(|| intent.clone());
836 let consumed = self.dispatch_intent(
837 anchor_id,
838 intent,
839 propagate_when_disabled,
840 &mut *ops,
841 );
842 self.drain_pending_intents(&mut *ops);
843 if consumed {
844 return;
845 }
846 if let Some(intent) = keep {
849 self.keep_unclaimed(intent, &mut *ops);
850 return;
851 }
852 }
859 }
860 }
865 }
868
869 if self.outbound_drag_source.is_some()
875 && let WidgetEvent::KeyDown {
876 key: Key::Escape, ..
877 } = &event
878 {
879 ops.cancel_os_drag();
880 }
885
886 if self.active_drag.is_some() {
888 match &event {
889 WidgetEvent::PointerMove { position, .. } => {
890 self.handle_drag_move(*position, &mut *ops);
891 return;
892 }
893 WidgetEvent::PointerUp { position, .. } => {
894 self.handle_drag_drop(*position, &mut *ops);
895 return;
896 }
897 WidgetEvent::KeyDown {
898 key: Key::Escape, ..
899 } => {
900 self.cancel_active_drag(&mut *ops);
901 return;
902 }
903 WidgetEvent::Scroll { .. } => {
904 let target_and_pos = self
910 .active_drag
911 .as_ref()
912 .and_then(|d| d.current_target.map(|t| (t, d.current_position)));
913 if let Some((target, _pos)) = target_and_pos {
914 self.dispatch_to_widget(target, &event, &mut *ops);
915 }
916 if let Some((_, pos)) = target_and_pos
917 && self.active_drag.is_some()
918 {
919 self.handle_drag_move(pos, &mut *ops);
920 }
921 return;
922 }
923 _ => {}
924 }
925 }
926
927 if let WidgetEvent::KeyDown { key, modifiers, .. } = &event
936 && is_context_menu_chord(*key, *modifiers)
937 && self.open_context_menu_from_keyboard(&mut *ops)
938 {
939 return;
940 }
941
942 match &event {
943 WidgetEvent::PointerMove { position, .. } => {
944 self.note_sequence_position(*position);
948 self.revalidate_sequence(&mut *ops);
949 self.tick_sequence_timers();
952 self.note_palm_sample(*position);
957 self.feed_pinch(super::pan_arbiter::PinchFeed::Move, *position, &mut *ops);
958 if let Some(captured) = self.current_pointer_capture() {
959 self.dispatch_to_widget(captured, &event, &mut *ops);
960 if self.active_drag.is_none() {
965 self.advance_sequence(&event, &mut *ops);
966 }
967 } else {
968 self.handle_pointer_move(&event, *position, &mut *ops);
969 if self.active_drag.is_none() {
974 self.advance_sequence(&event, &mut *ops);
975 }
976 }
977 self.advance_pan(*position, &mut *ops);
980 self.update_press(*position);
983 self.touch_route_moved(self.current_pointer_id());
986 if self.pointers.hover_owner_id() == Some(self.current_pointer_id()) {
996 self.update_pointer_leave_overlays(*position, &mut *ops);
997 }
998 }
999 WidgetEvent::PointerDown {
1000 position, button, ..
1001 } => {
1002 let pressed = self.current_pointer_id();
1005 self.cancelled_pointers.retain(|p| *p != pressed);
1006 self.tooltip_pointer_press(Some(*position));
1013 let pressing = self.current_input.pointer;
1017 if let Some(target) = self.hit_test_for(*position, &pressing) {
1018 if *button == PointerButton::Secondary
1019 && self.show_context_menu_for(
1020 target,
1021 *position,
1022 crate::widget_builder::ContextMenuTrigger::Pointer,
1023 &mut *ops,
1024 )
1025 {
1026 return;
1027 }
1028 self.begin_sequence(target, *position);
1034 let modifiers = match &event {
1039 WidgetEvent::PointerDown { modifiers, .. } => *modifiers,
1040 _ => crate::event::Modifiers::NONE,
1041 };
1042 self.begin_pan(target, *position, modifiers);
1043 self.begin_palm_watch(*position);
1044 self.feed_pinch(super::pan_arbiter::PinchFeed::Down, *position, &mut *ops);
1045 let focusable = self.find_focusable_at_or_above(target);
1049 self.begin_press(focusable);
1050 if !pressing.kind.is_direct()
1060 && let Some(focusable) = focusable
1061 && self.click_may_focus(focusable)
1062 {
1063 self.focus_with_origin_ops(
1064 focusable,
1065 crate::focus::FocusOrigin::Pointer(pressing.kind),
1066 &mut *ops,
1067 );
1068 }
1069 self.dispatch_to_widget(target, &event, &mut *ops);
1070 if self.active_drag.is_none() {
1075 self.enrol_sequence_members(&event, &mut *ops);
1076 }
1077 self.adopt_press_owner(*button);
1081 self.arm_touch_route(target, *position);
1086 }
1087 }
1088 WidgetEvent::PointerUp { position, .. } => {
1089 let released = self.current_pointer_id();
1095 self.cancel_touch_route(released);
1099 if let Some(index) = self.cancelled_pointers.iter().position(|p| *p == released) {
1100 self.cancelled_pointers.swap_remove(index);
1101 crate::trace_input!(
1102 Samples,
1103 "swallowing the Up for {released:?}: its press was cancelled"
1104 );
1105 return;
1106 }
1107 self.feed_pinch(super::pan_arbiter::PinchFeed::Up, *position, &mut *ops);
1114 if self.take_palm_verdict(released) {
1115 crate::trace_input!(
1116 Samples,
1117 "{released:?} released as a palm: large, and it never moved"
1118 );
1119 self.cancel_pointer(
1120 released,
1121 crate::pointer::CancelReason::PalmRejected,
1122 &mut *ops,
1123 );
1124 return;
1125 }
1126 self.end_pan(*position, &mut *ops);
1129 self.note_sequence_position(*position);
1136 self.end_sequence(&event, &mut *ops);
1137 self.focus_on_release(*position, &mut *ops);
1142 if let Some(captured) = self.current_pointer_capture() {
1143 self.dispatch_to_widget(captured, &event, &mut *ops);
1144 self.set_current_pointer_capture(None);
1147 } else {
1148 let releasing = self.current_input.pointer;
1149 if let Some(target) = self.hit_test_for(*position, &releasing) {
1150 self.dispatch_to_widget(target, &event, &mut *ops);
1151 }
1152 }
1153 let released = self.current_pointer_id();
1156 self.release_arenas_following(released);
1157 self.end_press(released);
1161 }
1162 WidgetEvent::PointerCancel {
1163 reason, pointer, ..
1164 } => {
1165 let (reason, pointer) = (*reason, pointer.id);
1170 self.cancel_pointer(pointer, reason, &mut *ops);
1171 }
1172 WidgetEvent::Scroll {
1173 window_position: position,
1174 ..
1175 } => {
1176 use super::pan_arbiter::ScrollDelivery;
1177 match ScrollDelivery::for_source(self.current_input.scroll_source) {
1178 ScrollDelivery::ClaimantChain => {
1183 let position = *position;
1184 self.route_scroll_along_chain(&event, position, &mut *ops);
1185 }
1186 ScrollDelivery::Bubble => {
1192 let scrolling = self.current_input.pointer;
1193 let target = match position {
1194 Some(p) => self.hit_test_for(*p, &scrolling),
1195 None => self.hovered_id().or(self.focused),
1196 };
1197 if let Some(target) = target {
1198 self.dispatch_to_widget(target, &event, &mut *ops);
1199 }
1200 }
1201 }
1202 }
1203 WidgetEvent::KeyDown { key, modifiers, .. } => {
1204 if *key == Key::Tab {
1205 let captured_focus = self
1220 .focused
1221 .is_some_and(|focused| self.is_keyboard_capture(focused));
1222 if captured_focus && modifiers.ctrl() {
1223 self.cycle_focus(modifiers.shift(), &mut *ops);
1224 return;
1225 }
1226 let handled = self
1233 .focused
1234 .map(|focused| {
1235 self.dispatch_to_widget_returning_handled(focused, &event, &mut *ops)
1236 })
1237 .unwrap_or(false);
1238 if !handled {
1239 self.cycle_focus(modifiers.shift(), &mut *ops);
1240 }
1241 } else if let Some(focused) = self.focused {
1242 self.dispatch_to_widget(focused, &event, &mut *ops);
1243 }
1244 }
1245 WidgetEvent::KeyUp { .. }
1246 | WidgetEvent::ImeComposition { .. }
1247 | WidgetEvent::ImeCommit { .. } => {
1248 if let Some(focused) = self.focused {
1249 self.dispatch_to_widget(focused, &event, &mut *ops);
1250 }
1251 }
1252 WidgetEvent::AccessAction { target, action, .. } => {
1253 if let Some(id) =
1271 target.filter(|id| self.arena.is_active(*id) && !self.is_behind_modal(*id))
1272 {
1273 if *action == accesskit::Action::Focus {
1274 if self.advertises_focus_action(id) {
1303 let target = self
1304 .focusable_composite_behind(id)
1305 .or_else(|| self.first_focusable_descendant(id))
1306 .unwrap_or(id);
1307 self.focus_with_origin_ops(
1313 target,
1314 crate::focus::FocusOrigin::Accessibility,
1315 &mut *ops,
1316 );
1317 self.access_action_handled = self.focused == Some(target);
1320 } else {
1321 self.access_action_handled = false;
1322 }
1323 } else if *action == accesskit::Action::ShowContextMenu {
1324 self.access_action_handled =
1340 if self.dispatch_to_widget_returning_handled(id, &event, &mut *ops) {
1341 true
1342 } else {
1343 let position = self.arena.bounds(id).center();
1344 self.show_context_menu_for(
1345 id,
1346 position,
1347 crate::widget_builder::ContextMenuTrigger::Accessibility,
1348 &mut *ops,
1349 )
1350 };
1351 } else {
1352 if *action == accesskit::Action::Click
1357 && self
1358 .find_focusable_at_or_above(id)
1359 .is_some_and(|focusable| !self.click_may_focus(focusable))
1360 {
1361 self.commit_pending_edit(&mut *ops);
1362 }
1363 self.access_action_handled =
1364 self.dispatch_to_widget_returning_handled(id, &event, &mut *ops);
1365 }
1366 }
1367 }
1368 WidgetEvent::Gesture { .. } => {
1369 if let Some(target) = self.hovered_id().or(self.focused) {
1370 self.dispatch_to_widget(target, &event, &mut *ops);
1371 }
1372 }
1373 WidgetEvent::ScrollIntoView { .. }
1374 | WidgetEvent::PointerEnter { .. }
1375 | WidgetEvent::PointerLeave { .. }
1376 | WidgetEvent::FocusGained { .. }
1377 | WidgetEvent::FocusLost => {}
1378 }
1379 self.drain_pending_intents(&mut *ops);
1384 }
1385
1386 fn open_context_menu_from_keyboard(&mut self, ops: &mut dyn crate::window::WindowOps) -> bool {
1399 let Some(focused) = self.focused else {
1400 return false;
1401 };
1402 let target = self
1403 .arena
1404 .get(focused)
1405 .and_then(|node| node.widget.context_menu_key_target())
1406 .filter(|id| self.arena.is_active(*id))
1407 .unwrap_or(focused);
1408
1409 let bounds = self.bounds(target);
1413 let anchor = Point {
1414 x: bounds.x + bounds.width / 2.0,
1415 y: bounds.y + bounds.height / 2.0,
1416 };
1417 self.show_context_menu_for(
1418 target,
1419 anchor,
1420 crate::widget_builder::ContextMenuTrigger::Keyboard,
1421 ops,
1422 )
1423 }
1424
1425 pub(super) fn show_context_menu_for(
1434 &mut self,
1435 target: WidgetId,
1436 position: Point,
1437 trigger: crate::widget_builder::ContextMenuTrigger,
1438 ops: &mut dyn crate::window::WindowOps,
1439 ) -> bool {
1440 let mut ctx = self.make_event_context(&mut *ops);
1446 ctx.context_menu_trigger = Some(trigger);
1447 let mut walker = Some(target);
1448 let menu_decision: Option<(WidgetId, Box<dyn Widget>)> = loop {
1449 let owner_id = {
1452 let mut probe = walker;
1453 loop {
1454 match probe {
1455 None => break None,
1456 Some(id) => {
1457 if self
1458 .arena
1459 .get(id)
1460 .is_some_and(|node| node.context_menu_factory.is_some())
1461 {
1462 break Some(id);
1463 }
1464 probe = self.arena.get(id).and_then(|node| node.parent);
1465 }
1466 }
1467 }
1468 };
1469 let Some(owner_id) = owner_id else {
1470 break None;
1471 };
1472 let outcome: Option<Box<dyn Widget>> = {
1479 let node = self
1480 .arena
1481 .get(owner_id)
1482 .expect("owner_id from active arena walk");
1483 let factory = node
1484 .context_menu_factory
1485 .as_ref()
1486 .expect("owner_id only set when factory present");
1487 factory(position, &mut ctx)
1488 };
1489 match outcome {
1490 Some(menu) => break Some((owner_id, menu)),
1491 None => {
1492 walker = self.arena.get(owner_id).and_then(|n| n.parent);
1494 }
1495 }
1496 };
1497
1498 let drain_anchor = menu_decision.as_ref().map(|(id, _)| *id).unwrap_or(target);
1504 self.collect_from_ctx(ctx, drain_anchor);
1505
1506 let Some((owner_id, menu_widget)) = menu_decision else {
1507 return false;
1508 };
1509
1510 let keep: std::collections::HashSet<WidgetId> = self
1516 .overlay_manager
1517 .stack
1518 .iter()
1519 .map(|o| o.content_id)
1520 .filter(|&content_id| self.is_descendant_of(owner_id, content_id))
1521 .collect();
1522 let dismissed = self.overlay_manager.dismiss_except(&keep);
1523 self.dormant_dismissed_content(&dismissed, &mut *ops);
1524
1525 let content_id = self.add_boxed(menu_widget);
1526 let prev_focus = self.focused;
1527 let placement =
1534 crate::overlay::OverlayPlacement::at_pointer_for(position, &self.current_input.pointer);
1535 self.overlay_manager
1536 .show(crate::overlay::OverlayRequest::new(
1537 content_id,
1538 owner_id,
1539 placement,
1540 crate::overlay::DismissBehavior::EscapeOrClickOutside,
1541 ));
1542 if let Some(focus_id) = prev_focus {
1543 self.overlay_manager.set_top_focus_restore(focus_id);
1544 }
1545 self.focus_ops(content_id, &mut *ops);
1546 self.drain_pending_intents(&mut *ops);
1551 true
1552 }
1553
1554 fn arm_outside_press_dismissal(
1575 &mut self,
1576 position: Point,
1577 button: PointerButton,
1578 ops: &mut dyn crate::window::WindowOps,
1579 ) -> bool {
1580 self.prune_stale_dismiss_arms();
1581 let pressing = self.current_input.pointer;
1582 let busy = self.busy_press_points(pressing.id);
1583
1584 if pressing.kind.is_direct() {
1585 return self
1586 .overlay_manager
1587 .arm_dismiss(pressing.id, position, &busy)
1588 .suppress_beneath;
1589 }
1590
1591 let (dismissed, focus_restore, toggle_anchors) =
1592 self.overlay_manager.dismiss_outside_press(position, &busy);
1593 if dismissed.is_empty() {
1594 return false;
1595 }
1596 self.dormant_dismissed_content(&dismissed, &mut *ops);
1597 if let Some(restore_id) = focus_restore
1598 && self.arena.is_active(restore_id)
1599 {
1600 self.focus_ops(restore_id, &mut *ops);
1601 }
1602 toggle_anchors.iter().any(|&anchor| {
1615 self.arena.is_active(anchor)
1616 && self.arena.bounds(anchor).contains(position)
1617 && (button == PointerButton::Primary
1618 || self
1619 .anchor_tap_buttons(anchor, position, &pressing)
1620 .contains(button))
1621 })
1622 }
1623
1624 fn anchor_tap_buttons(
1634 &self,
1635 anchor: WidgetId,
1636 position: Point,
1637 pointer: &crate::pointer::PointerInfo,
1638 ) -> ButtonMask {
1639 let mut mask = ButtonMask::NONE;
1640 let mut node = self.hit_test_for(position, pointer);
1641 while let Some(id) = node {
1642 mask = mask.union(self.tap_buttons(id));
1643 if id == anchor {
1644 return mask;
1645 }
1646 node = self.arena.parent(id);
1647 }
1648 ButtonMask::NONE
1649 }
1650
1651 fn commit_outside_press_dismissal(
1658 &mut self,
1659 position: Point,
1660 ops: &mut dyn crate::window::WindowOps,
1661 ) -> bool {
1662 let pointer = self.current_pointer_id();
1663 if !self.overlay_manager.has_armed_dismiss(pointer) {
1664 return false;
1665 }
1666 let (dismissed, focus_restore, _anchors) =
1667 self.overlay_manager.commit_dismiss(pointer, position);
1668 if !dismissed.is_empty() {
1669 self.dormant_dismissed_content(&dismissed, &mut *ops);
1670 if let Some(restore_id) = focus_restore
1671 && self.arena.is_active(restore_id)
1672 {
1673 self.focus_ops(restore_id, &mut *ops);
1674 }
1675 }
1676 true
1677 }
1678
1679 fn busy_press_points(&self, exclude: crate::pointer::PointerId) -> Vec<Point> {
1696 self.pointers
1697 .iter()
1698 .filter(|entry| entry.info.id != exclude)
1699 .filter(|entry| self.press_is_revocable(entry.info.id))
1700 .map(|entry| entry.down_position)
1701 .collect()
1702 }
1703
1704 fn prune_stale_dismiss_arms(&mut self) {
1711 let stale: Vec<crate::pointer::PointerId> = self
1712 .overlay_manager
1713 .armed_pointers()
1714 .into_iter()
1715 .filter(|id| self.pointers.get(*id).is_none())
1716 .collect();
1717 for id in stale {
1718 self.overlay_manager.abort_dismiss(id);
1719 }
1720 }
1721
1722 fn begin_press(&mut self, focusable: Option<WidgetId>) {
1743 let pointer = self.current_pointer_id();
1744 let now = self.sequence_now();
1745 let delay = self
1746 .pan_session_open(pointer)
1747 .then(|| self.current_profile().press_feedback_delay)
1748 .filter(|d| !d.is_zero());
1749 self.presses.press(pointer, focusable, now, delay);
1750 }
1751
1752 fn adopt_press_owner(&mut self, button: PointerButton) {
1775 let pointer = self.current_pointer_id();
1776 let Some(owner) = self.current_sequence().and_then(|s| s.pressed_owner()) else {
1777 return;
1778 };
1779 if !self.press_buttons(owner).contains(button) {
1780 crate::trace_input!(
1781 Samples,
1782 "no press visual for {owner:?}: {button:?} is not one of its accepted buttons"
1783 );
1784 return;
1785 }
1786 self.presses.set_owner(pointer, owner);
1787 self.publish_pressed(owner);
1788 }
1789
1790 fn update_press(&mut self, position: Point) {
1803 let pointer = self.current_pointer_id();
1804 if self.presses.get(pointer).is_none() {
1805 return;
1806 }
1807 let claimed_elsewhere = self.current_sequence().is_some_and(|sequence| {
1810 sequence
1811 .winner()
1812 .is_some_and(|winner| Some(winner) != sequence.pressed_owner())
1813 });
1814 if claimed_elsewhere {
1815 self.end_press(pointer);
1816 return;
1817 }
1818 let profile = self.current_profile();
1819 let origin = self.current_sequence().map(|s| s.press_origin());
1820 let owner = self.presses.get(pointer).and_then(|p| p.owner);
1821 let inside = match (origin, owner) {
1822 (Some(origin), Some(owner)) => {
1823 let bounds = self
1824 .arena
1825 .is_active(owner)
1826 .then(|| self.arena.bounds(owner));
1827 !crate::gesture::TapBoundary::for_pointer(&self.current_input.pointer, &profile)
1828 .left(origin, position, bounds, &profile)
1829 }
1830 _ => true,
1833 };
1834 let Some(press) = self.presses.get_mut(pointer) else {
1835 return;
1836 };
1837 if press.inside == inside {
1838 return;
1839 }
1840 press.inside = inside;
1841 if let Some(owner) = owner {
1842 self.publish_pressed(owner);
1843 }
1844 }
1845
1846 pub(crate) fn end_press(&mut self, pointer: crate::pointer::PointerId) {
1852 if let Some(owner) = self.presses.release(pointer) {
1853 self.publish_pressed(owner);
1854 }
1855 }
1856
1857 pub(crate) fn resolve_press_delays(&mut self, now: crate::pointer::EventTime) {
1860 if self.presses.is_empty() {
1861 return;
1862 }
1863 for id in self.presses.resolve_delays(now) {
1864 self.publish_pressed(id);
1865 self.arena.mark_needs_paint(id);
1866 }
1867 }
1868
1869 pub(crate) fn next_press_deadline(&self) -> Option<std::time::Instant> {
1872 self.presses.next_deadline().map(|t| self.instant_for(t))
1873 }
1874
1875 pub(crate) fn commit_pending_edit(&mut self, ops: &mut dyn crate::window::WindowOps) {
1890 let Some(focus) = self.focused.filter(|id| self.arena.is_active(*id)) else {
1891 return;
1892 };
1893 let (chain, _, _) = self.intent_walk(focus, &[]);
1894 for id in chain {
1895 for external in [true, false] {
1896 let handler = self.arena.get_mut(id).and_then(|node| {
1897 let handlers = if external {
1898 &mut node.external_handlers
1899 } else {
1900 &mut node.handlers
1901 };
1902 handlers.on_commit_edit.take()
1903 });
1904 let Some(mut handler) = handler else { continue };
1905 let mut ctx = self.make_event_context(&mut *ops).with_dispatch_node(id);
1906 handler(&mut ctx);
1907 if let Some(node) = self.arena.get_mut(id) {
1908 let handlers = if external {
1909 &mut node.external_handlers
1910 } else {
1911 &mut node.handlers
1912 };
1913 if handlers.on_commit_edit.is_none() {
1915 handlers.on_commit_edit = Some(handler);
1916 }
1917 }
1918 self.collect_from_ctx(ctx, id);
1919 }
1920 }
1921 }
1922
1923 fn click_may_focus(&self, focusable: WidgetId) -> bool {
1931 self.arena
1932 .get(focusable)
1933 .is_none_or(|node| node.focus_on_click)
1934 }
1935
1936 fn focus_on_release(&mut self, position: Point, ops: &mut dyn crate::window::WindowOps) {
1952 let releasing = self.current_input.pointer;
1953 let pointer = self.current_pointer_id();
1954 let Some(pressed) = self.presses.get(pointer).and_then(|p| p.focusable) else {
1955 return;
1956 };
1957 if !releasing.kind.is_direct() && self.click_may_focus(pressed) {
1958 return;
1959 }
1960 let Some(target) = self.hit_test_for(position, &releasing) else {
1961 return;
1962 };
1963 if self.find_focusable_at_or_above(target) != Some(pressed) {
1964 return;
1965 }
1966 if !self.click_may_focus(pressed) {
1967 self.commit_pending_edit(&mut *ops);
1968 } else {
1969 self.focus_with_origin_ops(
1970 pressed,
1971 crate::focus::FocusOrigin::Pointer(releasing.kind),
1972 &mut *ops,
1973 );
1974 }
1975 }
1976
1977 fn handle_pointer_move(
1997 &mut self,
1998 event: &WidgetEvent,
1999 position: Point,
2000 ops: &mut dyn crate::window::WindowOps,
2001 ) {
2002 let moving = self.current_input.pointer;
2007 if self.pointers.hover_owner_id() != Some(self.current_pointer_id()) {
2008 if let Some(target) = self.hit_test_for(position, &moving) {
2009 self.dispatch_to_widget(target, event, &mut *ops);
2010 }
2011 return;
2012 }
2013 let target = self.hit_test_for(position, &moving);
2014
2015 if target != self.hovered_id() {
2016 let hover = self.hover_transition_pointer();
2018 let previously_hovered = self.hovered_id();
2019 if let Some(old) = previously_hovered {
2020 self.dispatch_to_widget(
2021 old,
2022 &WidgetEvent::PointerLeave { pointer: hover },
2023 &mut *ops,
2024 );
2025 self.tooltip_pointer_leave(old, &mut *ops);
2026 }
2027 if let Some(new) = target {
2028 self.dispatch_to_widget(
2029 new,
2030 &WidgetEvent::PointerEnter { pointer: hover },
2031 &mut *ops,
2032 );
2033 self.tooltip_pointer_enter(new);
2034 }
2035 self.set_hovered(target);
2036 self.update_hover_within_signals(previously_hovered, target);
2037 } else if let Some(target) = target {
2038 self.tooltip_pointer_moved(target, position);
2041 }
2042
2043 if let Some(target) = target {
2044 self.dispatch_to_widget(target, event, &mut *ops);
2045 }
2046 }
2047
2048 pub(super) fn dispatch_to_widget(
2049 &mut self,
2050 target: WidgetId,
2051 event: &WidgetEvent,
2052 ops: &mut dyn crate::window::WindowOps,
2053 ) {
2054 self.dispatch_to_widget_returning_handled(target, event, ops);
2055 }
2056
2057 pub(super) fn localize_event(
2073 frame: &crate::arena::LocalFrame,
2074 event: &WidgetEvent,
2075 ) -> Option<WidgetEvent> {
2076 match event {
2077 WidgetEvent::PointerDown {
2078 position,
2079 button,
2080 modifiers,
2081 pointer,
2082 } => Some(WidgetEvent::PointerDown {
2083 position: frame.to_local(*position),
2084 button: *button,
2085 modifiers: *modifiers,
2086 pointer: *pointer,
2087 }),
2088 WidgetEvent::PointerUp {
2089 position,
2090 button,
2091 modifiers,
2092 pointer,
2093 } => Some(WidgetEvent::PointerUp {
2094 position: frame.to_local(*position),
2095 button: *button,
2096 modifiers: *modifiers,
2097 pointer: *pointer,
2098 }),
2099 WidgetEvent::PointerMove {
2100 position,
2101 modifiers,
2102 pointer,
2103 } => Some(WidgetEvent::PointerMove {
2104 position: frame.to_local(*position),
2105 modifiers: *modifiers,
2106 pointer: *pointer,
2107 }),
2108 WidgetEvent::Gesture { gesture } => Some(WidgetEvent::Gesture {
2109 gesture: Self::localize_gesture(frame, gesture),
2110 }),
2111 _ => None,
2117 }
2118 }
2119
2120 fn localize_gesture(frame: &crate::arena::LocalFrame, gesture: &GestureEvent) -> GestureEvent {
2127 let loc = |p: teksilo_canvas::Point| frame.to_local(p);
2128 let tap = |t: &TapEvent| {
2129 TapEvent::new(loc(t.position), t.button, t.modifiers).with_pointer(t.pointer)
2130 };
2131 match gesture {
2132 GestureEvent::Tap(t) => GestureEvent::Tap(tap(t)),
2133 GestureEvent::DoubleTap(t) => GestureEvent::DoubleTap(tap(t)),
2134 GestureEvent::TripleTap(t) => GestureEvent::TripleTap(tap(t)),
2135 GestureEvent::LongPress(t) => GestureEvent::LongPress(tap(t)),
2136 GestureEvent::DragStarted {
2137 position,
2138 button,
2139 pointer,
2140 } => GestureEvent::DragStarted {
2141 position: loc(*position),
2142 button: *button,
2143 pointer: *pointer,
2144 },
2145 GestureEvent::DragMoved {
2146 position,
2147 delta,
2148 pointer,
2149 } => GestureEvent::DragMoved {
2150 position: loc(*position),
2151 delta: *delta,
2152 pointer: *pointer,
2153 },
2154 GestureEvent::DragEnded { position, pointer } => GestureEvent::DragEnded {
2155 position: loc(*position),
2156 pointer: *pointer,
2157 },
2158 GestureEvent::DragCancelled {
2159 position,
2160 pointer,
2161 reason,
2162 } => GestureEvent::DragCancelled {
2163 position: loc(*position),
2164 pointer: *pointer,
2165 reason: *reason,
2166 },
2167 GestureEvent::PinchStarted { center } => GestureEvent::PinchStarted {
2168 center: loc(*center),
2169 },
2170 GestureEvent::PinchChanged {
2171 center,
2172 scale,
2173 rotation,
2174 } => GestureEvent::PinchChanged {
2175 center: loc(*center),
2176 scale: *scale,
2177 rotation: *rotation,
2178 },
2179 GestureEvent::PinchEnded => GestureEvent::PinchEnded,
2180 GestureEvent::PinchCancelled { reason } => {
2181 GestureEvent::PinchCancelled { reason: *reason }
2182 }
2183 GestureEvent::Swipe {
2184 direction,
2185 velocity,
2186 } => GestureEvent::Swipe {
2187 direction: *direction,
2188 velocity: *velocity,
2189 },
2190 }
2191 }
2192
2193 pub(super) fn dispatch_to_widget_returning_handled(
2199 &mut self,
2200 target: WidgetId,
2201 event: &WidgetEvent,
2202 ops: &mut dyn crate::window::WindowOps,
2203 ) -> bool {
2204 if !self.arena.is_enabled(target) {
2205 return false;
2206 }
2207
2208 let mut ancestors = Vec::new();
2209 let mut current = self.arena.parent(target);
2210 while let Some(id) = current {
2211 ancestors.push(id);
2212 current = self.arena.parent(id);
2213 }
2214 ancestors.reverse();
2215
2216 let tap_owner: Option<WidgetId> = if matches!(
2234 event,
2235 WidgetEvent::PointerDown { .. } | WidgetEvent::PointerUp { .. }
2236 ) {
2237 let mut owner = None;
2238 let mut cur = Some(target);
2239 while let Some(id) = cur {
2240 if self.arena.get(id).is_some_and(|n| {
2241 n.any_handler(|h| h.on_tap.is_some() || h.on_long_press.is_some())
2242 }) {
2243 owner = Some(id);
2244 break;
2245 }
2246 cur = self.arena.parent(id);
2247 }
2248 owner
2249 } else {
2250 None
2251 };
2252
2253 for &id in &ancestors {
2254 let frame = self.arena.local_frame(id);
2255 let mut ctx = self
2256 .make_event_context(&mut *ops)
2257 .with_dispatch_node(id)
2258 .with_dispatch_target(target)
2259 .with_local_frame(frame);
2260 ctx.press_claimed_by_interactive_child =
2261 tap_owner.is_some_and(|owner| owner != id && self.is_descendant_of(owner, id));
2262 let localized = Self::localize_event(&frame, event);
2265 let event = localized.as_ref().unwrap_or(event);
2266 let response = if let Some(node) = self.arena.get_mut(id) {
2267 Self::try_handler_preview(node, event, &mut ctx).unwrap_or(EventResponse::Ignored)
2268 } else {
2269 EventResponse::Ignored
2270 };
2271 self.collect_from_ctx(ctx, id);
2272 if response == EventResponse::Handled {
2273 self.arena.mark_needs_paint(id);
2274 if matches!(event, WidgetEvent::PointerDown { .. }) {
2282 self.note_preview_claim(id);
2283 }
2284 return true;
2285 }
2286 }
2287
2288 let needs_layout_on_handle = matches!(
2289 event,
2290 WidgetEvent::Scroll { .. } | WidgetEvent::ScrollIntoView { .. }
2291 );
2292 let mut current = Some(target);
2293 let mut is_target = true;
2294 while let Some(id) = current {
2295 let frame = self.arena.local_frame(id);
2296 let mut ctx = self
2297 .make_event_context(&mut *ops)
2298 .with_dispatch_node(id)
2299 .with_dispatch_target(target)
2300 .with_local_frame(frame);
2301 ctx.press_claimed_by_interactive_child =
2302 tap_owner.is_some_and(|owner| owner != id && self.is_descendant_of(owner, id));
2303 let localized = Self::localize_event(&frame, event);
2306 let gesture_cx = self.recognizer_context(id);
2307 let arena_blocked = self.sequence_blocks_arena(id);
2310 let WidgetTree {
2311 arena,
2312 gesture_owners,
2313 ..
2314 } = self;
2315 let event = localized.as_ref().unwrap_or(event);
2316 let response = if let Some(node) = arena.get_mut(id) {
2317 Self::try_handler_bubble(
2318 node,
2319 event,
2320 &mut ctx,
2321 BubbleGates {
2322 fire_on_pointer_event: is_target,
2323 arena_blocked,
2324 },
2325 id,
2326 gesture_owners,
2327 gesture_cx,
2328 )
2329 .unwrap_or(EventResponse::Ignored)
2330 } else {
2331 EventResponse::Ignored
2332 };
2333 self.collect_from_ctx(ctx, id);
2334 if response == EventResponse::Handled {
2335 if needs_layout_on_handle {
2336 self.arena.mark_needs_layout(id);
2337 } else {
2338 self.arena.mark_needs_paint(id);
2339 }
2340 self.note_pointer_acceptance(id, event);
2341 if !matches!(
2356 event,
2357 WidgetEvent::PointerEnter { .. } | WidgetEvent::PointerLeave { .. }
2358 ) {
2359 return true;
2360 }
2361 }
2362 is_target = false;
2363 current = self.arena.parent(id);
2364 }
2365 false
2366 }
2367
2368 pub(super) fn dispatch_to_widget_direct(
2369 &mut self,
2370 target: WidgetId,
2371 event: &WidgetEvent,
2372 ops: &mut dyn crate::window::WindowOps,
2373 ) {
2374 self.dispatch_to_widget_direct_returning_handled(target, event, ops);
2375 }
2376
2377 pub(super) fn dispatch_to_widget_direct_returning_handled(
2387 &mut self,
2388 target: WidgetId,
2389 event: &WidgetEvent,
2390 ops: &mut dyn crate::window::WindowOps,
2391 ) -> bool {
2392 if !self.arena.is_enabled(target) {
2393 return false;
2394 }
2395
2396 let frame = self.arena.local_frame(target);
2397 let mut ctx = self
2398 .make_event_context(&mut *ops)
2399 .with_dispatch_node(target)
2400 .with_dispatch_target(target)
2401 .with_local_frame(frame);
2402 let gesture_cx = self.recognizer_context(target);
2403 let arena_blocked = self.sequence_blocks_arena(target);
2404 let WidgetTree {
2405 arena,
2406 gesture_owners,
2407 ..
2408 } = self;
2409 let response = if let Some(node) = arena.get_mut(target) {
2410 Self::try_handler_bubble(
2411 node,
2412 event,
2413 &mut ctx,
2414 BubbleGates {
2415 fire_on_pointer_event: true,
2416 arena_blocked,
2417 },
2418 target,
2419 gesture_owners,
2420 gesture_cx,
2421 )
2422 .unwrap_or(EventResponse::Ignored)
2423 } else {
2424 EventResponse::Ignored
2425 };
2426 self.collect_from_ctx(ctx, target);
2427
2428 if response == EventResponse::Handled {
2429 if matches!(
2432 event,
2433 WidgetEvent::Scroll { .. } | WidgetEvent::ScrollIntoView { .. }
2434 ) {
2435 self.arena.mark_needs_layout(target);
2436 } else {
2437 self.arena.mark_needs_paint(target);
2438 }
2439 self.note_pointer_acceptance(target, event);
2440 }
2441 response == EventResponse::Handled
2442 }
2443
2444 pub(super) fn note_pointer_acceptance(&mut self, target: WidgetId, event: &WidgetEvent) {
2453 if !matches!(
2454 event,
2455 WidgetEvent::PointerDown { .. }
2456 | WidgetEvent::PointerMove { .. }
2457 | WidgetEvent::PointerUp { .. }
2458 ) {
2459 return;
2460 }
2461 let pointer = self.current_pointer_id();
2462 if let Some(entry) = self.pointers.get_mut(pointer) {
2463 entry.last_accepted = Some(target);
2464 }
2465 }
2466
2467 fn try_handler_preview(
2468 node: &mut crate::arena::WidgetNode,
2469 event: &WidgetEvent,
2470 ctx: &mut EventContext,
2471 ) -> Option<EventResponse> {
2472 match event {
2473 WidgetEvent::KeyDown { .. }
2481 | WidgetEvent::KeyUp { .. }
2482 | WidgetEvent::ImeComposition { .. }
2483 | WidgetEvent::ImeCommit { .. } => {
2484 let has = node.external_handlers.on_key_preview.is_some()
2485 || node.handlers.on_key_preview.is_some();
2486 if !has {
2487 return None;
2488 }
2489 Some(fire_event_handler_both(
2490 &mut node.external_handlers.on_key_preview,
2491 &mut node.handlers.on_key_preview,
2492 event,
2493 ctx,
2494 ))
2495 }
2496 WidgetEvent::PointerEnter { .. } | WidgetEvent::PointerLeave { .. } => None,
2508 _ => {
2509 let has = node.external_handlers.on_pointer_event.is_some()
2510 || node.handlers.on_pointer_event.is_some();
2511 if !has {
2512 return None;
2513 }
2514 Some(fire_event_handler_both(
2515 &mut node.external_handlers.on_pointer_event,
2516 &mut node.handlers.on_pointer_event,
2517 event,
2518 ctx,
2519 ))
2520 }
2521 }
2522 }
2523
2524 fn try_handler_bubble(
2525 node: &mut crate::arena::WidgetNode,
2526 event: &WidgetEvent,
2527 ctx: &mut EventContext,
2528 gates: BubbleGates,
2529 node_id: WidgetId,
2530 gesture_owners: &mut std::collections::HashSet<WidgetId>,
2531 gesture_cx: crate::gesture::RecognizerContext<'_>,
2532 ) -> Option<EventResponse> {
2533 let BubbleGates {
2534 fire_on_pointer_event,
2535 arena_blocked,
2536 } = gates;
2537 match event {
2538 WidgetEvent::PointerEnter { .. } => {
2539 if let Some(cursor) = node.node_cursor {
2540 ctx.declared_cursor_request = Some(cursor);
2545 }
2546 let mut fired = false;
2547 if let Some(h) = node.external_handlers.on_hover.as_mut() {
2548 h(true, ctx);
2549 fired = true;
2550 }
2551 if let Some(h) = node.handlers.on_hover.as_mut() {
2552 h(true, ctx);
2553 fired = true;
2554 }
2555 if fired {
2556 Some(EventResponse::Handled)
2557 } else {
2558 node.node_cursor.map(|_| EventResponse::Handled)
2559 }
2560 }
2561 WidgetEvent::PointerLeave { .. } => {
2562 if node.node_cursor.is_some() {
2563 ctx.declared_cursor_request = Some(crate::widget::CursorIcon::Default);
2564 }
2565 let mut fired = false;
2566 if let Some(h) = node.external_handlers.on_hover.as_mut() {
2567 h(false, ctx);
2568 fired = true;
2569 }
2570 if let Some(h) = node.handlers.on_hover.as_mut() {
2571 h(false, ctx);
2572 fired = true;
2573 }
2574 if fired {
2575 Some(EventResponse::Handled)
2576 } else {
2577 node.node_cursor.map(|_| EventResponse::Handled)
2578 }
2579 }
2580 WidgetEvent::FocusGained { origin } => {
2581 let previous_origin = ctx.focus_origin.replace(*origin);
2585 let mut fired = false;
2586 if let Some(h) = node.external_handlers.on_focus.as_mut() {
2587 h(true, ctx);
2588 fired = true;
2589 }
2590 if let Some(h) = node.handlers.on_focus.as_mut() {
2591 h(true, ctx);
2592 fired = true;
2593 }
2594 ctx.focus_origin = previous_origin;
2595 fired.then_some(EventResponse::Handled)
2596 }
2597 WidgetEvent::FocusLost => {
2598 let mut fired = false;
2599 if let Some(h) = node.external_handlers.on_focus.as_mut() {
2600 h(false, ctx);
2601 fired = true;
2602 }
2603 if let Some(h) = node.handlers.on_focus.as_mut() {
2604 h(false, ctx);
2605 fired = true;
2606 }
2607 fired.then_some(EventResponse::Handled)
2608 }
2609 WidgetEvent::KeyDown { .. }
2610 | WidgetEvent::KeyUp { .. }
2611 | WidgetEvent::ImeComposition { .. }
2612 | WidgetEvent::ImeCommit { .. } => {
2613 if node.external_handlers.on_key.is_some() || node.handlers.on_key.is_some() {
2614 Some(fire_event_handler_both(
2615 &mut node.external_handlers.on_key,
2616 &mut node.handlers.on_key,
2617 event,
2618 ctx,
2619 ))
2620 } else {
2621 None
2622 }
2623 }
2624 WidgetEvent::Scroll { .. } | WidgetEvent::ScrollIntoView { .. } => {
2625 if node.external_handlers.on_scroll.is_some() || node.handlers.on_scroll.is_some() {
2626 Some(fire_event_handler_both(
2627 &mut node.external_handlers.on_scroll,
2628 &mut node.handlers.on_scroll,
2629 event,
2630 ctx,
2631 ))
2632 } else {
2633 None
2634 }
2635 }
2636 WidgetEvent::AccessAction {
2637 action,
2638 target_node,
2639 data,
2640 ..
2641 } => {
2642 let saved_a11y_source = ctx
2662 .current_source
2663 .replace(crate::telemetry::IntentSource::Accessibility);
2664 let mut any_slot = false;
2665 let mut any_handled = false;
2666 if let Some(h) = node.external_handlers.on_access_action_request.as_mut() {
2667 any_slot = true;
2668 any_handled |=
2669 h(*action, *target_node, data.clone(), ctx) == EventResponse::Handled;
2670 }
2671 if let Some(h) = node.handlers.on_access_action_request.as_mut() {
2672 any_slot = true;
2673 any_handled |=
2674 h(*action, *target_node, data.clone(), ctx) == EventResponse::Handled;
2675 }
2676 if let Some(h) = node.external_handlers.on_access_action.as_mut() {
2677 any_slot = true;
2678 any_handled |= h(*action, ctx) == EventResponse::Handled;
2679 }
2680 if let Some(h) = node.handlers.on_access_action.as_mut() {
2681 any_slot = true;
2682 any_handled |= h(*action, ctx) == EventResponse::Handled;
2683 }
2684 let user_handled = any_slot.then_some(if any_handled {
2685 EventResponse::Handled
2686 } else {
2687 EventResponse::Ignored
2688 });
2689
2690 let mut override_handled = false;
2696 if let Some(ov) = node.access_overrides.as_deref_mut() {
2697 if matches!(action, accesskit::Action::CustomAction) {
2698 if let Some(accesskit::ActionData::CustomAction(idx)) = data
2699 && let Some((_, cb)) = ov.custom_actions.get_mut(*idx as usize)
2700 {
2701 cb(ctx);
2702 override_handled = true;
2703 }
2704 } else {
2705 for (a, cb) in ov.actions.iter_mut() {
2706 if *a == *action {
2707 cb(ctx);
2708 override_handled = true;
2709 }
2710 }
2711 }
2712 }
2713
2714 ctx.current_source = saved_a11y_source;
2715 match (user_handled, override_handled) {
2716 (Some(EventResponse::Handled), _) | (_, true) => Some(EventResponse::Handled),
2717 (Some(EventResponse::Ignored), false) => Some(EventResponse::Ignored),
2718 (None, false) => None,
2719 }
2720 }
2721 WidgetEvent::Gesture { gesture } => {
2722 let matched = matches!(
2726 gesture,
2727 GestureEvent::PinchStarted { .. }
2728 | GestureEvent::PinchChanged { .. }
2729 | GestureEvent::PinchEnded
2730 | GestureEvent::Swipe { .. }
2731 | GestureEvent::DoubleTap { .. }
2732 | GestureEvent::TripleTap { .. }
2733 ) && {
2734 let has_handler = match gesture {
2735 GestureEvent::PinchStarted { .. }
2736 | GestureEvent::PinchChanged { .. }
2737 | GestureEvent::PinchEnded => node.any_handler(|h| h.on_pinch.is_some()),
2738 GestureEvent::Swipe { .. } => node.any_handler(|h| h.on_swipe.is_some()),
2739 GestureEvent::DoubleTap { .. } => {
2740 node.any_handler(|h| h.on_double_tap.is_some())
2741 }
2742 GestureEvent::TripleTap { .. } => {
2743 node.any_handler(|h| h.on_triple_tap.is_some())
2744 }
2745 _ => false,
2746 };
2747 if has_handler {
2748 Self::dispatch_recognized_gesture(node, *gesture, ctx);
2749 }
2750 has_handler
2751 };
2752 if matched {
2753 Some(EventResponse::Handled)
2754 } else {
2755 None
2756 }
2757 }
2758 WidgetEvent::PointerDown {
2759 position,
2760 button,
2761 modifiers,
2762 ..
2763 } => {
2764 if fire_on_pointer_event {
2771 let r = fire_event_handler_both(
2772 &mut node.external_handlers.on_pointer_event,
2773 &mut node.handlers.on_pointer_event,
2774 event,
2775 ctx,
2776 );
2777 if r == EventResponse::Handled {
2778 return Some(EventResponse::Handled);
2779 }
2780 }
2781 if arena_blocked {
2782 return None;
2786 }
2787 Self::ensure_gesture_arena(node, node_id, gesture_owners);
2788 if let Some(arena) = node.handlers.gesture_arena.as_mut() {
2789 let cx = gesture_cx;
2790 ctx.capture_pointer_implicit();
2805 let result = arena.process(
2806 &RawPointerEvent::Down {
2807 position: *position,
2808 button: *button,
2809 modifiers: *modifiers,
2810 pointer: cx.pointer,
2811 time: cx.now,
2812 },
2813 &cx,
2814 );
2815 if let Some(gesture) = result {
2816 Self::dispatch_recognized_gesture(node, gesture, ctx);
2817 }
2818 return Some(EventResponse::Handled);
2819 }
2820 None
2821 }
2822 WidgetEvent::PointerUp {
2823 position,
2824 button,
2825 modifiers,
2826 ..
2827 } => {
2828 if fire_on_pointer_event {
2829 let r = fire_event_handler_both(
2830 &mut node.external_handlers.on_pointer_event,
2831 &mut node.handlers.on_pointer_event,
2832 event,
2833 ctx,
2834 );
2835 if r == EventResponse::Handled {
2836 return Some(EventResponse::Handled);
2837 }
2838 }
2839 if arena_blocked {
2840 return None;
2841 }
2842 if let Some(arena) = node.handlers.gesture_arena.as_mut() {
2843 let cx = gesture_cx;
2844 let result = arena.process(
2845 &RawPointerEvent::Up {
2846 position: *position,
2847 button: *button,
2848 modifiers: *modifiers,
2849 pointer: cx.pointer,
2850 time: cx.now,
2851 },
2852 &cx,
2853 );
2854 if let Some(gesture) = result {
2855 Self::dispatch_recognized_gesture(node, gesture, ctx);
2856 }
2857 return Some(EventResponse::Handled);
2858 }
2859 None
2860 }
2861 WidgetEvent::PointerMove { position, .. } => {
2862 if fire_on_pointer_event {
2863 let r = fire_event_handler_both(
2864 &mut node.external_handlers.on_pointer_event,
2865 &mut node.handlers.on_pointer_event,
2866 event,
2867 ctx,
2868 );
2869 if r == EventResponse::Handled {
2870 return Some(EventResponse::Handled);
2871 }
2872 }
2873 if arena_blocked {
2874 return None;
2875 }
2876 if let Some(arena) = node.handlers.gesture_arena.as_mut() {
2877 let cx = gesture_cx;
2878 let result = arena.process(
2879 &RawPointerEvent::Move {
2880 position: *position,
2881 pointer: cx.pointer,
2882 time: cx.now,
2883 },
2884 &cx,
2885 );
2886 if let Some(gesture) = result {
2887 Self::dispatch_recognized_gesture(node, gesture, ctx);
2888 return Some(EventResponse::Handled);
2900 }
2901 return Some(EventResponse::Ignored);
2902 }
2903 None
2904 }
2905 WidgetEvent::PointerCancel {
2906 reason, pointer, ..
2907 } => {
2908 for slot in [
2916 &mut node.external_handlers.on_pointer_cancel,
2917 &mut node.handlers.on_pointer_cancel,
2918 ] {
2919 if let Some(handler) = slot.as_mut() {
2920 handler(pointer, *reason, ctx);
2921 }
2922 }
2923 if fire_on_pointer_event {
2924 let r = fire_event_handler_both(
2925 &mut node.external_handlers.on_pointer_event,
2926 &mut node.handlers.on_pointer_event,
2927 event,
2928 ctx,
2929 );
2930 if r == EventResponse::Handled {
2931 return Some(EventResponse::Handled);
2932 }
2933 }
2934 None
2935 }
2936 }
2937 }
2938
2939 pub(super) fn collect_from_ctx<'ops>(
2940 &mut self,
2941 mut ctx: EventContext<'ops>,
2942 source_widget: WidgetId,
2943 ) {
2944 let local_ops = ctx.window_ops.take();
2950 let mut noop = crate::window::NoopWindowOps;
2951 let ops: &mut dyn crate::window::WindowOps = match local_ops {
2952 Some(o) => o,
2953 None => &mut noop,
2954 };
2955 if ctx.frame_requested {
2956 self.request_frame();
2957 }
2958 if let Some(declared) = ctx.declared_cursor_request {
2962 self.node_declared_cursor = declared;
2963 self.current_cursor = declared;
2964 }
2965 match ctx.cursor_request {
2966 Some(crate::widget::CursorRequest::Set(cursor)) => {
2967 self.current_cursor = cursor;
2968 }
2969 Some(crate::widget::CursorRequest::Release) => {
2973 self.current_cursor = self.node_declared_cursor;
2974 }
2975 None => {}
2976 }
2977 for intent in ctx.pending_intents {
2983 self.enqueue_intent(source_widget, intent, true);
2984 }
2985 if ctx.cancel_key_capture {
2994 self.cancel_key_capture();
2995 }
2996 if let Some(slot) = ctx.pending_key_capture {
2997 self.key_capture = Some(slot);
2998 }
2999 for mutation in ctx.pending_shortcut_mutations {
3001 match mutation {
3002 crate::widget::ShortcutMutation::RebindPrimary { id, keystroke } => {
3003 self.shortcut_registry.rebind_primary(id, keystroke);
3004 }
3005 crate::widget::ShortcutMutation::RebindSecondary { id, keystroke } => {
3006 self.shortcut_registry.rebind_secondary(id, keystroke);
3007 }
3008 crate::widget::ShortcutMutation::ClearOverride { id } => {
3009 self.shortcut_registry.clear_override(&id);
3010 }
3011 }
3012 }
3013 if ctx.close_window_requested {
3014 self.close_window_requested = true;
3015 }
3016 if ctx.force_close_requested {
3017 self.force_close_requested = true;
3018 }
3019 self.pending_modal_requests
3020 .extend(ctx.modal_requests.into_iter().map(|request| {
3021 crate::modal::QueuedModalRequest {
3022 source_widget,
3023 request,
3024 }
3025 }));
3026 let focus_intents = std::mem::take(&mut ctx.pending_focus_intents);
3032 let (fallback_crossings, fallback_modal) = if focus_intents.is_empty() {
3033 (Vec::new(), None)
3034 } else {
3035 let modal = self.intent_walk(source_widget, &[]).2;
3036 (self.intent_route(source_widget, &[], true).crossings, modal)
3037 };
3038 if ctx.dismiss_modal && !self.dismiss_modal_for_source(source_widget, &mut *ops) {
3039 self.pending_modal_dismissal = true;
3040 }
3041 for callback in ctx.idle_callbacks {
3042 self.idle_queue.push_boxed(callback);
3043 }
3044 match ctx.dismiss_scope {
3045 Some(crate::widget::DismissScope::All) => {
3046 let dismissed = self.overlay_manager.dismiss_all();
3047 self.dormant_dismissed_content(&dismissed, &mut *ops);
3048 }
3049 Some(crate::widget::DismissScope::AllExceptHosts) => {
3050 self.dismiss_all_overlays_except_hosts(&mut *ops);
3051 }
3052 Some(crate::widget::DismissScope::SelfChain) => {
3053 self.dismiss_self_overlay_chain_for_source(source_widget, &mut *ops);
3054 }
3055 Some(crate::widget::DismissScope::Top) => {
3056 if let Some((_id, content_ids, focus_restore)) = self.overlay_manager.dismiss_top()
3057 {
3058 self.dormant_dismissed_content(&content_ids, &mut *ops);
3059 if let Some(restore_id) = focus_restore
3060 && self.arena.is_active(restore_id)
3061 {
3062 self.focus_ops(restore_id, &mut *ops);
3063 }
3064 }
3065 }
3066 None => {
3067 for id in ctx.overlay_dismissals {
3068 let dismissed = self.overlay_manager.dismiss(id);
3069 self.dormant_dismissed_content(&dismissed, &mut *ops);
3070 }
3071 }
3072 }
3073 for content_id in ctx.overlay_content_dismissals {
3080 if let Some(overlay_id) = self.overlay_manager.find_by_content(content_id) {
3081 let dismissed = self.overlay_manager.dismiss(overlay_id);
3082 self.dormant_dismissed_content(&dismissed, &mut *ops);
3083 }
3084 }
3085 for (id, pause) in ctx.overlay_pause_requests {
3092 if pause {
3093 self.overlay_manager.pause_auto_dismiss(id);
3094 } else {
3095 self.overlay_manager.resume_auto_dismiss(id);
3096 }
3097 }
3098 for preserve_content in ctx.dismiss_descendant_overlays {
3099 self.dismiss_child_overlays_for_source(source_widget, preserve_content, &mut *ops);
3100 }
3101 if !focus_intents.is_empty() {
3110 let modal_closed = fallback_modal
3111 .is_some_and(|content| self.overlay_manager.find_by_content(content).is_none());
3112 let start = match self.focused.filter(|id| self.arena.is_active(*id)) {
3113 Some(focus) => Some((focus, None)),
3114 None if modal_closed => self.intent_anchor().map(|root| (root, None)),
3115 None if fallback_modal.is_none()
3119 && self.overlay_manager.topmost_modal().is_some() =>
3120 {
3121 self.focus_or_modal().map(|modal| (modal, None))
3122 }
3123 None => Some((source_widget, Some(fallback_crossings))),
3124 };
3125 if let Some((start, crossings)) = start {
3126 for intent in focus_intents {
3127 match &crossings {
3128 None => self.enqueue_intent(start, intent, true),
3129 Some(crossings) => {
3130 self.enqueue_intent_crossing(start, intent, true, crossings.clone())
3131 }
3132 }
3133 }
3134 }
3135 }
3136 let deferred_row_activations =
3137 self.apply_tree_mutations(std::mem::take(&mut ctx.tree_mutations));
3138 if ctx.request_a11y_update {
3139 self.a11y_dirty = true;
3140 }
3141 if let Some(visible) = ctx.soft_keyboard_request.take() {
3142 self.request_soft_keyboard(visible);
3143 }
3144 for (message, politeness) in std::mem::take(&mut ctx.announcements) {
3147 self.announce_with(message, politeness);
3148 }
3149 for mut req in ctx.overlay_requests {
3150 if req.parent_overlay.is_none() {
3151 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3152 }
3153 if self
3154 .overlay_manager
3155 .find_by_content(req.content_id)
3156 .is_some()
3157 {
3158 continue;
3159 }
3160 let current_focus = self.focused;
3161 self.overlay_manager.show(req);
3162 self.a11y_dirty = true;
3166 if let Some(focus_id) = current_focus {
3167 self.overlay_manager.set_top_focus_restore(focus_id);
3168 }
3169 }
3170 for (mut req, band) in ctx.overlay_band_requests {
3171 if req.parent_overlay.is_none() {
3172 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3173 }
3174 if self
3175 .overlay_manager
3176 .find_by_content(req.content_id)
3177 .is_some()
3178 {
3179 continue;
3180 }
3181 let content_id = req.content_id;
3182 self.overlay_manager.show_in_band(req, band);
3183 self.arena.activate(content_id);
3184 self.a11y_dirty = true;
3185 }
3189 for (content_id, placement) in ctx.overlay_placement_updates {
3192 if let Some(overlay_id) = self.overlay_manager.find_by_content(content_id) {
3193 self.overlay_manager.update_placement(overlay_id, placement);
3194 }
3195 }
3196 for (content_id, source) in ctx.overlay_placement_sources {
3197 if let Some(overlay_id) = self.overlay_manager.find_by_content(content_id) {
3198 self.overlay_manager
3199 .set_placement_source(overlay_id, source);
3200 }
3201 }
3202 for (mut req, duration) in ctx.timed_overlay_requests {
3203 if req.parent_overlay.is_none() {
3204 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3205 }
3206 if self
3207 .overlay_manager
3208 .find_by_content(req.content_id)
3209 .is_some()
3210 {
3211 continue;
3212 }
3213 let current_focus = self.focused;
3214 let overlay_id = self.overlay_manager.show_for(req, duration);
3215 self.overlay_manager
3216 .set_shown_at_sim(overlay_id, self.sim_clock);
3217 self.a11y_dirty = true;
3218 if let Some(focus_id) = current_focus {
3219 self.overlay_manager.set_top_focus_restore(focus_id);
3220 }
3221 }
3222 for (mut req, progress, duration) in ctx.reveal_overlay_requests {
3223 if req.parent_overlay.is_none() {
3224 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3225 }
3226 if self
3227 .overlay_manager
3228 .find_by_content(req.content_id)
3229 .is_some()
3230 {
3231 continue;
3232 }
3233 let content_id = req.content_id;
3234 let current_focus = self.focused;
3235 let overlay_id = self.overlay_manager.show(req);
3236 self.overlay_manager
3237 .set_shown_at_sim(overlay_id, self.sim_clock);
3238 self.a11y_dirty = true;
3239 if let Some(focus_id) = current_focus {
3240 self.overlay_manager.set_top_focus_restore(focus_id);
3241 }
3242 self.register_animated_signal(&progress, content_id);
3248 let _ = progress.try_animate_with_options(crate::animation::AnimationRequest {
3249 target: 1.0,
3250 duration,
3251 easing: teksilo_tokens::Easing::EaseOut,
3252 frame_interval: None,
3253 looping: false,
3254 epsilon: 0.0,
3255 max_duration: None,
3256 });
3257 self.overlay_manager
3258 .attach_fade(overlay_id, progress, duration);
3259 }
3260 if let Some((pointer, capture)) = ctx.pointer_capture {
3261 let named = pointer;
3265 let pointer = pointer.unwrap_or_else(|| self.current_pointer_id());
3266 self.set_pointer_capture(pointer, capture.then_some(source_widget));
3267 if capture && ctx.explicit_capture && named.is_none() {
3271 self.note_explicit_capture(source_widget);
3272 }
3273 }
3274 if let Some(activation) = ctx.drag_activation_override.take() {
3275 self.note_drag_activation_override(source_widget, activation);
3279 }
3280 if ctx.recognized_owning_gesture {
3281 self.note_gesture_recognized(source_widget);
3284 }
3285 if !ctx.gesture_acts.is_empty() {
3286 let acts = std::mem::take(&mut ctx.gesture_acts);
3287 self.apply_gesture_acts(&acts, source_widget);
3288 }
3289 if let Some(reason) = ctx.cancel_pointer_request {
3290 let pointer = self.current_pointer_id();
3291 self.cancel_pointer(pointer, reason, &mut *ops);
3292 }
3293 for (mut request, delay, focus_target, replace_siblings) in ctx.delayed_overlay_requests {
3294 if request.parent_overlay.is_none() {
3295 request.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3296 }
3297 if self
3298 .overlay_manager
3299 .find_by_content(request.content_id)
3300 .is_some()
3301 {
3302 continue;
3303 }
3304 let content_id = request.content_id;
3305 self.pending_delayed_overlays
3306 .retain(|pending| pending.request.content_id != content_id);
3307 self.pending_delayed_overlays.push(PendingDelayedOverlay {
3308 request,
3309 delay,
3310 focus_target,
3311 replace_siblings,
3312 real_requested_at: std::time::Instant::now(),
3313 sim_requested_at: self.sim_clock,
3314 });
3315 self.arena.mark_needs_paint(source_widget);
3316 }
3317 for content_id in ctx.cancel_delayed_overlays {
3318 self.pending_delayed_overlays
3319 .retain(|pending| pending.request.content_id != content_id);
3320 }
3321 for content_id in ctx.safe_region_arm_requests {
3328 if let Some(apex) = self
3329 .previous_pointer_position
3330 .or_else(|| self.hover_owner_position())
3331 {
3332 self.overlay_manager.arm_safe_region(
3333 content_id,
3334 apex,
3335 std::time::Instant::now(),
3336 self.sim_clock,
3337 );
3338 }
3339 }
3340 for id in ctx.repaint_requests {
3341 self.arena.mark_needs_paint(id);
3342 }
3343 for id in ctx.synthetic_clicks {
3344 self.synthesise_tap_with_ops(id, &mut *ops);
3349 }
3350 if ctx.clear_focus_requested {
3353 self.clear_focus_ops(&mut *ops);
3354 }
3355 if let Some(&id) = ctx.focus_requests.last() {
3360 self.request_focus_with_ops(id, super::FocusRequestMode::SelfOrDescendant, &mut *ops);
3370 }
3371 if let Some(&id) = ctx.focus_into_requests.last() {
3372 self.request_focus_with_ops(id, super::FocusRequestMode::Into, &mut *ops);
3379 }
3380
3381 for req in ctx.scroll_into_view_requests {
3390 self.scroll_rect_into_view(
3391 req.from.unwrap_or(source_widget),
3394 req.rect,
3395 req.margin,
3396 req.align,
3397 req.motion,
3398 &mut *ops,
3399 );
3400 }
3401 for (id, margin) in ctx.scroll_widget_into_view_requests {
3408 if self.arena.get(id).is_some() {
3409 let bounds = self.arena.bounds(id);
3410 self.scroll_rect_into_view(
3411 id,
3412 bounds,
3413 margin,
3414 crate::event::ScrollAlign::Minimal,
3415 crate::event::ScrollMotion::Instant,
3416 &mut *ops,
3417 );
3418 }
3419 }
3420
3421 if let Some(&id) = ctx.highlight_tooltip_requests.last() {
3426 self.show_highlight_tooltip(id, &mut *ops);
3427 }
3428
3429 if let Some((source_widget, payload, preview_widget)) = ctx.drag_start_request {
3431 let (preview_content_id, preview_overlay_id) = if let Some(preview) = preview_widget {
3432 let content_id = self.add_boxed(preview);
3439 let overlay_id = self
3440 .overlay_manager
3441 .show(crate::overlay::OverlayRequest::new(
3442 content_id,
3443 source_widget,
3444 crate::overlay::OverlayPlacement::AtPointer(teksilo_canvas::Point::ZERO),
3445 crate::overlay::DismissBehavior::Manual,
3446 ));
3447 self.arena.mark_needs_layout(content_id);
3451 (Some(content_id), Some(overlay_id))
3452 } else {
3453 (None, None)
3454 };
3455 self.active_drag = Some(crate::drag_state::DragSession {
3456 payload,
3457 pointer: self.current_input.pointer,
3463 source_widget: Some(source_widget),
3464 is_external: false,
3465 current_position: teksilo_canvas::Point::ZERO,
3466 current_target: None,
3467 feedback: crate::drag_state::DropFeedback::NoFeedback,
3468 preview_content_id,
3469 preview_overlay_id,
3470 });
3471 self.set_current_pointer_capture(Some(source_widget));
3472 self.current_cursor = crate::widget::CursorIcon::Grabbing;
3475 }
3476 if ctx.cancel_drag {
3477 self.cancel_active_drag(&mut *ops);
3478 }
3479
3480 if let Some(theme) = ctx.theme_request {
3482 self.pending_theme_request = Some(theme);
3488 }
3489 if ctx.follow_system_request {
3490 self.pending_follow_system_request = true;
3494 }
3495 if let Some(locale) = ctx.locale_request {
3496 self.pending_locale_request = Some(locale);
3502 }
3503 if let Some(scale) = ctx.text_scale_request {
3504 self.pending_text_scale_request = Some(scale);
3509 }
3510 if !deferred_row_activations.is_empty() {
3517 self.run_with_event_context(&mut *ops, move |ctx| {
3518 for action in deferred_row_activations {
3519 action(ctx);
3520 }
3521 });
3522 }
3523 }
3524
3525 #[must_use]
3531 fn apply_tree_mutations(
3532 &mut self,
3533 mutations: Vec<crate::widget::TreeMutation>,
3534 ) -> Vec<std::rc::Rc<dyn Fn(&mut crate::widget::EventContext)>> {
3535 let mut deferred_row_activations = Vec::new();
3536 use crate::binding::BindingLevel;
3537 use crate::widget::TreeMutation;
3538
3539 for mutation in mutations {
3540 match mutation {
3541 TreeMutation::SetDormant(id) => {
3542 self.park_subtree(id);
3543 }
3544 TreeMutation::Activate(id) => self.arena.activate(id),
3545 TreeMutation::Destroy(id) => {
3546 if let Some(overlay_id) = self.overlay_manager().find_by_content(id) {
3559 self.dismiss_overlay(overlay_id);
3560 }
3561 self.destroy_subtree(id);
3562 }
3563 TreeMutation::MaterializeNow(id) => {
3567 if self.arena.get(id).is_some() {
3568 self.rebuild_single_widget(id);
3569 }
3570 }
3571 TreeMutation::RowSpaceActivate { row, fallback } => {
3572 deferred_row_activations.push(self.keyboard_toggle_in(row).unwrap_or(fallback));
3582 }
3583 TreeMutation::WithWidgetMut { id, dirty, apply } => {
3584 let existed = if let Some(any) =
3589 self.arena.get_mut(id).and_then(|n| n.widget.as_any_mut())
3590 {
3591 apply(any);
3592 true
3593 } else {
3594 false
3595 };
3596 if existed {
3597 match dirty {
3598 BindingLevel::RepaintOnly => self.arena.mark_needs_paint(id),
3599 BindingLevel::SubtreeRepaint => self.arena.mark_subtree_needs_paint(id),
3600 BindingLevel::Relayout => {
3601 self.arena.mark_needs_layout(id);
3602 self.arena.mark_ancestors_need_layout(id);
3603 }
3604 BindingLevel::Rebuild => {
3605 self.arena.mark_needs_rebuild(id);
3606 self.arena.mark_ancestors_need_layout(id);
3607 }
3608 BindingLevel::AccessibilityOnly => self.a11y_dirty = true,
3609 }
3610 }
3611 }
3612 }
3613 }
3614 deferred_row_activations
3615 }
3616
3617 pub fn hit_test(&self, point: Point) -> Option<WidgetId> {
3626 self.hit_test_excluding_overlay_and_widget(point, None, None)
3627 }
3628
3629 pub fn hit_test_for(
3640 &self,
3641 point: Point,
3642 pointer: &crate::pointer::PointerInfo,
3643 ) -> Option<WidgetId> {
3644 self.hit_test_for_excluding(point, pointer, None, None)
3645 }
3646
3647 pub fn hit_test_for_excluding(
3650 &self,
3651 point: Point,
3652 pointer: &crate::pointer::PointerInfo,
3653 exclude_overlay: Option<crate::overlay::OverlayId>,
3654 exclude_widget: Option<WidgetId>,
3655 ) -> Option<WidgetId> {
3656 let surfaces = self.text_surfaces();
3657 let read_only = |id: WidgetId| surfaces.is_read_only(id);
3658 let hit =
3659 crate::pointer::hit_slop::HitContext::new(pointer.kind, &self.effective_theme.input)
3660 .direction(self.layout_direction)
3661 .read_only_probe(&read_only);
3662 self.hit_test_with(point, exclude_overlay, exclude_widget, &hit)
3663 }
3664
3665 pub fn hit_test_excluding_overlay_and_widget(
3672 &self,
3673 point: Point,
3674 exclude_overlay: Option<crate::overlay::OverlayId>,
3675 exclude_widget: Option<WidgetId>,
3676 ) -> Option<WidgetId> {
3677 self.hit_test_with(
3678 point,
3679 exclude_overlay,
3680 exclude_widget,
3681 &crate::pointer::hit_slop::HitContext::mouse(),
3682 )
3683 }
3684
3685 pub(crate) fn hit_test_with(
3688 &self,
3689 point: Point,
3690 exclude_overlay: Option<crate::overlay::OverlayId>,
3691 exclude_widget: Option<WidgetId>,
3692 hit: &crate::pointer::hit_slop::HitContext<'_>,
3693 ) -> Option<WidgetId> {
3694 if let Some(overlay_id) = self.overlay_manager.hit_test(point) {
3695 if Some(overlay_id) == exclude_overlay {
3696 } else if let Some(overlay) = self.overlay_manager.overlay(overlay_id) {
3698 let content_id = overlay.content_id;
3702 if let Some(found) =
3703 self.arena
3704 .hit_test_in_subtree_with_slop(content_id, point, exclude_widget, hit)
3705 {
3706 return Some(found);
3707 }
3708 if !self
3726 .arena
3727 .get(content_id)
3728 .is_some_and(|node| node.event_pass_through)
3729 {
3730 return None;
3731 }
3732 }
3733 }
3734
3735 if self.overlay_manager.topmost_modal().is_some() {
3736 return None;
3737 }
3738
3739 let mut roots = self.arena.roots();
3745 let inert: Vec<WidgetId> = self
3746 .overlay_manager
3747 .stack
3748 .iter()
3749 .filter(|overlay| overlay.inert)
3750 .map(|overlay| overlay.content_id)
3751 .collect();
3752 if !inert.is_empty() {
3753 roots.retain(|root| !inert.contains(root));
3754 }
3755 self.arena
3756 .hit_test_roots_with_slop(roots, point, exclude_widget, hit)
3757 }
3758}
3759
3760fn is_context_menu_chord(key: Key, modifiers: Modifiers) -> bool {
3778 match key {
3779 Key::ContextMenu => modifiers == Modifiers::NONE,
3780 Key::F10 => modifiers == Modifiers::SHIFT,
3781 #[cfg(target_os = "macos")]
3782 Key::M => modifiers == Modifiers::CTRL | Modifiers::SHIFT,
3783 _ => false,
3784 }
3785}
3786
3787#[cfg(test)]
3788mod tests {
3789 use super::*;
3790 use crate::test_widgets::FillWidget;
3791 use crate::widget::CursorIcon;
3792 use crate::widget_builder::WidgetBuilder;
3793
3794 #[test]
3795 fn pointer_enter_leave_synthesized() {
3796 let mut tree = WidgetTree::new();
3797 let widget = tree.add(FillWidget::new());
3798 tree.layout(SizeProposal::exact(100.0, 50.0));
3799 tree.pointer_move(Point::new(50.0, 25.0));
3800 assert_eq!(tree.hovered(), Some(widget));
3801 tree.pointer_move(Point::new(200.0, 200.0));
3802 assert_eq!(tree.hovered(), None);
3803 }
3804
3805 #[test]
3806 fn pointer_hover_updates_current_cursor() {
3807 let mut tree = WidgetTree::new();
3808 tree.add(FillWidget::new().cursor(CursorIcon::ColResize));
3809 tree.layout(SizeProposal::exact(100.0, 50.0));
3810
3811 tree.pointer_move(Point::new(50.0, 25.0));
3812 assert_eq!(tree.current_cursor(), CursorIcon::ColResize);
3813
3814 tree.pointer_move(Point::new(200.0, 200.0));
3815 assert_eq!(tree.current_cursor(), CursorIcon::Default);
3816 }
3817
3818 #[derive(Debug)]
3821 struct SelfDeclared(CursorIcon);
3822
3823 impl crate::widget::Widget for SelfDeclared {
3824 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
3825 ctx.apply_self_handlers(crate::widget_builder::HandlerSet::new().cursor(self.0));
3826 Vec::new()
3827 }
3828
3829 fn layout_response(
3830 &self,
3831 proposal: SizeProposal,
3832 _ctx: &crate::widget::LayoutContext,
3833 ) -> crate::widget::LayoutResponse {
3834 proposal.resolve(100.0, 50.0).into()
3835 }
3836 }
3837
3838 #[test]
3844 fn declared_cursor_reads_what_a_node_asks_for_and_current_cursor_follows_the_hover() {
3845 let over = Point::new(50.0, 25.0);
3846 let away = Point::new(200.0, 200.0);
3847
3848 let mut tree = WidgetTree::new();
3850 let built = tree.add(FillWidget::new().cursor(CursorIcon::ColResize));
3851 tree.layout(SizeProposal::exact(100.0, 50.0));
3852 assert_eq!(tree.declared_cursor(built), Some(CursorIcon::ColResize));
3853 assert_eq!(
3854 tree.current_cursor(),
3855 CursorIcon::Default,
3856 "not hovered yet"
3857 );
3858 tree.pointer_move(over);
3859 assert_eq!(tree.current_cursor(), CursorIcon::ColResize);
3860 assert_eq!(tree.declared_cursor(built), Some(CursorIcon::ColResize));
3861 tree.pointer_move(away);
3862 assert_eq!(tree.current_cursor(), CursorIcon::Default);
3863 assert_eq!(
3864 tree.declared_cursor(built),
3865 Some(CursorIcon::ColResize),
3866 "the declaration does not depend on the pointer",
3867 );
3868
3869 let mut tree = WidgetTree::new();
3871 let own = tree.add(SelfDeclared(CursorIcon::Crosshair));
3872 tree.layout(SizeProposal::exact(100.0, 50.0));
3873 assert_eq!(tree.declared_cursor(own), Some(CursorIcon::Crosshair));
3874 tree.pointer_move(over);
3875 assert_eq!(tree.current_cursor(), CursorIcon::Crosshair);
3876 tree.pointer_move(away);
3877 assert_eq!(tree.current_cursor(), CursorIcon::Default);
3878
3879 let mut tree = WidgetTree::new();
3881 let plain = tree.add(FillWidget::new());
3882 tree.layout(SizeProposal::exact(100.0, 50.0));
3883 assert_eq!(tree.declared_cursor(plain), None);
3884 let mut tree = WidgetTree::new();
3885 let explicit = tree.add(FillWidget::new().cursor(CursorIcon::Default));
3886 tree.layout(SizeProposal::exact(100.0, 50.0));
3887 assert_eq!(
3888 tree.declared_cursor(explicit),
3889 Some(CursorIcon::Default),
3890 "declaring the default cursor is not declaring nothing",
3891 );
3892 }
3893
3894 #[test]
3904 fn release_cursor_hands_the_cursor_back_to_the_node_that_declared_it() {
3905 let mut tree = WidgetTree::new();
3909 tree.add(
3910 FillWidget::new()
3911 .cursor(CursorIcon::ColResize)
3912 .on_pointer_event(|event, ctx| {
3913 if let WidgetEvent::PointerMove { position, .. } = event {
3914 if position.x < 30.0 {
3915 ctx.set_cursor(CursorIcon::Crosshair);
3916 } else {
3917 ctx.release_cursor();
3918 }
3919 }
3920 EventResponse::Ignored
3921 }),
3922 );
3923 tree.layout(SizeProposal::exact(100.0, 50.0));
3924
3925 tree.pointer_move(Point::new(10.0, 25.0));
3926 assert_eq!(
3927 tree.current_cursor(),
3928 CursorIcon::Crosshair,
3929 "the handler outranks the node it is attached to",
3930 );
3931 tree.pointer_move(Point::new(60.0, 25.0));
3932 assert_eq!(
3933 tree.current_cursor(),
3934 CursorIcon::ColResize,
3935 "and hands it back to the node, not to Default — no enter/leave \
3936 fires on a move within one node, so nothing else could",
3937 );
3938 tree.pointer_move(Point::new(10.0, 25.0));
3939 assert_eq!(
3940 tree.current_cursor(),
3941 CursorIcon::Crosshair,
3942 "and can take it again"
3943 );
3944 tree.pointer_move(Point::new(200.0, 200.0));
3945 assert_eq!(
3946 tree.current_cursor(),
3947 CursorIcon::Default,
3948 "leaving the node clears both the override and the declaration",
3949 );
3950 }
3951
3952 #[test]
3956 fn release_cursor_is_a_no_op_with_nothing_to_undo() {
3957 let mut tree = WidgetTree::new();
3958 tree.add(
3959 FillWidget::new()
3960 .cursor(CursorIcon::ColResize)
3961 .on_pointer_event(|event, ctx| {
3962 if matches!(event, WidgetEvent::PointerMove { .. }) {
3963 ctx.release_cursor();
3964 }
3965 EventResponse::Ignored
3966 }),
3967 );
3968 tree.layout(SizeProposal::exact(100.0, 50.0));
3969 tree.pointer_move(Point::new(50.0, 25.0));
3970 assert_eq!(tree.current_cursor(), CursorIcon::ColResize);
3971
3972 let mut tree = WidgetTree::new();
3974 tree.add(FillWidget::new().on_pointer_event(|event, ctx| {
3975 if let WidgetEvent::PointerMove { position, .. } = event {
3976 if position.x < 30.0 {
3977 ctx.set_cursor(CursorIcon::Crosshair);
3978 } else {
3979 ctx.release_cursor();
3980 }
3981 }
3982 EventResponse::Ignored
3983 }));
3984 tree.layout(SizeProposal::exact(100.0, 50.0));
3985 tree.pointer_move(Point::new(10.0, 25.0));
3986 assert_eq!(tree.current_cursor(), CursorIcon::Crosshair);
3987 tree.pointer_move(Point::new(60.0, 25.0));
3988 assert_eq!(
3989 tree.current_cursor(),
3990 CursorIcon::Default,
3991 "nothing declared a cursor for this chain, so the hand-back \
3992 resolves to Default",
3993 );
3994 }
3995
3996 #[test]
4004 fn an_on_hover_override_does_not_erase_the_declaration_it_outranks() {
4005 let mut tree = WidgetTree::new();
4006 tree.add(
4007 FillWidget::new()
4008 .cursor(CursorIcon::ColResize)
4009 .on_hover(|entered, ctx| {
4010 if entered {
4011 ctx.set_cursor(CursorIcon::Crosshair);
4012 }
4013 })
4014 .on_pointer_event(|event, ctx| {
4015 if let WidgetEvent::PointerMove { position, .. } = event
4016 && position.x >= 30.0
4017 {
4018 ctx.release_cursor();
4019 }
4020 EventResponse::Ignored
4021 }),
4022 );
4023 tree.layout(SizeProposal::exact(100.0, 50.0));
4024
4025 tree.pointer_move(Point::new(10.0, 25.0));
4026 assert_eq!(
4027 tree.current_cursor(),
4028 CursorIcon::Crosshair,
4029 "on_hover runs after the node cursor is applied, so it wins",
4030 );
4031 tree.pointer_move(Point::new(60.0, 25.0));
4032 assert_eq!(
4033 tree.current_cursor(),
4034 CursorIcon::ColResize,
4035 "and the node's declaration survived being overridden",
4036 );
4037 }
4038
4039 #[derive(Debug)]
4042 struct Bumpable {
4043 value: i32,
4044 }
4045
4046 impl crate::widget::Widget for Bumpable {
4047 fn layout_response(
4048 &self,
4049 proposal: SizeProposal,
4050 _ctx: &crate::widget::LayoutContext,
4051 ) -> crate::widget::LayoutResponse {
4052 proposal.resolve(10.0, 10.0).into()
4053 }
4054 fn as_any(&self) -> Option<&dyn std::any::Any> {
4055 Some(self)
4056 }
4057 fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
4058 Some(self)
4059 }
4060 }
4061
4062 #[test]
4063 fn with_widget_mut_applies_and_dirty_marks() {
4064 let mut tree = WidgetTree::new();
4065 let id = tree.add(Bumpable { value: 0 });
4066 tree.layout(SizeProposal::exact(100.0, 100.0));
4067
4068 let mut ctx = EventContext::new();
4069 ctx.with_widget_mut::<Bumpable>(id, crate::binding::BindingLevel::Relayout, |b| {
4070 b.value = 42;
4071 });
4072 tree.collect_from_ctx(ctx, id);
4073
4074 let value = tree
4075 .widget_as_any(id)
4076 .and_then(|a| a.downcast_ref::<Bumpable>())
4077 .map(|b| b.value);
4078 assert_eq!(
4079 value,
4080 Some(42),
4081 "the deferred closure must mutate the live widget"
4082 );
4083 assert!(
4084 tree.needs_layout(),
4085 "Relayout dirty level must mark the tree for relayout"
4086 );
4087 }
4088
4089 #[test]
4090 #[cfg(debug_assertions)]
4091 #[should_panic(expected = "not the requested type")]
4092 fn with_widget_mut_wrong_type_panics_in_debug() {
4093 struct Other;
4094 let mut tree = WidgetTree::new();
4095 let id = tree.add(Bumpable { value: 0 });
4096 let mut ctx = EventContext::new();
4097 ctx.with_widget_mut::<Other>(
4098 id,
4099 crate::binding::BindingLevel::RepaintOnly,
4100 |_o: &mut Other| {},
4101 );
4102 tree.collect_from_ctx(ctx, id);
4105 }
4106
4107 #[test]
4108 fn with_widget_mut_closure_may_fire_observed_signals() {
4109 use crate::signal::Signal;
4117 let mut tree = WidgetTree::new();
4118 let id = tree.add(Bumpable { value: 0 });
4119 tree.layout(SizeProposal::exact(100.0, 100.0));
4120
4121 let trigger = Signal::new(0_u64);
4122 let echo = Signal::new(0_u64);
4123 let echo_for_obs = echo.clone();
4124 let _obs = trigger.observe(move |v| echo_for_obs.set(*v));
4125
4126 let trigger_in = trigger.clone();
4127 let mut ctx = EventContext::new();
4128 ctx.with_widget_mut::<Bumpable>(id, crate::binding::BindingLevel::RepaintOnly, move |b| {
4129 b.value = 7;
4130 trigger_in.set(99);
4132 });
4133 tree.collect_from_ctx(ctx, id); assert_eq!(
4136 echo.get(),
4137 99,
4138 "the observer ran during the deferred mutation"
4139 );
4140 let value = tree
4141 .widget_as_any(id)
4142 .and_then(|a| a.downcast_ref::<Bumpable>())
4143 .map(|b| b.value);
4144 assert_eq!(value, Some(7));
4145 }
4146
4147 #[test]
4148 fn request_accessibility_update_forces_rewalk() {
4149 let mut tree = WidgetTree::new();
4150 let id = tree.add(Bumpable { value: 0 });
4151 tree.layout(SizeProposal::exact(100.0, 100.0));
4152 let _ = tree.sync_accessibility(); assert!(
4154 !tree.a11y_dirty,
4155 "sync_accessibility should clear the dirty flag"
4156 );
4157
4158 let mut ctx = EventContext::new();
4159 ctx.request_accessibility_update();
4160 tree.collect_from_ctx(ctx, id);
4161 assert!(
4162 tree.a11y_dirty,
4163 "request_accessibility_update must force an AT re-walk"
4164 );
4165 }
4166
4167 #[test]
4168 fn rebuild_dirties_accessibility_tree() {
4169 let mut tree = WidgetTree::new();
4176 let id = tree.add(Bumpable { value: 0 });
4177 tree.layout(SizeProposal::exact(100.0, 100.0));
4178 let _ = tree.sync_accessibility(); assert!(
4180 !tree.a11y_dirty,
4181 "sync_accessibility should clear the dirty flag"
4182 );
4183
4184 tree.arena_mark_needs_rebuild_for_testing(id);
4187 tree.layout(SizeProposal::exact(100.0, 100.0));
4188 assert!(
4189 tree.a11y_dirty,
4190 "a rebuild must dirty the AT tree so the next sync re-walks"
4191 );
4192 }
4193
4194 #[test]
4195 fn bound_access_label_change_dirties_accessibility_tree() {
4196 use crate::signal::Signal;
4197 use crate::test_widgets::FillWidget;
4198 use crate::widget_builder::WidgetBuilder;
4199
4200 let label = Signal::new("first".to_string());
4206 let mut tree = WidgetTree::new();
4207 let _id = tree.add(FillWidget::new().access_label(label.clone()));
4208 tree.layout(SizeProposal::exact(100.0, 100.0));
4209 let _ = tree.sync_accessibility(); assert!(!tree.a11y_dirty, "sync_accessibility should clear the flag");
4211
4212 label.set("second".to_string());
4213 tree.layout(SizeProposal::exact(100.0, 100.0));
4214 assert!(
4215 tree.a11y_dirty,
4216 "changing a bound access_label must dirty the AT tree"
4217 );
4218 }
4219
4220 #[test]
4221 fn disabled_ancestor_blocks_event_to_descendant() {
4222 use crate::signal::Signal;
4223 use crate::test_widgets::StackWidget;
4224 use std::cell::Cell;
4225 use std::rc::Rc;
4226
4227 let tapped = Rc::new(Cell::new(false));
4228 let flag = tapped.clone();
4229 let enabled = Signal::new(true);
4230
4231 let mut tree = WidgetTree::new();
4232 let child = tree.add(FillWidget::new().on_tap(move |_pos, _ctx| {
4233 flag.set(true);
4234 }));
4235 let parent = tree.add(StackWidget::new().child(child));
4236 tree.enabled_when(parent, enabled.clone());
4237 tree.layout(SizeProposal::exact(100.0, 50.0));
4238
4239 enabled.set(false);
4240 tree.click(child);
4241 assert!(
4242 !tapped.get(),
4243 "disabled ancestor should block descendant tap"
4244 );
4245
4246 enabled.set(true);
4247 tree.click(child);
4248 assert!(tapped.get(), "re-enabling should restore dispatch");
4249 }
4250
4251 #[test]
4252 fn pointer_positions_are_widget_local_at_nonzero_origin() {
4253 use crate::event::{Modifiers, PointerButton};
4254 use crate::test_widgets::InsetWidget;
4255 use std::cell::Cell;
4256 use std::rc::Rc;
4257
4258 let tap_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
4260 let down_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
4261 let drag_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
4262 let (tp, dp, gp) = (tap_pos.clone(), down_pos.clone(), drag_pos.clone());
4263
4264 let mut tree = WidgetTree::new();
4265 let child = tree.add(
4266 FillWidget::new()
4267 .on_tap(move |ev, _ctx| tp.set(Some(ev.position)))
4268 .on_pointer_event(move |ev, _ctx| {
4269 if let WidgetEvent::PointerDown { position, .. } = ev {
4270 dp.set(Some(*position));
4271 }
4272 crate::event::EventResponse::Ignored
4273 })
4274 .on_drag(move |phase, _ctx| {
4275 use crate::gesture::DragPhase;
4276 match phase {
4277 DragPhase::Started { position, .. }
4278 | DragPhase::Moved { position, .. }
4279 | DragPhase::Ended { position, .. } => gp.set(Some(position)),
4280 _ => {}
4281 }
4282 }),
4283 );
4284 let inset = tree.add(InsetWidget::new(20.0).set_child(child));
4285 let _ = inset;
4286 tree.layout(SizeProposal::exact(200.0, 200.0));
4287 assert_eq!(tree.bounds(child).origin(), Point::new(20.0, 20.0));
4288
4289 tree.dispatch_event(WidgetEvent::pointer_down(
4291 Point::new(50.0, 40.0),
4292 PointerButton::Primary,
4293 Modifiers::NONE,
4294 ));
4295 assert_eq!(
4296 down_pos.get(),
4297 Some(Point::new(30.0, 20.0)),
4298 "on_pointer_event PointerDown must be widget-local"
4299 );
4300 tree.dispatch_event(WidgetEvent::pointer_up(
4301 Point::new(50.0, 40.0),
4302 PointerButton::Primary,
4303 Modifiers::NONE,
4304 ));
4305 assert_eq!(
4306 tap_pos.get(),
4307 Some(Point::new(30.0, 20.0)),
4308 "on_tap position must be widget-local"
4309 );
4310
4311 tree.dispatch_event(WidgetEvent::pointer_down(
4314 Point::new(50.0, 40.0),
4315 PointerButton::Primary,
4316 Modifiers::NONE,
4317 ));
4318 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(65.0, 55.0)));
4321 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(90.0, 70.0)));
4322 assert_eq!(
4323 drag_pos.get(),
4324 Some(Point::new(70.0, 50.0)),
4325 "on_drag position must be widget-local"
4326 );
4327 }
4328
4329 #[test]
4330 fn dormant_widget_not_hit_tested() {
4331 let mut tree = WidgetTree::new();
4332 let widget = tree.add(FillWidget::new());
4333 tree.layout(SizeProposal::exact(100.0, 50.0));
4334
4335 tree.pointer_move(Point::new(50.0, 25.0));
4336 assert_eq!(tree.hovered(), Some(widget));
4337
4338 tree.set_dormant(widget);
4339 tree.pointer_move(Point::new(200.0, 200.0));
4340 tree.pointer_move(Point::new(50.0, 25.0));
4341 assert_eq!(tree.hovered(), None);
4342 }
4343
4344 #[test]
4345 fn ancestor_pointer_handler_does_not_suppress_descendant_hover() {
4346 use crate::event::EventResponse;
4347 use crate::test_widgets::StackWidget;
4348 use std::cell::Cell;
4349 use std::rc::Rc;
4350
4351 let hovered = Rc::new(Cell::new(false));
4353 let h = hovered.clone();
4354
4355 let mut tree = WidgetTree::new();
4356 let child = tree.add(FillWidget::new().on_hover(move |entered, _ctx| h.set(entered)));
4357 tree.add(
4362 StackWidget::new()
4363 .child(child)
4364 .on_pointer_event(|_event, _ctx| EventResponse::Handled),
4365 );
4366 tree.layout(SizeProposal::exact(100.0, 50.0));
4367
4368 tree.pointer_move(Point::new(50.0, 25.0));
4369 assert!(
4370 hovered.get(),
4371 "a greedy ancestor on_pointer_event must NOT swallow the child's PointerEnter"
4372 );
4373
4374 tree.pointer_move(Point::new(500.0, 500.0));
4375 assert!(
4376 !hovered.get(),
4377 "PointerLeave must likewise reach the child despite the ancestor"
4378 );
4379 }
4380
4381 #[test]
4389 fn a_child_hover_handler_does_not_swallow_its_ancestors() {
4390 use crate::test_widgets::StackWidget;
4391 use std::cell::Cell;
4392 use std::rc::Rc;
4393
4394 let (row, button) = (Rc::new(Cell::new(false)), Rc::new(Cell::new(false)));
4395 let (r, b) = (row.clone(), button.clone());
4396
4397 let mut tree = WidgetTree::new();
4398 let child = tree.add(FillWidget::new().on_hover(move |entered, _ctx| b.set(entered)));
4399 tree.add(
4400 StackWidget::new()
4401 .child(child)
4402 .on_hover(move |entered, _ctx| r.set(entered)),
4403 );
4404 tree.layout(SizeProposal::exact(100.0, 50.0));
4405
4406 tree.pointer_move(Point::new(50.0, 25.0));
4407 assert!(button.get(), "the child is hovered");
4408 assert!(row.get(), "and so is the row it is inside");
4409
4410 tree.pointer_move(Point::new(500.0, 500.0));
4411 assert!(!button.get(), "the child heard the leave");
4412 assert!(
4413 !row.get(),
4414 "and so did the row — a container is not still hovered because the \
4415 pointer left it through a button"
4416 );
4417 }
4418
4419 #[test]
4426 fn key_preview_consumes_before_focused_on_key() {
4427 use crate::event::EventResponse;
4430 use crate::test_widgets::StackWidget;
4431 use std::cell::Cell;
4432 use std::rc::Rc;
4433
4434 let leaf_fired = Rc::new(Cell::new(false));
4435 let leaf_flag = leaf_fired.clone();
4436 let preview_fired = Rc::new(Cell::new(false));
4437 let preview_flag = preview_fired.clone();
4438
4439 let mut tree = WidgetTree::new();
4440 let leaf = tree.add(FillWidget::new().focusable().on_key(move |event, _c| {
4441 if matches!(event, WidgetEvent::KeyDown { .. }) {
4444 leaf_flag.set(true);
4445 }
4446 EventResponse::Handled
4447 }));
4448 let mid = tree.add(StackWidget::new().child(leaf));
4449 let _root = tree.add(
4450 StackWidget::new()
4451 .child(mid)
4452 .on_key_preview(move |event, _c| match event {
4453 WidgetEvent::KeyDown {
4454 key: Key::Enter, ..
4455 } => {
4456 preview_flag.set(true);
4457 EventResponse::Handled
4458 }
4459 _ => EventResponse::Ignored,
4460 }),
4461 );
4462
4463 tree.layout(SizeProposal::exact(100.0, 50.0));
4464 tree.focus(leaf);
4465 tree.press_key(Key::Enter, Modifiers::NONE);
4466
4467 assert!(
4468 preview_fired.get(),
4469 "ancestor on_key_preview must fire for KeyDown on a focused descendant"
4470 );
4471 assert!(
4472 !leaf_fired.get(),
4473 "consuming the event in preview must prevent the focused widget's on_key from running"
4474 );
4475 }
4476
4477 #[test]
4478 fn key_preview_falls_through_when_returning_ignored() {
4479 use crate::event::EventResponse;
4482 use crate::test_widgets::StackWidget;
4483 use std::cell::Cell;
4484 use std::rc::Rc;
4485
4486 let leaf_fired = Rc::new(Cell::new(false));
4487 let leaf_flag = leaf_fired.clone();
4488 let preview_fired = Rc::new(Cell::new(false));
4489 let preview_flag = preview_fired.clone();
4490
4491 let mut tree = WidgetTree::new();
4492 let leaf = tree.add(FillWidget::new().focusable().on_key(move |_e, _c| {
4493 leaf_flag.set(true);
4494 EventResponse::Handled
4495 }));
4496 let mid = tree.add(StackWidget::new().child(leaf));
4497 let _root = tree.add(
4498 StackWidget::new()
4499 .child(mid)
4500 .on_key_preview(move |_event, _c| {
4501 preview_flag.set(true);
4502 EventResponse::Ignored
4503 }),
4504 );
4505
4506 tree.layout(SizeProposal::exact(100.0, 50.0));
4507 tree.focus(leaf);
4508 tree.press_key(Key::Enter, Modifiers::NONE);
4509
4510 assert!(preview_fired.get(), "preview must always be invoked");
4511 assert!(
4512 leaf_fired.get(),
4513 "preview returning Ignored must not block the focused widget's on_key"
4514 );
4515 }
4516
4517 #[test]
4518 fn key_preview_excludes_focused_target_itself() {
4519 use crate::event::EventResponse;
4523 use std::cell::Cell;
4524 use std::rc::Rc;
4525
4526 let preview_on_target = Rc::new(Cell::new(false));
4527 let pf = preview_on_target.clone();
4528
4529 let mut tree = WidgetTree::new();
4530 let leaf = tree.add(FillWidget::new().focusable().on_key_preview(move |_e, _c| {
4531 pf.set(true);
4532 EventResponse::Handled
4533 }));
4534 tree.layout(SizeProposal::exact(100.0, 50.0));
4535 tree.focus(leaf);
4536 tree.press_key(Key::Enter, Modifiers::NONE);
4537
4538 assert!(
4539 !preview_on_target.get(),
4540 "the focused widget itself must not see its own on_key_preview"
4541 );
4542 }
4543
4544 #[test]
4545 fn key_preview_root_to_target_order() {
4546 use crate::event::EventResponse;
4550 use crate::test_widgets::StackWidget;
4551 use std::cell::RefCell;
4552 use std::rc::Rc;
4553
4554 let order: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
4555 let outer_log = order.clone();
4556 let inner_log = order.clone();
4557
4558 let mut tree = WidgetTree::new();
4559 let leaf = tree.add(FillWidget::new().focusable());
4560 let inner = tree.add(
4561 StackWidget::new()
4562 .child(leaf)
4563 .on_key_preview(move |event, _c| {
4564 if matches!(event, WidgetEvent::KeyDown { .. }) {
4565 inner_log.borrow_mut().push("inner");
4566 }
4567 EventResponse::Ignored
4568 }),
4569 );
4570 let _outer = tree.add(
4571 StackWidget::new()
4572 .child(inner)
4573 .on_key_preview(move |event, _c| {
4574 if matches!(event, WidgetEvent::KeyDown { .. }) {
4575 outer_log.borrow_mut().push("outer");
4576 }
4577 EventResponse::Ignored
4578 }),
4579 );
4580
4581 tree.layout(SizeProposal::exact(100.0, 50.0));
4582 tree.focus(leaf);
4583 tree.dispatch_event(WidgetEvent::KeyDown {
4584 key: Key::Enter,
4585 modifiers: Modifiers::NONE,
4586 text: None,
4587 });
4588
4589 assert_eq!(
4590 *order.borrow(),
4591 vec!["outer", "inner"],
4592 "preview must walk root → parent-of-target"
4593 );
4594 }
4595
4596 #[test]
4597 fn access_action_routes_to_cursored_target_not_focus() {
4598 use crate::signal::Signal;
4602 let a_fired = Signal::new(false);
4603 let b_fired = Signal::new(false);
4604 let a_cb = a_fired.clone();
4605 let b_cb = b_fired.clone();
4606
4607 let mut tree = WidgetTree::new();
4608 let a = tree.add(
4609 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| a_cb.set(true)),
4610 );
4611 let b = tree.add(
4612 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| b_cb.set(true)),
4613 );
4614 tree.layout(SizeProposal::exact(200.0, 100.0));
4615
4616 tree.focus(a);
4617 tree.dispatch_event(WidgetEvent::AccessAction {
4618 action: accesskit::Action::Click,
4619 target: Some(b),
4620 target_node: crate::accessibility::widget_id_to_node_id(b),
4621 data: None,
4622 });
4623
4624 assert!(b_fired.get(), "the cursored target must receive the action");
4625 assert!(
4626 !a_fired.get(),
4627 "the keyboard-focused widget must NOT receive an action targeting another node"
4628 );
4629 }
4630
4631 #[test]
4632 fn access_action_without_target_is_dropped_not_redirected_to_focus() {
4633 use crate::signal::Signal;
4636 let fired = Signal::new(false);
4637 let cb = fired.clone();
4638
4639 let mut tree = WidgetTree::new();
4640 let widget = tree.add(
4641 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| cb.set(true)),
4642 );
4643 tree.layout(SizeProposal::exact(200.0, 100.0));
4644
4645 tree.focus(widget);
4646 tree.dispatch_event(WidgetEvent::AccessAction {
4647 action: accesskit::Action::Click,
4648 target: None,
4649 target_node: crate::accessibility::root_node_id(),
4650 data: None,
4651 });
4652
4653 assert!(
4654 !fired.get(),
4655 "a target-less action must not be redirected to the focused widget"
4656 );
4657 }
4658
4659 #[test]
4666 fn shortcut_fires_matching_action_on_source_widget() {
4667 use crate::action::Action;
4668 use crate::shortcut::{KeyStroke, Shortcut};
4669 use std::cell::Cell;
4670 use std::rc::Rc;
4671
4672 let fired = Rc::new(Cell::new(false));
4673 let fired_flag = fired.clone();
4674
4675 let mut tree = WidgetTree::new();
4676 let widget = tree.add(FillWidget::new().focusable());
4677 tree.push_action(
4678 widget,
4679 Action::new("app.save").on_invoke(move |_intent, _ctx| {
4680 fired_flag.set(true);
4681 }),
4682 );
4683 tree.shortcut_registry_mut().register(
4684 Shortcut::new("app.save")
4685 .primary(KeyStroke::command(Key::S))
4686 .build(),
4687 );
4688
4689 tree.layout(SizeProposal::exact(100.0, 50.0));
4690 tree.focus(widget);
4691
4692 tree.press_key(Key::S, Modifiers::COMMAND);
4693 assert!(fired.get(), "matching action must fire on KeyDown");
4694 }
4695
4696 #[test]
4697 fn global_shortcut_fires_without_focused_widget() {
4698 use crate::action::Action;
4699 use crate::shortcut::{KeyStroke, Shortcut};
4700 use std::cell::Cell;
4701 use std::rc::Rc;
4702
4703 let fired = Rc::new(Cell::new(false));
4707 let fired_flag = fired.clone();
4708
4709 let mut tree = WidgetTree::new();
4710 let root = tree.add(FillWidget::new());
4711 tree.push_action(
4712 root,
4713 Action::new("app.save").on_invoke(move |_intent, _ctx| {
4714 fired_flag.set(true);
4715 }),
4716 );
4717 tree.shortcut_registry_mut().register(
4718 Shortcut::new("app.save")
4719 .primary(KeyStroke::command(Key::S))
4720 .build(),
4721 );
4722
4723 tree.layout(SizeProposal::exact(100.0, 50.0));
4724 tree.press_key(Key::S, Modifiers::COMMAND);
4727 assert!(
4728 fired.get(),
4729 "global shortcut must fire without a focused widget"
4730 );
4731 }
4732
4733 #[test]
4734 fn global_shortcut_fires_after_focused_widget_destroyed() {
4735 use crate::action::Action;
4736 use crate::shortcut::{KeyStroke, Shortcut};
4737 use std::cell::Cell;
4738 use std::rc::Rc;
4739
4740 let fired = Rc::new(Cell::new(false));
4745 let fired_flag = fired.clone();
4746
4747 let mut tree = WidgetTree::new();
4748 let root = tree.add(FillWidget::new());
4749 let focusable = tree.add_child(root, FillWidget::new().focusable());
4750 tree.push_action(
4751 root,
4752 Action::new("app.save").on_invoke(move |_intent, _ctx| {
4753 fired_flag.set(true);
4754 }),
4755 );
4756 tree.shortcut_registry_mut().register(
4757 Shortcut::new("app.save")
4758 .primary(KeyStroke::command(Key::S))
4759 .build(),
4760 );
4761
4762 tree.layout(SizeProposal::exact(100.0, 50.0));
4763 tree.focus(focusable);
4764 assert_eq!(tree.focused(), Some(focusable));
4765
4766 tree.destroy_subtree(focusable);
4769 assert_eq!(tree.focused(), None, "focus must clear when destroyed");
4770
4771 tree.press_key(Key::S, Modifiers::COMMAND);
4772 assert!(
4773 fired.get(),
4774 "global shortcut must still fire after the focused widget is destroyed"
4775 );
4776 }
4777
4778 #[test]
4779 fn scoped_shortcut_matches_only_when_focus_in_scope() {
4780 use crate::action::Action;
4781 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
4782 use std::cell::Cell;
4783 use std::rc::Rc;
4784
4785 let fired = Rc::new(Cell::new(0));
4786 let fired_flag = fired.clone();
4787
4788 let mut tree = WidgetTree::new();
4789 let scope_root = tree.add(FillWidget::new().focusable());
4790 let inside = tree.add_child(scope_root, FillWidget::new().focusable());
4791 let outside = tree.add(FillWidget::new().focusable());
4792
4793 tree.push_action(
4794 scope_root,
4795 Action::new("editor.find").on_invoke(move |_i, _c| {
4796 fired_flag.set(fired_flag.get() + 1);
4797 }),
4798 );
4799 tree.shortcut_registry_mut().register(
4800 Shortcut::new("editor.find")
4801 .primary(KeyStroke::command(Key::F))
4802 .scope(ShortcutScope::Scoped(scope_root))
4803 .build(),
4804 );
4805
4806 tree.layout(SizeProposal::exact(200.0, 100.0));
4807
4808 tree.focus(outside);
4810 tree.press_key(Key::F, Modifiers::COMMAND);
4811 assert_eq!(
4812 fired.get(),
4813 0,
4814 "scoped shortcut must not fire outside scope"
4815 );
4816
4817 tree.focus(inside);
4819 tree.press_key(Key::F, Modifiers::COMMAND);
4820 assert_eq!(
4821 fired.get(),
4822 1,
4823 "scoped shortcut must fire when focus in scope"
4824 );
4825 }
4826
4827 #[test]
4828 fn same_chord_scoped_first_falls_back_to_global_when_focus_outside() {
4829 use crate::action::Action;
4835 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
4836 use std::cell::Cell;
4837 use std::rc::Rc;
4838
4839 let scoped_fired = Rc::new(Cell::new(0));
4840 let global_fired = Rc::new(Cell::new(0));
4841 let sf = scoped_fired.clone();
4842 let gf = global_fired.clone();
4843
4844 let mut tree = WidgetTree::new();
4845 let root = tree.add(FillWidget::new());
4846 let editor = tree.add_child(root, FillWidget::new().focusable());
4847 let _editor_inner = tree.add_child(editor, FillWidget::new().focusable());
4848 let sidebar = tree.add_child(root, FillWidget::new().focusable());
4849
4850 tree.push_action(
4851 editor,
4852 Action::new("editor.saveBlock").on_invoke(move |_i, _c| sf.set(sf.get() + 1)),
4853 );
4854 tree.push_action(
4855 root,
4856 Action::new("zzz.global.save").on_invoke(move |_i, _c| gf.set(gf.get() + 1)),
4857 );
4858 tree.shortcut_registry_mut().register(
4859 Shortcut::new("editor.saveBlock")
4860 .primary(KeyStroke::command(Key::S))
4861 .scope(ShortcutScope::Scoped(editor))
4862 .build(),
4863 );
4864 tree.shortcut_registry_mut().register(
4865 Shortcut::new("zzz.global.save")
4866 .primary(KeyStroke::command(Key::S))
4867 .build(),
4868 );
4869
4870 tree.layout(SizeProposal::exact(200.0, 100.0));
4871 tree.focus(sidebar);
4872 tree.press_key(Key::S, Modifiers::COMMAND);
4873
4874 assert_eq!(global_fired.get(), 1, "applicable global must fire");
4875 assert_eq!(
4876 scoped_fired.get(),
4877 0,
4878 "inapplicable scoped binding must not eat the chord"
4879 );
4880 }
4881
4882 #[test]
4883 fn same_chord_global_first_yields_to_scoped_when_focus_inside() {
4884 use crate::action::Action;
4889 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
4890 use std::cell::Cell;
4891 use std::rc::Rc;
4892
4893 let scoped_fired = Rc::new(Cell::new(0));
4894 let global_fired = Rc::new(Cell::new(0));
4895 let sf = scoped_fired.clone();
4896 let gf = global_fired.clone();
4897
4898 let mut tree = WidgetTree::new();
4899 let root = tree.add(FillWidget::new());
4900 let editor = tree.add_child(root, FillWidget::new().focusable());
4901 let editor_inner = tree.add_child(editor, FillWidget::new().focusable());
4902 let sidebar = tree.add_child(root, FillWidget::new().focusable());
4903
4904 tree.push_action(
4905 editor,
4906 Action::new("editor.saveBlock").on_invoke(move |_i, _c| sf.set(sf.get() + 1)),
4907 );
4908 tree.push_action(
4909 root,
4910 Action::new("app.save").on_invoke(move |_i, _c| gf.set(gf.get() + 1)),
4911 );
4912 tree.shortcut_registry_mut().register(
4913 Shortcut::new("app.save")
4914 .primary(KeyStroke::command(Key::S))
4915 .build(),
4916 );
4917 tree.shortcut_registry_mut().register(
4918 Shortcut::new("editor.saveBlock")
4919 .primary(KeyStroke::command(Key::S))
4920 .scope(ShortcutScope::Scoped(editor))
4921 .build(),
4922 );
4923
4924 tree.layout(SizeProposal::exact(200.0, 100.0));
4925
4926 tree.focus(editor_inner);
4928 tree.press_key(Key::S, Modifiers::COMMAND);
4929 assert_eq!(
4930 scoped_fired.get(),
4931 1,
4932 "in-focus scoped must win over global"
4933 );
4934 assert_eq!(
4935 global_fired.get(),
4936 0,
4937 "global must yield to the scoped binding"
4938 );
4939
4940 tree.focus(sidebar);
4942 tree.press_key(Key::S, Modifiers::COMMAND);
4943 assert_eq!(scoped_fired.get(), 1, "scoped stays put outside its scope");
4944 assert_eq!(
4945 global_fired.get(),
4946 1,
4947 "global fires when focus leaves the scope"
4948 );
4949 }
4950
4951 #[test]
4952 fn propagated_action_lets_ancestor_handle() {
4953 use crate::action::Action;
4954 use crate::intent::IntentResponse;
4955 use crate::shortcut::{KeyStroke, Shortcut};
4956 use std::cell::Cell;
4957 use std::rc::Rc;
4958
4959 let inner_seen = Rc::new(Cell::new(false));
4960 let outer_seen = Rc::new(Cell::new(false));
4961 let inner_flag = inner_seen.clone();
4962 let outer_flag = outer_seen.clone();
4963
4964 let mut tree = WidgetTree::new();
4965 let outer = tree.add(FillWidget::new().focusable());
4966 let inner = tree.add_child(outer, FillWidget::new().focusable());
4967
4968 tree.push_action(
4970 inner,
4971 Action::new("app.save").on_invoke_with_response(move |_i, _c| {
4972 inner_flag.set(true);
4973 IntentResponse::Propagated
4974 }),
4975 );
4976 tree.push_action(
4977 outer,
4978 Action::new("app.save").on_invoke(move |_i, _c| {
4979 outer_flag.set(true);
4980 }),
4981 );
4982 tree.shortcut_registry_mut().register(
4983 Shortcut::new("app.save")
4984 .primary(KeyStroke::command(Key::S))
4985 .build(),
4986 );
4987
4988 tree.layout(SizeProposal::exact(100.0, 50.0));
4989 tree.focus(inner);
4990
4991 tree.press_key(Key::S, Modifiers::COMMAND);
4992 assert!(inner_seen.get(), "inner action observed the intent");
4993 assert!(outer_seen.get(), "outer action reached after Propagated");
4994 }
4995
4996 #[test]
4997 fn handled_action_stops_propagation() {
4998 use crate::action::Action;
4999 use crate::shortcut::{KeyStroke, Shortcut};
5000 use std::cell::Cell;
5001 use std::rc::Rc;
5002
5003 let inner_seen = Rc::new(Cell::new(false));
5004 let outer_seen = Rc::new(Cell::new(false));
5005 let inner_flag = inner_seen.clone();
5006 let outer_flag = outer_seen.clone();
5007
5008 let mut tree = WidgetTree::new();
5009 let outer = tree.add(FillWidget::new().focusable());
5010 let inner = tree.add_child(outer, FillWidget::new().focusable());
5011
5012 tree.push_action(
5013 inner,
5014 Action::new("app.save").on_invoke(move |_i, _c| {
5015 inner_flag.set(true);
5016 }),
5017 );
5018 tree.push_action(
5019 outer,
5020 Action::new("app.save").on_invoke(move |_i, _c| {
5021 outer_flag.set(true);
5022 }),
5023 );
5024 tree.shortcut_registry_mut().register(
5025 Shortcut::new("app.save")
5026 .primary(KeyStroke::command(Key::S))
5027 .build(),
5028 );
5029
5030 tree.layout(SizeProposal::exact(100.0, 50.0));
5031 tree.focus(inner);
5032
5033 tree.press_key(Key::S, Modifiers::COMMAND);
5034 assert!(inner_seen.get());
5035 assert!(!outer_seen.get(), "Handled at inner must stop propagation");
5036 }
5037
5038 #[test]
5039 fn disabled_action_propagates_by_default() {
5040 use crate::action::Action;
5041 use crate::shortcut::{KeyStroke, Shortcut};
5042 use crate::signal::Signal;
5043 use std::cell::Cell;
5044 use std::rc::Rc;
5045
5046 let inner_seen = Rc::new(Cell::new(false));
5047 let outer_seen = Rc::new(Cell::new(false));
5048 let inner_flag = inner_seen.clone();
5049 let outer_flag = outer_seen.clone();
5050
5051 let mut tree = WidgetTree::new();
5052 let outer = tree.add(FillWidget::new().focusable());
5053 let inner = tree.add_child(outer, FillWidget::new().focusable());
5054
5055 let enabled = Signal::new(false);
5056 tree.push_action(
5057 inner,
5058 Action::new("app.save")
5059 .enabled_when(enabled.clone())
5060 .on_invoke(move |_i, _c| {
5061 inner_flag.set(true);
5062 }),
5063 );
5064 tree.push_action(
5065 outer,
5066 Action::new("app.save").on_invoke(move |_i, _c| {
5067 outer_flag.set(true);
5068 }),
5069 );
5070 tree.shortcut_registry_mut().register(
5071 Shortcut::new("app.save")
5072 .primary(KeyStroke::command(Key::S))
5073 .build(),
5074 );
5075
5076 tree.layout(SizeProposal::exact(100.0, 50.0));
5077 tree.focus(inner);
5078
5079 tree.press_key(Key::S, Modifiers::COMMAND);
5080 assert!(!inner_seen.get(), "disabled inner must not run");
5081 assert!(
5082 outer_seen.get(),
5083 "intent must propagate past disabled inner"
5084 );
5085 }
5086
5087 #[test]
5088 fn disabled_action_with_non_propagating_shortcut_consumes() {
5089 use crate::action::Action;
5090 use crate::shortcut::{KeyStroke, Shortcut};
5091 use crate::signal::Signal;
5092 use std::cell::Cell;
5093 use std::rc::Rc;
5094
5095 let inner_seen = Rc::new(Cell::new(false));
5096 let outer_seen = Rc::new(Cell::new(false));
5097 let inner_flag = inner_seen.clone();
5098 let outer_flag = outer_seen.clone();
5099
5100 let mut tree = WidgetTree::new();
5101 let outer = tree.add(FillWidget::new().focusable());
5102 let inner = tree.add_child(outer, FillWidget::new().focusable());
5103
5104 let enabled = Signal::new(false);
5105 tree.push_action(
5106 inner,
5107 Action::new("app.save")
5108 .enabled_when(enabled.clone())
5109 .on_invoke(move |_i, _c| {
5110 inner_flag.set(true);
5111 }),
5112 );
5113 tree.push_action(
5114 outer,
5115 Action::new("app.save").on_invoke(move |_i, _c| {
5116 outer_flag.set(true);
5117 }),
5118 );
5119 tree.shortcut_registry_mut().register(
5120 Shortcut::new("app.save")
5121 .primary(KeyStroke::command(Key::S))
5122 .propagate_when_disabled(false)
5123 .build(),
5124 );
5125
5126 tree.layout(SizeProposal::exact(100.0, 50.0));
5127 tree.focus(inner);
5128
5129 tree.press_key(Key::S, Modifiers::COMMAND);
5130 assert!(!inner_seen.get(), "disabled inner still does not run");
5131 assert!(
5132 !outer_seen.get(),
5133 "intent must NOT propagate when shortcut disallows it"
5134 );
5135 }
5136
5137 #[test]
5138 fn send_intent_from_handler_reaches_ancestor_action() {
5139 use crate::action::Action;
5140 use crate::intent::Intent;
5141 use std::cell::Cell;
5142 use std::rc::Rc;
5143
5144 let save_seen = Rc::new(Cell::new(false));
5145 let save_flag = save_seen.clone();
5146
5147 let mut tree = WidgetTree::new();
5148 let root = tree.add(FillWidget::new());
5149 let button = tree.add_child(
5150 root,
5151 FillWidget::new().on_tap(|_pos, ctx| {
5152 ctx.send_intent(Intent::new("app.save"));
5153 }),
5154 );
5155 tree.push_action(
5156 root,
5157 Action::new("app.save").on_invoke(move |_i, _c| {
5158 save_flag.set(true);
5159 }),
5160 );
5161
5162 tree.layout(SizeProposal::exact(100.0, 50.0));
5163 tree.click(button);
5164 assert!(
5165 save_seen.get(),
5166 "ctx.send_intent must reach ancestor action"
5167 );
5168 }
5169
5170 #[test]
5171 fn widget_type_histogram_counts_distinct_types() {
5172 let mut tree = WidgetTree::new();
5177 let _ = tree.add(FillWidget::new());
5178 let _ = tree.add(FillWidget::new());
5179 let _ = tree.add(FillWidget::new());
5180 tree.layout(SizeProposal::exact(100.0, 100.0));
5181 let histogram = tree.widget_type_histogram();
5182 let total: u32 = histogram.values().sum();
5183 assert!(
5184 total >= 3,
5185 "expected at least 3 active widgets, got {total}: {histogram:?}"
5186 );
5187 let fillwidget_entries: u32 = histogram
5188 .iter()
5189 .filter(|(k, _)| k.contains("FillWidget"))
5190 .map(|(_, v)| *v)
5191 .sum();
5192 assert!(
5193 fillwidget_entries >= 3,
5194 "expected ≥3 FillWidget instances; histogram = {histogram:?}"
5195 );
5196 assert_eq!(tree.active_widget_count() as u32, total);
5197 }
5198
5199 #[test]
5200 fn intent_source_tagged_handler_for_tap_activation() {
5201 use crate::action::Action;
5205 use crate::intent::Intent;
5206 use crate::telemetry::IntentSource;
5207 use std::cell::Cell;
5208 use std::rc::Rc;
5209 let captured = Rc::new(Cell::new(IntentSource::Unknown));
5210 let captured_for_action = captured.clone();
5211
5212 let mut tree = WidgetTree::new();
5213 let root = tree.add(FillWidget::new());
5214 let button = tree.add_child(
5215 root,
5216 FillWidget::new().on_tap(|_pos, ctx| {
5217 ctx.send_intent(Intent::new("app.save"));
5218 }),
5219 );
5220 tree.push_action(
5221 root,
5222 Action::new("app.save").on_invoke(move |intent, _c| {
5223 captured_for_action.set(intent.source);
5224 }),
5225 );
5226
5227 tree.layout(SizeProposal::exact(100.0, 50.0));
5228 tree.click(button);
5229 assert_eq!(
5230 captured.get(),
5231 IntentSource::Handler,
5232 "tap-driven intent must tag IntentSource::Handler"
5233 );
5234 }
5235
5236 #[test]
5237 fn intent_source_programmatic_when_no_handler_active() {
5238 use crate::intent::Intent;
5239 use crate::telemetry::IntentSource;
5240 let intent = Intent::new("app.demo");
5241 assert_eq!(intent.source, IntentSource::Programmatic);
5242
5243 let mut ctx = EventContext::new();
5245 ctx.send_intent(Intent::new("app.demo"));
5246 let queued = ctx.pending_intents.first().expect("intent queued");
5247 assert_eq!(queued.source, IntentSource::Programmatic);
5248 }
5249
5250 #[test]
5251 fn with_intent_source_overrides_for_managed_widgets() {
5252 use crate::intent::Intent;
5253 use crate::telemetry::IntentSource;
5254 let mut ctx = EventContext::new();
5255 ctx.with_intent_source(IntentSource::Menu, |ctx| {
5256 ctx.send_intent(Intent::new("app.demo"));
5257 });
5258 let queued = ctx.pending_intents.first().expect("intent queued");
5259 assert_eq!(
5260 queued.source,
5261 IntentSource::Menu,
5262 "with_intent_source(Menu) must tag the dispatched intent"
5263 );
5264
5265 ctx.send_intent(Intent::new("app.next"));
5269 let next = ctx.pending_intents.last().expect("second intent");
5270 assert_eq!(next.source, IntentSource::Programmatic);
5271 }
5272
5273 #[test]
5274 fn disabled_shortcut_falls_through_to_focused_widget() {
5275 use crate::action::Action;
5276 use crate::shortcut::{KeyStroke, Shortcut};
5277 use crate::signal::Signal;
5278 use std::cell::Cell;
5279 use std::rc::Rc;
5280
5281 let action_fired = Rc::new(Cell::new(false));
5282 let on_key_fired = Rc::new(Cell::new(false));
5283 let af = action_fired.clone();
5284 let kf = on_key_fired.clone();
5285
5286 let enabled = Signal::new(false);
5287
5288 let mut tree = WidgetTree::new();
5289 let widget = tree.add(FillWidget::new().focusable().on_key(move |event, _ctx| {
5290 if matches!(
5291 event,
5292 WidgetEvent::KeyDown {
5293 key: Key::S,
5294 modifiers,
5295 ..
5296 } if modifiers.command()
5297 ) {
5298 kf.set(true);
5299 return EventResponse::Handled;
5300 }
5301 EventResponse::Ignored
5302 }));
5303 tree.push_action(
5304 widget,
5305 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
5306 );
5307 tree.shortcut_registry_mut().register(
5308 Shortcut::new("app.save")
5309 .primary(KeyStroke::command(Key::S))
5310 .enabled_when(enabled.clone())
5311 .build(),
5312 );
5313
5314 tree.layout(SizeProposal::exact(100.0, 50.0));
5315 tree.focus(widget);
5316
5317 tree.press_key(Key::S, Modifiers::COMMAND);
5319 assert!(
5320 !action_fired.get(),
5321 "disabled shortcut must not invoke its action"
5322 );
5323 assert!(
5324 on_key_fired.get(),
5325 "disabled shortcut must let KeyDown reach the focused widget"
5326 );
5327
5328 on_key_fired.set(false);
5330 enabled.set(true);
5331 tree.press_key(Key::S, Modifiers::COMMAND);
5332 assert!(action_fired.get(), "re-enabled shortcut must dispatch");
5333 assert!(
5334 !on_key_fired.get(),
5335 "enabled shortcut must consume the KeyDown"
5336 );
5337 }
5338
5339 #[test]
5340 fn keyboard_capture_bypasses_shortcut() {
5341 use crate::action::Action;
5342 use crate::shortcut::{KeyStroke, Shortcut};
5343 use std::cell::Cell;
5344 use std::rc::Rc;
5345
5346 fn run(capture: bool) -> (bool, bool) {
5352 let action_fired = Rc::new(Cell::new(false));
5353 let on_key_fired = Rc::new(Cell::new(false));
5354 let af = action_fired.clone();
5355 let kf = on_key_fired.clone();
5356
5357 let mut tree = WidgetTree::new();
5358 let widget = tree.add(
5359 FillWidget::new()
5360 .focusable()
5361 .keyboard_capture(capture)
5362 .on_key(move |event, _ctx| {
5363 if matches!(
5364 event,
5365 WidgetEvent::KeyDown { key: Key::S, modifiers, .. } if modifiers.command()
5366 ) {
5367 kf.set(true);
5368 return EventResponse::Handled;
5369 }
5370 EventResponse::Ignored
5371 }),
5372 );
5373 tree.push_action(
5374 widget,
5375 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
5376 );
5377 tree.shortcut_registry_mut().register(
5378 Shortcut::new("app.save")
5379 .primary(KeyStroke::command(Key::S))
5380 .build(),
5381 );
5382
5383 tree.layout(SizeProposal::exact(100.0, 50.0));
5384 tree.focus(widget);
5385 tree.press_key(Key::S, Modifiers::COMMAND);
5386 (action_fired.get(), on_key_fired.get())
5387 }
5388
5389 let (action, on_key) = run(true);
5391 assert!(
5392 !action,
5393 "keyboard_capture must suppress the shortcut action"
5394 );
5395 assert!(on_key, "keyboard_capture must deliver the raw KeyDown");
5396
5397 let (action, on_key) = run(false);
5399 assert!(action, "without capture the shortcut must fire");
5400 assert!(!on_key, "without capture the widget must not see the key");
5401 }
5402
5403 fn key_recorder(
5405 tree: &mut WidgetTree,
5406 ) -> (WidgetId, std::rc::Rc<std::cell::RefCell<Vec<Key>>>) {
5407 let seen = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
5408 let log = seen.clone();
5409 let widget = tree.add(FillWidget::new().focusable().on_key(move |event, _ctx| {
5410 if let WidgetEvent::KeyDown { key, .. } = event {
5411 log.borrow_mut().push(*key);
5412 }
5413 EventResponse::Ignored
5414 }));
5415 tree.layout(SizeProposal::exact(100.0, 50.0));
5416 tree.focus(widget);
5417 (widget, seen)
5418 }
5419
5420 #[test]
5421 fn a_shortcut_no_action_takes_leaves_its_key_to_the_focused_widget() {
5422 use crate::action::Action;
5423 use crate::intent::IntentResponse;
5424 use crate::shortcut::{KeyStroke, Shortcut};
5425
5426 let mut tree = WidgetTree::new();
5427 let (widget, seen) = key_recorder(&mut tree);
5428 tree.shortcut_registry_mut().register(
5429 Shortcut::new("app.save")
5430 .primary(KeyStroke::command(Key::S))
5431 .build(),
5432 );
5433
5434 tree.press_key(Key::S, Modifiers::COMMAND);
5436 assert_eq!(
5437 *seen.borrow(),
5438 vec![Key::S],
5439 "an unhandled intent must not eat the key"
5440 );
5441
5442 tree.push_action(
5444 widget,
5445 Action::new("app.save").on_invoke_with_response(|_i, _c| IntentResponse::Propagated),
5446 );
5447 tree.press_key(Key::S, Modifiers::COMMAND);
5448 assert_eq!(
5449 seen.borrow().len(),
5450 2,
5451 "a propagated intent must leave the key too"
5452 );
5453
5454 let mut tree = WidgetTree::new();
5457 let (widget, seen) = key_recorder(&mut tree);
5458 tree.shortcut_registry_mut().register(
5459 Shortcut::new("app.save")
5460 .primary(KeyStroke::command(Key::S))
5461 .build(),
5462 );
5463 tree.push_action(widget, Action::new("app.save").on_invoke(|_i, _c| {}));
5464 tree.press_key(Key::S, Modifiers::COMMAND);
5465 assert!(
5466 seen.borrow().is_empty(),
5467 "a handled shortcut consumes its key"
5468 );
5469 }
5470
5471 #[test]
5472 fn standard_edit_chords_follow_a_rebind() {
5473 use crate::action::Action;
5474 use crate::shortcut::{KeyStroke, StandardEdit};
5475 use std::cell::Cell;
5476 use std::rc::Rc;
5477
5478 let mut tree = WidgetTree::new();
5479 let (widget, seen) = key_recorder(&mut tree);
5480 let copies = Rc::new(Cell::new(0));
5481 let count = copies.clone();
5482 tree.push_action(
5483 widget,
5484 Action::new(StandardEdit::Copy.id())
5485 .on_invoke(move |_i, _c| count.set(count.get() + 1)),
5486 );
5487
5488 tree.press_key(Key::C, Modifiers::COMMAND);
5490 assert_eq!(copies.get(), 1, "Ctrl+C reaches the edit.copy action");
5491 assert!(seen.borrow().is_empty());
5492
5493 let elsewhere = KeyStroke::new(Key::Character('à'), Modifiers::CTRL);
5496 tree.shortcut_registry_mut()
5497 .rebind_primary(StandardEdit::Copy.id(), Some(elsewhere));
5498 tree.press_key(Key::C, Modifiers::COMMAND);
5499 assert_eq!(copies.get(), 1, "the replaced chord must not copy");
5500 assert_eq!(*seen.borrow(), vec![Key::C]);
5501 tree.press_key(Key::Character('à'), Modifiers::CTRL);
5502 assert_eq!(copies.get(), 2, "the new chord copies");
5503 }
5504
5505 #[test]
5506 fn ctrl_tab_always_escapes_a_keyboard_capture_surface() {
5507 use std::cell::Cell;
5508 use std::rc::Rc;
5509
5510 let saw_key = Rc::new(Cell::new(false));
5516 let sk = saw_key.clone();
5517
5518 let mut tree = WidgetTree::new();
5519 let capture = tree.add(
5520 FillWidget::new()
5521 .focusable()
5522 .keyboard_capture(true)
5523 .on_key(move |_event, _ctx| {
5525 sk.set(true);
5526 EventResponse::Handled
5527 }),
5528 );
5529 let neighbour = tree.add(FillWidget::new().focusable());
5530 tree.layout(SizeProposal::exact(100.0, 50.0));
5531
5532 tree.focus(capture);
5535 tree.press_key(Key::Tab, Modifiers::NONE);
5536 assert!(saw_key.get(), "plain Tab must reach the capture surface");
5537 assert_eq!(
5538 tree.focused(),
5539 Some(capture),
5540 "plain Tab must not move focus off a capture surface"
5541 );
5542
5543 saw_key.set(false);
5545 tree.press_key(Key::Tab, Modifiers::CTRL);
5546 assert!(
5547 !saw_key.get(),
5548 "Ctrl+Tab is reserved and must not reach the capture surface"
5549 );
5550 assert_eq!(
5551 tree.focused(),
5552 Some(neighbour),
5553 "Ctrl+Tab must move focus out of a capture surface"
5554 );
5555
5556 tree.focus(capture);
5558 tree.press_key(Key::Tab, Modifiers::CTRL | Modifiers::SHIFT);
5559 assert_eq!(
5560 tree.focused(),
5561 Some(neighbour),
5562 "Ctrl+Shift+Tab must move focus out of a capture surface"
5563 );
5564 }
5565
5566 #[test]
5567 fn scope_mismatch_does_not_invoke_on_activate() {
5568 use crate::intent::Intent;
5569 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
5570 use std::cell::Cell;
5571 use std::rc::Rc;
5572
5573 let activated = Rc::new(Cell::new(false));
5579 let activated_flag = activated.clone();
5580
5581 let mut tree = WidgetTree::new();
5582 let scope_root = tree.add(FillWidget::new().focusable());
5583 let outside = tree.add(FillWidget::new().focusable());
5584
5585 tree.shortcut_registry_mut().register(
5586 Shortcut::new("editor.find")
5587 .primary(KeyStroke::command(Key::F))
5588 .scope(ShortcutScope::Scoped(scope_root))
5589 .on_activate(move |_ks, _ctx| {
5590 activated_flag.set(true);
5591 Intent::new("editor.find")
5592 })
5593 .build(),
5594 );
5595
5596 tree.layout(SizeProposal::exact(200.0, 100.0));
5597 tree.focus(outside);
5598
5599 tree.press_key(Key::F, Modifiers::COMMAND);
5600 assert!(
5601 !activated.get(),
5602 "on_activate must not run when focus is outside the shortcut's scope"
5603 );
5604 }
5605
5606 #[test]
5607 fn key_capture_runs_callback_and_bypasses_registry() {
5608 use crate::action::Action;
5609 use crate::shortcut::{KeyStroke, Shortcut};
5610 use std::cell::Cell;
5611 use std::rc::Rc;
5612
5613 let action_fired = Rc::new(Cell::new(false));
5614 let af = action_fired.clone();
5615 let captured = Rc::new(Cell::new(None));
5616 let cf = captured.clone();
5617
5618 let mut tree = WidgetTree::new();
5619 let widget = tree.add(FillWidget::new().focusable());
5620 tree.push_action(
5621 widget,
5622 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
5623 );
5624 tree.shortcut_registry_mut().register(
5625 Shortcut::new("app.save")
5626 .primary(KeyStroke::command(Key::S))
5627 .build(),
5628 );
5629
5630 tree.layout(SizeProposal::exact(100.0, 50.0));
5631 tree.focus(widget);
5632
5633 let handle = tree.begin_key_capture(move |ks, _reg, _ctx| cf.set(Some(ks)));
5634 assert!(tree.is_capturing_keys());
5635
5636 tree.press_key(Key::S, Modifiers::COMMAND);
5637 assert_eq!(
5638 captured.get(),
5639 Some(KeyStroke::command(Key::S)),
5640 "capture callback must receive the chord"
5641 );
5642 assert!(
5643 !action_fired.get(),
5644 "shortcut action must not fire while capture is armed"
5645 );
5646 assert!(
5647 !tree.is_capturing_keys(),
5648 "capture is one-shot; next KeyDown flows normally"
5649 );
5650 drop(handle);
5651 }
5652
5653 #[test]
5654 fn key_capture_can_rebind_through_registry() {
5655 use crate::shortcut::{KeyStroke, Shortcut};
5656
5657 let mut tree = WidgetTree::new();
5658 let widget = tree.add(FillWidget::new().focusable());
5659 tree.shortcut_registry_mut().register(
5660 Shortcut::new("app.save")
5661 .primary(KeyStroke::command(Key::S))
5662 .build(),
5663 );
5664
5665 tree.layout(SizeProposal::exact(100.0, 50.0));
5666 tree.focus(widget);
5667
5668 let _h = tree.begin_key_capture(|ks, reg, _ctx| {
5670 reg.rebind_primary("app.save", Some(ks));
5671 });
5672
5673 tree.press_key(Key::B, Modifiers::COMMAND | Modifiers::SHIFT);
5674 assert_eq!(
5675 tree.shortcut_registry()
5676 .effective("app.save")
5677 .unwrap()
5678 .primary,
5679 Some(KeyStroke::command_shift(Key::B))
5680 );
5681 }
5682
5683 #[test]
5684 fn dropping_capture_handle_cancels_capture() {
5685 use crate::shortcut::{KeyStroke, Shortcut};
5686 use std::cell::Cell;
5687 use std::rc::Rc;
5688
5689 let action_fired = Rc::new(Cell::new(false));
5690 let af = action_fired.clone();
5691 let capture_fired = Rc::new(Cell::new(false));
5692 let cf = capture_fired.clone();
5693
5694 let mut tree = WidgetTree::new();
5695 let widget = tree.add(FillWidget::new().focusable());
5696 tree.push_action(
5697 widget,
5698 crate::action::Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
5699 );
5700 tree.shortcut_registry_mut().register(
5701 Shortcut::new("app.save")
5702 .primary(KeyStroke::command(Key::S))
5703 .build(),
5704 );
5705 tree.layout(SizeProposal::exact(100.0, 50.0));
5706 tree.focus(widget);
5707
5708 {
5712 let _h = tree.begin_key_capture(move |_ks, _reg, _ctx| cf.set(true));
5713 assert!(tree.is_capturing_keys());
5714 }
5716 assert!(
5717 !tree.is_capturing_keys(),
5718 "dropping the handle must cancel the capture"
5719 );
5720
5721 tree.press_key(Key::S, Modifiers::COMMAND);
5722 assert!(!capture_fired.get(), "cancelled capture must not fire");
5723 assert!(
5724 action_fired.get(),
5725 "shortcut action runs after capture was cancelled"
5726 );
5727 }
5728
5729 #[test]
5730 fn second_begin_key_capture_does_not_racecancel_first() {
5731 use std::cell::Cell;
5732 use std::rc::Rc;
5733
5734 let first = Rc::new(Cell::new(false));
5735 let second = Rc::new(Cell::new(false));
5736 let f = first.clone();
5737 let s = second.clone();
5738
5739 let mut tree = WidgetTree::new();
5740 let widget = tree.add(FillWidget::new().focusable());
5741 tree.layout(SizeProposal::exact(100.0, 50.0));
5742 tree.focus(widget);
5743
5744 let h1 = tree.begin_key_capture(move |_ks, _reg, _ctx| f.set(true));
5750 let _h2 = tree.begin_key_capture(move |_ks, _reg, _ctx| s.set(true));
5751 drop(h1);
5752
5753 assert!(
5754 tree.is_capturing_keys(),
5755 "dropping the older handle must not cancel the active capture"
5756 );
5757 tree.press_key(Key::K, Modifiers::COMMAND);
5758 assert!(!first.get());
5759 assert!(second.get(), "newest capture wins");
5760 }
5761
5762 #[test]
5763 fn capture_callback_can_send_intent() {
5764 use crate::action::Action;
5765 use crate::intent::Intent;
5766
5767 use std::cell::Cell;
5768 use std::rc::Rc;
5769
5770 let ran = Rc::new(Cell::new(false));
5771 let flag = ran.clone();
5772
5773 let mut tree = WidgetTree::new();
5774 let widget = tree.add(FillWidget::new().focusable());
5775 tree.push_action(
5776 widget,
5777 Action::new("app.save").on_invoke(move |_i, _c| flag.set(true)),
5778 );
5779 tree.layout(SizeProposal::exact(100.0, 50.0));
5780 tree.focus(widget);
5781
5782 let _h = tree.begin_key_capture(|_ks, _reg, ctx| {
5783 ctx.send_intent(Intent::new("app.save"));
5784 });
5785 tree.press_key(Key::X, Modifiers::COMMAND);
5786 assert!(
5787 ran.get(),
5788 "intent queued from capture callback must dispatch"
5789 );
5790 }
5791
5792 #[test]
5793 fn binding_registry_does_not_accumulate_across_rebuilds() {
5794 use crate::binding::BindingLevel;
5795 use crate::signal::Signal;
5796
5797 #[derive(Debug)]
5798 struct BoundLeaf {
5799 tick: Signal<u64>,
5800 }
5801 impl crate::widget::Widget for BoundLeaf {
5802 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
5803 self.tick.bind_to(
5804 ctx.self_id(),
5805 ctx.binding_registry(),
5806 BindingLevel::Relayout,
5807 );
5808 Vec::new()
5809 }
5810 fn layout_response(
5811 &self,
5812 proposal: SizeProposal,
5813 _ctx: &crate::widget::LayoutContext,
5814 ) -> crate::widget::LayoutResponse {
5815 proposal.resolve(10.0, 10.0).into()
5816 }
5817 }
5818
5819 let mut tree = WidgetTree::new();
5820 let tick = Signal::new(0_u64);
5821 let widget = tree.add(BoundLeaf { tick: tick.clone() });
5822 tree.layout(SizeProposal::exact(200.0, 200.0));
5823 let after_first_build = tree.binding_registry().len();
5824 assert!(after_first_build >= 1);
5825
5826 for _ in 0..5 {
5830 tree.arena.mark_needs_rebuild(widget);
5831 tree.layout(SizeProposal::exact(200.0, 200.0));
5832 }
5833 assert_eq!(
5834 tree.binding_registry().len(),
5835 after_first_build,
5836 "bindings must be cleared on rebuild"
5837 );
5838
5839 tree.destroy_subtree(widget);
5840 assert_eq!(
5841 tree.binding_registry().len(),
5842 0,
5843 "bindings must be cleared on destroy"
5844 );
5845 let _ = tick;
5847 }
5848
5849 #[test]
5850 fn ctx_destroy_cancels_animations_and_bindings_via_deferred_path() {
5851 use crate::binding::BindingLevel;
5859 use crate::signal::Signal;
5860 use std::time::{Duration, Instant};
5861 use teksilo_tokens::Easing;
5862
5863 #[derive(Debug)]
5864 struct BoundLeaf {
5865 tick: Signal<u64>,
5866 }
5867 impl crate::widget::Widget for BoundLeaf {
5868 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
5869 self.tick.bind_to(
5870 ctx.self_id(),
5871 ctx.binding_registry(),
5872 BindingLevel::Relayout,
5873 );
5874 Vec::new()
5875 }
5876 fn layout_response(
5877 &self,
5878 proposal: SizeProposal,
5879 _ctx: &crate::widget::LayoutContext,
5880 ) -> crate::widget::LayoutResponse {
5881 proposal.resolve(10.0, 10.0).into()
5882 }
5883 }
5884
5885 let mut tree = WidgetTree::new();
5886 let widget = tree.add(BoundLeaf {
5887 tick: Signal::new(0_u64),
5888 });
5889 tree.layout(SizeProposal::exact(200.0, 200.0));
5890 assert!(tree.binding_registry().len() >= 1);
5891
5892 let anim = Signal::<f32>::new_animated(0.0);
5895 tree.animation_scheduler.animate(
5896 &anim,
5897 widget,
5898 1.0,
5899 Duration::from_secs(10),
5900 Easing::Linear,
5901 Instant::now(),
5902 );
5903 assert_eq!(tree.animation_scheduler.active_count(), 1);
5904
5905 let mut noop = crate::window::NoopWindowOps;
5907 tree.run_with_event_context(&mut noop, |ctx| ctx.destroy(widget));
5908
5909 assert_eq!(
5910 tree.animation_scheduler.active_count(),
5911 0,
5912 "ctx.destroy must cancel animations owned by the destroyed widget"
5913 );
5914 assert_eq!(
5915 tree.binding_registry().len(),
5916 0,
5917 "ctx.destroy must unregister the destroyed widget's bindings"
5918 );
5919 assert!(
5920 tree.arena.get(widget).is_none(),
5921 "node must be removed from the arena"
5922 );
5923 }
5924
5925 #[test]
5926 fn clear_shortcut_override_via_event_context_restores_default() {
5927 use crate::shortcut::{KeyStroke, Shortcut};
5928
5929 let mut tree = WidgetTree::new();
5930 tree.shortcut_registry_mut().register(
5931 Shortcut::new("app.save")
5932 .primary(KeyStroke::command(Key::S))
5933 .build(),
5934 );
5935 tree.shortcut_registry_mut()
5936 .rebind_primary("app.save", Some(KeyStroke::alt(Key::S)));
5937
5938 let source = tree.add(FillWidget::new());
5939 let mut ctx = EventContext::new();
5940 ctx.clear_shortcut_override("app.save");
5941 tree.collect_from_ctx(ctx, source);
5942
5943 assert_eq!(
5944 tree.shortcut_registry()
5945 .effective("app.save")
5946 .unwrap()
5947 .primary,
5948 Some(KeyStroke::command(Key::S))
5949 );
5950 }
5951
5952 #[test]
5953 fn rebind_shortcut_primary_via_event_context() {
5954 use crate::shortcut::{KeyStroke, Shortcut};
5955
5956 let mut tree = WidgetTree::new();
5957 tree.shortcut_registry_mut().register(
5958 Shortcut::new("app.save")
5959 .primary(KeyStroke::command(Key::S))
5960 .build(),
5961 );
5962 let source = tree.add(FillWidget::new());
5963
5964 let mut ctx = EventContext::new();
5965 ctx.rebind_shortcut_primary("app.save", Some(KeyStroke::alt(Key::S)));
5966 tree.collect_from_ctx(ctx, source);
5967
5968 assert_eq!(
5969 tree.shortcut_registry()
5970 .effective("app.save")
5971 .unwrap()
5972 .primary,
5973 Some(KeyStroke::alt(Key::S))
5974 );
5975 }
5976
5977 #[test]
5978 fn unregister_all_for_owner_called_on_destroy() {
5979 use crate::shortcut::{KeyStroke, Shortcut};
5980
5981 let mut tree = WidgetTree::new();
5982 let widget = tree.add(FillWidget::new());
5983 let widget_owner = widget;
5984 tree.shortcut_registry_mut().register_owned(
5985 Shortcut::new("scoped.thing")
5986 .primary(KeyStroke::command(Key::K))
5987 .build(),
5988 widget_owner,
5989 );
5990 assert!(
5991 tree.shortcut_registry()
5992 .get_default("scoped.thing")
5993 .is_some()
5994 );
5995
5996 tree.destroy_subtree(widget);
5997 assert!(
5998 tree.shortcut_registry()
5999 .get_default("scoped.thing")
6000 .is_none(),
6001 "destroying the owner must unregister its shortcut"
6002 );
6003 }
6004
6005 #[test]
6009 fn global_action_reached_from_unrelated_source() {
6010 use crate::action::Action;
6011 use crate::intent::Intent;
6012 use std::cell::Cell;
6013 use std::rc::Rc;
6014
6015 #[derive(Debug)]
6016 struct Registrar(Rc<Cell<bool>>);
6017 impl crate::widget::Widget for Registrar {
6018 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6019 let flag = self.0.clone();
6020 ctx.register_action_global(
6021 Action::new("test.global").on_invoke(move |_i, _c| flag.set(true)),
6022 );
6023 vec![]
6024 }
6025 fn layout_response(
6026 &self,
6027 _p: teksilo_canvas::SizeProposal,
6028 _c: &crate::widget::LayoutContext,
6029 ) -> crate::widget::LayoutResponse {
6030 teksilo_canvas::Size::new(0.0, 0.0).into()
6031 }
6032 }
6033
6034 let mut tree = WidgetTree::new();
6035 let fired = Rc::new(Cell::new(false));
6036 let registrar = tree.add(Registrar(fired.clone()));
6037 let source = tree.add(FillWidget::new()); let mut ops = crate::window::NoopWindowOps;
6039
6040 tree.dispatch_intent(source, Intent::new("test.global"), true, &mut ops);
6041 assert!(
6042 fired.get(),
6043 "global action must fire from an unrelated source"
6044 );
6045
6046 fired.set(false);
6048 tree.destroy_subtree(registrar);
6049 tree.dispatch_intent(source, Intent::new("test.global"), true, &mut ops);
6050 assert!(
6051 !fired.get(),
6052 "destroying the owner must remove its global action"
6053 );
6054 }
6055
6056 #[test]
6068 fn hit_test_through_translate_scope() {
6069 use crate::test_widgets::StackWidget;
6070 let mut tree = WidgetTree::new();
6071 let child = tree.add(FillWidget::new());
6072 let parent = tree.add(StackWidget::new().child(child));
6073 tree.set_transform(parent, teksilo_canvas::Transform2D::translate(100.0, 0.0));
6075 tree.layout(SizeProposal::exact(100.0, 50.0));
6076
6077 assert_eq!(
6081 tree.hit_test(Point::new(50.0, 25.0)),
6082 None,
6083 "pre-transform area is not visually populated and must not hit"
6084 );
6085 assert_eq!(
6087 tree.hit_test(Point::new(150.0, 25.0)),
6088 Some(child),
6089 "visually-rendered area must hit the child"
6090 );
6091 assert_eq!(tree.hit_test(Point::new(250.0, 25.0)), None);
6093 }
6094
6095 #[test]
6096 fn hit_test_through_scale_scope() {
6097 use crate::test_widgets::StackWidget;
6098 let mut tree = WidgetTree::new();
6099 let child = tree.add(FillWidget::new());
6100 let parent = tree.add(StackWidget::new().child(child));
6101 tree.set_transform(parent, teksilo_canvas::Transform2D::scale(0.5, 0.5));
6104 tree.layout(SizeProposal::exact(100.0, 50.0));
6105
6106 assert_eq!(tree.hit_test(Point::new(25.0, 12.0)), Some(child));
6108 assert_eq!(
6111 tree.hit_test(Point::new(75.0, 25.0)),
6112 None,
6113 "scaled-out region must not hit"
6114 );
6115 }
6116
6117 #[test]
6118 fn hit_test_through_nested_transforms_compose() {
6119 use crate::test_widgets::StackWidget;
6120 let mut tree = WidgetTree::new();
6121 let leaf = tree.add(FillWidget::new());
6122 let inner = tree.add(StackWidget::new().child(leaf));
6123 let outer = tree.add(StackWidget::new().child(inner));
6124 tree.set_transform(outer, teksilo_canvas::Transform2D::translate(100.0, 0.0));
6128 tree.set_transform(inner, teksilo_canvas::Transform2D::scale(2.0, 2.0));
6129 tree.layout(SizeProposal::exact(50.0, 25.0));
6130
6131 assert_eq!(tree.hit_test(Point::new(150.0, 25.0)), Some(leaf));
6135 assert_eq!(tree.hit_test(Point::new(50.0, 25.0)), None);
6136 assert_eq!(tree.hit_test(Point::new(250.0, 25.0)), None);
6137 }
6138
6139 #[test]
6140 fn hit_test_identity_transform_unchanged() {
6141 let mut tree = WidgetTree::new();
6145 let widget = tree.add(FillWidget::new());
6146 tree.set_transform(widget, teksilo_canvas::Transform2D::IDENTITY);
6147 tree.layout(SizeProposal::exact(100.0, 50.0));
6148 assert_eq!(tree.hit_test(Point::new(50.0, 25.0)), Some(widget));
6149 }
6150
6151 #[test]
6152 fn arena_effective_transform_composes_ancestors() {
6153 use crate::test_widgets::StackWidget;
6164 let mut tree = WidgetTree::new();
6165 let leaf = tree.add(FillWidget::new());
6166 let inner = tree.add(StackWidget::new().child(leaf));
6167 let outer = tree.add(StackWidget::new().child(inner));
6168 tree.set_transform(outer, teksilo_canvas::Transform2D::translate(100.0, 0.0));
6169 tree.set_transform(inner, teksilo_canvas::Transform2D::scale(2.0, 2.0));
6170 tree.layout(SizeProposal::exact(50.0, 25.0));
6171
6172 let eff = tree.arena.effective_transform(leaf);
6173 let expected = teksilo_canvas::Transform2D::scale(2.0, 2.0)
6174 .then(&teksilo_canvas::Transform2D::translate(100.0, 0.0));
6175 for (a, b) in eff.m.iter().zip(expected.m.iter()) {
6176 assert!(
6177 (a - b).abs() < 1e-5,
6178 "effective_transform mismatch: got {:?}, want {:?}",
6179 eff.m,
6180 expected.m
6181 );
6182 }
6183
6184 let screen_origin = eff.apply_point(Point::new(0.0, 0.0));
6190 assert!((screen_origin.x - 100.0).abs() < 1e-5);
6191 assert!((screen_origin.y - 0.0).abs() < 1e-5);
6192 let screen_corner = eff.apply_point(Point::new(50.0, 25.0));
6195 assert!((screen_corner.x - 200.0).abs() < 1e-5);
6196 assert!((screen_corner.y - 50.0).abs() < 1e-5);
6197 }
6198
6199 #[derive(Debug)]
6204 struct StubMenu;
6205 impl crate::widget::Widget for StubMenu {
6206 fn layout_response(
6207 &self,
6208 _proposal: SizeProposal,
6209 _ctx: &crate::widget::LayoutContext,
6210 ) -> crate::widget::LayoutResponse {
6211 teksilo_canvas::Size::new(100.0, 40.0).into()
6212 }
6213 }
6214
6215 #[derive(Debug)]
6227 struct NominatingWidget {
6228 row: std::cell::Cell<Option<WidgetId>>,
6229 }
6230
6231 impl crate::widget::Widget for NominatingWidget {
6232 fn layout_response(
6233 &self,
6234 proposal: SizeProposal,
6235 _ctx: &LayoutContext,
6236 ) -> crate::widget::LayoutResponse {
6237 proposal.resolve(50.0, 20.0).into()
6238 }
6239
6240 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6241 ctx.apply_self_handlers(crate::widget_builder::HandlerSet::new().focusable(true));
6242 Vec::new()
6243 }
6244
6245 fn context_menu_key_target(&self) -> Option<WidgetId> {
6246 self.row.get()
6247 }
6248 }
6249
6250 fn press(tree: &mut WidgetTree, key: Key, modifiers: Modifiers) {
6251 tree.dispatch_event(WidgetEvent::KeyDown {
6252 key,
6253 modifiers,
6254 text: None,
6255 });
6256 }
6257
6258 #[test]
6259 fn the_context_menu_key_opens_the_focused_widget_menu() {
6260 use std::cell::Cell;
6261 use std::rc::Rc;
6262
6263 let opened = Rc::new(Cell::new(false));
6264 let flag = opened.clone();
6265 let mut tree = WidgetTree::new();
6266 let widget = tree.add(
6267 FillWidget::new()
6268 .focusable()
6269 .context_menu(move |_pos, _ctx| {
6270 flag.set(true);
6271 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6272 }),
6273 );
6274 tree.layout(SizeProposal::exact(200.0, 100.0));
6275 tree.focus(widget);
6276
6277 press(&mut tree, Key::ContextMenu, Modifiers::NONE);
6278 assert!(opened.get(), "the dedicated Menu key must open the menu");
6279 }
6280
6281 #[test]
6284 fn shift_f10_opens_the_focused_widget_menu() {
6285 use std::cell::Cell;
6286 use std::rc::Rc;
6287
6288 let opened = Rc::new(Cell::new(false));
6289 let flag = opened.clone();
6290 let mut tree = WidgetTree::new();
6291 let widget = tree.add(
6292 FillWidget::new()
6293 .focusable()
6294 .context_menu(move |_pos, _ctx| {
6295 flag.set(true);
6296 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6297 }),
6298 );
6299 tree.layout(SizeProposal::exact(200.0, 100.0));
6300 tree.focus(widget);
6301
6302 press(&mut tree, Key::F10, Modifiers::SHIFT);
6303 assert!(opened.get(), "Shift+F10 must open the menu");
6304 }
6305
6306 #[test]
6309 fn a_near_miss_chord_is_not_a_context_menu_request() {
6310 use std::cell::Cell;
6311 use std::rc::Rc;
6312
6313 let opened = Rc::new(Cell::new(false));
6314 let flag = opened.clone();
6315 let mut tree = WidgetTree::new();
6316 let widget = tree.add(
6317 FillWidget::new()
6318 .focusable()
6319 .context_menu(move |_pos, _ctx| {
6320 flag.set(true);
6321 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6322 }),
6323 );
6324 tree.layout(SizeProposal::exact(200.0, 100.0));
6325 tree.focus(widget);
6326
6327 press(&mut tree, Key::F10, Modifiers::SHIFT | Modifiers::CTRL);
6328 press(&mut tree, Key::F10, Modifiers::NONE);
6329 assert!(!opened.get(), "only Shift+F10 exactly asks for a menu");
6330 }
6331
6332 #[test]
6336 fn the_keyboard_target_can_be_a_row_rather_than_the_focused_container() {
6337 use std::cell::Cell;
6338 use std::rc::Rc;
6339
6340 let menu_owner = Rc::new(Cell::new(None::<&'static str>));
6341
6342 let row_flag = menu_owner.clone();
6343 let container_flag = menu_owner.clone();
6344
6345 let mut tree = WidgetTree::new();
6346 let row = tree.add(FillWidget::new().context_menu(move |_pos, _ctx| {
6347 row_flag.set(Some("row"));
6348 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6349 }));
6350 let container = tree.add(
6351 crate::test_widgets::StackWidget::new()
6352 .child(row)
6353 .context_menu(move |_pos, _ctx| {
6354 container_flag.set(Some("container"));
6355 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6356 }),
6357 );
6358 tree.layout(SizeProposal::exact(200.0, 100.0));
6359
6360 let nominator = tree.add(NominatingWidget {
6362 row: std::cell::Cell::new(Some(row)),
6363 });
6364 tree.layout(SizeProposal::exact(200.0, 100.0));
6365 tree.focus(nominator);
6366 let _ = container;
6367
6368 press(&mut tree, Key::ContextMenu, Modifiers::NONE);
6369 assert_eq!(
6370 menu_owner.get(),
6371 Some("row"),
6372 "the nominated row's factory must be the one that runs"
6373 );
6374 }
6375
6376 #[test]
6379 fn the_chord_falls_through_when_there_is_no_menu_to_show() {
6380 use std::cell::Cell;
6381 use std::rc::Rc;
6382
6383 let saw_key = Rc::new(Cell::new(false));
6384 let flag = saw_key.clone();
6385 let mut tree = WidgetTree::new();
6386 let widget = tree.add(FillWidget::new().focusable().on_key(move |ev, _ctx| {
6387 if matches!(ev, WidgetEvent::KeyDown { key: Key::F10, .. }) {
6388 flag.set(true);
6389 }
6390 crate::event::EventResponse::Ignored
6391 }));
6392 tree.layout(SizeProposal::exact(200.0, 100.0));
6393 tree.focus(widget);
6394
6395 press(&mut tree, Key::F10, Modifiers::SHIFT);
6396 assert!(
6397 saw_key.get(),
6398 "with no factory anywhere, the key must reach the widget"
6399 );
6400 }
6401
6402 #[test]
6408 fn each_route_to_a_menu_tells_the_factory_what_asked_for_it() {
6409 use crate::widget_builder::ContextMenuTrigger;
6410 use std::cell::RefCell;
6411 use std::rc::Rc;
6412
6413 let seen = Rc::new(RefCell::new(Vec::new()));
6414 let log = seen.clone();
6415 let mut tree = WidgetTree::new();
6416 let widget = tree.add(
6417 FillWidget::new()
6418 .focusable()
6419 .context_menu(move |_pos, ctx| {
6420 log.borrow_mut().push(ctx.context_menu_trigger());
6421 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6422 }),
6423 );
6424 tree.layout(SizeProposal::exact(200.0, 100.0));
6425 tree.focus(widget);
6426 let inside = tree.bounds(widget).center();
6427
6428 press(&mut tree, Key::F10, Modifiers::SHIFT);
6429 press(&mut tree, Key::Escape, Modifiers::NONE);
6430 press(&mut tree, Key::ContextMenu, Modifiers::NONE);
6431 press(&mut tree, Key::Escape, Modifiers::NONE);
6432 tree.dispatch_event(WidgetEvent::pointer_down(
6433 inside,
6434 crate::event::PointerButton::Secondary,
6435 Modifiers::NONE,
6436 ));
6437 tree.dispatch_event(WidgetEvent::pointer_up(
6438 inside,
6439 crate::event::PointerButton::Secondary,
6440 Modifiers::NONE,
6441 ));
6442 press(&mut tree, Key::Escape, Modifiers::NONE);
6443 tree.dispatch_event(WidgetEvent::AccessAction {
6444 action: accesskit::Action::ShowContextMenu,
6445 target: Some(widget),
6446 target_node: crate::accessibility::widget_id_to_node_id(widget),
6447 data: None,
6448 });
6449 press(&mut tree, Key::Escape, Modifiers::NONE);
6450 tree.long_press_at(teksilo_tokens::PointerKind::Touch, inside);
6451
6452 assert_eq!(
6453 *seen.borrow(),
6454 vec![
6455 Some(ContextMenuTrigger::Keyboard),
6456 Some(ContextMenuTrigger::Keyboard),
6457 Some(ContextMenuTrigger::Pointer),
6458 Some(ContextMenuTrigger::Accessibility),
6459 Some(ContextMenuTrigger::Pointer),
6460 ],
6461 "Shift+F10, the Menu key, a right-click, the assistive action and a \
6462 hold, in that order"
6463 );
6464 }
6465
6466 #[test]
6467 fn context_menu_factory_receives_click_position() {
6468 use crate::event::{Modifiers, PointerButton};
6469 use std::cell::Cell;
6470 use std::rc::Rc;
6471
6472 let captured_position = Rc::new(Cell::new(None::<Point>));
6473 let cap = captured_position.clone();
6474 let mut tree = WidgetTree::new();
6475 let widget = tree.add(FillWidget::new().context_menu(move |pos, _ctx| {
6476 cap.set(Some(pos));
6477 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6478 }));
6479 tree.layout(SizeProposal::exact(200.0, 100.0));
6480
6481 let click = Point::new(73.0, 42.0);
6482 tree.dispatch_event(WidgetEvent::pointer_down(
6483 click,
6484 PointerButton::Secondary,
6485 Modifiers::NONE,
6486 ));
6487
6488 let got = captured_position.get();
6489 assert_eq!(
6490 got,
6491 Some(click),
6492 "factory must receive the click position; got {:?}",
6493 got
6494 );
6495 let _ = widget;
6496 }
6497
6498 #[test]
6499 fn context_menu_factory_returning_none_falls_through_to_parent() {
6500 use crate::event::{Modifiers, PointerButton};
6501 use crate::test_widgets::StackWidget;
6502 use std::cell::Cell;
6503 use std::rc::Rc;
6504
6505 let outer_called = Rc::new(Cell::new(0_u32));
6509 let outer_flag = outer_called.clone();
6510 let mut tree = WidgetTree::new();
6511 let inner = tree.add(FillWidget::new().context_menu(|_pos, _ctx| None));
6512 let _outer = tree.add(
6513 StackWidget::new()
6514 .child(inner)
6515 .context_menu(move |_pos, _ctx| {
6516 outer_flag.set(outer_flag.get() + 1);
6517 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6518 }),
6519 );
6520 tree.layout(SizeProposal::exact(200.0, 100.0));
6521
6522 tree.dispatch_event(WidgetEvent::pointer_down(
6523 Point::new(50.0, 25.0),
6524 PointerButton::Secondary,
6525 Modifiers::NONE,
6526 ));
6527
6528 assert_eq!(
6529 outer_called.get(),
6530 1,
6531 "inner returning None must fall through to the outer factory"
6532 );
6533 }
6534
6535 #[test]
6536 fn context_menu_factory_none_throughout_chain_does_not_show_overlay() {
6537 use crate::event::{Modifiers, PointerButton};
6538
6539 let mut tree = WidgetTree::new();
6541 tree.add(FillWidget::new().context_menu(|_pos, _ctx| None));
6542 tree.layout(SizeProposal::exact(200.0, 100.0));
6543
6544 let overlay_count_before = tree.overlay_manager.len();
6545 tree.dispatch_event(WidgetEvent::pointer_down(
6546 Point::new(50.0, 25.0),
6547 PointerButton::Secondary,
6548 Modifiers::NONE,
6549 ));
6550 let overlay_count_after = tree.overlay_manager.len();
6551 assert_eq!(
6552 overlay_count_before, overlay_count_after,
6553 "a factory returning None must not mount any overlay"
6554 );
6555 }
6556
6557 #[derive(Debug)]
6568 struct FreshChildHost {
6569 preserve: bool,
6570 }
6571 impl Widget for FreshChildHost {
6572 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6573 vec![ctx.add(FillWidget::new())]
6574 }
6575 fn layout_response(
6576 &self,
6577 p: SizeProposal,
6578 _c: &LayoutContext,
6579 ) -> crate::widget::LayoutResponse {
6580 p.resolve(10.0, 10.0).into()
6581 }
6582 fn preserves_children_on_rebuild(&self) -> bool {
6583 self.preserve
6584 }
6585 }
6586
6587 #[test]
6588 fn reconcile_reaps_dropped_children_no_leak() {
6589 for preserve in [false, true] {
6594 let mut tree = WidgetTree::new();
6595 let host = tree.add(FreshChildHost { preserve });
6596 tree.layout(SizeProposal::exact(100.0, 100.0));
6597 let total0 = tree.arena.len();
6598 let active0 = tree.active_widget_count();
6599 for _ in 0..5 {
6600 tree.arena_mark_needs_rebuild_for_testing(host);
6601 tree.layout(SizeProposal::exact(100.0, 100.0));
6602 }
6603 assert_eq!(
6604 tree.arena.len(),
6605 total0,
6606 "preserve={preserve}: dropped children must be reaped, not leaked"
6607 );
6608 assert_eq!(
6609 tree.active_widget_count(),
6610 active0,
6611 "preserve={preserve}: no stranded still-active orphans"
6612 );
6613 }
6614 }
6615
6616 #[derive(Debug)]
6622 struct DetachedContentHost {
6623 preserve: bool,
6624 }
6625 impl Widget for DetachedContentHost {
6626 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6627 let popup = ctx.add_detached(FillWidget::new());
6628 ctx.set_dormant(popup);
6629 vec![ctx.add(FillWidget::new())]
6630 }
6631 fn layout_response(
6632 &self,
6633 p: SizeProposal,
6634 _c: &LayoutContext,
6635 ) -> crate::widget::LayoutResponse {
6636 p.resolve(10.0, 10.0).into()
6637 }
6638 fn preserves_children_on_rebuild(&self) -> bool {
6639 self.preserve
6640 }
6641 }
6642
6643 #[test]
6644 fn rebuilding_reaps_detached_content_no_leak() {
6645 for preserve in [false, true] {
6651 let mut tree = WidgetTree::new();
6652 let host = tree.add(DetachedContentHost { preserve });
6653 tree.layout(SizeProposal::exact(100.0, 100.0));
6654 let total0 = tree.arena.len();
6655 for _ in 0..5 {
6656 tree.arena_mark_needs_rebuild_for_testing(host);
6657 tree.layout(SizeProposal::exact(100.0, 100.0));
6658 }
6659 assert_eq!(
6660 tree.arena.len(),
6661 total0,
6662 "preserve={preserve}: the previous build's detached content must be reaped"
6663 );
6664 }
6665 }
6666
6667 #[test]
6668 fn destroying_a_host_reaps_its_detached_content() {
6669 let mut tree = WidgetTree::new();
6670 let outer = tree.add(FillWidget::new());
6671 tree.layout(SizeProposal::exact(100.0, 100.0));
6672 let empty = tree.arena.len();
6673
6674 let host = tree.add_child(outer, DetachedContentHost { preserve: false });
6675 tree.layout(SizeProposal::exact(100.0, 100.0));
6676 assert!(tree.arena.len() > empty);
6677
6678 tree.destroy_subtree(host);
6679 assert_eq!(
6680 tree.arena.len(),
6681 empty,
6682 "the popup must die with the widget that built it"
6683 );
6684 }
6685
6686 #[derive(Debug)]
6688 struct StableChildHost {
6689 child: Option<WidgetId>,
6690 probe: std::rc::Rc<std::cell::Cell<Option<WidgetId>>>,
6691 }
6692 impl Widget for StableChildHost {
6693 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6694 let id = match self.child {
6695 Some(id) => id,
6696 None => {
6697 let id = ctx.add(FillWidget::new());
6698 self.child = Some(id);
6699 self.probe.set(Some(id));
6700 id
6701 }
6702 };
6703 vec![id]
6704 }
6705 fn layout_response(
6706 &self,
6707 p: SizeProposal,
6708 _c: &LayoutContext,
6709 ) -> crate::widget::LayoutResponse {
6710 p.resolve(10.0, 10.0).into()
6711 }
6712 fn preserves_children_on_rebuild(&self) -> bool {
6713 true
6714 }
6715 }
6716
6717 #[test]
6718 fn reconcile_preserves_reattached_child() {
6719 let probe = std::rc::Rc::new(std::cell::Cell::new(None));
6720 let mut tree = WidgetTree::new();
6721 let host = tree.add(StableChildHost {
6722 child: None,
6723 probe: probe.clone(),
6724 });
6725 tree.layout(SizeProposal::exact(100.0, 100.0));
6726 let child = probe.get().expect("child mounted");
6727 let total0 = tree.arena.len();
6728 for _ in 0..5 {
6729 tree.arena_mark_needs_rebuild_for_testing(host);
6730 tree.layout(SizeProposal::exact(100.0, 100.0));
6731 }
6732 assert!(
6733 tree.arena.is_active(child),
6734 "the re-attached child must survive every rebuild"
6735 );
6736 assert_eq!(tree.arena.len(), total0, "no growth — same child reused");
6737 }
6738
6739 #[derive(Debug)]
6741 struct Wrapper {
6742 child: WidgetId,
6743 }
6744 impl Widget for Wrapper {
6745 fn build(&mut self, _ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6746 vec![self.child]
6747 }
6748 fn layout_response(
6749 &self,
6750 p: SizeProposal,
6751 _c: &LayoutContext,
6752 ) -> crate::widget::LayoutResponse {
6753 p.resolve(10.0, 10.0).into()
6754 }
6755 }
6756
6757 #[derive(Debug)]
6761 struct ReparentHost {
6762 body: Option<WidgetId>,
6763 probe: std::rc::Rc<std::cell::Cell<Option<WidgetId>>>,
6764 }
6765 impl Widget for ReparentHost {
6766 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6767 let body = match self.body {
6768 Some(id) => id,
6769 None => {
6770 let id = ctx.add(FillWidget::new());
6771 self.body = Some(id);
6772 self.probe.set(Some(id));
6773 id
6774 }
6775 };
6776 vec![ctx.add(Wrapper { child: body })]
6777 }
6778 fn layout_response(
6779 &self,
6780 p: SizeProposal,
6781 _c: &LayoutContext,
6782 ) -> crate::widget::LayoutResponse {
6783 p.resolve(10.0, 10.0).into()
6784 }
6785 fn preserves_children_on_rebuild(&self) -> bool {
6786 true
6787 }
6788 }
6789
6790 #[test]
6791 fn reconcile_spares_reparented_survivor() {
6792 let probe = std::rc::Rc::new(std::cell::Cell::new(None));
6799 let mut tree = WidgetTree::new();
6800 let host = tree.add(ReparentHost {
6801 body: None,
6802 probe: probe.clone(),
6803 });
6804 tree.layout(SizeProposal::exact(100.0, 100.0));
6805 let body = probe.get().expect("body mounted");
6806 let total0 = tree.arena.len();
6807 for _ in 0..5 {
6808 tree.arena_mark_needs_rebuild_for_testing(host);
6809 tree.layout(SizeProposal::exact(100.0, 100.0));
6810 }
6811 assert!(
6812 tree.arena.is_active(body),
6813 "the re-parented body must survive — it was moved into the new tree, \
6814 not swept with the dropped wrapper"
6815 );
6816 assert_eq!(
6817 tree.arena.len(),
6818 total0,
6819 "dropped wrappers reaped — no per-rebuild leak"
6820 );
6821 }
6822
6823 #[derive(Debug)]
6833 struct BelowContainer {
6834 child: Option<WidgetId>,
6835 offset: f32,
6836 }
6837
6838 impl crate::widget::Widget for BelowContainer {
6839 fn layout_response(
6840 &self,
6841 proposal: SizeProposal,
6842 _ctx: &crate::widget::LayoutContext,
6843 ) -> crate::widget::LayoutResponse {
6844 proposal.resolve(0.0, 0.0).into()
6845 }
6846 fn place_children(
6847 &self,
6848 bounds: Rect,
6849 _proposal: SizeProposal,
6850 children: &mut [crate::widget::WidgetPlacement],
6851 _ctx: &crate::widget::LayoutContext,
6852 ) {
6853 for c in children.iter_mut() {
6854 c.origin = Point::new(bounds.x, bounds.y + self.offset);
6855 c.size = bounds.size();
6856 }
6857 }
6858 fn children(&self) -> Vec<WidgetId> {
6859 self.child.into_iter().collect()
6860 }
6861 }
6862
6863 fn recording_scroll_container(
6866 tree: &mut WidgetTree,
6867 child: WidgetId,
6868 recorded: std::rc::Rc<std::cell::Cell<Option<Rect>>>,
6869 ) -> WidgetId {
6870 use crate::test_widgets::StackWidget;
6871 tree.add(
6872 StackWidget::new()
6873 .child(child)
6874 .on_scroll(move |ev, _ctx| match ev {
6875 WidgetEvent::ScrollIntoView { target_bounds, .. } => {
6876 recorded.set(Some(*target_bounds));
6877 EventResponse::Handled
6878 }
6879 _ => EventResponse::Ignored,
6880 })
6881 .clips_children(true),
6882 )
6883 }
6884
6885 #[test]
6886 fn ensure_visible_dispatches_scroll_into_view_to_clipping_ancestor() {
6887 use std::cell::Cell;
6888 use std::rc::Rc;
6889 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6890 let mut tree = WidgetTree::new();
6891 let actor = tree.add(FillWidget::new());
6892 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
6893 tree.layout(SizeProposal::exact(100.0, 100.0));
6894
6895 let target = Rect::new(10.0, 500.0, 20.0, 15.0);
6898 let mut ctx = EventContext::new();
6899 ctx.ensure_visible(target);
6900 tree.collect_from_ctx(ctx, actor);
6901
6902 assert_eq!(
6903 recorded.get(),
6904 Some(target),
6905 "ensure_visible(rect) must dispatch ScrollIntoView with the exact rect \
6906 to the clips_children ancestor"
6907 );
6908 }
6909
6910 #[test]
6911 fn ensure_visible_is_noop_when_rect_already_visible() {
6912 use std::cell::Cell;
6913 use std::rc::Rc;
6914 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6915 let mut tree = WidgetTree::new();
6916 let actor = tree.add(FillWidget::new());
6917 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
6918 tree.layout(SizeProposal::exact(100.0, 100.0));
6919
6920 let mut ctx = EventContext::new();
6923 ctx.ensure_visible(Rect::new(10.0, 10.0, 20.0, 15.0));
6924 tree.collect_from_ctx(ctx, actor);
6925
6926 assert_eq!(
6927 recorded.get(),
6928 None,
6929 "a rect already inside the viewport must not trigger a scroll"
6930 );
6931 }
6932
6933 #[test]
6934 fn ensure_visible_margin_forces_scroll_near_edge() {
6935 use std::cell::Cell;
6936 use std::rc::Rc;
6937 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6938 let mut tree = WidgetTree::new();
6939 let actor = tree.add(FillWidget::new());
6940 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
6941 tree.layout(SizeProposal::exact(100.0, 100.0));
6942
6943 let rect = Rect::new(10.0, 95.0, 20.0, 4.0);
6946 let mut ctx = EventContext::new();
6947 ctx.ensure_visible_with_margin(rect, 10.0);
6948 tree.collect_from_ctx(ctx, actor);
6949
6950 assert_eq!(
6951 recorded.get(),
6952 Some(rect),
6953 "the margin must widen the visibility test so a near-edge rect scrolls"
6954 );
6955 }
6956
6957 fn recording_align_container(
6960 tree: &mut WidgetTree,
6961 child: WidgetId,
6962 recorded: std::rc::Rc<
6963 std::cell::Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>,
6964 >,
6965 ) -> WidgetId {
6966 use crate::test_widgets::StackWidget;
6967 tree.add(
6968 StackWidget::new()
6969 .child(child)
6970 .on_scroll(move |ev, _ctx| match ev {
6971 WidgetEvent::ScrollIntoView { align, motion, .. } => {
6972 recorded.set(Some((*align, *motion)));
6973 EventResponse::Handled
6974 }
6975 _ => EventResponse::Ignored,
6976 })
6977 .clips_children(true),
6978 )
6979 }
6980
6981 #[test]
6982 fn ensure_visible_aligned_scrolls_even_when_already_visible() {
6983 use std::cell::Cell;
6984 use std::rc::Rc;
6985 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6986 let mut tree = WidgetTree::new();
6987 let actor = tree.add(FillWidget::new());
6988 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
6989 tree.layout(SizeProposal::exact(100.0, 100.0));
6990
6991 let target = Rect::new(10.0, 10.0, 20.0, 15.0);
6996 let mut ctx = EventContext::new();
6997 ctx.ensure_visible_aligned(target, 0.5, crate::event::ScrollMotion::Instant);
6998 tree.collect_from_ctx(ctx, actor);
6999
7000 assert_eq!(
7001 recorded.get(),
7002 Some(target),
7003 "an aligned reveal must dispatch even when the rect is already visible"
7004 );
7005 }
7006
7007 #[test]
7008 fn ensure_visible_aligned_forwards_fraction_and_motion() {
7009 use std::cell::Cell;
7010 use std::rc::Rc;
7011 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
7012 Rc::new(Cell::new(None));
7013 let mut tree = WidgetTree::new();
7014 let actor = tree.add(FillWidget::new());
7015 let _container = recording_align_container(&mut tree, actor, recorded.clone());
7016 tree.layout(SizeProposal::exact(100.0, 100.0));
7017
7018 let mut ctx = EventContext::new();
7019 ctx.ensure_visible_aligned(
7020 Rect::new(10.0, 10.0, 20.0, 15.0),
7021 0.25,
7022 crate::event::ScrollMotion::Smooth,
7023 );
7024 tree.collect_from_ctx(ctx, actor);
7025
7026 assert_eq!(
7027 recorded.get(),
7028 Some((
7029 crate::event::ScrollAlign::Fraction(0.25),
7030 crate::event::ScrollMotion::Smooth
7031 )),
7032 "the container must receive the requested fraction and motion verbatim"
7033 );
7034 }
7035
7036 #[test]
7037 fn ensure_visible_aligned_clamps_the_fraction() {
7038 use std::cell::Cell;
7039 use std::rc::Rc;
7040 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
7041 Rc::new(Cell::new(None));
7042 let mut tree = WidgetTree::new();
7043 let actor = tree.add(FillWidget::new());
7044 let _container = recording_align_container(&mut tree, actor, recorded.clone());
7045 tree.layout(SizeProposal::exact(100.0, 100.0));
7046
7047 let mut ctx = EventContext::new();
7048 ctx.ensure_visible_aligned(
7049 Rect::new(10.0, 10.0, 20.0, 15.0),
7050 4.2,
7051 crate::event::ScrollMotion::Instant,
7052 );
7053 tree.collect_from_ctx(ctx, actor);
7054
7055 assert_eq!(
7056 recorded.get().map(|(a, _)| a),
7057 Some(crate::event::ScrollAlign::Fraction(1.0)),
7058 "an out-of-range fraction must clamp rather than aim the pin off-screen"
7059 );
7060 }
7061
7062 #[test]
7063 fn plain_ensure_visible_requests_minimal_alignment() {
7064 use std::cell::Cell;
7065 use std::rc::Rc;
7066 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
7067 Rc::new(Cell::new(None));
7068 let mut tree = WidgetTree::new();
7069 let actor = tree.add(FillWidget::new());
7070 let _container = recording_align_container(&mut tree, actor, recorded.clone());
7071 tree.layout(SizeProposal::exact(100.0, 100.0));
7072
7073 let mut ctx = EventContext::new();
7074 ctx.ensure_visible(Rect::new(10.0, 500.0, 20.0, 15.0));
7075 tree.collect_from_ctx(ctx, actor);
7076
7077 assert_eq!(
7078 recorded.get(),
7079 Some((
7080 crate::event::ScrollAlign::Minimal,
7081 crate::event::ScrollMotion::Instant
7082 )),
7083 "the pre-existing reveal API must keep its exact semantics"
7084 );
7085 }
7086
7087 #[test]
7088 fn only_the_innermost_container_aligns() {
7089 use std::cell::Cell;
7090 use std::rc::Rc;
7091 let inner_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
7092 Rc::new(Cell::new(None));
7093 let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
7094 Rc::new(Cell::new(None));
7095
7096 let mut tree = WidgetTree::new();
7097 let actor = tree.add(FillWidget::new());
7098 let inner = recording_align_container(&mut tree, actor, inner_rec.clone());
7099 let _outer = recording_align_container(&mut tree, inner, outer_rec.clone());
7100 tree.layout(SizeProposal::exact(100.0, 100.0));
7101
7102 let mut ctx = EventContext::new();
7105 ctx.ensure_visible_aligned(
7106 Rect::new(10.0, 500.0, 20.0, 15.0),
7107 0.5,
7108 crate::event::ScrollMotion::Instant,
7109 );
7110 tree.collect_from_ctx(ctx, actor);
7111
7112 assert_eq!(
7113 inner_rec.get().map(|(a, _)| a),
7114 Some(crate::event::ScrollAlign::Fraction(0.5)),
7115 "the innermost clipping ancestor owns the pin"
7116 );
7117 assert_eq!(
7118 outer_rec.get().map(|(a, _)| a),
7119 Some(crate::event::ScrollAlign::Minimal),
7120 "an outer container must only bring the inner viewport into view — a \
7121 fraction names a height in one viewport, not in every ancestor's"
7122 );
7123 }
7124
7125 #[test]
7126 fn a_clipper_that_does_not_scroll_passes_the_pin_on() {
7127 use crate::test_widgets::StackWidget;
7134 use std::cell::Cell;
7135 use std::rc::Rc;
7136 let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
7137 Rc::new(Cell::new(None));
7138
7139 let mut tree = WidgetTree::new();
7140 let actor = tree.add(FillWidget::new());
7141 let clipper = tree.add(StackWidget::new().child(actor).clips_children_on(true));
7142 let _scroller = recording_align_container(&mut tree, clipper, outer_rec.clone());
7143 tree.layout(SizeProposal::exact(100.0, 100.0));
7144
7145 let mut ctx = EventContext::new();
7148 ctx.ensure_visible_aligned(
7149 Rect::new(10.0, 10.0, 20.0, 15.0),
7150 0.5,
7151 crate::event::ScrollMotion::Instant,
7152 );
7153 tree.collect_from_ctx(ctx, actor);
7154
7155 assert_eq!(
7156 outer_rec.get().map(|(a, _)| a),
7157 Some(crate::event::ScrollAlign::Fraction(0.5)),
7158 "a clipping wrapper with no scroll handler must pass the pin through \
7159 to the scroll container above it"
7160 );
7161 }
7162
7163 #[test]
7164 fn a_scroller_that_declines_to_move_still_holds_the_pin() {
7165 use crate::test_widgets::StackWidget;
7170 use std::cell::Cell;
7171 use std::rc::Rc;
7172 let inner_rec: Rc<Cell<Option<crate::event::ScrollAlign>>> = Rc::new(Cell::new(None));
7173 let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
7174 Rc::new(Cell::new(None));
7175
7176 let mut tree = WidgetTree::new();
7177 let actor = tree.add(FillWidget::new());
7178 let rec = inner_rec.clone();
7179 let inner = tree.add(
7180 StackWidget::new()
7181 .child(actor)
7182 .on_scroll(move |ev, _ctx| match ev {
7183 WidgetEvent::ScrollIntoView { align, .. } => {
7184 rec.set(Some(*align));
7185 EventResponse::Ignored
7186 }
7187 _ => EventResponse::Ignored,
7188 })
7189 .clips_children(true),
7190 );
7191 let _outer = recording_align_container(&mut tree, inner, outer_rec.clone());
7192 tree.layout(SizeProposal::exact(100.0, 100.0));
7193
7194 let mut ctx = EventContext::new();
7196 ctx.ensure_visible_aligned(
7197 Rect::new(10.0, 500.0, 20.0, 15.0),
7198 0.5,
7199 crate::event::ScrollMotion::Instant,
7200 );
7201 tree.collect_from_ctx(ctx, actor);
7202
7203 assert_eq!(
7204 inner_rec.get(),
7205 Some(crate::event::ScrollAlign::Fraction(0.5)),
7206 "the innermost scroller is offered the pin"
7207 );
7208 assert_eq!(
7209 outer_rec.get().map(|(a, _)| a),
7210 Some(crate::event::ScrollAlign::Minimal),
7211 "declining to move must not pass the pin outward"
7212 );
7213 }
7214
7215 #[test]
7216 fn ensure_widget_visible_uses_target_arena_bounds() {
7217 use std::cell::Cell;
7218 use std::rc::Rc;
7219 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
7220 let mut tree = WidgetTree::new();
7221 let target = tree.add(FillWidget::new());
7223 let rec = recorded.clone();
7224 let container = tree.add(
7225 BelowContainer {
7226 child: Some(target),
7227 offset: 500.0,
7228 }
7229 .on_scroll(move |ev, _ctx| match ev {
7230 WidgetEvent::ScrollIntoView { target_bounds, .. } => {
7231 rec.set(Some(*target_bounds));
7232 EventResponse::Handled
7233 }
7234 _ => EventResponse::Ignored,
7235 })
7236 .clips_children(true),
7237 );
7238 tree.layout(SizeProposal::exact(100.0, 100.0));
7239
7240 let expected = tree.bounds(target);
7241 assert!(
7242 expected.y > 100.0,
7243 "fixture sanity: the target must sit below the viewport (y={})",
7244 expected.y
7245 );
7246
7247 let mut ctx = EventContext::new();
7250 ctx.ensure_widget_visible(target);
7251 tree.collect_from_ctx(ctx, container);
7252
7253 assert_eq!(
7254 recorded.get(),
7255 Some(expected),
7256 "ensure_widget_visible(id) must dispatch ScrollIntoView with the \
7257 target's current arena bounds"
7258 );
7259 }
7260
7261 #[test]
7262 fn ensure_widget_visible_ignores_missing_widget() {
7263 use std::cell::Cell;
7265 use std::rc::Rc;
7266 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
7267 let mut tree = WidgetTree::new();
7268 let actor = tree.add(FillWidget::new());
7269 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
7270 tree.layout(SizeProposal::exact(100.0, 100.0));
7271
7272 let mut ctx = EventContext::new();
7273 ctx.ensure_widget_visible(WidgetId::default());
7274 tree.collect_from_ctx(ctx, actor); assert_eq!(
7277 recorded.get(),
7278 None,
7279 "an unmounted id must not trigger a scroll"
7280 );
7281 }
7282
7283 #[test]
7284 fn context_menu_inside_a_modal_keeps_the_modal() {
7285 use crate::event::{Modifiers, PointerButton, WidgetEvent};
7289 use crate::overlay::{DismissBehavior, OverlayPlacement, OverlayRequest};
7290 use crate::test_widgets::{FillWidget, StackWidget};
7291
7292 let mut tree = WidgetTree::new();
7293 let modal_content = tree.add(StackWidget::new());
7296 let _editor = tree.add_child(
7297 modal_content,
7298 FillWidget::new()
7299 .context_menu(|_pos, _ctx| Some(Box::new(FillWidget::new()) as Box<dyn Widget>)),
7300 );
7301 tree.layout(SizeProposal::exact(200.0, 100.0));
7302
7303 let modal = tree.overlay_manager.show(OverlayRequest::new(
7304 modal_content,
7305 modal_content,
7306 OverlayPlacement::Centered,
7307 DismissBehavior::EscapeKey,
7308 ));
7309 tree.overlay_manager
7311 .stack
7312 .iter_mut()
7313 .find(|o| o.id == modal)
7314 .unwrap()
7315 .bounds = Rect::new(0.0, 0.0, 200.0, 100.0);
7316 assert_eq!(tree.overlay_manager.len(), 1);
7317
7318 tree.dispatch_event(WidgetEvent::pointer_down(
7320 Point::new(50.0, 25.0),
7321 PointerButton::Secondary,
7322 Modifiers::NONE,
7323 ));
7324
7325 assert!(
7326 tree.overlay_manager.active_ids().contains(&modal),
7327 "the modal must survive opening a context menu inside it"
7328 );
7329 assert_eq!(
7330 tree.overlay_manager.len(),
7331 2,
7332 "the context menu should now be open on top of the surviving modal"
7333 );
7334 }
7335}
7336
7337#[cfg(test)]
7340mod input_ingress_tests {
7341 use super::*;
7342 use crate::event::{EventResponse, Modifiers, PointerButton, ScrollDelta};
7343 use crate::pointer::{
7344 EventTime, PointerId, PointerInfo, PointerPhase, PointerSample, ScrollPhase, ScrollSample,
7345 ScrollSource,
7346 };
7347 use crate::test_widgets::FillWidget;
7348 use crate::widget::{LayoutContext, WidgetPlacement};
7349 use crate::widget_builder::WidgetBuilder;
7350 use std::cell::RefCell;
7351 use std::rc::Rc;
7352
7353 #[derive(Debug)]
7360 struct RowWidget {
7361 children: Vec<WidgetId>,
7362 }
7363
7364 impl crate::widget::Widget for RowWidget {
7365 fn layout_response(
7366 &self,
7367 proposal: SizeProposal,
7368 _ctx: &LayoutContext,
7369 ) -> crate::widget::LayoutResponse {
7370 proposal.resolve(0.0, 0.0).into()
7371 }
7372
7373 fn place_children(
7374 &self,
7375 bounds: Rect,
7376 _proposal: SizeProposal,
7377 children: &mut [WidgetPlacement],
7378 _ctx: &LayoutContext,
7379 ) {
7380 let n = children.len().max(1) as f32;
7381 let w = bounds.width / n;
7382 for (i, child) in children.iter_mut().enumerate() {
7383 child.origin = Point::new(bounds.x + w * i as f32, bounds.y);
7384 child.size = teksilo_canvas::Size::new(w, bounds.height);
7385 }
7386 }
7387
7388 fn children(&self) -> Vec<WidgetId> {
7389 self.children.clone()
7390 }
7391 }
7392
7393 fn record(drive: impl FnOnce(&mut WidgetTree)) -> Vec<String> {
7396 let log: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(Vec::new()));
7397
7398 let mut tree = WidgetTree::new();
7399 let pointer_log = log.clone();
7400 let hover_log = log.clone();
7401 tree.add(
7402 FillWidget::new()
7403 .on_pointer_event(move |event, _ctx| {
7404 pointer_log.borrow_mut().push(format!("{event:?}"));
7405 EventResponse::Ignored
7406 })
7407 .on_hover(move |entered, _ctx| {
7408 hover_log.borrow_mut().push(format!("hover({entered})"));
7409 }),
7410 );
7411 tree.layout(SizeProposal::exact(100.0, 100.0));
7412
7413 drive(&mut tree);
7414 log.borrow().clone()
7415 }
7416
7417 #[test]
7423 fn a_mouse_sample_reproduces_todays_event_stream() {
7424 let inside = Point::new(40.0, 40.0);
7425 let moved = Point::new(60.0, 55.0);
7426 let outside = Point::new(400.0, 400.0);
7427
7428 let legacy = record(|tree| {
7429 tree.dispatch_event(WidgetEvent::pointer_move(inside));
7430 tree.dispatch_event(WidgetEvent::pointer_down(
7431 inside,
7432 PointerButton::Primary,
7433 Modifiers::NONE,
7434 ));
7435 tree.dispatch_event(WidgetEvent::pointer_move(moved));
7436 tree.dispatch_event(WidgetEvent::pointer_up(
7437 moved,
7438 PointerButton::Primary,
7439 Modifiers::NONE,
7440 ));
7441 tree.dispatch_event(WidgetEvent::pointer_move(outside));
7442 });
7443
7444 let sampled = record(|tree| {
7445 let t = EventTime::ZERO;
7446 tree.dispatch_pointer(PointerSample::mouse(PointerPhase::Move, inside, t));
7447 tree.dispatch_pointer(
7448 PointerSample::mouse(PointerPhase::Down, inside, t)
7449 .with_button(PointerButton::Primary),
7450 );
7451 tree.dispatch_pointer(PointerSample::mouse(PointerPhase::Move, moved, t));
7452 tree.dispatch_pointer(
7453 PointerSample::mouse(PointerPhase::Up, moved, t)
7454 .with_button(PointerButton::Primary),
7455 );
7456 tree.dispatch_pointer(PointerSample::mouse(PointerPhase::Move, outside, t));
7457 });
7458
7459 assert_eq!(legacy, sampled);
7460 assert!(
7461 legacy.contains(&"hover(true)".to_string())
7462 && legacy.contains(&"hover(false)".to_string()),
7463 "the fixture must actually exercise enter and leave: {legacy:?}"
7464 );
7465 }
7466
7467 #[test]
7471 fn a_buttonless_press_lowers_to_primary() {
7472 let events = record(|tree| {
7473 tree.dispatch_pointer(PointerSample::mouse(
7474 PointerPhase::Down,
7475 Point::new(20.0, 20.0),
7476 EventTime::ZERO,
7477 ));
7478 });
7479 assert!(
7480 events.iter().any(|e| e.contains("button: Primary")),
7481 "{events:?}"
7482 );
7483 }
7484
7485 #[test]
7491 fn a_sampled_press_and_release_form_a_tap() {
7492 let taps = Rc::new(std::cell::Cell::new(0));
7493 let seen = taps.clone();
7494 let mut tree = WidgetTree::new();
7495 tree.add(FillWidget::new().on_tap(move |_event, _ctx| seen.set(seen.get() + 1)));
7496 tree.layout(SizeProposal::exact(100.0, 100.0));
7497
7498 let at = Point::new(50.0, 50.0);
7499 tree.dispatch_pointer(PointerSample::mouse(
7500 PointerPhase::Down,
7501 at,
7502 EventTime::from_millis(0),
7503 ));
7504 tree.dispatch_pointer(PointerSample::mouse(
7505 PointerPhase::Up,
7506 at,
7507 EventTime::from_millis(5),
7508 ));
7509
7510 assert_eq!(taps.get(), 1);
7511 }
7512
7513 fn scroll_fixture() -> (WidgetTree, Rc<RefCell<Vec<&'static str>>>) {
7518 let hits: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
7519 let mut tree = WidgetTree::new();
7520
7521 let leading_hits = hits.clone();
7522 let trailing_hits = hits.clone();
7523 let leading = tree.add(FillWidget::new().on_scroll(move |_event, _ctx| {
7524 leading_hits.borrow_mut().push("leading");
7525 EventResponse::Handled
7526 }));
7527 let trailing = tree.add(FillWidget::new().on_scroll(move |_event, _ctx| {
7528 trailing_hits.borrow_mut().push("trailing");
7529 EventResponse::Handled
7530 }));
7531 tree.add(RowWidget {
7532 children: vec![leading, trailing],
7533 });
7534 tree.layout(SizeProposal::exact(200.0, 100.0));
7535 (tree, hits)
7536 }
7537
7538 fn notch() -> ScrollDelta {
7539 ScrollDelta::Lines { x: 0.0, y: -1.0 }
7540 }
7541
7542 #[test]
7545 fn a_positionless_scroll_routes_by_hover() {
7546 let (mut tree, hits) = scroll_fixture();
7547
7548 tree.pointer_move(Point::new(150.0, 50.0)); tree.dispatch_event(WidgetEvent::scroll(notch(), Modifiers::NONE));
7550 assert_eq!(*hits.borrow(), vec!["trailing"]);
7551
7552 tree.pointer_move(Point::new(50.0, 50.0)); tree.dispatch_scroll(ScrollSample::wheel(
7554 notch(),
7555 Modifiers::NONE,
7556 EventTime::ZERO,
7557 ));
7558 assert_eq!(*hits.borrow(), vec!["trailing", "leading"]);
7559 }
7560
7561 #[test]
7566 fn a_positioned_scroll_routes_by_hit_test() {
7567 let (mut tree, hits) = scroll_fixture();
7568 tree.pointer_move(Point::new(150.0, 50.0)); tree.dispatch_event(WidgetEvent::scroll_at(
7571 notch(),
7572 Modifiers::NONE,
7573 Point::new(50.0, 50.0), ));
7575 assert_eq!(*hits.borrow(), vec!["leading"]);
7576
7577 tree.dispatch_scroll(
7578 ScrollSample::wheel(notch(), Modifiers::NONE, EventTime::ZERO)
7579 .at(Point::new(150.0, 50.0)),
7580 );
7581 assert_eq!(*hits.borrow(), vec!["leading", "trailing"]);
7582 }
7583
7584 #[test]
7588 fn a_positionless_scroll_with_no_hover_goes_nowhere() {
7589 let (mut tree, hits) = scroll_fixture();
7590 tree.dispatch_event(WidgetEvent::scroll(notch(), Modifiers::NONE));
7591 assert!(hits.borrow().is_empty());
7592 }
7593
7594 #[test]
7599 fn a_handler_sees_the_sample_it_is_serving() {
7600 let seen: Rc<RefCell<Vec<(ScrollPhase, ScrollSource, Option<Point>)>>> =
7601 Rc::new(RefCell::new(Vec::new()));
7602 let sink = seen.clone();
7603
7604 let mut tree = WidgetTree::new();
7605 tree.add(
7612 FillWidget::new()
7613 .scroll_container(crate::pointer::touch_action::PanAxes::BOTH)
7614 .on_scroll(move |_event, ctx| {
7615 sink.borrow_mut().push((
7616 ctx.scroll_phase(),
7617 ctx.scroll_source(),
7618 ctx.pointer_position(),
7619 ));
7620 EventResponse::Handled
7621 }),
7622 );
7623 tree.layout(SizeProposal::exact(100.0, 100.0));
7624
7625 tree.dispatch_scroll(ScrollSample {
7626 delta: notch(),
7627 position: Some(Point::new(50.0, 50.0)),
7628 phase: ScrollPhase::Momentum,
7629 source: ScrollSource::TouchPan,
7630 pointer: PointerInfo::mouse(EventTime::from_millis(12)),
7631 modifiers: Modifiers::NONE,
7632 });
7633
7634 assert_eq!(
7635 *seen.borrow(),
7636 vec![(
7637 ScrollPhase::Momentum,
7638 ScrollSource::TouchPan,
7639 Some(Point::new(50.0, 50.0))
7640 )]
7641 );
7642 }
7643
7644 #[test]
7647 fn a_legacy_event_reports_the_mouse_at_the_epoch() {
7648 let seen: Rc<RefCell<Option<(PointerId, ScrollPhase)>>> = Rc::new(RefCell::new(None));
7649 let sink = seen.clone();
7650
7651 let mut tree = WidgetTree::new();
7652 tree.add(FillWidget::new().on_pointer_event(move |_event, ctx| {
7653 *sink.borrow_mut() = Some((ctx.pointer().id, ctx.scroll_phase()));
7654 EventResponse::Ignored
7655 }));
7656 tree.layout(SizeProposal::exact(100.0, 100.0));
7657 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(50.0, 50.0)));
7658
7659 assert_eq!(
7660 *seen.borrow(),
7661 Some((PointerId::MOUSE, ScrollPhase::Discrete))
7662 );
7663 }
7664
7665 #[test]
7669 fn the_snapshot_is_restored_after_a_dispatch() {
7670 let mut tree = WidgetTree::new();
7671 tree.add(FillWidget::new());
7672 tree.layout(SizeProposal::exact(100.0, 100.0));
7673
7674 let before = tree.current_input.clone();
7675 tree.dispatch_scroll(
7676 ScrollSample::wheel(notch(), Modifiers::NONE, EventTime::ZERO)
7677 .at(Point::new(10.0, 10.0)),
7678 );
7679 assert_eq!(tree.current_input, before);
7680 }
7681}
7682
7683#[cfg(test)]
7684mod press_and_focus_tests {
7685 use std::cell::{Cell, RefCell};
7690 use std::rc::Rc;
7691 use teksilo_canvas::{Point, SizeProposal};
7692
7693 use crate::WidgetId;
7694 use crate::event::{EventResponse, Key, Modifiers, PointerButton, WidgetEvent};
7695 use crate::focus::FocusOrigin;
7696 use crate::pointer::clock::ManualClock;
7697 use crate::pointer::touch_action::PanClaim;
7698 use crate::pointer::{
7699 BackendDeviceKey, EventTime, PointerId, PointerIdAllocator, PointerInfo, PointerPhase,
7700 PointerSample,
7701 };
7702 use crate::test_widgets::{FillWidget, StackWidget};
7703 use crate::widget_builder::WidgetBuilder;
7704 use crate::widget_tree::WidgetTree;
7705
7706 fn contact_id() -> PointerId {
7712 use std::sync::atomic::{AtomicU64, Ordering};
7713 static NEXT: AtomicU64 = AtomicU64::new(1);
7714 PointerIdAllocator::global().begin(
7715 BackendDeviceKey::new(0x0B11),
7716 NEXT.fetch_add(1, Ordering::Relaxed),
7717 )
7718 }
7719
7720 fn contact(id: PointerId, phase: PointerPhase, at: Point, t: EventTime) -> PointerSample {
7721 PointerSample {
7722 pointer: PointerInfo::touch(id, t),
7723 phase,
7724 position: at,
7725 button: None,
7726 modifiers: Modifiers::NONE,
7727 coalesced: Vec::new(),
7728 }
7729 }
7730
7731 fn tree_on_a_clock() -> (WidgetTree, Rc<ManualClock>) {
7734 let mut tree = WidgetTree::new();
7735 let clock = Rc::new(ManualClock::new(EventTime::ZERO));
7736 tree.set_input_clock(clock.clone());
7737 (tree, clock)
7738 }
7739
7740 fn tappable(tree: &mut WidgetTree) -> (WidgetId, crate::signal::Signal<bool>) {
7747 let id = tree.add(FillWidget::new().focusable().on_tap(|_e, _c| {}));
7748 let signal = tree.pressed_signal(id);
7749 (id, signal)
7750 }
7751
7752 #[test]
7759 fn a_pointer_origin_names_its_device() {
7760 use teksilo_tokens::{PenKind, PointerKind};
7761
7762 assert!(FocusOrigin::Pointer(PointerKind::Touch).is_pointer());
7763 assert!(FocusOrigin::Pointer(PointerKind::Pen(PenKind::Pen)).is_pointer());
7764 assert!(FocusOrigin::POINTER.is_pointer());
7765 assert!(!FocusOrigin::Keyboard.is_pointer());
7766 assert!(!FocusOrigin::Programmatic.is_pointer());
7767 assert!(!FocusOrigin::Accessibility.is_pointer());
7768
7769 assert_eq!(
7770 FocusOrigin::Pointer(PointerKind::Touch).pointer_kind(),
7771 Some(PointerKind::Touch),
7772 );
7773 assert_eq!(FocusOrigin::Keyboard.pointer_kind(), None);
7774 assert_eq!(
7775 FocusOrigin::POINTER.pointer_kind(),
7776 Some(PointerKind::Unknown),
7777 "a widget deriving its own origin says so rather than naming a device it never saw",
7778 );
7779 }
7780
7781 #[test]
7785 fn only_a_real_navigation_declares_a_modality() {
7786 use teksilo_tokens::PointerKind;
7787
7788 assert_eq!(FocusOrigin::Keyboard.focus_visible(), Some(true));
7789 assert_eq!(FocusOrigin::Accessibility.focus_visible(), Some(true));
7790 assert_eq!(
7791 FocusOrigin::Pointer(PointerKind::Touch).focus_visible(),
7792 Some(false),
7793 );
7794 assert_eq!(FocusOrigin::Programmatic.focus_visible(), None);
7795 }
7796
7797 #[test]
7800 fn the_tier_three_configs_are_unchanged() {
7801 let origin: crate::signal::Signal<Option<FocusOrigin>> =
7802 crate::signal::Signal::new(Some(FocusOrigin::Keyboard));
7803 let slider: crate::signal::Signal<Option<FocusOrigin>> = origin.clone();
7804 let splitter: crate::signal::Signal<Option<FocusOrigin>> = origin.clone();
7805 let segmented: crate::signal::Signal<Option<FocusOrigin>> = origin.clone();
7806 for field in [slider, splitter, segmented] {
7807 assert_eq!(field.get(), Some(FocusOrigin::Keyboard));
7808 }
7809 origin.set(Some(FocusOrigin::POINTER));
7810 assert_ne!(
7811 origin.get(),
7812 Some(FocusOrigin::Keyboard),
7813 "the `== Some(Keyboard)` test every consumer makes still discriminates",
7814 );
7815 }
7816
7817 #[test]
7828 fn a_touch_tap_after_keyboard_focus_leaves_no_ring() {
7829 let (mut tree, _clock) = tree_on_a_clock();
7830 let a = tree.add(FillWidget::new().focusable());
7831 let b = tree.add(FillWidget::new().focusable());
7832 let root = tree.add(SideBySide {
7833 children: vec![a, b],
7834 });
7835 tree.layout(SizeProposal::exact(200.0, 100.0));
7836 let _ = root;
7837
7838 let visible = tree.focus_visible_signal();
7839 tree.press_key(Key::Tab, Modifiers::NONE);
7840 assert_eq!(tree.focused(), Some(a));
7841 assert!(visible.get(), "keyboard navigation reveals the ring");
7842
7843 let id = contact_id();
7844 let at = tree.bounds(b).center();
7845 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
7846 tree.dispatch_pointer(contact(
7847 id,
7848 PointerPhase::Up,
7849 at,
7850 EventTime::from_millis(30),
7851 ));
7852
7853 assert_eq!(tree.focused(), Some(b), "the release moved focus");
7854 assert!(!visible.get(), "and the ring did not come with it");
7855 assert_eq!(
7856 tree.focus_origin(),
7857 Some(FocusOrigin::Pointer(teksilo_tokens::PointerKind::Touch)),
7858 );
7859 }
7860
7861 #[test]
7866 fn an_assistive_focus_reveals_the_ring() {
7867 let mut tree = WidgetTree::new();
7868 let a = tree.add(FillWidget::new().focusable());
7869 let b = tree.add(FillWidget::new().focusable());
7870 let root = tree.add(SideBySide {
7871 children: vec![a, b],
7872 });
7873 tree.layout(SizeProposal::exact(200.0, 50.0));
7874 let _ = root;
7875 let visible = tree.focus_visible_signal();
7876
7877 tree.click(a);
7878 assert_eq!(tree.focused(), Some(a));
7879 assert!(!visible.get(), "the click hid it");
7880
7881 tree.dispatch_event(WidgetEvent::AccessAction {
7882 target: Some(b),
7883 action: accesskit::Action::Focus,
7884 target_node: crate::accessibility::widget_id_to_node_id(b),
7885 data: None,
7886 });
7887 assert_eq!(tree.focused(), Some(b));
7888 assert!(visible.get(), "an assistive move is a navigation");
7889 assert_eq!(tree.focus_origin(), Some(FocusOrigin::Accessibility));
7890 }
7891
7892 #[test]
7897 fn a_programmatic_focus_leaves_the_modality_alone() {
7898 let mut tree = WidgetTree::new();
7899 let a = tree.add(FillWidget::new().focusable());
7900 let b = tree.add(FillWidget::new().focusable());
7901 let root = tree.add(SideBySide {
7902 children: vec![a, b],
7903 });
7904 tree.layout(SizeProposal::exact(200.0, 100.0));
7905 let _ = root;
7906 let visible = tree.focus_visible_signal();
7907
7908 tree.press_key(Key::Tab, Modifiers::NONE);
7909 assert_eq!(tree.focused(), Some(a));
7910 assert!(visible.get());
7911 tree.focus(b);
7912 assert!(
7913 visible.get(),
7914 "a scripted focus does not hide a keyboard ring"
7915 );
7916
7917 tree.click(a);
7918 assert!(!visible.get());
7919 tree.focus(b);
7920 assert!(
7921 !visible.get(),
7922 "and does not reveal one after a click either",
7923 );
7924 }
7925
7926 #[test]
7932 fn a_mouse_still_focuses_on_press() {
7933 let mut tree = WidgetTree::new();
7934 let w = tree.add(FillWidget::new().focusable());
7935 tree.layout(SizeProposal::exact(100.0, 50.0));
7936 let center = tree.bounds(w).center();
7937
7938 tree.dispatch_event(WidgetEvent::pointer_down(
7939 center,
7940 PointerButton::Primary,
7941 Modifiers::NONE,
7942 ));
7943 assert_eq!(
7944 tree.focused(),
7945 Some(w),
7946 "focus lands on the press for an indirect pointer",
7947 );
7948 }
7949
7950 #[test]
7952 fn a_finger_focuses_on_release() {
7953 let (mut tree, _clock) = tree_on_a_clock();
7954 let w = tree.add(FillWidget::new().focusable());
7955 tree.layout(SizeProposal::exact(100.0, 50.0));
7956 let center = tree.bounds(w).center();
7957
7958 let id = contact_id();
7959 tree.dispatch_pointer(contact(id, PointerPhase::Down, center, EventTime::ZERO));
7960 assert_eq!(
7961 tree.focused(),
7962 None,
7963 "a finger that has only landed has chosen nothing yet",
7964 );
7965
7966 tree.dispatch_pointer(contact(
7967 id,
7968 PointerPhase::Up,
7969 center,
7970 EventTime::from_millis(40),
7971 ));
7972 assert_eq!(tree.focused(), Some(w));
7973 assert_eq!(
7974 tree.focus_origin().and_then(FocusOrigin::pointer_kind),
7975 Some(teksilo_tokens::PointerKind::Touch),
7976 "the origin names the device that delivered it",
7977 );
7978 }
7979
7980 #[test]
7984 fn a_release_on_a_different_focusable_moves_no_focus() {
7985 let (mut tree, _clock) = tree_on_a_clock();
7986 let a = tree.add(FillWidget::new().focusable());
7987 let b = tree.add(FillWidget::new().focusable());
7988 let root = tree.add(SideBySide {
7989 children: vec![a, b],
7990 });
7991 tree.layout(SizeProposal::exact(200.0, 50.0));
7992 let _ = root;
7993
7994 let on_a = tree.bounds(a).center();
7995 let on_b = tree.bounds(b).center();
7996 let id = contact_id();
7997 tree.dispatch_pointer(contact(id, PointerPhase::Down, on_a, EventTime::ZERO));
7998 tree.dispatch_pointer(contact(
7999 id,
8000 PointerPhase::Move,
8001 on_b,
8002 EventTime::from_millis(20),
8003 ));
8004 tree.dispatch_pointer(contact(
8005 id,
8006 PointerPhase::Up,
8007 on_b,
8008 EventTime::from_millis(40),
8009 ));
8010
8011 assert_eq!(
8012 tree.focused(),
8013 None,
8014 "the guard is `the release landed on the same focusable as the press`",
8015 );
8016 }
8017
8018 #[test]
8025 fn a_mouse_press_visual_is_unchanged() {
8026 let mut tree = WidgetTree::new();
8027 let (w, pressed) = tappable(&mut tree);
8028 tree.layout(SizeProposal::exact(100.0, 50.0));
8029 let center = tree.bounds(w).center();
8030
8031 assert!(!pressed.get(), "not pressed at rest");
8032 tree.dispatch_event(WidgetEvent::pointer_down(
8033 center,
8034 PointerButton::Primary,
8035 Modifiers::NONE,
8036 ));
8037 assert!(pressed.get(), "an indirect pointer lights up on the press");
8038 assert!(
8039 !tree.press_pending(w),
8040 "and never waits: a mouse opens no pan session, so there is no \
8041 ambiguity to wait out",
8042 );
8043 assert_eq!(tree.pressed_by(w), Some(PointerId::MOUSE));
8044
8045 tree.dispatch_event(WidgetEvent::pointer_up(
8046 center,
8047 PointerButton::Primary,
8048 Modifiers::NONE,
8049 ));
8050 assert!(!pressed.get(), "the release clears it");
8051 assert_eq!(tree.pressed_by(w), None);
8052 }
8053
8054 #[test]
8058 fn a_slide_off_clears_the_visual_and_re_entry_restores_it() {
8059 let (mut tree, _clock) = tree_on_a_clock();
8060 let (w, pressed) = tappable(&mut tree);
8061 tree.layout(SizeProposal::exact(100.0, 200.0));
8062 let inside = tree.bounds(w).center();
8063 let outside = Point::new(inside.x, inside.y + 400.0);
8064
8065 let id = contact_id();
8066 tree.dispatch_pointer(contact(id, PointerPhase::Down, inside, EventTime::ZERO));
8067 assert!(
8068 pressed.get(),
8069 "nothing here claims a pan, so no delay applies"
8070 );
8071
8072 tree.dispatch_pointer(contact(
8073 id,
8074 PointerPhase::Move,
8075 outside,
8076 EventTime::from_millis(20),
8077 ));
8078 assert!(!pressed.get(), "the press has left its target");
8079 assert!(!tree.press_is_inside(w));
8080 assert_eq!(
8081 tree.pressed_by(w),
8082 Some(id),
8083 "the contact still holds the press — it is the *visual* that is off",
8084 );
8085
8086 tree.dispatch_pointer(contact(
8087 id,
8088 PointerPhase::Move,
8089 inside,
8090 EventTime::from_millis(40),
8091 ));
8092 assert!(pressed.get(), "sliding back on restores it");
8093
8094 tree.dispatch_pointer(contact(
8095 id,
8096 PointerPhase::Up,
8097 inside,
8098 EventTime::from_millis(60),
8099 ));
8100 assert!(!pressed.get());
8101 }
8102
8103 #[test]
8106 fn a_second_contact_cannot_clear_the_first_visual() {
8107 let (mut tree, _clock) = tree_on_a_clock();
8108 let (w, pressed) = tappable(&mut tree);
8109 tree.layout(SizeProposal::exact(100.0, 50.0));
8110 let bounds = tree.bounds(w);
8111 let first_at = Point::new(bounds.x + 20.0, bounds.y + 25.0);
8112 let second_at = Point::new(bounds.x + 70.0, bounds.y + 25.0);
8113
8114 let first = contact_id();
8115 let second = contact_id();
8116 tree.dispatch_pointer(contact(
8117 first,
8118 PointerPhase::Down,
8119 first_at,
8120 EventTime::ZERO,
8121 ));
8122 assert!(pressed.get());
8123 assert_eq!(tree.pressed_by(w), Some(first));
8124
8125 tree.dispatch_pointer(contact(
8126 second,
8127 PointerPhase::Down,
8128 second_at,
8129 EventTime::from_millis(10),
8130 ));
8131 assert_eq!(
8132 tree.pressed_by(w),
8133 Some(first),
8134 "under `MultiContact::First` the second contact is terminated before \
8135 it reaches the arena at all",
8136 );
8137
8138 tree.dispatch_pointer(contact(
8139 second,
8140 PointerPhase::Up,
8141 second_at,
8142 EventTime::from_millis(20),
8143 ));
8144 assert!(
8145 pressed.get(),
8146 "so the second contact's release cannot clear a visual it never owned",
8147 );
8148 assert_eq!(tree.pressed_by(w), Some(first));
8149
8150 tree.dispatch_pointer(contact(
8151 first,
8152 PointerPhase::Up,
8153 first_at,
8154 EventTime::from_millis(30),
8155 ));
8156 assert!(!pressed.get(), "the owner's release does clear it");
8157 }
8158
8159 #[test]
8162 fn a_cancel_clears_the_visual() {
8163 let (mut tree, _clock) = tree_on_a_clock();
8164 let (w, pressed) = tappable(&mut tree);
8165 tree.layout(SizeProposal::exact(100.0, 50.0));
8166 let center = tree.bounds(w).center();
8167
8168 let id = contact_id();
8169 tree.dispatch_pointer(contact(id, PointerPhase::Down, center, EventTime::ZERO));
8170 assert!(pressed.get());
8171
8172 tree.dispatch_pointer(contact(
8173 id,
8174 PointerPhase::Cancel,
8175 center,
8176 EventTime::from_millis(20),
8177 ));
8178 assert!(!pressed.get(), "a press that was taken away is not painted");
8179 assert_eq!(tree.pressed_by(w), None);
8180 }
8181
8182 #[test]
8185 fn a_pan_claim_clears_the_visual() {
8186 let (mut tree, clock) = tree_on_a_clock();
8187 let (row, pressed) = tappable(&mut tree);
8188 let list = tree.add(
8189 StackWidget::new()
8190 .child(row)
8191 .pan_claim(PanClaim::vertical())
8192 .on_scroll(|_e, _c| EventResponse::Handled),
8193 );
8194 tree.layout(SizeProposal::exact(200.0, 400.0));
8195 let _ = list;
8196 let start = Point::new(100.0, 200.0);
8197
8198 let id = contact_id();
8199 tree.dispatch_pointer(contact(id, PointerPhase::Down, start, EventTime::ZERO));
8200 assert!(tree.press_pending(row));
8202 clock.set(EventTime::from_millis(100));
8203 tree.tick_gestures(std::time::Instant::now());
8204 assert!(pressed.get(), "…and appears once the delay has elapsed");
8205
8206 tree.dispatch_pointer(contact(
8208 id,
8209 PointerPhase::Move,
8210 Point::new(100.0, 260.0),
8211 EventTime::from_millis(120),
8212 ));
8213 assert!(
8214 !pressed.get(),
8215 "the row lost the press to the list and must stop advertising it",
8216 );
8217 assert_eq!(tree.pressed_by(row), None);
8218 }
8219
8220 #[test]
8229 fn a_button_the_control_cannot_act_on_raises_no_visual() {
8230 let taps = Rc::new(Cell::new(0u32));
8231 let count = taps.clone();
8232 let mut tree = WidgetTree::new();
8233 let w = tree.add(FillWidget::new().focusable().on_tap(move |_e, _c| {
8234 count.set(count.get() + 1);
8235 }));
8236 let pressed = tree.pressed_signal(w);
8237 tree.layout(SizeProposal::exact(100.0, 50.0));
8238 let center = tree.bounds(w).center();
8239
8240 for button in [
8241 PointerButton::Middle,
8242 PointerButton::Back,
8243 PointerButton::Forward,
8244 ] {
8245 tree.dispatch_event(WidgetEvent::pointer_down(center, button, Modifiers::NONE));
8246 assert!(
8247 !pressed.get(),
8248 "{button:?} cannot activate an `on_tap` node, so it must not light one up",
8249 );
8250 assert_eq!(tree.pressed_by(w), None, "and owns no visual to clear");
8251 tree.dispatch_event(WidgetEvent::pointer_up(center, button, Modifiers::NONE));
8252 assert_eq!(
8253 taps.get(),
8254 0,
8255 "the tap recognizer refuses {button:?} too — that is the point",
8256 );
8257 }
8258
8259 tree.dispatch_event(WidgetEvent::pointer_down(
8261 center,
8262 PointerButton::Primary,
8263 Modifiers::NONE,
8264 ));
8265 assert!(pressed.get(), "a primary press lights up as it always has");
8266 assert_eq!(tree.pressed_by(w), Some(PointerId::MOUSE));
8267 tree.dispatch_event(WidgetEvent::pointer_up(
8268 center,
8269 PointerButton::Primary,
8270 Modifiers::NONE,
8271 ));
8272 assert!(!pressed.get());
8273 assert_eq!(taps.get(), 1);
8274 }
8275
8276 #[test]
8280 fn a_secondary_press_with_no_context_menu_raises_no_visual() {
8281 let mut tree = WidgetTree::new();
8282 let (w, pressed) = tappable(&mut tree);
8283 tree.layout(SizeProposal::exact(100.0, 50.0));
8284 let center = tree.bounds(w).center();
8285
8286 tree.dispatch_event(WidgetEvent::pointer_down(
8287 center,
8288 PointerButton::Secondary,
8289 Modifiers::NONE,
8290 ));
8291 assert!(
8292 !pressed.get(),
8293 "nothing opened, and nothing may look pressed either",
8294 );
8295 assert_eq!(tree.pressed_by(w), None);
8296 assert!(!tree.press_is_inside(w));
8297 }
8298
8299 #[test]
8303 fn a_widened_mask_widens_the_visual_with_it() {
8304 use crate::event::ButtonMask;
8305
8306 let mut tree = WidgetTree::new();
8307 let w = tree.add(
8308 FillWidget::new()
8309 .on_tap(|_e, _c| {})
8310 .accept_tap_buttons(ButtonMask::PRIMARY | ButtonMask::MIDDLE),
8311 );
8312 let pressed = tree.pressed_signal(w);
8313 tree.layout(SizeProposal::exact(100.0, 50.0));
8314 let center = tree.bounds(w).center();
8315
8316 tree.dispatch_event(WidgetEvent::pointer_down(
8317 center,
8318 PointerButton::Middle,
8319 Modifiers::NONE,
8320 ));
8321 assert!(pressed.get(), "this node really does act on a middle-click");
8322
8323 tree.dispatch_event(WidgetEvent::pointer_up(
8324 center,
8325 PointerButton::Middle,
8326 Modifiers::NONE,
8327 ));
8328 assert!(!pressed.get());
8329 }
8330
8331 #[test]
8337 fn a_press_that_raises_no_visual_still_defers_its_focus() {
8338 use teksilo_tokens::{PenKind, PointerKind};
8339
8340 let (mut tree, _clock) = tree_on_a_clock();
8341 let (w, pressed) = tappable(&mut tree);
8342 tree.layout(SizeProposal::exact(100.0, 50.0));
8343 let center = tree.bounds(w).center();
8344
8345 let id = contact_id();
8346 let barrel = |phase, t| PointerSample {
8347 pointer: PointerInfo {
8348 kind: PointerKind::Pen(PenKind::Pen),
8349 ..PointerInfo::touch(id, t)
8350 },
8351 phase,
8352 position: center,
8353 button: Some(PointerButton::Secondary),
8354 modifiers: Modifiers::NONE,
8355 coalesced: Vec::new(),
8356 };
8357
8358 tree.dispatch_pointer(barrel(PointerPhase::Down, EventTime::ZERO));
8359 assert!(!pressed.get(), "the barrel button activates nothing here");
8360 assert_eq!(tree.pressed_by(w), None);
8361 assert_eq!(
8362 tree.focused(),
8363 None,
8364 "a direct pointer has chosen nothing until it lifts",
8365 );
8366
8367 tree.dispatch_pointer(barrel(PointerPhase::Up, EventTime::from_millis(40)));
8368 assert_eq!(
8369 tree.focused(),
8370 Some(w),
8371 "the deferral is not button-gated: the release landed where the press did",
8372 );
8373 assert_eq!(
8374 tree.focus_origin().and_then(FocusOrigin::pointer_kind),
8375 Some(PointerKind::Pen(PenKind::Pen)),
8376 );
8377 }
8378
8379 #[test]
8386 fn the_feedback_delay_applies_only_inside_a_claimant() {
8387 let (mut tree, _clock) = tree_on_a_clock();
8389 let (w, pressed) = tappable(&mut tree);
8390 tree.layout(SizeProposal::exact(200.0, 400.0));
8391 let id = contact_id();
8392 let at = tree.bounds(w).center();
8393 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8394 assert!(
8395 !tree.press_pending(w),
8396 "no claimant above it, so nothing to rule out",
8397 );
8398 assert!(pressed.get());
8399
8400 let (mut tree, clock) = tree_on_a_clock();
8402 let (row, pressed) = tappable(&mut tree);
8403 let list = tree.add(
8404 StackWidget::new()
8405 .child(row)
8406 .pan_claim(PanClaim::vertical())
8407 .on_scroll(|_e, _c| EventResponse::Handled),
8408 );
8409 tree.layout(SizeProposal::exact(200.0, 400.0));
8410 let _ = list;
8411 let id = contact_id();
8412 let at = Point::new(100.0, 200.0);
8413 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8414 assert!(tree.press_pending(row), "a finger might be about to scroll");
8415 assert!(!pressed.get(), "so the row does not flash");
8416 assert!(
8417 tree.press_is_inside(row),
8418 "the press is real; only its visual is being withheld",
8419 );
8420
8421 clock.set(EventTime::from_millis(99));
8422 tree.tick_gestures(std::time::Instant::now());
8423 assert!(!pressed.get(), "99 ms is under the 100 ms delay");
8424
8425 clock.set(EventTime::from_millis(100));
8426 tree.tick_gestures(std::time::Instant::now());
8427 assert!(pressed.get(), "and 100 ms is it");
8428 assert!(!tree.press_pending(row));
8429 }
8430
8431 #[test]
8434 fn a_mouse_inside_a_claimant_never_waits() {
8435 let mut tree = WidgetTree::new();
8436 let (row, pressed) = tappable(&mut tree);
8437 let list = tree.add(
8438 StackWidget::new()
8439 .child(row)
8440 .pan_claim(PanClaim::vertical())
8441 .on_scroll(|_e, _c| EventResponse::Handled),
8442 );
8443 tree.layout(SizeProposal::exact(200.0, 400.0));
8444 let _ = list;
8445
8446 tree.dispatch_event(WidgetEvent::pointer_down(
8447 Point::new(100.0, 200.0),
8448 PointerButton::Primary,
8449 Modifiers::NONE,
8450 ));
8451 assert!(!tree.press_pending(row));
8452 assert!(pressed.get(), "Compact with a mouse is exactly as it was");
8453 }
8454
8455 #[test]
8462 fn a_handler_can_read_the_press_it_is_inside() {
8463 let (mut tree, _clock) = tree_on_a_clock();
8464 let seen: Rc<RefCell<Vec<(bool, bool, bool)>>> = Rc::new(RefCell::new(Vec::new()));
8465 let log = seen.clone();
8466 let row = tree.add(FillWidget::new().on_tap(|_e, _c| {}).on_pointer_event(
8467 move |event, ctx| {
8468 if matches!(event, WidgetEvent::PointerUp { .. }) {
8469 log.borrow_mut().push((
8470 ctx.is_pressed(),
8471 ctx.press_is_inside(),
8472 ctx.press_pending(),
8473 ));
8474 }
8475 EventResponse::Ignored
8476 },
8477 ));
8478 let list = tree.add(
8479 StackWidget::new()
8480 .child(row)
8481 .pan_claim(PanClaim::vertical())
8482 .on_scroll(|_e, _c| EventResponse::Handled),
8483 );
8484 tree.layout(SizeProposal::exact(200.0, 400.0));
8485 let _ = list;
8486
8487 let id = contact_id();
8488 let at = Point::new(100.0, 200.0);
8489 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8490 tree.dispatch_pointer(contact(
8491 id,
8492 PointerPhase::Up,
8493 at,
8494 EventTime::from_millis(30),
8495 ));
8496
8497 assert_eq!(
8498 seen.borrow().as_slice(),
8499 &[(false, true, true)],
8500 "released inside the claimant before the delay elapsed: inside, \
8501 pending, and therefore not yet showing",
8502 );
8503 }
8504
8505 #[test]
8508 fn a_handler_outside_a_press_reads_nothing() {
8509 let mut tree = WidgetTree::new();
8510 let asked = Rc::new(Cell::new(false));
8511 let flag = asked.clone();
8512 let w = tree.add(FillWidget::new().focusable().on_key(move |_e, ctx| {
8513 flag.set(ctx.is_pressed() || ctx.press_is_inside() || ctx.press_pending());
8514 EventResponse::Ignored
8515 }));
8516 tree.layout(SizeProposal::exact(100.0, 50.0));
8517 tree.focus(w);
8518 tree.press_key(Key::ArrowDown, Modifiers::NONE);
8519 assert!(!asked.get());
8520 }
8521
8522 #[test]
8526 fn a_completed_press_leaves_the_detector_clean() {
8527 let (mut tree, _clock) = tree_on_a_clock();
8528 let (w, pressed) = tappable(&mut tree);
8529 tree.layout(SizeProposal::exact(100.0, 50.0));
8530 let at = tree.bounds(w).center();
8531
8532 let id = contact_id();
8533 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8534 assert!(pressed.get());
8535 tree.dispatch_pointer(contact(
8536 id,
8537 PointerPhase::Up,
8538 at,
8539 EventTime::from_millis(30),
8540 ));
8541 tree.assert_no_leaked_pointer_state();
8542 }
8543
8544 #[test]
8548 fn the_detector_reports_a_press_still_held() {
8549 let (mut tree, _clock) = tree_on_a_clock();
8550 let (w, _pressed) = tappable(&mut tree);
8551 tree.layout(SizeProposal::exact(100.0, 50.0));
8552 let at = tree.bounds(w).center();
8553
8554 let id = contact_id();
8555 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8556 let held = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
8557 tree.assert_no_leaked_pointer_state()
8558 }));
8559 let message = *held
8560 .expect_err("a live press is leaked state")
8561 .downcast::<String>()
8562 .expect("the detector panics with a message");
8563 assert!(
8564 message.contains("is still pressed by"),
8565 "the press is named in the report, got: {message}",
8566 );
8567 }
8568
8569 #[derive(Debug)]
8573 struct SideBySide {
8574 children: Vec<WidgetId>,
8575 }
8576
8577 impl crate::widget::Widget for SideBySide {
8578 fn layout_response(
8579 &self,
8580 proposal: SizeProposal,
8581 _ctx: &crate::widget::LayoutContext,
8582 ) -> crate::widget::LayoutResponse {
8583 proposal.resolve(0.0, 0.0).into()
8584 }
8585
8586 fn place_children(
8587 &self,
8588 bounds: teksilo_canvas::Rect,
8589 _proposal: SizeProposal,
8590 children: &mut [crate::widget::WidgetPlacement],
8591 _ctx: &crate::widget::LayoutContext,
8592 ) {
8593 let n = children.len().max(1) as f32;
8594 let w = bounds.width / n;
8595 for (i, child) in children.iter_mut().enumerate() {
8596 child.origin = Point::new(bounds.x + w * i as f32, bounds.y);
8597 child.size = teksilo_canvas::Size::new(w, bounds.height);
8598 }
8599 }
8600
8601 fn children(&self) -> Vec<WidgetId> {
8602 self.children.clone()
8603 }
8604 }
8605}
8606
8607#[cfg(test)]
8608mod overlay_release_dismissal_tests {
8609 use std::cell::Cell;
8619 use std::rc::Rc;
8620
8621 use teksilo_canvas::{Point, Rect, Size, SizeProposal};
8622
8623 use crate::WidgetId;
8624 use crate::event::{Modifiers, PointerButton, WidgetEvent};
8625 use crate::overlay::{DismissBehavior, OverlayPlacement, OverlayRequest};
8626 use crate::pointer::{
8627 BackendDeviceKey, EventTime, PointerId, PointerIdAllocator, PointerInfo, PointerPhase,
8628 PointerSample,
8629 };
8630 use crate::test_widgets::FillWidget;
8631 use crate::widget::{LayoutContext, LayoutResponse, Widget};
8632 use crate::widget_builder::WidgetBuilder;
8633 use crate::widget_tree::WidgetTree;
8634
8635 #[derive(Debug)]
8642 struct Panel(Size);
8643
8644 impl Widget for Panel {
8645 fn layout_response(&self, _proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
8646 self.0.into()
8647 }
8648 }
8649
8650 #[derive(Debug)]
8658 struct PageAndTrigger {
8659 children: Vec<WidgetId>,
8660 }
8661
8662 impl Widget for PageAndTrigger {
8663 fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
8664 proposal.resolve(0.0, 0.0).into()
8665 }
8666
8667 fn place_children(
8668 &self,
8669 bounds: Rect,
8670 _proposal: SizeProposal,
8671 children: &mut [crate::widget::WidgetPlacement],
8672 _ctx: &LayoutContext,
8673 ) {
8674 if let Some(page) = children.get_mut(0) {
8675 page.origin = bounds.origin();
8676 page.size = bounds.size();
8677 }
8678 if let Some(trigger) = children.get_mut(1) {
8679 trigger.origin = bounds.origin();
8680 trigger.size = Size::new(10.0, 10.0);
8681 }
8682 }
8683
8684 fn children(&self) -> Vec<WidgetId> {
8685 self.children.clone()
8686 }
8687 }
8688
8689 fn contact_id(n: u64) -> PointerId {
8690 PointerIdAllocator::global().begin(BackendDeviceKey::new(0x0FA2), n)
8691 }
8692
8693 fn touch(id: PointerId, phase: PointerPhase, at: Point) -> PointerSample {
8694 PointerSample {
8695 pointer: PointerInfo::touch(id, EventTime::ZERO),
8696 phase,
8697 position: at,
8698 button: None,
8699 modifiers: Modifiers::NONE,
8700 coalesced: Vec::new(),
8701 }
8702 }
8703
8704 fn page_with_a_menu() -> (WidgetTree, WidgetId, Rc<Cell<u32>>) {
8711 let mut tree = WidgetTree::new();
8712 let taps = Rc::new(Cell::new(0u32));
8713 let counter = taps.clone();
8714 let page = tree.add(
8715 FillWidget::new()
8716 .focusable()
8717 .on_tap(move |_e, _c| counter.set(counter.get() + 1)),
8718 );
8719 let trigger = tree.add(Panel(Size::new(10.0, 10.0)));
8722 let _root = tree.add(PageAndTrigger {
8723 children: vec![page, trigger],
8724 });
8725 let menu = tree.add(Panel(Size::new(200.0, 200.0)));
8726 tree.show_overlay(OverlayRequest::new(
8727 menu,
8728 trigger,
8729 OverlayPlacement::AtPointer(Point::new(100.0, 100.0)),
8730 DismissBehavior::ClickOutside,
8731 ));
8732 tree.layout(SizeProposal::exact(800.0, 600.0));
8733 assert_eq!(tree.active_overlays().len(), 1, "the menu is open");
8734 (tree, page, taps)
8735 }
8736
8737 fn outside() -> Point {
8738 Point::new(600.0, 500.0)
8739 }
8740
8741 fn inside_menu() -> Point {
8742 Point::new(150.0, 150.0)
8743 }
8744
8745 #[test]
8752 fn the_suppressed_down_leaves_nothing_pressed_or_captured_beneath() {
8753 let (mut tree, page, taps) = page_with_a_menu();
8754 let finger = contact_id(1);
8755 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8756
8757 assert_eq!(
8758 tree.active_overlays().len(),
8759 1,
8760 "the press does not close the menu; the release does"
8761 );
8762 assert_eq!(tree.pressed_by(page), None, "nothing beneath is pressed");
8763 let entry = tree.pointers.get(finger).expect("the contact is live");
8764 assert_eq!(entry.captured_by, None, "nothing beneath captured it");
8765 assert!(entry.sequence.is_none(), "no arbitration was opened");
8766 assert_eq!(taps.get(), 0);
8767 }
8768
8769 #[test]
8771 fn a_touch_tap_outside_a_menu_closes_it_and_actuates_nothing() {
8772 let (mut tree, page, taps) = page_with_a_menu();
8773 let finger = contact_id(2);
8774 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8775 tree.dispatch_pointer(touch(finger, PointerPhase::Up, outside()));
8776
8777 assert!(tree.active_overlays().is_empty(), "the menu closed");
8778 assert_eq!(taps.get(), 0, "the page beneath was never tapped");
8779 assert_eq!(tree.pressed_by(page), None);
8780 tree.assert_no_leaked_pointer_state();
8781 }
8782
8783 #[test]
8786 fn a_cancelled_press_aborts_the_arm() {
8787 let (mut tree, page, taps) = page_with_a_menu();
8788 let finger = contact_id(3);
8789 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8790 tree.dispatch_pointer(touch(finger, PointerPhase::Cancel, outside()));
8791
8792 assert_eq!(tree.active_overlays().len(), 1, "the menu survives");
8793 assert_eq!(taps.get(), 0);
8794 assert_eq!(tree.pressed_by(page), None);
8795 tree.assert_no_leaked_pointer_state();
8796 }
8797
8798 #[test]
8802 fn a_press_slid_onto_the_menu_dismisses_nothing() {
8803 let (mut tree, _page, taps) = page_with_a_menu();
8804 let finger = contact_id(4);
8805 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8806 tree.dispatch_pointer(touch(finger, PointerPhase::Move, inside_menu()));
8807 tree.dispatch_pointer(touch(finger, PointerPhase::Up, inside_menu()));
8808
8809 assert_eq!(tree.active_overlays().len(), 1, "the menu survives");
8810 assert_eq!(taps.get(), 0);
8811 tree.assert_no_leaked_pointer_state();
8812 }
8813
8814 #[test]
8817 fn a_second_contact_cannot_dismiss_what_the_first_is_manipulating() {
8818 let (mut tree, _page, _taps) = page_with_a_menu();
8819 let first = contact_id(5);
8820 let second = contact_id(6);
8821
8822 tree.dispatch_pointer(touch(first, PointerPhase::Down, inside_menu()));
8823 assert!(
8824 tree.pointers
8825 .get(first)
8826 .is_some_and(|e| e.sequence.is_some()),
8827 "the first contact holds a live press inside the menu"
8828 );
8829
8830 tree.dispatch_pointer(touch(second, PointerPhase::Down, outside()));
8831 tree.dispatch_pointer(touch(second, PointerPhase::Up, outside()));
8832 assert_eq!(
8833 tree.active_overlays().len(),
8834 1,
8835 "the menu the first finger is holding must not close under it"
8836 );
8837
8838 tree.dispatch_pointer(touch(first, PointerPhase::Up, inside_menu()));
8841 let third = contact_id(7);
8842 tree.dispatch_pointer(touch(third, PointerPhase::Down, outside()));
8843 tree.dispatch_pointer(touch(third, PointerPhase::Up, outside()));
8844 assert!(tree.active_overlays().is_empty());
8845 tree.assert_no_leaked_pointer_state();
8846 }
8847
8848 #[test]
8852 fn an_arm_is_not_itself_a_press_that_blocks_another_contact() {
8853 let (mut tree, _page, _taps) = page_with_a_menu();
8854 let first = contact_id(8);
8855 let second = contact_id(9);
8856
8857 tree.dispatch_pointer(touch(first, PointerPhase::Down, outside()));
8858 assert!(
8859 tree.overlay_manager().has_armed_dismiss(first),
8860 "the first contact armed"
8861 );
8862 assert!(tree.busy_press_points(second).is_empty());
8864
8865 tree.dispatch_pointer(touch(second, PointerPhase::Down, outside()));
8866 assert!(tree.overlay_manager().has_armed_dismiss(second));
8867 tree.dispatch_pointer(touch(second, PointerPhase::Up, outside()));
8868 assert!(tree.active_overlays().is_empty());
8869
8870 tree.dispatch_pointer(touch(first, PointerPhase::Up, outside()));
8873 tree.assert_no_leaked_pointer_state();
8874 }
8875
8876 #[test]
8879 fn a_touch_inside_the_menu_arms_nothing() {
8880 let (mut tree, _page, _taps) = page_with_a_menu();
8881 let finger = contact_id(10);
8882 tree.dispatch_pointer(touch(finger, PointerPhase::Down, inside_menu()));
8883 assert!(!tree.overlay_manager().has_armed_dismiss(finger));
8884 tree.dispatch_pointer(touch(finger, PointerPhase::Up, inside_menu()));
8885 assert_eq!(tree.active_overlays().len(), 1);
8886 tree.assert_no_leaked_pointer_state();
8887 }
8888
8889 #[test]
8899 fn a_contact_with_no_press_still_ends_when_the_platform_revokes_it() {
8900 let mut tree = WidgetTree::new();
8901 tree.layout(SizeProposal::exact(800.0, 600.0));
8902 let finger = contact_id(99);
8903 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8904 tree.dispatch_pointer(touch(finger, PointerPhase::Cancel, outside()));
8905 tree.assert_no_leaked_pointer_state();
8906 }
8907
8908 #[test]
8915 fn a_mouse_click_outside_a_menu_dismisses_on_the_press_and_falls_through() {
8916 let (mut tree, _page, taps) = page_with_a_menu();
8917 tree.dispatch_event(WidgetEvent::pointer_down(
8918 outside(),
8919 PointerButton::Primary,
8920 Modifiers::NONE,
8921 ));
8922 assert!(
8923 tree.active_overlays().is_empty(),
8924 "the mouse still dismisses on the press"
8925 );
8926 assert!(
8927 tree.pointers
8928 .get(PointerId::MOUSE)
8929 .is_some_and(|e| e.sequence.is_some()),
8930 "and the press still reaches the page beneath"
8931 );
8932 tree.dispatch_event(WidgetEvent::pointer_up(
8933 outside(),
8934 PointerButton::Primary,
8935 Modifiers::NONE,
8936 ));
8937 assert_eq!(taps.get(), 1, "the control beneath activated");
8938 assert!(
8939 !tree.overlay_manager().has_armed_dismiss(PointerId::MOUSE),
8940 "a mouse never arms"
8941 );
8942 tree.assert_no_leaked_pointer_state();
8943 }
8944
8945 #[test]
8952 fn a_context_menu_avoids_the_contact_that_opened_it() {
8953 fn menu_bounds(pointer: PointerInfo, at: Point) -> Rect {
8954 let mut tree = WidgetTree::new();
8955 let page = tree.add(
8956 FillWidget::new()
8957 .focusable()
8958 .context_menu(|_p, _ctx| Some(Box::new(Panel(Size::new(200.0, 160.0))))),
8959 );
8960 let _ = page;
8961 tree.layout(SizeProposal::exact(800.0, 600.0));
8962 tree.dispatch_pointer(PointerSample {
8963 pointer,
8964 phase: PointerPhase::Down,
8965 position: at,
8966 button: Some(PointerButton::Secondary),
8967 modifiers: Modifiers::NONE,
8968 coalesced: Vec::new(),
8969 });
8970 tree.layout(SizeProposal::exact(800.0, 600.0));
8971 let id = *tree
8972 .active_overlays()
8973 .first()
8974 .expect("the context menu opened");
8975 tree.overlay_manager().bounds_for(id).expect("bounds")
8976 }
8977
8978 let at = Point::new(400.0, 300.0);
8979 let finger = menu_bounds(PointerInfo::touch(contact_id(11), EventTime::ZERO), at);
8980 let contact = crate::overlay::rect_centred_on(at, crate::overlay::ASSUMED_CONTACT_PATCH);
8981 assert!(
8982 finger.x < contact.x && finger.right() <= contact.x,
8983 "a touch menu clears the contact patch: {finger:?} vs {contact:?}"
8984 );
8985
8986 let mouse = menu_bounds(PointerInfo::mouse(EventTime::ZERO), at);
8987 assert_eq!(
8988 (mouse.x, mouse.y),
8989 (at.x, at.y),
8990 "a mouse menu still opens with its corner on the pointer"
8991 );
8992 }
8993}
8994
8995#[cfg(test)]
9011mod pass_through_overlay_tests {
9012 use teksilo_canvas::{Point, Rect, SizeProposal};
9013
9014 use crate::WidgetId;
9015 use crate::overlay::{DismissBehavior, OverlayPlacement, OverlayRequest};
9016 use crate::test_widgets::FillWidget;
9017 use crate::widget::{LayoutContext, LayoutResponse, Widget};
9018 use crate::widget_builder::WidgetBuilder;
9019 use crate::widget_tree::WidgetTree;
9020
9021 #[derive(Debug)]
9024 struct HandleLayer {
9025 handle: WidgetId,
9026 at: Rect,
9027 }
9028
9029 impl Widget for HandleLayer {
9030 fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
9031 proposal.resolve(0.0, 0.0).into()
9032 }
9033
9034 fn place_children(
9035 &self,
9036 _bounds: Rect,
9037 _proposal: SizeProposal,
9038 children: &mut [crate::widget::WidgetPlacement],
9039 _ctx: &LayoutContext,
9040 ) {
9041 if let Some(child) = children.get_mut(0) {
9042 child.origin = self.at.origin();
9043 child.size = self.at.size();
9044 }
9045 }
9046
9047 fn children(&self) -> Vec<WidgetId> {
9048 vec![self.handle]
9049 }
9050 }
9051
9052 const HANDLE: Rect = Rect {
9053 x: 10.0,
9054 y: 10.0,
9055 width: 20.0,
9056 height: 20.0,
9057 };
9058
9059 #[derive(Clone, Copy, PartialEq)]
9061 enum RootFlag {
9062 PassThrough,
9064 HitTransparent,
9067 }
9068
9069 fn field_under_a_layer(flag: RootFlag) -> (WidgetTree, WidgetId, WidgetId) {
9075 let mut tree = WidgetTree::new();
9076 let field = tree.add(FillWidget::new());
9077 let handle = tree.add(FillWidget::new());
9078 let layer = HandleLayer { handle, at: HANDLE };
9079 let layer = match flag {
9080 RootFlag::PassThrough => tree.add(layer.event_pass_through(true)),
9081 RootFlag::HitTransparent => tree.add(layer.hit_transparent(true)),
9082 };
9083 tree.show_overlay(OverlayRequest::new(
9084 layer,
9085 field,
9086 OverlayPlacement::FullViewport,
9087 DismissBehavior::Manual,
9088 ));
9089 tree.layout(SizeProposal::exact(400.0, 200.0));
9090 assert_eq!(tree.active_overlays().len(), 1, "the layer is up");
9091 (tree, field, handle)
9092 }
9093
9094 #[test]
9096 fn a_pass_through_layer_hands_back_what_it_did_not_claim() {
9097 let (tree, field, handle) = field_under_a_layer(RootFlag::PassThrough);
9098
9099 assert_eq!(
9100 tree.hit_test(Point::new(15.0, 15.0)),
9101 Some(handle),
9102 "the layer must still win the point its own child covers"
9103 );
9104 assert_eq!(
9105 tree.hit_test(Point::new(200.0, 100.0)),
9106 Some(field),
9107 "a press the layer did not claim must reach the surface beneath it"
9108 );
9109 }
9110
9111 #[test]
9122 fn a_pass_through_root_still_hears_a_press_that_landed_on_its_child() {
9123 use std::cell::Cell;
9124 use std::rc::Rc;
9125
9126 let seen = Rc::new(Cell::new(0u32));
9127 let counter = seen.clone();
9128 let mut tree = WidgetTree::new();
9129 let field = tree.add(FillWidget::new());
9130 let handle = tree.add(FillWidget::new().on_tap(|_e, _c| {}));
9131 let layer = tree.add(
9132 HandleLayer { handle, at: HANDLE }
9133 .event_pass_through(true)
9134 .on_pointer_event(move |_event, _ctx| {
9135 counter.set(counter.get() + 1);
9136 crate::event::EventResponse::Ignored
9137 }),
9138 );
9139 tree.show_overlay(OverlayRequest::new(
9140 layer,
9141 field,
9142 OverlayPlacement::FullViewport,
9143 DismissBehavior::Manual,
9144 ));
9145 tree.layout(SizeProposal::exact(400.0, 200.0));
9146
9147 tree.pointer_down_button(Point::new(15.0, 15.0), crate::event::PointerButton::Primary);
9148 assert!(
9149 seen.get() > 0,
9150 "a pass-through root heard nothing about a press on its own child"
9151 );
9152
9153 let after_child = seen.get();
9154 tree.pointer_up_button(Point::new(15.0, 15.0), crate::event::PointerButton::Primary);
9155 tree.pointer_down_button(
9158 Point::new(200.0, 100.0),
9159 crate::event::PointerButton::Primary,
9160 );
9161 assert_eq!(
9162 seen.get(),
9163 after_child + 1,
9164 "the press-and-lift on the child accounts for the count, and the \
9165 press on the surface beneath added nothing"
9166 );
9167 }
9168
9169 #[test]
9183 fn a_hit_transparent_root_does_not_get_the_fall_through() {
9184 let (tree, field, _handle) = field_under_a_layer(RootFlag::HitTransparent);
9185
9186 assert_eq!(
9188 tree.hit_test(Point::new(15.0, 15.0)),
9189 None,
9190 "hit_transparent must exclude the subtree, or this fixture is not \
9191 testing the gate"
9192 );
9193 assert_eq!(
9194 tree.hit_test(Point::new(200.0, 100.0)),
9195 None,
9196 "the fall-through reached past an overlay that did not ask for it"
9197 );
9198 let _ = field;
9199 }
9200}