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();
181 let modal = focus.and_then(|f| self.intent_walk(f, &[]).2);
182 for &(id, scope, propagate) in candidates {
183 let score = match scope {
188 ShortcutScope::Global => (crate::shortcut::is_framework_shortcut_id(id) || {
189 let owners = self.shortcut_registry.owners(id);
190 owners.is_empty()
191 || owners
192 .iter()
193 .any(|&owner| self.global_applies(owner, modal))
194 })
195 .then_some(0usize),
196 ShortcutScope::Scoped(scope_id) => focus
197 .and_then(|f| self.scope_distance_from_focus(f, scope_id))
198 .map(|hops| usize::MAX - hops),
199 };
200 let Some(score) = score else { continue };
201 if best.as_ref().is_none_or(|(b, _)| score > *b) {
204 best = Some((score, (id, scope, propagate)));
205 }
206 }
207 best.map(|(_, c)| c)
208 }
209
210 pub fn dispatch_event_with_ops(
224 &mut self,
225 event: WidgetEvent,
226 ops: &mut dyn crate::window::WindowOps,
227 ) {
228 let mut snapshot = crate::pointer::InputSnapshot::from_event(&event);
231 if snapshot.pointer.time == crate::pointer::EventTime::ZERO {
236 snapshot.pointer.time = self.input_now();
237 }
238 self.dispatch_with_input_snapshot(event, snapshot, ops)
239 }
240
241 pub fn dispatch_event(&mut self, event: WidgetEvent) {
247 let mut noop = crate::window::NoopWindowOps;
248 self.dispatch_event_with_ops(event, &mut noop);
249 }
250
251 pub fn dispatch_pointer(&mut self, sample: crate::pointer::PointerSample) {
273 let mut noop = crate::window::NoopWindowOps;
274 self.dispatch_pointer_with_ops(sample, &mut noop);
275 }
276
277 pub fn dispatch_pointer_with_ops(
281 &mut self,
282 sample: crate::pointer::PointerSample,
283 ops: &mut dyn crate::window::WindowOps,
284 ) {
285 use crate::pointer::PointerPhase;
286
287 crate::trace_input!(
288 Samples,
289 "{:?} {:?} at {:?} buttons={:?} t={:?}",
290 sample.phase,
291 sample.pointer.id,
292 sample.position,
293 sample.pointer.buttons,
294 sample.pointer.time
295 );
296
297 let button = sample
301 .button
302 .unwrap_or(crate::event::PointerButton::Primary);
303 let event = match sample.phase {
304 PointerPhase::Down => WidgetEvent::PointerDown {
305 position: sample.position,
306 button,
307 modifiers: sample.modifiers,
308 pointer: sample.pointer,
309 },
310 PointerPhase::Move => WidgetEvent::PointerMove {
311 position: sample.position,
312 modifiers: sample.modifiers,
313 pointer: sample.pointer,
314 },
315 PointerPhase::Up => WidgetEvent::PointerUp {
316 position: sample.position,
317 button,
318 modifiers: sample.modifiers,
319 pointer: sample.pointer,
320 },
321 PointerPhase::Cancel => {
327 self.overlay_manager.abort_dismiss(sample.pointer.id);
332 self.cancel_pointer(
333 sample.pointer.id,
334 crate::pointer::CancelReason::Platform,
335 ops,
336 );
337 if !sample.pointer.kind.hovers() {
344 self.pointers.end(sample.pointer.id);
345 }
346 return;
347 }
348 };
349
350 if !self.pointers.would_admit(&sample.pointer) {
356 return;
357 }
358 let ends_pointer = matches!(sample.phase, PointerPhase::Up | PointerPhase::Cancel)
362 && !sample.pointer.kind.hovers();
363 let pointer_id = sample.pointer.id;
364
365 let snapshot = crate::pointer::InputSnapshot::from_pointer_sample(&sample);
366 self.dispatch_with_input_snapshot(event, snapshot, ops);
367
368 if ends_pointer {
369 self.pointers.end(pointer_id);
370 }
371 }
372
373 pub fn dispatch_scroll(&mut self, sample: crate::pointer::ScrollSample) {
381 let mut noop = crate::window::NoopWindowOps;
382 self.dispatch_scroll_with_ops(sample, &mut noop);
383 }
384
385 pub fn dispatch_scroll_with_ops(
388 &mut self,
389 sample: crate::pointer::ScrollSample,
390 ops: &mut dyn crate::window::WindowOps,
391 ) {
392 crate::trace_input!(
393 Samples,
394 "scroll {:?} {:?}/{:?} at {:?}",
395 sample.delta,
396 sample.phase,
397 sample.source,
398 sample.position
399 );
400
401 let event = WidgetEvent::Scroll {
402 delta: sample.delta,
403 modifiers: sample.modifiers,
404 window_position: sample.position,
405 phase: sample.phase,
406 pointer: sample.pointer,
407 };
408 let snapshot = crate::pointer::InputSnapshot::from_scroll_sample(&sample);
409 self.dispatch_with_input_snapshot(event, snapshot, ops);
410 }
411
412 pub fn pointer_left_window(&mut self, ops: &mut dyn crate::window::WindowOps) {
437 if self
441 .pointers
442 .hover_owner_id()
443 .and_then(|id| self.captured_by(id))
444 .is_some()
445 {
446 return;
447 }
448 self.hover_owner_outside_window = true;
449 self.tooltip_dwell_held = false;
450 let Some(hovered) = self.hovered_id() else {
451 return;
452 };
453 let leave = WidgetEvent::PointerLeave {
456 pointer: self.hover_transition_pointer(),
457 };
458 self.dispatch_to_widget(hovered, &leave, &mut *ops);
459 self.tooltip_pointer_leave(hovered, &mut *ops);
460 self.set_hovered(None);
461 self.update_hover_within_signals(Some(hovered), None);
462 }
463
464 pub(super) fn hover_transition_pointer(&self) -> crate::pointer::PointerInfo {
481 self.pointers
482 .hover_owner()
483 .map(|entry| entry.info)
484 .unwrap_or_else(|| crate::pointer::PointerInfo::mouse(self.input_now()))
485 }
486
487 fn dispatch_with_input_snapshot(
494 &mut self,
495 event: WidgetEvent,
496 snapshot: crate::pointer::InputSnapshot,
497 ops: &mut dyn crate::window::WindowOps,
498 ) {
499 self.pending_dispatch.push_back(QueuedDispatch::Event {
509 event,
510 snapshot: Box::new(snapshot),
511 });
512 if self.dispatch_depth > 0 {
513 return;
514 }
515 self.drain_pending_dispatch(&mut *ops);
516 }
517
518 pub(super) fn drain_pending_dispatch(&mut self, ops: &mut dyn crate::window::WindowOps) {
524 while let Some(queued) = self.pending_dispatch.pop_front() {
525 match queued {
526 QueuedDispatch::Event { event, snapshot } => {
527 self.run_one_dispatch(event, *snapshot, &mut *ops);
528 }
529 QueuedDispatch::Cancel {
530 pointer,
531 reason,
532 recipient,
533 } => {
534 self.run_one_cancel(pointer, reason, recipient, &mut *ops);
535 }
536 }
537 }
538 }
539
540 fn run_one_dispatch(
547 &mut self,
548 event: WidgetEvent,
549 snapshot: crate::pointer::InputSnapshot,
550 ops: &mut dyn crate::window::WindowOps,
551 ) {
552 let previous = std::mem::replace(&mut self.current_input, snapshot);
553 self.dispatch_depth += 1;
554 self.dispatch_event_impl(event, ops);
555 self.dispatch_depth -= 1;
556 self.current_input = previous;
557 }
558
559 fn dismiss_top_on_escape(&mut self, ops: &mut dyn crate::window::WindowOps) -> bool {
563 let mark = self.dismissal_mark();
564 let Some((_id, content_ids, focus_restore)) =
565 self.overlay_manager.try_dismiss_top_on_escape()
566 else {
567 return false;
568 };
569 self.dormant_dismissed_content(&content_ids, ops);
570 self.return_focus_after_dismissal(focus_restore, mark, ops);
571 true
572 }
573
574 #[doc(hidden)]
588 pub fn answer_close_request_with_modal(
589 &mut self,
590 ops: &mut dyn crate::window::WindowOps,
591 ) -> bool {
592 if !self.overlay_manager.has_open_modal() {
593 return false;
594 }
595 self.tooltip_escape_pressed();
598 self.inert_overlays_escape_pressed(ops);
599 self.dismiss_top_on_escape(ops);
600 true
601 }
602
603 fn note_key_press(&mut self, target: WidgetId) {
607 let Some((identity, repeat)) = self.current_key else {
608 return;
609 };
610 match self.key_presses.iter_mut().find(|(id, _)| *id == identity) {
611 Some(_) if repeat => {}
612 Some(record) => record.1 = target,
613 None => self.key_presses.push((identity, target)),
614 }
615 }
616
617 fn receives_every_key_release(&self, id: WidgetId) -> bool {
621 self.arena
622 .get(id)
623 .is_some_and(|node| node.receives_every_key_release)
624 }
625
626 fn dispatch_key_release(
637 &mut self,
638 key: Key,
639 event: &WidgetEvent,
640 ops: &mut dyn crate::window::WindowOps,
641 ) {
642 let identity = self.current_key.map(|(id, _)| id);
643 let pressed = identity.and_then(|identity| {
644 let at = self
645 .key_presses
646 .iter()
647 .position(|(id, _)| *id == identity)?;
648 Some(self.key_presses.remove(at).1)
649 });
650 match pressed {
651 Some(target) if Some(target) == self.focused => {
652 self.dispatch_to_widget(target, event, &mut *ops);
653 }
654 Some(target) => {
655 if self.arena.is_active(target)
656 && (!self.is_behind_modal(target)
657 || self.is_keyboard_capture(target)
658 || self.receives_every_key_release(target))
659 {
660 let behind_modal = self.is_behind_modal(target);
666 let mut current = Some(target);
667 while let Some(id) = current {
668 if self.dispatch_to_widget_direct_returning_handled(id, event, &mut *ops)
669 || behind_modal
670 {
671 break;
672 }
673 current = self.arena.parent(id);
674 }
675 }
676 }
677 None if matches!(key, Key::Enter | Key::Space) => {}
678 None => {
679 if let Some(focused) = self.focused {
680 self.dispatch_to_widget(focused, event, &mut *ops);
681 }
682 }
683 }
684 }
685
686 fn dispatch_event_impl(&mut self, event: WidgetEvent, ops: &mut dyn crate::window::WindowOps) {
687 if let Some(position) = pointer_event_position(&event) {
693 let is_down = matches!(event, WidgetEvent::PointerDown { .. });
694 let is_move = matches!(event, WidgetEvent::PointerMove { .. });
695 if !self.admit_current_pointer(position, is_down, is_move) {
696 return;
697 }
698 if self.current_input.pointer.kind.hovers() {
702 self.claim_hover_owner_for_current(&mut *ops);
703 }
704 }
705
706 if let WidgetEvent::KeyDown { key, .. } | WidgetEvent::KeyUp { key, .. } = &event {
709 let noted = self
710 .window_state
711 .as_ref()
712 .and_then(|w| w.take_key_identity());
713 self.current_key = Some(noted.unwrap_or_else(|| (logical_key_identity(*key), false)));
714 } else if self.dispatch_depth <= 1 {
715 self.current_key = None;
717 }
718
719 match &event {
723 WidgetEvent::KeyDown { .. } if !self.focus_visible.get() => {
724 self.focus_visible.set(true);
725 }
726 WidgetEvent::PointerDown { .. } if self.focus_visible.get() => {
727 self.focus_visible.set(false);
728 }
729 _ => {}
730 }
731
732 let overlay_back_key = match self.layout_direction {
738 crate::environment::LayoutDirection::RightToLeft => Key::ArrowRight,
739 crate::environment::LayoutDirection::LeftToRight => Key::ArrowLeft,
740 };
741 if let WidgetEvent::KeyDown { key, .. } = &event
742 && *key == overlay_back_key
743 {
744 let nested_menu_overlays = {
774 let ids: Vec<_> = self
775 .overlay_manager
776 .stack
777 .iter()
778 .filter(|o| o.band == crate::overlay::OverlayBand::Standard && !o.inert)
779 .map(|o| o.id)
780 .collect();
781 ids.into_iter()
782 .filter(|&id| !self.overlay_is_host_surface(id))
783 .count()
784 };
785 let top_id = self
792 .overlay_manager
793 .stack
794 .iter()
795 .rev()
796 .find(|o| !o.inert)
797 .map(|o| o.id);
798 let top_is_back_navigable = top_id.is_some_and(|id| !self.overlay_is_host_surface(id));
799 if nested_menu_overlays > 1
800 && top_is_back_navigable
801 && let Some(top_id) = top_id
802 {
803 let mark = self.dismissal_mark();
804 let (content_ids, focus_restore) =
805 self.overlay_manager.dismiss_with_focus_restore_because(
806 top_id,
807 crate::overlay::DismissReason::Escape,
808 );
809 self.dormant_dismissed_content(&content_ids, &mut *ops);
810 self.return_focus_after_dismissal(focus_restore, mark, &mut *ops);
811 return;
812 }
813 }
814
815 if let WidgetEvent::KeyDown {
821 key: Key::Escape, ..
822 } = &event
823 {
824 self.tooltip_escape_pressed();
825 self.inert_overlays_escape_pressed(&mut *ops);
828 }
829
830 if let WidgetEvent::KeyDown {
831 key: Key::Escape, ..
832 } = &event
833 && !self.overlay_manager.is_empty()
834 && self.dismiss_top_on_escape(&mut *ops)
835 {
836 return;
837 }
838
839 match &event {
844 WidgetEvent::PointerDown {
845 position, button, ..
846 } => {
847 if self.arm_outside_press_dismissal(*position, *button, &mut *ops) {
848 return;
849 }
850 }
851 WidgetEvent::PointerUp { position, .. } => {
852 if self.commit_outside_press_dismissal(*position, &mut *ops) {
853 return;
854 }
855 }
856 WidgetEvent::PointerCancel { pointer, .. } => {
857 self.overlay_manager.abort_dismiss(pointer.id);
861 }
862 _ => {}
863 }
864
865 if let WidgetEvent::KeyDown { key, modifiers, .. } = &event
876 && let Some(callback) = self.take_key_capture()
877 {
878 let keystroke = crate::shortcut::KeyStroke::new(*key, *modifiers);
879 let mut cap_ctx = self.make_event_context(&mut *ops);
880 callback(keystroke, self.shortcut_registry_mut(), &mut cap_ctx);
881 let anchor = self.focused.or_else(|| self.intent_anchor());
884 if let Some(anchor_id) = anchor {
885 self.collect_from_ctx(cap_ctx, anchor_id);
886 self.drain_pending_intents(&mut *ops);
887 }
888 return;
889 }
890
891 let focus_captures_keys = matches!(&event, WidgetEvent::KeyDown { .. })
899 && self.focused.is_some_and(|f| self.is_keyboard_capture(f));
900
901 if !focus_captures_keys && let WidgetEvent::KeyDown { key, modifiers, .. } = &event {
917 let keystroke = crate::shortcut::KeyStroke::new(*key, *modifiers);
918 let candidates: Vec<(&'static str, crate::shortcut::ShortcutScope, bool)> = self
927 .shortcut_registry
928 .matches_by_keystroke(keystroke)
929 .map(|eff| {
930 (
931 eff.shortcut.id,
932 eff.shortcut.scope,
933 eff.shortcut.propagate_when_disabled,
934 )
935 })
936 .collect();
937 let (region_commands, others): (Vec<_>, Vec<_>) = candidates
943 .into_iter()
944 .partition(|(id, _, _)| crate::shortcut::region_direction(id).is_some());
945 let lookup = self.select_shortcut_for_focus(&others).or_else(|| {
946 let source = self.focus_or_modal().or_else(|| self.intent_anchor());
947 let live: Vec<_> = region_commands
948 .into_iter()
949 .filter(|(id, _, _)| {
950 crate::shortcut::region_direction(id).is_some_and(|forward| {
951 self.region_cycle_target_from(source, forward).is_some()
952 || source.is_some_and(|source| self.can_handle_from(source, id))
953 })
954 })
955 .collect();
956 self.select_shortcut_for_focus(&live)
957 });
958 if let Some((id, scope, propagate_when_disabled)) = lookup {
959 let anchor = match scope {
960 crate::shortcut::ShortcutScope::Global => {
966 self.focus_or_modal().or_else(|| self.intent_anchor())
967 }
968 crate::shortcut::ShortcutScope::Scoped(scope_id) => self
969 .focus_or_modal()
970 .filter(|f| self.scope_distance_from_focus(*f, scope_id).is_some()),
971 };
972 if let Some(anchor_id) = anchor {
973 let mut act_ctx = self.make_event_context(&mut *ops);
974 if let Some(intent) =
975 self.shortcut_registry
976 .invoke_on_activate(id, keystroke, &mut act_ctx)
977 {
978 self.collect_from_ctx(act_ctx, anchor_id);
979 self.drain_pending_intents(&mut *ops);
983 let intent = self.sent_here(
987 intent.with_source(crate::telemetry::IntentSource::Shortcut),
988 );
989 let keep = self.keeps_unclaimed(&intent).then(|| intent.clone());
990 let consumed = self.dispatch_intent(
991 anchor_id,
992 intent,
993 propagate_when_disabled,
994 &mut *ops,
995 );
996 self.drain_pending_intents(&mut *ops);
997 if consumed {
998 return;
999 }
1000 if let Some(intent) = keep {
1003 self.keep_unclaimed(intent, &mut *ops);
1004 return;
1005 }
1006 }
1013 }
1014 }
1019 }
1022
1023 if self.outbound_drag_source.is_some()
1029 && let WidgetEvent::KeyDown {
1030 key: Key::Escape, ..
1031 } = &event
1032 {
1033 ops.cancel_os_drag();
1034 }
1039
1040 if self.os_drag_reentered
1054 && matches!(
1055 event,
1056 WidgetEvent::PointerMove { .. } | WidgetEvent::PointerUp { .. }
1057 )
1058 {
1059 let dragging = self.current_pointer_id();
1060 let own = self
1061 .active_drag
1062 .as_ref()
1063 .is_some_and(|d| d.pointer.id == dragging);
1064 if own && matches!(event, WidgetEvent::PointerMove { .. }) {
1065 return;
1066 }
1067 } else if self.active_drag.is_some() {
1068 match &event {
1069 WidgetEvent::PointerMove { position, .. } => {
1070 self.handle_drag_sample(*position, &mut *ops);
1071 return;
1072 }
1073 WidgetEvent::PointerUp { position, .. } => {
1074 self.handle_drag_drop(*position, &mut *ops);
1075 return;
1076 }
1077 WidgetEvent::KeyDown {
1078 key: Key::Escape, ..
1079 } => {
1080 self.cancel_active_drag(&mut *ops);
1081 return;
1082 }
1083 WidgetEvent::Scroll { .. } => {
1084 let target_and_pos = self
1090 .active_drag
1091 .as_ref()
1092 .and_then(|d| d.current_target.map(|t| (t, d.current_position)));
1093 if let Some((target, _pos)) = target_and_pos {
1094 self.dispatch_to_widget(target, &event, &mut *ops);
1095 }
1096 if let Some((_, pos)) = target_and_pos
1097 && self.active_drag.is_some()
1098 {
1099 self.handle_drag_move(pos, &mut *ops);
1100 }
1101 return;
1102 }
1103 _ => {}
1104 }
1105 }
1106
1107 if let WidgetEvent::KeyDown { key, modifiers, .. } = &event
1116 && is_context_menu_chord(*key, *modifiers)
1117 && self.open_context_menu_from_keyboard(&mut *ops)
1118 {
1119 return;
1120 }
1121
1122 match &event {
1123 WidgetEvent::PointerMove { position, .. } => {
1124 self.note_sequence_position(*position);
1128 self.revalidate_sequence(&mut *ops);
1129 self.tick_sequence_timers();
1132 self.note_palm_sample(*position);
1137 self.feed_pinch(super::pan_arbiter::PinchFeed::Move, *position, &mut *ops);
1138 if let Some(captured) = self.current_pointer_capture() {
1139 self.dispatch_to_widget(captured, &event, &mut *ops);
1140 if self.active_drag.is_none() {
1145 self.advance_sequence(&event, &mut *ops);
1146 }
1147 } else {
1148 self.handle_pointer_move(&event, *position, &mut *ops);
1149 if self.active_drag.is_none() {
1154 self.advance_sequence(&event, &mut *ops);
1155 }
1156 }
1157 self.advance_pan(*position, &mut *ops);
1160 self.update_press(*position);
1163 self.touch_route_moved(self.current_pointer_id());
1166 if self.pointers.hover_owner_id() == Some(self.current_pointer_id()) {
1176 self.update_pointer_leave_overlays(*position, &mut *ops);
1177 }
1178 }
1179 WidgetEvent::PointerDown {
1180 position, button, ..
1181 } => {
1182 let pressed = self.current_pointer_id();
1185 self.cancelled_pointers.retain(|p| *p != pressed);
1186 self.tooltip_pointer_press(Some(*position));
1193 let pressing = self.current_input.pointer;
1197 if let Some(target) = self.hit_test_for(*position, &pressing) {
1198 if *button == PointerButton::Secondary
1199 && self.show_context_menu_for(
1200 target,
1201 *position,
1202 crate::widget_builder::ContextMenuTrigger::Pointer,
1203 &mut *ops,
1204 )
1205 {
1206 return;
1207 }
1208 self.begin_sequence(target, *position);
1214 let modifiers = match &event {
1219 WidgetEvent::PointerDown { modifiers, .. } => *modifiers,
1220 _ => crate::event::Modifiers::NONE,
1221 };
1222 self.begin_pan(target, *position, modifiers);
1223 self.begin_palm_watch(*position);
1224 self.feed_pinch(super::pan_arbiter::PinchFeed::Down, *position, &mut *ops);
1225 let focusable = self.find_focusable_at_or_above(target);
1229 self.begin_press(focusable);
1230 if !pressing.kind.is_direct()
1240 && let Some(focusable) = focusable
1241 && self.click_may_focus(focusable)
1242 {
1243 self.focus_with_origin_ops(
1244 focusable,
1245 crate::focus::FocusOrigin::Pointer(pressing.kind),
1246 &mut *ops,
1247 );
1248 }
1249 self.dispatch_to_widget(target, &event, &mut *ops);
1250 if self.active_drag.is_none() {
1255 self.enrol_sequence_members(&event, &mut *ops);
1256 }
1257 self.adopt_press_owner(*button);
1261 self.arm_touch_route(target, *position);
1266 }
1267 }
1268 WidgetEvent::PointerUp {
1269 position, button, ..
1270 } => {
1271 let released = self.current_pointer_id();
1277 self.cancel_touch_route(released);
1281 if let Some(index) = self.cancelled_pointers.iter().position(|p| *p == released) {
1282 self.cancelled_pointers.swap_remove(index);
1283 crate::trace_input!(
1284 Samples,
1285 "swallowing the Up for {released:?}: its press was cancelled"
1286 );
1287 return;
1288 }
1289 self.feed_pinch(super::pan_arbiter::PinchFeed::Up, *position, &mut *ops);
1296 if self.take_palm_verdict(released) {
1297 crate::trace_input!(
1298 Samples,
1299 "{released:?} released as a palm: large, and it never moved"
1300 );
1301 self.cancel_pointer(
1302 released,
1303 crate::pointer::CancelReason::PalmRejected,
1304 &mut *ops,
1305 );
1306 return;
1307 }
1308 self.end_pan(*position, &mut *ops);
1311 self.note_sequence_position(*position);
1318 let taps_unfocusable = self.release_taps_unfocusable(*position, *button);
1321 self.end_sequence(&event, &mut *ops);
1322 self.focus_on_release(*position, taps_unfocusable, &mut *ops);
1327 if let Some(captured) = self.current_pointer_capture() {
1328 self.dispatch_to_widget(captured, &event, &mut *ops);
1329 self.set_current_pointer_capture(None);
1332 } else {
1333 let releasing = self.current_input.pointer;
1334 if let Some(target) = self.hit_test_for(*position, &releasing) {
1335 self.dispatch_to_widget(target, &event, &mut *ops);
1336 }
1337 }
1338 let released = self.current_pointer_id();
1341 self.release_arenas_following(released);
1342 self.end_press(released);
1346 }
1347 WidgetEvent::PointerCancel {
1348 reason, pointer, ..
1349 } => {
1350 let (reason, pointer) = (*reason, pointer.id);
1355 self.cancel_pointer(pointer, reason, &mut *ops);
1356 }
1357 WidgetEvent::Scroll {
1358 window_position: position,
1359 ..
1360 } => {
1361 use super::pan_arbiter::ScrollDelivery;
1362 match ScrollDelivery::for_source(self.current_input.scroll_source) {
1363 ScrollDelivery::ClaimantChain => {
1368 let position = *position;
1369 self.route_scroll_along_chain(&event, position, &mut *ops);
1370 }
1371 ScrollDelivery::Bubble => {
1377 let scrolling = self.current_input.pointer;
1378 let target = match position {
1379 Some(p) => self.hit_test_for(*p, &scrolling),
1380 None => self.hovered_id().or(self.focused),
1381 };
1382 if let Some(target) = target {
1383 self.dispatch_to_widget(target, &event, &mut *ops);
1384 }
1385 }
1386 }
1387 }
1388 WidgetEvent::KeyDown { key, modifiers, .. } => {
1389 if *key == Key::Tab {
1390 let captured_focus = self
1405 .focused
1406 .is_some_and(|focused| self.is_keyboard_capture(focused));
1407 if captured_focus && modifiers.ctrl() {
1408 self.cycle_focus(modifiers.shift(), &mut *ops);
1409 return;
1410 }
1411 if let Some(focused) = self.focused {
1418 self.note_key_press(focused);
1419 }
1420 let handled = self
1421 .focused
1422 .map(|focused| {
1423 self.dispatch_to_widget_returning_handled(focused, &event, &mut *ops)
1424 })
1425 .unwrap_or(false);
1426 if !handled {
1427 self.cycle_focus(modifiers.shift(), &mut *ops);
1428 }
1429 } else if let Some(focused) = self.focused {
1430 self.note_key_press(focused);
1431 self.dispatch_to_widget(focused, &event, &mut *ops);
1432 }
1433 }
1434 WidgetEvent::KeyUp { key, .. } => {
1435 self.dispatch_key_release(*key, &event, &mut *ops);
1436 }
1437 WidgetEvent::ImeComposition { .. } | WidgetEvent::ImeCommit { .. } => {
1438 if let Some(focused) = self.focused {
1439 self.dispatch_to_widget(focused, &event, &mut *ops);
1440 }
1441 }
1442 WidgetEvent::AccessAction {
1443 target,
1444 action,
1445 target_node,
1446 ..
1447 } => {
1448 if let Some(id) =
1466 target.filter(|id| self.arena.is_active(*id) && !self.is_behind_modal(*id))
1467 {
1468 if *action == accesskit::Action::Focus {
1469 let synthetic =
1508 *target_node != crate::accessibility::widget_id_to_node_id(id);
1509 let takes_focus_itself = !synthetic
1510 && (self
1511 .arena
1512 .get(id)
1513 .is_some_and(|node| self.is_node_focusable(node))
1514 || self.focusable_composite_behind(id).is_some());
1515 let offered = !takes_focus_itself
1521 && if synthetic {
1522 self.published_node_advertises_focus(*target_node)
1523 } else {
1524 self.advertises_focus_action(id)
1525 && self.first_focusable_descendant(id).is_none()
1526 };
1527 let focused_before = self.focused;
1528 let answered = if offered {
1529 self.offer_item_focus(id, synthetic, &event, &mut *ops)
1530 } else {
1531 None
1532 };
1533 if let Some(answerer) = answered {
1534 let landed = self.focused.is_some_and(|focused| {
1543 Some(focused) != focused_before
1544 || focused == answerer
1545 || self.is_descendant_of(focused, answerer)
1546 || answerer == id
1547 });
1548 if landed {
1549 self.focus_origin = Some(crate::focus::FocusOrigin::Accessibility);
1553 if let Some(visible) =
1554 crate::focus::FocusOrigin::Accessibility.focus_visible()
1555 && self.focus_visible.get() != visible
1556 {
1557 self.focus_visible.set(visible);
1558 }
1559 }
1560 self.access_action_handled = landed;
1561 } else if self.advertises_focus_action(id) {
1562 let target = self
1563 .focusable_composite_behind(id)
1564 .or_else(|| self.first_focusable_descendant(id))
1565 .unwrap_or(id);
1566 if self.arena.is_enabled(target)
1567 && self
1568 .arena
1569 .get(target)
1570 .is_some_and(|node| self.is_node_focusable(node))
1571 {
1572 self.focus_with_origin_ops(
1579 target,
1580 crate::focus::FocusOrigin::Accessibility,
1581 &mut *ops,
1582 );
1583 self.access_action_handled = self.focused == Some(target);
1587 } else {
1588 self.access_action_handled = false;
1593 }
1594 } else {
1595 self.access_action_handled = false;
1596 }
1597 } else if *action == accesskit::Action::ShowContextMenu {
1598 self.access_action_handled =
1614 if self.dispatch_to_widget_returning_handled(id, &event, &mut *ops) {
1615 true
1616 } else {
1617 let target = self.resolve_context_menu_target(id);
1621 let position = self.arena.bounds(target).center();
1622 self.show_context_menu_for(
1623 target,
1624 position,
1625 crate::widget_builder::ContextMenuTrigger::Accessibility,
1626 &mut *ops,
1627 )
1628 };
1629 } else {
1630 if *action == accesskit::Action::Click
1637 && !self.in_scroll_chrome(id)
1638 && match self.find_focusable_at_or_above(id) {
1639 Some(focusable) => !self.click_may_focus(focusable),
1640 None => !self.belongs_to_focused(id),
1641 }
1642 {
1643 self.commit_pending_edit(&mut *ops);
1644 }
1645 self.access_action_handled =
1646 self.dispatch_to_widget_returning_handled(id, &event, &mut *ops);
1647 }
1648 }
1649 }
1650 WidgetEvent::Gesture { .. } => {
1651 if let Some(target) = self.hovered_id().or(self.focused) {
1652 self.dispatch_to_widget(target, &event, &mut *ops);
1653 }
1654 }
1655 WidgetEvent::ScrollIntoView { .. }
1656 | WidgetEvent::PointerEnter { .. }
1657 | WidgetEvent::PointerLeave { .. }
1658 | WidgetEvent::FocusGained { .. }
1659 | WidgetEvent::FocusLost => {}
1660 }
1661 self.drain_pending_intents(&mut *ops);
1666 }
1667
1668 fn offer_item_focus(
1688 &mut self,
1689 id: WidgetId,
1690 synthetic: bool,
1691 event: &WidgetEvent,
1692 ops: &mut dyn crate::window::WindowOps,
1693 ) -> Option<WidgetId> {
1694 let start = if self.arena.is_enabled(id) {
1695 Some(id)
1696 } else {
1697 let greyed_item = !synthetic
1698 && self
1699 .arena
1700 .get(id)
1701 .and_then(|node| node.enabled_state.as_ref())
1702 .is_some_and(|state| !state.get())
1703 && !self.has_focusable_descendant(id);
1704 self.arena
1705 .parent(id)
1706 .filter(|&parent| greyed_item && self.arena.is_enabled(parent))
1707 };
1708 let mut at = start;
1709 while let Some(node) = at {
1710 if self.dispatch_to_widget_direct_returning_handled(node, event, &mut *ops) {
1711 return Some(node);
1712 }
1713 if self
1714 .arena
1715 .get(node)
1716 .is_some_and(|n| self.is_node_focusable(n))
1717 {
1718 return None;
1719 }
1720 at = self.arena.parent(node);
1721 }
1722 None
1723 }
1724
1725 fn has_focusable_descendant(&self, id: WidgetId) -> bool {
1728 let mut stack: Vec<WidgetId> = self.arena.children(id).to_vec();
1729 while let Some(node) = stack.pop() {
1730 if self
1731 .arena
1732 .get(node)
1733 .is_some_and(|n| self.is_node_focusable(n))
1734 {
1735 return true;
1736 }
1737 stack.extend_from_slice(self.arena.children(node));
1738 }
1739 false
1740 }
1741
1742 fn published_node_advertises_focus(&self, node_id: accesskit::NodeId) -> bool {
1746 self.cached_a11y.as_ref().is_some_and(|update| {
1747 update
1748 .nodes
1749 .iter()
1750 .any(|(id, node)| *id == node_id && node.supports_action(accesskit::Action::Focus))
1751 })
1752 }
1753
1754 fn open_context_menu_from_keyboard(&mut self, ops: &mut dyn crate::window::WindowOps) -> bool {
1767 let Some(focused) = self.focused else {
1768 return false;
1769 };
1770 let target = self.resolve_context_menu_target(focused);
1771
1772 let bounds = self.bounds(target);
1776 let anchor = Point {
1777 x: bounds.x + bounds.width / 2.0,
1778 y: bounds.y + bounds.height / 2.0,
1779 };
1780 self.show_context_menu_for(
1781 target,
1782 anchor,
1783 crate::widget_builder::ContextMenuTrigger::Keyboard,
1784 ops,
1785 )
1786 }
1787
1788 pub(super) fn resolve_context_menu_target(&self, start: WidgetId) -> WidgetId {
1798 const MAX_HOPS: usize = 8;
1799 let next_of = |id: WidgetId| {
1800 self.arena
1801 .get(id)
1802 .and_then(|node| node.widget.context_menu_key_target())
1803 .filter(|next| self.arena.is_active(*next))
1804 };
1805 let mut passed = [start; MAX_HOPS + 1];
1806 let mut target = start;
1807 for hop in 0..MAX_HOPS {
1808 let Some(next) = next_of(target) else {
1809 break;
1810 };
1811 if passed[..=hop].contains(&next) {
1812 debug_assert!(
1813 false,
1814 "context_menu_key_target cycle through {next:?}: a widget names an ancestor's target"
1815 );
1816 break;
1817 }
1818 passed[hop + 1] = next;
1819 target = next;
1820 }
1821 target
1822 }
1823
1824 pub(super) fn show_context_menu_for(
1842 &mut self,
1843 target: WidgetId,
1844 position: Point,
1845 trigger: crate::widget_builder::ContextMenuTrigger,
1846 ops: &mut dyn crate::window::WindowOps,
1847 ) -> bool {
1848 let mut ctx = self.make_event_context(&mut *ops);
1854 ctx.context_menu_trigger = Some(trigger);
1855 let mut walker = Some(target);
1856 let mut menu_decision: Option<(WidgetId, Box<dyn Widget>, bool)> = None;
1859 while let Some(id) = walker {
1860 let Some(node) = self.arena.get(id) else {
1861 break;
1862 };
1863 if let Some(factory) = node.context_menu_factory.as_ref()
1868 && let Some(menu) = factory(position, &mut ctx)
1869 {
1870 menu_decision = Some((id, menu, true));
1871 break;
1872 }
1873 if let Some(contribution) = node.context_menu_contribution.as_ref()
1876 && let Some(section) = contribution(&mut ctx)
1877 {
1878 menu_decision = Some((id, section, false));
1879 break;
1880 }
1881 walker = node.parent;
1882 }
1883
1884 if let Some((owner_id, menu, by_factory)) = menu_decision.as_mut() {
1890 let sealed = *by_factory && self.is_focusable_control(*owner_id);
1891 if !sealed {
1892 let mut probe = if *by_factory {
1895 Some(*owner_id)
1896 } else {
1897 self.arena.get(*owner_id).and_then(|node| node.parent)
1898 };
1899 while let Some(id) = probe {
1900 let Some(node) = self.arena.get(id) else {
1901 break;
1902 };
1903 if id != *owner_id && self.is_focusable_control(id) {
1904 break;
1905 }
1906 if let Some(contribution) = node.context_menu_contribution.as_ref()
1907 && let Some(section) = contribution(&mut ctx)
1908 && menu.append_context_menu_section(section).is_err()
1909 {
1910 note_refused_section(*owner_id);
1911 }
1912 probe = node.parent;
1913 }
1914 }
1915 }
1916 let menu_decision = menu_decision.map(|(owner, menu, _)| (owner, menu));
1917
1918 let drain_anchor = menu_decision.as_ref().map(|(id, _)| *id).unwrap_or(target);
1924 self.collect_from_ctx(ctx, drain_anchor);
1925
1926 let Some((owner_id, menu_widget)) = menu_decision else {
1927 return false;
1928 };
1929
1930 let keep: std::collections::HashSet<WidgetId> = self
1936 .overlay_manager
1937 .stack
1938 .iter()
1939 .map(|o| o.content_id)
1940 .filter(|&content_id| self.is_descendant_of(owner_id, content_id))
1941 .collect();
1942 let dismissed = self.overlay_manager.dismiss_except(&keep);
1943 self.dormant_dismissed_content(&dismissed, &mut *ops);
1944
1945 let content_id = self.add_boxed(menu_widget);
1946 let prev_focus = self.focused;
1947 let placement =
1954 crate::overlay::OverlayPlacement::at_pointer_for(position, &self.current_input.pointer);
1955 let mut request = crate::overlay::OverlayRequest::new(
1958 content_id,
1959 owner_id,
1960 placement,
1961 crate::overlay::DismissBehavior::EscapeOrClickOutside,
1962 );
1963 request.destroy_content_on_dismiss = true;
1964 self.overlay_manager.show(request);
1965 if let Some(focus_id) = prev_focus {
1966 self.record_top_focus_restore(focus_id);
1967 }
1968 if trigger != crate::widget_builder::ContextMenuTrigger::Pointer
1972 && !self.focus_visible.get()
1973 {
1974 self.focus_visible.set(true);
1975 }
1976 self.focus_ops(content_id, &mut *ops);
1977 self.drain_pending_intents(&mut *ops);
1982 true
1983 }
1984
1985 fn is_focusable_control(&self, id: WidgetId) -> bool {
1990 let focusable = |id: WidgetId| {
1991 self.arena
1992 .get(id)
1993 .is_some_and(|node| self.is_node_focusable(node))
1994 };
1995 if focusable(id) || self.focusable_composite_behind(id).is_some() {
1996 return true;
1997 }
1998 let mut current = id;
1999 for _ in 0..8 {
2000 let Some(proxy) = self.live_accessibility_proxy(current) else {
2001 return false;
2002 };
2003 if focusable(proxy) {
2004 return true;
2005 }
2006 current = proxy;
2007 }
2008 false
2009 }
2010
2011 fn arm_outside_press_dismissal(
2032 &mut self,
2033 position: Point,
2034 button: PointerButton,
2035 ops: &mut dyn crate::window::WindowOps,
2036 ) -> bool {
2037 self.prune_stale_dismiss_arms();
2038 let pressing = self.current_input.pointer;
2039 let busy = self.busy_press_points(pressing.id);
2040
2041 if pressing.kind.is_direct() {
2042 return self
2043 .overlay_manager
2044 .arm_dismiss(pressing.id, position, &busy)
2045 .suppress_beneath;
2046 }
2047
2048 let mark = self.dismissal_mark();
2049 let (dismissed, focus_restore, toggle_anchors) = self
2050 .overlay_manager
2051 .dismiss_outside_press_toggles(position, &busy);
2052 if dismissed.is_empty() {
2053 return false;
2054 }
2055 self.dormant_dismissed_content(&dismissed, &mut *ops);
2056 self.return_focus_after_dismissal(focus_restore, mark, &mut *ops);
2057 toggle_anchors
2070 .iter()
2071 .any(|&toggle| self.press_reopens(toggle, position, button, &pressing))
2072 }
2073
2074 fn press_reopens(
2086 &self,
2087 toggle: crate::overlay::ToggleAnchor,
2088 position: Point,
2089 button: PointerButton,
2090 pointer: &crate::pointer::PointerInfo,
2091 ) -> bool {
2092 let anchor = toggle.anchor;
2093 if !self.arena.is_active(anchor) || !self.arena.bounds(anchor).contains(position) {
2094 return false;
2095 }
2096 if toggle.modal {
2097 return self.first_tapper(position, button, pointer) == Some(anchor);
2098 }
2099 button == PointerButton::Primary
2100 || self
2101 .anchor_tap_buttons(anchor, position, pointer)
2102 .contains(button)
2103 }
2104
2105 fn first_tapper(
2108 &self,
2109 position: Point,
2110 button: PointerButton,
2111 pointer: &crate::pointer::PointerInfo,
2112 ) -> Option<WidgetId> {
2113 let mut node = self.hit_test_for(position, pointer);
2114 while let Some(id) = node {
2115 if self.tap_buttons(id).contains(button) {
2116 return Some(id);
2117 }
2118 node = self.arena.parent(id);
2119 }
2120 None
2121 }
2122
2123 fn anchor_tap_buttons(
2133 &self,
2134 anchor: WidgetId,
2135 position: Point,
2136 pointer: &crate::pointer::PointerInfo,
2137 ) -> ButtonMask {
2138 let mut mask = ButtonMask::NONE;
2139 let mut node = self.hit_test_for(position, pointer);
2140 while let Some(id) = node {
2141 mask = mask.union(self.tap_buttons(id));
2142 if id == anchor {
2143 return mask;
2144 }
2145 node = self.arena.parent(id);
2146 }
2147 ButtonMask::NONE
2148 }
2149
2150 fn commit_outside_press_dismissal(
2157 &mut self,
2158 position: Point,
2159 ops: &mut dyn crate::window::WindowOps,
2160 ) -> bool {
2161 let pointer = self.current_pointer_id();
2162 if !self.overlay_manager.has_armed_dismiss(pointer) {
2163 return false;
2164 }
2165 let mark = self.dismissal_mark();
2166 let (dismissed, focus_restore, _anchors) =
2167 self.overlay_manager.commit_dismiss(pointer, position);
2168 if !dismissed.is_empty() {
2169 self.dormant_dismissed_content(&dismissed, &mut *ops);
2170 self.return_focus_after_dismissal(focus_restore, mark, &mut *ops);
2171 }
2172 true
2173 }
2174
2175 fn busy_press_points(&self, exclude: crate::pointer::PointerId) -> Vec<Point> {
2192 self.pointers
2193 .iter()
2194 .filter(|entry| entry.info.id != exclude)
2195 .filter(|entry| self.press_is_revocable(entry.info.id))
2196 .map(|entry| entry.down_position)
2197 .collect()
2198 }
2199
2200 fn prune_stale_dismiss_arms(&mut self) {
2207 let stale: Vec<crate::pointer::PointerId> = self
2208 .overlay_manager
2209 .armed_pointers()
2210 .into_iter()
2211 .filter(|id| self.pointers.get(*id).is_none())
2212 .collect();
2213 for id in stale {
2214 self.overlay_manager.abort_dismiss(id);
2215 }
2216 }
2217
2218 fn begin_press(&mut self, focusable: Option<WidgetId>) {
2239 let pointer = self.current_pointer_id();
2240 let now = self.sequence_now();
2241 let delay = self
2242 .pan_session_open(pointer)
2243 .then(|| self.current_profile().press_feedback_delay)
2244 .filter(|d| !d.is_zero());
2245 self.presses.press(pointer, focusable, now, delay);
2246 }
2247
2248 fn adopt_press_owner(&mut self, button: PointerButton) {
2271 let pointer = self.current_pointer_id();
2272 let Some(owner) = self.current_sequence().and_then(|s| s.pressed_owner()) else {
2273 return;
2274 };
2275 if !self.press_buttons(owner).contains(button) {
2276 crate::trace_input!(
2277 Samples,
2278 "no press visual for {owner:?}: {button:?} is not one of its accepted buttons"
2279 );
2280 return;
2281 }
2282 self.presses.set_owner(pointer, owner);
2283 self.publish_pressed(owner);
2284 }
2285
2286 fn update_press(&mut self, position: Point) {
2299 let pointer = self.current_pointer_id();
2300 if self.presses.get(pointer).is_none() {
2301 return;
2302 }
2303 let claimed_elsewhere = self.current_sequence().is_some_and(|sequence| {
2306 sequence
2307 .winner()
2308 .is_some_and(|winner| Some(winner) != sequence.pressed_owner())
2309 });
2310 if claimed_elsewhere {
2311 self.end_press(pointer);
2312 return;
2313 }
2314 let profile = self.current_profile();
2315 let origin = self.current_sequence().map(|s| s.press_origin());
2316 let owner = self.presses.get(pointer).and_then(|p| p.owner);
2317 let inside = match (origin, owner) {
2318 (Some(origin), Some(owner)) => {
2319 let bounds = self
2320 .arena
2321 .is_active(owner)
2322 .then(|| self.arena.bounds(owner));
2323 !crate::gesture::TapBoundary::for_pointer(&self.current_input.pointer, &profile)
2324 .left(origin, position, bounds, &profile)
2325 }
2326 _ => true,
2329 };
2330 let Some(press) = self.presses.get_mut(pointer) else {
2331 return;
2332 };
2333 if press.inside == inside {
2334 return;
2335 }
2336 press.inside = inside;
2337 if let Some(owner) = owner {
2338 self.publish_pressed(owner);
2339 }
2340 }
2341
2342 pub(crate) fn end_press(&mut self, pointer: crate::pointer::PointerId) {
2348 if let Some(owner) = self.presses.release(pointer) {
2349 self.publish_pressed(owner);
2350 }
2351 }
2352
2353 pub(crate) fn resolve_press_delays(&mut self, now: crate::pointer::EventTime) {
2356 if self.presses.is_empty() {
2357 return;
2358 }
2359 for id in self.presses.resolve_delays(now) {
2360 self.publish_pressed(id);
2361 self.arena.mark_needs_paint(id);
2362 }
2363 }
2364
2365 pub(crate) fn next_press_deadline(&self) -> Option<std::time::Instant> {
2368 self.presses.next_deadline().map(|t| self.instant_for(t))
2369 }
2370
2371 pub(crate) fn commit_pending_edit(&mut self, ops: &mut dyn crate::window::WindowOps) {
2387 let Some(focus) = self.focused.filter(|id| self.arena.is_active(*id)) else {
2388 return;
2389 };
2390 let (chain, _, _) = self.intent_walk(focus, &[]);
2391 for id in chain {
2392 for external in [true, false] {
2393 let handler = self.arena.get_mut(id).and_then(|node| {
2394 let handlers = if external {
2395 &mut node.external_handlers
2396 } else {
2397 &mut node.handlers
2398 };
2399 handlers.on_commit_edit.take()
2400 });
2401 let Some(mut handler) = handler else { continue };
2402 let mut ctx = self.make_event_context(&mut *ops).with_dispatch_node(id);
2403 handler(&mut ctx);
2404 if let Some(node) = self.arena.get_mut(id) {
2405 let handlers = if external {
2406 &mut node.external_handlers
2407 } else {
2408 &mut node.handlers
2409 };
2410 if handlers.on_commit_edit.is_none() {
2412 handlers.on_commit_edit = Some(handler);
2413 }
2414 }
2415 self.collect_from_ctx(ctx, id);
2416 }
2417 }
2418 }
2419
2420 fn release_taps_unfocusable(&self, position: Point, button: PointerButton) -> bool {
2441 let Some(press) = self.presses.get(self.current_pointer_id()) else {
2442 return false;
2443 };
2444 let Some(owner) = press.owner.filter(|_| press.focusable.is_none()) else {
2445 return false;
2446 };
2447 if !self.arena.is_active(owner) || !self.tap_buttons(owner).contains(button) {
2448 return false;
2449 }
2450 let Some(sequence) = self.current_sequence() else {
2451 return false;
2452 };
2453 if let Some(winner) = sequence.winner()
2458 && (winner != owner
2459 || sequence.members().iter().any(|member| {
2460 member.id == winner
2461 && matches!(member.role, crate::gesture::MemberRole::Gesture)
2462 }))
2463 {
2464 return false;
2465 }
2466 let profile = self.current_profile();
2467 let left = crate::gesture::TapBoundary::for_pointer(&self.current_input.pointer, &profile)
2468 .left(
2469 sequence.press_origin(),
2470 position,
2471 Some(self.arena.bounds(owner)),
2472 &profile,
2473 );
2474 !left && !self.in_scroll_chrome(owner) && !self.belongs_to_focused(owner)
2475 }
2476
2477 fn in_scroll_chrome(&self, id: WidgetId) -> bool {
2482 let mut current = Some(id);
2483 while let Some(node) = current {
2484 if self.arena.get(node).is_some_and(|n| n.scroll_chrome) {
2485 return true;
2486 }
2487 current = self.arena.parent(node);
2488 }
2489 false
2490 }
2491
2492 fn belongs_to_focused(&self, id: WidgetId) -> bool {
2497 self.focused
2498 .is_some_and(|focus| self.intent_walk(id, &[]).0.contains(&focus))
2499 }
2500
2501 fn click_may_focus(&self, focusable: WidgetId) -> bool {
2509 self.arena
2510 .get(focusable)
2511 .is_none_or(|node| node.focus_on_click)
2512 }
2513
2514 fn focus_on_release(
2538 &mut self,
2539 position: Point,
2540 taps_unfocusable: bool,
2541 ops: &mut dyn crate::window::WindowOps,
2542 ) {
2543 if taps_unfocusable {
2544 self.commit_pending_edit(&mut *ops);
2545 return;
2546 }
2547 let releasing = self.current_input.pointer;
2548 let pointer = self.current_pointer_id();
2549 let Some(pressed) = self.presses.get(pointer).and_then(|p| p.focusable) else {
2550 return;
2551 };
2552 if !releasing.kind.is_direct() && self.click_may_focus(pressed) {
2553 return;
2554 }
2555 let Some(target) = self.hit_test_for(position, &releasing) else {
2556 return;
2557 };
2558 if self.find_focusable_at_or_above(target) != Some(pressed) {
2559 return;
2560 }
2561 if !self.click_may_focus(pressed) {
2562 if !self.in_scroll_chrome(target) {
2565 self.commit_pending_edit(&mut *ops);
2566 }
2567 } else {
2568 self.focus_with_origin_ops(
2569 pressed,
2570 crate::focus::FocusOrigin::Pointer(releasing.kind),
2571 &mut *ops,
2572 );
2573 }
2574 }
2575
2576 fn handle_pointer_move(
2596 &mut self,
2597 event: &WidgetEvent,
2598 position: Point,
2599 ops: &mut dyn crate::window::WindowOps,
2600 ) {
2601 let moving = self.current_input.pointer;
2606 if self.pointers.hover_owner_id() != Some(self.current_pointer_id()) {
2607 if let Some(target) = self.hit_test_for(position, &moving) {
2608 self.dispatch_to_widget(target, event, &mut *ops);
2609 }
2610 return;
2611 }
2612 let target = self.hit_test_for(position, &moving);
2613
2614 if target != self.hovered_id() {
2615 let hover = self.hover_transition_pointer();
2617 let previously_hovered = self.hovered_id();
2618 if let Some(old) = previously_hovered {
2619 self.dispatch_to_widget(
2620 old,
2621 &WidgetEvent::PointerLeave { pointer: hover },
2622 &mut *ops,
2623 );
2624 self.tooltip_pointer_leave(old, &mut *ops);
2625 }
2626 if let Some(new) = target {
2627 self.dispatch_to_widget(
2628 new,
2629 &WidgetEvent::PointerEnter { pointer: hover },
2630 &mut *ops,
2631 );
2632 self.tooltip_pointer_enter(new);
2633 }
2634 self.tooltip_dwell_held = false;
2637 self.set_hovered(target);
2638 self.update_hover_within_signals(previously_hovered, target);
2639 } else if let Some(target) = target {
2640 self.tooltip_pointer_moved(target, position);
2643 }
2644
2645 if let Some(target) = target {
2646 self.dispatch_to_widget(target, event, &mut *ops);
2647 }
2648 }
2649
2650 pub(super) fn dispatch_to_widget(
2651 &mut self,
2652 target: WidgetId,
2653 event: &WidgetEvent,
2654 ops: &mut dyn crate::window::WindowOps,
2655 ) {
2656 self.dispatch_to_widget_returning_handled(target, event, ops);
2657 }
2658
2659 pub(super) fn localize_event(
2675 frame: &crate::arena::LocalFrame,
2676 event: &WidgetEvent,
2677 ) -> Option<WidgetEvent> {
2678 match event {
2679 WidgetEvent::PointerDown {
2680 position,
2681 button,
2682 modifiers,
2683 pointer,
2684 } => Some(WidgetEvent::PointerDown {
2685 position: frame.to_local(*position),
2686 button: *button,
2687 modifiers: *modifiers,
2688 pointer: *pointer,
2689 }),
2690 WidgetEvent::PointerUp {
2691 position,
2692 button,
2693 modifiers,
2694 pointer,
2695 } => Some(WidgetEvent::PointerUp {
2696 position: frame.to_local(*position),
2697 button: *button,
2698 modifiers: *modifiers,
2699 pointer: *pointer,
2700 }),
2701 WidgetEvent::PointerMove {
2702 position,
2703 modifiers,
2704 pointer,
2705 } => Some(WidgetEvent::PointerMove {
2706 position: frame.to_local(*position),
2707 modifiers: *modifiers,
2708 pointer: *pointer,
2709 }),
2710 WidgetEvent::Gesture { gesture } => Some(WidgetEvent::Gesture {
2711 gesture: Self::localize_gesture(frame, gesture),
2712 }),
2713 _ => None,
2719 }
2720 }
2721
2722 fn localize_gesture(frame: &crate::arena::LocalFrame, gesture: &GestureEvent) -> GestureEvent {
2729 let loc = |p: teksilo_canvas::Point| frame.to_local(p);
2730 let tap = |t: &TapEvent| {
2731 TapEvent::new(loc(t.position), t.button, t.modifiers).with_pointer(t.pointer)
2732 };
2733 match gesture {
2734 GestureEvent::Tap(t) => GestureEvent::Tap(tap(t)),
2735 GestureEvent::DoubleTap(t) => GestureEvent::DoubleTap(tap(t)),
2736 GestureEvent::TripleTap(t) => GestureEvent::TripleTap(tap(t)),
2737 GestureEvent::LongPress(t) => GestureEvent::LongPress(tap(t)),
2738 GestureEvent::DragStarted {
2739 position,
2740 button,
2741 pointer,
2742 } => GestureEvent::DragStarted {
2743 position: loc(*position),
2744 button: *button,
2745 pointer: *pointer,
2746 },
2747 GestureEvent::DragMoved {
2748 position,
2749 delta,
2750 pointer,
2751 } => GestureEvent::DragMoved {
2752 position: loc(*position),
2753 delta: *delta,
2754 pointer: *pointer,
2755 },
2756 GestureEvent::DragEnded { position, pointer } => GestureEvent::DragEnded {
2757 position: loc(*position),
2758 pointer: *pointer,
2759 },
2760 GestureEvent::DragCancelled {
2761 position,
2762 pointer,
2763 reason,
2764 } => GestureEvent::DragCancelled {
2765 position: loc(*position),
2766 pointer: *pointer,
2767 reason: *reason,
2768 },
2769 GestureEvent::PinchStarted { center } => GestureEvent::PinchStarted {
2770 center: loc(*center),
2771 },
2772 GestureEvent::PinchChanged {
2773 center,
2774 scale,
2775 rotation,
2776 } => GestureEvent::PinchChanged {
2777 center: loc(*center),
2778 scale: *scale,
2779 rotation: *rotation,
2780 },
2781 GestureEvent::PinchEnded => GestureEvent::PinchEnded,
2782 GestureEvent::PinchCancelled { reason } => {
2783 GestureEvent::PinchCancelled { reason: *reason }
2784 }
2785 GestureEvent::Swipe {
2786 direction,
2787 velocity,
2788 } => GestureEvent::Swipe {
2789 direction: *direction,
2790 velocity: *velocity,
2791 },
2792 }
2793 }
2794
2795 pub(super) fn dispatch_to_widget_returning_handled(
2801 &mut self,
2802 target: WidgetId,
2803 event: &WidgetEvent,
2804 ops: &mut dyn crate::window::WindowOps,
2805 ) -> bool {
2806 if !self.arena.is_enabled(target) && !matches!(event, WidgetEvent::FocusLost) {
2812 return false;
2813 }
2814
2815 let mut ancestors = Vec::new();
2816 let mut current = self.arena.parent(target);
2817 while let Some(id) = current {
2818 ancestors.push(id);
2819 current = self.arena.parent(id);
2820 }
2821 ancestors.reverse();
2822
2823 let tap_owner: Option<WidgetId> = if matches!(
2841 event,
2842 WidgetEvent::PointerDown { .. } | WidgetEvent::PointerUp { .. }
2843 ) {
2844 let mut owner = None;
2845 let mut cur = Some(target);
2846 while let Some(id) = cur {
2847 if self.arena.get(id).is_some_and(|n| {
2848 n.any_handler(|h| h.on_tap.is_some() || h.on_long_press.is_some())
2849 }) {
2850 owner = Some(id);
2851 break;
2852 }
2853 cur = self.arena.parent(id);
2854 }
2855 owner
2856 } else {
2857 None
2858 };
2859
2860 for &id in &ancestors {
2861 let frame = self.arena.local_frame(id);
2862 let mut ctx = self
2863 .make_event_context(&mut *ops)
2864 .with_dispatch_node(id)
2865 .with_dispatch_target(target)
2866 .with_local_frame(frame);
2867 ctx.press_claimed_by_interactive_child =
2868 tap_owner.is_some_and(|owner| owner != id && self.is_descendant_of(owner, id));
2869 let localized = Self::localize_event(&frame, event);
2872 let event = localized.as_ref().unwrap_or(event);
2873 let response = if let Some(node) = self.arena.get_mut(id) {
2874 Self::try_handler_preview(node, event, &mut ctx).unwrap_or(EventResponse::Ignored)
2875 } else {
2876 EventResponse::Ignored
2877 };
2878 self.collect_from_ctx(ctx, id);
2879 if response == EventResponse::Handled {
2880 self.arena.mark_needs_paint(id);
2881 if matches!(event, WidgetEvent::PointerDown { .. }) {
2889 self.note_preview_claim(id);
2890 }
2891 return true;
2892 }
2893 }
2894
2895 let needs_layout_on_handle = matches!(
2896 event,
2897 WidgetEvent::Scroll { .. } | WidgetEvent::ScrollIntoView { .. }
2898 );
2899 let mut current = Some(target);
2900 let mut is_target = true;
2901 while let Some(id) = current {
2902 let frame = self.arena.local_frame(id);
2903 let mut ctx = self
2904 .make_event_context(&mut *ops)
2905 .with_dispatch_node(id)
2906 .with_dispatch_target(target)
2907 .with_local_frame(frame);
2908 ctx.press_claimed_by_interactive_child =
2909 tap_owner.is_some_and(|owner| owner != id && self.is_descendant_of(owner, id));
2910 let localized = Self::localize_event(&frame, event);
2913 let gesture_cx = self.recognizer_context(id);
2914 let arena_blocked = self.sequence_blocks_arena(id);
2917 let WidgetTree {
2918 arena,
2919 gesture_owners,
2920 ..
2921 } = self;
2922 let event = localized.as_ref().unwrap_or(event);
2923 let response = if let Some(node) = arena.get_mut(id) {
2924 Self::try_handler_bubble(
2925 node,
2926 event,
2927 &mut ctx,
2928 BubbleGates {
2929 fire_on_pointer_event: is_target,
2930 arena_blocked,
2931 },
2932 id,
2933 gesture_owners,
2934 gesture_cx,
2935 )
2936 .unwrap_or(EventResponse::Ignored)
2937 } else {
2938 EventResponse::Ignored
2939 };
2940 self.collect_from_ctx(ctx, id);
2941 if response == EventResponse::Handled {
2942 if needs_layout_on_handle {
2943 self.arena.mark_needs_layout(id);
2944 } else {
2945 self.arena.mark_needs_paint(id);
2946 }
2947 self.note_pointer_acceptance(id, event);
2948 if !matches!(
2963 event,
2964 WidgetEvent::PointerEnter { .. } | WidgetEvent::PointerLeave { .. }
2965 ) {
2966 return true;
2967 }
2968 }
2969 is_target = false;
2970 current = self.arena.parent(id);
2971 }
2972 false
2973 }
2974
2975 pub(super) fn dispatch_to_widget_direct(
2976 &mut self,
2977 target: WidgetId,
2978 event: &WidgetEvent,
2979 ops: &mut dyn crate::window::WindowOps,
2980 ) {
2981 self.dispatch_to_widget_direct_returning_handled(target, event, ops);
2982 }
2983
2984 pub(super) fn dispatch_to_widget_direct_returning_handled(
2994 &mut self,
2995 target: WidgetId,
2996 event: &WidgetEvent,
2997 ops: &mut dyn crate::window::WindowOps,
2998 ) -> bool {
2999 if !self.arena.is_enabled(target) && !matches!(event, WidgetEvent::FocusLost) {
3002 return false;
3003 }
3004
3005 let frame = self.arena.local_frame(target);
3006 let mut ctx = self
3007 .make_event_context(&mut *ops)
3008 .with_dispatch_node(target)
3009 .with_dispatch_target(target)
3010 .with_local_frame(frame);
3011 let gesture_cx = self.recognizer_context(target);
3012 let arena_blocked = self.sequence_blocks_arena(target);
3013 let WidgetTree {
3014 arena,
3015 gesture_owners,
3016 ..
3017 } = self;
3018 let response = if let Some(node) = arena.get_mut(target) {
3019 Self::try_handler_bubble(
3020 node,
3021 event,
3022 &mut ctx,
3023 BubbleGates {
3024 fire_on_pointer_event: true,
3025 arena_blocked,
3026 },
3027 target,
3028 gesture_owners,
3029 gesture_cx,
3030 )
3031 .unwrap_or(EventResponse::Ignored)
3032 } else {
3033 EventResponse::Ignored
3034 };
3035 self.collect_from_ctx(ctx, target);
3036
3037 if response == EventResponse::Handled {
3038 if matches!(
3041 event,
3042 WidgetEvent::Scroll { .. } | WidgetEvent::ScrollIntoView { .. }
3043 ) {
3044 self.arena.mark_needs_layout(target);
3045 } else {
3046 self.arena.mark_needs_paint(target);
3047 }
3048 self.note_pointer_acceptance(target, event);
3049 }
3050 response == EventResponse::Handled
3051 }
3052
3053 pub(super) fn note_pointer_acceptance(&mut self, target: WidgetId, event: &WidgetEvent) {
3062 if !matches!(
3063 event,
3064 WidgetEvent::PointerDown { .. }
3065 | WidgetEvent::PointerMove { .. }
3066 | WidgetEvent::PointerUp { .. }
3067 ) {
3068 return;
3069 }
3070 let pointer = self.current_pointer_id();
3071 if let Some(entry) = self.pointers.get_mut(pointer) {
3072 entry.last_accepted = Some(target);
3073 }
3074 }
3075
3076 fn try_handler_preview(
3077 node: &mut crate::arena::WidgetNode,
3078 event: &WidgetEvent,
3079 ctx: &mut EventContext,
3080 ) -> Option<EventResponse> {
3081 match event {
3082 WidgetEvent::KeyDown { .. }
3090 | WidgetEvent::KeyUp { .. }
3091 | WidgetEvent::ImeComposition { .. }
3092 | WidgetEvent::ImeCommit { .. } => {
3093 let has = node.external_handlers.on_key_preview.is_some()
3094 || node.handlers.on_key_preview.is_some();
3095 if !has {
3096 return None;
3097 }
3098 Some(fire_event_handler_both(
3099 &mut node.external_handlers.on_key_preview,
3100 &mut node.handlers.on_key_preview,
3101 event,
3102 ctx,
3103 ))
3104 }
3105 WidgetEvent::PointerEnter { .. } | WidgetEvent::PointerLeave { .. } => None,
3117 _ => {
3118 let has = node.external_handlers.on_pointer_event.is_some()
3119 || node.handlers.on_pointer_event.is_some();
3120 if !has {
3121 return None;
3122 }
3123 Some(fire_event_handler_both(
3124 &mut node.external_handlers.on_pointer_event,
3125 &mut node.handlers.on_pointer_event,
3126 event,
3127 ctx,
3128 ))
3129 }
3130 }
3131 }
3132
3133 fn try_handler_bubble(
3134 node: &mut crate::arena::WidgetNode,
3135 event: &WidgetEvent,
3136 ctx: &mut EventContext,
3137 gates: BubbleGates,
3138 node_id: WidgetId,
3139 gesture_owners: &mut std::collections::HashSet<WidgetId>,
3140 gesture_cx: crate::gesture::RecognizerContext<'_>,
3141 ) -> Option<EventResponse> {
3142 let BubbleGates {
3143 fire_on_pointer_event,
3144 arena_blocked,
3145 } = gates;
3146 match event {
3147 WidgetEvent::PointerEnter { .. } => {
3148 if let Some(cursor) = node.node_cursor {
3149 ctx.declared_cursor_request = Some(cursor);
3154 }
3155 let mut fired = false;
3156 if let Some(h) = node.external_handlers.on_hover.as_mut() {
3157 h(true, ctx);
3158 fired = true;
3159 }
3160 if let Some(h) = node.handlers.on_hover.as_mut() {
3161 h(true, ctx);
3162 fired = true;
3163 }
3164 if fired {
3165 Some(EventResponse::Handled)
3166 } else {
3167 node.node_cursor.map(|_| EventResponse::Handled)
3168 }
3169 }
3170 WidgetEvent::PointerLeave { .. } => {
3171 if node.node_cursor.is_some() {
3172 ctx.declared_cursor_request = Some(crate::widget::CursorIcon::Default);
3173 }
3174 let mut fired = false;
3175 if let Some(h) = node.external_handlers.on_hover.as_mut() {
3176 h(false, ctx);
3177 fired = true;
3178 }
3179 if let Some(h) = node.handlers.on_hover.as_mut() {
3180 h(false, ctx);
3181 fired = true;
3182 }
3183 if fired {
3184 Some(EventResponse::Handled)
3185 } else {
3186 node.node_cursor.map(|_| EventResponse::Handled)
3187 }
3188 }
3189 WidgetEvent::FocusGained { origin } => {
3190 let previous_origin = ctx.focus_origin.replace(*origin);
3194 let mut fired = false;
3195 if let Some(h) = node.external_handlers.on_focus.as_mut() {
3196 h(true, ctx);
3197 fired = true;
3198 }
3199 if let Some(h) = node.handlers.on_focus.as_mut() {
3200 h(true, ctx);
3201 fired = true;
3202 }
3203 ctx.focus_origin = previous_origin;
3204 fired.then_some(EventResponse::Handled)
3205 }
3206 WidgetEvent::FocusLost => {
3207 let mut fired = false;
3208 if let Some(h) = node.external_handlers.on_focus.as_mut() {
3209 h(false, ctx);
3210 fired = true;
3211 }
3212 if let Some(h) = node.handlers.on_focus.as_mut() {
3213 h(false, ctx);
3214 fired = true;
3215 }
3216 fired.then_some(EventResponse::Handled)
3217 }
3218 WidgetEvent::KeyDown { .. }
3219 | WidgetEvent::KeyUp { .. }
3220 | WidgetEvent::ImeComposition { .. }
3221 | WidgetEvent::ImeCommit { .. } => {
3222 if node.external_handlers.on_key.is_some() || node.handlers.on_key.is_some() {
3223 Some(fire_event_handler_both(
3224 &mut node.external_handlers.on_key,
3225 &mut node.handlers.on_key,
3226 event,
3227 ctx,
3228 ))
3229 } else {
3230 None
3231 }
3232 }
3233 WidgetEvent::Scroll { .. } | WidgetEvent::ScrollIntoView { .. } => {
3234 if node.external_handlers.on_scroll.is_some() || node.handlers.on_scroll.is_some() {
3235 Some(fire_event_handler_both(
3236 &mut node.external_handlers.on_scroll,
3237 &mut node.handlers.on_scroll,
3238 event,
3239 ctx,
3240 ))
3241 } else {
3242 None
3243 }
3244 }
3245 WidgetEvent::AccessAction {
3246 action,
3247 target_node,
3248 data,
3249 ..
3250 } => {
3251 let saved_a11y_source = ctx
3271 .current_source
3272 .replace(crate::telemetry::IntentSource::Accessibility);
3273 let mut any_slot = false;
3274 let mut any_handled = false;
3275 if let Some(h) = node.external_handlers.on_access_action_request.as_mut() {
3276 any_slot = true;
3277 any_handled |=
3278 h(*action, *target_node, data.clone(), ctx) == EventResponse::Handled;
3279 }
3280 if let Some(h) = node.handlers.on_access_action_request.as_mut() {
3281 any_slot = true;
3282 any_handled |=
3283 h(*action, *target_node, data.clone(), ctx) == EventResponse::Handled;
3284 }
3285 if let Some(h) = node.external_handlers.on_access_action.as_mut() {
3286 any_slot = true;
3287 any_handled |= h(*action, ctx) == EventResponse::Handled;
3288 }
3289 if let Some(h) = node.handlers.on_access_action.as_mut() {
3290 any_slot = true;
3291 any_handled |= h(*action, ctx) == EventResponse::Handled;
3292 }
3293 let user_handled = any_slot.then_some(if any_handled {
3294 EventResponse::Handled
3295 } else {
3296 EventResponse::Ignored
3297 });
3298
3299 let mut override_handled = false;
3305 if let Some(ov) = node.access_overrides.as_deref_mut() {
3306 if matches!(action, accesskit::Action::CustomAction) {
3307 if let Some(accesskit::ActionData::CustomAction(idx)) = data
3308 && let Some((_, cb)) = ov.custom_actions.get_mut(*idx as usize)
3309 {
3310 cb(ctx);
3311 override_handled = true;
3312 }
3313 } else {
3314 for (a, cb) in ov.actions.iter_mut() {
3315 if *a == *action {
3316 cb(ctx);
3317 override_handled = true;
3318 }
3319 }
3320 }
3321 }
3322
3323 ctx.current_source = saved_a11y_source;
3324 match (user_handled, override_handled) {
3325 (Some(EventResponse::Handled), _) | (_, true) => Some(EventResponse::Handled),
3326 (Some(EventResponse::Ignored), false) => Some(EventResponse::Ignored),
3327 (None, false) => None,
3328 }
3329 }
3330 WidgetEvent::Gesture { gesture } => {
3331 let matched = matches!(
3335 gesture,
3336 GestureEvent::PinchStarted { .. }
3337 | GestureEvent::PinchChanged { .. }
3338 | GestureEvent::PinchEnded
3339 | GestureEvent::Swipe { .. }
3340 | GestureEvent::DoubleTap { .. }
3341 | GestureEvent::TripleTap { .. }
3342 ) && {
3343 let has_handler = match gesture {
3344 GestureEvent::PinchStarted { .. }
3345 | GestureEvent::PinchChanged { .. }
3346 | GestureEvent::PinchEnded => node.any_handler(|h| h.on_pinch.is_some()),
3347 GestureEvent::Swipe { .. } => node.any_handler(|h| h.on_swipe.is_some()),
3348 GestureEvent::DoubleTap { .. } => {
3349 node.any_handler(|h| h.on_double_tap.is_some())
3350 }
3351 GestureEvent::TripleTap { .. } => {
3352 node.any_handler(|h| h.on_triple_tap.is_some())
3353 }
3354 _ => false,
3355 };
3356 if has_handler {
3357 Self::dispatch_recognized_gesture(node, *gesture, ctx);
3358 }
3359 has_handler
3360 };
3361 if matched {
3362 Some(EventResponse::Handled)
3363 } else {
3364 None
3365 }
3366 }
3367 WidgetEvent::PointerDown {
3368 position,
3369 button,
3370 modifiers,
3371 ..
3372 } => {
3373 if fire_on_pointer_event {
3380 let r = fire_event_handler_both(
3381 &mut node.external_handlers.on_pointer_event,
3382 &mut node.handlers.on_pointer_event,
3383 event,
3384 ctx,
3385 );
3386 if r == EventResponse::Handled {
3387 return Some(EventResponse::Handled);
3388 }
3389 }
3390 if arena_blocked {
3391 return None;
3395 }
3396 Self::ensure_gesture_arena(node, node_id, gesture_owners);
3397 if let Some(arena) = node.handlers.gesture_arena.as_mut() {
3398 let cx = gesture_cx;
3399 ctx.capture_pointer_implicit();
3414 let result = arena.process(
3415 &RawPointerEvent::Down {
3416 position: *position,
3417 button: *button,
3418 modifiers: *modifiers,
3419 pointer: cx.pointer,
3420 time: cx.now,
3421 },
3422 &cx,
3423 );
3424 if let Some(gesture) = result {
3425 Self::dispatch_recognized_gesture(node, gesture, ctx);
3426 }
3427 return Some(EventResponse::Handled);
3428 }
3429 None
3430 }
3431 WidgetEvent::PointerUp {
3432 position,
3433 button,
3434 modifiers,
3435 ..
3436 } => {
3437 if fire_on_pointer_event {
3438 let r = fire_event_handler_both(
3439 &mut node.external_handlers.on_pointer_event,
3440 &mut node.handlers.on_pointer_event,
3441 event,
3442 ctx,
3443 );
3444 if r == EventResponse::Handled {
3445 return Some(EventResponse::Handled);
3446 }
3447 }
3448 if arena_blocked {
3449 return None;
3450 }
3451 if let Some(arena) = node.handlers.gesture_arena.as_mut() {
3452 let cx = gesture_cx;
3453 let result = arena.process(
3454 &RawPointerEvent::Up {
3455 position: *position,
3456 button: *button,
3457 modifiers: *modifiers,
3458 pointer: cx.pointer,
3459 time: cx.now,
3460 },
3461 &cx,
3462 );
3463 if let Some(gesture) = result {
3464 Self::dispatch_recognized_gesture(node, gesture, ctx);
3465 }
3466 return Some(EventResponse::Handled);
3467 }
3468 None
3469 }
3470 WidgetEvent::PointerMove { position, .. } => {
3471 if fire_on_pointer_event {
3472 let r = fire_event_handler_both(
3473 &mut node.external_handlers.on_pointer_event,
3474 &mut node.handlers.on_pointer_event,
3475 event,
3476 ctx,
3477 );
3478 if r == EventResponse::Handled {
3479 return Some(EventResponse::Handled);
3480 }
3481 }
3482 if arena_blocked {
3483 return None;
3484 }
3485 if let Some(arena) = node.handlers.gesture_arena.as_mut() {
3486 let cx = gesture_cx;
3487 let result = arena.process(
3488 &RawPointerEvent::Move {
3489 position: *position,
3490 pointer: cx.pointer,
3491 time: cx.now,
3492 },
3493 &cx,
3494 );
3495 if let Some(gesture) = result {
3496 Self::dispatch_recognized_gesture(node, gesture, ctx);
3497 return Some(EventResponse::Handled);
3509 }
3510 return Some(EventResponse::Ignored);
3511 }
3512 None
3513 }
3514 WidgetEvent::PointerCancel {
3515 reason, pointer, ..
3516 } => {
3517 for slot in [
3525 &mut node.external_handlers.on_pointer_cancel,
3526 &mut node.handlers.on_pointer_cancel,
3527 ] {
3528 if let Some(handler) = slot.as_mut() {
3529 handler(pointer, *reason, ctx);
3530 }
3531 }
3532 if fire_on_pointer_event {
3533 let r = fire_event_handler_both(
3534 &mut node.external_handlers.on_pointer_event,
3535 &mut node.handlers.on_pointer_event,
3536 event,
3537 ctx,
3538 );
3539 if r == EventResponse::Handled {
3540 return Some(EventResponse::Handled);
3541 }
3542 }
3543 None
3544 }
3545 }
3546 }
3547
3548 pub(super) fn collect_from_ctx<'ops>(
3549 &mut self,
3550 mut ctx: EventContext<'ops>,
3551 source_widget: WidgetId,
3552 ) {
3553 let local_ops = ctx.window_ops.take();
3559 let mut noop = crate::window::NoopWindowOps;
3560 let ops: &mut dyn crate::window::WindowOps = match local_ops {
3561 Some(o) => o,
3562 None => &mut noop,
3563 };
3564 if ctx.frame_requested {
3565 self.request_frame();
3566 }
3567 if let Some(declared) = ctx.declared_cursor_request {
3571 self.node_declared_cursor = declared;
3572 self.current_cursor = declared;
3573 }
3574 match ctx.cursor_request {
3575 Some(crate::widget::CursorRequest::Set(cursor)) => {
3576 self.current_cursor = cursor;
3577 }
3578 Some(crate::widget::CursorRequest::Release) => {
3582 self.current_cursor = self.node_declared_cursor;
3583 }
3584 None => {}
3585 }
3586 for intent in ctx.pending_intents {
3592 self.enqueue_intent(source_widget, intent, true);
3593 }
3594 if ctx.cancel_key_capture {
3603 self.cancel_key_capture();
3604 }
3605 if let Some(slot) = ctx.pending_key_capture {
3606 self.key_capture = Some(slot);
3607 }
3608 for mutation in ctx.pending_shortcut_mutations {
3610 match mutation {
3611 crate::widget::ShortcutMutation::RebindPrimary { id, keystroke } => {
3612 self.shortcut_registry.rebind_primary(id, keystroke);
3613 }
3614 crate::widget::ShortcutMutation::RebindSecondary { id, keystroke } => {
3615 self.shortcut_registry.rebind_secondary(id, keystroke);
3616 }
3617 crate::widget::ShortcutMutation::ClearOverride { id } => {
3618 self.shortcut_registry.clear_override(&id);
3619 }
3620 }
3621 }
3622 if ctx.close_request_to_modal {
3623 self.answer_close_request_with_modal(&mut *ops);
3624 }
3625 if ctx.close_window_requested {
3626 self.close_window_requested = true;
3627 }
3628 if ctx.force_close_requested {
3629 self.force_close_requested = true;
3630 }
3631 self.pending_modal_requests
3632 .extend(ctx.modal_requests.into_iter().map(|request| {
3633 crate::modal::QueuedModalRequest {
3634 source_widget,
3635 request,
3636 }
3637 }));
3638 let focus_intents = std::mem::take(&mut ctx.pending_focus_intents);
3648 let (fallback_crossings, fallback_modal) = if focus_intents.is_empty() {
3649 (Vec::new(), None)
3650 } else {
3651 let modal = self.intent_walk(source_widget, &[]).2;
3652 (self.intent_route(source_widget, &[], true).crossings, modal)
3653 };
3654 if ctx.dismiss_modal && !self.dismiss_modal_for_source(source_widget, &mut *ops) {
3655 self.pending_modal_dismissal = true;
3656 }
3657 for callback in ctx.idle_callbacks {
3658 self.idle_queue.push_boxed(callback);
3659 }
3660 match ctx.dismiss_scope {
3661 Some(crate::widget::DismissScope::All) => {
3662 let mark = self.dismissal_mark();
3663 let dismissed = self.overlay_manager.dismiss_all();
3664 self.dormant_dismissed_content(&dismissed, &mut *ops);
3665 self.return_focus_after_dismissal(None, mark, &mut *ops);
3666 }
3667 Some(crate::widget::DismissScope::AllExceptHosts) => {
3668 self.dismiss_all_overlays_except_hosts(&mut *ops);
3669 }
3670 Some(crate::widget::DismissScope::SelfChain) => {
3671 self.dismiss_self_overlay_chain_for_source(source_widget, &mut *ops);
3672 }
3673 Some(crate::widget::DismissScope::Top) => {
3674 let mark = self.dismissal_mark();
3675 if let Some((_id, content_ids, focus_restore)) = self.overlay_manager.dismiss_top()
3676 {
3677 self.dormant_dismissed_content(&content_ids, &mut *ops);
3678 self.return_focus_after_dismissal(focus_restore, mark, &mut *ops);
3679 }
3680 }
3681 None => {
3682 for id in ctx.overlay_dismissals {
3683 let mark = self.dismissal_mark();
3684 let dismissed = self.overlay_manager.dismiss(id);
3685 self.dormant_dismissed_content(&dismissed, &mut *ops);
3686 self.return_focus_after_dismissal(None, mark, &mut *ops);
3687 }
3688 }
3689 }
3690 for content_id in ctx.overlay_content_dismissals {
3697 if let Some(overlay_id) = self.overlay_manager.find_by_content(content_id) {
3698 let mark = self.dismissal_mark();
3699 let dismissed = self.overlay_manager.dismiss(overlay_id);
3700 self.dormant_dismissed_content(&dismissed, &mut *ops);
3701 self.return_focus_after_dismissal(None, mark, &mut *ops);
3702 }
3703 }
3704 for (id, pause) in ctx.overlay_pause_requests {
3711 if pause {
3712 self.overlay_manager.pause_auto_dismiss(id);
3713 } else {
3714 self.overlay_manager.resume_auto_dismiss(id);
3715 }
3716 }
3717 for preserve_content in ctx.dismiss_descendant_overlays {
3718 self.dismiss_child_overlays_for_source(source_widget, preserve_content, &mut *ops);
3719 }
3720 if !focus_intents.is_empty() {
3729 let modal_closed = fallback_modal
3730 .is_some_and(|content| self.overlay_manager.find_by_content(content).is_none());
3731 let start = match self.focused.filter(|id| self.arena.is_active(*id)) {
3732 Some(focus) => Some((focus, None)),
3733 None if modal_closed => self.intent_anchor().map(|root| (root, None)),
3734 None if fallback_modal.is_none()
3738 && self.overlay_manager.topmost_modal().is_some() =>
3739 {
3740 self.focus_or_modal().map(|modal| (modal, None))
3741 }
3742 None => Some((source_widget, Some(fallback_crossings))),
3743 };
3744 if let Some((start, crossings)) = start {
3745 for intent in focus_intents {
3746 match &crossings {
3747 None => self.enqueue_intent(start, intent, true),
3748 Some(crossings) => {
3749 self.enqueue_intent_crossing(start, intent, true, crossings.clone())
3750 }
3751 }
3752 }
3753 }
3754 }
3755 let deferred_row_activations =
3756 self.apply_tree_mutations(std::mem::take(&mut ctx.tree_mutations));
3757 if ctx.request_a11y_update {
3758 self.a11y_dirty = true;
3759 }
3760 if let Some(visible) = ctx.soft_keyboard_request.take() {
3761 self.request_soft_keyboard(visible);
3762 }
3763 for (message, politeness) in std::mem::take(&mut ctx.announcements) {
3766 self.announce_with(message, politeness);
3767 }
3768 for mut req in ctx.overlay_requests {
3769 if req.parent_overlay.is_none() {
3770 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3771 }
3772 if self
3773 .overlay_manager
3774 .find_by_content(req.content_id)
3775 .is_some()
3776 {
3777 continue;
3778 }
3779 let current_focus = self.focused;
3780 self.overlay_manager.show(req);
3781 self.a11y_dirty = true;
3785 if let Some(focus_id) = current_focus {
3786 self.record_top_focus_restore(focus_id);
3787 }
3788 }
3789 for (mut req, band) in ctx.overlay_band_requests {
3790 if req.parent_overlay.is_none() {
3791 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3792 }
3793 if self
3794 .overlay_manager
3795 .find_by_content(req.content_id)
3796 .is_some()
3797 {
3798 continue;
3799 }
3800 let content_id = req.content_id;
3801 self.overlay_manager.show_in_band(req, band);
3802 self.arena.activate(content_id);
3803 self.a11y_dirty = true;
3804 }
3808 for (content_id, placement) in ctx.overlay_placement_updates {
3811 if let Some(overlay_id) = self.overlay_manager.find_by_content(content_id) {
3812 self.overlay_manager.update_placement(overlay_id, placement);
3813 }
3814 }
3815 for (content_id, source) in ctx.overlay_placement_sources {
3816 if let Some(overlay_id) = self.overlay_manager.find_by_content(content_id) {
3817 self.overlay_manager
3818 .set_placement_source(overlay_id, source);
3819 }
3820 }
3821 for (mut req, duration) in ctx.timed_overlay_requests {
3822 if req.parent_overlay.is_none() {
3823 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3824 }
3825 if self
3826 .overlay_manager
3827 .find_by_content(req.content_id)
3828 .is_some()
3829 {
3830 continue;
3831 }
3832 let current_focus = self.focused;
3833 let overlay_id = self.overlay_manager.show_for(req, duration);
3834 self.overlay_manager
3835 .set_shown_at_sim(overlay_id, self.sim_clock);
3836 self.a11y_dirty = true;
3837 if let Some(focus_id) = current_focus {
3838 self.record_top_focus_restore(focus_id);
3839 }
3840 }
3841 for (mut req, progress, duration) in ctx.reveal_overlay_requests {
3842 if req.parent_overlay.is_none() {
3843 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3844 }
3845 if self
3846 .overlay_manager
3847 .find_by_content(req.content_id)
3848 .is_some()
3849 {
3850 continue;
3851 }
3852 let content_id = req.content_id;
3853 let current_focus = self.focused;
3854 let overlay_id = self.overlay_manager.show(req);
3855 self.overlay_manager
3856 .set_shown_at_sim(overlay_id, self.sim_clock);
3857 self.a11y_dirty = true;
3858 if let Some(focus_id) = current_focus {
3859 self.record_top_focus_restore(focus_id);
3860 }
3861 self.register_animated_signal(&progress, content_id);
3867 let _ = progress.try_animate_with_options(crate::animation::AnimationRequest {
3868 target: 1.0,
3869 duration,
3870 easing: teksilo_tokens::Easing::EaseOut,
3871 frame_interval: None,
3872 looping: false,
3873 epsilon: 0.0,
3874 max_duration: None,
3875 });
3876 self.overlay_manager
3877 .attach_fade(overlay_id, progress, duration);
3878 }
3879 if let Some((pointer, capture)) = ctx.pointer_capture {
3880 let named = pointer;
3884 let pointer = pointer.unwrap_or_else(|| self.current_pointer_id());
3885 self.set_pointer_capture(pointer, capture.then_some(source_widget));
3886 if capture && ctx.explicit_capture && named.is_none() {
3890 self.note_explicit_capture(source_widget);
3891 }
3892 }
3893 if let Some(activation) = ctx.drag_activation_override.take() {
3894 self.note_drag_activation_override(source_widget, activation);
3898 }
3899 if ctx.recognized_owning_gesture {
3900 self.note_gesture_recognized(source_widget);
3903 }
3904 if !ctx.gesture_acts.is_empty() {
3905 let acts = std::mem::take(&mut ctx.gesture_acts);
3906 self.apply_gesture_acts(&acts, source_widget);
3907 }
3908 if let Some(reason) = ctx.cancel_pointer_request {
3909 let pointer = self.current_pointer_id();
3910 self.cancel_pointer(pointer, reason, &mut *ops);
3911 }
3912 for (mut request, delay, focus_target, replace_siblings) in ctx.delayed_overlay_requests {
3913 if request.parent_overlay.is_none() {
3914 request.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3915 }
3916 if self
3917 .overlay_manager
3918 .find_by_content(request.content_id)
3919 .is_some()
3920 {
3921 continue;
3922 }
3923 let content_id = request.content_id;
3924 self.pending_delayed_overlays
3925 .retain(|pending| pending.request.content_id != content_id);
3926 self.pending_delayed_overlays.push(PendingDelayedOverlay {
3927 request,
3928 delay,
3929 focus_target,
3930 replace_siblings,
3931 real_requested_at: std::time::Instant::now(),
3932 sim_requested_at: self.sim_clock,
3933 });
3934 self.arena.mark_needs_paint(source_widget);
3935 }
3936 for content_id in ctx.cancel_delayed_overlays {
3937 self.pending_delayed_overlays
3938 .retain(|pending| pending.request.content_id != content_id);
3939 }
3940 for content_id in ctx.safe_region_arm_requests {
3947 if let Some(apex) = self
3948 .previous_pointer_position
3949 .or_else(|| self.hover_owner_position())
3950 {
3951 self.overlay_manager.arm_safe_region(
3952 content_id,
3953 apex,
3954 std::time::Instant::now(),
3955 self.sim_clock,
3956 );
3957 }
3958 }
3959 for id in ctx.repaint_requests {
3960 self.arena.mark_needs_paint(id);
3961 }
3962 for id in ctx.synthetic_clicks {
3963 self.synthesise_tap_with_ops(id, &mut *ops);
3968 }
3969 if ctx.clear_focus_requested {
3972 self.clear_focus_ops(&mut *ops);
3973 }
3974 if let Some(&id) = ctx.focus_requests.last() {
3979 self.request_focus_with_ops(id, super::FocusRequestMode::SelfOrDescendant, &mut *ops);
3989 }
3990 if let Some(&id) = ctx.focus_into_requests.last() {
3991 self.request_focus_with_ops(id, super::FocusRequestMode::Into, &mut *ops);
3998 }
3999
4000 for req in ctx.scroll_into_view_requests {
4009 self.scroll_rect_into_view(
4010 req.from.unwrap_or(source_widget),
4013 req.rect,
4014 req.margin,
4015 req.align,
4016 req.motion,
4017 &mut *ops,
4018 );
4019 }
4020 for (id, margin) in ctx.scroll_widget_into_view_requests {
4027 if self.arena.get(id).is_some() {
4028 let bounds = self.arena.bounds(id);
4029 self.scroll_rect_into_view(
4030 id,
4031 bounds,
4032 margin,
4033 crate::event::ScrollAlign::Minimal,
4034 crate::event::ScrollMotion::Instant,
4035 &mut *ops,
4036 );
4037 }
4038 }
4039
4040 if let Some(&id) = ctx.highlight_tooltip_requests.last() {
4045 self.show_highlight_tooltip(id, &mut *ops);
4046 }
4047
4048 if let Some((source_widget, payload, preview_widget)) = ctx.drag_start_request {
4050 let (preview_content_id, preview_overlay_id) = if let Some(preview) = preview_widget {
4051 let content_id = self.add_boxed(preview);
4058 let overlay_id = self
4059 .overlay_manager
4060 .show(crate::overlay::OverlayRequest::new(
4061 content_id,
4062 source_widget,
4063 crate::overlay::OverlayPlacement::AtPointer(teksilo_canvas::Point::ZERO),
4064 crate::overlay::DismissBehavior::Manual,
4065 ));
4066 self.arena.mark_needs_layout(content_id);
4070 (Some(content_id), Some(overlay_id))
4071 } else {
4072 (None, None)
4073 };
4074 self.active_drag = Some(crate::drag_state::DragSession {
4075 payload,
4076 pointer: self.current_input.pointer,
4082 source_widget: Some(source_widget),
4083 is_external: false,
4084 current_position: teksilo_canvas::Point::ZERO,
4085 current_target: None,
4086 feedback: crate::drag_state::DropFeedback::NoFeedback,
4087 preview_content_id,
4088 preview_overlay_id,
4089 });
4090 self.set_current_pointer_capture(Some(source_widget));
4091 self.current_cursor = crate::widget::CursorIcon::Grabbing;
4094 }
4095 if ctx.cancel_drag {
4096 self.cancel_active_drag(&mut *ops);
4097 }
4098
4099 if let Some(theme) = ctx.theme_request {
4101 self.pending_theme_request = Some(theme);
4107 }
4108 if ctx.follow_system_request {
4109 self.pending_follow_system_request = true;
4113 }
4114 if let Some(locale) = ctx.locale_request {
4115 self.pending_locale_request = Some(locale);
4121 }
4122 if let Some(scale) = ctx.text_scale_request {
4123 self.pending_text_scale_request = Some(scale);
4128 }
4129 if !deferred_row_activations.is_empty() {
4136 self.run_with_event_context(&mut *ops, move |ctx| {
4137 for action in deferred_row_activations {
4138 action(ctx);
4139 }
4140 });
4141 }
4142 }
4143
4144 #[must_use]
4150 fn apply_tree_mutations(
4151 &mut self,
4152 mutations: Vec<crate::widget::TreeMutation>,
4153 ) -> Vec<std::rc::Rc<dyn Fn(&mut crate::widget::EventContext)>> {
4154 let mut deferred_row_activations = Vec::new();
4155 use crate::binding::BindingLevel;
4156 use crate::widget::TreeMutation;
4157
4158 for mutation in mutations {
4159 match mutation {
4160 TreeMutation::SetDormant(id) => {
4161 self.park_subtree(id);
4162 }
4163 TreeMutation::Activate(id) => self.arena.activate(id),
4164 TreeMutation::Destroy(id) => {
4165 if let Some(overlay_id) = self.overlay_manager().find_by_content(id) {
4178 self.dismiss_overlay(overlay_id);
4179 }
4180 self.destroy_subtree(id);
4181 }
4182 TreeMutation::MaterializeNow(id) => {
4186 if self.arena.get(id).is_some() {
4187 self.rebuild_single_widget(id);
4188 }
4189 }
4190 TreeMutation::RowSpaceActivate { row, fallback } => {
4191 deferred_row_activations.push(self.keyboard_toggle_in(row).unwrap_or(fallback));
4201 }
4202 TreeMutation::WithWidgetMut { id, dirty, apply } => {
4203 let existed = if let Some(any) =
4208 self.arena.get_mut(id).and_then(|n| n.widget.as_any_mut())
4209 {
4210 apply(any);
4211 true
4212 } else {
4213 false
4214 };
4215 if existed {
4216 match dirty {
4217 BindingLevel::RepaintOnly => self.arena.mark_needs_paint(id),
4218 BindingLevel::SubtreeRepaint => self.arena.mark_subtree_needs_paint(id),
4219 BindingLevel::Relayout => {
4220 self.arena.mark_needs_layout(id);
4221 self.arena.mark_ancestors_need_layout(id);
4222 }
4223 BindingLevel::Rebuild => {
4224 self.arena.mark_needs_rebuild(id);
4225 self.arena.mark_ancestors_need_layout(id);
4226 }
4227 BindingLevel::AccessibilityOnly => self.a11y_dirty = true,
4228 }
4229 }
4230 }
4231 }
4232 }
4233 deferred_row_activations
4234 }
4235
4236 pub fn hit_test(&self, point: Point) -> Option<WidgetId> {
4245 self.hit_test_excluding_overlay_and_widget(point, None, None)
4246 }
4247
4248 pub fn hit_test_for(
4259 &self,
4260 point: Point,
4261 pointer: &crate::pointer::PointerInfo,
4262 ) -> Option<WidgetId> {
4263 self.hit_test_for_excluding(point, pointer, None, None)
4264 }
4265
4266 pub fn hit_test_for_excluding(
4269 &self,
4270 point: Point,
4271 pointer: &crate::pointer::PointerInfo,
4272 exclude_overlay: Option<crate::overlay::OverlayId>,
4273 exclude_widget: Option<WidgetId>,
4274 ) -> Option<WidgetId> {
4275 let surfaces = self.text_surfaces();
4276 let read_only = |id: WidgetId| surfaces.is_read_only(id);
4277 let hit =
4278 crate::pointer::hit_slop::HitContext::new(pointer.kind, &self.effective_theme.input)
4279 .direction(self.layout_direction)
4280 .read_only_probe(&read_only);
4281 self.hit_test_with(point, exclude_overlay, exclude_widget, &hit)
4282 }
4283
4284 pub fn hit_test_excluding_overlay_and_widget(
4291 &self,
4292 point: Point,
4293 exclude_overlay: Option<crate::overlay::OverlayId>,
4294 exclude_widget: Option<WidgetId>,
4295 ) -> Option<WidgetId> {
4296 self.hit_test_with(
4297 point,
4298 exclude_overlay,
4299 exclude_widget,
4300 &crate::pointer::hit_slop::HitContext::mouse(),
4301 )
4302 }
4303
4304 pub(crate) fn hit_test_with(
4307 &self,
4308 point: Point,
4309 exclude_overlay: Option<crate::overlay::OverlayId>,
4310 exclude_widget: Option<WidgetId>,
4311 hit: &crate::pointer::hit_slop::HitContext<'_>,
4312 ) -> Option<WidgetId> {
4313 if let Some(overlay_id) = self.overlay_manager.hit_test(point) {
4314 if Some(overlay_id) == exclude_overlay {
4315 } else if let Some(overlay) = self.overlay_manager.overlay(overlay_id) {
4317 if self.overlay_manager.is_modal_scrim(overlay)
4319 && let Some(chrome) = self.window_chrome_under_modal(point, exclude_widget, hit)
4320 {
4321 return Some(chrome);
4322 }
4323 let content_id = overlay.content_id;
4327 if let Some(found) =
4328 self.arena
4329 .hit_test_in_subtree_with_slop(content_id, point, exclude_widget, hit)
4330 {
4331 return Some(found);
4332 }
4333 if !self
4351 .arena
4352 .get(content_id)
4353 .is_some_and(|node| node.event_pass_through)
4354 {
4355 return None;
4356 }
4357 }
4358 }
4359
4360 if self.overlay_manager.topmost_modal().is_some() {
4361 return self.window_chrome_under_modal(point, exclude_widget, hit);
4362 }
4363
4364 let mut roots = self.arena.roots();
4370 let inert: Vec<WidgetId> = self
4371 .overlay_manager
4372 .stack
4373 .iter()
4374 .filter(|overlay| overlay.inert)
4375 .map(|overlay| overlay.content_id)
4376 .collect();
4377 if !inert.is_empty() {
4378 roots.retain(|root| !inert.contains(root));
4379 }
4380 self.arena
4381 .hit_test_roots_with_slop(roots, point, exclude_widget, hit)
4382 }
4383}
4384
4385impl WidgetTree {
4386 fn window_chrome_under_modal(
4401 &self,
4402 point: Point,
4403 exclude_widget: Option<WidgetId>,
4404 hit: &crate::pointer::hit_slop::HitContext<'_>,
4405 ) -> Option<WidgetId> {
4406 let overlays: Vec<WidgetId> = self
4407 .overlay_manager
4408 .stack
4409 .iter()
4410 .map(|overlay| overlay.content_id)
4411 .collect();
4412 let mut roots = self.arena.roots();
4413 roots.retain(|root| !overlays.contains(root));
4414 let found = self
4415 .arena
4416 .hit_test_roots_with_slop(roots, point, exclude_widget, hit)?;
4417 let mut node = Some(found);
4418 while let Some(id) = node {
4419 let current = self.arena.get(id)?;
4420 if current.window_chrome {
4421 return Some(id);
4422 }
4423 if current.gesture_dead_zone {
4424 return None;
4425 }
4426 node = self.arena.parent(id);
4427 }
4428 None
4429 }
4430}
4431
4432fn is_context_menu_chord(key: Key, modifiers: Modifiers) -> bool {
4450 match key {
4451 Key::ContextMenu => modifiers == Modifiers::NONE,
4452 Key::F10 => modifiers == Modifiers::SHIFT,
4453 #[cfg(target_os = "macos")]
4454 Key::M => modifiers == Modifiers::CTRL | Modifiers::SHIFT,
4455 _ => false,
4456 }
4457}
4458
4459#[cfg(test)]
4460mod tests {
4461 use super::*;
4462 use crate::test_widgets::FillWidget;
4463 use crate::widget::CursorIcon;
4464 use crate::widget_builder::WidgetBuilder;
4465
4466 #[test]
4467 fn pointer_enter_leave_synthesized() {
4468 let mut tree = WidgetTree::new();
4469 let widget = tree.add(FillWidget::new());
4470 tree.layout(SizeProposal::exact(100.0, 50.0));
4471 tree.pointer_move(Point::new(50.0, 25.0));
4472 assert_eq!(tree.hovered(), Some(widget));
4473 tree.pointer_move(Point::new(200.0, 200.0));
4474 assert_eq!(tree.hovered(), None);
4475 }
4476
4477 #[test]
4478 fn pointer_hover_updates_current_cursor() {
4479 let mut tree = WidgetTree::new();
4480 tree.add(FillWidget::new().cursor(CursorIcon::ColResize));
4481 tree.layout(SizeProposal::exact(100.0, 50.0));
4482
4483 tree.pointer_move(Point::new(50.0, 25.0));
4484 assert_eq!(tree.current_cursor(), CursorIcon::ColResize);
4485
4486 tree.pointer_move(Point::new(200.0, 200.0));
4487 assert_eq!(tree.current_cursor(), CursorIcon::Default);
4488 }
4489
4490 #[derive(Debug)]
4493 struct SelfDeclared(CursorIcon);
4494
4495 impl crate::widget::Widget for SelfDeclared {
4496 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
4497 ctx.apply_self_handlers(crate::widget_builder::HandlerSet::new().cursor(self.0));
4498 Vec::new()
4499 }
4500
4501 fn layout_response(
4502 &self,
4503 proposal: SizeProposal,
4504 _ctx: &crate::widget::LayoutContext,
4505 ) -> crate::widget::LayoutResponse {
4506 proposal.resolve(100.0, 50.0).into()
4507 }
4508 }
4509
4510 #[test]
4516 fn declared_cursor_reads_what_a_node_asks_for_and_current_cursor_follows_the_hover() {
4517 let over = Point::new(50.0, 25.0);
4518 let away = Point::new(200.0, 200.0);
4519
4520 let mut tree = WidgetTree::new();
4522 let built = tree.add(FillWidget::new().cursor(CursorIcon::ColResize));
4523 tree.layout(SizeProposal::exact(100.0, 50.0));
4524 assert_eq!(tree.declared_cursor(built), Some(CursorIcon::ColResize));
4525 assert_eq!(
4526 tree.current_cursor(),
4527 CursorIcon::Default,
4528 "not hovered yet"
4529 );
4530 tree.pointer_move(over);
4531 assert_eq!(tree.current_cursor(), CursorIcon::ColResize);
4532 assert_eq!(tree.declared_cursor(built), Some(CursorIcon::ColResize));
4533 tree.pointer_move(away);
4534 assert_eq!(tree.current_cursor(), CursorIcon::Default);
4535 assert_eq!(
4536 tree.declared_cursor(built),
4537 Some(CursorIcon::ColResize),
4538 "the declaration does not depend on the pointer",
4539 );
4540
4541 let mut tree = WidgetTree::new();
4543 let own = tree.add(SelfDeclared(CursorIcon::Crosshair));
4544 tree.layout(SizeProposal::exact(100.0, 50.0));
4545 assert_eq!(tree.declared_cursor(own), Some(CursorIcon::Crosshair));
4546 tree.pointer_move(over);
4547 assert_eq!(tree.current_cursor(), CursorIcon::Crosshair);
4548 tree.pointer_move(away);
4549 assert_eq!(tree.current_cursor(), CursorIcon::Default);
4550
4551 let mut tree = WidgetTree::new();
4553 let plain = tree.add(FillWidget::new());
4554 tree.layout(SizeProposal::exact(100.0, 50.0));
4555 assert_eq!(tree.declared_cursor(plain), None);
4556 let mut tree = WidgetTree::new();
4557 let explicit = tree.add(FillWidget::new().cursor(CursorIcon::Default));
4558 tree.layout(SizeProposal::exact(100.0, 50.0));
4559 assert_eq!(
4560 tree.declared_cursor(explicit),
4561 Some(CursorIcon::Default),
4562 "declaring the default cursor is not declaring nothing",
4563 );
4564 }
4565
4566 #[test]
4576 fn release_cursor_hands_the_cursor_back_to_the_node_that_declared_it() {
4577 let mut tree = WidgetTree::new();
4581 tree.add(
4582 FillWidget::new()
4583 .cursor(CursorIcon::ColResize)
4584 .on_pointer_event(|event, ctx| {
4585 if let WidgetEvent::PointerMove { position, .. } = event {
4586 if position.x < 30.0 {
4587 ctx.set_cursor(CursorIcon::Crosshair);
4588 } else {
4589 ctx.release_cursor();
4590 }
4591 }
4592 EventResponse::Ignored
4593 }),
4594 );
4595 tree.layout(SizeProposal::exact(100.0, 50.0));
4596
4597 tree.pointer_move(Point::new(10.0, 25.0));
4598 assert_eq!(
4599 tree.current_cursor(),
4600 CursorIcon::Crosshair,
4601 "the handler outranks the node it is attached to",
4602 );
4603 tree.pointer_move(Point::new(60.0, 25.0));
4604 assert_eq!(
4605 tree.current_cursor(),
4606 CursorIcon::ColResize,
4607 "and hands it back to the node, not to Default — no enter/leave \
4608 fires on a move within one node, so nothing else could",
4609 );
4610 tree.pointer_move(Point::new(10.0, 25.0));
4611 assert_eq!(
4612 tree.current_cursor(),
4613 CursorIcon::Crosshair,
4614 "and can take it again"
4615 );
4616 tree.pointer_move(Point::new(200.0, 200.0));
4617 assert_eq!(
4618 tree.current_cursor(),
4619 CursorIcon::Default,
4620 "leaving the node clears both the override and the declaration",
4621 );
4622 }
4623
4624 #[test]
4628 fn release_cursor_is_a_no_op_with_nothing_to_undo() {
4629 let mut tree = WidgetTree::new();
4630 tree.add(
4631 FillWidget::new()
4632 .cursor(CursorIcon::ColResize)
4633 .on_pointer_event(|event, ctx| {
4634 if matches!(event, WidgetEvent::PointerMove { .. }) {
4635 ctx.release_cursor();
4636 }
4637 EventResponse::Ignored
4638 }),
4639 );
4640 tree.layout(SizeProposal::exact(100.0, 50.0));
4641 tree.pointer_move(Point::new(50.0, 25.0));
4642 assert_eq!(tree.current_cursor(), CursorIcon::ColResize);
4643
4644 let mut tree = WidgetTree::new();
4646 tree.add(FillWidget::new().on_pointer_event(|event, ctx| {
4647 if let WidgetEvent::PointerMove { position, .. } = event {
4648 if position.x < 30.0 {
4649 ctx.set_cursor(CursorIcon::Crosshair);
4650 } else {
4651 ctx.release_cursor();
4652 }
4653 }
4654 EventResponse::Ignored
4655 }));
4656 tree.layout(SizeProposal::exact(100.0, 50.0));
4657 tree.pointer_move(Point::new(10.0, 25.0));
4658 assert_eq!(tree.current_cursor(), CursorIcon::Crosshair);
4659 tree.pointer_move(Point::new(60.0, 25.0));
4660 assert_eq!(
4661 tree.current_cursor(),
4662 CursorIcon::Default,
4663 "nothing declared a cursor for this chain, so the hand-back \
4664 resolves to Default",
4665 );
4666 }
4667
4668 #[test]
4676 fn an_on_hover_override_does_not_erase_the_declaration_it_outranks() {
4677 let mut tree = WidgetTree::new();
4678 tree.add(
4679 FillWidget::new()
4680 .cursor(CursorIcon::ColResize)
4681 .on_hover(|entered, ctx| {
4682 if entered {
4683 ctx.set_cursor(CursorIcon::Crosshair);
4684 }
4685 })
4686 .on_pointer_event(|event, ctx| {
4687 if let WidgetEvent::PointerMove { position, .. } = event
4688 && position.x >= 30.0
4689 {
4690 ctx.release_cursor();
4691 }
4692 EventResponse::Ignored
4693 }),
4694 );
4695 tree.layout(SizeProposal::exact(100.0, 50.0));
4696
4697 tree.pointer_move(Point::new(10.0, 25.0));
4698 assert_eq!(
4699 tree.current_cursor(),
4700 CursorIcon::Crosshair,
4701 "on_hover runs after the node cursor is applied, so it wins",
4702 );
4703 tree.pointer_move(Point::new(60.0, 25.0));
4704 assert_eq!(
4705 tree.current_cursor(),
4706 CursorIcon::ColResize,
4707 "and the node's declaration survived being overridden",
4708 );
4709 }
4710
4711 #[derive(Debug)]
4714 struct Bumpable {
4715 value: i32,
4716 }
4717
4718 impl crate::widget::Widget for Bumpable {
4719 fn layout_response(
4720 &self,
4721 proposal: SizeProposal,
4722 _ctx: &crate::widget::LayoutContext,
4723 ) -> crate::widget::LayoutResponse {
4724 proposal.resolve(10.0, 10.0).into()
4725 }
4726 fn as_any(&self) -> Option<&dyn std::any::Any> {
4727 Some(self)
4728 }
4729 fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
4730 Some(self)
4731 }
4732 }
4733
4734 #[test]
4735 fn with_widget_mut_applies_and_dirty_marks() {
4736 let mut tree = WidgetTree::new();
4737 let id = tree.add(Bumpable { value: 0 });
4738 tree.layout(SizeProposal::exact(100.0, 100.0));
4739
4740 let mut ctx = EventContext::new();
4741 ctx.with_widget_mut::<Bumpable>(id, crate::binding::BindingLevel::Relayout, |b| {
4742 b.value = 42;
4743 });
4744 tree.collect_from_ctx(ctx, id);
4745
4746 let value = tree
4747 .widget_as_any(id)
4748 .and_then(|a| a.downcast_ref::<Bumpable>())
4749 .map(|b| b.value);
4750 assert_eq!(
4751 value,
4752 Some(42),
4753 "the deferred closure must mutate the live widget"
4754 );
4755 assert!(
4756 tree.needs_layout(),
4757 "Relayout dirty level must mark the tree for relayout"
4758 );
4759 }
4760
4761 #[test]
4762 #[cfg(debug_assertions)]
4763 #[should_panic(expected = "not the requested type")]
4764 fn with_widget_mut_wrong_type_panics_in_debug() {
4765 struct Other;
4766 let mut tree = WidgetTree::new();
4767 let id = tree.add(Bumpable { value: 0 });
4768 let mut ctx = EventContext::new();
4769 ctx.with_widget_mut::<Other>(
4770 id,
4771 crate::binding::BindingLevel::RepaintOnly,
4772 |_o: &mut Other| {},
4773 );
4774 tree.collect_from_ctx(ctx, id);
4777 }
4778
4779 #[test]
4780 fn with_widget_mut_closure_may_fire_observed_signals() {
4781 use crate::signal::Signal;
4789 let mut tree = WidgetTree::new();
4790 let id = tree.add(Bumpable { value: 0 });
4791 tree.layout(SizeProposal::exact(100.0, 100.0));
4792
4793 let trigger = Signal::new(0_u64);
4794 let echo = Signal::new(0_u64);
4795 let echo_for_obs = echo.clone();
4796 let _obs = trigger.observe(move |v| echo_for_obs.set(*v));
4797
4798 let trigger_in = trigger.clone();
4799 let mut ctx = EventContext::new();
4800 ctx.with_widget_mut::<Bumpable>(id, crate::binding::BindingLevel::RepaintOnly, move |b| {
4801 b.value = 7;
4802 trigger_in.set(99);
4804 });
4805 tree.collect_from_ctx(ctx, id); assert_eq!(
4808 echo.get(),
4809 99,
4810 "the observer ran during the deferred mutation"
4811 );
4812 let value = tree
4813 .widget_as_any(id)
4814 .and_then(|a| a.downcast_ref::<Bumpable>())
4815 .map(|b| b.value);
4816 assert_eq!(value, Some(7));
4817 }
4818
4819 #[test]
4820 fn request_accessibility_update_forces_rewalk() {
4821 let mut tree = WidgetTree::new();
4822 let id = tree.add(Bumpable { value: 0 });
4823 tree.layout(SizeProposal::exact(100.0, 100.0));
4824 let _ = tree.sync_accessibility(); assert!(
4826 !tree.a11y_dirty,
4827 "sync_accessibility should clear the dirty flag"
4828 );
4829
4830 let mut ctx = EventContext::new();
4831 ctx.request_accessibility_update();
4832 tree.collect_from_ctx(ctx, id);
4833 assert!(
4834 tree.a11y_dirty,
4835 "request_accessibility_update must force an AT re-walk"
4836 );
4837 }
4838
4839 #[test]
4840 fn rebuild_dirties_accessibility_tree() {
4841 let mut tree = WidgetTree::new();
4848 let id = tree.add(Bumpable { value: 0 });
4849 tree.layout(SizeProposal::exact(100.0, 100.0));
4850 let _ = tree.sync_accessibility(); assert!(
4852 !tree.a11y_dirty,
4853 "sync_accessibility should clear the dirty flag"
4854 );
4855
4856 tree.arena_mark_needs_rebuild_for_testing(id);
4859 tree.layout(SizeProposal::exact(100.0, 100.0));
4860 assert!(
4861 tree.a11y_dirty,
4862 "a rebuild must dirty the AT tree so the next sync re-walks"
4863 );
4864 }
4865
4866 #[test]
4867 fn bound_access_label_change_dirties_accessibility_tree() {
4868 use crate::signal::Signal;
4869 use crate::test_widgets::FillWidget;
4870 use crate::widget_builder::WidgetBuilder;
4871
4872 let label = Signal::new("first".to_string());
4878 let mut tree = WidgetTree::new();
4879 let _id = tree.add(FillWidget::new().access_label(label.clone()));
4880 tree.layout(SizeProposal::exact(100.0, 100.0));
4881 let _ = tree.sync_accessibility(); assert!(!tree.a11y_dirty, "sync_accessibility should clear the flag");
4883
4884 label.set("second".to_string());
4885 tree.layout(SizeProposal::exact(100.0, 100.0));
4886 assert!(
4887 tree.a11y_dirty,
4888 "changing a bound access_label must dirty the AT tree"
4889 );
4890 }
4891
4892 #[test]
4893 fn disabled_ancestor_blocks_event_to_descendant() {
4894 use crate::signal::Signal;
4895 use crate::test_widgets::StackWidget;
4896 use std::cell::Cell;
4897 use std::rc::Rc;
4898
4899 let tapped = Rc::new(Cell::new(false));
4900 let flag = tapped.clone();
4901 let enabled = Signal::new(true);
4902
4903 let mut tree = WidgetTree::new();
4904 let child = tree.add(FillWidget::new().on_tap(move |_pos, _ctx| {
4905 flag.set(true);
4906 }));
4907 let parent = tree.add(StackWidget::new().child(child));
4908 tree.enabled_when(parent, enabled.clone());
4909 tree.layout(SizeProposal::exact(100.0, 50.0));
4910
4911 enabled.set(false);
4912 tree.click(child);
4913 assert!(
4914 !tapped.get(),
4915 "disabled ancestor should block descendant tap"
4916 );
4917
4918 enabled.set(true);
4919 tree.click(child);
4920 assert!(tapped.get(), "re-enabling should restore dispatch");
4921 }
4922
4923 #[test]
4924 fn pointer_positions_are_widget_local_at_nonzero_origin() {
4925 use crate::event::{Modifiers, PointerButton};
4926 use crate::test_widgets::InsetWidget;
4927 use std::cell::Cell;
4928 use std::rc::Rc;
4929
4930 let tap_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
4932 let down_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
4933 let drag_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
4934 let (tp, dp, gp) = (tap_pos.clone(), down_pos.clone(), drag_pos.clone());
4935
4936 let mut tree = WidgetTree::new();
4937 let child = tree.add(
4938 FillWidget::new()
4939 .on_tap(move |ev, _ctx| tp.set(Some(ev.position)))
4940 .on_pointer_event(move |ev, _ctx| {
4941 if let WidgetEvent::PointerDown { position, .. } = ev {
4942 dp.set(Some(*position));
4943 }
4944 crate::event::EventResponse::Ignored
4945 })
4946 .on_drag(move |phase, _ctx| {
4947 use crate::gesture::DragPhase;
4948 match phase {
4949 DragPhase::Started { position, .. }
4950 | DragPhase::Moved { position, .. }
4951 | DragPhase::Ended { position, .. } => gp.set(Some(position)),
4952 _ => {}
4953 }
4954 }),
4955 );
4956 let inset = tree.add(InsetWidget::new(20.0).set_child(child));
4957 let _ = inset;
4958 tree.layout(SizeProposal::exact(200.0, 200.0));
4959 assert_eq!(tree.bounds(child).origin(), Point::new(20.0, 20.0));
4960
4961 tree.dispatch_event(WidgetEvent::pointer_down(
4963 Point::new(50.0, 40.0),
4964 PointerButton::Primary,
4965 Modifiers::NONE,
4966 ));
4967 assert_eq!(
4968 down_pos.get(),
4969 Some(Point::new(30.0, 20.0)),
4970 "on_pointer_event PointerDown must be widget-local"
4971 );
4972 tree.dispatch_event(WidgetEvent::pointer_up(
4973 Point::new(50.0, 40.0),
4974 PointerButton::Primary,
4975 Modifiers::NONE,
4976 ));
4977 assert_eq!(
4978 tap_pos.get(),
4979 Some(Point::new(30.0, 20.0)),
4980 "on_tap position must be widget-local"
4981 );
4982
4983 tree.dispatch_event(WidgetEvent::pointer_down(
4986 Point::new(50.0, 40.0),
4987 PointerButton::Primary,
4988 Modifiers::NONE,
4989 ));
4990 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(65.0, 55.0)));
4993 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(90.0, 70.0)));
4994 assert_eq!(
4995 drag_pos.get(),
4996 Some(Point::new(70.0, 50.0)),
4997 "on_drag position must be widget-local"
4998 );
4999 }
5000
5001 #[test]
5002 fn dormant_widget_not_hit_tested() {
5003 let mut tree = WidgetTree::new();
5004 let widget = tree.add(FillWidget::new());
5005 tree.layout(SizeProposal::exact(100.0, 50.0));
5006
5007 tree.pointer_move(Point::new(50.0, 25.0));
5008 assert_eq!(tree.hovered(), Some(widget));
5009
5010 tree.set_dormant(widget);
5011 tree.pointer_move(Point::new(200.0, 200.0));
5012 tree.pointer_move(Point::new(50.0, 25.0));
5013 assert_eq!(tree.hovered(), None);
5014 }
5015
5016 #[test]
5017 fn ancestor_pointer_handler_does_not_suppress_descendant_hover() {
5018 use crate::event::EventResponse;
5019 use crate::test_widgets::StackWidget;
5020 use std::cell::Cell;
5021 use std::rc::Rc;
5022
5023 let hovered = Rc::new(Cell::new(false));
5025 let h = hovered.clone();
5026
5027 let mut tree = WidgetTree::new();
5028 let child = tree.add(FillWidget::new().on_hover(move |entered, _ctx| h.set(entered)));
5029 tree.add(
5034 StackWidget::new()
5035 .child(child)
5036 .on_pointer_event(|_event, _ctx| EventResponse::Handled),
5037 );
5038 tree.layout(SizeProposal::exact(100.0, 50.0));
5039
5040 tree.pointer_move(Point::new(50.0, 25.0));
5041 assert!(
5042 hovered.get(),
5043 "a greedy ancestor on_pointer_event must NOT swallow the child's PointerEnter"
5044 );
5045
5046 tree.pointer_move(Point::new(500.0, 500.0));
5047 assert!(
5048 !hovered.get(),
5049 "PointerLeave must likewise reach the child despite the ancestor"
5050 );
5051 }
5052
5053 #[test]
5061 fn a_child_hover_handler_does_not_swallow_its_ancestors() {
5062 use crate::test_widgets::StackWidget;
5063 use std::cell::Cell;
5064 use std::rc::Rc;
5065
5066 let (row, button) = (Rc::new(Cell::new(false)), Rc::new(Cell::new(false)));
5067 let (r, b) = (row.clone(), button.clone());
5068
5069 let mut tree = WidgetTree::new();
5070 let child = tree.add(FillWidget::new().on_hover(move |entered, _ctx| b.set(entered)));
5071 tree.add(
5072 StackWidget::new()
5073 .child(child)
5074 .on_hover(move |entered, _ctx| r.set(entered)),
5075 );
5076 tree.layout(SizeProposal::exact(100.0, 50.0));
5077
5078 tree.pointer_move(Point::new(50.0, 25.0));
5079 assert!(button.get(), "the child is hovered");
5080 assert!(row.get(), "and so is the row it is inside");
5081
5082 tree.pointer_move(Point::new(500.0, 500.0));
5083 assert!(!button.get(), "the child heard the leave");
5084 assert!(
5085 !row.get(),
5086 "and so did the row — a container is not still hovered because the \
5087 pointer left it through a button"
5088 );
5089 }
5090
5091 #[test]
5098 fn key_preview_consumes_before_focused_on_key() {
5099 use crate::event::EventResponse;
5102 use crate::test_widgets::StackWidget;
5103 use std::cell::Cell;
5104 use std::rc::Rc;
5105
5106 let leaf_fired = Rc::new(Cell::new(false));
5107 let leaf_flag = leaf_fired.clone();
5108 let preview_fired = Rc::new(Cell::new(false));
5109 let preview_flag = preview_fired.clone();
5110
5111 let mut tree = WidgetTree::new();
5112 let leaf = tree.add(FillWidget::new().focusable().on_key(move |event, _c| {
5113 if matches!(event, WidgetEvent::KeyDown { .. }) {
5116 leaf_flag.set(true);
5117 }
5118 EventResponse::Handled
5119 }));
5120 let mid = tree.add(StackWidget::new().child(leaf));
5121 let _root = tree.add(
5122 StackWidget::new()
5123 .child(mid)
5124 .on_key_preview(move |event, _c| match event {
5125 WidgetEvent::KeyDown {
5126 key: Key::Enter, ..
5127 } => {
5128 preview_flag.set(true);
5129 EventResponse::Handled
5130 }
5131 _ => EventResponse::Ignored,
5132 }),
5133 );
5134
5135 tree.layout(SizeProposal::exact(100.0, 50.0));
5136 tree.focus(leaf);
5137 tree.press_key(Key::Enter, Modifiers::NONE);
5138
5139 assert!(
5140 preview_fired.get(),
5141 "ancestor on_key_preview must fire for KeyDown on a focused descendant"
5142 );
5143 assert!(
5144 !leaf_fired.get(),
5145 "consuming the event in preview must prevent the focused widget's on_key from running"
5146 );
5147 }
5148
5149 #[test]
5150 fn key_preview_falls_through_when_returning_ignored() {
5151 use crate::event::EventResponse;
5154 use crate::test_widgets::StackWidget;
5155 use std::cell::Cell;
5156 use std::rc::Rc;
5157
5158 let leaf_fired = Rc::new(Cell::new(false));
5159 let leaf_flag = leaf_fired.clone();
5160 let preview_fired = Rc::new(Cell::new(false));
5161 let preview_flag = preview_fired.clone();
5162
5163 let mut tree = WidgetTree::new();
5164 let leaf = tree.add(FillWidget::new().focusable().on_key(move |_e, _c| {
5165 leaf_flag.set(true);
5166 EventResponse::Handled
5167 }));
5168 let mid = tree.add(StackWidget::new().child(leaf));
5169 let _root = tree.add(
5170 StackWidget::new()
5171 .child(mid)
5172 .on_key_preview(move |_event, _c| {
5173 preview_flag.set(true);
5174 EventResponse::Ignored
5175 }),
5176 );
5177
5178 tree.layout(SizeProposal::exact(100.0, 50.0));
5179 tree.focus(leaf);
5180 tree.press_key(Key::Enter, Modifiers::NONE);
5181
5182 assert!(preview_fired.get(), "preview must always be invoked");
5183 assert!(
5184 leaf_fired.get(),
5185 "preview returning Ignored must not block the focused widget's on_key"
5186 );
5187 }
5188
5189 #[test]
5190 fn key_preview_excludes_focused_target_itself() {
5191 use crate::event::EventResponse;
5195 use std::cell::Cell;
5196 use std::rc::Rc;
5197
5198 let preview_on_target = Rc::new(Cell::new(false));
5199 let pf = preview_on_target.clone();
5200
5201 let mut tree = WidgetTree::new();
5202 let leaf = tree.add(FillWidget::new().focusable().on_key_preview(move |_e, _c| {
5203 pf.set(true);
5204 EventResponse::Handled
5205 }));
5206 tree.layout(SizeProposal::exact(100.0, 50.0));
5207 tree.focus(leaf);
5208 tree.press_key(Key::Enter, Modifiers::NONE);
5209
5210 assert!(
5211 !preview_on_target.get(),
5212 "the focused widget itself must not see its own on_key_preview"
5213 );
5214 }
5215
5216 #[test]
5217 fn key_preview_root_to_target_order() {
5218 use crate::event::EventResponse;
5222 use crate::test_widgets::StackWidget;
5223 use std::cell::RefCell;
5224 use std::rc::Rc;
5225
5226 let order: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
5227 let outer_log = order.clone();
5228 let inner_log = order.clone();
5229
5230 let mut tree = WidgetTree::new();
5231 let leaf = tree.add(FillWidget::new().focusable());
5232 let inner = tree.add(
5233 StackWidget::new()
5234 .child(leaf)
5235 .on_key_preview(move |event, _c| {
5236 if matches!(event, WidgetEvent::KeyDown { .. }) {
5237 inner_log.borrow_mut().push("inner");
5238 }
5239 EventResponse::Ignored
5240 }),
5241 );
5242 let _outer = tree.add(
5243 StackWidget::new()
5244 .child(inner)
5245 .on_key_preview(move |event, _c| {
5246 if matches!(event, WidgetEvent::KeyDown { .. }) {
5247 outer_log.borrow_mut().push("outer");
5248 }
5249 EventResponse::Ignored
5250 }),
5251 );
5252
5253 tree.layout(SizeProposal::exact(100.0, 50.0));
5254 tree.focus(leaf);
5255 tree.dispatch_event(WidgetEvent::KeyDown {
5256 key: Key::Enter,
5257 modifiers: Modifiers::NONE,
5258 text: None,
5259 });
5260
5261 assert_eq!(
5262 *order.borrow(),
5263 vec!["outer", "inner"],
5264 "preview must walk root → parent-of-target"
5265 );
5266 }
5267
5268 #[test]
5269 fn access_action_routes_to_cursored_target_not_focus() {
5270 use crate::signal::Signal;
5274 let a_fired = Signal::new(false);
5275 let b_fired = Signal::new(false);
5276 let a_cb = a_fired.clone();
5277 let b_cb = b_fired.clone();
5278
5279 let mut tree = WidgetTree::new();
5280 let a = tree.add(
5281 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| a_cb.set(true)),
5282 );
5283 let b = tree.add(
5284 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| b_cb.set(true)),
5285 );
5286 tree.layout(SizeProposal::exact(200.0, 100.0));
5287
5288 tree.focus(a);
5289 tree.dispatch_event(WidgetEvent::AccessAction {
5290 action: accesskit::Action::Click,
5291 target: Some(b),
5292 target_node: crate::accessibility::widget_id_to_node_id(b),
5293 data: None,
5294 });
5295
5296 assert!(b_fired.get(), "the cursored target must receive the action");
5297 assert!(
5298 !a_fired.get(),
5299 "the keyboard-focused widget must NOT receive an action targeting another node"
5300 );
5301 }
5302
5303 #[test]
5304 fn access_action_without_target_is_dropped_not_redirected_to_focus() {
5305 use crate::signal::Signal;
5308 let fired = Signal::new(false);
5309 let cb = fired.clone();
5310
5311 let mut tree = WidgetTree::new();
5312 let widget = tree.add(
5313 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| cb.set(true)),
5314 );
5315 tree.layout(SizeProposal::exact(200.0, 100.0));
5316
5317 tree.focus(widget);
5318 tree.dispatch_event(WidgetEvent::AccessAction {
5319 action: accesskit::Action::Click,
5320 target: None,
5321 target_node: crate::accessibility::root_node_id(),
5322 data: None,
5323 });
5324
5325 assert!(
5326 !fired.get(),
5327 "a target-less action must not be redirected to the focused widget"
5328 );
5329 }
5330
5331 #[test]
5338 fn shortcut_fires_matching_action_on_source_widget() {
5339 use crate::action::Action;
5340 use crate::shortcut::{KeyStroke, Shortcut};
5341 use std::cell::Cell;
5342 use std::rc::Rc;
5343
5344 let fired = Rc::new(Cell::new(false));
5345 let fired_flag = fired.clone();
5346
5347 let mut tree = WidgetTree::new();
5348 let widget = tree.add(FillWidget::new().focusable());
5349 tree.push_action(
5350 widget,
5351 Action::new("app.save").on_invoke(move |_intent, _ctx| {
5352 fired_flag.set(true);
5353 }),
5354 );
5355 tree.shortcut_registry_mut().register(
5356 Shortcut::new("app.save")
5357 .primary(KeyStroke::command(Key::S))
5358 .build(),
5359 );
5360
5361 tree.layout(SizeProposal::exact(100.0, 50.0));
5362 tree.focus(widget);
5363
5364 tree.press_key(Key::S, Modifiers::COMMAND);
5365 assert!(fired.get(), "matching action must fire on KeyDown");
5366 }
5367
5368 #[test]
5369 fn global_shortcut_fires_without_focused_widget() {
5370 use crate::action::Action;
5371 use crate::shortcut::{KeyStroke, Shortcut};
5372 use std::cell::Cell;
5373 use std::rc::Rc;
5374
5375 let fired = Rc::new(Cell::new(false));
5379 let fired_flag = fired.clone();
5380
5381 let mut tree = WidgetTree::new();
5382 let root = tree.add(FillWidget::new());
5383 tree.push_action(
5384 root,
5385 Action::new("app.save").on_invoke(move |_intent, _ctx| {
5386 fired_flag.set(true);
5387 }),
5388 );
5389 tree.shortcut_registry_mut().register(
5390 Shortcut::new("app.save")
5391 .primary(KeyStroke::command(Key::S))
5392 .build(),
5393 );
5394
5395 tree.layout(SizeProposal::exact(100.0, 50.0));
5396 tree.press_key(Key::S, Modifiers::COMMAND);
5399 assert!(
5400 fired.get(),
5401 "global shortcut must fire without a focused widget"
5402 );
5403 }
5404
5405 #[test]
5406 fn global_shortcut_fires_after_focused_widget_destroyed() {
5407 use crate::action::Action;
5408 use crate::shortcut::{KeyStroke, Shortcut};
5409 use std::cell::Cell;
5410 use std::rc::Rc;
5411
5412 let fired = Rc::new(Cell::new(false));
5417 let fired_flag = fired.clone();
5418
5419 let mut tree = WidgetTree::new();
5420 let root = tree.add(FillWidget::new());
5421 let focusable = tree.add_child(root, FillWidget::new().focusable());
5422 tree.push_action(
5423 root,
5424 Action::new("app.save").on_invoke(move |_intent, _ctx| {
5425 fired_flag.set(true);
5426 }),
5427 );
5428 tree.shortcut_registry_mut().register(
5429 Shortcut::new("app.save")
5430 .primary(KeyStroke::command(Key::S))
5431 .build(),
5432 );
5433
5434 tree.layout(SizeProposal::exact(100.0, 50.0));
5435 tree.focus(focusable);
5436 assert_eq!(tree.focused(), Some(focusable));
5437
5438 tree.destroy_subtree(focusable);
5441 assert_eq!(tree.focused(), None, "focus must clear when destroyed");
5442
5443 tree.press_key(Key::S, Modifiers::COMMAND);
5444 assert!(
5445 fired.get(),
5446 "global shortcut must still fire after the focused widget is destroyed"
5447 );
5448 }
5449
5450 #[test]
5451 fn scoped_shortcut_matches_only_when_focus_in_scope() {
5452 use crate::action::Action;
5453 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
5454 use std::cell::Cell;
5455 use std::rc::Rc;
5456
5457 let fired = Rc::new(Cell::new(0));
5458 let fired_flag = fired.clone();
5459
5460 let mut tree = WidgetTree::new();
5461 let scope_root = tree.add(FillWidget::new().focusable());
5462 let inside = tree.add_child(scope_root, FillWidget::new().focusable());
5463 let outside = tree.add(FillWidget::new().focusable());
5464
5465 tree.push_action(
5466 scope_root,
5467 Action::new("editor.find").on_invoke(move |_i, _c| {
5468 fired_flag.set(fired_flag.get() + 1);
5469 }),
5470 );
5471 tree.shortcut_registry_mut().register(
5472 Shortcut::new("editor.find")
5473 .primary(KeyStroke::command(Key::F))
5474 .scope(ShortcutScope::Scoped(scope_root))
5475 .build(),
5476 );
5477
5478 tree.layout(SizeProposal::exact(200.0, 100.0));
5479
5480 tree.focus(outside);
5482 tree.press_key(Key::F, Modifiers::COMMAND);
5483 assert_eq!(
5484 fired.get(),
5485 0,
5486 "scoped shortcut must not fire outside scope"
5487 );
5488
5489 tree.focus(inside);
5491 tree.press_key(Key::F, Modifiers::COMMAND);
5492 assert_eq!(
5493 fired.get(),
5494 1,
5495 "scoped shortcut must fire when focus in scope"
5496 );
5497 }
5498
5499 #[test]
5500 fn same_chord_scoped_first_falls_back_to_global_when_focus_outside() {
5501 use crate::action::Action;
5507 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
5508 use std::cell::Cell;
5509 use std::rc::Rc;
5510
5511 let scoped_fired = Rc::new(Cell::new(0));
5512 let global_fired = Rc::new(Cell::new(0));
5513 let sf = scoped_fired.clone();
5514 let gf = global_fired.clone();
5515
5516 let mut tree = WidgetTree::new();
5517 let root = tree.add(FillWidget::new());
5518 let editor = tree.add_child(root, FillWidget::new().focusable());
5519 let _editor_inner = tree.add_child(editor, FillWidget::new().focusable());
5520 let sidebar = tree.add_child(root, FillWidget::new().focusable());
5521
5522 tree.push_action(
5523 editor,
5524 Action::new("editor.saveBlock").on_invoke(move |_i, _c| sf.set(sf.get() + 1)),
5525 );
5526 tree.push_action(
5527 root,
5528 Action::new("zzz.global.save").on_invoke(move |_i, _c| gf.set(gf.get() + 1)),
5529 );
5530 tree.shortcut_registry_mut().register(
5531 Shortcut::new("editor.saveBlock")
5532 .primary(KeyStroke::command(Key::S))
5533 .scope(ShortcutScope::Scoped(editor))
5534 .build(),
5535 );
5536 tree.shortcut_registry_mut().register(
5537 Shortcut::new("zzz.global.save")
5538 .primary(KeyStroke::command(Key::S))
5539 .build(),
5540 );
5541
5542 tree.layout(SizeProposal::exact(200.0, 100.0));
5543 tree.focus(sidebar);
5544 tree.press_key(Key::S, Modifiers::COMMAND);
5545
5546 assert_eq!(global_fired.get(), 1, "applicable global must fire");
5547 assert_eq!(
5548 scoped_fired.get(),
5549 0,
5550 "inapplicable scoped binding must not eat the chord"
5551 );
5552 }
5553
5554 #[test]
5555 fn same_chord_global_first_yields_to_scoped_when_focus_inside() {
5556 use crate::action::Action;
5561 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
5562 use std::cell::Cell;
5563 use std::rc::Rc;
5564
5565 let scoped_fired = Rc::new(Cell::new(0));
5566 let global_fired = Rc::new(Cell::new(0));
5567 let sf = scoped_fired.clone();
5568 let gf = global_fired.clone();
5569
5570 let mut tree = WidgetTree::new();
5571 let root = tree.add(FillWidget::new());
5572 let editor = tree.add_child(root, FillWidget::new().focusable());
5573 let editor_inner = tree.add_child(editor, FillWidget::new().focusable());
5574 let sidebar = tree.add_child(root, FillWidget::new().focusable());
5575
5576 tree.push_action(
5577 editor,
5578 Action::new("editor.saveBlock").on_invoke(move |_i, _c| sf.set(sf.get() + 1)),
5579 );
5580 tree.push_action(
5581 root,
5582 Action::new("app.save").on_invoke(move |_i, _c| gf.set(gf.get() + 1)),
5583 );
5584 tree.shortcut_registry_mut().register(
5585 Shortcut::new("app.save")
5586 .primary(KeyStroke::command(Key::S))
5587 .build(),
5588 );
5589 tree.shortcut_registry_mut().register(
5590 Shortcut::new("editor.saveBlock")
5591 .primary(KeyStroke::command(Key::S))
5592 .scope(ShortcutScope::Scoped(editor))
5593 .build(),
5594 );
5595
5596 tree.layout(SizeProposal::exact(200.0, 100.0));
5597
5598 tree.focus(editor_inner);
5600 tree.press_key(Key::S, Modifiers::COMMAND);
5601 assert_eq!(
5602 scoped_fired.get(),
5603 1,
5604 "in-focus scoped must win over global"
5605 );
5606 assert_eq!(
5607 global_fired.get(),
5608 0,
5609 "global must yield to the scoped binding"
5610 );
5611
5612 tree.focus(sidebar);
5614 tree.press_key(Key::S, Modifiers::COMMAND);
5615 assert_eq!(scoped_fired.get(), 1, "scoped stays put outside its scope");
5616 assert_eq!(
5617 global_fired.get(),
5618 1,
5619 "global fires when focus leaves the scope"
5620 );
5621 }
5622
5623 #[test]
5624 fn propagated_action_lets_ancestor_handle() {
5625 use crate::action::Action;
5626 use crate::intent::IntentResponse;
5627 use crate::shortcut::{KeyStroke, Shortcut};
5628 use std::cell::Cell;
5629 use std::rc::Rc;
5630
5631 let inner_seen = Rc::new(Cell::new(false));
5632 let outer_seen = Rc::new(Cell::new(false));
5633 let inner_flag = inner_seen.clone();
5634 let outer_flag = outer_seen.clone();
5635
5636 let mut tree = WidgetTree::new();
5637 let outer = tree.add(FillWidget::new().focusable());
5638 let inner = tree.add_child(outer, FillWidget::new().focusable());
5639
5640 tree.push_action(
5642 inner,
5643 Action::new("app.save").on_invoke_with_response(move |_i, _c| {
5644 inner_flag.set(true);
5645 IntentResponse::Propagated
5646 }),
5647 );
5648 tree.push_action(
5649 outer,
5650 Action::new("app.save").on_invoke(move |_i, _c| {
5651 outer_flag.set(true);
5652 }),
5653 );
5654 tree.shortcut_registry_mut().register(
5655 Shortcut::new("app.save")
5656 .primary(KeyStroke::command(Key::S))
5657 .build(),
5658 );
5659
5660 tree.layout(SizeProposal::exact(100.0, 50.0));
5661 tree.focus(inner);
5662
5663 tree.press_key(Key::S, Modifiers::COMMAND);
5664 assert!(inner_seen.get(), "inner action observed the intent");
5665 assert!(outer_seen.get(), "outer action reached after Propagated");
5666 }
5667
5668 #[test]
5669 fn handled_action_stops_propagation() {
5670 use crate::action::Action;
5671 use crate::shortcut::{KeyStroke, Shortcut};
5672 use std::cell::Cell;
5673 use std::rc::Rc;
5674
5675 let inner_seen = Rc::new(Cell::new(false));
5676 let outer_seen = Rc::new(Cell::new(false));
5677 let inner_flag = inner_seen.clone();
5678 let outer_flag = outer_seen.clone();
5679
5680 let mut tree = WidgetTree::new();
5681 let outer = tree.add(FillWidget::new().focusable());
5682 let inner = tree.add_child(outer, FillWidget::new().focusable());
5683
5684 tree.push_action(
5685 inner,
5686 Action::new("app.save").on_invoke(move |_i, _c| {
5687 inner_flag.set(true);
5688 }),
5689 );
5690 tree.push_action(
5691 outer,
5692 Action::new("app.save").on_invoke(move |_i, _c| {
5693 outer_flag.set(true);
5694 }),
5695 );
5696 tree.shortcut_registry_mut().register(
5697 Shortcut::new("app.save")
5698 .primary(KeyStroke::command(Key::S))
5699 .build(),
5700 );
5701
5702 tree.layout(SizeProposal::exact(100.0, 50.0));
5703 tree.focus(inner);
5704
5705 tree.press_key(Key::S, Modifiers::COMMAND);
5706 assert!(inner_seen.get());
5707 assert!(!outer_seen.get(), "Handled at inner must stop propagation");
5708 }
5709
5710 #[test]
5711 fn disabled_action_propagates_by_default() {
5712 use crate::action::Action;
5713 use crate::shortcut::{KeyStroke, Shortcut};
5714 use crate::signal::Signal;
5715 use std::cell::Cell;
5716 use std::rc::Rc;
5717
5718 let inner_seen = Rc::new(Cell::new(false));
5719 let outer_seen = Rc::new(Cell::new(false));
5720 let inner_flag = inner_seen.clone();
5721 let outer_flag = outer_seen.clone();
5722
5723 let mut tree = WidgetTree::new();
5724 let outer = tree.add(FillWidget::new().focusable());
5725 let inner = tree.add_child(outer, FillWidget::new().focusable());
5726
5727 let enabled = Signal::new(false);
5728 tree.push_action(
5729 inner,
5730 Action::new("app.save")
5731 .enabled_when(enabled.clone())
5732 .on_invoke(move |_i, _c| {
5733 inner_flag.set(true);
5734 }),
5735 );
5736 tree.push_action(
5737 outer,
5738 Action::new("app.save").on_invoke(move |_i, _c| {
5739 outer_flag.set(true);
5740 }),
5741 );
5742 tree.shortcut_registry_mut().register(
5743 Shortcut::new("app.save")
5744 .primary(KeyStroke::command(Key::S))
5745 .build(),
5746 );
5747
5748 tree.layout(SizeProposal::exact(100.0, 50.0));
5749 tree.focus(inner);
5750
5751 tree.press_key(Key::S, Modifiers::COMMAND);
5752 assert!(!inner_seen.get(), "disabled inner must not run");
5753 assert!(
5754 outer_seen.get(),
5755 "intent must propagate past disabled inner"
5756 );
5757 }
5758
5759 #[test]
5760 fn disabled_action_with_non_propagating_shortcut_consumes() {
5761 use crate::action::Action;
5762 use crate::shortcut::{KeyStroke, Shortcut};
5763 use crate::signal::Signal;
5764 use std::cell::Cell;
5765 use std::rc::Rc;
5766
5767 let inner_seen = Rc::new(Cell::new(false));
5768 let outer_seen = Rc::new(Cell::new(false));
5769 let inner_flag = inner_seen.clone();
5770 let outer_flag = outer_seen.clone();
5771
5772 let mut tree = WidgetTree::new();
5773 let outer = tree.add(FillWidget::new().focusable());
5774 let inner = tree.add_child(outer, FillWidget::new().focusable());
5775
5776 let enabled = Signal::new(false);
5777 tree.push_action(
5778 inner,
5779 Action::new("app.save")
5780 .enabled_when(enabled.clone())
5781 .on_invoke(move |_i, _c| {
5782 inner_flag.set(true);
5783 }),
5784 );
5785 tree.push_action(
5786 outer,
5787 Action::new("app.save").on_invoke(move |_i, _c| {
5788 outer_flag.set(true);
5789 }),
5790 );
5791 tree.shortcut_registry_mut().register(
5792 Shortcut::new("app.save")
5793 .primary(KeyStroke::command(Key::S))
5794 .propagate_when_disabled(false)
5795 .build(),
5796 );
5797
5798 tree.layout(SizeProposal::exact(100.0, 50.0));
5799 tree.focus(inner);
5800
5801 tree.press_key(Key::S, Modifiers::COMMAND);
5802 assert!(!inner_seen.get(), "disabled inner still does not run");
5803 assert!(
5804 !outer_seen.get(),
5805 "intent must NOT propagate when shortcut disallows it"
5806 );
5807 }
5808
5809 #[test]
5810 fn send_intent_from_handler_reaches_ancestor_action() {
5811 use crate::action::Action;
5812 use crate::intent::Intent;
5813 use std::cell::Cell;
5814 use std::rc::Rc;
5815
5816 let save_seen = Rc::new(Cell::new(false));
5817 let save_flag = save_seen.clone();
5818
5819 let mut tree = WidgetTree::new();
5820 let root = tree.add(FillWidget::new());
5821 let button = tree.add_child(
5822 root,
5823 FillWidget::new().on_tap(|_pos, ctx| {
5824 ctx.send_intent(Intent::new("app.save"));
5825 }),
5826 );
5827 tree.push_action(
5828 root,
5829 Action::new("app.save").on_invoke(move |_i, _c| {
5830 save_flag.set(true);
5831 }),
5832 );
5833
5834 tree.layout(SizeProposal::exact(100.0, 50.0));
5835 tree.click(button);
5836 assert!(
5837 save_seen.get(),
5838 "ctx.send_intent must reach ancestor action"
5839 );
5840 }
5841
5842 #[test]
5843 fn widget_type_histogram_counts_distinct_types() {
5844 let mut tree = WidgetTree::new();
5849 let _ = tree.add(FillWidget::new());
5850 let _ = tree.add(FillWidget::new());
5851 let _ = tree.add(FillWidget::new());
5852 tree.layout(SizeProposal::exact(100.0, 100.0));
5853 let histogram = tree.widget_type_histogram();
5854 let total: u32 = histogram.values().sum();
5855 assert!(
5856 total >= 3,
5857 "expected at least 3 active widgets, got {total}: {histogram:?}"
5858 );
5859 let fillwidget_entries: u32 = histogram
5860 .iter()
5861 .filter(|(k, _)| k.contains("FillWidget"))
5862 .map(|(_, v)| *v)
5863 .sum();
5864 assert!(
5865 fillwidget_entries >= 3,
5866 "expected ≥3 FillWidget instances; histogram = {histogram:?}"
5867 );
5868 assert_eq!(tree.active_widget_count() as u32, total);
5869 }
5870
5871 #[test]
5872 fn intent_source_tagged_handler_for_tap_activation() {
5873 use crate::action::Action;
5877 use crate::intent::Intent;
5878 use crate::telemetry::IntentSource;
5879 use std::cell::Cell;
5880 use std::rc::Rc;
5881 let captured = Rc::new(Cell::new(IntentSource::Unknown));
5882 let captured_for_action = captured.clone();
5883
5884 let mut tree = WidgetTree::new();
5885 let root = tree.add(FillWidget::new());
5886 let button = tree.add_child(
5887 root,
5888 FillWidget::new().on_tap(|_pos, ctx| {
5889 ctx.send_intent(Intent::new("app.save"));
5890 }),
5891 );
5892 tree.push_action(
5893 root,
5894 Action::new("app.save").on_invoke(move |intent, _c| {
5895 captured_for_action.set(intent.source);
5896 }),
5897 );
5898
5899 tree.layout(SizeProposal::exact(100.0, 50.0));
5900 tree.click(button);
5901 assert_eq!(
5902 captured.get(),
5903 IntentSource::Handler,
5904 "tap-driven intent must tag IntentSource::Handler"
5905 );
5906 }
5907
5908 #[test]
5909 fn intent_source_programmatic_when_no_handler_active() {
5910 use crate::intent::Intent;
5911 use crate::telemetry::IntentSource;
5912 let intent = Intent::new("app.demo");
5913 assert_eq!(intent.source, IntentSource::Programmatic);
5914
5915 let mut ctx = EventContext::new();
5917 ctx.send_intent(Intent::new("app.demo"));
5918 let queued = ctx.pending_intents.first().expect("intent queued");
5919 assert_eq!(queued.source, IntentSource::Programmatic);
5920 }
5921
5922 #[test]
5923 fn with_intent_source_overrides_for_managed_widgets() {
5924 use crate::intent::Intent;
5925 use crate::telemetry::IntentSource;
5926 let mut ctx = EventContext::new();
5927 ctx.with_intent_source(IntentSource::Menu, |ctx| {
5928 ctx.send_intent(Intent::new("app.demo"));
5929 });
5930 let queued = ctx.pending_intents.first().expect("intent queued");
5931 assert_eq!(
5932 queued.source,
5933 IntentSource::Menu,
5934 "with_intent_source(Menu) must tag the dispatched intent"
5935 );
5936
5937 ctx.send_intent(Intent::new("app.next"));
5941 let next = ctx.pending_intents.last().expect("second intent");
5942 assert_eq!(next.source, IntentSource::Programmatic);
5943 }
5944
5945 #[test]
5946 fn disabled_shortcut_falls_through_to_focused_widget() {
5947 use crate::action::Action;
5948 use crate::shortcut::{KeyStroke, Shortcut};
5949 use crate::signal::Signal;
5950 use std::cell::Cell;
5951 use std::rc::Rc;
5952
5953 let action_fired = Rc::new(Cell::new(false));
5954 let on_key_fired = Rc::new(Cell::new(false));
5955 let af = action_fired.clone();
5956 let kf = on_key_fired.clone();
5957
5958 let enabled = Signal::new(false);
5959
5960 let mut tree = WidgetTree::new();
5961 let widget = tree.add(FillWidget::new().focusable().on_key(move |event, _ctx| {
5962 if matches!(
5963 event,
5964 WidgetEvent::KeyDown {
5965 key: Key::S,
5966 modifiers,
5967 ..
5968 } if modifiers.command()
5969 ) {
5970 kf.set(true);
5971 return EventResponse::Handled;
5972 }
5973 EventResponse::Ignored
5974 }));
5975 tree.push_action(
5976 widget,
5977 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
5978 );
5979 tree.shortcut_registry_mut().register(
5980 Shortcut::new("app.save")
5981 .primary(KeyStroke::command(Key::S))
5982 .enabled_when(enabled.clone())
5983 .build(),
5984 );
5985
5986 tree.layout(SizeProposal::exact(100.0, 50.0));
5987 tree.focus(widget);
5988
5989 tree.press_key(Key::S, Modifiers::COMMAND);
5991 assert!(
5992 !action_fired.get(),
5993 "disabled shortcut must not invoke its action"
5994 );
5995 assert!(
5996 on_key_fired.get(),
5997 "disabled shortcut must let KeyDown reach the focused widget"
5998 );
5999
6000 on_key_fired.set(false);
6002 enabled.set(true);
6003 tree.press_key(Key::S, Modifiers::COMMAND);
6004 assert!(action_fired.get(), "re-enabled shortcut must dispatch");
6005 assert!(
6006 !on_key_fired.get(),
6007 "enabled shortcut must consume the KeyDown"
6008 );
6009 }
6010
6011 #[test]
6012 fn keyboard_capture_bypasses_shortcut() {
6013 use crate::action::Action;
6014 use crate::shortcut::{KeyStroke, Shortcut};
6015 use std::cell::Cell;
6016 use std::rc::Rc;
6017
6018 fn run(capture: bool) -> (bool, bool) {
6024 let action_fired = Rc::new(Cell::new(false));
6025 let on_key_fired = Rc::new(Cell::new(false));
6026 let af = action_fired.clone();
6027 let kf = on_key_fired.clone();
6028
6029 let mut tree = WidgetTree::new();
6030 let widget = tree.add(
6031 FillWidget::new()
6032 .focusable()
6033 .keyboard_capture(capture)
6034 .on_key(move |event, _ctx| {
6035 if matches!(
6036 event,
6037 WidgetEvent::KeyDown { key: Key::S, modifiers, .. } if modifiers.command()
6038 ) {
6039 kf.set(true);
6040 return EventResponse::Handled;
6041 }
6042 EventResponse::Ignored
6043 }),
6044 );
6045 tree.push_action(
6046 widget,
6047 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
6048 );
6049 tree.shortcut_registry_mut().register(
6050 Shortcut::new("app.save")
6051 .primary(KeyStroke::command(Key::S))
6052 .build(),
6053 );
6054
6055 tree.layout(SizeProposal::exact(100.0, 50.0));
6056 tree.focus(widget);
6057 tree.press_key(Key::S, Modifiers::COMMAND);
6058 (action_fired.get(), on_key_fired.get())
6059 }
6060
6061 let (action, on_key) = run(true);
6063 assert!(
6064 !action,
6065 "keyboard_capture must suppress the shortcut action"
6066 );
6067 assert!(on_key, "keyboard_capture must deliver the raw KeyDown");
6068
6069 let (action, on_key) = run(false);
6071 assert!(action, "without capture the shortcut must fire");
6072 assert!(!on_key, "without capture the widget must not see the key");
6073 }
6074
6075 fn key_recorder(
6077 tree: &mut WidgetTree,
6078 ) -> (WidgetId, std::rc::Rc<std::cell::RefCell<Vec<Key>>>) {
6079 let seen = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
6080 let log = seen.clone();
6081 let widget = tree.add(FillWidget::new().focusable().on_key(move |event, _ctx| {
6082 if let WidgetEvent::KeyDown { key, .. } = event {
6083 log.borrow_mut().push(*key);
6084 }
6085 EventResponse::Ignored
6086 }));
6087 tree.layout(SizeProposal::exact(100.0, 50.0));
6088 tree.focus(widget);
6089 (widget, seen)
6090 }
6091
6092 #[test]
6093 fn a_shortcut_no_action_takes_leaves_its_key_to_the_focused_widget() {
6094 use crate::action::Action;
6095 use crate::intent::IntentResponse;
6096 use crate::shortcut::{KeyStroke, Shortcut};
6097
6098 let mut tree = WidgetTree::new();
6099 let (widget, seen) = key_recorder(&mut tree);
6100 tree.shortcut_registry_mut().register(
6101 Shortcut::new("app.save")
6102 .primary(KeyStroke::command(Key::S))
6103 .build(),
6104 );
6105
6106 tree.press_key(Key::S, Modifiers::COMMAND);
6108 assert_eq!(
6109 *seen.borrow(),
6110 vec![Key::S],
6111 "an unhandled intent must not eat the key"
6112 );
6113
6114 tree.push_action(
6116 widget,
6117 Action::new("app.save").on_invoke_with_response(|_i, _c| IntentResponse::Propagated),
6118 );
6119 tree.press_key(Key::S, Modifiers::COMMAND);
6120 assert_eq!(
6121 seen.borrow().len(),
6122 2,
6123 "a propagated intent must leave the key too"
6124 );
6125
6126 let mut tree = WidgetTree::new();
6129 let (widget, seen) = key_recorder(&mut tree);
6130 tree.shortcut_registry_mut().register(
6131 Shortcut::new("app.save")
6132 .primary(KeyStroke::command(Key::S))
6133 .build(),
6134 );
6135 tree.push_action(widget, Action::new("app.save").on_invoke(|_i, _c| {}));
6136 tree.press_key(Key::S, Modifiers::COMMAND);
6137 assert!(
6138 seen.borrow().is_empty(),
6139 "a handled shortcut consumes its key"
6140 );
6141 }
6142
6143 #[test]
6144 fn standard_edit_chords_follow_a_rebind() {
6145 use crate::action::Action;
6146 use crate::shortcut::{KeyStroke, StandardEdit};
6147 use std::cell::Cell;
6148 use std::rc::Rc;
6149
6150 let mut tree = WidgetTree::new();
6151 let (widget, seen) = key_recorder(&mut tree);
6152 let copies = Rc::new(Cell::new(0));
6153 let count = copies.clone();
6154 tree.push_action(
6155 widget,
6156 Action::new(StandardEdit::Copy.id())
6157 .on_invoke(move |_i, _c| count.set(count.get() + 1)),
6158 );
6159
6160 tree.press_key(Key::C, Modifiers::COMMAND);
6162 assert_eq!(copies.get(), 1, "Ctrl+C reaches the edit.copy action");
6163 assert!(seen.borrow().is_empty());
6164
6165 let elsewhere = KeyStroke::new(Key::Character('à'), Modifiers::CTRL);
6168 tree.shortcut_registry_mut()
6169 .rebind_primary(StandardEdit::Copy.id(), Some(elsewhere));
6170 tree.press_key(Key::C, Modifiers::COMMAND);
6171 assert_eq!(copies.get(), 1, "the replaced chord must not copy");
6172 assert_eq!(*seen.borrow(), vec![Key::C]);
6173 tree.press_key(Key::Character('à'), Modifiers::CTRL);
6174 assert_eq!(copies.get(), 2, "the new chord copies");
6175 }
6176
6177 #[test]
6178 fn ctrl_tab_always_escapes_a_keyboard_capture_surface() {
6179 use std::cell::Cell;
6180 use std::rc::Rc;
6181
6182 let saw_key = Rc::new(Cell::new(false));
6188 let sk = saw_key.clone();
6189
6190 let mut tree = WidgetTree::new();
6191 let capture = tree.add(
6192 FillWidget::new()
6193 .focusable()
6194 .keyboard_capture(true)
6195 .on_key(move |_event, _ctx| {
6197 sk.set(true);
6198 EventResponse::Handled
6199 }),
6200 );
6201 let neighbour = tree.add(FillWidget::new().focusable());
6202 tree.layout(SizeProposal::exact(100.0, 50.0));
6203
6204 tree.focus(capture);
6207 tree.press_key(Key::Tab, Modifiers::NONE);
6208 assert!(saw_key.get(), "plain Tab must reach the capture surface");
6209 assert_eq!(
6210 tree.focused(),
6211 Some(capture),
6212 "plain Tab must not move focus off a capture surface"
6213 );
6214
6215 saw_key.set(false);
6217 tree.press_key(Key::Tab, Modifiers::CTRL);
6218 assert!(
6219 !saw_key.get(),
6220 "Ctrl+Tab is reserved and must not reach the capture surface"
6221 );
6222 assert_eq!(
6223 tree.focused(),
6224 Some(neighbour),
6225 "Ctrl+Tab must move focus out of a capture surface"
6226 );
6227
6228 tree.focus(capture);
6230 tree.press_key(Key::Tab, Modifiers::CTRL | Modifiers::SHIFT);
6231 assert_eq!(
6232 tree.focused(),
6233 Some(neighbour),
6234 "Ctrl+Shift+Tab must move focus out of a capture surface"
6235 );
6236 }
6237
6238 #[test]
6239 fn scope_mismatch_does_not_invoke_on_activate() {
6240 use crate::intent::Intent;
6241 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
6242 use std::cell::Cell;
6243 use std::rc::Rc;
6244
6245 let activated = Rc::new(Cell::new(false));
6251 let activated_flag = activated.clone();
6252
6253 let mut tree = WidgetTree::new();
6254 let scope_root = tree.add(FillWidget::new().focusable());
6255 let outside = tree.add(FillWidget::new().focusable());
6256
6257 tree.shortcut_registry_mut().register(
6258 Shortcut::new("editor.find")
6259 .primary(KeyStroke::command(Key::F))
6260 .scope(ShortcutScope::Scoped(scope_root))
6261 .on_activate(move |_ks, _ctx| {
6262 activated_flag.set(true);
6263 Intent::new("editor.find")
6264 })
6265 .build(),
6266 );
6267
6268 tree.layout(SizeProposal::exact(200.0, 100.0));
6269 tree.focus(outside);
6270
6271 tree.press_key(Key::F, Modifiers::COMMAND);
6272 assert!(
6273 !activated.get(),
6274 "on_activate must not run when focus is outside the shortcut's scope"
6275 );
6276 }
6277
6278 #[test]
6279 fn key_capture_runs_callback_and_bypasses_registry() {
6280 use crate::action::Action;
6281 use crate::shortcut::{KeyStroke, Shortcut};
6282 use std::cell::Cell;
6283 use std::rc::Rc;
6284
6285 let action_fired = Rc::new(Cell::new(false));
6286 let af = action_fired.clone();
6287 let captured = Rc::new(Cell::new(None));
6288 let cf = captured.clone();
6289
6290 let mut tree = WidgetTree::new();
6291 let widget = tree.add(FillWidget::new().focusable());
6292 tree.push_action(
6293 widget,
6294 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
6295 );
6296 tree.shortcut_registry_mut().register(
6297 Shortcut::new("app.save")
6298 .primary(KeyStroke::command(Key::S))
6299 .build(),
6300 );
6301
6302 tree.layout(SizeProposal::exact(100.0, 50.0));
6303 tree.focus(widget);
6304
6305 let handle = tree.begin_key_capture(move |ks, _reg, _ctx| cf.set(Some(ks)));
6306 assert!(tree.is_capturing_keys());
6307
6308 tree.press_key(Key::S, Modifiers::COMMAND);
6309 assert_eq!(
6310 captured.get(),
6311 Some(KeyStroke::command(Key::S)),
6312 "capture callback must receive the chord"
6313 );
6314 assert!(
6315 !action_fired.get(),
6316 "shortcut action must not fire while capture is armed"
6317 );
6318 assert!(
6319 !tree.is_capturing_keys(),
6320 "capture is one-shot; next KeyDown flows normally"
6321 );
6322 drop(handle);
6323 }
6324
6325 #[test]
6326 fn key_capture_can_rebind_through_registry() {
6327 use crate::shortcut::{KeyStroke, Shortcut};
6328
6329 let mut tree = WidgetTree::new();
6330 let widget = tree.add(FillWidget::new().focusable());
6331 tree.shortcut_registry_mut().register(
6332 Shortcut::new("app.save")
6333 .primary(KeyStroke::command(Key::S))
6334 .build(),
6335 );
6336
6337 tree.layout(SizeProposal::exact(100.0, 50.0));
6338 tree.focus(widget);
6339
6340 let _h = tree.begin_key_capture(|ks, reg, _ctx| {
6342 reg.rebind_primary("app.save", Some(ks));
6343 });
6344
6345 tree.press_key(Key::B, Modifiers::COMMAND | Modifiers::SHIFT);
6346 assert_eq!(
6347 tree.shortcut_registry()
6348 .effective("app.save")
6349 .unwrap()
6350 .primary,
6351 Some(KeyStroke::command_shift(Key::B))
6352 );
6353 }
6354
6355 #[test]
6356 fn dropping_capture_handle_cancels_capture() {
6357 use crate::shortcut::{KeyStroke, Shortcut};
6358 use std::cell::Cell;
6359 use std::rc::Rc;
6360
6361 let action_fired = Rc::new(Cell::new(false));
6362 let af = action_fired.clone();
6363 let capture_fired = Rc::new(Cell::new(false));
6364 let cf = capture_fired.clone();
6365
6366 let mut tree = WidgetTree::new();
6367 let widget = tree.add(FillWidget::new().focusable());
6368 tree.push_action(
6369 widget,
6370 crate::action::Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
6371 );
6372 tree.shortcut_registry_mut().register(
6373 Shortcut::new("app.save")
6374 .primary(KeyStroke::command(Key::S))
6375 .build(),
6376 );
6377 tree.layout(SizeProposal::exact(100.0, 50.0));
6378 tree.focus(widget);
6379
6380 {
6384 let _h = tree.begin_key_capture(move |_ks, _reg, _ctx| cf.set(true));
6385 assert!(tree.is_capturing_keys());
6386 }
6388 assert!(
6389 !tree.is_capturing_keys(),
6390 "dropping the handle must cancel the capture"
6391 );
6392
6393 tree.press_key(Key::S, Modifiers::COMMAND);
6394 assert!(!capture_fired.get(), "cancelled capture must not fire");
6395 assert!(
6396 action_fired.get(),
6397 "shortcut action runs after capture was cancelled"
6398 );
6399 }
6400
6401 #[test]
6402 fn second_begin_key_capture_does_not_racecancel_first() {
6403 use std::cell::Cell;
6404 use std::rc::Rc;
6405
6406 let first = Rc::new(Cell::new(false));
6407 let second = Rc::new(Cell::new(false));
6408 let f = first.clone();
6409 let s = second.clone();
6410
6411 let mut tree = WidgetTree::new();
6412 let widget = tree.add(FillWidget::new().focusable());
6413 tree.layout(SizeProposal::exact(100.0, 50.0));
6414 tree.focus(widget);
6415
6416 let h1 = tree.begin_key_capture(move |_ks, _reg, _ctx| f.set(true));
6422 let _h2 = tree.begin_key_capture(move |_ks, _reg, _ctx| s.set(true));
6423 drop(h1);
6424
6425 assert!(
6426 tree.is_capturing_keys(),
6427 "dropping the older handle must not cancel the active capture"
6428 );
6429 tree.press_key(Key::K, Modifiers::COMMAND);
6430 assert!(!first.get());
6431 assert!(second.get(), "newest capture wins");
6432 }
6433
6434 #[test]
6435 fn capture_callback_can_send_intent() {
6436 use crate::action::Action;
6437 use crate::intent::Intent;
6438
6439 use std::cell::Cell;
6440 use std::rc::Rc;
6441
6442 let ran = Rc::new(Cell::new(false));
6443 let flag = ran.clone();
6444
6445 let mut tree = WidgetTree::new();
6446 let widget = tree.add(FillWidget::new().focusable());
6447 tree.push_action(
6448 widget,
6449 Action::new("app.save").on_invoke(move |_i, _c| flag.set(true)),
6450 );
6451 tree.layout(SizeProposal::exact(100.0, 50.0));
6452 tree.focus(widget);
6453
6454 let _h = tree.begin_key_capture(|_ks, _reg, ctx| {
6455 ctx.send_intent(Intent::new("app.save"));
6456 });
6457 tree.press_key(Key::X, Modifiers::COMMAND);
6458 assert!(
6459 ran.get(),
6460 "intent queued from capture callback must dispatch"
6461 );
6462 }
6463
6464 #[test]
6465 fn binding_registry_does_not_accumulate_across_rebuilds() {
6466 use crate::binding::BindingLevel;
6467 use crate::signal::Signal;
6468
6469 #[derive(Debug)]
6470 struct BoundLeaf {
6471 tick: Signal<u64>,
6472 }
6473 impl crate::widget::Widget for BoundLeaf {
6474 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6475 self.tick.bind_to(
6476 ctx.self_id(),
6477 ctx.binding_registry(),
6478 BindingLevel::Relayout,
6479 );
6480 Vec::new()
6481 }
6482 fn layout_response(
6483 &self,
6484 proposal: SizeProposal,
6485 _ctx: &crate::widget::LayoutContext,
6486 ) -> crate::widget::LayoutResponse {
6487 proposal.resolve(10.0, 10.0).into()
6488 }
6489 }
6490
6491 let mut tree = WidgetTree::new();
6492 let tick = Signal::new(0_u64);
6493 let widget = tree.add(BoundLeaf { tick: tick.clone() });
6494 tree.layout(SizeProposal::exact(200.0, 200.0));
6495 let after_first_build = tree.binding_registry().len();
6496 assert!(after_first_build >= 1);
6497
6498 for _ in 0..5 {
6502 tree.arena.mark_needs_rebuild(widget);
6503 tree.layout(SizeProposal::exact(200.0, 200.0));
6504 }
6505 assert_eq!(
6506 tree.binding_registry().len(),
6507 after_first_build,
6508 "bindings must be cleared on rebuild"
6509 );
6510
6511 tree.destroy_subtree(widget);
6512 assert_eq!(
6513 tree.binding_registry().len(),
6514 0,
6515 "bindings must be cleared on destroy"
6516 );
6517 let _ = tick;
6519 }
6520
6521 #[test]
6522 fn ctx_destroy_cancels_animations_and_bindings_via_deferred_path() {
6523 use crate::binding::BindingLevel;
6531 use crate::signal::Signal;
6532 use std::time::{Duration, Instant};
6533 use teksilo_tokens::Easing;
6534
6535 #[derive(Debug)]
6536 struct BoundLeaf {
6537 tick: Signal<u64>,
6538 }
6539 impl crate::widget::Widget for BoundLeaf {
6540 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6541 self.tick.bind_to(
6542 ctx.self_id(),
6543 ctx.binding_registry(),
6544 BindingLevel::Relayout,
6545 );
6546 Vec::new()
6547 }
6548 fn layout_response(
6549 &self,
6550 proposal: SizeProposal,
6551 _ctx: &crate::widget::LayoutContext,
6552 ) -> crate::widget::LayoutResponse {
6553 proposal.resolve(10.0, 10.0).into()
6554 }
6555 }
6556
6557 let mut tree = WidgetTree::new();
6558 let widget = tree.add(BoundLeaf {
6559 tick: Signal::new(0_u64),
6560 });
6561 tree.layout(SizeProposal::exact(200.0, 200.0));
6562 assert!(tree.binding_registry().len() >= 1);
6563
6564 let anim = Signal::<f32>::new_animated(0.0);
6567 tree.animation_scheduler.animate(
6568 &anim,
6569 widget,
6570 1.0,
6571 Duration::from_secs(10),
6572 Easing::Linear,
6573 Instant::now(),
6574 );
6575 assert_eq!(tree.animation_scheduler.active_count(), 1);
6576
6577 let mut noop = crate::window::NoopWindowOps;
6579 tree.run_with_event_context(&mut noop, |ctx| ctx.destroy(widget));
6580
6581 assert_eq!(
6582 tree.animation_scheduler.active_count(),
6583 0,
6584 "ctx.destroy must cancel animations owned by the destroyed widget"
6585 );
6586 assert_eq!(
6587 tree.binding_registry().len(),
6588 0,
6589 "ctx.destroy must unregister the destroyed widget's bindings"
6590 );
6591 assert!(
6592 tree.arena.get(widget).is_none(),
6593 "node must be removed from the arena"
6594 );
6595 }
6596
6597 #[test]
6598 fn clear_shortcut_override_via_event_context_restores_default() {
6599 use crate::shortcut::{KeyStroke, Shortcut};
6600
6601 let mut tree = WidgetTree::new();
6602 tree.shortcut_registry_mut().register(
6603 Shortcut::new("app.save")
6604 .primary(KeyStroke::command(Key::S))
6605 .build(),
6606 );
6607 tree.shortcut_registry_mut()
6608 .rebind_primary("app.save", Some(KeyStroke::alt(Key::S)));
6609
6610 let source = tree.add(FillWidget::new());
6611 let mut ctx = EventContext::new();
6612 ctx.clear_shortcut_override("app.save");
6613 tree.collect_from_ctx(ctx, source);
6614
6615 assert_eq!(
6616 tree.shortcut_registry()
6617 .effective("app.save")
6618 .unwrap()
6619 .primary,
6620 Some(KeyStroke::command(Key::S))
6621 );
6622 }
6623
6624 #[test]
6625 fn rebind_shortcut_primary_via_event_context() {
6626 use crate::shortcut::{KeyStroke, Shortcut};
6627
6628 let mut tree = WidgetTree::new();
6629 tree.shortcut_registry_mut().register(
6630 Shortcut::new("app.save")
6631 .primary(KeyStroke::command(Key::S))
6632 .build(),
6633 );
6634 let source = tree.add(FillWidget::new());
6635
6636 let mut ctx = EventContext::new();
6637 ctx.rebind_shortcut_primary("app.save", Some(KeyStroke::alt(Key::S)));
6638 tree.collect_from_ctx(ctx, source);
6639
6640 assert_eq!(
6641 tree.shortcut_registry()
6642 .effective("app.save")
6643 .unwrap()
6644 .primary,
6645 Some(KeyStroke::alt(Key::S))
6646 );
6647 }
6648
6649 #[test]
6650 fn unregister_all_for_owner_called_on_destroy() {
6651 use crate::shortcut::{KeyStroke, Shortcut};
6652
6653 let mut tree = WidgetTree::new();
6654 let widget = tree.add(FillWidget::new());
6655 let widget_owner = widget;
6656 tree.shortcut_registry_mut().register_owned(
6657 Shortcut::new("scoped.thing")
6658 .primary(KeyStroke::command(Key::K))
6659 .build(),
6660 widget_owner,
6661 );
6662 assert!(
6663 tree.shortcut_registry()
6664 .get_default("scoped.thing")
6665 .is_some()
6666 );
6667
6668 tree.destroy_subtree(widget);
6669 assert!(
6670 tree.shortcut_registry()
6671 .get_default("scoped.thing")
6672 .is_none(),
6673 "destroying the owner must unregister its shortcut"
6674 );
6675 }
6676
6677 #[test]
6681 fn global_action_reached_from_unrelated_source() {
6682 use crate::action::Action;
6683 use crate::intent::Intent;
6684 use std::cell::Cell;
6685 use std::rc::Rc;
6686
6687 #[derive(Debug)]
6688 struct Registrar(Rc<Cell<bool>>);
6689 impl crate::widget::Widget for Registrar {
6690 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6691 let flag = self.0.clone();
6692 ctx.register_action_global(
6693 Action::new("test.global").on_invoke(move |_i, _c| flag.set(true)),
6694 );
6695 vec![]
6696 }
6697 fn layout_response(
6698 &self,
6699 _p: teksilo_canvas::SizeProposal,
6700 _c: &crate::widget::LayoutContext,
6701 ) -> crate::widget::LayoutResponse {
6702 teksilo_canvas::Size::new(0.0, 0.0).into()
6703 }
6704 }
6705
6706 let mut tree = WidgetTree::new();
6707 let fired = Rc::new(Cell::new(false));
6708 let registrar = tree.add(Registrar(fired.clone()));
6709 let source = tree.add(FillWidget::new()); let mut ops = crate::window::NoopWindowOps;
6711
6712 tree.dispatch_intent(source, Intent::new("test.global"), true, &mut ops);
6713 assert!(
6714 fired.get(),
6715 "global action must fire from an unrelated source"
6716 );
6717
6718 fired.set(false);
6720 tree.destroy_subtree(registrar);
6721 tree.dispatch_intent(source, Intent::new("test.global"), true, &mut ops);
6722 assert!(
6723 !fired.get(),
6724 "destroying the owner must remove its global action"
6725 );
6726 }
6727
6728 #[test]
6740 fn hit_test_through_translate_scope() {
6741 use crate::test_widgets::StackWidget;
6742 let mut tree = WidgetTree::new();
6743 let child = tree.add(FillWidget::new());
6744 let parent = tree.add(StackWidget::new().child(child));
6745 tree.set_transform(parent, teksilo_canvas::Transform2D::translate(100.0, 0.0));
6747 tree.layout(SizeProposal::exact(100.0, 50.0));
6748
6749 assert_eq!(
6753 tree.hit_test(Point::new(50.0, 25.0)),
6754 None,
6755 "pre-transform area is not visually populated and must not hit"
6756 );
6757 assert_eq!(
6759 tree.hit_test(Point::new(150.0, 25.0)),
6760 Some(child),
6761 "visually-rendered area must hit the child"
6762 );
6763 assert_eq!(tree.hit_test(Point::new(250.0, 25.0)), None);
6765 }
6766
6767 #[test]
6768 fn hit_test_through_scale_scope() {
6769 use crate::test_widgets::StackWidget;
6770 let mut tree = WidgetTree::new();
6771 let child = tree.add(FillWidget::new());
6772 let parent = tree.add(StackWidget::new().child(child));
6773 tree.set_transform(parent, teksilo_canvas::Transform2D::scale(0.5, 0.5));
6776 tree.layout(SizeProposal::exact(100.0, 50.0));
6777
6778 assert_eq!(tree.hit_test(Point::new(25.0, 12.0)), Some(child));
6780 assert_eq!(
6783 tree.hit_test(Point::new(75.0, 25.0)),
6784 None,
6785 "scaled-out region must not hit"
6786 );
6787 }
6788
6789 #[test]
6790 fn hit_test_through_nested_transforms_compose() {
6791 use crate::test_widgets::StackWidget;
6792 let mut tree = WidgetTree::new();
6793 let leaf = tree.add(FillWidget::new());
6794 let inner = tree.add(StackWidget::new().child(leaf));
6795 let outer = tree.add(StackWidget::new().child(inner));
6796 tree.set_transform(outer, teksilo_canvas::Transform2D::translate(100.0, 0.0));
6800 tree.set_transform(inner, teksilo_canvas::Transform2D::scale(2.0, 2.0));
6801 tree.layout(SizeProposal::exact(50.0, 25.0));
6802
6803 assert_eq!(tree.hit_test(Point::new(150.0, 25.0)), Some(leaf));
6807 assert_eq!(tree.hit_test(Point::new(50.0, 25.0)), None);
6808 assert_eq!(tree.hit_test(Point::new(250.0, 25.0)), None);
6809 }
6810
6811 #[test]
6812 fn hit_test_identity_transform_unchanged() {
6813 let mut tree = WidgetTree::new();
6817 let widget = tree.add(FillWidget::new());
6818 tree.set_transform(widget, teksilo_canvas::Transform2D::IDENTITY);
6819 tree.layout(SizeProposal::exact(100.0, 50.0));
6820 assert_eq!(tree.hit_test(Point::new(50.0, 25.0)), Some(widget));
6821 }
6822
6823 #[test]
6824 fn arena_effective_transform_composes_ancestors() {
6825 use crate::test_widgets::StackWidget;
6836 let mut tree = WidgetTree::new();
6837 let leaf = tree.add(FillWidget::new());
6838 let inner = tree.add(StackWidget::new().child(leaf));
6839 let outer = tree.add(StackWidget::new().child(inner));
6840 tree.set_transform(outer, teksilo_canvas::Transform2D::translate(100.0, 0.0));
6841 tree.set_transform(inner, teksilo_canvas::Transform2D::scale(2.0, 2.0));
6842 tree.layout(SizeProposal::exact(50.0, 25.0));
6843
6844 let eff = tree.arena.effective_transform(leaf);
6845 let expected = teksilo_canvas::Transform2D::scale(2.0, 2.0)
6846 .then(&teksilo_canvas::Transform2D::translate(100.0, 0.0));
6847 for (a, b) in eff.m.iter().zip(expected.m.iter()) {
6848 assert!(
6849 (a - b).abs() < 1e-5,
6850 "effective_transform mismatch: got {:?}, want {:?}",
6851 eff.m,
6852 expected.m
6853 );
6854 }
6855
6856 let screen_origin = eff.apply_point(Point::new(0.0, 0.0));
6862 assert!((screen_origin.x - 100.0).abs() < 1e-5);
6863 assert!((screen_origin.y - 0.0).abs() < 1e-5);
6864 let screen_corner = eff.apply_point(Point::new(50.0, 25.0));
6867 assert!((screen_corner.x - 200.0).abs() < 1e-5);
6868 assert!((screen_corner.y - 50.0).abs() < 1e-5);
6869 }
6870
6871 #[derive(Debug)]
6876 struct StubMenu;
6877 impl crate::widget::Widget for StubMenu {
6878 fn layout_response(
6879 &self,
6880 _proposal: SizeProposal,
6881 _ctx: &crate::widget::LayoutContext,
6882 ) -> crate::widget::LayoutResponse {
6883 teksilo_canvas::Size::new(100.0, 40.0).into()
6884 }
6885 }
6886
6887 #[derive(Debug)]
6899 struct NominatingWidget {
6900 row: std::cell::Cell<Option<WidgetId>>,
6901 }
6902
6903 impl crate::widget::Widget for NominatingWidget {
6904 fn layout_response(
6905 &self,
6906 proposal: SizeProposal,
6907 _ctx: &LayoutContext,
6908 ) -> crate::widget::LayoutResponse {
6909 proposal.resolve(50.0, 20.0).into()
6910 }
6911
6912 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6913 ctx.apply_self_handlers(crate::widget_builder::HandlerSet::new().focusable(true));
6914 Vec::new()
6915 }
6916
6917 fn context_menu_key_target(&self) -> Option<WidgetId> {
6918 self.row.get()
6919 }
6920 }
6921
6922 fn press(tree: &mut WidgetTree, key: Key, modifiers: Modifiers) {
6923 tree.dispatch_event(WidgetEvent::KeyDown {
6924 key,
6925 modifiers,
6926 text: None,
6927 });
6928 }
6929
6930 #[test]
6931 fn the_context_menu_key_opens_the_focused_widget_menu() {
6932 use std::cell::Cell;
6933 use std::rc::Rc;
6934
6935 let opened = Rc::new(Cell::new(false));
6936 let flag = opened.clone();
6937 let mut tree = WidgetTree::new();
6938 let widget = tree.add(
6939 FillWidget::new()
6940 .focusable()
6941 .context_menu(move |_pos, _ctx| {
6942 flag.set(true);
6943 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6944 }),
6945 );
6946 tree.layout(SizeProposal::exact(200.0, 100.0));
6947 tree.focus(widget);
6948
6949 press(&mut tree, Key::ContextMenu, Modifiers::NONE);
6950 assert!(opened.get(), "the dedicated Menu key must open the menu");
6951 }
6952
6953 #[test]
6956 fn shift_f10_opens_the_focused_widget_menu() {
6957 use std::cell::Cell;
6958 use std::rc::Rc;
6959
6960 let opened = Rc::new(Cell::new(false));
6961 let flag = opened.clone();
6962 let mut tree = WidgetTree::new();
6963 let widget = tree.add(
6964 FillWidget::new()
6965 .focusable()
6966 .context_menu(move |_pos, _ctx| {
6967 flag.set(true);
6968 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6969 }),
6970 );
6971 tree.layout(SizeProposal::exact(200.0, 100.0));
6972 tree.focus(widget);
6973
6974 press(&mut tree, Key::F10, Modifiers::SHIFT);
6975 assert!(opened.get(), "Shift+F10 must open the menu");
6976 }
6977
6978 #[test]
6981 fn a_near_miss_chord_is_not_a_context_menu_request() {
6982 use std::cell::Cell;
6983 use std::rc::Rc;
6984
6985 let opened = Rc::new(Cell::new(false));
6986 let flag = opened.clone();
6987 let mut tree = WidgetTree::new();
6988 let widget = tree.add(
6989 FillWidget::new()
6990 .focusable()
6991 .context_menu(move |_pos, _ctx| {
6992 flag.set(true);
6993 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6994 }),
6995 );
6996 tree.layout(SizeProposal::exact(200.0, 100.0));
6997 tree.focus(widget);
6998
6999 press(&mut tree, Key::F10, Modifiers::SHIFT | Modifiers::CTRL);
7000 press(&mut tree, Key::F10, Modifiers::NONE);
7001 assert!(!opened.get(), "only Shift+F10 exactly asks for a menu");
7002 }
7003
7004 #[test]
7008 fn the_keyboard_target_can_be_a_row_rather_than_the_focused_container() {
7009 use std::cell::Cell;
7010 use std::rc::Rc;
7011
7012 let menu_owner = Rc::new(Cell::new(None::<&'static str>));
7013
7014 let row_flag = menu_owner.clone();
7015 let container_flag = menu_owner.clone();
7016
7017 let mut tree = WidgetTree::new();
7018 let row = tree.add(FillWidget::new().context_menu(move |_pos, _ctx| {
7019 row_flag.set(Some("row"));
7020 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
7021 }));
7022 let container = tree.add(
7023 crate::test_widgets::StackWidget::new()
7024 .child(row)
7025 .context_menu(move |_pos, _ctx| {
7026 container_flag.set(Some("container"));
7027 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
7028 }),
7029 );
7030 tree.layout(SizeProposal::exact(200.0, 100.0));
7031
7032 let nominator = tree.add(NominatingWidget {
7034 row: std::cell::Cell::new(Some(row)),
7035 });
7036 tree.layout(SizeProposal::exact(200.0, 100.0));
7037 tree.focus(nominator);
7038 let _ = container;
7039
7040 press(&mut tree, Key::ContextMenu, Modifiers::NONE);
7041 assert_eq!(
7042 menu_owner.get(),
7043 Some("row"),
7044 "the nominated row's factory must be the one that runs"
7045 );
7046 }
7047
7048 #[test]
7051 fn the_chord_falls_through_when_there_is_no_menu_to_show() {
7052 use std::cell::Cell;
7053 use std::rc::Rc;
7054
7055 let saw_key = Rc::new(Cell::new(false));
7056 let flag = saw_key.clone();
7057 let mut tree = WidgetTree::new();
7058 let widget = tree.add(FillWidget::new().focusable().on_key(move |ev, _ctx| {
7059 if matches!(ev, WidgetEvent::KeyDown { key: Key::F10, .. }) {
7060 flag.set(true);
7061 }
7062 crate::event::EventResponse::Ignored
7063 }));
7064 tree.layout(SizeProposal::exact(200.0, 100.0));
7065 tree.focus(widget);
7066
7067 press(&mut tree, Key::F10, Modifiers::SHIFT);
7068 assert!(
7069 saw_key.get(),
7070 "with no factory anywhere, the key must reach the widget"
7071 );
7072 }
7073
7074 #[test]
7080 fn each_route_to_a_menu_tells_the_factory_what_asked_for_it() {
7081 use crate::widget_builder::ContextMenuTrigger;
7082 use std::cell::RefCell;
7083 use std::rc::Rc;
7084
7085 let seen = Rc::new(RefCell::new(Vec::new()));
7086 let log = seen.clone();
7087 let mut tree = WidgetTree::new();
7088 let widget = tree.add(
7089 FillWidget::new()
7090 .focusable()
7091 .context_menu(move |_pos, ctx| {
7092 log.borrow_mut().push(ctx.context_menu_trigger());
7093 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
7094 }),
7095 );
7096 tree.layout(SizeProposal::exact(200.0, 100.0));
7097 tree.focus(widget);
7098 let inside = tree.bounds(widget).center();
7099
7100 press(&mut tree, Key::F10, Modifiers::SHIFT);
7101 press(&mut tree, Key::Escape, Modifiers::NONE);
7102 press(&mut tree, Key::ContextMenu, Modifiers::NONE);
7103 press(&mut tree, Key::Escape, Modifiers::NONE);
7104 tree.dispatch_event(WidgetEvent::pointer_down(
7105 inside,
7106 crate::event::PointerButton::Secondary,
7107 Modifiers::NONE,
7108 ));
7109 tree.dispatch_event(WidgetEvent::pointer_up(
7110 inside,
7111 crate::event::PointerButton::Secondary,
7112 Modifiers::NONE,
7113 ));
7114 press(&mut tree, Key::Escape, Modifiers::NONE);
7115 tree.dispatch_event(WidgetEvent::AccessAction {
7116 action: accesskit::Action::ShowContextMenu,
7117 target: Some(widget),
7118 target_node: crate::accessibility::widget_id_to_node_id(widget),
7119 data: None,
7120 });
7121 press(&mut tree, Key::Escape, Modifiers::NONE);
7122 tree.long_press_at(teksilo_tokens::PointerKind::Touch, inside);
7123
7124 assert_eq!(
7125 *seen.borrow(),
7126 vec![
7127 Some(ContextMenuTrigger::Keyboard),
7128 Some(ContextMenuTrigger::Keyboard),
7129 Some(ContextMenuTrigger::Pointer),
7130 Some(ContextMenuTrigger::Accessibility),
7131 Some(ContextMenuTrigger::Pointer),
7132 ],
7133 "Shift+F10, the Menu key, a right-click, the assistive action and a \
7134 hold, in that order"
7135 );
7136 }
7137
7138 #[test]
7139 fn context_menu_factory_receives_click_position() {
7140 use crate::event::{Modifiers, PointerButton};
7141 use std::cell::Cell;
7142 use std::rc::Rc;
7143
7144 let captured_position = Rc::new(Cell::new(None::<Point>));
7145 let cap = captured_position.clone();
7146 let mut tree = WidgetTree::new();
7147 let widget = tree.add(FillWidget::new().context_menu(move |pos, _ctx| {
7148 cap.set(Some(pos));
7149 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
7150 }));
7151 tree.layout(SizeProposal::exact(200.0, 100.0));
7152
7153 let click = Point::new(73.0, 42.0);
7154 tree.dispatch_event(WidgetEvent::pointer_down(
7155 click,
7156 PointerButton::Secondary,
7157 Modifiers::NONE,
7158 ));
7159
7160 let got = captured_position.get();
7161 assert_eq!(
7162 got,
7163 Some(click),
7164 "factory must receive the click position; got {:?}",
7165 got
7166 );
7167 let _ = widget;
7168 }
7169
7170 #[test]
7171 fn context_menu_factory_returning_none_falls_through_to_parent() {
7172 use crate::event::{Modifiers, PointerButton};
7173 use crate::test_widgets::StackWidget;
7174 use std::cell::Cell;
7175 use std::rc::Rc;
7176
7177 let outer_called = Rc::new(Cell::new(0_u32));
7181 let outer_flag = outer_called.clone();
7182 let mut tree = WidgetTree::new();
7183 let inner = tree.add(FillWidget::new().context_menu(|_pos, _ctx| None));
7184 let _outer = tree.add(
7185 StackWidget::new()
7186 .child(inner)
7187 .context_menu(move |_pos, _ctx| {
7188 outer_flag.set(outer_flag.get() + 1);
7189 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
7190 }),
7191 );
7192 tree.layout(SizeProposal::exact(200.0, 100.0));
7193
7194 tree.dispatch_event(WidgetEvent::pointer_down(
7195 Point::new(50.0, 25.0),
7196 PointerButton::Secondary,
7197 Modifiers::NONE,
7198 ));
7199
7200 assert_eq!(
7201 outer_called.get(),
7202 1,
7203 "inner returning None must fall through to the outer factory"
7204 );
7205 }
7206
7207 #[test]
7208 fn context_menu_factory_none_throughout_chain_does_not_show_overlay() {
7209 use crate::event::{Modifiers, PointerButton};
7210
7211 let mut tree = WidgetTree::new();
7213 tree.add(FillWidget::new().context_menu(|_pos, _ctx| None));
7214 tree.layout(SizeProposal::exact(200.0, 100.0));
7215
7216 let overlay_count_before = tree.overlay_manager.len();
7217 tree.dispatch_event(WidgetEvent::pointer_down(
7218 Point::new(50.0, 25.0),
7219 PointerButton::Secondary,
7220 Modifiers::NONE,
7221 ));
7222 let overlay_count_after = tree.overlay_manager.len();
7223 assert_eq!(
7224 overlay_count_before, overlay_count_after,
7225 "a factory returning None must not mount any overlay"
7226 );
7227 }
7228
7229 #[derive(Debug)]
7240 struct FreshChildHost {
7241 preserve: bool,
7242 }
7243 impl Widget for FreshChildHost {
7244 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
7245 vec![ctx.add(FillWidget::new())]
7246 }
7247 fn layout_response(
7248 &self,
7249 p: SizeProposal,
7250 _c: &LayoutContext,
7251 ) -> crate::widget::LayoutResponse {
7252 p.resolve(10.0, 10.0).into()
7253 }
7254 fn preserves_children_on_rebuild(&self) -> bool {
7255 self.preserve
7256 }
7257 }
7258
7259 #[test]
7260 fn reconcile_reaps_dropped_children_no_leak() {
7261 for preserve in [false, true] {
7266 let mut tree = WidgetTree::new();
7267 let host = tree.add(FreshChildHost { preserve });
7268 tree.layout(SizeProposal::exact(100.0, 100.0));
7269 let total0 = tree.arena.len();
7270 let active0 = tree.active_widget_count();
7271 for _ in 0..5 {
7272 tree.arena_mark_needs_rebuild_for_testing(host);
7273 tree.layout(SizeProposal::exact(100.0, 100.0));
7274 }
7275 assert_eq!(
7276 tree.arena.len(),
7277 total0,
7278 "preserve={preserve}: dropped children must be reaped, not leaked"
7279 );
7280 assert_eq!(
7281 tree.active_widget_count(),
7282 active0,
7283 "preserve={preserve}: no stranded still-active orphans"
7284 );
7285 }
7286 }
7287
7288 #[derive(Debug)]
7294 struct DetachedContentHost {
7295 preserve: bool,
7296 }
7297 impl Widget for DetachedContentHost {
7298 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
7299 let popup = ctx.add_detached(FillWidget::new());
7300 ctx.set_dormant(popup);
7301 vec![ctx.add(FillWidget::new())]
7302 }
7303 fn layout_response(
7304 &self,
7305 p: SizeProposal,
7306 _c: &LayoutContext,
7307 ) -> crate::widget::LayoutResponse {
7308 p.resolve(10.0, 10.0).into()
7309 }
7310 fn preserves_children_on_rebuild(&self) -> bool {
7311 self.preserve
7312 }
7313 }
7314
7315 #[test]
7316 fn rebuilding_reaps_detached_content_no_leak() {
7317 for preserve in [false, true] {
7323 let mut tree = WidgetTree::new();
7324 let host = tree.add(DetachedContentHost { preserve });
7325 tree.layout(SizeProposal::exact(100.0, 100.0));
7326 let total0 = tree.arena.len();
7327 for _ in 0..5 {
7328 tree.arena_mark_needs_rebuild_for_testing(host);
7329 tree.layout(SizeProposal::exact(100.0, 100.0));
7330 }
7331 assert_eq!(
7332 tree.arena.len(),
7333 total0,
7334 "preserve={preserve}: the previous build's detached content must be reaped"
7335 );
7336 }
7337 }
7338
7339 #[test]
7340 fn destroying_a_host_reaps_its_detached_content() {
7341 let mut tree = WidgetTree::new();
7342 let outer = tree.add(FillWidget::new());
7343 tree.layout(SizeProposal::exact(100.0, 100.0));
7344 let empty = tree.arena.len();
7345
7346 let host = tree.add_child(outer, DetachedContentHost { preserve: false });
7347 tree.layout(SizeProposal::exact(100.0, 100.0));
7348 assert!(tree.arena.len() > empty);
7349
7350 tree.destroy_subtree(host);
7351 assert_eq!(
7352 tree.arena.len(),
7353 empty,
7354 "the popup must die with the widget that built it"
7355 );
7356 }
7357
7358 #[derive(Debug)]
7360 struct StableChildHost {
7361 child: Option<WidgetId>,
7362 probe: std::rc::Rc<std::cell::Cell<Option<WidgetId>>>,
7363 }
7364 impl Widget for StableChildHost {
7365 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
7366 let id = match self.child {
7367 Some(id) => id,
7368 None => {
7369 let id = ctx.add(FillWidget::new());
7370 self.child = Some(id);
7371 self.probe.set(Some(id));
7372 id
7373 }
7374 };
7375 vec![id]
7376 }
7377 fn layout_response(
7378 &self,
7379 p: SizeProposal,
7380 _c: &LayoutContext,
7381 ) -> crate::widget::LayoutResponse {
7382 p.resolve(10.0, 10.0).into()
7383 }
7384 fn preserves_children_on_rebuild(&self) -> bool {
7385 true
7386 }
7387 }
7388
7389 #[test]
7390 fn reconcile_preserves_reattached_child() {
7391 let probe = std::rc::Rc::new(std::cell::Cell::new(None));
7392 let mut tree = WidgetTree::new();
7393 let host = tree.add(StableChildHost {
7394 child: None,
7395 probe: probe.clone(),
7396 });
7397 tree.layout(SizeProposal::exact(100.0, 100.0));
7398 let child = probe.get().expect("child mounted");
7399 let total0 = tree.arena.len();
7400 for _ in 0..5 {
7401 tree.arena_mark_needs_rebuild_for_testing(host);
7402 tree.layout(SizeProposal::exact(100.0, 100.0));
7403 }
7404 assert!(
7405 tree.arena.is_active(child),
7406 "the re-attached child must survive every rebuild"
7407 );
7408 assert_eq!(tree.arena.len(), total0, "no growth — same child reused");
7409 }
7410
7411 #[derive(Debug)]
7413 struct Wrapper {
7414 child: WidgetId,
7415 }
7416 impl Widget for Wrapper {
7417 fn build(&mut self, _ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
7418 vec![self.child]
7419 }
7420 fn layout_response(
7421 &self,
7422 p: SizeProposal,
7423 _c: &LayoutContext,
7424 ) -> crate::widget::LayoutResponse {
7425 p.resolve(10.0, 10.0).into()
7426 }
7427 }
7428
7429 #[derive(Debug)]
7433 struct ReparentHost {
7434 body: Option<WidgetId>,
7435 probe: std::rc::Rc<std::cell::Cell<Option<WidgetId>>>,
7436 }
7437 impl Widget for ReparentHost {
7438 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
7439 let body = match self.body {
7440 Some(id) => id,
7441 None => {
7442 let id = ctx.add(FillWidget::new());
7443 self.body = Some(id);
7444 self.probe.set(Some(id));
7445 id
7446 }
7447 };
7448 vec![ctx.add(Wrapper { child: body })]
7449 }
7450 fn layout_response(
7451 &self,
7452 p: SizeProposal,
7453 _c: &LayoutContext,
7454 ) -> crate::widget::LayoutResponse {
7455 p.resolve(10.0, 10.0).into()
7456 }
7457 fn preserves_children_on_rebuild(&self) -> bool {
7458 true
7459 }
7460 }
7461
7462 #[test]
7463 fn reconcile_spares_reparented_survivor() {
7464 let probe = std::rc::Rc::new(std::cell::Cell::new(None));
7471 let mut tree = WidgetTree::new();
7472 let host = tree.add(ReparentHost {
7473 body: None,
7474 probe: probe.clone(),
7475 });
7476 tree.layout(SizeProposal::exact(100.0, 100.0));
7477 let body = probe.get().expect("body mounted");
7478 let total0 = tree.arena.len();
7479 for _ in 0..5 {
7480 tree.arena_mark_needs_rebuild_for_testing(host);
7481 tree.layout(SizeProposal::exact(100.0, 100.0));
7482 }
7483 assert!(
7484 tree.arena.is_active(body),
7485 "the re-parented body must survive — it was moved into the new tree, \
7486 not swept with the dropped wrapper"
7487 );
7488 assert_eq!(
7489 tree.arena.len(),
7490 total0,
7491 "dropped wrappers reaped — no per-rebuild leak"
7492 );
7493 }
7494
7495 #[derive(Debug)]
7505 struct BelowContainer {
7506 child: Option<WidgetId>,
7507 offset: f32,
7508 }
7509
7510 impl crate::widget::Widget for BelowContainer {
7511 fn layout_response(
7512 &self,
7513 proposal: SizeProposal,
7514 _ctx: &crate::widget::LayoutContext,
7515 ) -> crate::widget::LayoutResponse {
7516 proposal.resolve(0.0, 0.0).into()
7517 }
7518 fn place_children(
7519 &self,
7520 bounds: Rect,
7521 _proposal: SizeProposal,
7522 children: &mut [crate::widget::WidgetPlacement],
7523 _ctx: &crate::widget::LayoutContext,
7524 ) {
7525 for c in children.iter_mut() {
7526 c.origin = Point::new(bounds.x, bounds.y + self.offset);
7527 c.size = bounds.size();
7528 }
7529 }
7530 fn children(&self) -> Vec<WidgetId> {
7531 self.child.into_iter().collect()
7532 }
7533 }
7534
7535 fn recording_scroll_container(
7538 tree: &mut WidgetTree,
7539 child: WidgetId,
7540 recorded: std::rc::Rc<std::cell::Cell<Option<Rect>>>,
7541 ) -> WidgetId {
7542 use crate::test_widgets::StackWidget;
7543 tree.add(
7544 StackWidget::new()
7545 .child(child)
7546 .on_scroll(move |ev, _ctx| match ev {
7547 WidgetEvent::ScrollIntoView { target_bounds, .. } => {
7548 recorded.set(Some(*target_bounds));
7549 EventResponse::Handled
7550 }
7551 _ => EventResponse::Ignored,
7552 })
7553 .clips_children(true),
7554 )
7555 }
7556
7557 #[test]
7558 fn ensure_visible_dispatches_scroll_into_view_to_clipping_ancestor() {
7559 use std::cell::Cell;
7560 use std::rc::Rc;
7561 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
7562 let mut tree = WidgetTree::new();
7563 let actor = tree.add(FillWidget::new());
7564 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
7565 tree.layout(SizeProposal::exact(100.0, 100.0));
7566
7567 let target = Rect::new(10.0, 500.0, 20.0, 15.0);
7570 let mut ctx = EventContext::new();
7571 ctx.ensure_visible(target);
7572 tree.collect_from_ctx(ctx, actor);
7573
7574 assert_eq!(
7575 recorded.get(),
7576 Some(target),
7577 "ensure_visible(rect) must dispatch ScrollIntoView with the exact rect \
7578 to the clips_children ancestor"
7579 );
7580 }
7581
7582 #[test]
7583 fn ensure_visible_is_noop_when_rect_already_visible() {
7584 use std::cell::Cell;
7585 use std::rc::Rc;
7586 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
7587 let mut tree = WidgetTree::new();
7588 let actor = tree.add(FillWidget::new());
7589 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
7590 tree.layout(SizeProposal::exact(100.0, 100.0));
7591
7592 let mut ctx = EventContext::new();
7595 ctx.ensure_visible(Rect::new(10.0, 10.0, 20.0, 15.0));
7596 tree.collect_from_ctx(ctx, actor);
7597
7598 assert_eq!(
7599 recorded.get(),
7600 None,
7601 "a rect already inside the viewport must not trigger a scroll"
7602 );
7603 }
7604
7605 #[test]
7606 fn ensure_visible_margin_forces_scroll_near_edge() {
7607 use std::cell::Cell;
7608 use std::rc::Rc;
7609 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
7610 let mut tree = WidgetTree::new();
7611 let actor = tree.add(FillWidget::new());
7612 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
7613 tree.layout(SizeProposal::exact(100.0, 100.0));
7614
7615 let rect = Rect::new(10.0, 95.0, 20.0, 4.0);
7618 let mut ctx = EventContext::new();
7619 ctx.ensure_visible_with_margin(rect, 10.0);
7620 tree.collect_from_ctx(ctx, actor);
7621
7622 assert_eq!(
7623 recorded.get(),
7624 Some(rect),
7625 "the margin must widen the visibility test so a near-edge rect scrolls"
7626 );
7627 }
7628
7629 fn recording_align_container(
7632 tree: &mut WidgetTree,
7633 child: WidgetId,
7634 recorded: std::rc::Rc<
7635 std::cell::Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>,
7636 >,
7637 ) -> WidgetId {
7638 use crate::test_widgets::StackWidget;
7639 tree.add(
7640 StackWidget::new()
7641 .child(child)
7642 .on_scroll(move |ev, _ctx| match ev {
7643 WidgetEvent::ScrollIntoView { align, motion, .. } => {
7644 recorded.set(Some((*align, *motion)));
7645 EventResponse::Handled
7646 }
7647 _ => EventResponse::Ignored,
7648 })
7649 .clips_children(true),
7650 )
7651 }
7652
7653 #[test]
7654 fn ensure_visible_aligned_scrolls_even_when_already_visible() {
7655 use std::cell::Cell;
7656 use std::rc::Rc;
7657 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
7658 let mut tree = WidgetTree::new();
7659 let actor = tree.add(FillWidget::new());
7660 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
7661 tree.layout(SizeProposal::exact(100.0, 100.0));
7662
7663 let target = Rect::new(10.0, 10.0, 20.0, 15.0);
7668 let mut ctx = EventContext::new();
7669 ctx.ensure_visible_aligned(target, 0.5, crate::event::ScrollMotion::Instant);
7670 tree.collect_from_ctx(ctx, actor);
7671
7672 assert_eq!(
7673 recorded.get(),
7674 Some(target),
7675 "an aligned reveal must dispatch even when the rect is already visible"
7676 );
7677 }
7678
7679 #[test]
7680 fn ensure_visible_aligned_forwards_fraction_and_motion() {
7681 use std::cell::Cell;
7682 use std::rc::Rc;
7683 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
7684 Rc::new(Cell::new(None));
7685 let mut tree = WidgetTree::new();
7686 let actor = tree.add(FillWidget::new());
7687 let _container = recording_align_container(&mut tree, actor, recorded.clone());
7688 tree.layout(SizeProposal::exact(100.0, 100.0));
7689
7690 let mut ctx = EventContext::new();
7691 ctx.ensure_visible_aligned(
7692 Rect::new(10.0, 10.0, 20.0, 15.0),
7693 0.25,
7694 crate::event::ScrollMotion::Smooth,
7695 );
7696 tree.collect_from_ctx(ctx, actor);
7697
7698 assert_eq!(
7699 recorded.get(),
7700 Some((
7701 crate::event::ScrollAlign::Fraction(0.25),
7702 crate::event::ScrollMotion::Smooth
7703 )),
7704 "the container must receive the requested fraction and motion verbatim"
7705 );
7706 }
7707
7708 #[test]
7709 fn ensure_visible_aligned_clamps_the_fraction() {
7710 use std::cell::Cell;
7711 use std::rc::Rc;
7712 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
7713 Rc::new(Cell::new(None));
7714 let mut tree = WidgetTree::new();
7715 let actor = tree.add(FillWidget::new());
7716 let _container = recording_align_container(&mut tree, actor, recorded.clone());
7717 tree.layout(SizeProposal::exact(100.0, 100.0));
7718
7719 let mut ctx = EventContext::new();
7720 ctx.ensure_visible_aligned(
7721 Rect::new(10.0, 10.0, 20.0, 15.0),
7722 4.2,
7723 crate::event::ScrollMotion::Instant,
7724 );
7725 tree.collect_from_ctx(ctx, actor);
7726
7727 assert_eq!(
7728 recorded.get().map(|(a, _)| a),
7729 Some(crate::event::ScrollAlign::Fraction(1.0)),
7730 "an out-of-range fraction must clamp rather than aim the pin off-screen"
7731 );
7732 }
7733
7734 #[test]
7735 fn plain_ensure_visible_requests_minimal_alignment() {
7736 use std::cell::Cell;
7737 use std::rc::Rc;
7738 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
7739 Rc::new(Cell::new(None));
7740 let mut tree = WidgetTree::new();
7741 let actor = tree.add(FillWidget::new());
7742 let _container = recording_align_container(&mut tree, actor, recorded.clone());
7743 tree.layout(SizeProposal::exact(100.0, 100.0));
7744
7745 let mut ctx = EventContext::new();
7746 ctx.ensure_visible(Rect::new(10.0, 500.0, 20.0, 15.0));
7747 tree.collect_from_ctx(ctx, actor);
7748
7749 assert_eq!(
7750 recorded.get(),
7751 Some((
7752 crate::event::ScrollAlign::Minimal,
7753 crate::event::ScrollMotion::Instant
7754 )),
7755 "the pre-existing reveal API must keep its exact semantics"
7756 );
7757 }
7758
7759 #[test]
7760 fn only_the_innermost_container_aligns() {
7761 use std::cell::Cell;
7762 use std::rc::Rc;
7763 let inner_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
7764 Rc::new(Cell::new(None));
7765 let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
7766 Rc::new(Cell::new(None));
7767
7768 let mut tree = WidgetTree::new();
7769 let actor = tree.add(FillWidget::new());
7770 let inner = recording_align_container(&mut tree, actor, inner_rec.clone());
7771 let _outer = recording_align_container(&mut tree, inner, outer_rec.clone());
7772 tree.layout(SizeProposal::exact(100.0, 100.0));
7773
7774 let mut ctx = EventContext::new();
7777 ctx.ensure_visible_aligned(
7778 Rect::new(10.0, 500.0, 20.0, 15.0),
7779 0.5,
7780 crate::event::ScrollMotion::Instant,
7781 );
7782 tree.collect_from_ctx(ctx, actor);
7783
7784 assert_eq!(
7785 inner_rec.get().map(|(a, _)| a),
7786 Some(crate::event::ScrollAlign::Fraction(0.5)),
7787 "the innermost clipping ancestor owns the pin"
7788 );
7789 assert_eq!(
7790 outer_rec.get().map(|(a, _)| a),
7791 Some(crate::event::ScrollAlign::Minimal),
7792 "an outer container must only bring the inner viewport into view — a \
7793 fraction names a height in one viewport, not in every ancestor's"
7794 );
7795 }
7796
7797 #[test]
7798 fn a_clipper_that_does_not_scroll_passes_the_pin_on() {
7799 use crate::test_widgets::StackWidget;
7806 use std::cell::Cell;
7807 use std::rc::Rc;
7808 let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
7809 Rc::new(Cell::new(None));
7810
7811 let mut tree = WidgetTree::new();
7812 let actor = tree.add(FillWidget::new());
7813 let clipper = tree.add(StackWidget::new().child(actor).clips_children_on(true));
7814 let _scroller = recording_align_container(&mut tree, clipper, outer_rec.clone());
7815 tree.layout(SizeProposal::exact(100.0, 100.0));
7816
7817 let mut ctx = EventContext::new();
7820 ctx.ensure_visible_aligned(
7821 Rect::new(10.0, 10.0, 20.0, 15.0),
7822 0.5,
7823 crate::event::ScrollMotion::Instant,
7824 );
7825 tree.collect_from_ctx(ctx, actor);
7826
7827 assert_eq!(
7828 outer_rec.get().map(|(a, _)| a),
7829 Some(crate::event::ScrollAlign::Fraction(0.5)),
7830 "a clipping wrapper with no scroll handler must pass the pin through \
7831 to the scroll container above it"
7832 );
7833 }
7834
7835 #[test]
7836 fn a_scroller_that_declines_to_move_still_holds_the_pin() {
7837 use crate::test_widgets::StackWidget;
7842 use std::cell::Cell;
7843 use std::rc::Rc;
7844 let inner_rec: Rc<Cell<Option<crate::event::ScrollAlign>>> = Rc::new(Cell::new(None));
7845 let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
7846 Rc::new(Cell::new(None));
7847
7848 let mut tree = WidgetTree::new();
7849 let actor = tree.add(FillWidget::new());
7850 let rec = inner_rec.clone();
7851 let inner = tree.add(
7852 StackWidget::new()
7853 .child(actor)
7854 .on_scroll(move |ev, _ctx| match ev {
7855 WidgetEvent::ScrollIntoView { align, .. } => {
7856 rec.set(Some(*align));
7857 EventResponse::Ignored
7858 }
7859 _ => EventResponse::Ignored,
7860 })
7861 .clips_children(true),
7862 );
7863 let _outer = recording_align_container(&mut tree, inner, outer_rec.clone());
7864 tree.layout(SizeProposal::exact(100.0, 100.0));
7865
7866 let mut ctx = EventContext::new();
7868 ctx.ensure_visible_aligned(
7869 Rect::new(10.0, 500.0, 20.0, 15.0),
7870 0.5,
7871 crate::event::ScrollMotion::Instant,
7872 );
7873 tree.collect_from_ctx(ctx, actor);
7874
7875 assert_eq!(
7876 inner_rec.get(),
7877 Some(crate::event::ScrollAlign::Fraction(0.5)),
7878 "the innermost scroller is offered the pin"
7879 );
7880 assert_eq!(
7881 outer_rec.get().map(|(a, _)| a),
7882 Some(crate::event::ScrollAlign::Minimal),
7883 "declining to move must not pass the pin outward"
7884 );
7885 }
7886
7887 #[test]
7888 fn ensure_widget_visible_uses_target_arena_bounds() {
7889 use std::cell::Cell;
7890 use std::rc::Rc;
7891 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
7892 let mut tree = WidgetTree::new();
7893 let target = tree.add(FillWidget::new());
7895 let rec = recorded.clone();
7896 let container = tree.add(
7897 BelowContainer {
7898 child: Some(target),
7899 offset: 500.0,
7900 }
7901 .on_scroll(move |ev, _ctx| match ev {
7902 WidgetEvent::ScrollIntoView { target_bounds, .. } => {
7903 rec.set(Some(*target_bounds));
7904 EventResponse::Handled
7905 }
7906 _ => EventResponse::Ignored,
7907 })
7908 .clips_children(true),
7909 );
7910 tree.layout(SizeProposal::exact(100.0, 100.0));
7911
7912 let expected = tree.bounds(target);
7913 assert!(
7914 expected.y > 100.0,
7915 "fixture sanity: the target must sit below the viewport (y={})",
7916 expected.y
7917 );
7918
7919 let mut ctx = EventContext::new();
7922 ctx.ensure_widget_visible(target);
7923 tree.collect_from_ctx(ctx, container);
7924
7925 assert_eq!(
7926 recorded.get(),
7927 Some(expected),
7928 "ensure_widget_visible(id) must dispatch ScrollIntoView with the \
7929 target's current arena bounds"
7930 );
7931 }
7932
7933 #[test]
7934 fn ensure_widget_visible_ignores_missing_widget() {
7935 use std::cell::Cell;
7937 use std::rc::Rc;
7938 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
7939 let mut tree = WidgetTree::new();
7940 let actor = tree.add(FillWidget::new());
7941 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
7942 tree.layout(SizeProposal::exact(100.0, 100.0));
7943
7944 let mut ctx = EventContext::new();
7945 ctx.ensure_widget_visible(WidgetId::default());
7946 tree.collect_from_ctx(ctx, actor); assert_eq!(
7949 recorded.get(),
7950 None,
7951 "an unmounted id must not trigger a scroll"
7952 );
7953 }
7954
7955 #[test]
7956 fn context_menu_inside_a_modal_keeps_the_modal() {
7957 use crate::event::{Modifiers, PointerButton, WidgetEvent};
7961 use crate::overlay::{DismissBehavior, OverlayPlacement, OverlayRequest};
7962 use crate::test_widgets::{FillWidget, StackWidget};
7963
7964 let mut tree = WidgetTree::new();
7965 let modal_content = tree.add(StackWidget::new());
7968 let _editor = tree.add_child(
7969 modal_content,
7970 FillWidget::new()
7971 .context_menu(|_pos, _ctx| Some(Box::new(FillWidget::new()) as Box<dyn Widget>)),
7972 );
7973 tree.layout(SizeProposal::exact(200.0, 100.0));
7974
7975 let modal = tree.overlay_manager.show(OverlayRequest::new(
7976 modal_content,
7977 modal_content,
7978 OverlayPlacement::Centered,
7979 DismissBehavior::EscapeKey,
7980 ));
7981 tree.overlay_manager
7983 .stack
7984 .iter_mut()
7985 .find(|o| o.id == modal)
7986 .unwrap()
7987 .bounds = Rect::new(0.0, 0.0, 200.0, 100.0);
7988 assert_eq!(tree.overlay_manager.len(), 1);
7989
7990 tree.dispatch_event(WidgetEvent::pointer_down(
7992 Point::new(50.0, 25.0),
7993 PointerButton::Secondary,
7994 Modifiers::NONE,
7995 ));
7996
7997 assert!(
7998 tree.overlay_manager.active_ids().contains(&modal),
7999 "the modal must survive opening a context menu inside it"
8000 );
8001 assert_eq!(
8002 tree.overlay_manager.len(),
8003 2,
8004 "the context menu should now be open on top of the surviving modal"
8005 );
8006 }
8007}
8008
8009#[cfg(test)]
8012mod input_ingress_tests {
8013 use super::*;
8014 use crate::event::{EventResponse, Modifiers, PointerButton, ScrollDelta};
8015 use crate::pointer::{
8016 EventTime, PointerId, PointerInfo, PointerPhase, PointerSample, ScrollPhase, ScrollSample,
8017 ScrollSource,
8018 };
8019 use crate::test_widgets::FillWidget;
8020 use crate::widget::{LayoutContext, WidgetPlacement};
8021 use crate::widget_builder::WidgetBuilder;
8022 use std::cell::RefCell;
8023 use std::rc::Rc;
8024
8025 #[derive(Debug)]
8032 struct RowWidget {
8033 children: Vec<WidgetId>,
8034 }
8035
8036 impl crate::widget::Widget for RowWidget {
8037 fn layout_response(
8038 &self,
8039 proposal: SizeProposal,
8040 _ctx: &LayoutContext,
8041 ) -> crate::widget::LayoutResponse {
8042 proposal.resolve(0.0, 0.0).into()
8043 }
8044
8045 fn place_children(
8046 &self,
8047 bounds: Rect,
8048 _proposal: SizeProposal,
8049 children: &mut [WidgetPlacement],
8050 _ctx: &LayoutContext,
8051 ) {
8052 let n = children.len().max(1) as f32;
8053 let w = bounds.width / n;
8054 for (i, child) in children.iter_mut().enumerate() {
8055 child.origin = Point::new(bounds.x + w * i as f32, bounds.y);
8056 child.size = teksilo_canvas::Size::new(w, bounds.height);
8057 }
8058 }
8059
8060 fn children(&self) -> Vec<WidgetId> {
8061 self.children.clone()
8062 }
8063 }
8064
8065 fn record(drive: impl FnOnce(&mut WidgetTree)) -> Vec<String> {
8068 let log: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(Vec::new()));
8069
8070 let mut tree = WidgetTree::new();
8071 let pointer_log = log.clone();
8072 let hover_log = log.clone();
8073 tree.add(
8074 FillWidget::new()
8075 .on_pointer_event(move |event, _ctx| {
8076 pointer_log.borrow_mut().push(format!("{event:?}"));
8077 EventResponse::Ignored
8078 })
8079 .on_hover(move |entered, _ctx| {
8080 hover_log.borrow_mut().push(format!("hover({entered})"));
8081 }),
8082 );
8083 tree.layout(SizeProposal::exact(100.0, 100.0));
8084
8085 drive(&mut tree);
8086 log.borrow().clone()
8087 }
8088
8089 #[test]
8095 fn a_mouse_sample_reproduces_todays_event_stream() {
8096 let inside = Point::new(40.0, 40.0);
8097 let moved = Point::new(60.0, 55.0);
8098 let outside = Point::new(400.0, 400.0);
8099
8100 let legacy = record(|tree| {
8101 tree.dispatch_event(WidgetEvent::pointer_move(inside));
8102 tree.dispatch_event(WidgetEvent::pointer_down(
8103 inside,
8104 PointerButton::Primary,
8105 Modifiers::NONE,
8106 ));
8107 tree.dispatch_event(WidgetEvent::pointer_move(moved));
8108 tree.dispatch_event(WidgetEvent::pointer_up(
8109 moved,
8110 PointerButton::Primary,
8111 Modifiers::NONE,
8112 ));
8113 tree.dispatch_event(WidgetEvent::pointer_move(outside));
8114 });
8115
8116 let sampled = record(|tree| {
8117 let t = EventTime::ZERO;
8118 tree.dispatch_pointer(PointerSample::mouse(PointerPhase::Move, inside, t));
8119 tree.dispatch_pointer(
8120 PointerSample::mouse(PointerPhase::Down, inside, t)
8121 .with_button(PointerButton::Primary),
8122 );
8123 tree.dispatch_pointer(PointerSample::mouse(PointerPhase::Move, moved, t));
8124 tree.dispatch_pointer(
8125 PointerSample::mouse(PointerPhase::Up, moved, t)
8126 .with_button(PointerButton::Primary),
8127 );
8128 tree.dispatch_pointer(PointerSample::mouse(PointerPhase::Move, outside, t));
8129 });
8130
8131 assert_eq!(legacy, sampled);
8132 assert!(
8133 legacy.contains(&"hover(true)".to_string())
8134 && legacy.contains(&"hover(false)".to_string()),
8135 "the fixture must actually exercise enter and leave: {legacy:?}"
8136 );
8137 }
8138
8139 #[test]
8143 fn a_buttonless_press_lowers_to_primary() {
8144 let events = record(|tree| {
8145 tree.dispatch_pointer(PointerSample::mouse(
8146 PointerPhase::Down,
8147 Point::new(20.0, 20.0),
8148 EventTime::ZERO,
8149 ));
8150 });
8151 assert!(
8152 events.iter().any(|e| e.contains("button: Primary")),
8153 "{events:?}"
8154 );
8155 }
8156
8157 #[test]
8163 fn a_sampled_press_and_release_form_a_tap() {
8164 let taps = Rc::new(std::cell::Cell::new(0));
8165 let seen = taps.clone();
8166 let mut tree = WidgetTree::new();
8167 tree.add(FillWidget::new().on_tap(move |_event, _ctx| seen.set(seen.get() + 1)));
8168 tree.layout(SizeProposal::exact(100.0, 100.0));
8169
8170 let at = Point::new(50.0, 50.0);
8171 tree.dispatch_pointer(PointerSample::mouse(
8172 PointerPhase::Down,
8173 at,
8174 EventTime::from_millis(0),
8175 ));
8176 tree.dispatch_pointer(PointerSample::mouse(
8177 PointerPhase::Up,
8178 at,
8179 EventTime::from_millis(5),
8180 ));
8181
8182 assert_eq!(taps.get(), 1);
8183 }
8184
8185 fn scroll_fixture() -> (WidgetTree, Rc<RefCell<Vec<&'static str>>>) {
8190 let hits: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
8191 let mut tree = WidgetTree::new();
8192
8193 let leading_hits = hits.clone();
8194 let trailing_hits = hits.clone();
8195 let leading = tree.add(FillWidget::new().on_scroll(move |_event, _ctx| {
8196 leading_hits.borrow_mut().push("leading");
8197 EventResponse::Handled
8198 }));
8199 let trailing = tree.add(FillWidget::new().on_scroll(move |_event, _ctx| {
8200 trailing_hits.borrow_mut().push("trailing");
8201 EventResponse::Handled
8202 }));
8203 tree.add(RowWidget {
8204 children: vec![leading, trailing],
8205 });
8206 tree.layout(SizeProposal::exact(200.0, 100.0));
8207 (tree, hits)
8208 }
8209
8210 fn notch() -> ScrollDelta {
8211 ScrollDelta::Lines { x: 0.0, y: -1.0 }
8212 }
8213
8214 #[test]
8217 fn a_positionless_scroll_routes_by_hover() {
8218 let (mut tree, hits) = scroll_fixture();
8219
8220 tree.pointer_move(Point::new(150.0, 50.0)); tree.dispatch_event(WidgetEvent::scroll(notch(), Modifiers::NONE));
8222 assert_eq!(*hits.borrow(), vec!["trailing"]);
8223
8224 tree.pointer_move(Point::new(50.0, 50.0)); tree.dispatch_scroll(ScrollSample::wheel(
8226 notch(),
8227 Modifiers::NONE,
8228 EventTime::ZERO,
8229 ));
8230 assert_eq!(*hits.borrow(), vec!["trailing", "leading"]);
8231 }
8232
8233 #[test]
8238 fn a_positioned_scroll_routes_by_hit_test() {
8239 let (mut tree, hits) = scroll_fixture();
8240 tree.pointer_move(Point::new(150.0, 50.0)); tree.dispatch_event(WidgetEvent::scroll_at(
8243 notch(),
8244 Modifiers::NONE,
8245 Point::new(50.0, 50.0), ));
8247 assert_eq!(*hits.borrow(), vec!["leading"]);
8248
8249 tree.dispatch_scroll(
8250 ScrollSample::wheel(notch(), Modifiers::NONE, EventTime::ZERO)
8251 .at(Point::new(150.0, 50.0)),
8252 );
8253 assert_eq!(*hits.borrow(), vec!["leading", "trailing"]);
8254 }
8255
8256 #[test]
8260 fn a_positionless_scroll_with_no_hover_goes_nowhere() {
8261 let (mut tree, hits) = scroll_fixture();
8262 tree.dispatch_event(WidgetEvent::scroll(notch(), Modifiers::NONE));
8263 assert!(hits.borrow().is_empty());
8264 }
8265
8266 #[test]
8271 fn a_handler_sees_the_sample_it_is_serving() {
8272 let seen: Rc<RefCell<Vec<(ScrollPhase, ScrollSource, Option<Point>)>>> =
8273 Rc::new(RefCell::new(Vec::new()));
8274 let sink = seen.clone();
8275
8276 let mut tree = WidgetTree::new();
8277 tree.add(
8284 FillWidget::new()
8285 .scroll_container(crate::pointer::touch_action::PanAxes::BOTH)
8286 .on_scroll(move |_event, ctx| {
8287 sink.borrow_mut().push((
8288 ctx.scroll_phase(),
8289 ctx.scroll_source(),
8290 ctx.pointer_position(),
8291 ));
8292 EventResponse::Handled
8293 }),
8294 );
8295 tree.layout(SizeProposal::exact(100.0, 100.0));
8296
8297 tree.dispatch_scroll(ScrollSample {
8298 delta: notch(),
8299 position: Some(Point::new(50.0, 50.0)),
8300 phase: ScrollPhase::Momentum,
8301 source: ScrollSource::TouchPan,
8302 pointer: PointerInfo::mouse(EventTime::from_millis(12)),
8303 modifiers: Modifiers::NONE,
8304 });
8305
8306 assert_eq!(
8307 *seen.borrow(),
8308 vec![(
8309 ScrollPhase::Momentum,
8310 ScrollSource::TouchPan,
8311 Some(Point::new(50.0, 50.0))
8312 )]
8313 );
8314 }
8315
8316 #[test]
8319 fn a_legacy_event_reports_the_mouse_at_the_epoch() {
8320 let seen: Rc<RefCell<Option<(PointerId, ScrollPhase)>>> = Rc::new(RefCell::new(None));
8321 let sink = seen.clone();
8322
8323 let mut tree = WidgetTree::new();
8324 tree.add(FillWidget::new().on_pointer_event(move |_event, ctx| {
8325 *sink.borrow_mut() = Some((ctx.pointer().id, ctx.scroll_phase()));
8326 EventResponse::Ignored
8327 }));
8328 tree.layout(SizeProposal::exact(100.0, 100.0));
8329 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(50.0, 50.0)));
8330
8331 assert_eq!(
8332 *seen.borrow(),
8333 Some((PointerId::MOUSE, ScrollPhase::Discrete))
8334 );
8335 }
8336
8337 #[test]
8341 fn the_snapshot_is_restored_after_a_dispatch() {
8342 let mut tree = WidgetTree::new();
8343 tree.add(FillWidget::new());
8344 tree.layout(SizeProposal::exact(100.0, 100.0));
8345
8346 let before = tree.current_input.clone();
8347 tree.dispatch_scroll(
8348 ScrollSample::wheel(notch(), Modifiers::NONE, EventTime::ZERO)
8349 .at(Point::new(10.0, 10.0)),
8350 );
8351 assert_eq!(tree.current_input, before);
8352 }
8353}
8354
8355#[cfg(test)]
8356mod press_and_focus_tests {
8357 use std::cell::{Cell, RefCell};
8362 use std::rc::Rc;
8363 use teksilo_canvas::{Point, SizeProposal};
8364
8365 use crate::WidgetId;
8366 use crate::event::{EventResponse, Key, Modifiers, PointerButton, WidgetEvent};
8367 use crate::focus::FocusOrigin;
8368 use crate::pointer::clock::ManualClock;
8369 use crate::pointer::touch_action::PanClaim;
8370 use crate::pointer::{
8371 BackendDeviceKey, EventTime, PointerId, PointerIdAllocator, PointerInfo, PointerPhase,
8372 PointerSample,
8373 };
8374 use crate::test_widgets::{FillWidget, StackWidget};
8375 use crate::widget_builder::WidgetBuilder;
8376 use crate::widget_tree::WidgetTree;
8377
8378 fn contact_id() -> PointerId {
8384 use std::sync::atomic::{AtomicU64, Ordering};
8385 static NEXT: AtomicU64 = AtomicU64::new(1);
8386 PointerIdAllocator::global().begin(
8387 BackendDeviceKey::new(0x0B11),
8388 NEXT.fetch_add(1, Ordering::Relaxed),
8389 )
8390 }
8391
8392 fn contact(id: PointerId, phase: PointerPhase, at: Point, t: EventTime) -> PointerSample {
8393 PointerSample {
8394 pointer: PointerInfo::touch(id, t),
8395 phase,
8396 position: at,
8397 button: None,
8398 modifiers: Modifiers::NONE,
8399 coalesced: Vec::new(),
8400 }
8401 }
8402
8403 fn tree_on_a_clock() -> (WidgetTree, Rc<ManualClock>) {
8406 let mut tree = WidgetTree::new();
8407 let clock = Rc::new(ManualClock::new(EventTime::ZERO));
8408 tree.set_input_clock(clock.clone());
8409 (tree, clock)
8410 }
8411
8412 fn tappable(tree: &mut WidgetTree) -> (WidgetId, crate::signal::Signal<bool>) {
8419 let id = tree.add(FillWidget::new().focusable().on_tap(|_e, _c| {}));
8420 let signal = tree.pressed_signal(id);
8421 (id, signal)
8422 }
8423
8424 #[test]
8431 fn a_pointer_origin_names_its_device() {
8432 use teksilo_tokens::{PenKind, PointerKind};
8433
8434 assert!(FocusOrigin::Pointer(PointerKind::Touch).is_pointer());
8435 assert!(FocusOrigin::Pointer(PointerKind::Pen(PenKind::Pen)).is_pointer());
8436 assert!(FocusOrigin::POINTER.is_pointer());
8437 assert!(!FocusOrigin::Keyboard.is_pointer());
8438 assert!(!FocusOrigin::Programmatic.is_pointer());
8439 assert!(!FocusOrigin::Accessibility.is_pointer());
8440
8441 assert_eq!(
8442 FocusOrigin::Pointer(PointerKind::Touch).pointer_kind(),
8443 Some(PointerKind::Touch),
8444 );
8445 assert_eq!(FocusOrigin::Keyboard.pointer_kind(), None);
8446 assert_eq!(
8447 FocusOrigin::POINTER.pointer_kind(),
8448 Some(PointerKind::Unknown),
8449 "a widget deriving its own origin says so rather than naming a device it never saw",
8450 );
8451 }
8452
8453 #[test]
8457 fn only_a_real_navigation_declares_a_modality() {
8458 use teksilo_tokens::PointerKind;
8459
8460 assert_eq!(FocusOrigin::Keyboard.focus_visible(), Some(true));
8461 assert_eq!(FocusOrigin::Accessibility.focus_visible(), Some(true));
8462 assert_eq!(
8463 FocusOrigin::Pointer(PointerKind::Touch).focus_visible(),
8464 Some(false),
8465 );
8466 assert_eq!(FocusOrigin::Programmatic.focus_visible(), None);
8467 }
8468
8469 #[test]
8472 fn the_tier_three_configs_are_unchanged() {
8473 let origin: crate::signal::Signal<Option<FocusOrigin>> =
8474 crate::signal::Signal::new(Some(FocusOrigin::Keyboard));
8475 let slider: crate::signal::Signal<Option<FocusOrigin>> = origin.clone();
8476 let splitter: crate::signal::Signal<Option<FocusOrigin>> = origin.clone();
8477 let segmented: crate::signal::Signal<Option<FocusOrigin>> = origin.clone();
8478 for field in [slider, splitter, segmented] {
8479 assert_eq!(field.get(), Some(FocusOrigin::Keyboard));
8480 }
8481 origin.set(Some(FocusOrigin::POINTER));
8482 assert_ne!(
8483 origin.get(),
8484 Some(FocusOrigin::Keyboard),
8485 "the `== Some(Keyboard)` test every consumer makes still discriminates",
8486 );
8487 }
8488
8489 #[test]
8500 fn a_touch_tap_after_keyboard_focus_leaves_no_ring() {
8501 let (mut tree, _clock) = tree_on_a_clock();
8502 let a = tree.add(FillWidget::new().focusable());
8503 let b = tree.add(FillWidget::new().focusable());
8504 let root = tree.add(SideBySide {
8505 children: vec![a, b],
8506 });
8507 tree.layout(SizeProposal::exact(200.0, 100.0));
8508 let _ = root;
8509
8510 let visible = tree.focus_visible_signal();
8511 tree.press_key(Key::Tab, Modifiers::NONE);
8512 assert_eq!(tree.focused(), Some(a));
8513 assert!(visible.get(), "keyboard navigation reveals the ring");
8514
8515 let id = contact_id();
8516 let at = tree.bounds(b).center();
8517 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8518 tree.dispatch_pointer(contact(
8519 id,
8520 PointerPhase::Up,
8521 at,
8522 EventTime::from_millis(30),
8523 ));
8524
8525 assert_eq!(tree.focused(), Some(b), "the release moved focus");
8526 assert!(!visible.get(), "and the ring did not come with it");
8527 assert_eq!(
8528 tree.focus_origin(),
8529 Some(FocusOrigin::Pointer(teksilo_tokens::PointerKind::Touch)),
8530 );
8531 }
8532
8533 #[test]
8538 fn an_assistive_focus_reveals_the_ring() {
8539 let mut tree = WidgetTree::new();
8540 let a = tree.add(FillWidget::new().focusable());
8541 let b = tree.add(FillWidget::new().focusable());
8542 let root = tree.add(SideBySide {
8543 children: vec![a, b],
8544 });
8545 tree.layout(SizeProposal::exact(200.0, 50.0));
8546 let _ = root;
8547 let visible = tree.focus_visible_signal();
8548
8549 tree.click(a);
8550 assert_eq!(tree.focused(), Some(a));
8551 assert!(!visible.get(), "the click hid it");
8552
8553 tree.dispatch_event(WidgetEvent::AccessAction {
8554 target: Some(b),
8555 action: accesskit::Action::Focus,
8556 target_node: crate::accessibility::widget_id_to_node_id(b),
8557 data: None,
8558 });
8559 assert_eq!(tree.focused(), Some(b));
8560 assert!(visible.get(), "an assistive move is a navigation");
8561 assert_eq!(tree.focus_origin(), Some(FocusOrigin::Accessibility));
8562 }
8563
8564 struct ItemFocusFixture {
8568 tree: WidgetTree,
8569 other: WidgetId,
8570 group: WidgetId,
8572 owner: WidgetId,
8573 refused: WidgetId,
8576 taken: WidgetId,
8577 label: WidgetId,
8579 cursor: Rc<Cell<Option<accesskit::NodeId>>>,
8580 catch_all: Rc<Cell<u32>>,
8581 }
8582
8583 fn item_focus_fixture() -> ItemFocusFixture {
8584 let advertises_focus = |b: &mut crate::accessibility::AccessNodeBuilder| {
8585 b.add_action(accesskit::Action::Focus)
8586 };
8587 let mut tree = WidgetTree::new();
8588 let other = tree.add(FillWidget::new().focusable());
8589 let refused = tree.add(FillWidget::new().access_customize(advertises_focus));
8590 let taken = tree.add(FillWidget::new().access_customize(advertises_focus));
8591 let label = tree.add(FillWidget::new());
8592 let cursor: Rc<Cell<Option<accesskit::NodeId>>> = Rc::new(Cell::new(None));
8593 let owner_slot: Rc<Cell<Option<WidgetId>>> = Rc::new(Cell::new(None));
8594 let taken_node = crate::accessibility::widget_id_to_node_id(taken);
8595 let owner = tree.add(
8596 SideBySide {
8597 children: vec![refused, taken],
8598 }
8599 .focusable(true)
8600 .on_access_action_request({
8601 let cursor = cursor.clone();
8602 let owner_slot = owner_slot.clone();
8603 move |action, node, _data, ctx| {
8604 if action != accesskit::Action::Focus || node != taken_node {
8605 return EventResponse::Ignored;
8606 }
8607 cursor.set(Some(node));
8608 if let Some(owner) = owner_slot.get() {
8609 ctx.request_focus(owner);
8610 }
8611 EventResponse::Handled
8612 }
8613 }),
8614 );
8615 owner_slot.set(Some(owner));
8616 let group = tree.add(SideBySide {
8618 children: vec![owner],
8619 });
8620 let catch_all = Rc::new(Cell::new(0));
8621 let card = tree.add(
8622 SideBySide {
8623 children: vec![group, label],
8624 }
8625 .focusable(true)
8626 .on_access_action({
8627 let catch_all = catch_all.clone();
8628 move |action, _ctx| {
8629 if action != accesskit::Action::Focus {
8630 return EventResponse::Ignored;
8631 }
8632 catch_all.set(catch_all.get() + 1);
8633 EventResponse::Handled
8634 }
8635 }),
8636 );
8637 tree.add(SideBySide {
8638 children: vec![other, card],
8639 });
8640 tree.layout(SizeProposal::exact(400.0, 50.0));
8641 ItemFocusFixture {
8642 tree,
8643 other,
8644 group,
8645 owner,
8646 refused,
8647 taken,
8648 label,
8649 cursor,
8650 catch_all,
8651 }
8652 }
8653
8654 fn assistive_focus(tree: &mut WidgetTree, id: WidgetId) -> bool {
8655 tree.dispatch_access_action(
8656 crate::accessibility::widget_id_to_node_id(id),
8657 accesskit::Action::Focus,
8658 None,
8659 &mut crate::window::NoopWindowOps,
8660 )
8661 }
8662
8663 #[test]
8670 fn an_assistive_focus_on_an_item_goes_to_the_widget_that_points_at_it() {
8671 let mut f = item_focus_fixture();
8672 let visible = f.tree.focus_visible_signal();
8673 f.tree.click(f.other);
8674 assert!(!visible.get());
8675
8676 assert!(
8677 assistive_focus(&mut f.tree, f.taken),
8678 "the widget that points at the item took it"
8679 );
8680 let item = crate::accessibility::widget_id_to_node_id(f.taken);
8681 assert_eq!(f.cursor.get(), Some(item), "its cursor moved to the item");
8682 assert_eq!(
8683 f.tree.focused(),
8684 Some(f.owner),
8685 "the keyboard is on the widget, not on the item"
8686 );
8687 assert!(visible.get(), "an assistive move is a navigation");
8688 assert_eq!(f.tree.focus_origin(), Some(FocusOrigin::Accessibility));
8689 assert_eq!(
8690 f.catch_all.get(),
8691 0,
8692 "no ancestor above the owner was asked"
8693 );
8694 }
8695
8696 #[test]
8700 fn an_item_its_owner_refuses_takes_no_assistive_focus() {
8701 let mut f = item_focus_fixture();
8702 f.tree.click(f.other);
8703 assert!(!assistive_focus(&mut f.tree, f.refused));
8704 assert_eq!(f.tree.focused(), Some(f.other), "focus did not move");
8705 assert_eq!(f.cursor.get(), None);
8706 assert_eq!(f.catch_all.get(), 0);
8707 }
8708
8709 #[test]
8712 fn a_node_that_offers_no_focus_takes_no_assistive_focus() {
8713 let mut f = item_focus_fixture();
8714 f.tree.click(f.other);
8715 assert!(!assistive_focus(&mut f.tree, f.label));
8716 assert_eq!(f.tree.focused(), Some(f.other));
8717 assert_eq!(f.catch_all.get(), 0);
8718 }
8719
8720 #[test]
8724 fn a_greyed_item_reaches_its_owner_and_a_disabled_container_takes_nothing() {
8725 let mut f = item_focus_fixture();
8726 let item_enabled = crate::signal::Signal::new(false);
8727 f.tree.enabled_when(f.taken, item_enabled.clone());
8728 f.tree.layout(SizeProposal::exact(400.0, 50.0));
8729 f.tree.click(f.other);
8730 assert!(assistive_focus(&mut f.tree, f.taken), "a greyed item");
8731 assert_eq!(f.tree.focused(), Some(f.owner));
8732
8733 let mut f = item_focus_fixture();
8734 f.tree
8735 .enabled_when(f.group, crate::signal::Signal::new(false));
8736 f.tree.layout(SizeProposal::exact(400.0, 50.0));
8737 f.tree.click(f.other);
8738 assert!(
8739 !assistive_focus(&mut f.tree, f.taken),
8740 "a disabled container"
8741 );
8742 assert_eq!(f.tree.focused(), Some(f.other));
8743 assert_eq!(f.cursor.get(), None);
8744 assert_eq!(f.catch_all.get(), 0);
8745 }
8746
8747 #[test]
8751 fn a_composite_in_a_catch_all_ancestor_gets_its_stop() {
8752 let mut tree = WidgetTree::new();
8753 let other = tree.add(FillWidget::new().focusable());
8754 let leaf = tree.add(FillWidget::new().focusable());
8755 let composite = tree.add(
8756 SideBySide {
8757 children: vec![leaf],
8758 }
8759 .access_customize(|b| b.add_action(accesskit::Action::Focus)),
8760 );
8761 let catch_all = Rc::new(Cell::new(0));
8762 let card = tree.add(
8763 SideBySide {
8764 children: vec![composite],
8765 }
8766 .focusable(true)
8767 .on_access_action({
8768 let catch_all = catch_all.clone();
8769 move |action, _ctx| {
8770 if action == accesskit::Action::Focus {
8771 catch_all.set(catch_all.get() + 1);
8772 return EventResponse::Handled;
8773 }
8774 EventResponse::Ignored
8775 }
8776 }),
8777 );
8778 tree.add(SideBySide {
8779 children: vec![other, card],
8780 });
8781 tree.layout(SizeProposal::exact(400.0, 50.0));
8782 tree.click(other);
8783 assert!(assistive_focus(&mut tree, composite));
8784 assert_eq!(tree.focused(), Some(leaf), "the keyboard is on its stop");
8785 assert_eq!(catch_all.get(), 0);
8786 }
8787
8788 #[derive(Debug)]
8791 struct SyntheticOwner {
8792 advertises: bool,
8793 }
8794
8795 impl crate::widget::Widget for SyntheticOwner {
8796 fn layout_response(
8797 &self,
8798 proposal: SizeProposal,
8799 _ctx: &crate::widget::LayoutContext,
8800 ) -> crate::widget::LayoutResponse {
8801 proposal.resolve(0.0, 0.0).into()
8802 }
8803
8804 fn accessibility(&self, builder: &mut crate::accessibility::AccessNodeBuilder) {
8805 let advertises = self.advertises;
8806 builder.push_scene_child(1, crate::accessibility::SyntheticKind::ChartMark, |child| {
8807 child.set_role(accesskit::Role::GraphicsObject);
8808 if advertises {
8809 child.add_action(accesskit::Action::Focus);
8810 }
8811 });
8812 }
8813 }
8814
8815 fn catch_all_card(tree: &mut WidgetTree, child: WidgetId) -> (WidgetId, Rc<Cell<u32>>) {
8817 let catch_all = Rc::new(Cell::new(0));
8818 let card = tree.add(
8819 SideBySide {
8820 children: vec![child],
8821 }
8822 .focusable(true)
8823 .on_access_action({
8824 let catch_all = catch_all.clone();
8825 move |action, _ctx| {
8826 if action == accesskit::Action::Focus {
8827 catch_all.set(catch_all.get() + 1);
8828 return EventResponse::Handled;
8829 }
8830 EventResponse::Ignored
8831 }
8832 }),
8833 );
8834 (card, catch_all)
8835 }
8836
8837 #[test]
8841 fn a_disabled_synthetic_owner_or_composite_offers_nothing_upward() {
8842 let mut tree = WidgetTree::new();
8843 let other = tree.add(FillWidget::new().focusable());
8844 let owner = tree.add(SyntheticOwner { advertises: true }.focusable(true));
8845 let leaf = tree.add(FillWidget::new().focusable());
8846 let composite = tree.add(
8847 SideBySide {
8848 children: vec![leaf],
8849 }
8850 .access_customize(|b| b.add_action(accesskit::Action::Focus)),
8851 );
8852 let group = tree.add(SideBySide {
8853 children: vec![owner, composite],
8854 });
8855 let (card, catch_all) = catch_all_card(&mut tree, group);
8856 tree.add(SideBySide {
8857 children: vec![other, card],
8858 });
8859 tree.layout(SizeProposal::exact(400.0, 50.0));
8860 let _ = tree.sync_accessibility();
8861 tree.enabled_when(owner, crate::signal::Signal::new(false));
8862 tree.enabled_when(composite, crate::signal::Signal::new(false));
8863 tree.layout(SizeProposal::exact(400.0, 50.0));
8864 tree.click(other);
8865
8866 let mark = crate::accessibility::synthetic_node_id(
8867 owner,
8868 1,
8869 crate::accessibility::SyntheticKind::ChartMark,
8870 );
8871 let handled = tree.dispatch_access_action(
8872 mark,
8873 accesskit::Action::Focus,
8874 None,
8875 &mut crate::window::NoopWindowOps,
8876 );
8877 assert!(!handled, "a disabled owner's item");
8878 assert!(
8879 !assistive_focus(&mut tree, composite),
8880 "a disabled composite"
8881 );
8882 assert_eq!(tree.focused(), Some(other));
8883 assert_eq!(catch_all.get(), 0, "no ancestor was asked");
8884 }
8885
8886 #[test]
8889 fn a_synthetic_node_that_offers_no_focus_is_offered_to_no_one() {
8890 let mut tree = WidgetTree::new();
8891 let other = tree.add(FillWidget::new().focusable());
8892 let owner = tree.add(SyntheticOwner { advertises: false });
8894 let (card, catch_all) = catch_all_card(&mut tree, owner);
8895 tree.add(SideBySide {
8896 children: vec![other, card],
8897 });
8898 tree.layout(SizeProposal::exact(400.0, 50.0));
8899 let _ = tree.sync_accessibility();
8900 tree.click(other);
8901 let mark = crate::accessibility::synthetic_node_id(
8902 owner,
8903 1,
8904 crate::accessibility::SyntheticKind::ChartMark,
8905 );
8906 let _ = tree.dispatch_access_action(
8907 mark,
8908 accesskit::Action::Focus,
8909 None,
8910 &mut crate::window::NoopWindowOps,
8911 );
8912 assert_eq!(catch_all.get(), 0, "no handler was asked");
8913 }
8914
8915 #[test]
8919 fn an_answer_that_moves_no_focus_is_no_success() {
8920 let mut tree = WidgetTree::new();
8921 let other = tree.add(FillWidget::new().focusable());
8922 let item = tree
8923 .add(FillWidget::new().access_customize(|b| b.add_action(accesskit::Action::Focus)));
8924 let owner = tree.add(
8925 SideBySide {
8926 children: vec![item],
8927 }
8928 .focusable(true)
8929 .on_access_action(|action, _ctx| {
8930 if action == accesskit::Action::Focus {
8931 EventResponse::Handled
8932 } else {
8933 EventResponse::Ignored
8934 }
8935 }),
8936 );
8937 tree.add(SideBySide {
8938 children: vec![other, owner],
8939 });
8940 tree.layout(SizeProposal::exact(400.0, 50.0));
8941 let visible = tree.focus_visible_signal();
8942 tree.click(other);
8943 assert!(!assistive_focus(&mut tree, item), "reported unhandled");
8944 assert_eq!(tree.focused(), Some(other));
8945 assert!(!visible.get(), "no ring on the mouse-focused control");
8946 }
8947
8948 #[test]
8953 fn a_programmatic_focus_leaves_the_modality_alone() {
8954 let mut tree = WidgetTree::new();
8955 let a = tree.add(FillWidget::new().focusable());
8956 let b = tree.add(FillWidget::new().focusable());
8957 let root = tree.add(SideBySide {
8958 children: vec![a, b],
8959 });
8960 tree.layout(SizeProposal::exact(200.0, 100.0));
8961 let _ = root;
8962 let visible = tree.focus_visible_signal();
8963
8964 tree.press_key(Key::Tab, Modifiers::NONE);
8965 assert_eq!(tree.focused(), Some(a));
8966 assert!(visible.get());
8967 tree.focus(b);
8968 assert!(
8969 visible.get(),
8970 "a scripted focus does not hide a keyboard ring"
8971 );
8972
8973 tree.click(a);
8974 assert!(!visible.get());
8975 tree.focus(b);
8976 assert!(
8977 !visible.get(),
8978 "and does not reveal one after a click either",
8979 );
8980 }
8981
8982 #[test]
8988 fn a_mouse_still_focuses_on_press() {
8989 let mut tree = WidgetTree::new();
8990 let w = tree.add(FillWidget::new().focusable());
8991 tree.layout(SizeProposal::exact(100.0, 50.0));
8992 let center = tree.bounds(w).center();
8993
8994 tree.dispatch_event(WidgetEvent::pointer_down(
8995 center,
8996 PointerButton::Primary,
8997 Modifiers::NONE,
8998 ));
8999 assert_eq!(
9000 tree.focused(),
9001 Some(w),
9002 "focus lands on the press for an indirect pointer",
9003 );
9004 }
9005
9006 #[test]
9008 fn a_finger_focuses_on_release() {
9009 let (mut tree, _clock) = tree_on_a_clock();
9010 let w = tree.add(FillWidget::new().focusable());
9011 tree.layout(SizeProposal::exact(100.0, 50.0));
9012 let center = tree.bounds(w).center();
9013
9014 let id = contact_id();
9015 tree.dispatch_pointer(contact(id, PointerPhase::Down, center, EventTime::ZERO));
9016 assert_eq!(
9017 tree.focused(),
9018 None,
9019 "a finger that has only landed has chosen nothing yet",
9020 );
9021
9022 tree.dispatch_pointer(contact(
9023 id,
9024 PointerPhase::Up,
9025 center,
9026 EventTime::from_millis(40),
9027 ));
9028 assert_eq!(tree.focused(), Some(w));
9029 assert_eq!(
9030 tree.focus_origin().and_then(FocusOrigin::pointer_kind),
9031 Some(teksilo_tokens::PointerKind::Touch),
9032 "the origin names the device that delivered it",
9033 );
9034 }
9035
9036 #[test]
9040 fn a_release_on_a_different_focusable_moves_no_focus() {
9041 let (mut tree, _clock) = tree_on_a_clock();
9042 let a = tree.add(FillWidget::new().focusable());
9043 let b = tree.add(FillWidget::new().focusable());
9044 let root = tree.add(SideBySide {
9045 children: vec![a, b],
9046 });
9047 tree.layout(SizeProposal::exact(200.0, 50.0));
9048 let _ = root;
9049
9050 let on_a = tree.bounds(a).center();
9051 let on_b = tree.bounds(b).center();
9052 let id = contact_id();
9053 tree.dispatch_pointer(contact(id, PointerPhase::Down, on_a, EventTime::ZERO));
9054 tree.dispatch_pointer(contact(
9055 id,
9056 PointerPhase::Move,
9057 on_b,
9058 EventTime::from_millis(20),
9059 ));
9060 tree.dispatch_pointer(contact(
9061 id,
9062 PointerPhase::Up,
9063 on_b,
9064 EventTime::from_millis(40),
9065 ));
9066
9067 assert_eq!(
9068 tree.focused(),
9069 None,
9070 "the guard is `the release landed on the same focusable as the press`",
9071 );
9072 }
9073
9074 #[test]
9081 fn a_mouse_press_visual_is_unchanged() {
9082 let mut tree = WidgetTree::new();
9083 let (w, pressed) = tappable(&mut tree);
9084 tree.layout(SizeProposal::exact(100.0, 50.0));
9085 let center = tree.bounds(w).center();
9086
9087 assert!(!pressed.get(), "not pressed at rest");
9088 tree.dispatch_event(WidgetEvent::pointer_down(
9089 center,
9090 PointerButton::Primary,
9091 Modifiers::NONE,
9092 ));
9093 assert!(pressed.get(), "an indirect pointer lights up on the press");
9094 assert!(
9095 !tree.press_pending(w),
9096 "and never waits: a mouse opens no pan session, so there is no \
9097 ambiguity to wait out",
9098 );
9099 assert_eq!(tree.pressed_by(w), Some(PointerId::MOUSE));
9100
9101 tree.dispatch_event(WidgetEvent::pointer_up(
9102 center,
9103 PointerButton::Primary,
9104 Modifiers::NONE,
9105 ));
9106 assert!(!pressed.get(), "the release clears it");
9107 assert_eq!(tree.pressed_by(w), None);
9108 }
9109
9110 #[test]
9114 fn a_slide_off_clears_the_visual_and_re_entry_restores_it() {
9115 let (mut tree, _clock) = tree_on_a_clock();
9116 let (w, pressed) = tappable(&mut tree);
9117 tree.layout(SizeProposal::exact(100.0, 200.0));
9118 let inside = tree.bounds(w).center();
9119 let outside = Point::new(inside.x, inside.y + 400.0);
9120
9121 let id = contact_id();
9122 tree.dispatch_pointer(contact(id, PointerPhase::Down, inside, EventTime::ZERO));
9123 assert!(
9124 pressed.get(),
9125 "nothing here claims a pan, so no delay applies"
9126 );
9127
9128 tree.dispatch_pointer(contact(
9129 id,
9130 PointerPhase::Move,
9131 outside,
9132 EventTime::from_millis(20),
9133 ));
9134 assert!(!pressed.get(), "the press has left its target");
9135 assert!(!tree.press_is_inside(w));
9136 assert_eq!(
9137 tree.pressed_by(w),
9138 Some(id),
9139 "the contact still holds the press — it is the *visual* that is off",
9140 );
9141
9142 tree.dispatch_pointer(contact(
9143 id,
9144 PointerPhase::Move,
9145 inside,
9146 EventTime::from_millis(40),
9147 ));
9148 assert!(pressed.get(), "sliding back on restores it");
9149
9150 tree.dispatch_pointer(contact(
9151 id,
9152 PointerPhase::Up,
9153 inside,
9154 EventTime::from_millis(60),
9155 ));
9156 assert!(!pressed.get());
9157 }
9158
9159 #[test]
9162 fn a_second_contact_cannot_clear_the_first_visual() {
9163 let (mut tree, _clock) = tree_on_a_clock();
9164 let (w, pressed) = tappable(&mut tree);
9165 tree.layout(SizeProposal::exact(100.0, 50.0));
9166 let bounds = tree.bounds(w);
9167 let first_at = Point::new(bounds.x + 20.0, bounds.y + 25.0);
9168 let second_at = Point::new(bounds.x + 70.0, bounds.y + 25.0);
9169
9170 let first = contact_id();
9171 let second = contact_id();
9172 tree.dispatch_pointer(contact(
9173 first,
9174 PointerPhase::Down,
9175 first_at,
9176 EventTime::ZERO,
9177 ));
9178 assert!(pressed.get());
9179 assert_eq!(tree.pressed_by(w), Some(first));
9180
9181 tree.dispatch_pointer(contact(
9182 second,
9183 PointerPhase::Down,
9184 second_at,
9185 EventTime::from_millis(10),
9186 ));
9187 assert_eq!(
9188 tree.pressed_by(w),
9189 Some(first),
9190 "under `MultiContact::First` the second contact is terminated before \
9191 it reaches the arena at all",
9192 );
9193
9194 tree.dispatch_pointer(contact(
9195 second,
9196 PointerPhase::Up,
9197 second_at,
9198 EventTime::from_millis(20),
9199 ));
9200 assert!(
9201 pressed.get(),
9202 "so the second contact's release cannot clear a visual it never owned",
9203 );
9204 assert_eq!(tree.pressed_by(w), Some(first));
9205
9206 tree.dispatch_pointer(contact(
9207 first,
9208 PointerPhase::Up,
9209 first_at,
9210 EventTime::from_millis(30),
9211 ));
9212 assert!(!pressed.get(), "the owner's release does clear it");
9213 }
9214
9215 #[test]
9218 fn a_cancel_clears_the_visual() {
9219 let (mut tree, _clock) = tree_on_a_clock();
9220 let (w, pressed) = tappable(&mut tree);
9221 tree.layout(SizeProposal::exact(100.0, 50.0));
9222 let center = tree.bounds(w).center();
9223
9224 let id = contact_id();
9225 tree.dispatch_pointer(contact(id, PointerPhase::Down, center, EventTime::ZERO));
9226 assert!(pressed.get());
9227
9228 tree.dispatch_pointer(contact(
9229 id,
9230 PointerPhase::Cancel,
9231 center,
9232 EventTime::from_millis(20),
9233 ));
9234 assert!(!pressed.get(), "a press that was taken away is not painted");
9235 assert_eq!(tree.pressed_by(w), None);
9236 }
9237
9238 #[test]
9241 fn a_pan_claim_clears_the_visual() {
9242 let (mut tree, clock) = tree_on_a_clock();
9243 let (row, pressed) = tappable(&mut tree);
9244 let list = tree.add(
9245 StackWidget::new()
9246 .child(row)
9247 .pan_claim(PanClaim::vertical())
9248 .on_scroll(|_e, _c| EventResponse::Handled),
9249 );
9250 tree.layout(SizeProposal::exact(200.0, 400.0));
9251 let _ = list;
9252 let start = Point::new(100.0, 200.0);
9253
9254 let id = contact_id();
9255 tree.dispatch_pointer(contact(id, PointerPhase::Down, start, EventTime::ZERO));
9256 assert!(tree.press_pending(row));
9258 clock.set(EventTime::from_millis(100));
9259 tree.tick_gestures(std::time::Instant::now());
9260 assert!(pressed.get(), "…and appears once the delay has elapsed");
9261
9262 tree.dispatch_pointer(contact(
9264 id,
9265 PointerPhase::Move,
9266 Point::new(100.0, 260.0),
9267 EventTime::from_millis(120),
9268 ));
9269 assert!(
9270 !pressed.get(),
9271 "the row lost the press to the list and must stop advertising it",
9272 );
9273 assert_eq!(tree.pressed_by(row), None);
9274 }
9275
9276 #[test]
9285 fn a_button_the_control_cannot_act_on_raises_no_visual() {
9286 let taps = Rc::new(Cell::new(0u32));
9287 let count = taps.clone();
9288 let mut tree = WidgetTree::new();
9289 let w = tree.add(FillWidget::new().focusable().on_tap(move |_e, _c| {
9290 count.set(count.get() + 1);
9291 }));
9292 let pressed = tree.pressed_signal(w);
9293 tree.layout(SizeProposal::exact(100.0, 50.0));
9294 let center = tree.bounds(w).center();
9295
9296 for button in [
9297 PointerButton::Middle,
9298 PointerButton::Back,
9299 PointerButton::Forward,
9300 ] {
9301 tree.dispatch_event(WidgetEvent::pointer_down(center, button, Modifiers::NONE));
9302 assert!(
9303 !pressed.get(),
9304 "{button:?} cannot activate an `on_tap` node, so it must not light one up",
9305 );
9306 assert_eq!(tree.pressed_by(w), None, "and owns no visual to clear");
9307 tree.dispatch_event(WidgetEvent::pointer_up(center, button, Modifiers::NONE));
9308 assert_eq!(
9309 taps.get(),
9310 0,
9311 "the tap recognizer refuses {button:?} too — that is the point",
9312 );
9313 }
9314
9315 tree.dispatch_event(WidgetEvent::pointer_down(
9317 center,
9318 PointerButton::Primary,
9319 Modifiers::NONE,
9320 ));
9321 assert!(pressed.get(), "a primary press lights up as it always has");
9322 assert_eq!(tree.pressed_by(w), Some(PointerId::MOUSE));
9323 tree.dispatch_event(WidgetEvent::pointer_up(
9324 center,
9325 PointerButton::Primary,
9326 Modifiers::NONE,
9327 ));
9328 assert!(!pressed.get());
9329 assert_eq!(taps.get(), 1);
9330 }
9331
9332 #[test]
9336 fn a_secondary_press_with_no_context_menu_raises_no_visual() {
9337 let mut tree = WidgetTree::new();
9338 let (w, pressed) = tappable(&mut tree);
9339 tree.layout(SizeProposal::exact(100.0, 50.0));
9340 let center = tree.bounds(w).center();
9341
9342 tree.dispatch_event(WidgetEvent::pointer_down(
9343 center,
9344 PointerButton::Secondary,
9345 Modifiers::NONE,
9346 ));
9347 assert!(
9348 !pressed.get(),
9349 "nothing opened, and nothing may look pressed either",
9350 );
9351 assert_eq!(tree.pressed_by(w), None);
9352 assert!(!tree.press_is_inside(w));
9353 }
9354
9355 #[test]
9359 fn a_widened_mask_widens_the_visual_with_it() {
9360 use crate::event::ButtonMask;
9361
9362 let mut tree = WidgetTree::new();
9363 let w = tree.add(
9364 FillWidget::new()
9365 .on_tap(|_e, _c| {})
9366 .accept_tap_buttons(ButtonMask::PRIMARY | ButtonMask::MIDDLE),
9367 );
9368 let pressed = tree.pressed_signal(w);
9369 tree.layout(SizeProposal::exact(100.0, 50.0));
9370 let center = tree.bounds(w).center();
9371
9372 tree.dispatch_event(WidgetEvent::pointer_down(
9373 center,
9374 PointerButton::Middle,
9375 Modifiers::NONE,
9376 ));
9377 assert!(pressed.get(), "this node really does act on a middle-click");
9378
9379 tree.dispatch_event(WidgetEvent::pointer_up(
9380 center,
9381 PointerButton::Middle,
9382 Modifiers::NONE,
9383 ));
9384 assert!(!pressed.get());
9385 }
9386
9387 #[test]
9393 fn a_press_that_raises_no_visual_still_defers_its_focus() {
9394 use teksilo_tokens::{PenKind, PointerKind};
9395
9396 let (mut tree, _clock) = tree_on_a_clock();
9397 let (w, pressed) = tappable(&mut tree);
9398 tree.layout(SizeProposal::exact(100.0, 50.0));
9399 let center = tree.bounds(w).center();
9400
9401 let id = contact_id();
9402 let barrel = |phase, t| PointerSample {
9403 pointer: PointerInfo {
9404 kind: PointerKind::Pen(PenKind::Pen),
9405 ..PointerInfo::touch(id, t)
9406 },
9407 phase,
9408 position: center,
9409 button: Some(PointerButton::Secondary),
9410 modifiers: Modifiers::NONE,
9411 coalesced: Vec::new(),
9412 };
9413
9414 tree.dispatch_pointer(barrel(PointerPhase::Down, EventTime::ZERO));
9415 assert!(!pressed.get(), "the barrel button activates nothing here");
9416 assert_eq!(tree.pressed_by(w), None);
9417 assert_eq!(
9418 tree.focused(),
9419 None,
9420 "a direct pointer has chosen nothing until it lifts",
9421 );
9422
9423 tree.dispatch_pointer(barrel(PointerPhase::Up, EventTime::from_millis(40)));
9424 assert_eq!(
9425 tree.focused(),
9426 Some(w),
9427 "the deferral is not button-gated: the release landed where the press did",
9428 );
9429 assert_eq!(
9430 tree.focus_origin().and_then(FocusOrigin::pointer_kind),
9431 Some(PointerKind::Pen(PenKind::Pen)),
9432 );
9433 }
9434
9435 #[test]
9442 fn the_feedback_delay_applies_only_inside_a_claimant() {
9443 let (mut tree, _clock) = tree_on_a_clock();
9445 let (w, pressed) = tappable(&mut tree);
9446 tree.layout(SizeProposal::exact(200.0, 400.0));
9447 let id = contact_id();
9448 let at = tree.bounds(w).center();
9449 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
9450 assert!(
9451 !tree.press_pending(w),
9452 "no claimant above it, so nothing to rule out",
9453 );
9454 assert!(pressed.get());
9455
9456 let (mut tree, clock) = tree_on_a_clock();
9458 let (row, pressed) = tappable(&mut tree);
9459 let list = tree.add(
9460 StackWidget::new()
9461 .child(row)
9462 .pan_claim(PanClaim::vertical())
9463 .on_scroll(|_e, _c| EventResponse::Handled),
9464 );
9465 tree.layout(SizeProposal::exact(200.0, 400.0));
9466 let _ = list;
9467 let id = contact_id();
9468 let at = Point::new(100.0, 200.0);
9469 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
9470 assert!(tree.press_pending(row), "a finger might be about to scroll");
9471 assert!(!pressed.get(), "so the row does not flash");
9472 assert!(
9473 tree.press_is_inside(row),
9474 "the press is real; only its visual is being withheld",
9475 );
9476
9477 clock.set(EventTime::from_millis(99));
9478 tree.tick_gestures(std::time::Instant::now());
9479 assert!(!pressed.get(), "99 ms is under the 100 ms delay");
9480
9481 clock.set(EventTime::from_millis(100));
9482 tree.tick_gestures(std::time::Instant::now());
9483 assert!(pressed.get(), "and 100 ms is it");
9484 assert!(!tree.press_pending(row));
9485 }
9486
9487 #[test]
9490 fn a_mouse_inside_a_claimant_never_waits() {
9491 let mut tree = WidgetTree::new();
9492 let (row, pressed) = tappable(&mut tree);
9493 let list = tree.add(
9494 StackWidget::new()
9495 .child(row)
9496 .pan_claim(PanClaim::vertical())
9497 .on_scroll(|_e, _c| EventResponse::Handled),
9498 );
9499 tree.layout(SizeProposal::exact(200.0, 400.0));
9500 let _ = list;
9501
9502 tree.dispatch_event(WidgetEvent::pointer_down(
9503 Point::new(100.0, 200.0),
9504 PointerButton::Primary,
9505 Modifiers::NONE,
9506 ));
9507 assert!(!tree.press_pending(row));
9508 assert!(pressed.get(), "Compact with a mouse is exactly as it was");
9509 }
9510
9511 #[test]
9518 fn a_handler_can_read_the_press_it_is_inside() {
9519 let (mut tree, _clock) = tree_on_a_clock();
9520 let seen: Rc<RefCell<Vec<(bool, bool, bool)>>> = Rc::new(RefCell::new(Vec::new()));
9521 let log = seen.clone();
9522 let row = tree.add(FillWidget::new().on_tap(|_e, _c| {}).on_pointer_event(
9523 move |event, ctx| {
9524 if matches!(event, WidgetEvent::PointerUp { .. }) {
9525 log.borrow_mut().push((
9526 ctx.is_pressed(),
9527 ctx.press_is_inside(),
9528 ctx.press_pending(),
9529 ));
9530 }
9531 EventResponse::Ignored
9532 },
9533 ));
9534 let list = tree.add(
9535 StackWidget::new()
9536 .child(row)
9537 .pan_claim(PanClaim::vertical())
9538 .on_scroll(|_e, _c| EventResponse::Handled),
9539 );
9540 tree.layout(SizeProposal::exact(200.0, 400.0));
9541 let _ = list;
9542
9543 let id = contact_id();
9544 let at = Point::new(100.0, 200.0);
9545 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
9546 tree.dispatch_pointer(contact(
9547 id,
9548 PointerPhase::Up,
9549 at,
9550 EventTime::from_millis(30),
9551 ));
9552
9553 assert_eq!(
9554 seen.borrow().as_slice(),
9555 &[(false, true, true)],
9556 "released inside the claimant before the delay elapsed: inside, \
9557 pending, and therefore not yet showing",
9558 );
9559 }
9560
9561 #[test]
9564 fn a_handler_outside_a_press_reads_nothing() {
9565 let mut tree = WidgetTree::new();
9566 let asked = Rc::new(Cell::new(false));
9567 let flag = asked.clone();
9568 let w = tree.add(FillWidget::new().focusable().on_key(move |_e, ctx| {
9569 flag.set(ctx.is_pressed() || ctx.press_is_inside() || ctx.press_pending());
9570 EventResponse::Ignored
9571 }));
9572 tree.layout(SizeProposal::exact(100.0, 50.0));
9573 tree.focus(w);
9574 tree.press_key(Key::ArrowDown, Modifiers::NONE);
9575 assert!(!asked.get());
9576 }
9577
9578 #[test]
9582 fn a_completed_press_leaves_the_detector_clean() {
9583 let (mut tree, _clock) = tree_on_a_clock();
9584 let (w, pressed) = tappable(&mut tree);
9585 tree.layout(SizeProposal::exact(100.0, 50.0));
9586 let at = tree.bounds(w).center();
9587
9588 let id = contact_id();
9589 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
9590 assert!(pressed.get());
9591 tree.dispatch_pointer(contact(
9592 id,
9593 PointerPhase::Up,
9594 at,
9595 EventTime::from_millis(30),
9596 ));
9597 tree.assert_no_leaked_pointer_state();
9598 }
9599
9600 #[test]
9604 fn the_detector_reports_a_press_still_held() {
9605 let (mut tree, _clock) = tree_on_a_clock();
9606 let (w, _pressed) = tappable(&mut tree);
9607 tree.layout(SizeProposal::exact(100.0, 50.0));
9608 let at = tree.bounds(w).center();
9609
9610 let id = contact_id();
9611 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
9612 let held = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
9613 tree.assert_no_leaked_pointer_state()
9614 }));
9615 let message = *held
9616 .expect_err("a live press is leaked state")
9617 .downcast::<String>()
9618 .expect("the detector panics with a message");
9619 assert!(
9620 message.contains("is still pressed by"),
9621 "the press is named in the report, got: {message}",
9622 );
9623 }
9624
9625 #[derive(Debug)]
9629 struct SideBySide {
9630 children: Vec<WidgetId>,
9631 }
9632
9633 impl crate::widget::Widget for SideBySide {
9634 fn layout_response(
9635 &self,
9636 proposal: SizeProposal,
9637 _ctx: &crate::widget::LayoutContext,
9638 ) -> crate::widget::LayoutResponse {
9639 proposal.resolve(0.0, 0.0).into()
9640 }
9641
9642 fn place_children(
9643 &self,
9644 bounds: teksilo_canvas::Rect,
9645 _proposal: SizeProposal,
9646 children: &mut [crate::widget::WidgetPlacement],
9647 _ctx: &crate::widget::LayoutContext,
9648 ) {
9649 let n = children.len().max(1) as f32;
9650 let w = bounds.width / n;
9651 for (i, child) in children.iter_mut().enumerate() {
9652 child.origin = Point::new(bounds.x + w * i as f32, bounds.y);
9653 child.size = teksilo_canvas::Size::new(w, bounds.height);
9654 }
9655 }
9656
9657 fn children(&self) -> Vec<WidgetId> {
9658 self.children.clone()
9659 }
9660 }
9661}
9662
9663#[cfg(test)]
9664mod overlay_release_dismissal_tests {
9665 use std::cell::Cell;
9675 use std::rc::Rc;
9676
9677 use teksilo_canvas::{Point, Rect, Size, SizeProposal};
9678
9679 use crate::WidgetId;
9680 use crate::event::{Modifiers, PointerButton, WidgetEvent};
9681 use crate::overlay::{DismissBehavior, OverlayPlacement, OverlayRequest};
9682 use crate::pointer::{
9683 BackendDeviceKey, EventTime, PointerId, PointerIdAllocator, PointerInfo, PointerPhase,
9684 PointerSample,
9685 };
9686 use crate::test_widgets::FillWidget;
9687 use crate::widget::{LayoutContext, LayoutResponse, Widget};
9688 use crate::widget_builder::WidgetBuilder;
9689 use crate::widget_tree::WidgetTree;
9690
9691 #[derive(Debug)]
9698 struct Panel(Size);
9699
9700 impl Widget for Panel {
9701 fn layout_response(&self, _proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
9702 self.0.into()
9703 }
9704 }
9705
9706 #[derive(Debug)]
9714 struct PageAndTrigger {
9715 children: Vec<WidgetId>,
9716 }
9717
9718 impl Widget for PageAndTrigger {
9719 fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
9720 proposal.resolve(0.0, 0.0).into()
9721 }
9722
9723 fn place_children(
9724 &self,
9725 bounds: Rect,
9726 _proposal: SizeProposal,
9727 children: &mut [crate::widget::WidgetPlacement],
9728 _ctx: &LayoutContext,
9729 ) {
9730 if let Some(page) = children.get_mut(0) {
9731 page.origin = bounds.origin();
9732 page.size = bounds.size();
9733 }
9734 if let Some(trigger) = children.get_mut(1) {
9735 trigger.origin = bounds.origin();
9736 trigger.size = Size::new(10.0, 10.0);
9737 }
9738 }
9739
9740 fn children(&self) -> Vec<WidgetId> {
9741 self.children.clone()
9742 }
9743 }
9744
9745 fn contact_id(n: u64) -> PointerId {
9746 PointerIdAllocator::global().begin(BackendDeviceKey::new(0x0FA2), n)
9747 }
9748
9749 fn touch(id: PointerId, phase: PointerPhase, at: Point) -> PointerSample {
9750 PointerSample {
9751 pointer: PointerInfo::touch(id, EventTime::ZERO),
9752 phase,
9753 position: at,
9754 button: None,
9755 modifiers: Modifiers::NONE,
9756 coalesced: Vec::new(),
9757 }
9758 }
9759
9760 fn page_with_a_menu() -> (WidgetTree, WidgetId, Rc<Cell<u32>>) {
9767 let mut tree = WidgetTree::new();
9768 let taps = Rc::new(Cell::new(0u32));
9769 let counter = taps.clone();
9770 let page = tree.add(
9771 FillWidget::new()
9772 .focusable()
9773 .on_tap(move |_e, _c| counter.set(counter.get() + 1)),
9774 );
9775 let trigger = tree.add(Panel(Size::new(10.0, 10.0)));
9778 let _root = tree.add(PageAndTrigger {
9779 children: vec![page, trigger],
9780 });
9781 let menu = tree.add(Panel(Size::new(200.0, 200.0)));
9782 tree.show_overlay(OverlayRequest::new(
9783 menu,
9784 trigger,
9785 OverlayPlacement::AtPointer(Point::new(100.0, 100.0)),
9786 DismissBehavior::ClickOutside,
9787 ));
9788 tree.layout(SizeProposal::exact(800.0, 600.0));
9789 assert_eq!(tree.active_overlays().len(), 1, "the menu is open");
9790 (tree, page, taps)
9791 }
9792
9793 fn outside() -> Point {
9794 Point::new(600.0, 500.0)
9795 }
9796
9797 fn inside_menu() -> Point {
9798 Point::new(150.0, 150.0)
9799 }
9800
9801 #[test]
9808 fn the_suppressed_down_leaves_nothing_pressed_or_captured_beneath() {
9809 let (mut tree, page, taps) = page_with_a_menu();
9810 let finger = contact_id(1);
9811 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
9812
9813 assert_eq!(
9814 tree.active_overlays().len(),
9815 1,
9816 "the press does not close the menu; the release does"
9817 );
9818 assert_eq!(tree.pressed_by(page), None, "nothing beneath is pressed");
9819 let entry = tree.pointers.get(finger).expect("the contact is live");
9820 assert_eq!(entry.captured_by, None, "nothing beneath captured it");
9821 assert!(entry.sequence.is_none(), "no arbitration was opened");
9822 assert_eq!(taps.get(), 0);
9823 }
9824
9825 #[test]
9827 fn a_touch_tap_outside_a_menu_closes_it_and_actuates_nothing() {
9828 let (mut tree, page, taps) = page_with_a_menu();
9829 let finger = contact_id(2);
9830 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
9831 tree.dispatch_pointer(touch(finger, PointerPhase::Up, outside()));
9832
9833 assert!(tree.active_overlays().is_empty(), "the menu closed");
9834 assert_eq!(taps.get(), 0, "the page beneath was never tapped");
9835 assert_eq!(tree.pressed_by(page), None);
9836 tree.assert_no_leaked_pointer_state();
9837 }
9838
9839 #[test]
9842 fn a_cancelled_press_aborts_the_arm() {
9843 let (mut tree, page, taps) = page_with_a_menu();
9844 let finger = contact_id(3);
9845 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
9846 tree.dispatch_pointer(touch(finger, PointerPhase::Cancel, outside()));
9847
9848 assert_eq!(tree.active_overlays().len(), 1, "the menu survives");
9849 assert_eq!(taps.get(), 0);
9850 assert_eq!(tree.pressed_by(page), None);
9851 tree.assert_no_leaked_pointer_state();
9852 }
9853
9854 #[test]
9858 fn a_press_slid_onto_the_menu_dismisses_nothing() {
9859 let (mut tree, _page, taps) = page_with_a_menu();
9860 let finger = contact_id(4);
9861 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
9862 tree.dispatch_pointer(touch(finger, PointerPhase::Move, inside_menu()));
9863 tree.dispatch_pointer(touch(finger, PointerPhase::Up, inside_menu()));
9864
9865 assert_eq!(tree.active_overlays().len(), 1, "the menu survives");
9866 assert_eq!(taps.get(), 0);
9867 tree.assert_no_leaked_pointer_state();
9868 }
9869
9870 #[test]
9873 fn a_second_contact_cannot_dismiss_what_the_first_is_manipulating() {
9874 let (mut tree, _page, _taps) = page_with_a_menu();
9875 let first = contact_id(5);
9876 let second = contact_id(6);
9877
9878 tree.dispatch_pointer(touch(first, PointerPhase::Down, inside_menu()));
9879 assert!(
9880 tree.pointers
9881 .get(first)
9882 .is_some_and(|e| e.sequence.is_some()),
9883 "the first contact holds a live press inside the menu"
9884 );
9885
9886 tree.dispatch_pointer(touch(second, PointerPhase::Down, outside()));
9887 tree.dispatch_pointer(touch(second, PointerPhase::Up, outside()));
9888 assert_eq!(
9889 tree.active_overlays().len(),
9890 1,
9891 "the menu the first finger is holding must not close under it"
9892 );
9893
9894 tree.dispatch_pointer(touch(first, PointerPhase::Up, inside_menu()));
9897 let third = contact_id(7);
9898 tree.dispatch_pointer(touch(third, PointerPhase::Down, outside()));
9899 tree.dispatch_pointer(touch(third, PointerPhase::Up, outside()));
9900 assert!(tree.active_overlays().is_empty());
9901 tree.assert_no_leaked_pointer_state();
9902 }
9903
9904 #[test]
9908 fn an_arm_is_not_itself_a_press_that_blocks_another_contact() {
9909 let (mut tree, _page, _taps) = page_with_a_menu();
9910 let first = contact_id(8);
9911 let second = contact_id(9);
9912
9913 tree.dispatch_pointer(touch(first, PointerPhase::Down, outside()));
9914 assert!(
9915 tree.overlay_manager().has_armed_dismiss(first),
9916 "the first contact armed"
9917 );
9918 assert!(tree.busy_press_points(second).is_empty());
9920
9921 tree.dispatch_pointer(touch(second, PointerPhase::Down, outside()));
9922 assert!(tree.overlay_manager().has_armed_dismiss(second));
9923 tree.dispatch_pointer(touch(second, PointerPhase::Up, outside()));
9924 assert!(tree.active_overlays().is_empty());
9925
9926 tree.dispatch_pointer(touch(first, PointerPhase::Up, outside()));
9929 tree.assert_no_leaked_pointer_state();
9930 }
9931
9932 #[test]
9935 fn a_touch_inside_the_menu_arms_nothing() {
9936 let (mut tree, _page, _taps) = page_with_a_menu();
9937 let finger = contact_id(10);
9938 tree.dispatch_pointer(touch(finger, PointerPhase::Down, inside_menu()));
9939 assert!(!tree.overlay_manager().has_armed_dismiss(finger));
9940 tree.dispatch_pointer(touch(finger, PointerPhase::Up, inside_menu()));
9941 assert_eq!(tree.active_overlays().len(), 1);
9942 tree.assert_no_leaked_pointer_state();
9943 }
9944
9945 #[test]
9955 fn a_contact_with_no_press_still_ends_when_the_platform_revokes_it() {
9956 let mut tree = WidgetTree::new();
9957 tree.layout(SizeProposal::exact(800.0, 600.0));
9958 let finger = contact_id(99);
9959 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
9960 tree.dispatch_pointer(touch(finger, PointerPhase::Cancel, outside()));
9961 tree.assert_no_leaked_pointer_state();
9962 }
9963
9964 #[test]
9971 fn a_mouse_click_outside_a_menu_dismisses_on_the_press_and_falls_through() {
9972 let (mut tree, _page, taps) = page_with_a_menu();
9973 tree.dispatch_event(WidgetEvent::pointer_down(
9974 outside(),
9975 PointerButton::Primary,
9976 Modifiers::NONE,
9977 ));
9978 assert!(
9979 tree.active_overlays().is_empty(),
9980 "the mouse still dismisses on the press"
9981 );
9982 assert!(
9983 tree.pointers
9984 .get(PointerId::MOUSE)
9985 .is_some_and(|e| e.sequence.is_some()),
9986 "and the press still reaches the page beneath"
9987 );
9988 tree.dispatch_event(WidgetEvent::pointer_up(
9989 outside(),
9990 PointerButton::Primary,
9991 Modifiers::NONE,
9992 ));
9993 assert_eq!(taps.get(), 1, "the control beneath activated");
9994 assert!(
9995 !tree.overlay_manager().has_armed_dismiss(PointerId::MOUSE),
9996 "a mouse never arms"
9997 );
9998 tree.assert_no_leaked_pointer_state();
9999 }
10000
10001 #[test]
10008 fn a_context_menu_avoids_the_contact_that_opened_it() {
10009 fn menu_bounds(pointer: PointerInfo, at: Point) -> Rect {
10010 let mut tree = WidgetTree::new();
10011 let page = tree.add(
10012 FillWidget::new()
10013 .focusable()
10014 .context_menu(|_p, _ctx| Some(Box::new(Panel(Size::new(200.0, 160.0))))),
10015 );
10016 let _ = page;
10017 tree.layout(SizeProposal::exact(800.0, 600.0));
10018 tree.dispatch_pointer(PointerSample {
10019 pointer,
10020 phase: PointerPhase::Down,
10021 position: at,
10022 button: Some(PointerButton::Secondary),
10023 modifiers: Modifiers::NONE,
10024 coalesced: Vec::new(),
10025 });
10026 tree.layout(SizeProposal::exact(800.0, 600.0));
10027 let id = *tree
10028 .active_overlays()
10029 .first()
10030 .expect("the context menu opened");
10031 tree.overlay_manager().bounds_for(id).expect("bounds")
10032 }
10033
10034 let at = Point::new(400.0, 300.0);
10035 let finger = menu_bounds(PointerInfo::touch(contact_id(11), EventTime::ZERO), at);
10036 let contact = crate::overlay::rect_centred_on(at, crate::overlay::ASSUMED_CONTACT_PATCH);
10037 assert!(
10038 finger.x < contact.x && finger.right() <= contact.x,
10039 "a touch menu clears the contact patch: {finger:?} vs {contact:?}"
10040 );
10041
10042 let mouse = menu_bounds(PointerInfo::mouse(EventTime::ZERO), at);
10043 assert_eq!(
10044 (mouse.x, mouse.y),
10045 (at.x, at.y),
10046 "a mouse menu still opens with its corner on the pointer"
10047 );
10048 }
10049}
10050
10051#[cfg(test)]
10067mod pass_through_overlay_tests {
10068 use teksilo_canvas::{Point, Rect, SizeProposal};
10069
10070 use crate::WidgetId;
10071 use crate::overlay::{DismissBehavior, OverlayPlacement, OverlayRequest};
10072 use crate::test_widgets::FillWidget;
10073 use crate::widget::{LayoutContext, LayoutResponse, Widget};
10074 use crate::widget_builder::WidgetBuilder;
10075 use crate::widget_tree::WidgetTree;
10076
10077 #[derive(Debug)]
10080 struct HandleLayer {
10081 handle: WidgetId,
10082 at: Rect,
10083 }
10084
10085 impl Widget for HandleLayer {
10086 fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
10087 proposal.resolve(0.0, 0.0).into()
10088 }
10089
10090 fn place_children(
10091 &self,
10092 _bounds: Rect,
10093 _proposal: SizeProposal,
10094 children: &mut [crate::widget::WidgetPlacement],
10095 _ctx: &LayoutContext,
10096 ) {
10097 if let Some(child) = children.get_mut(0) {
10098 child.origin = self.at.origin();
10099 child.size = self.at.size();
10100 }
10101 }
10102
10103 fn children(&self) -> Vec<WidgetId> {
10104 vec![self.handle]
10105 }
10106 }
10107
10108 const HANDLE: Rect = Rect {
10109 x: 10.0,
10110 y: 10.0,
10111 width: 20.0,
10112 height: 20.0,
10113 };
10114
10115 #[derive(Clone, Copy, PartialEq)]
10117 enum RootFlag {
10118 PassThrough,
10120 HitTransparent,
10123 }
10124
10125 fn field_under_a_layer(flag: RootFlag) -> (WidgetTree, WidgetId, WidgetId) {
10131 let mut tree = WidgetTree::new();
10132 let field = tree.add(FillWidget::new());
10133 let handle = tree.add(FillWidget::new());
10134 let layer = HandleLayer { handle, at: HANDLE };
10135 let layer = match flag {
10136 RootFlag::PassThrough => tree.add(layer.event_pass_through(true)),
10137 RootFlag::HitTransparent => tree.add(layer.hit_transparent(true)),
10138 };
10139 tree.show_overlay(OverlayRequest::new(
10140 layer,
10141 field,
10142 OverlayPlacement::FullViewport,
10143 DismissBehavior::Manual,
10144 ));
10145 tree.layout(SizeProposal::exact(400.0, 200.0));
10146 assert_eq!(tree.active_overlays().len(), 1, "the layer is up");
10147 (tree, field, handle)
10148 }
10149
10150 #[test]
10152 fn a_pass_through_layer_hands_back_what_it_did_not_claim() {
10153 let (tree, field, handle) = field_under_a_layer(RootFlag::PassThrough);
10154
10155 assert_eq!(
10156 tree.hit_test(Point::new(15.0, 15.0)),
10157 Some(handle),
10158 "the layer must still win the point its own child covers"
10159 );
10160 assert_eq!(
10161 tree.hit_test(Point::new(200.0, 100.0)),
10162 Some(field),
10163 "a press the layer did not claim must reach the surface beneath it"
10164 );
10165 }
10166
10167 #[test]
10178 fn a_pass_through_root_still_hears_a_press_that_landed_on_its_child() {
10179 use std::cell::Cell;
10180 use std::rc::Rc;
10181
10182 let seen = Rc::new(Cell::new(0u32));
10183 let counter = seen.clone();
10184 let mut tree = WidgetTree::new();
10185 let field = tree.add(FillWidget::new());
10186 let handle = tree.add(FillWidget::new().on_tap(|_e, _c| {}));
10187 let layer = tree.add(
10188 HandleLayer { handle, at: HANDLE }
10189 .event_pass_through(true)
10190 .on_pointer_event(move |_event, _ctx| {
10191 counter.set(counter.get() + 1);
10192 crate::event::EventResponse::Ignored
10193 }),
10194 );
10195 tree.show_overlay(OverlayRequest::new(
10196 layer,
10197 field,
10198 OverlayPlacement::FullViewport,
10199 DismissBehavior::Manual,
10200 ));
10201 tree.layout(SizeProposal::exact(400.0, 200.0));
10202
10203 tree.pointer_down_button(Point::new(15.0, 15.0), crate::event::PointerButton::Primary);
10204 assert!(
10205 seen.get() > 0,
10206 "a pass-through root heard nothing about a press on its own child"
10207 );
10208
10209 let after_child = seen.get();
10210 tree.pointer_up_button(Point::new(15.0, 15.0), crate::event::PointerButton::Primary);
10211 tree.pointer_down_button(
10214 Point::new(200.0, 100.0),
10215 crate::event::PointerButton::Primary,
10216 );
10217 assert_eq!(
10218 seen.get(),
10219 after_child + 1,
10220 "the press-and-lift on the child accounts for the count, and the \
10221 press on the surface beneath added nothing"
10222 );
10223 }
10224
10225 #[test]
10239 fn a_hit_transparent_root_does_not_get_the_fall_through() {
10240 let (tree, field, _handle) = field_under_a_layer(RootFlag::HitTransparent);
10241
10242 assert_eq!(
10244 tree.hit_test(Point::new(15.0, 15.0)),
10245 None,
10246 "hit_transparent must exclude the subtree, or this fixture is not \
10247 testing the gate"
10248 );
10249 assert_eq!(
10250 tree.hit_test(Point::new(200.0, 100.0)),
10251 None,
10252 "the fall-through reached past an overlay that did not ask for it"
10253 );
10254 let _ = field;
10255 }
10256}
10257
10258fn note_refused_section(owner: WidgetId) {
10262 #[cfg(debug_assertions)]
10263 {
10264 thread_local! {
10265 static NOTED: std::cell::RefCell<std::collections::HashSet<WidgetId>> =
10266 std::cell::RefCell::new(std::collections::HashSet::new());
10267 }
10268 if NOTED.with(|noted| noted.borrow_mut().insert(owner)) {
10269 eprintln!(
10270 "teksilo: the context menu of {owner:?} takes no section \
10271 (Widget::append_context_menu_section), so a contribution was left out"
10272 );
10273 }
10274 }
10275 #[cfg(not(debug_assertions))]
10276 let _ = owner;
10277}
10278
10279fn logical_key_identity(key: Key) -> u64 {
10284 use std::hash::{Hash, Hasher};
10285 let key = match key {
10286 Key::Character(c) => Key::Character(c.to_ascii_lowercase()),
10287 other => other,
10288 };
10289 let mut hasher = std::collections::hash_map::DefaultHasher::new();
10290 key.hash(&mut hasher);
10291 hasher.finish()
10292}