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 fn escape_taken_inside_top_overlay(
589 &mut self,
590 event: &WidgetEvent,
591 ops: &mut dyn crate::window::WindowOps,
592 ) -> bool {
593 let Some((_, content)) = self.overlay_manager.top_escape_target() else {
594 return false;
595 };
596 let Some(focus) = self.focused else {
597 return false;
598 };
599 let mut path = Vec::new();
600 let mut current = self.arena.parent(focus);
601 let mut inside = false;
602 while let Some(id) = current {
603 path.push(id);
604 if id == content {
605 inside = true;
606 break;
607 }
608 current = self.arena.parent(id);
609 }
610 if !inside || !self.arena.is_enabled(focus) {
611 return false;
612 }
613 for &id in path.iter().rev() {
614 let frame = self.arena.local_frame(id);
615 let mut ctx = self
616 .make_event_context(&mut *ops)
617 .with_dispatch_node(id)
618 .with_dispatch_target(focus)
619 .with_local_frame(frame);
620 let response = if let Some(node) = self.arena.get_mut(id) {
621 Self::try_handler_preview(node, event, &mut ctx).unwrap_or(EventResponse::Ignored)
622 } else {
623 EventResponse::Ignored
624 };
625 self.collect_from_ctx(ctx, id);
626 if response == EventResponse::Handled {
627 self.arena.mark_needs_paint(id);
628 self.note_key_press(focus);
631 return true;
632 }
633 }
634 false
635 }
636
637 #[doc(hidden)]
651 pub fn answer_close_request_with_modal(
652 &mut self,
653 ops: &mut dyn crate::window::WindowOps,
654 ) -> bool {
655 if !self.overlay_manager.has_open_modal() {
656 return false;
657 }
658 self.tooltip_escape_pressed();
661 self.inert_overlays_escape_pressed(ops);
662 self.dismiss_top_on_escape(ops);
663 true
664 }
665
666 fn note_key_press(&mut self, target: WidgetId) {
670 let Some((identity, repeat)) = self.current_key else {
671 return;
672 };
673 match self.key_presses.iter_mut().find(|(id, _)| *id == identity) {
674 Some(_) if repeat => {}
675 Some(record) => record.1 = target,
676 None => self.key_presses.push((identity, target)),
677 }
678 }
679
680 fn receives_every_key_release(&self, id: WidgetId) -> bool {
684 self.arena
685 .get(id)
686 .is_some_and(|node| node.receives_every_key_release)
687 }
688
689 fn dispatch_key_release(
700 &mut self,
701 key: Key,
702 event: &WidgetEvent,
703 ops: &mut dyn crate::window::WindowOps,
704 ) {
705 let identity = self.current_key.map(|(id, _)| id);
706 let pressed = identity.and_then(|identity| {
707 let at = self
708 .key_presses
709 .iter()
710 .position(|(id, _)| *id == identity)?;
711 Some(self.key_presses.remove(at).1)
712 });
713 match pressed {
714 Some(target) if Some(target) == self.focused => {
715 self.dispatch_to_widget(target, event, &mut *ops);
716 }
717 Some(target) => {
718 if self.arena.is_active(target)
719 && (!self.is_behind_modal(target)
720 || self.is_keyboard_capture(target)
721 || self.receives_every_key_release(target))
722 {
723 let behind_modal = self.is_behind_modal(target);
729 let mut current = Some(target);
730 while let Some(id) = current {
731 if self.dispatch_to_widget_direct_returning_handled(id, event, &mut *ops)
732 || behind_modal
733 {
734 break;
735 }
736 current = self.arena.parent(id);
737 }
738 }
739 }
740 None if matches!(key, Key::Enter | Key::Space) => {}
741 None => {
742 if let Some(focused) = self.focused {
743 self.dispatch_to_widget(focused, event, &mut *ops);
744 }
745 }
746 }
747 }
748
749 fn dispatch_event_impl(&mut self, event: WidgetEvent, ops: &mut dyn crate::window::WindowOps) {
750 if let Some(position) = pointer_event_position(&event) {
756 let is_down = matches!(event, WidgetEvent::PointerDown { .. });
757 let is_move = matches!(event, WidgetEvent::PointerMove { .. });
758 if !self.admit_current_pointer(position, is_down, is_move) {
759 return;
760 }
761 if self.current_input.pointer.kind.hovers() {
765 self.claim_hover_owner_for_current(&mut *ops);
766 }
767 }
768
769 if let WidgetEvent::KeyDown { key, .. } | WidgetEvent::KeyUp { key, .. } = &event {
772 let noted = self
773 .window_state
774 .as_ref()
775 .and_then(|w| w.take_key_identity());
776 self.current_key = Some(noted.unwrap_or_else(|| (logical_key_identity(*key), false)));
777 } else if self.dispatch_depth <= 1 {
778 self.current_key = None;
780 }
781
782 match &event {
786 WidgetEvent::KeyDown { .. } if !self.focus_visible.get() => {
787 self.focus_visible.set(true);
788 }
789 WidgetEvent::PointerDown { .. } if self.focus_visible.get() => {
790 self.focus_visible.set(false);
791 }
792 _ => {}
793 }
794
795 let overlay_back_key = match self.layout_direction {
801 crate::environment::LayoutDirection::RightToLeft => Key::ArrowRight,
802 crate::environment::LayoutDirection::LeftToRight => Key::ArrowLeft,
803 };
804 if let WidgetEvent::KeyDown { key, .. } = &event
805 && *key == overlay_back_key
806 {
807 let nested_menu_overlays = {
837 let ids: Vec<_> = self
838 .overlay_manager
839 .stack
840 .iter()
841 .filter(|o| o.band == crate::overlay::OverlayBand::Standard && !o.inert)
842 .map(|o| o.id)
843 .collect();
844 ids.into_iter()
845 .filter(|&id| !self.overlay_is_host_surface(id))
846 .count()
847 };
848 let top_id = self
855 .overlay_manager
856 .stack
857 .iter()
858 .rev()
859 .find(|o| !o.inert)
860 .map(|o| o.id);
861 let top_is_back_navigable = top_id.is_some_and(|id| !self.overlay_is_host_surface(id));
862 if nested_menu_overlays > 1
863 && top_is_back_navigable
864 && let Some(top_id) = top_id
865 {
866 let mark = self.dismissal_mark();
867 let (content_ids, focus_restore) =
868 self.overlay_manager.dismiss_with_focus_restore_because(
869 top_id,
870 crate::overlay::DismissReason::Escape,
871 );
872 self.dormant_dismissed_content(&content_ids, &mut *ops);
873 self.return_focus_after_dismissal(focus_restore, mark, &mut *ops);
874 return;
875 }
876 }
877
878 if let WidgetEvent::KeyDown {
884 key: Key::Escape, ..
885 } = &event
886 {
887 self.tooltip_escape_pressed();
888 self.inert_overlays_escape_pressed(&mut *ops);
891 }
892
893 if let WidgetEvent::KeyDown {
894 key: Key::Escape, ..
895 } = &event
896 && !self.overlay_manager.is_empty()
897 && (self.escape_taken_inside_top_overlay(&event, &mut *ops)
898 || self.dismiss_top_on_escape(&mut *ops))
899 {
900 return;
901 }
902
903 match &event {
908 WidgetEvent::PointerDown {
909 position, button, ..
910 } => {
911 if self.arm_outside_press_dismissal(*position, *button, &mut *ops) {
912 return;
913 }
914 }
915 WidgetEvent::PointerUp { position, .. } => {
916 if self.commit_outside_press_dismissal(*position, &mut *ops) {
917 return;
918 }
919 }
920 WidgetEvent::PointerCancel { pointer, .. } => {
921 self.overlay_manager.abort_dismiss(pointer.id);
925 }
926 _ => {}
927 }
928
929 if let WidgetEvent::KeyDown { key, modifiers, .. } = &event
940 && let Some(callback) = self.take_key_capture()
941 {
942 let keystroke = crate::shortcut::KeyStroke::new(*key, *modifiers);
943 let mut cap_ctx = self.make_event_context(&mut *ops);
944 callback(keystroke, self.shortcut_registry_mut(), &mut cap_ctx);
945 let anchor = self.focused.or_else(|| self.intent_anchor());
948 if let Some(anchor_id) = anchor {
949 self.collect_from_ctx(cap_ctx, anchor_id);
950 self.drain_pending_intents(&mut *ops);
951 }
952 return;
953 }
954
955 let focus_captures_keys = matches!(&event, WidgetEvent::KeyDown { .. })
963 && self.focused.is_some_and(|f| self.is_keyboard_capture(f));
964
965 if !focus_captures_keys && let WidgetEvent::KeyDown { key, modifiers, .. } = &event {
981 let keystroke = crate::shortcut::KeyStroke::new(*key, *modifiers);
982 let candidates: Vec<(&'static str, crate::shortcut::ShortcutScope, bool)> = self
991 .shortcut_registry
992 .matches_by_keystroke(keystroke)
993 .map(|eff| {
994 (
995 eff.shortcut.id,
996 eff.shortcut.scope,
997 eff.shortcut.propagate_when_disabled,
998 )
999 })
1000 .collect();
1001 let (region_commands, others): (Vec<_>, Vec<_>) = candidates
1007 .into_iter()
1008 .partition(|(id, _, _)| crate::shortcut::region_direction(id).is_some());
1009 let lookup = self.select_shortcut_for_focus(&others).or_else(|| {
1010 let source = self.focus_or_modal().or_else(|| self.intent_anchor());
1011 let live: Vec<_> = region_commands
1012 .into_iter()
1013 .filter(|(id, _, _)| {
1014 crate::shortcut::region_direction(id).is_some_and(|forward| {
1015 self.region_cycle_target_from(source, forward).is_some()
1016 || source.is_some_and(|source| self.can_handle_from(source, id))
1017 })
1018 })
1019 .collect();
1020 self.select_shortcut_for_focus(&live)
1021 });
1022 if let Some((id, scope, propagate_when_disabled)) = lookup {
1023 let anchor = match scope {
1024 crate::shortcut::ShortcutScope::Global => {
1030 self.focus_or_modal().or_else(|| self.intent_anchor())
1031 }
1032 crate::shortcut::ShortcutScope::Scoped(scope_id) => self
1033 .focus_or_modal()
1034 .filter(|f| self.scope_distance_from_focus(*f, scope_id).is_some()),
1035 };
1036 if let Some(anchor_id) = anchor {
1037 let mut act_ctx = self.make_event_context(&mut *ops);
1038 if let Some(intent) =
1039 self.shortcut_registry
1040 .invoke_on_activate(id, keystroke, &mut act_ctx)
1041 {
1042 self.collect_from_ctx(act_ctx, anchor_id);
1043 self.drain_pending_intents(&mut *ops);
1047 let intent = self.sent_here(
1051 intent.with_source(crate::telemetry::IntentSource::Shortcut),
1052 );
1053 let keep = self.keeps_unclaimed(&intent).then(|| intent.clone());
1054 let consumed = self.dispatch_intent(
1055 anchor_id,
1056 intent,
1057 propagate_when_disabled,
1058 &mut *ops,
1059 );
1060 self.drain_pending_intents(&mut *ops);
1061 if consumed {
1062 return;
1063 }
1064 if let Some(intent) = keep {
1067 self.keep_unclaimed(intent, &mut *ops);
1068 return;
1069 }
1070 }
1077 }
1078 }
1083 }
1086
1087 if self.outbound_drag_source.is_some()
1093 && let WidgetEvent::KeyDown {
1094 key: Key::Escape, ..
1095 } = &event
1096 {
1097 ops.cancel_os_drag();
1098 }
1103
1104 if self.os_drag_reentered
1118 && matches!(
1119 event,
1120 WidgetEvent::PointerMove { .. } | WidgetEvent::PointerUp { .. }
1121 )
1122 {
1123 let dragging = self.current_pointer_id();
1124 let own = self
1125 .active_drag
1126 .as_ref()
1127 .is_some_and(|d| d.pointer.id == dragging);
1128 if own && matches!(event, WidgetEvent::PointerMove { .. }) {
1129 return;
1130 }
1131 } else if self.active_drag.is_some() {
1132 match &event {
1133 WidgetEvent::PointerMove { position, .. } => {
1134 self.handle_drag_sample(*position, &mut *ops);
1135 return;
1136 }
1137 WidgetEvent::PointerUp { position, .. } => {
1138 self.handle_drag_drop(*position, &mut *ops);
1139 return;
1140 }
1141 WidgetEvent::KeyDown {
1142 key: Key::Escape, ..
1143 } => {
1144 self.cancel_active_drag(&mut *ops);
1145 return;
1146 }
1147 WidgetEvent::Scroll { .. } => {
1148 let target_and_pos = self
1154 .active_drag
1155 .as_ref()
1156 .and_then(|d| d.current_target.map(|t| (t, d.current_position)));
1157 if let Some((target, _pos)) = target_and_pos {
1158 self.dispatch_to_widget(target, &event, &mut *ops);
1159 }
1160 if let Some((_, pos)) = target_and_pos
1161 && self.active_drag.is_some()
1162 {
1163 self.handle_drag_move(pos, &mut *ops);
1164 }
1165 return;
1166 }
1167 _ => {}
1168 }
1169 }
1170
1171 if let WidgetEvent::KeyDown { key, modifiers, .. } = &event
1180 && is_context_menu_chord(*key, *modifiers)
1181 && self.open_context_menu_from_keyboard(&mut *ops)
1182 {
1183 return;
1184 }
1185
1186 match &event {
1187 WidgetEvent::PointerMove { position, .. } => {
1188 self.note_sequence_position(*position);
1192 self.revalidate_sequence(&mut *ops);
1193 self.tick_sequence_timers();
1196 self.note_palm_sample(*position);
1201 self.feed_pinch(super::pan_arbiter::PinchFeed::Move, *position, &mut *ops);
1202 if let Some(captured) = self.current_pointer_capture() {
1203 self.dispatch_to_widget(captured, &event, &mut *ops);
1204 if self.active_drag.is_none() {
1209 self.advance_sequence(&event, &mut *ops);
1210 }
1211 } else {
1212 self.handle_pointer_move(&event, *position, &mut *ops);
1213 if self.active_drag.is_none() {
1218 self.advance_sequence(&event, &mut *ops);
1219 }
1220 }
1221 self.advance_pan(*position, &mut *ops);
1224 self.update_press(*position);
1227 self.touch_route_moved(self.current_pointer_id());
1230 if self.pointers.hover_owner_id() == Some(self.current_pointer_id()) {
1240 self.update_pointer_leave_overlays(*position, &mut *ops);
1241 }
1242 }
1243 WidgetEvent::PointerDown {
1244 position, button, ..
1245 } => {
1246 let pressed = self.current_pointer_id();
1249 self.cancelled_pointers.retain(|p| *p != pressed);
1250 self.tooltip_pointer_press(Some(*position));
1257 let pressing = self.current_input.pointer;
1261 if let Some(target) = self.hit_test_for(*position, &pressing) {
1262 if *button == PointerButton::Secondary
1263 && self.show_context_menu_for(
1264 target,
1265 *position,
1266 crate::widget_builder::ContextMenuTrigger::Pointer,
1267 &mut *ops,
1268 )
1269 {
1270 return;
1271 }
1272 self.begin_sequence(target, *position);
1278 let modifiers = match &event {
1283 WidgetEvent::PointerDown { modifiers, .. } => *modifiers,
1284 _ => crate::event::Modifiers::NONE,
1285 };
1286 self.begin_pan(target, *position, modifiers);
1287 self.begin_palm_watch(*position);
1288 self.feed_pinch(super::pan_arbiter::PinchFeed::Down, *position, &mut *ops);
1289 let focusable = self.find_focusable_at_or_above(target);
1293 self.begin_press(focusable);
1294 if !pressing.kind.is_direct()
1304 && let Some(focusable) = focusable
1305 && self.click_may_focus(focusable)
1306 {
1307 self.focus_with_origin_ops(
1308 focusable,
1309 crate::focus::FocusOrigin::Pointer(pressing.kind),
1310 &mut *ops,
1311 );
1312 }
1313 self.dispatch_to_widget(target, &event, &mut *ops);
1314 if self.active_drag.is_none() {
1319 self.enrol_sequence_members(&event, &mut *ops);
1320 }
1321 self.adopt_press_owner(*button);
1325 self.arm_touch_route(target, *position);
1330 }
1331 }
1332 WidgetEvent::PointerUp {
1333 position, button, ..
1334 } => {
1335 let released = self.current_pointer_id();
1341 self.cancel_touch_route(released);
1345 if let Some(index) = self.cancelled_pointers.iter().position(|p| *p == released) {
1346 self.cancelled_pointers.swap_remove(index);
1347 crate::trace_input!(
1348 Samples,
1349 "swallowing the Up for {released:?}: its press was cancelled"
1350 );
1351 return;
1352 }
1353 self.feed_pinch(super::pan_arbiter::PinchFeed::Up, *position, &mut *ops);
1360 if self.take_palm_verdict(released) {
1361 crate::trace_input!(
1362 Samples,
1363 "{released:?} released as a palm: large, and it never moved"
1364 );
1365 self.cancel_pointer(
1366 released,
1367 crate::pointer::CancelReason::PalmRejected,
1368 &mut *ops,
1369 );
1370 return;
1371 }
1372 self.end_pan(*position, &mut *ops);
1375 self.note_sequence_position(*position);
1382 let taps_unfocusable = self.release_taps_unfocusable(*position, *button);
1385 self.end_sequence(&event, &mut *ops);
1386 self.focus_on_release(*position, taps_unfocusable, &mut *ops);
1391 if let Some(captured) = self.current_pointer_capture() {
1392 self.dispatch_to_widget(captured, &event, &mut *ops);
1393 self.set_current_pointer_capture(None);
1396 } else {
1397 let releasing = self.current_input.pointer;
1398 if let Some(target) = self.hit_test_for(*position, &releasing) {
1399 self.dispatch_to_widget(target, &event, &mut *ops);
1400 }
1401 }
1402 let released = self.current_pointer_id();
1405 self.release_arenas_following(released);
1406 self.end_press(released);
1410 }
1411 WidgetEvent::PointerCancel {
1412 reason, pointer, ..
1413 } => {
1414 let (reason, pointer) = (*reason, pointer.id);
1419 self.cancel_pointer(pointer, reason, &mut *ops);
1420 }
1421 WidgetEvent::Scroll {
1422 window_position: position,
1423 ..
1424 } => {
1425 use super::pan_arbiter::ScrollDelivery;
1426 match ScrollDelivery::for_source(self.current_input.scroll_source) {
1427 ScrollDelivery::ClaimantChain => {
1432 let position = *position;
1433 self.route_scroll_along_chain(&event, position, &mut *ops);
1434 }
1435 ScrollDelivery::Bubble => {
1441 let scrolling = self.current_input.pointer;
1442 let target = match position {
1443 Some(p) => self.hit_test_for(*p, &scrolling),
1444 None => self.hovered_id().or(self.focused),
1445 };
1446 if let Some(target) = target {
1447 self.dispatch_to_widget(target, &event, &mut *ops);
1448 }
1449 }
1450 }
1451 }
1452 WidgetEvent::KeyDown { key, modifiers, .. } => {
1453 if *key == Key::Tab {
1454 let captured_focus = self
1469 .focused
1470 .is_some_and(|focused| self.is_keyboard_capture(focused));
1471 if captured_focus && modifiers.ctrl() {
1472 self.cycle_focus(modifiers.shift(), &mut *ops);
1473 return;
1474 }
1475 if let Some(focused) = self.focused {
1482 self.note_key_press(focused);
1483 }
1484 let handled = self
1485 .focused
1486 .map(|focused| {
1487 self.dispatch_to_widget_returning_handled(focused, &event, &mut *ops)
1488 })
1489 .unwrap_or(false);
1490 if !handled {
1491 self.cycle_focus(modifiers.shift(), &mut *ops);
1492 }
1493 } else if let Some(focused) = self.focused {
1494 self.note_key_press(focused);
1495 self.dispatch_to_widget(focused, &event, &mut *ops);
1496 }
1497 }
1498 WidgetEvent::KeyUp { key, .. } => {
1499 self.dispatch_key_release(*key, &event, &mut *ops);
1500 }
1501 WidgetEvent::ImeComposition { .. } | WidgetEvent::ImeCommit { .. } => {
1502 if let Some(focused) = self.focused {
1503 self.dispatch_to_widget(focused, &event, &mut *ops);
1504 }
1505 }
1506 WidgetEvent::AccessAction {
1507 target,
1508 action,
1509 target_node,
1510 ..
1511 } => {
1512 if let Some(id) =
1530 target.filter(|id| self.arena.is_active(*id) && !self.is_behind_modal(*id))
1531 {
1532 if *action == accesskit::Action::Focus {
1533 let synthetic =
1572 *target_node != crate::accessibility::widget_id_to_node_id(id);
1573 let takes_focus_itself = !synthetic
1574 && (self
1575 .arena
1576 .get(id)
1577 .is_some_and(|node| self.is_node_focusable(node))
1578 || self.focusable_composite_behind(id).is_some());
1579 let offered = !takes_focus_itself
1585 && if synthetic {
1586 self.published_node_advertises_focus(*target_node)
1587 } else {
1588 self.advertises_focus_action(id)
1589 && self.first_focusable_descendant(id).is_none()
1590 };
1591 let focused_before = self.focused;
1592 let answered = if offered {
1593 self.offer_item_focus(id, synthetic, &event, &mut *ops)
1594 } else {
1595 None
1596 };
1597 if let Some(answerer) = answered {
1598 let landed = self.focused.is_some_and(|focused| {
1607 Some(focused) != focused_before
1608 || focused == answerer
1609 || self.is_descendant_of(focused, answerer)
1610 || answerer == id
1611 });
1612 if landed {
1613 self.focus_origin = Some(crate::focus::FocusOrigin::Accessibility);
1617 if let Some(visible) =
1618 crate::focus::FocusOrigin::Accessibility.focus_visible()
1619 && self.focus_visible.get() != visible
1620 {
1621 self.focus_visible.set(visible);
1622 }
1623 }
1624 self.access_action_handled = landed;
1625 } else if self.advertises_focus_action(id) {
1626 let target = self
1627 .focusable_composite_behind(id)
1628 .or_else(|| self.first_focusable_descendant(id))
1629 .unwrap_or(id);
1630 if self.arena.is_enabled(target)
1631 && self
1632 .arena
1633 .get(target)
1634 .is_some_and(|node| self.is_node_focusable(node))
1635 {
1636 self.focus_with_origin_ops(
1643 target,
1644 crate::focus::FocusOrigin::Accessibility,
1645 &mut *ops,
1646 );
1647 self.access_action_handled = self.focused == Some(target);
1651 } else {
1652 self.access_action_handled = false;
1657 }
1658 } else {
1659 self.access_action_handled = false;
1660 }
1661 } else if *action == accesskit::Action::ShowContextMenu {
1662 self.access_action_handled =
1678 if self.dispatch_to_widget_returning_handled(id, &event, &mut *ops) {
1679 true
1680 } else {
1681 let target = self.resolve_context_menu_target(id);
1685 let position = self.arena.bounds(target).center();
1686 self.show_context_menu_for(
1687 target,
1688 position,
1689 crate::widget_builder::ContextMenuTrigger::Accessibility,
1690 &mut *ops,
1691 )
1692 };
1693 } else {
1694 if *action == accesskit::Action::Click
1701 && !self.in_scroll_chrome(id)
1702 && match self.find_focusable_at_or_above(id) {
1703 Some(focusable) => !self.click_may_focus(focusable),
1704 None => !self.belongs_to_focused(id),
1705 }
1706 {
1707 self.commit_pending_edit(&mut *ops);
1708 }
1709 self.access_action_handled =
1710 self.dispatch_to_widget_returning_handled(id, &event, &mut *ops);
1711 }
1712 }
1713 }
1714 WidgetEvent::Gesture { .. } => {
1715 if let Some(target) = self.hovered_id().or(self.focused) {
1716 self.dispatch_to_widget(target, &event, &mut *ops);
1717 }
1718 }
1719 WidgetEvent::ScrollIntoView { .. }
1720 | WidgetEvent::PointerEnter { .. }
1721 | WidgetEvent::PointerLeave { .. }
1722 | WidgetEvent::FocusGained { .. }
1723 | WidgetEvent::FocusLost => {}
1724 }
1725 self.drain_pending_intents(&mut *ops);
1730 }
1731
1732 fn offer_item_focus(
1752 &mut self,
1753 id: WidgetId,
1754 synthetic: bool,
1755 event: &WidgetEvent,
1756 ops: &mut dyn crate::window::WindowOps,
1757 ) -> Option<WidgetId> {
1758 let start = if self.arena.is_enabled(id) {
1759 Some(id)
1760 } else {
1761 let greyed_item = !synthetic
1762 && self
1763 .arena
1764 .get(id)
1765 .and_then(|node| node.enabled_state.as_ref())
1766 .is_some_and(|state| !state.get())
1767 && !self.has_focusable_descendant(id);
1768 self.arena
1769 .parent(id)
1770 .filter(|&parent| greyed_item && self.arena.is_enabled(parent))
1771 };
1772 let mut at = start;
1773 while let Some(node) = at {
1774 if self.dispatch_to_widget_direct_returning_handled(node, event, &mut *ops) {
1775 return Some(node);
1776 }
1777 if self
1778 .arena
1779 .get(node)
1780 .is_some_and(|n| self.is_node_focusable(n))
1781 {
1782 return None;
1783 }
1784 at = self.arena.parent(node);
1785 }
1786 None
1787 }
1788
1789 fn has_focusable_descendant(&self, id: WidgetId) -> bool {
1792 let mut stack: Vec<WidgetId> = self.arena.children(id).to_vec();
1793 while let Some(node) = stack.pop() {
1794 if self
1795 .arena
1796 .get(node)
1797 .is_some_and(|n| self.is_node_focusable(n))
1798 {
1799 return true;
1800 }
1801 stack.extend_from_slice(self.arena.children(node));
1802 }
1803 false
1804 }
1805
1806 fn published_node_advertises_focus(&self, node_id: accesskit::NodeId) -> bool {
1810 self.cached_a11y.as_ref().is_some_and(|update| {
1811 update
1812 .nodes
1813 .iter()
1814 .any(|(id, node)| *id == node_id && node.supports_action(accesskit::Action::Focus))
1815 })
1816 }
1817
1818 fn open_context_menu_from_keyboard(&mut self, ops: &mut dyn crate::window::WindowOps) -> bool {
1831 let Some(focused) = self.focused else {
1832 return false;
1833 };
1834 let target = self.resolve_context_menu_target(focused);
1835
1836 let bounds = self.bounds(target);
1840 let anchor = Point {
1841 x: bounds.x + bounds.width / 2.0,
1842 y: bounds.y + bounds.height / 2.0,
1843 };
1844 self.show_context_menu_for(
1845 target,
1846 anchor,
1847 crate::widget_builder::ContextMenuTrigger::Keyboard,
1848 ops,
1849 )
1850 }
1851
1852 pub(super) fn resolve_context_menu_target(&self, start: WidgetId) -> WidgetId {
1862 const MAX_HOPS: usize = 8;
1863 let next_of = |id: WidgetId| {
1864 self.arena
1865 .get(id)
1866 .and_then(|node| node.widget.context_menu_key_target())
1867 .filter(|next| self.arena.is_active(*next))
1868 };
1869 let mut passed = [start; MAX_HOPS + 1];
1870 let mut target = start;
1871 for hop in 0..MAX_HOPS {
1872 let Some(next) = next_of(target) else {
1873 break;
1874 };
1875 if passed[..=hop].contains(&next) {
1876 debug_assert!(
1877 false,
1878 "context_menu_key_target cycle through {next:?}: a widget names an ancestor's target"
1879 );
1880 break;
1881 }
1882 passed[hop + 1] = next;
1883 target = next;
1884 }
1885 target
1886 }
1887
1888 pub(super) fn show_context_menu_for(
1906 &mut self,
1907 target: WidgetId,
1908 position: Point,
1909 trigger: crate::widget_builder::ContextMenuTrigger,
1910 ops: &mut dyn crate::window::WindowOps,
1911 ) -> bool {
1912 let mut ctx = self.make_event_context(&mut *ops);
1918 ctx.context_menu_trigger = Some(trigger);
1919 let mut walker = Some(target);
1920 let mut menu_decision: Option<(WidgetId, Box<dyn Widget>, bool)> = None;
1923 while let Some(id) = walker {
1924 let Some(node) = self.arena.get(id) else {
1925 break;
1926 };
1927 if let Some(factory) = node.context_menu_factory.as_ref()
1932 && let Some(menu) = factory(position, &mut ctx)
1933 {
1934 menu_decision = Some((id, menu, true));
1935 break;
1936 }
1937 if let Some(contribution) = node.context_menu_contribution.as_ref()
1940 && let Some(section) = contribution(&mut ctx)
1941 {
1942 menu_decision = Some((id, section, false));
1943 break;
1944 }
1945 walker = node.parent;
1946 }
1947
1948 if let Some((owner_id, menu, by_factory)) = menu_decision.as_mut() {
1954 let sealed = *by_factory && self.is_focusable_control(*owner_id);
1955 if !sealed {
1956 let mut probe = if *by_factory {
1959 Some(*owner_id)
1960 } else {
1961 self.arena.get(*owner_id).and_then(|node| node.parent)
1962 };
1963 while let Some(id) = probe {
1964 let Some(node) = self.arena.get(id) else {
1965 break;
1966 };
1967 if id != *owner_id && self.is_focusable_control(id) {
1968 break;
1969 }
1970 if let Some(contribution) = node.context_menu_contribution.as_ref()
1971 && let Some(section) = contribution(&mut ctx)
1972 && menu.append_context_menu_section(section).is_err()
1973 {
1974 note_refused_section(*owner_id);
1975 }
1976 probe = node.parent;
1977 }
1978 }
1979 }
1980 let menu_decision = menu_decision.map(|(owner, menu, _)| (owner, menu));
1981
1982 let drain_anchor = menu_decision.as_ref().map(|(id, _)| *id).unwrap_or(target);
1988 self.collect_from_ctx(ctx, drain_anchor);
1989
1990 let Some((owner_id, menu_widget)) = menu_decision else {
1991 return false;
1992 };
1993
1994 let keep: std::collections::HashSet<WidgetId> = self
2000 .overlay_manager
2001 .stack
2002 .iter()
2003 .map(|o| o.content_id)
2004 .filter(|&content_id| self.is_descendant_of(owner_id, content_id))
2005 .collect();
2006 let dismissed = self.overlay_manager.dismiss_except(&keep);
2007 self.dormant_dismissed_content(&dismissed, &mut *ops);
2008
2009 let content_id = self.add_boxed(menu_widget);
2010 let prev_focus = self.focused;
2011 let placement =
2018 crate::overlay::OverlayPlacement::at_pointer_for(position, &self.current_input.pointer);
2019 let mut request = crate::overlay::OverlayRequest::new(
2022 content_id,
2023 owner_id,
2024 placement,
2025 crate::overlay::DismissBehavior::EscapeOrClickOutside,
2026 );
2027 request.destroy_content_on_dismiss = true;
2028 self.overlay_manager.show(request);
2029 if let Some(focus_id) = prev_focus {
2030 self.record_top_focus_restore(focus_id);
2031 }
2032 if trigger != crate::widget_builder::ContextMenuTrigger::Pointer
2036 && !self.focus_visible.get()
2037 {
2038 self.focus_visible.set(true);
2039 }
2040 self.focus_ops(content_id, &mut *ops);
2041 self.drain_pending_intents(&mut *ops);
2046 true
2047 }
2048
2049 fn is_focusable_control(&self, id: WidgetId) -> bool {
2054 let focusable = |id: WidgetId| {
2055 self.arena
2056 .get(id)
2057 .is_some_and(|node| self.is_node_focusable(node))
2058 };
2059 if focusable(id) || self.focusable_composite_behind(id).is_some() {
2060 return true;
2061 }
2062 let mut current = id;
2063 for _ in 0..8 {
2064 let Some(proxy) = self.live_accessibility_proxy(current) else {
2065 return false;
2066 };
2067 if focusable(proxy) {
2068 return true;
2069 }
2070 current = proxy;
2071 }
2072 false
2073 }
2074
2075 fn arm_outside_press_dismissal(
2096 &mut self,
2097 position: Point,
2098 button: PointerButton,
2099 ops: &mut dyn crate::window::WindowOps,
2100 ) -> bool {
2101 self.prune_stale_dismiss_arms();
2102 let pressing = self.current_input.pointer;
2103 let busy = self.busy_press_points(pressing.id);
2104
2105 if pressing.kind.is_direct() {
2106 return self
2107 .overlay_manager
2108 .arm_dismiss(pressing.id, position, &busy)
2109 .suppress_beneath;
2110 }
2111
2112 let mark = self.dismissal_mark();
2113 let (dismissed, focus_restore, toggle_anchors) = self
2114 .overlay_manager
2115 .dismiss_outside_press_toggles(position, &busy);
2116 if dismissed.is_empty() {
2117 return false;
2118 }
2119 self.dormant_dismissed_content(&dismissed, &mut *ops);
2120 self.return_focus_after_dismissal(focus_restore, mark, &mut *ops);
2121 toggle_anchors
2134 .iter()
2135 .any(|&toggle| self.press_reopens(toggle, position, button, &pressing))
2136 }
2137
2138 fn press_reopens(
2150 &self,
2151 toggle: crate::overlay::ToggleAnchor,
2152 position: Point,
2153 button: PointerButton,
2154 pointer: &crate::pointer::PointerInfo,
2155 ) -> bool {
2156 let anchor = toggle.anchor;
2157 if !self.arena.is_active(anchor) || !self.arena.bounds(anchor).contains(position) {
2158 return false;
2159 }
2160 if toggle.modal {
2161 return self.first_tapper(position, button, pointer) == Some(anchor);
2162 }
2163 button == PointerButton::Primary
2164 || self
2165 .anchor_tap_buttons(anchor, position, pointer)
2166 .contains(button)
2167 }
2168
2169 fn first_tapper(
2172 &self,
2173 position: Point,
2174 button: PointerButton,
2175 pointer: &crate::pointer::PointerInfo,
2176 ) -> Option<WidgetId> {
2177 let mut node = self.hit_test_for(position, pointer);
2178 while let Some(id) = node {
2179 if self.tap_buttons(id).contains(button) {
2180 return Some(id);
2181 }
2182 node = self.arena.parent(id);
2183 }
2184 None
2185 }
2186
2187 fn anchor_tap_buttons(
2197 &self,
2198 anchor: WidgetId,
2199 position: Point,
2200 pointer: &crate::pointer::PointerInfo,
2201 ) -> ButtonMask {
2202 let mut mask = ButtonMask::NONE;
2203 let mut node = self.hit_test_for(position, pointer);
2204 while let Some(id) = node {
2205 mask = mask.union(self.tap_buttons(id));
2206 if id == anchor {
2207 return mask;
2208 }
2209 node = self.arena.parent(id);
2210 }
2211 ButtonMask::NONE
2212 }
2213
2214 fn commit_outside_press_dismissal(
2221 &mut self,
2222 position: Point,
2223 ops: &mut dyn crate::window::WindowOps,
2224 ) -> bool {
2225 let pointer = self.current_pointer_id();
2226 if !self.overlay_manager.has_armed_dismiss(pointer) {
2227 return false;
2228 }
2229 let mark = self.dismissal_mark();
2230 let (dismissed, focus_restore, _anchors) =
2231 self.overlay_manager.commit_dismiss(pointer, position);
2232 if !dismissed.is_empty() {
2233 self.dormant_dismissed_content(&dismissed, &mut *ops);
2234 self.return_focus_after_dismissal(focus_restore, mark, &mut *ops);
2235 }
2236 true
2237 }
2238
2239 fn busy_press_points(&self, exclude: crate::pointer::PointerId) -> Vec<Point> {
2256 self.pointers
2257 .iter()
2258 .filter(|entry| entry.info.id != exclude)
2259 .filter(|entry| self.press_is_revocable(entry.info.id))
2260 .map(|entry| entry.down_position)
2261 .collect()
2262 }
2263
2264 fn prune_stale_dismiss_arms(&mut self) {
2271 let stale: Vec<crate::pointer::PointerId> = self
2272 .overlay_manager
2273 .armed_pointers()
2274 .into_iter()
2275 .filter(|id| self.pointers.get(*id).is_none())
2276 .collect();
2277 for id in stale {
2278 self.overlay_manager.abort_dismiss(id);
2279 }
2280 }
2281
2282 fn begin_press(&mut self, focusable: Option<WidgetId>) {
2303 let pointer = self.current_pointer_id();
2304 let now = self.sequence_now();
2305 let delay = self
2306 .pan_session_open(pointer)
2307 .then(|| self.current_profile().press_feedback_delay)
2308 .filter(|d| !d.is_zero());
2309 self.presses.press(pointer, focusable, now, delay);
2310 }
2311
2312 fn adopt_press_owner(&mut self, button: PointerButton) {
2335 let pointer = self.current_pointer_id();
2336 let Some(owner) = self.current_sequence().and_then(|s| s.pressed_owner()) else {
2337 return;
2338 };
2339 if !self.press_buttons(owner).contains(button) {
2340 crate::trace_input!(
2341 Samples,
2342 "no press visual for {owner:?}: {button:?} is not one of its accepted buttons"
2343 );
2344 return;
2345 }
2346 self.presses.set_owner(pointer, owner);
2347 self.publish_pressed(owner);
2348 }
2349
2350 fn update_press(&mut self, position: Point) {
2363 let pointer = self.current_pointer_id();
2364 if self.presses.get(pointer).is_none() {
2365 return;
2366 }
2367 let claimed_elsewhere = self.current_sequence().is_some_and(|sequence| {
2370 sequence
2371 .winner()
2372 .is_some_and(|winner| Some(winner) != sequence.pressed_owner())
2373 });
2374 if claimed_elsewhere {
2375 self.end_press(pointer);
2376 return;
2377 }
2378 let profile = self.current_profile();
2379 let origin = self.current_sequence().map(|s| s.press_origin());
2380 let owner = self.presses.get(pointer).and_then(|p| p.owner);
2381 let inside = match (origin, owner) {
2382 (Some(origin), Some(owner)) => {
2383 let bounds = self
2384 .arena
2385 .is_active(owner)
2386 .then(|| self.arena.bounds(owner));
2387 !crate::gesture::TapBoundary::for_pointer(&self.current_input.pointer, &profile)
2388 .left(origin, position, bounds, &profile)
2389 }
2390 _ => true,
2393 };
2394 let Some(press) = self.presses.get_mut(pointer) else {
2395 return;
2396 };
2397 if press.inside == inside {
2398 return;
2399 }
2400 press.inside = inside;
2401 if let Some(owner) = owner {
2402 self.publish_pressed(owner);
2403 }
2404 }
2405
2406 pub(crate) fn end_press(&mut self, pointer: crate::pointer::PointerId) {
2412 if let Some(owner) = self.presses.release(pointer) {
2413 self.publish_pressed(owner);
2414 }
2415 }
2416
2417 pub(crate) fn resolve_press_delays(&mut self, now: crate::pointer::EventTime) {
2420 if self.presses.is_empty() {
2421 return;
2422 }
2423 for id in self.presses.resolve_delays(now) {
2424 self.publish_pressed(id);
2425 self.arena.mark_needs_paint(id);
2426 }
2427 }
2428
2429 pub(crate) fn next_press_deadline(&self) -> Option<std::time::Instant> {
2432 self.presses.next_deadline().map(|t| self.instant_for(t))
2433 }
2434
2435 pub(crate) fn commit_pending_edit(&mut self, ops: &mut dyn crate::window::WindowOps) {
2451 let Some(focus) = self.focused.filter(|id| self.arena.is_active(*id)) else {
2452 return;
2453 };
2454 let (chain, _, _) = self.intent_walk(focus, &[]);
2455 for id in chain {
2456 for external in [true, false] {
2457 let handler = self.arena.get_mut(id).and_then(|node| {
2458 let handlers = if external {
2459 &mut node.external_handlers
2460 } else {
2461 &mut node.handlers
2462 };
2463 handlers.on_commit_edit.take()
2464 });
2465 let Some(mut handler) = handler else { continue };
2466 let mut ctx = self.make_event_context(&mut *ops).with_dispatch_node(id);
2467 handler(&mut ctx);
2468 if let Some(node) = self.arena.get_mut(id) {
2469 let handlers = if external {
2470 &mut node.external_handlers
2471 } else {
2472 &mut node.handlers
2473 };
2474 if handlers.on_commit_edit.is_none() {
2476 handlers.on_commit_edit = Some(handler);
2477 }
2478 }
2479 self.collect_from_ctx(ctx, id);
2480 }
2481 }
2482 }
2483
2484 fn release_taps_unfocusable(&self, position: Point, button: PointerButton) -> bool {
2505 let Some(press) = self.presses.get(self.current_pointer_id()) else {
2506 return false;
2507 };
2508 let Some(owner) = press.owner.filter(|_| press.focusable.is_none()) else {
2509 return false;
2510 };
2511 if !self.arena.is_active(owner) || !self.tap_buttons(owner).contains(button) {
2512 return false;
2513 }
2514 let Some(sequence) = self.current_sequence() else {
2515 return false;
2516 };
2517 if let Some(winner) = sequence.winner()
2522 && (winner != owner
2523 || sequence.members().iter().any(|member| {
2524 member.id == winner
2525 && matches!(member.role, crate::gesture::MemberRole::Gesture)
2526 }))
2527 {
2528 return false;
2529 }
2530 let profile = self.current_profile();
2531 let left = crate::gesture::TapBoundary::for_pointer(&self.current_input.pointer, &profile)
2532 .left(
2533 sequence.press_origin(),
2534 position,
2535 Some(self.arena.bounds(owner)),
2536 &profile,
2537 );
2538 !left && !self.in_scroll_chrome(owner) && !self.belongs_to_focused(owner)
2539 }
2540
2541 fn in_scroll_chrome(&self, id: WidgetId) -> bool {
2546 let mut current = Some(id);
2547 while let Some(node) = current {
2548 if self.arena.get(node).is_some_and(|n| n.scroll_chrome) {
2549 return true;
2550 }
2551 current = self.arena.parent(node);
2552 }
2553 false
2554 }
2555
2556 fn belongs_to_focused(&self, id: WidgetId) -> bool {
2561 self.focused
2562 .is_some_and(|focus| self.intent_walk(id, &[]).0.contains(&focus))
2563 }
2564
2565 fn click_may_focus(&self, focusable: WidgetId) -> bool {
2573 self.arena
2574 .get(focusable)
2575 .is_none_or(|node| node.focus_on_click)
2576 }
2577
2578 fn focus_on_release(
2602 &mut self,
2603 position: Point,
2604 taps_unfocusable: bool,
2605 ops: &mut dyn crate::window::WindowOps,
2606 ) {
2607 if taps_unfocusable {
2608 self.commit_pending_edit(&mut *ops);
2609 return;
2610 }
2611 let releasing = self.current_input.pointer;
2612 let pointer = self.current_pointer_id();
2613 let Some(pressed) = self.presses.get(pointer).and_then(|p| p.focusable) else {
2614 return;
2615 };
2616 if !releasing.kind.is_direct() && self.click_may_focus(pressed) {
2617 return;
2618 }
2619 let Some(target) = self.hit_test_for(position, &releasing) else {
2620 return;
2621 };
2622 if self.find_focusable_at_or_above(target) != Some(pressed) {
2623 return;
2624 }
2625 if !self.click_may_focus(pressed) {
2626 if !self.in_scroll_chrome(target) {
2629 self.commit_pending_edit(&mut *ops);
2630 }
2631 } else {
2632 self.focus_with_origin_ops(
2633 pressed,
2634 crate::focus::FocusOrigin::Pointer(releasing.kind),
2635 &mut *ops,
2636 );
2637 }
2638 }
2639
2640 fn handle_pointer_move(
2660 &mut self,
2661 event: &WidgetEvent,
2662 position: Point,
2663 ops: &mut dyn crate::window::WindowOps,
2664 ) {
2665 let moving = self.current_input.pointer;
2670 if self.pointers.hover_owner_id() != Some(self.current_pointer_id()) {
2671 if let Some(target) = self.hit_test_for(position, &moving) {
2672 self.dispatch_to_widget(target, event, &mut *ops);
2673 }
2674 return;
2675 }
2676 let target = self.hit_test_for(position, &moving);
2677
2678 if target != self.hovered_id() {
2679 let hover = self.hover_transition_pointer();
2681 let previously_hovered = self.hovered_id();
2682 if let Some(old) = previously_hovered {
2683 self.dispatch_to_widget(
2684 old,
2685 &WidgetEvent::PointerLeave { pointer: hover },
2686 &mut *ops,
2687 );
2688 self.tooltip_pointer_leave(old, &mut *ops);
2689 }
2690 if let Some(new) = target {
2691 self.dispatch_to_widget(
2692 new,
2693 &WidgetEvent::PointerEnter { pointer: hover },
2694 &mut *ops,
2695 );
2696 self.tooltip_pointer_enter(new);
2697 }
2698 self.tooltip_dwell_held = false;
2701 self.set_hovered(target);
2702 self.update_hover_within_signals(previously_hovered, target);
2703 } else if let Some(target) = target {
2704 self.tooltip_pointer_moved(target, position);
2707 }
2708
2709 if let Some(target) = target {
2710 self.dispatch_to_widget(target, event, &mut *ops);
2711 }
2712 }
2713
2714 pub(super) fn dispatch_to_widget(
2715 &mut self,
2716 target: WidgetId,
2717 event: &WidgetEvent,
2718 ops: &mut dyn crate::window::WindowOps,
2719 ) {
2720 self.dispatch_to_widget_returning_handled(target, event, ops);
2721 }
2722
2723 pub(super) fn localize_event(
2739 frame: &crate::arena::LocalFrame,
2740 event: &WidgetEvent,
2741 ) -> Option<WidgetEvent> {
2742 match event {
2743 WidgetEvent::PointerDown {
2744 position,
2745 button,
2746 modifiers,
2747 pointer,
2748 } => Some(WidgetEvent::PointerDown {
2749 position: frame.to_local(*position),
2750 button: *button,
2751 modifiers: *modifiers,
2752 pointer: *pointer,
2753 }),
2754 WidgetEvent::PointerUp {
2755 position,
2756 button,
2757 modifiers,
2758 pointer,
2759 } => Some(WidgetEvent::PointerUp {
2760 position: frame.to_local(*position),
2761 button: *button,
2762 modifiers: *modifiers,
2763 pointer: *pointer,
2764 }),
2765 WidgetEvent::PointerMove {
2766 position,
2767 modifiers,
2768 pointer,
2769 } => Some(WidgetEvent::PointerMove {
2770 position: frame.to_local(*position),
2771 modifiers: *modifiers,
2772 pointer: *pointer,
2773 }),
2774 WidgetEvent::Gesture { gesture } => Some(WidgetEvent::Gesture {
2775 gesture: Self::localize_gesture(frame, gesture),
2776 }),
2777 _ => None,
2783 }
2784 }
2785
2786 fn localize_gesture(frame: &crate::arena::LocalFrame, gesture: &GestureEvent) -> GestureEvent {
2793 let loc = |p: teksilo_canvas::Point| frame.to_local(p);
2794 let tap = |t: &TapEvent| {
2795 TapEvent::new(loc(t.position), t.button, t.modifiers).with_pointer(t.pointer)
2796 };
2797 match gesture {
2798 GestureEvent::Tap(t) => GestureEvent::Tap(tap(t)),
2799 GestureEvent::DoubleTap(t) => GestureEvent::DoubleTap(tap(t)),
2800 GestureEvent::TripleTap(t) => GestureEvent::TripleTap(tap(t)),
2801 GestureEvent::LongPress(t) => GestureEvent::LongPress(tap(t)),
2802 GestureEvent::DragStarted {
2803 position,
2804 button,
2805 pointer,
2806 } => GestureEvent::DragStarted {
2807 position: loc(*position),
2808 button: *button,
2809 pointer: *pointer,
2810 },
2811 GestureEvent::DragMoved {
2812 position,
2813 delta,
2814 pointer,
2815 } => GestureEvent::DragMoved {
2816 position: loc(*position),
2817 delta: *delta,
2818 pointer: *pointer,
2819 },
2820 GestureEvent::DragEnded { position, pointer } => GestureEvent::DragEnded {
2821 position: loc(*position),
2822 pointer: *pointer,
2823 },
2824 GestureEvent::DragCancelled {
2825 position,
2826 pointer,
2827 reason,
2828 } => GestureEvent::DragCancelled {
2829 position: loc(*position),
2830 pointer: *pointer,
2831 reason: *reason,
2832 },
2833 GestureEvent::PinchStarted { center } => GestureEvent::PinchStarted {
2834 center: loc(*center),
2835 },
2836 GestureEvent::PinchChanged {
2837 center,
2838 scale,
2839 rotation,
2840 } => GestureEvent::PinchChanged {
2841 center: loc(*center),
2842 scale: *scale,
2843 rotation: *rotation,
2844 },
2845 GestureEvent::PinchEnded => GestureEvent::PinchEnded,
2846 GestureEvent::PinchCancelled { reason } => {
2847 GestureEvent::PinchCancelled { reason: *reason }
2848 }
2849 GestureEvent::Swipe {
2850 direction,
2851 velocity,
2852 } => GestureEvent::Swipe {
2853 direction: *direction,
2854 velocity: *velocity,
2855 },
2856 }
2857 }
2858
2859 pub(super) fn dispatch_to_widget_returning_handled(
2865 &mut self,
2866 target: WidgetId,
2867 event: &WidgetEvent,
2868 ops: &mut dyn crate::window::WindowOps,
2869 ) -> bool {
2870 if !self.arena.is_enabled(target) && !matches!(event, WidgetEvent::FocusLost) {
2876 return false;
2877 }
2878
2879 let mut ancestors = Vec::new();
2880 let mut current = self.arena.parent(target);
2881 while let Some(id) = current {
2882 ancestors.push(id);
2883 current = self.arena.parent(id);
2884 }
2885 ancestors.reverse();
2886
2887 let tap_owner: Option<WidgetId> = if matches!(
2905 event,
2906 WidgetEvent::PointerDown { .. } | WidgetEvent::PointerUp { .. }
2907 ) {
2908 let mut owner = None;
2909 let mut cur = Some(target);
2910 while let Some(id) = cur {
2911 if self.arena.get(id).is_some_and(|n| {
2912 n.any_handler(|h| h.on_tap.is_some() || h.on_long_press.is_some())
2913 }) {
2914 owner = Some(id);
2915 break;
2916 }
2917 cur = self.arena.parent(id);
2918 }
2919 owner
2920 } else {
2921 None
2922 };
2923
2924 for &id in &ancestors {
2925 let frame = self.arena.local_frame(id);
2926 let mut ctx = self
2927 .make_event_context(&mut *ops)
2928 .with_dispatch_node(id)
2929 .with_dispatch_target(target)
2930 .with_local_frame(frame);
2931 ctx.press_claimed_by_interactive_child =
2932 tap_owner.is_some_and(|owner| owner != id && self.is_descendant_of(owner, id));
2933 let localized = Self::localize_event(&frame, event);
2936 let event = localized.as_ref().unwrap_or(event);
2937 let response = if let Some(node) = self.arena.get_mut(id) {
2938 Self::try_handler_preview(node, event, &mut ctx).unwrap_or(EventResponse::Ignored)
2939 } else {
2940 EventResponse::Ignored
2941 };
2942 self.collect_from_ctx(ctx, id);
2943 if response == EventResponse::Handled {
2944 self.arena.mark_needs_paint(id);
2945 if matches!(event, WidgetEvent::PointerDown { .. }) {
2953 self.note_preview_claim(id);
2954 }
2955 return true;
2956 }
2957 }
2958
2959 let needs_layout_on_handle = matches!(
2960 event,
2961 WidgetEvent::Scroll { .. } | WidgetEvent::ScrollIntoView { .. }
2962 );
2963 let mut current = Some(target);
2964 let mut is_target = true;
2965 while let Some(id) = current {
2966 let frame = self.arena.local_frame(id);
2967 let mut ctx = self
2968 .make_event_context(&mut *ops)
2969 .with_dispatch_node(id)
2970 .with_dispatch_target(target)
2971 .with_local_frame(frame);
2972 ctx.press_claimed_by_interactive_child =
2973 tap_owner.is_some_and(|owner| owner != id && self.is_descendant_of(owner, id));
2974 let localized = Self::localize_event(&frame, event);
2977 let gesture_cx = self.recognizer_context(id);
2978 let arena_blocked = self.sequence_blocks_arena(id);
2981 let WidgetTree {
2982 arena,
2983 gesture_owners,
2984 ..
2985 } = self;
2986 let event = localized.as_ref().unwrap_or(event);
2987 let response = if let Some(node) = arena.get_mut(id) {
2988 Self::try_handler_bubble(
2989 node,
2990 event,
2991 &mut ctx,
2992 BubbleGates {
2993 fire_on_pointer_event: is_target,
2994 arena_blocked,
2995 },
2996 id,
2997 gesture_owners,
2998 gesture_cx,
2999 )
3000 .unwrap_or(EventResponse::Ignored)
3001 } else {
3002 EventResponse::Ignored
3003 };
3004 self.collect_from_ctx(ctx, id);
3005 if response == EventResponse::Handled {
3006 if needs_layout_on_handle {
3007 self.arena.mark_needs_layout(id);
3008 } else {
3009 self.arena.mark_needs_paint(id);
3010 }
3011 self.note_pointer_acceptance(id, event);
3012 if !matches!(
3027 event,
3028 WidgetEvent::PointerEnter { .. } | WidgetEvent::PointerLeave { .. }
3029 ) {
3030 return true;
3031 }
3032 }
3033 is_target = false;
3034 current = self.arena.parent(id);
3035 }
3036 false
3037 }
3038
3039 pub(super) fn dispatch_to_widget_direct(
3040 &mut self,
3041 target: WidgetId,
3042 event: &WidgetEvent,
3043 ops: &mut dyn crate::window::WindowOps,
3044 ) {
3045 self.dispatch_to_widget_direct_returning_handled(target, event, ops);
3046 }
3047
3048 pub(super) fn dispatch_to_widget_direct_returning_handled(
3058 &mut self,
3059 target: WidgetId,
3060 event: &WidgetEvent,
3061 ops: &mut dyn crate::window::WindowOps,
3062 ) -> bool {
3063 if !self.arena.is_enabled(target) && !matches!(event, WidgetEvent::FocusLost) {
3066 return false;
3067 }
3068
3069 let frame = self.arena.local_frame(target);
3070 let mut ctx = self
3071 .make_event_context(&mut *ops)
3072 .with_dispatch_node(target)
3073 .with_dispatch_target(target)
3074 .with_local_frame(frame);
3075 let gesture_cx = self.recognizer_context(target);
3076 let arena_blocked = self.sequence_blocks_arena(target);
3077 let WidgetTree {
3078 arena,
3079 gesture_owners,
3080 ..
3081 } = self;
3082 let response = if let Some(node) = arena.get_mut(target) {
3083 Self::try_handler_bubble(
3084 node,
3085 event,
3086 &mut ctx,
3087 BubbleGates {
3088 fire_on_pointer_event: true,
3089 arena_blocked,
3090 },
3091 target,
3092 gesture_owners,
3093 gesture_cx,
3094 )
3095 .unwrap_or(EventResponse::Ignored)
3096 } else {
3097 EventResponse::Ignored
3098 };
3099 self.collect_from_ctx(ctx, target);
3100
3101 if response == EventResponse::Handled {
3102 if matches!(
3105 event,
3106 WidgetEvent::Scroll { .. } | WidgetEvent::ScrollIntoView { .. }
3107 ) {
3108 self.arena.mark_needs_layout(target);
3109 } else {
3110 self.arena.mark_needs_paint(target);
3111 }
3112 self.note_pointer_acceptance(target, event);
3113 }
3114 response == EventResponse::Handled
3115 }
3116
3117 pub(super) fn note_pointer_acceptance(&mut self, target: WidgetId, event: &WidgetEvent) {
3126 if !matches!(
3127 event,
3128 WidgetEvent::PointerDown { .. }
3129 | WidgetEvent::PointerMove { .. }
3130 | WidgetEvent::PointerUp { .. }
3131 ) {
3132 return;
3133 }
3134 let pointer = self.current_pointer_id();
3135 if let Some(entry) = self.pointers.get_mut(pointer) {
3136 entry.last_accepted = Some(target);
3137 }
3138 }
3139
3140 fn try_handler_preview(
3141 node: &mut crate::arena::WidgetNode,
3142 event: &WidgetEvent,
3143 ctx: &mut EventContext,
3144 ) -> Option<EventResponse> {
3145 match event {
3146 WidgetEvent::KeyDown { .. }
3154 | WidgetEvent::KeyUp { .. }
3155 | WidgetEvent::ImeComposition { .. }
3156 | WidgetEvent::ImeCommit { .. } => {
3157 let has = node.external_handlers.on_key_preview.is_some()
3158 || node.handlers.on_key_preview.is_some();
3159 if !has {
3160 return None;
3161 }
3162 Some(fire_event_handler_both(
3163 &mut node.external_handlers.on_key_preview,
3164 &mut node.handlers.on_key_preview,
3165 event,
3166 ctx,
3167 ))
3168 }
3169 WidgetEvent::PointerEnter { .. } | WidgetEvent::PointerLeave { .. } => None,
3181 _ => {
3182 let has = node.external_handlers.on_pointer_event.is_some()
3183 || node.handlers.on_pointer_event.is_some();
3184 if !has {
3185 return None;
3186 }
3187 Some(fire_event_handler_both(
3188 &mut node.external_handlers.on_pointer_event,
3189 &mut node.handlers.on_pointer_event,
3190 event,
3191 ctx,
3192 ))
3193 }
3194 }
3195 }
3196
3197 fn try_handler_bubble(
3198 node: &mut crate::arena::WidgetNode,
3199 event: &WidgetEvent,
3200 ctx: &mut EventContext,
3201 gates: BubbleGates,
3202 node_id: WidgetId,
3203 gesture_owners: &mut std::collections::HashSet<WidgetId>,
3204 gesture_cx: crate::gesture::RecognizerContext<'_>,
3205 ) -> Option<EventResponse> {
3206 let BubbleGates {
3207 fire_on_pointer_event,
3208 arena_blocked,
3209 } = gates;
3210 match event {
3211 WidgetEvent::PointerEnter { .. } => {
3212 if let Some(cursor) = node.node_cursor {
3213 ctx.declared_cursor_request = Some(cursor);
3218 }
3219 let mut fired = false;
3220 if let Some(h) = node.external_handlers.on_hover.as_mut() {
3221 h(true, ctx);
3222 fired = true;
3223 }
3224 if let Some(h) = node.handlers.on_hover.as_mut() {
3225 h(true, ctx);
3226 fired = true;
3227 }
3228 if fired {
3229 Some(EventResponse::Handled)
3230 } else {
3231 node.node_cursor.map(|_| EventResponse::Handled)
3232 }
3233 }
3234 WidgetEvent::PointerLeave { .. } => {
3235 if node.node_cursor.is_some() {
3236 ctx.declared_cursor_request = Some(crate::widget::CursorIcon::Default);
3237 }
3238 let mut fired = false;
3239 if let Some(h) = node.external_handlers.on_hover.as_mut() {
3240 h(false, ctx);
3241 fired = true;
3242 }
3243 if let Some(h) = node.handlers.on_hover.as_mut() {
3244 h(false, ctx);
3245 fired = true;
3246 }
3247 if fired {
3248 Some(EventResponse::Handled)
3249 } else {
3250 node.node_cursor.map(|_| EventResponse::Handled)
3251 }
3252 }
3253 WidgetEvent::FocusGained { origin } => {
3254 let previous_origin = ctx.focus_origin.replace(*origin);
3258 let mut fired = false;
3259 if let Some(h) = node.external_handlers.on_focus.as_mut() {
3260 h(true, ctx);
3261 fired = true;
3262 }
3263 if let Some(h) = node.handlers.on_focus.as_mut() {
3264 h(true, ctx);
3265 fired = true;
3266 }
3267 ctx.focus_origin = previous_origin;
3268 fired.then_some(EventResponse::Handled)
3269 }
3270 WidgetEvent::FocusLost => {
3271 let mut fired = false;
3272 if let Some(h) = node.external_handlers.on_focus.as_mut() {
3273 h(false, ctx);
3274 fired = true;
3275 }
3276 if let Some(h) = node.handlers.on_focus.as_mut() {
3277 h(false, ctx);
3278 fired = true;
3279 }
3280 fired.then_some(EventResponse::Handled)
3281 }
3282 WidgetEvent::KeyDown { .. }
3283 | WidgetEvent::KeyUp { .. }
3284 | WidgetEvent::ImeComposition { .. }
3285 | WidgetEvent::ImeCommit { .. } => {
3286 if node.external_handlers.on_key.is_some() || node.handlers.on_key.is_some() {
3287 Some(fire_event_handler_both(
3288 &mut node.external_handlers.on_key,
3289 &mut node.handlers.on_key,
3290 event,
3291 ctx,
3292 ))
3293 } else {
3294 None
3295 }
3296 }
3297 WidgetEvent::Scroll { .. } | WidgetEvent::ScrollIntoView { .. } => {
3298 if node.external_handlers.on_scroll.is_some() || node.handlers.on_scroll.is_some() {
3299 Some(fire_event_handler_both(
3300 &mut node.external_handlers.on_scroll,
3301 &mut node.handlers.on_scroll,
3302 event,
3303 ctx,
3304 ))
3305 } else {
3306 None
3307 }
3308 }
3309 WidgetEvent::AccessAction {
3310 action,
3311 target_node,
3312 data,
3313 ..
3314 } => {
3315 let saved_a11y_source = ctx
3335 .current_source
3336 .replace(crate::telemetry::IntentSource::Accessibility);
3337 let mut any_slot = false;
3338 let mut any_handled = false;
3339 if let Some(h) = node.external_handlers.on_access_action_request.as_mut() {
3340 any_slot = true;
3341 any_handled |=
3342 h(*action, *target_node, data.clone(), ctx) == EventResponse::Handled;
3343 }
3344 if let Some(h) = node.handlers.on_access_action_request.as_mut() {
3345 any_slot = true;
3346 any_handled |=
3347 h(*action, *target_node, data.clone(), ctx) == EventResponse::Handled;
3348 }
3349 if let Some(h) = node.external_handlers.on_access_action.as_mut() {
3350 any_slot = true;
3351 any_handled |= h(*action, ctx) == EventResponse::Handled;
3352 }
3353 if let Some(h) = node.handlers.on_access_action.as_mut() {
3354 any_slot = true;
3355 any_handled |= h(*action, ctx) == EventResponse::Handled;
3356 }
3357 let user_handled = any_slot.then_some(if any_handled {
3358 EventResponse::Handled
3359 } else {
3360 EventResponse::Ignored
3361 });
3362
3363 let mut override_handled = false;
3369 if let Some(ov) = node.access_overrides.as_deref_mut() {
3370 if matches!(action, accesskit::Action::CustomAction) {
3371 if let Some(accesskit::ActionData::CustomAction(idx)) = data
3372 && let Some((_, cb)) = ov.custom_actions.get_mut(*idx as usize)
3373 {
3374 cb(ctx);
3375 override_handled = true;
3376 }
3377 } else {
3378 for (a, cb) in ov.actions.iter_mut() {
3379 if *a == *action {
3380 cb(ctx);
3381 override_handled = true;
3382 }
3383 }
3384 }
3385 }
3386
3387 ctx.current_source = saved_a11y_source;
3388 match (user_handled, override_handled) {
3389 (Some(EventResponse::Handled), _) | (_, true) => Some(EventResponse::Handled),
3390 (Some(EventResponse::Ignored), false) => Some(EventResponse::Ignored),
3391 (None, false) => None,
3392 }
3393 }
3394 WidgetEvent::Gesture { gesture } => {
3395 let matched = matches!(
3399 gesture,
3400 GestureEvent::PinchStarted { .. }
3401 | GestureEvent::PinchChanged { .. }
3402 | GestureEvent::PinchEnded
3403 | GestureEvent::Swipe { .. }
3404 | GestureEvent::DoubleTap { .. }
3405 | GestureEvent::TripleTap { .. }
3406 ) && {
3407 let has_handler = match gesture {
3408 GestureEvent::PinchStarted { .. }
3409 | GestureEvent::PinchChanged { .. }
3410 | GestureEvent::PinchEnded => node.any_handler(|h| h.on_pinch.is_some()),
3411 GestureEvent::Swipe { .. } => node.any_handler(|h| h.on_swipe.is_some()),
3412 GestureEvent::DoubleTap { .. } => {
3413 node.any_handler(|h| h.on_double_tap.is_some())
3414 }
3415 GestureEvent::TripleTap { .. } => {
3416 node.any_handler(|h| h.on_triple_tap.is_some())
3417 }
3418 _ => false,
3419 };
3420 if has_handler {
3421 Self::dispatch_recognized_gesture(node, *gesture, ctx);
3422 }
3423 has_handler
3424 };
3425 if matched {
3426 Some(EventResponse::Handled)
3427 } else {
3428 None
3429 }
3430 }
3431 WidgetEvent::PointerDown {
3432 position,
3433 button,
3434 modifiers,
3435 ..
3436 } => {
3437 if fire_on_pointer_event {
3444 let r = fire_event_handler_both(
3445 &mut node.external_handlers.on_pointer_event,
3446 &mut node.handlers.on_pointer_event,
3447 event,
3448 ctx,
3449 );
3450 if r == EventResponse::Handled {
3451 return Some(EventResponse::Handled);
3452 }
3453 }
3454 if arena_blocked {
3455 return None;
3459 }
3460 Self::ensure_gesture_arena(node, node_id, gesture_owners);
3461 if let Some(arena) = node.handlers.gesture_arena.as_mut() {
3462 let cx = gesture_cx;
3463 ctx.capture_pointer_implicit();
3478 let result = arena.process(
3479 &RawPointerEvent::Down {
3480 position: *position,
3481 button: *button,
3482 modifiers: *modifiers,
3483 pointer: cx.pointer,
3484 time: cx.now,
3485 },
3486 &cx,
3487 );
3488 if let Some(gesture) = result {
3489 Self::dispatch_recognized_gesture(node, gesture, ctx);
3490 }
3491 return Some(EventResponse::Handled);
3492 }
3493 None
3494 }
3495 WidgetEvent::PointerUp {
3496 position,
3497 button,
3498 modifiers,
3499 ..
3500 } => {
3501 if fire_on_pointer_event {
3502 let r = fire_event_handler_both(
3503 &mut node.external_handlers.on_pointer_event,
3504 &mut node.handlers.on_pointer_event,
3505 event,
3506 ctx,
3507 );
3508 if r == EventResponse::Handled {
3509 return Some(EventResponse::Handled);
3510 }
3511 }
3512 if arena_blocked {
3513 return None;
3514 }
3515 if let Some(arena) = node.handlers.gesture_arena.as_mut() {
3516 let cx = gesture_cx;
3517 let result = arena.process(
3518 &RawPointerEvent::Up {
3519 position: *position,
3520 button: *button,
3521 modifiers: *modifiers,
3522 pointer: cx.pointer,
3523 time: cx.now,
3524 },
3525 &cx,
3526 );
3527 if let Some(gesture) = result {
3528 Self::dispatch_recognized_gesture(node, gesture, ctx);
3529 }
3530 return Some(EventResponse::Handled);
3531 }
3532 None
3533 }
3534 WidgetEvent::PointerMove { position, .. } => {
3535 if fire_on_pointer_event {
3536 let r = fire_event_handler_both(
3537 &mut node.external_handlers.on_pointer_event,
3538 &mut node.handlers.on_pointer_event,
3539 event,
3540 ctx,
3541 );
3542 if r == EventResponse::Handled {
3543 return Some(EventResponse::Handled);
3544 }
3545 }
3546 if arena_blocked {
3547 return None;
3548 }
3549 if let Some(arena) = node.handlers.gesture_arena.as_mut() {
3550 let cx = gesture_cx;
3551 let result = arena.process(
3552 &RawPointerEvent::Move {
3553 position: *position,
3554 pointer: cx.pointer,
3555 time: cx.now,
3556 },
3557 &cx,
3558 );
3559 if let Some(gesture) = result {
3560 Self::dispatch_recognized_gesture(node, gesture, ctx);
3561 return Some(EventResponse::Handled);
3573 }
3574 return Some(EventResponse::Ignored);
3575 }
3576 None
3577 }
3578 WidgetEvent::PointerCancel {
3579 reason, pointer, ..
3580 } => {
3581 for slot in [
3589 &mut node.external_handlers.on_pointer_cancel,
3590 &mut node.handlers.on_pointer_cancel,
3591 ] {
3592 if let Some(handler) = slot.as_mut() {
3593 handler(pointer, *reason, ctx);
3594 }
3595 }
3596 if fire_on_pointer_event {
3597 let r = fire_event_handler_both(
3598 &mut node.external_handlers.on_pointer_event,
3599 &mut node.handlers.on_pointer_event,
3600 event,
3601 ctx,
3602 );
3603 if r == EventResponse::Handled {
3604 return Some(EventResponse::Handled);
3605 }
3606 }
3607 None
3608 }
3609 }
3610 }
3611
3612 pub(super) fn collect_from_ctx<'ops>(
3613 &mut self,
3614 mut ctx: EventContext<'ops>,
3615 source_widget: WidgetId,
3616 ) {
3617 let local_ops = ctx.window_ops.take();
3623 let mut noop = crate::window::NoopWindowOps;
3624 let ops: &mut dyn crate::window::WindowOps = match local_ops {
3625 Some(o) => o,
3626 None => &mut noop,
3627 };
3628 if ctx.frame_requested {
3629 self.request_frame();
3630 }
3631 if let Some(declared) = ctx.declared_cursor_request {
3635 self.node_declared_cursor = declared;
3636 self.current_cursor = declared;
3637 }
3638 match ctx.cursor_request {
3639 Some(crate::widget::CursorRequest::Set(cursor)) => {
3640 self.current_cursor = cursor;
3641 }
3642 Some(crate::widget::CursorRequest::Release) => {
3646 self.current_cursor = self.node_declared_cursor;
3647 }
3648 None => {}
3649 }
3650 for intent in ctx.pending_intents {
3656 self.enqueue_intent(source_widget, intent, true);
3657 }
3658 if ctx.cancel_key_capture {
3667 self.cancel_key_capture();
3668 }
3669 if let Some(slot) = ctx.pending_key_capture {
3670 self.key_capture = Some(slot);
3671 }
3672 for mutation in ctx.pending_shortcut_mutations {
3674 match mutation {
3675 crate::widget::ShortcutMutation::RebindPrimary { id, keystroke } => {
3676 self.shortcut_registry.rebind_primary(id, keystroke);
3677 }
3678 crate::widget::ShortcutMutation::RebindSecondary { id, keystroke } => {
3679 self.shortcut_registry.rebind_secondary(id, keystroke);
3680 }
3681 crate::widget::ShortcutMutation::ClearOverride { id } => {
3682 self.shortcut_registry.clear_override(&id);
3683 }
3684 }
3685 }
3686 if ctx.close_request_to_modal {
3687 self.answer_close_request_with_modal(&mut *ops);
3688 }
3689 if ctx.close_window_requested {
3690 self.close_window_requested = true;
3691 }
3692 if ctx.force_close_requested {
3693 self.force_close_requested = true;
3694 }
3695 self.pending_modal_requests
3696 .extend(ctx.modal_requests.into_iter().map(|request| {
3697 crate::modal::QueuedModalRequest {
3698 source_widget,
3699 request,
3700 }
3701 }));
3702 let focus_intents = std::mem::take(&mut ctx.pending_focus_intents);
3712 let (fallback_crossings, fallback_modal) = if focus_intents.is_empty() {
3713 (Vec::new(), None)
3714 } else {
3715 let modal = self.intent_walk(source_widget, &[]).2;
3716 (self.intent_route(source_widget, &[], true).crossings, modal)
3717 };
3718 if ctx.dismiss_modal && !self.dismiss_modal_for_source(source_widget, &mut *ops) {
3719 self.pending_modal_dismissal = true;
3720 }
3721 for callback in ctx.idle_callbacks {
3722 self.idle_queue.push_boxed(callback);
3723 }
3724 match ctx.dismiss_scope {
3725 Some(crate::widget::DismissScope::All) => {
3726 let mark = self.dismissal_mark();
3727 let dismissed = self.overlay_manager.dismiss_all();
3728 self.dormant_dismissed_content(&dismissed, &mut *ops);
3729 self.return_focus_after_dismissal(None, mark, &mut *ops);
3730 }
3731 Some(crate::widget::DismissScope::AllExceptHosts) => {
3732 self.dismiss_all_overlays_except_hosts(&mut *ops);
3733 }
3734 Some(crate::widget::DismissScope::SelfChain) => {
3735 self.dismiss_self_overlay_chain_for_source(source_widget, &mut *ops);
3736 }
3737 Some(crate::widget::DismissScope::Top) => {
3738 let mark = self.dismissal_mark();
3739 if let Some((_id, content_ids, focus_restore)) = self.overlay_manager.dismiss_top()
3740 {
3741 self.dormant_dismissed_content(&content_ids, &mut *ops);
3742 self.return_focus_after_dismissal(focus_restore, mark, &mut *ops);
3743 }
3744 }
3745 None => {
3746 for id in ctx.overlay_dismissals {
3747 let mark = self.dismissal_mark();
3748 let dismissed = self.overlay_manager.dismiss(id);
3749 self.dormant_dismissed_content(&dismissed, &mut *ops);
3750 self.return_focus_after_dismissal(None, mark, &mut *ops);
3751 }
3752 }
3753 }
3754 for content_id in ctx.overlay_content_dismissals {
3761 if let Some(overlay_id) = self.overlay_manager.find_by_content(content_id) {
3762 let mark = self.dismissal_mark();
3763 let dismissed = self.overlay_manager.dismiss(overlay_id);
3764 self.dormant_dismissed_content(&dismissed, &mut *ops);
3765 self.return_focus_after_dismissal(None, mark, &mut *ops);
3766 }
3767 }
3768 for (id, pause) in ctx.overlay_pause_requests {
3775 if pause {
3776 self.overlay_manager.pause_auto_dismiss(id);
3777 } else {
3778 self.overlay_manager.resume_auto_dismiss(id);
3779 }
3780 }
3781 for preserve_content in ctx.dismiss_descendant_overlays {
3782 self.dismiss_child_overlays_for_source(source_widget, preserve_content, &mut *ops);
3783 }
3784 if !focus_intents.is_empty() {
3793 let modal_closed = fallback_modal
3794 .is_some_and(|content| self.overlay_manager.find_by_content(content).is_none());
3795 let start = match self.focused.filter(|id| self.arena.is_active(*id)) {
3796 Some(focus) => Some((focus, None)),
3797 None if modal_closed => self.intent_anchor().map(|root| (root, None)),
3798 None if fallback_modal.is_none()
3802 && self.overlay_manager.topmost_modal().is_some() =>
3803 {
3804 self.focus_or_modal().map(|modal| (modal, None))
3805 }
3806 None => Some((source_widget, Some(fallback_crossings))),
3807 };
3808 if let Some((start, crossings)) = start {
3809 for intent in focus_intents {
3810 match &crossings {
3811 None => self.enqueue_intent(start, intent, true),
3812 Some(crossings) => {
3813 self.enqueue_intent_crossing(start, intent, true, crossings.clone())
3814 }
3815 }
3816 }
3817 }
3818 }
3819 let deferred_row_activations =
3820 self.apply_tree_mutations(std::mem::take(&mut ctx.tree_mutations));
3821 if ctx.request_a11y_update {
3822 self.a11y_dirty = true;
3823 }
3824 if let Some(visible) = ctx.soft_keyboard_request.take() {
3825 self.request_soft_keyboard(visible);
3826 }
3827 for (message, politeness) in std::mem::take(&mut ctx.announcements) {
3830 self.announce_with(message, politeness);
3831 }
3832 for mut req in ctx.overlay_requests {
3833 if req.parent_overlay.is_none() {
3834 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3835 }
3836 if self
3837 .overlay_manager
3838 .find_by_content(req.content_id)
3839 .is_some()
3840 {
3841 continue;
3842 }
3843 let current_focus = self.focused;
3844 self.overlay_manager.show(req);
3845 self.a11y_dirty = true;
3849 if let Some(focus_id) = current_focus {
3850 self.record_top_focus_restore(focus_id);
3851 }
3852 }
3853 for (mut req, band) in ctx.overlay_band_requests {
3854 if req.parent_overlay.is_none() {
3855 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3856 }
3857 if self
3858 .overlay_manager
3859 .find_by_content(req.content_id)
3860 .is_some()
3861 {
3862 continue;
3863 }
3864 let content_id = req.content_id;
3865 self.overlay_manager.show_in_band(req, band);
3866 self.arena.activate(content_id);
3867 self.a11y_dirty = true;
3868 }
3872 for (content_id, placement) in ctx.overlay_placement_updates {
3875 if let Some(overlay_id) = self.overlay_manager.find_by_content(content_id) {
3876 self.overlay_manager.update_placement(overlay_id, placement);
3877 }
3878 }
3879 for (content_id, source) in ctx.overlay_placement_sources {
3880 if let Some(overlay_id) = self.overlay_manager.find_by_content(content_id) {
3881 self.overlay_manager
3882 .set_placement_source(overlay_id, source);
3883 }
3884 }
3885 for (mut req, duration) in ctx.timed_overlay_requests {
3886 if req.parent_overlay.is_none() {
3887 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3888 }
3889 if self
3890 .overlay_manager
3891 .find_by_content(req.content_id)
3892 .is_some()
3893 {
3894 continue;
3895 }
3896 let current_focus = self.focused;
3897 let overlay_id = self.overlay_manager.show_for(req, duration);
3898 self.overlay_manager
3899 .set_shown_at_sim(overlay_id, self.sim_clock);
3900 self.a11y_dirty = true;
3901 if let Some(focus_id) = current_focus {
3902 self.record_top_focus_restore(focus_id);
3903 }
3904 }
3905 for (mut req, progress, duration) in ctx.reveal_overlay_requests {
3906 if req.parent_overlay.is_none() {
3907 req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3908 }
3909 if self
3910 .overlay_manager
3911 .find_by_content(req.content_id)
3912 .is_some()
3913 {
3914 continue;
3915 }
3916 let content_id = req.content_id;
3917 let current_focus = self.focused;
3918 let overlay_id = self.overlay_manager.show(req);
3919 self.overlay_manager
3920 .set_shown_at_sim(overlay_id, self.sim_clock);
3921 self.a11y_dirty = true;
3922 if let Some(focus_id) = current_focus {
3923 self.record_top_focus_restore(focus_id);
3924 }
3925 self.register_animated_signal(&progress, content_id);
3931 let _ = progress.try_animate_with_options(crate::animation::AnimationRequest {
3932 target: 1.0,
3933 duration,
3934 easing: teksilo_tokens::Easing::EaseOut,
3935 frame_interval: None,
3936 looping: false,
3937 epsilon: 0.0,
3938 max_duration: None,
3939 });
3940 self.overlay_manager
3941 .attach_fade(overlay_id, progress, duration);
3942 }
3943 if let Some((pointer, capture)) = ctx.pointer_capture {
3944 let named = pointer;
3948 let pointer = pointer.unwrap_or_else(|| self.current_pointer_id());
3949 self.set_pointer_capture(pointer, capture.then_some(source_widget));
3950 if capture && ctx.explicit_capture && named.is_none() {
3954 self.note_explicit_capture(source_widget);
3955 }
3956 }
3957 if let Some(activation) = ctx.drag_activation_override.take() {
3958 self.note_drag_activation_override(source_widget, activation);
3962 }
3963 if ctx.recognized_owning_gesture {
3964 self.note_gesture_recognized(source_widget);
3967 }
3968 if !ctx.gesture_acts.is_empty() {
3969 let acts = std::mem::take(&mut ctx.gesture_acts);
3970 self.apply_gesture_acts(&acts, source_widget);
3971 }
3972 if let Some(reason) = ctx.cancel_pointer_request {
3973 let pointer = self.current_pointer_id();
3974 self.cancel_pointer(pointer, reason, &mut *ops);
3975 }
3976 for (mut request, delay, focus_target, replace_siblings) in ctx.delayed_overlay_requests {
3977 if request.parent_overlay.is_none() {
3978 request.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3979 }
3980 if self
3981 .overlay_manager
3982 .find_by_content(request.content_id)
3983 .is_some()
3984 {
3985 continue;
3986 }
3987 let content_id = request.content_id;
3988 self.pending_delayed_overlays
3989 .retain(|pending| pending.request.content_id != content_id);
3990 self.pending_delayed_overlays.push(PendingDelayedOverlay {
3991 request,
3992 delay,
3993 focus_target,
3994 replace_siblings,
3995 real_requested_at: std::time::Instant::now(),
3996 sim_requested_at: self.sim_clock,
3997 });
3998 self.arena.mark_needs_paint(source_widget);
3999 }
4000 for content_id in ctx.cancel_delayed_overlays {
4001 self.pending_delayed_overlays
4002 .retain(|pending| pending.request.content_id != content_id);
4003 }
4004 for content_id in ctx.safe_region_arm_requests {
4011 if let Some(apex) = self
4012 .previous_pointer_position
4013 .or_else(|| self.hover_owner_position())
4014 {
4015 self.overlay_manager.arm_safe_region(
4016 content_id,
4017 apex,
4018 std::time::Instant::now(),
4019 self.sim_clock,
4020 );
4021 }
4022 }
4023 for id in ctx.repaint_requests {
4024 self.arena.mark_needs_paint(id);
4025 }
4026 for id in ctx.synthetic_clicks {
4027 self.synthesise_tap_with_ops(id, &mut *ops);
4032 }
4033 if ctx.clear_focus_requested {
4036 self.clear_focus_ops(&mut *ops);
4037 }
4038 if let Some(&id) = ctx.focus_requests.last() {
4043 self.request_focus_with_ops(id, super::FocusRequestMode::SelfOrDescendant, &mut *ops);
4053 }
4054 if let Some(&id) = ctx.focus_into_requests.last() {
4055 self.request_focus_with_ops(id, super::FocusRequestMode::Into, &mut *ops);
4062 }
4063
4064 for req in ctx.scroll_into_view_requests {
4073 self.scroll_rect_into_view(
4074 req.from.unwrap_or(source_widget),
4077 req.rect,
4078 req.margin,
4079 req.align,
4080 req.motion,
4081 &mut *ops,
4082 );
4083 }
4084 for (id, margin) in ctx.scroll_widget_into_view_requests {
4091 if self.arena.get(id).is_some() {
4092 let bounds = self.arena.bounds(id);
4093 self.scroll_rect_into_view(
4094 id,
4095 bounds,
4096 margin,
4097 crate::event::ScrollAlign::Minimal,
4098 crate::event::ScrollMotion::Instant,
4099 &mut *ops,
4100 );
4101 }
4102 }
4103
4104 if let Some(&id) = ctx.highlight_tooltip_requests.last() {
4109 self.show_highlight_tooltip(id, &mut *ops);
4110 }
4111
4112 if let Some((source_widget, payload, preview_widget)) = ctx.drag_start_request {
4114 let (preview_content_id, preview_overlay_id) = if let Some(preview) = preview_widget {
4115 let content_id = self.add_boxed(preview);
4122 let overlay_id = self
4123 .overlay_manager
4124 .show(crate::overlay::OverlayRequest::new(
4125 content_id,
4126 source_widget,
4127 crate::overlay::OverlayPlacement::AtPointer(teksilo_canvas::Point::ZERO),
4128 crate::overlay::DismissBehavior::Manual,
4129 ));
4130 self.arena.mark_needs_layout(content_id);
4134 (Some(content_id), Some(overlay_id))
4135 } else {
4136 (None, None)
4137 };
4138 self.active_drag = Some(crate::drag_state::DragSession {
4139 payload,
4140 pointer: self.current_input.pointer,
4146 source_widget: Some(source_widget),
4147 is_external: false,
4148 current_position: teksilo_canvas::Point::ZERO,
4149 current_target: None,
4150 feedback: crate::drag_state::DropFeedback::NoFeedback,
4151 preview_content_id,
4152 preview_overlay_id,
4153 });
4154 self.set_current_pointer_capture(Some(source_widget));
4155 self.current_cursor = crate::widget::CursorIcon::Grabbing;
4158 }
4159 if ctx.cancel_drag {
4160 self.cancel_active_drag(&mut *ops);
4161 }
4162
4163 if let Some(theme) = ctx.theme_request {
4165 self.pending_theme_request = Some(theme);
4171 }
4172 if ctx.follow_system_request {
4173 self.pending_follow_system_request = true;
4177 }
4178 if let Some(locale) = ctx.locale_request {
4179 self.pending_locale_request = Some(locale);
4185 }
4186 if let Some(scale) = ctx.text_scale_request {
4187 self.pending_text_scale_request = Some(scale);
4192 }
4193 if !deferred_row_activations.is_empty() {
4200 self.run_with_event_context(&mut *ops, move |ctx| {
4201 for action in deferred_row_activations {
4202 action(ctx);
4203 }
4204 });
4205 }
4206 }
4207
4208 #[must_use]
4214 fn apply_tree_mutations(
4215 &mut self,
4216 mutations: Vec<crate::widget::TreeMutation>,
4217 ) -> Vec<std::rc::Rc<dyn Fn(&mut crate::widget::EventContext)>> {
4218 let mut deferred_row_activations = Vec::new();
4219 use crate::binding::BindingLevel;
4220 use crate::widget::TreeMutation;
4221
4222 for mutation in mutations {
4223 match mutation {
4224 TreeMutation::SetDormant(id) => {
4225 self.park_subtree(id);
4226 }
4227 TreeMutation::Activate(id) => self.arena.activate(id),
4228 TreeMutation::Destroy(id) => {
4229 if let Some(overlay_id) = self.overlay_manager().find_by_content(id) {
4242 self.dismiss_overlay(overlay_id);
4243 }
4244 self.destroy_subtree(id);
4245 }
4246 TreeMutation::MaterializeNow(id) => {
4250 if self.arena.get(id).is_some() {
4251 self.rebuild_single_widget(id);
4252 }
4253 }
4254 TreeMutation::RowSpaceActivate { row, fallback } => {
4255 deferred_row_activations.push(self.keyboard_toggle_in(row).unwrap_or(fallback));
4265 }
4266 TreeMutation::WithWidgetMut { id, dirty, apply } => {
4267 let existed = if let Some(any) =
4272 self.arena.get_mut(id).and_then(|n| n.widget.as_any_mut())
4273 {
4274 apply(any);
4275 true
4276 } else {
4277 false
4278 };
4279 if existed {
4280 match dirty {
4281 BindingLevel::RepaintOnly => self.arena.mark_needs_paint(id),
4282 BindingLevel::SubtreeRepaint => self.arena.mark_subtree_needs_paint(id),
4283 BindingLevel::Relayout => {
4284 self.arena.mark_needs_layout(id);
4285 self.arena.mark_ancestors_need_layout(id);
4286 }
4287 BindingLevel::Rebuild => {
4288 self.arena.mark_needs_rebuild(id);
4289 self.arena.mark_ancestors_need_layout(id);
4290 }
4291 BindingLevel::AccessibilityOnly => self.a11y_dirty = true,
4292 }
4293 }
4294 }
4295 }
4296 }
4297 deferred_row_activations
4298 }
4299
4300 pub fn hit_test(&self, point: Point) -> Option<WidgetId> {
4309 self.hit_test_excluding_overlay_and_widget(point, None, None)
4310 }
4311
4312 pub fn hit_test_for(
4323 &self,
4324 point: Point,
4325 pointer: &crate::pointer::PointerInfo,
4326 ) -> Option<WidgetId> {
4327 self.hit_test_for_excluding(point, pointer, None, None)
4328 }
4329
4330 pub fn hit_test_for_excluding(
4333 &self,
4334 point: Point,
4335 pointer: &crate::pointer::PointerInfo,
4336 exclude_overlay: Option<crate::overlay::OverlayId>,
4337 exclude_widget: Option<WidgetId>,
4338 ) -> Option<WidgetId> {
4339 let surfaces = self.text_surfaces();
4340 let read_only = |id: WidgetId| surfaces.is_read_only(id);
4341 let hit =
4342 crate::pointer::hit_slop::HitContext::new(pointer.kind, &self.effective_theme.input)
4343 .direction(self.layout_direction)
4344 .read_only_probe(&read_only);
4345 self.hit_test_with(point, exclude_overlay, exclude_widget, &hit)
4346 }
4347
4348 pub fn hit_test_excluding_overlay_and_widget(
4355 &self,
4356 point: Point,
4357 exclude_overlay: Option<crate::overlay::OverlayId>,
4358 exclude_widget: Option<WidgetId>,
4359 ) -> Option<WidgetId> {
4360 self.hit_test_with(
4361 point,
4362 exclude_overlay,
4363 exclude_widget,
4364 &crate::pointer::hit_slop::HitContext::mouse(),
4365 )
4366 }
4367
4368 pub(crate) fn hit_test_with(
4371 &self,
4372 point: Point,
4373 exclude_overlay: Option<crate::overlay::OverlayId>,
4374 exclude_widget: Option<WidgetId>,
4375 hit: &crate::pointer::hit_slop::HitContext<'_>,
4376 ) -> Option<WidgetId> {
4377 if let Some(overlay_id) = self.overlay_manager.hit_test(point) {
4378 if Some(overlay_id) == exclude_overlay {
4379 } else if let Some(overlay) = self.overlay_manager.overlay(overlay_id) {
4381 if self.overlay_manager.is_modal_scrim(overlay)
4383 && let Some(chrome) = self.window_chrome_under_modal(point, exclude_widget, hit)
4384 {
4385 return Some(chrome);
4386 }
4387 let content_id = overlay.content_id;
4391 if let Some(found) =
4392 self.arena
4393 .hit_test_in_subtree_with_slop(content_id, point, exclude_widget, hit)
4394 {
4395 return Some(found);
4396 }
4397 if !self
4415 .arena
4416 .get(content_id)
4417 .is_some_and(|node| node.event_pass_through)
4418 {
4419 return None;
4420 }
4421 }
4422 }
4423
4424 if self.overlay_manager.topmost_modal().is_some() {
4425 return self.window_chrome_under_modal(point, exclude_widget, hit);
4426 }
4427
4428 let mut roots = self.arena.roots();
4434 let inert: Vec<WidgetId> = self
4435 .overlay_manager
4436 .stack
4437 .iter()
4438 .filter(|overlay| overlay.inert)
4439 .map(|overlay| overlay.content_id)
4440 .collect();
4441 if !inert.is_empty() {
4442 roots.retain(|root| !inert.contains(root));
4443 }
4444 self.arena
4445 .hit_test_roots_with_slop(roots, point, exclude_widget, hit)
4446 }
4447}
4448
4449impl WidgetTree {
4450 fn window_chrome_under_modal(
4465 &self,
4466 point: Point,
4467 exclude_widget: Option<WidgetId>,
4468 hit: &crate::pointer::hit_slop::HitContext<'_>,
4469 ) -> Option<WidgetId> {
4470 let overlays: Vec<WidgetId> = self
4471 .overlay_manager
4472 .stack
4473 .iter()
4474 .map(|overlay| overlay.content_id)
4475 .collect();
4476 let mut roots = self.arena.roots();
4477 roots.retain(|root| !overlays.contains(root));
4478 let found = self
4479 .arena
4480 .hit_test_roots_with_slop(roots, point, exclude_widget, hit)?;
4481 let mut node = Some(found);
4482 while let Some(id) = node {
4483 let current = self.arena.get(id)?;
4484 if current.window_chrome {
4485 return Some(id);
4486 }
4487 if current.gesture_dead_zone {
4488 return None;
4489 }
4490 node = self.arena.parent(id);
4491 }
4492 None
4493 }
4494}
4495
4496fn is_context_menu_chord(key: Key, modifiers: Modifiers) -> bool {
4514 match key {
4515 Key::ContextMenu => modifiers == Modifiers::NONE,
4516 Key::F10 => modifiers == Modifiers::SHIFT,
4517 #[cfg(target_os = "macos")]
4518 Key::M => modifiers == Modifiers::CTRL | Modifiers::SHIFT,
4519 _ => false,
4520 }
4521}
4522
4523#[cfg(test)]
4524mod tests {
4525 use super::*;
4526 use crate::test_widgets::FillWidget;
4527 use crate::widget::CursorIcon;
4528 use crate::widget_builder::WidgetBuilder;
4529
4530 #[test]
4531 fn pointer_enter_leave_synthesized() {
4532 let mut tree = WidgetTree::new();
4533 let widget = tree.add(FillWidget::new());
4534 tree.layout(SizeProposal::exact(100.0, 50.0));
4535 tree.pointer_move(Point::new(50.0, 25.0));
4536 assert_eq!(tree.hovered(), Some(widget));
4537 tree.pointer_move(Point::new(200.0, 200.0));
4538 assert_eq!(tree.hovered(), None);
4539 }
4540
4541 #[test]
4542 fn pointer_hover_updates_current_cursor() {
4543 let mut tree = WidgetTree::new();
4544 tree.add(FillWidget::new().cursor(CursorIcon::ColResize));
4545 tree.layout(SizeProposal::exact(100.0, 50.0));
4546
4547 tree.pointer_move(Point::new(50.0, 25.0));
4548 assert_eq!(tree.current_cursor(), CursorIcon::ColResize);
4549
4550 tree.pointer_move(Point::new(200.0, 200.0));
4551 assert_eq!(tree.current_cursor(), CursorIcon::Default);
4552 }
4553
4554 #[derive(Debug)]
4557 struct SelfDeclared(CursorIcon);
4558
4559 impl crate::widget::Widget for SelfDeclared {
4560 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
4561 ctx.apply_self_handlers(crate::widget_builder::HandlerSet::new().cursor(self.0));
4562 Vec::new()
4563 }
4564
4565 fn layout_response(
4566 &self,
4567 proposal: SizeProposal,
4568 _ctx: &crate::widget::LayoutContext,
4569 ) -> crate::widget::LayoutResponse {
4570 proposal.resolve(100.0, 50.0).into()
4571 }
4572 }
4573
4574 #[test]
4580 fn declared_cursor_reads_what_a_node_asks_for_and_current_cursor_follows_the_hover() {
4581 let over = Point::new(50.0, 25.0);
4582 let away = Point::new(200.0, 200.0);
4583
4584 let mut tree = WidgetTree::new();
4586 let built = tree.add(FillWidget::new().cursor(CursorIcon::ColResize));
4587 tree.layout(SizeProposal::exact(100.0, 50.0));
4588 assert_eq!(tree.declared_cursor(built), Some(CursorIcon::ColResize));
4589 assert_eq!(
4590 tree.current_cursor(),
4591 CursorIcon::Default,
4592 "not hovered yet"
4593 );
4594 tree.pointer_move(over);
4595 assert_eq!(tree.current_cursor(), CursorIcon::ColResize);
4596 assert_eq!(tree.declared_cursor(built), Some(CursorIcon::ColResize));
4597 tree.pointer_move(away);
4598 assert_eq!(tree.current_cursor(), CursorIcon::Default);
4599 assert_eq!(
4600 tree.declared_cursor(built),
4601 Some(CursorIcon::ColResize),
4602 "the declaration does not depend on the pointer",
4603 );
4604
4605 let mut tree = WidgetTree::new();
4607 let own = tree.add(SelfDeclared(CursorIcon::Crosshair));
4608 tree.layout(SizeProposal::exact(100.0, 50.0));
4609 assert_eq!(tree.declared_cursor(own), Some(CursorIcon::Crosshair));
4610 tree.pointer_move(over);
4611 assert_eq!(tree.current_cursor(), CursorIcon::Crosshair);
4612 tree.pointer_move(away);
4613 assert_eq!(tree.current_cursor(), CursorIcon::Default);
4614
4615 let mut tree = WidgetTree::new();
4617 let plain = tree.add(FillWidget::new());
4618 tree.layout(SizeProposal::exact(100.0, 50.0));
4619 assert_eq!(tree.declared_cursor(plain), None);
4620 let mut tree = WidgetTree::new();
4621 let explicit = tree.add(FillWidget::new().cursor(CursorIcon::Default));
4622 tree.layout(SizeProposal::exact(100.0, 50.0));
4623 assert_eq!(
4624 tree.declared_cursor(explicit),
4625 Some(CursorIcon::Default),
4626 "declaring the default cursor is not declaring nothing",
4627 );
4628 }
4629
4630 #[test]
4640 fn release_cursor_hands_the_cursor_back_to_the_node_that_declared_it() {
4641 let mut tree = WidgetTree::new();
4645 tree.add(
4646 FillWidget::new()
4647 .cursor(CursorIcon::ColResize)
4648 .on_pointer_event(|event, ctx| {
4649 if let WidgetEvent::PointerMove { position, .. } = event {
4650 if position.x < 30.0 {
4651 ctx.set_cursor(CursorIcon::Crosshair);
4652 } else {
4653 ctx.release_cursor();
4654 }
4655 }
4656 EventResponse::Ignored
4657 }),
4658 );
4659 tree.layout(SizeProposal::exact(100.0, 50.0));
4660
4661 tree.pointer_move(Point::new(10.0, 25.0));
4662 assert_eq!(
4663 tree.current_cursor(),
4664 CursorIcon::Crosshair,
4665 "the handler outranks the node it is attached to",
4666 );
4667 tree.pointer_move(Point::new(60.0, 25.0));
4668 assert_eq!(
4669 tree.current_cursor(),
4670 CursorIcon::ColResize,
4671 "and hands it back to the node, not to Default — no enter/leave \
4672 fires on a move within one node, so nothing else could",
4673 );
4674 tree.pointer_move(Point::new(10.0, 25.0));
4675 assert_eq!(
4676 tree.current_cursor(),
4677 CursorIcon::Crosshair,
4678 "and can take it again"
4679 );
4680 tree.pointer_move(Point::new(200.0, 200.0));
4681 assert_eq!(
4682 tree.current_cursor(),
4683 CursorIcon::Default,
4684 "leaving the node clears both the override and the declaration",
4685 );
4686 }
4687
4688 #[test]
4692 fn release_cursor_is_a_no_op_with_nothing_to_undo() {
4693 let mut tree = WidgetTree::new();
4694 tree.add(
4695 FillWidget::new()
4696 .cursor(CursorIcon::ColResize)
4697 .on_pointer_event(|event, ctx| {
4698 if matches!(event, WidgetEvent::PointerMove { .. }) {
4699 ctx.release_cursor();
4700 }
4701 EventResponse::Ignored
4702 }),
4703 );
4704 tree.layout(SizeProposal::exact(100.0, 50.0));
4705 tree.pointer_move(Point::new(50.0, 25.0));
4706 assert_eq!(tree.current_cursor(), CursorIcon::ColResize);
4707
4708 let mut tree = WidgetTree::new();
4710 tree.add(FillWidget::new().on_pointer_event(|event, ctx| {
4711 if let WidgetEvent::PointerMove { position, .. } = event {
4712 if position.x < 30.0 {
4713 ctx.set_cursor(CursorIcon::Crosshair);
4714 } else {
4715 ctx.release_cursor();
4716 }
4717 }
4718 EventResponse::Ignored
4719 }));
4720 tree.layout(SizeProposal::exact(100.0, 50.0));
4721 tree.pointer_move(Point::new(10.0, 25.0));
4722 assert_eq!(tree.current_cursor(), CursorIcon::Crosshair);
4723 tree.pointer_move(Point::new(60.0, 25.0));
4724 assert_eq!(
4725 tree.current_cursor(),
4726 CursorIcon::Default,
4727 "nothing declared a cursor for this chain, so the hand-back \
4728 resolves to Default",
4729 );
4730 }
4731
4732 #[test]
4740 fn an_on_hover_override_does_not_erase_the_declaration_it_outranks() {
4741 let mut tree = WidgetTree::new();
4742 tree.add(
4743 FillWidget::new()
4744 .cursor(CursorIcon::ColResize)
4745 .on_hover(|entered, ctx| {
4746 if entered {
4747 ctx.set_cursor(CursorIcon::Crosshair);
4748 }
4749 })
4750 .on_pointer_event(|event, ctx| {
4751 if let WidgetEvent::PointerMove { position, .. } = event
4752 && position.x >= 30.0
4753 {
4754 ctx.release_cursor();
4755 }
4756 EventResponse::Ignored
4757 }),
4758 );
4759 tree.layout(SizeProposal::exact(100.0, 50.0));
4760
4761 tree.pointer_move(Point::new(10.0, 25.0));
4762 assert_eq!(
4763 tree.current_cursor(),
4764 CursorIcon::Crosshair,
4765 "on_hover runs after the node cursor is applied, so it wins",
4766 );
4767 tree.pointer_move(Point::new(60.0, 25.0));
4768 assert_eq!(
4769 tree.current_cursor(),
4770 CursorIcon::ColResize,
4771 "and the node's declaration survived being overridden",
4772 );
4773 }
4774
4775 #[derive(Debug)]
4778 struct Bumpable {
4779 value: i32,
4780 }
4781
4782 impl crate::widget::Widget for Bumpable {
4783 fn layout_response(
4784 &self,
4785 proposal: SizeProposal,
4786 _ctx: &crate::widget::LayoutContext,
4787 ) -> crate::widget::LayoutResponse {
4788 proposal.resolve(10.0, 10.0).into()
4789 }
4790 fn as_any(&self) -> Option<&dyn std::any::Any> {
4791 Some(self)
4792 }
4793 fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
4794 Some(self)
4795 }
4796 }
4797
4798 #[test]
4799 fn with_widget_mut_applies_and_dirty_marks() {
4800 let mut tree = WidgetTree::new();
4801 let id = tree.add(Bumpable { value: 0 });
4802 tree.layout(SizeProposal::exact(100.0, 100.0));
4803
4804 let mut ctx = EventContext::new();
4805 ctx.with_widget_mut::<Bumpable>(id, crate::binding::BindingLevel::Relayout, |b| {
4806 b.value = 42;
4807 });
4808 tree.collect_from_ctx(ctx, id);
4809
4810 let value = tree
4811 .widget_as_any(id)
4812 .and_then(|a| a.downcast_ref::<Bumpable>())
4813 .map(|b| b.value);
4814 assert_eq!(
4815 value,
4816 Some(42),
4817 "the deferred closure must mutate the live widget"
4818 );
4819 assert!(
4820 tree.needs_layout(),
4821 "Relayout dirty level must mark the tree for relayout"
4822 );
4823 }
4824
4825 #[test]
4826 #[cfg(debug_assertions)]
4827 #[should_panic(expected = "not the requested type")]
4828 fn with_widget_mut_wrong_type_panics_in_debug() {
4829 struct Other;
4830 let mut tree = WidgetTree::new();
4831 let id = tree.add(Bumpable { value: 0 });
4832 let mut ctx = EventContext::new();
4833 ctx.with_widget_mut::<Other>(
4834 id,
4835 crate::binding::BindingLevel::RepaintOnly,
4836 |_o: &mut Other| {},
4837 );
4838 tree.collect_from_ctx(ctx, id);
4841 }
4842
4843 #[test]
4844 fn with_widget_mut_closure_may_fire_observed_signals() {
4845 use crate::signal::Signal;
4853 let mut tree = WidgetTree::new();
4854 let id = tree.add(Bumpable { value: 0 });
4855 tree.layout(SizeProposal::exact(100.0, 100.0));
4856
4857 let trigger = Signal::new(0_u64);
4858 let echo = Signal::new(0_u64);
4859 let echo_for_obs = echo.clone();
4860 let _obs = trigger.observe(move |v| echo_for_obs.set(*v));
4861
4862 let trigger_in = trigger.clone();
4863 let mut ctx = EventContext::new();
4864 ctx.with_widget_mut::<Bumpable>(id, crate::binding::BindingLevel::RepaintOnly, move |b| {
4865 b.value = 7;
4866 trigger_in.set(99);
4868 });
4869 tree.collect_from_ctx(ctx, id); assert_eq!(
4872 echo.get(),
4873 99,
4874 "the observer ran during the deferred mutation"
4875 );
4876 let value = tree
4877 .widget_as_any(id)
4878 .and_then(|a| a.downcast_ref::<Bumpable>())
4879 .map(|b| b.value);
4880 assert_eq!(value, Some(7));
4881 }
4882
4883 #[test]
4884 fn request_accessibility_update_forces_rewalk() {
4885 let mut tree = WidgetTree::new();
4886 let id = tree.add(Bumpable { value: 0 });
4887 tree.layout(SizeProposal::exact(100.0, 100.0));
4888 let _ = tree.sync_accessibility(); assert!(
4890 !tree.a11y_dirty,
4891 "sync_accessibility should clear the dirty flag"
4892 );
4893
4894 let mut ctx = EventContext::new();
4895 ctx.request_accessibility_update();
4896 tree.collect_from_ctx(ctx, id);
4897 assert!(
4898 tree.a11y_dirty,
4899 "request_accessibility_update must force an AT re-walk"
4900 );
4901 }
4902
4903 #[test]
4904 fn rebuild_dirties_accessibility_tree() {
4905 let mut tree = WidgetTree::new();
4912 let id = tree.add(Bumpable { value: 0 });
4913 tree.layout(SizeProposal::exact(100.0, 100.0));
4914 let _ = tree.sync_accessibility(); assert!(
4916 !tree.a11y_dirty,
4917 "sync_accessibility should clear the dirty flag"
4918 );
4919
4920 tree.arena_mark_needs_rebuild_for_testing(id);
4923 tree.layout(SizeProposal::exact(100.0, 100.0));
4924 assert!(
4925 tree.a11y_dirty,
4926 "a rebuild must dirty the AT tree so the next sync re-walks"
4927 );
4928 }
4929
4930 #[test]
4931 fn bound_access_label_change_dirties_accessibility_tree() {
4932 use crate::signal::Signal;
4933 use crate::test_widgets::FillWidget;
4934 use crate::widget_builder::WidgetBuilder;
4935
4936 let label = Signal::new("first".to_string());
4942 let mut tree = WidgetTree::new();
4943 let _id = tree.add(FillWidget::new().access_label(label.clone()));
4944 tree.layout(SizeProposal::exact(100.0, 100.0));
4945 let _ = tree.sync_accessibility(); assert!(!tree.a11y_dirty, "sync_accessibility should clear the flag");
4947
4948 label.set("second".to_string());
4949 tree.layout(SizeProposal::exact(100.0, 100.0));
4950 assert!(
4951 tree.a11y_dirty,
4952 "changing a bound access_label must dirty the AT tree"
4953 );
4954 }
4955
4956 #[test]
4957 fn disabled_ancestor_blocks_event_to_descendant() {
4958 use crate::signal::Signal;
4959 use crate::test_widgets::StackWidget;
4960 use std::cell::Cell;
4961 use std::rc::Rc;
4962
4963 let tapped = Rc::new(Cell::new(false));
4964 let flag = tapped.clone();
4965 let enabled = Signal::new(true);
4966
4967 let mut tree = WidgetTree::new();
4968 let child = tree.add(FillWidget::new().on_tap(move |_pos, _ctx| {
4969 flag.set(true);
4970 }));
4971 let parent = tree.add(StackWidget::new().child(child));
4972 tree.enabled_when(parent, enabled.clone());
4973 tree.layout(SizeProposal::exact(100.0, 50.0));
4974
4975 enabled.set(false);
4976 tree.click(child);
4977 assert!(
4978 !tapped.get(),
4979 "disabled ancestor should block descendant tap"
4980 );
4981
4982 enabled.set(true);
4983 tree.click(child);
4984 assert!(tapped.get(), "re-enabling should restore dispatch");
4985 }
4986
4987 #[test]
4988 fn pointer_positions_are_widget_local_at_nonzero_origin() {
4989 use crate::event::{Modifiers, PointerButton};
4990 use crate::test_widgets::InsetWidget;
4991 use std::cell::Cell;
4992 use std::rc::Rc;
4993
4994 let tap_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
4996 let down_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
4997 let drag_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
4998 let (tp, dp, gp) = (tap_pos.clone(), down_pos.clone(), drag_pos.clone());
4999
5000 let mut tree = WidgetTree::new();
5001 let child = tree.add(
5002 FillWidget::new()
5003 .on_tap(move |ev, _ctx| tp.set(Some(ev.position)))
5004 .on_pointer_event(move |ev, _ctx| {
5005 if let WidgetEvent::PointerDown { position, .. } = ev {
5006 dp.set(Some(*position));
5007 }
5008 crate::event::EventResponse::Ignored
5009 })
5010 .on_drag(move |phase, _ctx| {
5011 use crate::gesture::DragPhase;
5012 match phase {
5013 DragPhase::Started { position, .. }
5014 | DragPhase::Moved { position, .. }
5015 | DragPhase::Ended { position, .. } => gp.set(Some(position)),
5016 _ => {}
5017 }
5018 }),
5019 );
5020 let inset = tree.add(InsetWidget::new(20.0).set_child(child));
5021 let _ = inset;
5022 tree.layout(SizeProposal::exact(200.0, 200.0));
5023 assert_eq!(tree.bounds(child).origin(), Point::new(20.0, 20.0));
5024
5025 tree.dispatch_event(WidgetEvent::pointer_down(
5027 Point::new(50.0, 40.0),
5028 PointerButton::Primary,
5029 Modifiers::NONE,
5030 ));
5031 assert_eq!(
5032 down_pos.get(),
5033 Some(Point::new(30.0, 20.0)),
5034 "on_pointer_event PointerDown must be widget-local"
5035 );
5036 tree.dispatch_event(WidgetEvent::pointer_up(
5037 Point::new(50.0, 40.0),
5038 PointerButton::Primary,
5039 Modifiers::NONE,
5040 ));
5041 assert_eq!(
5042 tap_pos.get(),
5043 Some(Point::new(30.0, 20.0)),
5044 "on_tap position must be widget-local"
5045 );
5046
5047 tree.dispatch_event(WidgetEvent::pointer_down(
5050 Point::new(50.0, 40.0),
5051 PointerButton::Primary,
5052 Modifiers::NONE,
5053 ));
5054 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(65.0, 55.0)));
5057 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(90.0, 70.0)));
5058 assert_eq!(
5059 drag_pos.get(),
5060 Some(Point::new(70.0, 50.0)),
5061 "on_drag position must be widget-local"
5062 );
5063 }
5064
5065 #[test]
5066 fn dormant_widget_not_hit_tested() {
5067 let mut tree = WidgetTree::new();
5068 let widget = tree.add(FillWidget::new());
5069 tree.layout(SizeProposal::exact(100.0, 50.0));
5070
5071 tree.pointer_move(Point::new(50.0, 25.0));
5072 assert_eq!(tree.hovered(), Some(widget));
5073
5074 tree.set_dormant(widget);
5075 tree.pointer_move(Point::new(200.0, 200.0));
5076 tree.pointer_move(Point::new(50.0, 25.0));
5077 assert_eq!(tree.hovered(), None);
5078 }
5079
5080 #[test]
5081 fn ancestor_pointer_handler_does_not_suppress_descendant_hover() {
5082 use crate::event::EventResponse;
5083 use crate::test_widgets::StackWidget;
5084 use std::cell::Cell;
5085 use std::rc::Rc;
5086
5087 let hovered = Rc::new(Cell::new(false));
5089 let h = hovered.clone();
5090
5091 let mut tree = WidgetTree::new();
5092 let child = tree.add(FillWidget::new().on_hover(move |entered, _ctx| h.set(entered)));
5093 tree.add(
5098 StackWidget::new()
5099 .child(child)
5100 .on_pointer_event(|_event, _ctx| EventResponse::Handled),
5101 );
5102 tree.layout(SizeProposal::exact(100.0, 50.0));
5103
5104 tree.pointer_move(Point::new(50.0, 25.0));
5105 assert!(
5106 hovered.get(),
5107 "a greedy ancestor on_pointer_event must NOT swallow the child's PointerEnter"
5108 );
5109
5110 tree.pointer_move(Point::new(500.0, 500.0));
5111 assert!(
5112 !hovered.get(),
5113 "PointerLeave must likewise reach the child despite the ancestor"
5114 );
5115 }
5116
5117 #[test]
5125 fn a_child_hover_handler_does_not_swallow_its_ancestors() {
5126 use crate::test_widgets::StackWidget;
5127 use std::cell::Cell;
5128 use std::rc::Rc;
5129
5130 let (row, button) = (Rc::new(Cell::new(false)), Rc::new(Cell::new(false)));
5131 let (r, b) = (row.clone(), button.clone());
5132
5133 let mut tree = WidgetTree::new();
5134 let child = tree.add(FillWidget::new().on_hover(move |entered, _ctx| b.set(entered)));
5135 tree.add(
5136 StackWidget::new()
5137 .child(child)
5138 .on_hover(move |entered, _ctx| r.set(entered)),
5139 );
5140 tree.layout(SizeProposal::exact(100.0, 50.0));
5141
5142 tree.pointer_move(Point::new(50.0, 25.0));
5143 assert!(button.get(), "the child is hovered");
5144 assert!(row.get(), "and so is the row it is inside");
5145
5146 tree.pointer_move(Point::new(500.0, 500.0));
5147 assert!(!button.get(), "the child heard the leave");
5148 assert!(
5149 !row.get(),
5150 "and so did the row — a container is not still hovered because the \
5151 pointer left it through a button"
5152 );
5153 }
5154
5155 #[test]
5162 fn key_preview_consumes_before_focused_on_key() {
5163 use crate::event::EventResponse;
5166 use crate::test_widgets::StackWidget;
5167 use std::cell::Cell;
5168 use std::rc::Rc;
5169
5170 let leaf_fired = Rc::new(Cell::new(false));
5171 let leaf_flag = leaf_fired.clone();
5172 let preview_fired = Rc::new(Cell::new(false));
5173 let preview_flag = preview_fired.clone();
5174
5175 let mut tree = WidgetTree::new();
5176 let leaf = tree.add(FillWidget::new().focusable().on_key(move |event, _c| {
5177 if matches!(event, WidgetEvent::KeyDown { .. }) {
5180 leaf_flag.set(true);
5181 }
5182 EventResponse::Handled
5183 }));
5184 let mid = tree.add(StackWidget::new().child(leaf));
5185 let _root = tree.add(
5186 StackWidget::new()
5187 .child(mid)
5188 .on_key_preview(move |event, _c| match event {
5189 WidgetEvent::KeyDown {
5190 key: Key::Enter, ..
5191 } => {
5192 preview_flag.set(true);
5193 EventResponse::Handled
5194 }
5195 _ => EventResponse::Ignored,
5196 }),
5197 );
5198
5199 tree.layout(SizeProposal::exact(100.0, 50.0));
5200 tree.focus(leaf);
5201 tree.press_key(Key::Enter, Modifiers::NONE);
5202
5203 assert!(
5204 preview_fired.get(),
5205 "ancestor on_key_preview must fire for KeyDown on a focused descendant"
5206 );
5207 assert!(
5208 !leaf_fired.get(),
5209 "consuming the event in preview must prevent the focused widget's on_key from running"
5210 );
5211 }
5212
5213 #[test]
5214 fn key_preview_falls_through_when_returning_ignored() {
5215 use crate::event::EventResponse;
5218 use crate::test_widgets::StackWidget;
5219 use std::cell::Cell;
5220 use std::rc::Rc;
5221
5222 let leaf_fired = Rc::new(Cell::new(false));
5223 let leaf_flag = leaf_fired.clone();
5224 let preview_fired = Rc::new(Cell::new(false));
5225 let preview_flag = preview_fired.clone();
5226
5227 let mut tree = WidgetTree::new();
5228 let leaf = tree.add(FillWidget::new().focusable().on_key(move |_e, _c| {
5229 leaf_flag.set(true);
5230 EventResponse::Handled
5231 }));
5232 let mid = tree.add(StackWidget::new().child(leaf));
5233 let _root = tree.add(
5234 StackWidget::new()
5235 .child(mid)
5236 .on_key_preview(move |_event, _c| {
5237 preview_flag.set(true);
5238 EventResponse::Ignored
5239 }),
5240 );
5241
5242 tree.layout(SizeProposal::exact(100.0, 50.0));
5243 tree.focus(leaf);
5244 tree.press_key(Key::Enter, Modifiers::NONE);
5245
5246 assert!(preview_fired.get(), "preview must always be invoked");
5247 assert!(
5248 leaf_fired.get(),
5249 "preview returning Ignored must not block the focused widget's on_key"
5250 );
5251 }
5252
5253 #[test]
5254 fn key_preview_excludes_focused_target_itself() {
5255 use crate::event::EventResponse;
5259 use std::cell::Cell;
5260 use std::rc::Rc;
5261
5262 let preview_on_target = Rc::new(Cell::new(false));
5263 let pf = preview_on_target.clone();
5264
5265 let mut tree = WidgetTree::new();
5266 let leaf = tree.add(FillWidget::new().focusable().on_key_preview(move |_e, _c| {
5267 pf.set(true);
5268 EventResponse::Handled
5269 }));
5270 tree.layout(SizeProposal::exact(100.0, 50.0));
5271 tree.focus(leaf);
5272 tree.press_key(Key::Enter, Modifiers::NONE);
5273
5274 assert!(
5275 !preview_on_target.get(),
5276 "the focused widget itself must not see its own on_key_preview"
5277 );
5278 }
5279
5280 #[test]
5281 fn key_preview_root_to_target_order() {
5282 use crate::event::EventResponse;
5286 use crate::test_widgets::StackWidget;
5287 use std::cell::RefCell;
5288 use std::rc::Rc;
5289
5290 let order: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
5291 let outer_log = order.clone();
5292 let inner_log = order.clone();
5293
5294 let mut tree = WidgetTree::new();
5295 let leaf = tree.add(FillWidget::new().focusable());
5296 let inner = tree.add(
5297 StackWidget::new()
5298 .child(leaf)
5299 .on_key_preview(move |event, _c| {
5300 if matches!(event, WidgetEvent::KeyDown { .. }) {
5301 inner_log.borrow_mut().push("inner");
5302 }
5303 EventResponse::Ignored
5304 }),
5305 );
5306 let _outer = tree.add(
5307 StackWidget::new()
5308 .child(inner)
5309 .on_key_preview(move |event, _c| {
5310 if matches!(event, WidgetEvent::KeyDown { .. }) {
5311 outer_log.borrow_mut().push("outer");
5312 }
5313 EventResponse::Ignored
5314 }),
5315 );
5316
5317 tree.layout(SizeProposal::exact(100.0, 50.0));
5318 tree.focus(leaf);
5319 tree.dispatch_event(WidgetEvent::KeyDown {
5320 key: Key::Enter,
5321 modifiers: Modifiers::NONE,
5322 text: None,
5323 });
5324
5325 assert_eq!(
5326 *order.borrow(),
5327 vec!["outer", "inner"],
5328 "preview must walk root → parent-of-target"
5329 );
5330 }
5331
5332 #[test]
5333 fn access_action_routes_to_cursored_target_not_focus() {
5334 use crate::signal::Signal;
5338 let a_fired = Signal::new(false);
5339 let b_fired = Signal::new(false);
5340 let a_cb = a_fired.clone();
5341 let b_cb = b_fired.clone();
5342
5343 let mut tree = WidgetTree::new();
5344 let a = tree.add(
5345 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| a_cb.set(true)),
5346 );
5347 let b = tree.add(
5348 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| b_cb.set(true)),
5349 );
5350 tree.layout(SizeProposal::exact(200.0, 100.0));
5351
5352 tree.focus(a);
5353 tree.dispatch_event(WidgetEvent::AccessAction {
5354 action: accesskit::Action::Click,
5355 target: Some(b),
5356 target_node: crate::accessibility::widget_id_to_node_id(b),
5357 data: None,
5358 });
5359
5360 assert!(b_fired.get(), "the cursored target must receive the action");
5361 assert!(
5362 !a_fired.get(),
5363 "the keyboard-focused widget must NOT receive an action targeting another node"
5364 );
5365 }
5366
5367 #[test]
5368 fn access_action_without_target_is_dropped_not_redirected_to_focus() {
5369 use crate::signal::Signal;
5372 let fired = Signal::new(false);
5373 let cb = fired.clone();
5374
5375 let mut tree = WidgetTree::new();
5376 let widget = tree.add(
5377 FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| cb.set(true)),
5378 );
5379 tree.layout(SizeProposal::exact(200.0, 100.0));
5380
5381 tree.focus(widget);
5382 tree.dispatch_event(WidgetEvent::AccessAction {
5383 action: accesskit::Action::Click,
5384 target: None,
5385 target_node: crate::accessibility::root_node_id(),
5386 data: None,
5387 });
5388
5389 assert!(
5390 !fired.get(),
5391 "a target-less action must not be redirected to the focused widget"
5392 );
5393 }
5394
5395 #[test]
5402 fn shortcut_fires_matching_action_on_source_widget() {
5403 use crate::action::Action;
5404 use crate::shortcut::{KeyStroke, Shortcut};
5405 use std::cell::Cell;
5406 use std::rc::Rc;
5407
5408 let fired = Rc::new(Cell::new(false));
5409 let fired_flag = fired.clone();
5410
5411 let mut tree = WidgetTree::new();
5412 let widget = tree.add(FillWidget::new().focusable());
5413 tree.push_action(
5414 widget,
5415 Action::new("app.save").on_invoke(move |_intent, _ctx| {
5416 fired_flag.set(true);
5417 }),
5418 );
5419 tree.shortcut_registry_mut().register(
5420 Shortcut::new("app.save")
5421 .primary(KeyStroke::command(Key::S))
5422 .build(),
5423 );
5424
5425 tree.layout(SizeProposal::exact(100.0, 50.0));
5426 tree.focus(widget);
5427
5428 tree.press_key(Key::S, Modifiers::COMMAND);
5429 assert!(fired.get(), "matching action must fire on KeyDown");
5430 }
5431
5432 #[test]
5433 fn global_shortcut_fires_without_focused_widget() {
5434 use crate::action::Action;
5435 use crate::shortcut::{KeyStroke, Shortcut};
5436 use std::cell::Cell;
5437 use std::rc::Rc;
5438
5439 let fired = Rc::new(Cell::new(false));
5443 let fired_flag = fired.clone();
5444
5445 let mut tree = WidgetTree::new();
5446 let root = tree.add(FillWidget::new());
5447 tree.push_action(
5448 root,
5449 Action::new("app.save").on_invoke(move |_intent, _ctx| {
5450 fired_flag.set(true);
5451 }),
5452 );
5453 tree.shortcut_registry_mut().register(
5454 Shortcut::new("app.save")
5455 .primary(KeyStroke::command(Key::S))
5456 .build(),
5457 );
5458
5459 tree.layout(SizeProposal::exact(100.0, 50.0));
5460 tree.press_key(Key::S, Modifiers::COMMAND);
5463 assert!(
5464 fired.get(),
5465 "global shortcut must fire without a focused widget"
5466 );
5467 }
5468
5469 #[test]
5470 fn global_shortcut_fires_after_focused_widget_destroyed() {
5471 use crate::action::Action;
5472 use crate::shortcut::{KeyStroke, Shortcut};
5473 use std::cell::Cell;
5474 use std::rc::Rc;
5475
5476 let fired = Rc::new(Cell::new(false));
5481 let fired_flag = fired.clone();
5482
5483 let mut tree = WidgetTree::new();
5484 let root = tree.add(FillWidget::new());
5485 let focusable = tree.add_child(root, FillWidget::new().focusable());
5486 tree.push_action(
5487 root,
5488 Action::new("app.save").on_invoke(move |_intent, _ctx| {
5489 fired_flag.set(true);
5490 }),
5491 );
5492 tree.shortcut_registry_mut().register(
5493 Shortcut::new("app.save")
5494 .primary(KeyStroke::command(Key::S))
5495 .build(),
5496 );
5497
5498 tree.layout(SizeProposal::exact(100.0, 50.0));
5499 tree.focus(focusable);
5500 assert_eq!(tree.focused(), Some(focusable));
5501
5502 tree.destroy_subtree(focusable);
5505 assert_eq!(tree.focused(), None, "focus must clear when destroyed");
5506
5507 tree.press_key(Key::S, Modifiers::COMMAND);
5508 assert!(
5509 fired.get(),
5510 "global shortcut must still fire after the focused widget is destroyed"
5511 );
5512 }
5513
5514 #[test]
5515 fn scoped_shortcut_matches_only_when_focus_in_scope() {
5516 use crate::action::Action;
5517 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
5518 use std::cell::Cell;
5519 use std::rc::Rc;
5520
5521 let fired = Rc::new(Cell::new(0));
5522 let fired_flag = fired.clone();
5523
5524 let mut tree = WidgetTree::new();
5525 let scope_root = tree.add(FillWidget::new().focusable());
5526 let inside = tree.add_child(scope_root, FillWidget::new().focusable());
5527 let outside = tree.add(FillWidget::new().focusable());
5528
5529 tree.push_action(
5530 scope_root,
5531 Action::new("editor.find").on_invoke(move |_i, _c| {
5532 fired_flag.set(fired_flag.get() + 1);
5533 }),
5534 );
5535 tree.shortcut_registry_mut().register(
5536 Shortcut::new("editor.find")
5537 .primary(KeyStroke::command(Key::F))
5538 .scope(ShortcutScope::Scoped(scope_root))
5539 .build(),
5540 );
5541
5542 tree.layout(SizeProposal::exact(200.0, 100.0));
5543
5544 tree.focus(outside);
5546 tree.press_key(Key::F, Modifiers::COMMAND);
5547 assert_eq!(
5548 fired.get(),
5549 0,
5550 "scoped shortcut must not fire outside scope"
5551 );
5552
5553 tree.focus(inside);
5555 tree.press_key(Key::F, Modifiers::COMMAND);
5556 assert_eq!(
5557 fired.get(),
5558 1,
5559 "scoped shortcut must fire when focus in scope"
5560 );
5561 }
5562
5563 #[test]
5564 fn same_chord_scoped_first_falls_back_to_global_when_focus_outside() {
5565 use crate::action::Action;
5571 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
5572 use std::cell::Cell;
5573 use std::rc::Rc;
5574
5575 let scoped_fired = Rc::new(Cell::new(0));
5576 let global_fired = Rc::new(Cell::new(0));
5577 let sf = scoped_fired.clone();
5578 let gf = global_fired.clone();
5579
5580 let mut tree = WidgetTree::new();
5581 let root = tree.add(FillWidget::new());
5582 let editor = tree.add_child(root, FillWidget::new().focusable());
5583 let _editor_inner = tree.add_child(editor, FillWidget::new().focusable());
5584 let sidebar = tree.add_child(root, FillWidget::new().focusable());
5585
5586 tree.push_action(
5587 editor,
5588 Action::new("editor.saveBlock").on_invoke(move |_i, _c| sf.set(sf.get() + 1)),
5589 );
5590 tree.push_action(
5591 root,
5592 Action::new("zzz.global.save").on_invoke(move |_i, _c| gf.set(gf.get() + 1)),
5593 );
5594 tree.shortcut_registry_mut().register(
5595 Shortcut::new("editor.saveBlock")
5596 .primary(KeyStroke::command(Key::S))
5597 .scope(ShortcutScope::Scoped(editor))
5598 .build(),
5599 );
5600 tree.shortcut_registry_mut().register(
5601 Shortcut::new("zzz.global.save")
5602 .primary(KeyStroke::command(Key::S))
5603 .build(),
5604 );
5605
5606 tree.layout(SizeProposal::exact(200.0, 100.0));
5607 tree.focus(sidebar);
5608 tree.press_key(Key::S, Modifiers::COMMAND);
5609
5610 assert_eq!(global_fired.get(), 1, "applicable global must fire");
5611 assert_eq!(
5612 scoped_fired.get(),
5613 0,
5614 "inapplicable scoped binding must not eat the chord"
5615 );
5616 }
5617
5618 #[test]
5619 fn same_chord_global_first_yields_to_scoped_when_focus_inside() {
5620 use crate::action::Action;
5625 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
5626 use std::cell::Cell;
5627 use std::rc::Rc;
5628
5629 let scoped_fired = Rc::new(Cell::new(0));
5630 let global_fired = Rc::new(Cell::new(0));
5631 let sf = scoped_fired.clone();
5632 let gf = global_fired.clone();
5633
5634 let mut tree = WidgetTree::new();
5635 let root = tree.add(FillWidget::new());
5636 let editor = tree.add_child(root, FillWidget::new().focusable());
5637 let editor_inner = tree.add_child(editor, FillWidget::new().focusable());
5638 let sidebar = tree.add_child(root, FillWidget::new().focusable());
5639
5640 tree.push_action(
5641 editor,
5642 Action::new("editor.saveBlock").on_invoke(move |_i, _c| sf.set(sf.get() + 1)),
5643 );
5644 tree.push_action(
5645 root,
5646 Action::new("app.save").on_invoke(move |_i, _c| gf.set(gf.get() + 1)),
5647 );
5648 tree.shortcut_registry_mut().register(
5649 Shortcut::new("app.save")
5650 .primary(KeyStroke::command(Key::S))
5651 .build(),
5652 );
5653 tree.shortcut_registry_mut().register(
5654 Shortcut::new("editor.saveBlock")
5655 .primary(KeyStroke::command(Key::S))
5656 .scope(ShortcutScope::Scoped(editor))
5657 .build(),
5658 );
5659
5660 tree.layout(SizeProposal::exact(200.0, 100.0));
5661
5662 tree.focus(editor_inner);
5664 tree.press_key(Key::S, Modifiers::COMMAND);
5665 assert_eq!(
5666 scoped_fired.get(),
5667 1,
5668 "in-focus scoped must win over global"
5669 );
5670 assert_eq!(
5671 global_fired.get(),
5672 0,
5673 "global must yield to the scoped binding"
5674 );
5675
5676 tree.focus(sidebar);
5678 tree.press_key(Key::S, Modifiers::COMMAND);
5679 assert_eq!(scoped_fired.get(), 1, "scoped stays put outside its scope");
5680 assert_eq!(
5681 global_fired.get(),
5682 1,
5683 "global fires when focus leaves the scope"
5684 );
5685 }
5686
5687 #[test]
5688 fn propagated_action_lets_ancestor_handle() {
5689 use crate::action::Action;
5690 use crate::intent::IntentResponse;
5691 use crate::shortcut::{KeyStroke, Shortcut};
5692 use std::cell::Cell;
5693 use std::rc::Rc;
5694
5695 let inner_seen = Rc::new(Cell::new(false));
5696 let outer_seen = Rc::new(Cell::new(false));
5697 let inner_flag = inner_seen.clone();
5698 let outer_flag = outer_seen.clone();
5699
5700 let mut tree = WidgetTree::new();
5701 let outer = tree.add(FillWidget::new().focusable());
5702 let inner = tree.add_child(outer, FillWidget::new().focusable());
5703
5704 tree.push_action(
5706 inner,
5707 Action::new("app.save").on_invoke_with_response(move |_i, _c| {
5708 inner_flag.set(true);
5709 IntentResponse::Propagated
5710 }),
5711 );
5712 tree.push_action(
5713 outer,
5714 Action::new("app.save").on_invoke(move |_i, _c| {
5715 outer_flag.set(true);
5716 }),
5717 );
5718 tree.shortcut_registry_mut().register(
5719 Shortcut::new("app.save")
5720 .primary(KeyStroke::command(Key::S))
5721 .build(),
5722 );
5723
5724 tree.layout(SizeProposal::exact(100.0, 50.0));
5725 tree.focus(inner);
5726
5727 tree.press_key(Key::S, Modifiers::COMMAND);
5728 assert!(inner_seen.get(), "inner action observed the intent");
5729 assert!(outer_seen.get(), "outer action reached after Propagated");
5730 }
5731
5732 #[test]
5733 fn handled_action_stops_propagation() {
5734 use crate::action::Action;
5735 use crate::shortcut::{KeyStroke, Shortcut};
5736 use std::cell::Cell;
5737 use std::rc::Rc;
5738
5739 let inner_seen = Rc::new(Cell::new(false));
5740 let outer_seen = Rc::new(Cell::new(false));
5741 let inner_flag = inner_seen.clone();
5742 let outer_flag = outer_seen.clone();
5743
5744 let mut tree = WidgetTree::new();
5745 let outer = tree.add(FillWidget::new().focusable());
5746 let inner = tree.add_child(outer, FillWidget::new().focusable());
5747
5748 tree.push_action(
5749 inner,
5750 Action::new("app.save").on_invoke(move |_i, _c| {
5751 inner_flag.set(true);
5752 }),
5753 );
5754 tree.push_action(
5755 outer,
5756 Action::new("app.save").on_invoke(move |_i, _c| {
5757 outer_flag.set(true);
5758 }),
5759 );
5760 tree.shortcut_registry_mut().register(
5761 Shortcut::new("app.save")
5762 .primary(KeyStroke::command(Key::S))
5763 .build(),
5764 );
5765
5766 tree.layout(SizeProposal::exact(100.0, 50.0));
5767 tree.focus(inner);
5768
5769 tree.press_key(Key::S, Modifiers::COMMAND);
5770 assert!(inner_seen.get());
5771 assert!(!outer_seen.get(), "Handled at inner must stop propagation");
5772 }
5773
5774 #[test]
5775 fn disabled_action_propagates_by_default() {
5776 use crate::action::Action;
5777 use crate::shortcut::{KeyStroke, Shortcut};
5778 use crate::signal::Signal;
5779 use std::cell::Cell;
5780 use std::rc::Rc;
5781
5782 let inner_seen = Rc::new(Cell::new(false));
5783 let outer_seen = Rc::new(Cell::new(false));
5784 let inner_flag = inner_seen.clone();
5785 let outer_flag = outer_seen.clone();
5786
5787 let mut tree = WidgetTree::new();
5788 let outer = tree.add(FillWidget::new().focusable());
5789 let inner = tree.add_child(outer, FillWidget::new().focusable());
5790
5791 let enabled = Signal::new(false);
5792 tree.push_action(
5793 inner,
5794 Action::new("app.save")
5795 .enabled_when(enabled.clone())
5796 .on_invoke(move |_i, _c| {
5797 inner_flag.set(true);
5798 }),
5799 );
5800 tree.push_action(
5801 outer,
5802 Action::new("app.save").on_invoke(move |_i, _c| {
5803 outer_flag.set(true);
5804 }),
5805 );
5806 tree.shortcut_registry_mut().register(
5807 Shortcut::new("app.save")
5808 .primary(KeyStroke::command(Key::S))
5809 .build(),
5810 );
5811
5812 tree.layout(SizeProposal::exact(100.0, 50.0));
5813 tree.focus(inner);
5814
5815 tree.press_key(Key::S, Modifiers::COMMAND);
5816 assert!(!inner_seen.get(), "disabled inner must not run");
5817 assert!(
5818 outer_seen.get(),
5819 "intent must propagate past disabled inner"
5820 );
5821 }
5822
5823 #[test]
5824 fn disabled_action_with_non_propagating_shortcut_consumes() {
5825 use crate::action::Action;
5826 use crate::shortcut::{KeyStroke, Shortcut};
5827 use crate::signal::Signal;
5828 use std::cell::Cell;
5829 use std::rc::Rc;
5830
5831 let inner_seen = Rc::new(Cell::new(false));
5832 let outer_seen = Rc::new(Cell::new(false));
5833 let inner_flag = inner_seen.clone();
5834 let outer_flag = outer_seen.clone();
5835
5836 let mut tree = WidgetTree::new();
5837 let outer = tree.add(FillWidget::new().focusable());
5838 let inner = tree.add_child(outer, FillWidget::new().focusable());
5839
5840 let enabled = Signal::new(false);
5841 tree.push_action(
5842 inner,
5843 Action::new("app.save")
5844 .enabled_when(enabled.clone())
5845 .on_invoke(move |_i, _c| {
5846 inner_flag.set(true);
5847 }),
5848 );
5849 tree.push_action(
5850 outer,
5851 Action::new("app.save").on_invoke(move |_i, _c| {
5852 outer_flag.set(true);
5853 }),
5854 );
5855 tree.shortcut_registry_mut().register(
5856 Shortcut::new("app.save")
5857 .primary(KeyStroke::command(Key::S))
5858 .propagate_when_disabled(false)
5859 .build(),
5860 );
5861
5862 tree.layout(SizeProposal::exact(100.0, 50.0));
5863 tree.focus(inner);
5864
5865 tree.press_key(Key::S, Modifiers::COMMAND);
5866 assert!(!inner_seen.get(), "disabled inner still does not run");
5867 assert!(
5868 !outer_seen.get(),
5869 "intent must NOT propagate when shortcut disallows it"
5870 );
5871 }
5872
5873 #[test]
5874 fn send_intent_from_handler_reaches_ancestor_action() {
5875 use crate::action::Action;
5876 use crate::intent::Intent;
5877 use std::cell::Cell;
5878 use std::rc::Rc;
5879
5880 let save_seen = Rc::new(Cell::new(false));
5881 let save_flag = save_seen.clone();
5882
5883 let mut tree = WidgetTree::new();
5884 let root = tree.add(FillWidget::new());
5885 let button = tree.add_child(
5886 root,
5887 FillWidget::new().on_tap(|_pos, ctx| {
5888 ctx.send_intent(Intent::new("app.save"));
5889 }),
5890 );
5891 tree.push_action(
5892 root,
5893 Action::new("app.save").on_invoke(move |_i, _c| {
5894 save_flag.set(true);
5895 }),
5896 );
5897
5898 tree.layout(SizeProposal::exact(100.0, 50.0));
5899 tree.click(button);
5900 assert!(
5901 save_seen.get(),
5902 "ctx.send_intent must reach ancestor action"
5903 );
5904 }
5905
5906 #[test]
5907 fn widget_type_histogram_counts_distinct_types() {
5908 let mut tree = WidgetTree::new();
5913 let _ = tree.add(FillWidget::new());
5914 let _ = tree.add(FillWidget::new());
5915 let _ = tree.add(FillWidget::new());
5916 tree.layout(SizeProposal::exact(100.0, 100.0));
5917 let histogram = tree.widget_type_histogram();
5918 let total: u32 = histogram.values().sum();
5919 assert!(
5920 total >= 3,
5921 "expected at least 3 active widgets, got {total}: {histogram:?}"
5922 );
5923 let fillwidget_entries: u32 = histogram
5924 .iter()
5925 .filter(|(k, _)| k.contains("FillWidget"))
5926 .map(|(_, v)| *v)
5927 .sum();
5928 assert!(
5929 fillwidget_entries >= 3,
5930 "expected ≥3 FillWidget instances; histogram = {histogram:?}"
5931 );
5932 assert_eq!(tree.active_widget_count() as u32, total);
5933 }
5934
5935 #[test]
5936 fn intent_source_tagged_handler_for_tap_activation() {
5937 use crate::action::Action;
5941 use crate::intent::Intent;
5942 use crate::telemetry::IntentSource;
5943 use std::cell::Cell;
5944 use std::rc::Rc;
5945 let captured = Rc::new(Cell::new(IntentSource::Unknown));
5946 let captured_for_action = captured.clone();
5947
5948 let mut tree = WidgetTree::new();
5949 let root = tree.add(FillWidget::new());
5950 let button = tree.add_child(
5951 root,
5952 FillWidget::new().on_tap(|_pos, ctx| {
5953 ctx.send_intent(Intent::new("app.save"));
5954 }),
5955 );
5956 tree.push_action(
5957 root,
5958 Action::new("app.save").on_invoke(move |intent, _c| {
5959 captured_for_action.set(intent.source);
5960 }),
5961 );
5962
5963 tree.layout(SizeProposal::exact(100.0, 50.0));
5964 tree.click(button);
5965 assert_eq!(
5966 captured.get(),
5967 IntentSource::Handler,
5968 "tap-driven intent must tag IntentSource::Handler"
5969 );
5970 }
5971
5972 #[test]
5973 fn intent_source_programmatic_when_no_handler_active() {
5974 use crate::intent::Intent;
5975 use crate::telemetry::IntentSource;
5976 let intent = Intent::new("app.demo");
5977 assert_eq!(intent.source, IntentSource::Programmatic);
5978
5979 let mut ctx = EventContext::new();
5981 ctx.send_intent(Intent::new("app.demo"));
5982 let queued = ctx.pending_intents.first().expect("intent queued");
5983 assert_eq!(queued.source, IntentSource::Programmatic);
5984 }
5985
5986 #[test]
5987 fn with_intent_source_overrides_for_managed_widgets() {
5988 use crate::intent::Intent;
5989 use crate::telemetry::IntentSource;
5990 let mut ctx = EventContext::new();
5991 ctx.with_intent_source(IntentSource::Menu, |ctx| {
5992 ctx.send_intent(Intent::new("app.demo"));
5993 });
5994 let queued = ctx.pending_intents.first().expect("intent queued");
5995 assert_eq!(
5996 queued.source,
5997 IntentSource::Menu,
5998 "with_intent_source(Menu) must tag the dispatched intent"
5999 );
6000
6001 ctx.send_intent(Intent::new("app.next"));
6005 let next = ctx.pending_intents.last().expect("second intent");
6006 assert_eq!(next.source, IntentSource::Programmatic);
6007 }
6008
6009 #[test]
6010 fn disabled_shortcut_falls_through_to_focused_widget() {
6011 use crate::action::Action;
6012 use crate::shortcut::{KeyStroke, Shortcut};
6013 use crate::signal::Signal;
6014 use std::cell::Cell;
6015 use std::rc::Rc;
6016
6017 let action_fired = Rc::new(Cell::new(false));
6018 let on_key_fired = Rc::new(Cell::new(false));
6019 let af = action_fired.clone();
6020 let kf = on_key_fired.clone();
6021
6022 let enabled = Signal::new(false);
6023
6024 let mut tree = WidgetTree::new();
6025 let widget = tree.add(FillWidget::new().focusable().on_key(move |event, _ctx| {
6026 if matches!(
6027 event,
6028 WidgetEvent::KeyDown {
6029 key: Key::S,
6030 modifiers,
6031 ..
6032 } if modifiers.command()
6033 ) {
6034 kf.set(true);
6035 return EventResponse::Handled;
6036 }
6037 EventResponse::Ignored
6038 }));
6039 tree.push_action(
6040 widget,
6041 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
6042 );
6043 tree.shortcut_registry_mut().register(
6044 Shortcut::new("app.save")
6045 .primary(KeyStroke::command(Key::S))
6046 .enabled_when(enabled.clone())
6047 .build(),
6048 );
6049
6050 tree.layout(SizeProposal::exact(100.0, 50.0));
6051 tree.focus(widget);
6052
6053 tree.press_key(Key::S, Modifiers::COMMAND);
6055 assert!(
6056 !action_fired.get(),
6057 "disabled shortcut must not invoke its action"
6058 );
6059 assert!(
6060 on_key_fired.get(),
6061 "disabled shortcut must let KeyDown reach the focused widget"
6062 );
6063
6064 on_key_fired.set(false);
6066 enabled.set(true);
6067 tree.press_key(Key::S, Modifiers::COMMAND);
6068 assert!(action_fired.get(), "re-enabled shortcut must dispatch");
6069 assert!(
6070 !on_key_fired.get(),
6071 "enabled shortcut must consume the KeyDown"
6072 );
6073 }
6074
6075 #[test]
6076 fn keyboard_capture_bypasses_shortcut() {
6077 use crate::action::Action;
6078 use crate::shortcut::{KeyStroke, Shortcut};
6079 use std::cell::Cell;
6080 use std::rc::Rc;
6081
6082 fn run(capture: bool) -> (bool, bool) {
6088 let action_fired = Rc::new(Cell::new(false));
6089 let on_key_fired = Rc::new(Cell::new(false));
6090 let af = action_fired.clone();
6091 let kf = on_key_fired.clone();
6092
6093 let mut tree = WidgetTree::new();
6094 let widget = tree.add(
6095 FillWidget::new()
6096 .focusable()
6097 .keyboard_capture(capture)
6098 .on_key(move |event, _ctx| {
6099 if matches!(
6100 event,
6101 WidgetEvent::KeyDown { key: Key::S, modifiers, .. } if modifiers.command()
6102 ) {
6103 kf.set(true);
6104 return EventResponse::Handled;
6105 }
6106 EventResponse::Ignored
6107 }),
6108 );
6109 tree.push_action(
6110 widget,
6111 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
6112 );
6113 tree.shortcut_registry_mut().register(
6114 Shortcut::new("app.save")
6115 .primary(KeyStroke::command(Key::S))
6116 .build(),
6117 );
6118
6119 tree.layout(SizeProposal::exact(100.0, 50.0));
6120 tree.focus(widget);
6121 tree.press_key(Key::S, Modifiers::COMMAND);
6122 (action_fired.get(), on_key_fired.get())
6123 }
6124
6125 let (action, on_key) = run(true);
6127 assert!(
6128 !action,
6129 "keyboard_capture must suppress the shortcut action"
6130 );
6131 assert!(on_key, "keyboard_capture must deliver the raw KeyDown");
6132
6133 let (action, on_key) = run(false);
6135 assert!(action, "without capture the shortcut must fire");
6136 assert!(!on_key, "without capture the widget must not see the key");
6137 }
6138
6139 fn key_recorder(
6141 tree: &mut WidgetTree,
6142 ) -> (WidgetId, std::rc::Rc<std::cell::RefCell<Vec<Key>>>) {
6143 let seen = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
6144 let log = seen.clone();
6145 let widget = tree.add(FillWidget::new().focusable().on_key(move |event, _ctx| {
6146 if let WidgetEvent::KeyDown { key, .. } = event {
6147 log.borrow_mut().push(*key);
6148 }
6149 EventResponse::Ignored
6150 }));
6151 tree.layout(SizeProposal::exact(100.0, 50.0));
6152 tree.focus(widget);
6153 (widget, seen)
6154 }
6155
6156 #[test]
6157 fn a_shortcut_no_action_takes_leaves_its_key_to_the_focused_widget() {
6158 use crate::action::Action;
6159 use crate::intent::IntentResponse;
6160 use crate::shortcut::{KeyStroke, Shortcut};
6161
6162 let mut tree = WidgetTree::new();
6163 let (widget, seen) = key_recorder(&mut tree);
6164 tree.shortcut_registry_mut().register(
6165 Shortcut::new("app.save")
6166 .primary(KeyStroke::command(Key::S))
6167 .build(),
6168 );
6169
6170 tree.press_key(Key::S, Modifiers::COMMAND);
6172 assert_eq!(
6173 *seen.borrow(),
6174 vec![Key::S],
6175 "an unhandled intent must not eat the key"
6176 );
6177
6178 tree.push_action(
6180 widget,
6181 Action::new("app.save").on_invoke_with_response(|_i, _c| IntentResponse::Propagated),
6182 );
6183 tree.press_key(Key::S, Modifiers::COMMAND);
6184 assert_eq!(
6185 seen.borrow().len(),
6186 2,
6187 "a propagated intent must leave the key too"
6188 );
6189
6190 let mut tree = WidgetTree::new();
6193 let (widget, seen) = key_recorder(&mut tree);
6194 tree.shortcut_registry_mut().register(
6195 Shortcut::new("app.save")
6196 .primary(KeyStroke::command(Key::S))
6197 .build(),
6198 );
6199 tree.push_action(widget, Action::new("app.save").on_invoke(|_i, _c| {}));
6200 tree.press_key(Key::S, Modifiers::COMMAND);
6201 assert!(
6202 seen.borrow().is_empty(),
6203 "a handled shortcut consumes its key"
6204 );
6205 }
6206
6207 #[test]
6208 fn standard_edit_chords_follow_a_rebind() {
6209 use crate::action::Action;
6210 use crate::shortcut::{KeyStroke, StandardEdit};
6211 use std::cell::Cell;
6212 use std::rc::Rc;
6213
6214 let mut tree = WidgetTree::new();
6215 let (widget, seen) = key_recorder(&mut tree);
6216 let copies = Rc::new(Cell::new(0));
6217 let count = copies.clone();
6218 tree.push_action(
6219 widget,
6220 Action::new(StandardEdit::Copy.id())
6221 .on_invoke(move |_i, _c| count.set(count.get() + 1)),
6222 );
6223
6224 tree.press_key(Key::C, Modifiers::COMMAND);
6226 assert_eq!(copies.get(), 1, "Ctrl+C reaches the edit.copy action");
6227 assert!(seen.borrow().is_empty());
6228
6229 let elsewhere = KeyStroke::new(Key::Character('à'), Modifiers::CTRL);
6232 tree.shortcut_registry_mut()
6233 .rebind_primary(StandardEdit::Copy.id(), Some(elsewhere));
6234 tree.press_key(Key::C, Modifiers::COMMAND);
6235 assert_eq!(copies.get(), 1, "the replaced chord must not copy");
6236 assert_eq!(*seen.borrow(), vec![Key::C]);
6237 tree.press_key(Key::Character('à'), Modifiers::CTRL);
6238 assert_eq!(copies.get(), 2, "the new chord copies");
6239 }
6240
6241 #[test]
6242 fn ctrl_tab_always_escapes_a_keyboard_capture_surface() {
6243 use std::cell::Cell;
6244 use std::rc::Rc;
6245
6246 let saw_key = Rc::new(Cell::new(false));
6252 let sk = saw_key.clone();
6253
6254 let mut tree = WidgetTree::new();
6255 let capture = tree.add(
6256 FillWidget::new()
6257 .focusable()
6258 .keyboard_capture(true)
6259 .on_key(move |_event, _ctx| {
6261 sk.set(true);
6262 EventResponse::Handled
6263 }),
6264 );
6265 let neighbour = tree.add(FillWidget::new().focusable());
6266 tree.layout(SizeProposal::exact(100.0, 50.0));
6267
6268 tree.focus(capture);
6271 tree.press_key(Key::Tab, Modifiers::NONE);
6272 assert!(saw_key.get(), "plain Tab must reach the capture surface");
6273 assert_eq!(
6274 tree.focused(),
6275 Some(capture),
6276 "plain Tab must not move focus off a capture surface"
6277 );
6278
6279 saw_key.set(false);
6281 tree.press_key(Key::Tab, Modifiers::CTRL);
6282 assert!(
6283 !saw_key.get(),
6284 "Ctrl+Tab is reserved and must not reach the capture surface"
6285 );
6286 assert_eq!(
6287 tree.focused(),
6288 Some(neighbour),
6289 "Ctrl+Tab must move focus out of a capture surface"
6290 );
6291
6292 tree.focus(capture);
6294 tree.press_key(Key::Tab, Modifiers::CTRL | Modifiers::SHIFT);
6295 assert_eq!(
6296 tree.focused(),
6297 Some(neighbour),
6298 "Ctrl+Shift+Tab must move focus out of a capture surface"
6299 );
6300 }
6301
6302 #[test]
6303 fn scope_mismatch_does_not_invoke_on_activate() {
6304 use crate::intent::Intent;
6305 use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
6306 use std::cell::Cell;
6307 use std::rc::Rc;
6308
6309 let activated = Rc::new(Cell::new(false));
6315 let activated_flag = activated.clone();
6316
6317 let mut tree = WidgetTree::new();
6318 let scope_root = tree.add(FillWidget::new().focusable());
6319 let outside = tree.add(FillWidget::new().focusable());
6320
6321 tree.shortcut_registry_mut().register(
6322 Shortcut::new("editor.find")
6323 .primary(KeyStroke::command(Key::F))
6324 .scope(ShortcutScope::Scoped(scope_root))
6325 .on_activate(move |_ks, _ctx| {
6326 activated_flag.set(true);
6327 Intent::new("editor.find")
6328 })
6329 .build(),
6330 );
6331
6332 tree.layout(SizeProposal::exact(200.0, 100.0));
6333 tree.focus(outside);
6334
6335 tree.press_key(Key::F, Modifiers::COMMAND);
6336 assert!(
6337 !activated.get(),
6338 "on_activate must not run when focus is outside the shortcut's scope"
6339 );
6340 }
6341
6342 #[test]
6343 fn key_capture_runs_callback_and_bypasses_registry() {
6344 use crate::action::Action;
6345 use crate::shortcut::{KeyStroke, Shortcut};
6346 use std::cell::Cell;
6347 use std::rc::Rc;
6348
6349 let action_fired = Rc::new(Cell::new(false));
6350 let af = action_fired.clone();
6351 let captured = Rc::new(Cell::new(None));
6352 let cf = captured.clone();
6353
6354 let mut tree = WidgetTree::new();
6355 let widget = tree.add(FillWidget::new().focusable());
6356 tree.push_action(
6357 widget,
6358 Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
6359 );
6360 tree.shortcut_registry_mut().register(
6361 Shortcut::new("app.save")
6362 .primary(KeyStroke::command(Key::S))
6363 .build(),
6364 );
6365
6366 tree.layout(SizeProposal::exact(100.0, 50.0));
6367 tree.focus(widget);
6368
6369 let handle = tree.begin_key_capture(move |ks, _reg, _ctx| cf.set(Some(ks)));
6370 assert!(tree.is_capturing_keys());
6371
6372 tree.press_key(Key::S, Modifiers::COMMAND);
6373 assert_eq!(
6374 captured.get(),
6375 Some(KeyStroke::command(Key::S)),
6376 "capture callback must receive the chord"
6377 );
6378 assert!(
6379 !action_fired.get(),
6380 "shortcut action must not fire while capture is armed"
6381 );
6382 assert!(
6383 !tree.is_capturing_keys(),
6384 "capture is one-shot; next KeyDown flows normally"
6385 );
6386 drop(handle);
6387 }
6388
6389 #[test]
6390 fn key_capture_can_rebind_through_registry() {
6391 use crate::shortcut::{KeyStroke, Shortcut};
6392
6393 let mut tree = WidgetTree::new();
6394 let widget = tree.add(FillWidget::new().focusable());
6395 tree.shortcut_registry_mut().register(
6396 Shortcut::new("app.save")
6397 .primary(KeyStroke::command(Key::S))
6398 .build(),
6399 );
6400
6401 tree.layout(SizeProposal::exact(100.0, 50.0));
6402 tree.focus(widget);
6403
6404 let _h = tree.begin_key_capture(|ks, reg, _ctx| {
6406 reg.rebind_primary("app.save", Some(ks));
6407 });
6408
6409 tree.press_key(Key::B, Modifiers::COMMAND | Modifiers::SHIFT);
6410 assert_eq!(
6411 tree.shortcut_registry()
6412 .effective("app.save")
6413 .unwrap()
6414 .primary,
6415 Some(KeyStroke::command_shift(Key::B))
6416 );
6417 }
6418
6419 #[test]
6420 fn dropping_capture_handle_cancels_capture() {
6421 use crate::shortcut::{KeyStroke, Shortcut};
6422 use std::cell::Cell;
6423 use std::rc::Rc;
6424
6425 let action_fired = Rc::new(Cell::new(false));
6426 let af = action_fired.clone();
6427 let capture_fired = Rc::new(Cell::new(false));
6428 let cf = capture_fired.clone();
6429
6430 let mut tree = WidgetTree::new();
6431 let widget = tree.add(FillWidget::new().focusable());
6432 tree.push_action(
6433 widget,
6434 crate::action::Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
6435 );
6436 tree.shortcut_registry_mut().register(
6437 Shortcut::new("app.save")
6438 .primary(KeyStroke::command(Key::S))
6439 .build(),
6440 );
6441 tree.layout(SizeProposal::exact(100.0, 50.0));
6442 tree.focus(widget);
6443
6444 {
6448 let _h = tree.begin_key_capture(move |_ks, _reg, _ctx| cf.set(true));
6449 assert!(tree.is_capturing_keys());
6450 }
6452 assert!(
6453 !tree.is_capturing_keys(),
6454 "dropping the handle must cancel the capture"
6455 );
6456
6457 tree.press_key(Key::S, Modifiers::COMMAND);
6458 assert!(!capture_fired.get(), "cancelled capture must not fire");
6459 assert!(
6460 action_fired.get(),
6461 "shortcut action runs after capture was cancelled"
6462 );
6463 }
6464
6465 #[test]
6466 fn second_begin_key_capture_does_not_racecancel_first() {
6467 use std::cell::Cell;
6468 use std::rc::Rc;
6469
6470 let first = Rc::new(Cell::new(false));
6471 let second = Rc::new(Cell::new(false));
6472 let f = first.clone();
6473 let s = second.clone();
6474
6475 let mut tree = WidgetTree::new();
6476 let widget = tree.add(FillWidget::new().focusable());
6477 tree.layout(SizeProposal::exact(100.0, 50.0));
6478 tree.focus(widget);
6479
6480 let h1 = tree.begin_key_capture(move |_ks, _reg, _ctx| f.set(true));
6486 let _h2 = tree.begin_key_capture(move |_ks, _reg, _ctx| s.set(true));
6487 drop(h1);
6488
6489 assert!(
6490 tree.is_capturing_keys(),
6491 "dropping the older handle must not cancel the active capture"
6492 );
6493 tree.press_key(Key::K, Modifiers::COMMAND);
6494 assert!(!first.get());
6495 assert!(second.get(), "newest capture wins");
6496 }
6497
6498 #[test]
6499 fn capture_callback_can_send_intent() {
6500 use crate::action::Action;
6501 use crate::intent::Intent;
6502
6503 use std::cell::Cell;
6504 use std::rc::Rc;
6505
6506 let ran = Rc::new(Cell::new(false));
6507 let flag = ran.clone();
6508
6509 let mut tree = WidgetTree::new();
6510 let widget = tree.add(FillWidget::new().focusable());
6511 tree.push_action(
6512 widget,
6513 Action::new("app.save").on_invoke(move |_i, _c| flag.set(true)),
6514 );
6515 tree.layout(SizeProposal::exact(100.0, 50.0));
6516 tree.focus(widget);
6517
6518 let _h = tree.begin_key_capture(|_ks, _reg, ctx| {
6519 ctx.send_intent(Intent::new("app.save"));
6520 });
6521 tree.press_key(Key::X, Modifiers::COMMAND);
6522 assert!(
6523 ran.get(),
6524 "intent queued from capture callback must dispatch"
6525 );
6526 }
6527
6528 #[test]
6529 fn binding_registry_does_not_accumulate_across_rebuilds() {
6530 use crate::binding::BindingLevel;
6531 use crate::signal::Signal;
6532
6533 #[derive(Debug)]
6534 struct BoundLeaf {
6535 tick: Signal<u64>,
6536 }
6537 impl crate::widget::Widget for BoundLeaf {
6538 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6539 self.tick.bind_to(
6540 ctx.self_id(),
6541 ctx.binding_registry(),
6542 BindingLevel::Relayout,
6543 );
6544 Vec::new()
6545 }
6546 fn layout_response(
6547 &self,
6548 proposal: SizeProposal,
6549 _ctx: &crate::widget::LayoutContext,
6550 ) -> crate::widget::LayoutResponse {
6551 proposal.resolve(10.0, 10.0).into()
6552 }
6553 }
6554
6555 let mut tree = WidgetTree::new();
6556 let tick = Signal::new(0_u64);
6557 let widget = tree.add(BoundLeaf { tick: tick.clone() });
6558 tree.layout(SizeProposal::exact(200.0, 200.0));
6559 let after_first_build = tree.binding_registry().len();
6560 assert!(after_first_build >= 1);
6561
6562 for _ in 0..5 {
6566 tree.arena.mark_needs_rebuild(widget);
6567 tree.layout(SizeProposal::exact(200.0, 200.0));
6568 }
6569 assert_eq!(
6570 tree.binding_registry().len(),
6571 after_first_build,
6572 "bindings must be cleared on rebuild"
6573 );
6574
6575 tree.destroy_subtree(widget);
6576 assert_eq!(
6577 tree.binding_registry().len(),
6578 0,
6579 "bindings must be cleared on destroy"
6580 );
6581 let _ = tick;
6583 }
6584
6585 #[test]
6586 fn ctx_destroy_cancels_animations_and_bindings_via_deferred_path() {
6587 use crate::binding::BindingLevel;
6595 use crate::signal::Signal;
6596 use std::time::{Duration, Instant};
6597 use teksilo_tokens::Easing;
6598
6599 #[derive(Debug)]
6600 struct BoundLeaf {
6601 tick: Signal<u64>,
6602 }
6603 impl crate::widget::Widget for BoundLeaf {
6604 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6605 self.tick.bind_to(
6606 ctx.self_id(),
6607 ctx.binding_registry(),
6608 BindingLevel::Relayout,
6609 );
6610 Vec::new()
6611 }
6612 fn layout_response(
6613 &self,
6614 proposal: SizeProposal,
6615 _ctx: &crate::widget::LayoutContext,
6616 ) -> crate::widget::LayoutResponse {
6617 proposal.resolve(10.0, 10.0).into()
6618 }
6619 }
6620
6621 let mut tree = WidgetTree::new();
6622 let widget = tree.add(BoundLeaf {
6623 tick: Signal::new(0_u64),
6624 });
6625 tree.layout(SizeProposal::exact(200.0, 200.0));
6626 assert!(tree.binding_registry().len() >= 1);
6627
6628 let anim = Signal::<f32>::new_animated(0.0);
6631 tree.animation_scheduler.animate(
6632 &anim,
6633 widget,
6634 1.0,
6635 Duration::from_secs(10),
6636 Easing::Linear,
6637 Instant::now(),
6638 );
6639 assert_eq!(tree.animation_scheduler.active_count(), 1);
6640
6641 let mut noop = crate::window::NoopWindowOps;
6643 tree.run_with_event_context(&mut noop, |ctx| ctx.destroy(widget));
6644
6645 assert_eq!(
6646 tree.animation_scheduler.active_count(),
6647 0,
6648 "ctx.destroy must cancel animations owned by the destroyed widget"
6649 );
6650 assert_eq!(
6651 tree.binding_registry().len(),
6652 0,
6653 "ctx.destroy must unregister the destroyed widget's bindings"
6654 );
6655 assert!(
6656 tree.arena.get(widget).is_none(),
6657 "node must be removed from the arena"
6658 );
6659 }
6660
6661 #[test]
6662 fn clear_shortcut_override_via_event_context_restores_default() {
6663 use crate::shortcut::{KeyStroke, Shortcut};
6664
6665 let mut tree = WidgetTree::new();
6666 tree.shortcut_registry_mut().register(
6667 Shortcut::new("app.save")
6668 .primary(KeyStroke::command(Key::S))
6669 .build(),
6670 );
6671 tree.shortcut_registry_mut()
6672 .rebind_primary("app.save", Some(KeyStroke::alt(Key::S)));
6673
6674 let source = tree.add(FillWidget::new());
6675 let mut ctx = EventContext::new();
6676 ctx.clear_shortcut_override("app.save");
6677 tree.collect_from_ctx(ctx, source);
6678
6679 assert_eq!(
6680 tree.shortcut_registry()
6681 .effective("app.save")
6682 .unwrap()
6683 .primary,
6684 Some(KeyStroke::command(Key::S))
6685 );
6686 }
6687
6688 #[test]
6689 fn rebind_shortcut_primary_via_event_context() {
6690 use crate::shortcut::{KeyStroke, Shortcut};
6691
6692 let mut tree = WidgetTree::new();
6693 tree.shortcut_registry_mut().register(
6694 Shortcut::new("app.save")
6695 .primary(KeyStroke::command(Key::S))
6696 .build(),
6697 );
6698 let source = tree.add(FillWidget::new());
6699
6700 let mut ctx = EventContext::new();
6701 ctx.rebind_shortcut_primary("app.save", Some(KeyStroke::alt(Key::S)));
6702 tree.collect_from_ctx(ctx, source);
6703
6704 assert_eq!(
6705 tree.shortcut_registry()
6706 .effective("app.save")
6707 .unwrap()
6708 .primary,
6709 Some(KeyStroke::alt(Key::S))
6710 );
6711 }
6712
6713 #[test]
6714 fn unregister_all_for_owner_called_on_destroy() {
6715 use crate::shortcut::{KeyStroke, Shortcut};
6716
6717 let mut tree = WidgetTree::new();
6718 let widget = tree.add(FillWidget::new());
6719 let widget_owner = widget;
6720 tree.shortcut_registry_mut().register_owned(
6721 Shortcut::new("scoped.thing")
6722 .primary(KeyStroke::command(Key::K))
6723 .build(),
6724 widget_owner,
6725 );
6726 assert!(
6727 tree.shortcut_registry()
6728 .get_default("scoped.thing")
6729 .is_some()
6730 );
6731
6732 tree.destroy_subtree(widget);
6733 assert!(
6734 tree.shortcut_registry()
6735 .get_default("scoped.thing")
6736 .is_none(),
6737 "destroying the owner must unregister its shortcut"
6738 );
6739 }
6740
6741 #[test]
6745 fn global_action_reached_from_unrelated_source() {
6746 use crate::action::Action;
6747 use crate::intent::Intent;
6748 use std::cell::Cell;
6749 use std::rc::Rc;
6750
6751 #[derive(Debug)]
6752 struct Registrar(Rc<Cell<bool>>);
6753 impl crate::widget::Widget for Registrar {
6754 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6755 let flag = self.0.clone();
6756 ctx.register_action_global(
6757 Action::new("test.global").on_invoke(move |_i, _c| flag.set(true)),
6758 );
6759 vec![]
6760 }
6761 fn layout_response(
6762 &self,
6763 _p: teksilo_canvas::SizeProposal,
6764 _c: &crate::widget::LayoutContext,
6765 ) -> crate::widget::LayoutResponse {
6766 teksilo_canvas::Size::new(0.0, 0.0).into()
6767 }
6768 }
6769
6770 let mut tree = WidgetTree::new();
6771 let fired = Rc::new(Cell::new(false));
6772 let registrar = tree.add(Registrar(fired.clone()));
6773 let source = tree.add(FillWidget::new()); let mut ops = crate::window::NoopWindowOps;
6775
6776 tree.dispatch_intent(source, Intent::new("test.global"), true, &mut ops);
6777 assert!(
6778 fired.get(),
6779 "global action must fire from an unrelated source"
6780 );
6781
6782 fired.set(false);
6784 tree.destroy_subtree(registrar);
6785 tree.dispatch_intent(source, Intent::new("test.global"), true, &mut ops);
6786 assert!(
6787 !fired.get(),
6788 "destroying the owner must remove its global action"
6789 );
6790 }
6791
6792 #[test]
6804 fn hit_test_through_translate_scope() {
6805 use crate::test_widgets::StackWidget;
6806 let mut tree = WidgetTree::new();
6807 let child = tree.add(FillWidget::new());
6808 let parent = tree.add(StackWidget::new().child(child));
6809 tree.set_transform(parent, teksilo_canvas::Transform2D::translate(100.0, 0.0));
6811 tree.layout(SizeProposal::exact(100.0, 50.0));
6812
6813 assert_eq!(
6817 tree.hit_test(Point::new(50.0, 25.0)),
6818 None,
6819 "pre-transform area is not visually populated and must not hit"
6820 );
6821 assert_eq!(
6823 tree.hit_test(Point::new(150.0, 25.0)),
6824 Some(child),
6825 "visually-rendered area must hit the child"
6826 );
6827 assert_eq!(tree.hit_test(Point::new(250.0, 25.0)), None);
6829 }
6830
6831 #[test]
6832 fn hit_test_through_scale_scope() {
6833 use crate::test_widgets::StackWidget;
6834 let mut tree = WidgetTree::new();
6835 let child = tree.add(FillWidget::new());
6836 let parent = tree.add(StackWidget::new().child(child));
6837 tree.set_transform(parent, teksilo_canvas::Transform2D::scale(0.5, 0.5));
6840 tree.layout(SizeProposal::exact(100.0, 50.0));
6841
6842 assert_eq!(tree.hit_test(Point::new(25.0, 12.0)), Some(child));
6844 assert_eq!(
6847 tree.hit_test(Point::new(75.0, 25.0)),
6848 None,
6849 "scaled-out region must not hit"
6850 );
6851 }
6852
6853 #[test]
6854 fn hit_test_through_nested_transforms_compose() {
6855 use crate::test_widgets::StackWidget;
6856 let mut tree = WidgetTree::new();
6857 let leaf = tree.add(FillWidget::new());
6858 let inner = tree.add(StackWidget::new().child(leaf));
6859 let outer = tree.add(StackWidget::new().child(inner));
6860 tree.set_transform(outer, teksilo_canvas::Transform2D::translate(100.0, 0.0));
6864 tree.set_transform(inner, teksilo_canvas::Transform2D::scale(2.0, 2.0));
6865 tree.layout(SizeProposal::exact(50.0, 25.0));
6866
6867 assert_eq!(tree.hit_test(Point::new(150.0, 25.0)), Some(leaf));
6871 assert_eq!(tree.hit_test(Point::new(50.0, 25.0)), None);
6872 assert_eq!(tree.hit_test(Point::new(250.0, 25.0)), None);
6873 }
6874
6875 #[test]
6876 fn hit_test_identity_transform_unchanged() {
6877 let mut tree = WidgetTree::new();
6881 let widget = tree.add(FillWidget::new());
6882 tree.set_transform(widget, teksilo_canvas::Transform2D::IDENTITY);
6883 tree.layout(SizeProposal::exact(100.0, 50.0));
6884 assert_eq!(tree.hit_test(Point::new(50.0, 25.0)), Some(widget));
6885 }
6886
6887 #[test]
6888 fn arena_effective_transform_composes_ancestors() {
6889 use crate::test_widgets::StackWidget;
6900 let mut tree = WidgetTree::new();
6901 let leaf = tree.add(FillWidget::new());
6902 let inner = tree.add(StackWidget::new().child(leaf));
6903 let outer = tree.add(StackWidget::new().child(inner));
6904 tree.set_transform(outer, teksilo_canvas::Transform2D::translate(100.0, 0.0));
6905 tree.set_transform(inner, teksilo_canvas::Transform2D::scale(2.0, 2.0));
6906 tree.layout(SizeProposal::exact(50.0, 25.0));
6907
6908 let eff = tree.arena.effective_transform(leaf);
6909 let expected = teksilo_canvas::Transform2D::scale(2.0, 2.0)
6910 .then(&teksilo_canvas::Transform2D::translate(100.0, 0.0));
6911 for (a, b) in eff.m.iter().zip(expected.m.iter()) {
6912 assert!(
6913 (a - b).abs() < 1e-5,
6914 "effective_transform mismatch: got {:?}, want {:?}",
6915 eff.m,
6916 expected.m
6917 );
6918 }
6919
6920 let screen_origin = eff.apply_point(Point::new(0.0, 0.0));
6926 assert!((screen_origin.x - 100.0).abs() < 1e-5);
6927 assert!((screen_origin.y - 0.0).abs() < 1e-5);
6928 let screen_corner = eff.apply_point(Point::new(50.0, 25.0));
6931 assert!((screen_corner.x - 200.0).abs() < 1e-5);
6932 assert!((screen_corner.y - 50.0).abs() < 1e-5);
6933 }
6934
6935 #[derive(Debug)]
6940 struct StubMenu;
6941 impl crate::widget::Widget for StubMenu {
6942 fn layout_response(
6943 &self,
6944 _proposal: SizeProposal,
6945 _ctx: &crate::widget::LayoutContext,
6946 ) -> crate::widget::LayoutResponse {
6947 teksilo_canvas::Size::new(100.0, 40.0).into()
6948 }
6949 }
6950
6951 #[derive(Debug)]
6963 struct NominatingWidget {
6964 row: std::cell::Cell<Option<WidgetId>>,
6965 }
6966
6967 impl crate::widget::Widget for NominatingWidget {
6968 fn layout_response(
6969 &self,
6970 proposal: SizeProposal,
6971 _ctx: &LayoutContext,
6972 ) -> crate::widget::LayoutResponse {
6973 proposal.resolve(50.0, 20.0).into()
6974 }
6975
6976 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6977 ctx.apply_self_handlers(crate::widget_builder::HandlerSet::new().focusable(true));
6978 Vec::new()
6979 }
6980
6981 fn context_menu_key_target(&self) -> Option<WidgetId> {
6982 self.row.get()
6983 }
6984 }
6985
6986 fn press(tree: &mut WidgetTree, key: Key, modifiers: Modifiers) {
6987 tree.dispatch_event(WidgetEvent::KeyDown {
6988 key,
6989 modifiers,
6990 text: None,
6991 });
6992 }
6993
6994 #[test]
6995 fn the_context_menu_key_opens_the_focused_widget_menu() {
6996 use std::cell::Cell;
6997 use std::rc::Rc;
6998
6999 let opened = Rc::new(Cell::new(false));
7000 let flag = opened.clone();
7001 let mut tree = WidgetTree::new();
7002 let widget = tree.add(
7003 FillWidget::new()
7004 .focusable()
7005 .context_menu(move |_pos, _ctx| {
7006 flag.set(true);
7007 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
7008 }),
7009 );
7010 tree.layout(SizeProposal::exact(200.0, 100.0));
7011 tree.focus(widget);
7012
7013 press(&mut tree, Key::ContextMenu, Modifiers::NONE);
7014 assert!(opened.get(), "the dedicated Menu key must open the menu");
7015 }
7016
7017 #[test]
7020 fn shift_f10_opens_the_focused_widget_menu() {
7021 use std::cell::Cell;
7022 use std::rc::Rc;
7023
7024 let opened = Rc::new(Cell::new(false));
7025 let flag = opened.clone();
7026 let mut tree = WidgetTree::new();
7027 let widget = tree.add(
7028 FillWidget::new()
7029 .focusable()
7030 .context_menu(move |_pos, _ctx| {
7031 flag.set(true);
7032 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
7033 }),
7034 );
7035 tree.layout(SizeProposal::exact(200.0, 100.0));
7036 tree.focus(widget);
7037
7038 press(&mut tree, Key::F10, Modifiers::SHIFT);
7039 assert!(opened.get(), "Shift+F10 must open the menu");
7040 }
7041
7042 #[test]
7045 fn a_near_miss_chord_is_not_a_context_menu_request() {
7046 use std::cell::Cell;
7047 use std::rc::Rc;
7048
7049 let opened = Rc::new(Cell::new(false));
7050 let flag = opened.clone();
7051 let mut tree = WidgetTree::new();
7052 let widget = tree.add(
7053 FillWidget::new()
7054 .focusable()
7055 .context_menu(move |_pos, _ctx| {
7056 flag.set(true);
7057 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
7058 }),
7059 );
7060 tree.layout(SizeProposal::exact(200.0, 100.0));
7061 tree.focus(widget);
7062
7063 press(&mut tree, Key::F10, Modifiers::SHIFT | Modifiers::CTRL);
7064 press(&mut tree, Key::F10, Modifiers::NONE);
7065 assert!(!opened.get(), "only Shift+F10 exactly asks for a menu");
7066 }
7067
7068 #[test]
7072 fn the_keyboard_target_can_be_a_row_rather_than_the_focused_container() {
7073 use std::cell::Cell;
7074 use std::rc::Rc;
7075
7076 let menu_owner = Rc::new(Cell::new(None::<&'static str>));
7077
7078 let row_flag = menu_owner.clone();
7079 let container_flag = menu_owner.clone();
7080
7081 let mut tree = WidgetTree::new();
7082 let row = tree.add(FillWidget::new().context_menu(move |_pos, _ctx| {
7083 row_flag.set(Some("row"));
7084 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
7085 }));
7086 let container = tree.add(
7087 crate::test_widgets::StackWidget::new()
7088 .child(row)
7089 .context_menu(move |_pos, _ctx| {
7090 container_flag.set(Some("container"));
7091 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
7092 }),
7093 );
7094 tree.layout(SizeProposal::exact(200.0, 100.0));
7095
7096 let nominator = tree.add(NominatingWidget {
7098 row: std::cell::Cell::new(Some(row)),
7099 });
7100 tree.layout(SizeProposal::exact(200.0, 100.0));
7101 tree.focus(nominator);
7102 let _ = container;
7103
7104 press(&mut tree, Key::ContextMenu, Modifiers::NONE);
7105 assert_eq!(
7106 menu_owner.get(),
7107 Some("row"),
7108 "the nominated row's factory must be the one that runs"
7109 );
7110 }
7111
7112 #[test]
7115 fn the_chord_falls_through_when_there_is_no_menu_to_show() {
7116 use std::cell::Cell;
7117 use std::rc::Rc;
7118
7119 let saw_key = Rc::new(Cell::new(false));
7120 let flag = saw_key.clone();
7121 let mut tree = WidgetTree::new();
7122 let widget = tree.add(FillWidget::new().focusable().on_key(move |ev, _ctx| {
7123 if matches!(ev, WidgetEvent::KeyDown { key: Key::F10, .. }) {
7124 flag.set(true);
7125 }
7126 crate::event::EventResponse::Ignored
7127 }));
7128 tree.layout(SizeProposal::exact(200.0, 100.0));
7129 tree.focus(widget);
7130
7131 press(&mut tree, Key::F10, Modifiers::SHIFT);
7132 assert!(
7133 saw_key.get(),
7134 "with no factory anywhere, the key must reach the widget"
7135 );
7136 }
7137
7138 #[test]
7144 fn each_route_to_a_menu_tells_the_factory_what_asked_for_it() {
7145 use crate::widget_builder::ContextMenuTrigger;
7146 use std::cell::RefCell;
7147 use std::rc::Rc;
7148
7149 let seen = Rc::new(RefCell::new(Vec::new()));
7150 let log = seen.clone();
7151 let mut tree = WidgetTree::new();
7152 let widget = tree.add(
7153 FillWidget::new()
7154 .focusable()
7155 .context_menu(move |_pos, ctx| {
7156 log.borrow_mut().push(ctx.context_menu_trigger());
7157 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
7158 }),
7159 );
7160 tree.layout(SizeProposal::exact(200.0, 100.0));
7161 tree.focus(widget);
7162 let inside = tree.bounds(widget).center();
7163
7164 press(&mut tree, Key::F10, Modifiers::SHIFT);
7165 press(&mut tree, Key::Escape, Modifiers::NONE);
7166 press(&mut tree, Key::ContextMenu, Modifiers::NONE);
7167 press(&mut tree, Key::Escape, Modifiers::NONE);
7168 tree.dispatch_event(WidgetEvent::pointer_down(
7169 inside,
7170 crate::event::PointerButton::Secondary,
7171 Modifiers::NONE,
7172 ));
7173 tree.dispatch_event(WidgetEvent::pointer_up(
7174 inside,
7175 crate::event::PointerButton::Secondary,
7176 Modifiers::NONE,
7177 ));
7178 press(&mut tree, Key::Escape, Modifiers::NONE);
7179 tree.dispatch_event(WidgetEvent::AccessAction {
7180 action: accesskit::Action::ShowContextMenu,
7181 target: Some(widget),
7182 target_node: crate::accessibility::widget_id_to_node_id(widget),
7183 data: None,
7184 });
7185 press(&mut tree, Key::Escape, Modifiers::NONE);
7186 tree.long_press_at(teksilo_tokens::PointerKind::Touch, inside);
7187
7188 assert_eq!(
7189 *seen.borrow(),
7190 vec![
7191 Some(ContextMenuTrigger::Keyboard),
7192 Some(ContextMenuTrigger::Keyboard),
7193 Some(ContextMenuTrigger::Pointer),
7194 Some(ContextMenuTrigger::Accessibility),
7195 Some(ContextMenuTrigger::Pointer),
7196 ],
7197 "Shift+F10, the Menu key, a right-click, the assistive action and a \
7198 hold, in that order"
7199 );
7200 }
7201
7202 #[test]
7203 fn context_menu_factory_receives_click_position() {
7204 use crate::event::{Modifiers, PointerButton};
7205 use std::cell::Cell;
7206 use std::rc::Rc;
7207
7208 let captured_position = Rc::new(Cell::new(None::<Point>));
7209 let cap = captured_position.clone();
7210 let mut tree = WidgetTree::new();
7211 let widget = tree.add(FillWidget::new().context_menu(move |pos, _ctx| {
7212 cap.set(Some(pos));
7213 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
7214 }));
7215 tree.layout(SizeProposal::exact(200.0, 100.0));
7216
7217 let click = Point::new(73.0, 42.0);
7218 tree.dispatch_event(WidgetEvent::pointer_down(
7219 click,
7220 PointerButton::Secondary,
7221 Modifiers::NONE,
7222 ));
7223
7224 let got = captured_position.get();
7225 assert_eq!(
7226 got,
7227 Some(click),
7228 "factory must receive the click position; got {:?}",
7229 got
7230 );
7231 let _ = widget;
7232 }
7233
7234 #[test]
7235 fn context_menu_factory_returning_none_falls_through_to_parent() {
7236 use crate::event::{Modifiers, PointerButton};
7237 use crate::test_widgets::StackWidget;
7238 use std::cell::Cell;
7239 use std::rc::Rc;
7240
7241 let outer_called = Rc::new(Cell::new(0_u32));
7245 let outer_flag = outer_called.clone();
7246 let mut tree = WidgetTree::new();
7247 let inner = tree.add(FillWidget::new().context_menu(|_pos, _ctx| None));
7248 let _outer = tree.add(
7249 StackWidget::new()
7250 .child(inner)
7251 .context_menu(move |_pos, _ctx| {
7252 outer_flag.set(outer_flag.get() + 1);
7253 Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
7254 }),
7255 );
7256 tree.layout(SizeProposal::exact(200.0, 100.0));
7257
7258 tree.dispatch_event(WidgetEvent::pointer_down(
7259 Point::new(50.0, 25.0),
7260 PointerButton::Secondary,
7261 Modifiers::NONE,
7262 ));
7263
7264 assert_eq!(
7265 outer_called.get(),
7266 1,
7267 "inner returning None must fall through to the outer factory"
7268 );
7269 }
7270
7271 #[test]
7272 fn context_menu_factory_none_throughout_chain_does_not_show_overlay() {
7273 use crate::event::{Modifiers, PointerButton};
7274
7275 let mut tree = WidgetTree::new();
7277 tree.add(FillWidget::new().context_menu(|_pos, _ctx| None));
7278 tree.layout(SizeProposal::exact(200.0, 100.0));
7279
7280 let overlay_count_before = tree.overlay_manager.len();
7281 tree.dispatch_event(WidgetEvent::pointer_down(
7282 Point::new(50.0, 25.0),
7283 PointerButton::Secondary,
7284 Modifiers::NONE,
7285 ));
7286 let overlay_count_after = tree.overlay_manager.len();
7287 assert_eq!(
7288 overlay_count_before, overlay_count_after,
7289 "a factory returning None must not mount any overlay"
7290 );
7291 }
7292
7293 #[derive(Debug)]
7304 struct FreshChildHost {
7305 preserve: bool,
7306 }
7307 impl Widget for FreshChildHost {
7308 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
7309 vec![ctx.add(FillWidget::new())]
7310 }
7311 fn layout_response(
7312 &self,
7313 p: SizeProposal,
7314 _c: &LayoutContext,
7315 ) -> crate::widget::LayoutResponse {
7316 p.resolve(10.0, 10.0).into()
7317 }
7318 fn preserves_children_on_rebuild(&self) -> bool {
7319 self.preserve
7320 }
7321 }
7322
7323 #[test]
7324 fn reconcile_reaps_dropped_children_no_leak() {
7325 for preserve in [false, true] {
7330 let mut tree = WidgetTree::new();
7331 let host = tree.add(FreshChildHost { preserve });
7332 tree.layout(SizeProposal::exact(100.0, 100.0));
7333 let total0 = tree.arena.len();
7334 let active0 = tree.active_widget_count();
7335 for _ in 0..5 {
7336 tree.arena_mark_needs_rebuild_for_testing(host);
7337 tree.layout(SizeProposal::exact(100.0, 100.0));
7338 }
7339 assert_eq!(
7340 tree.arena.len(),
7341 total0,
7342 "preserve={preserve}: dropped children must be reaped, not leaked"
7343 );
7344 assert_eq!(
7345 tree.active_widget_count(),
7346 active0,
7347 "preserve={preserve}: no stranded still-active orphans"
7348 );
7349 }
7350 }
7351
7352 #[derive(Debug)]
7358 struct DetachedContentHost {
7359 preserve: bool,
7360 }
7361 impl Widget for DetachedContentHost {
7362 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
7363 let popup = ctx.add_detached(FillWidget::new());
7364 ctx.set_dormant(popup);
7365 vec![ctx.add(FillWidget::new())]
7366 }
7367 fn layout_response(
7368 &self,
7369 p: SizeProposal,
7370 _c: &LayoutContext,
7371 ) -> crate::widget::LayoutResponse {
7372 p.resolve(10.0, 10.0).into()
7373 }
7374 fn preserves_children_on_rebuild(&self) -> bool {
7375 self.preserve
7376 }
7377 }
7378
7379 #[test]
7380 fn rebuilding_reaps_detached_content_no_leak() {
7381 for preserve in [false, true] {
7387 let mut tree = WidgetTree::new();
7388 let host = tree.add(DetachedContentHost { preserve });
7389 tree.layout(SizeProposal::exact(100.0, 100.0));
7390 let total0 = tree.arena.len();
7391 for _ in 0..5 {
7392 tree.arena_mark_needs_rebuild_for_testing(host);
7393 tree.layout(SizeProposal::exact(100.0, 100.0));
7394 }
7395 assert_eq!(
7396 tree.arena.len(),
7397 total0,
7398 "preserve={preserve}: the previous build's detached content must be reaped"
7399 );
7400 }
7401 }
7402
7403 #[test]
7404 fn destroying_a_host_reaps_its_detached_content() {
7405 let mut tree = WidgetTree::new();
7406 let outer = tree.add(FillWidget::new());
7407 tree.layout(SizeProposal::exact(100.0, 100.0));
7408 let empty = tree.arena.len();
7409
7410 let host = tree.add_child(outer, DetachedContentHost { preserve: false });
7411 tree.layout(SizeProposal::exact(100.0, 100.0));
7412 assert!(tree.arena.len() > empty);
7413
7414 tree.destroy_subtree(host);
7415 assert_eq!(
7416 tree.arena.len(),
7417 empty,
7418 "the popup must die with the widget that built it"
7419 );
7420 }
7421
7422 #[derive(Debug)]
7424 struct StableChildHost {
7425 child: Option<WidgetId>,
7426 probe: std::rc::Rc<std::cell::Cell<Option<WidgetId>>>,
7427 }
7428 impl Widget for StableChildHost {
7429 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
7430 let id = match self.child {
7431 Some(id) => id,
7432 None => {
7433 let id = ctx.add(FillWidget::new());
7434 self.child = Some(id);
7435 self.probe.set(Some(id));
7436 id
7437 }
7438 };
7439 vec![id]
7440 }
7441 fn layout_response(
7442 &self,
7443 p: SizeProposal,
7444 _c: &LayoutContext,
7445 ) -> crate::widget::LayoutResponse {
7446 p.resolve(10.0, 10.0).into()
7447 }
7448 fn preserves_children_on_rebuild(&self) -> bool {
7449 true
7450 }
7451 }
7452
7453 #[test]
7454 fn reconcile_preserves_reattached_child() {
7455 let probe = std::rc::Rc::new(std::cell::Cell::new(None));
7456 let mut tree = WidgetTree::new();
7457 let host = tree.add(StableChildHost {
7458 child: None,
7459 probe: probe.clone(),
7460 });
7461 tree.layout(SizeProposal::exact(100.0, 100.0));
7462 let child = probe.get().expect("child mounted");
7463 let total0 = tree.arena.len();
7464 for _ in 0..5 {
7465 tree.arena_mark_needs_rebuild_for_testing(host);
7466 tree.layout(SizeProposal::exact(100.0, 100.0));
7467 }
7468 assert!(
7469 tree.arena.is_active(child),
7470 "the re-attached child must survive every rebuild"
7471 );
7472 assert_eq!(tree.arena.len(), total0, "no growth — same child reused");
7473 }
7474
7475 #[derive(Debug)]
7477 struct Wrapper {
7478 child: WidgetId,
7479 }
7480 impl Widget for Wrapper {
7481 fn build(&mut self, _ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
7482 vec![self.child]
7483 }
7484 fn layout_response(
7485 &self,
7486 p: SizeProposal,
7487 _c: &LayoutContext,
7488 ) -> crate::widget::LayoutResponse {
7489 p.resolve(10.0, 10.0).into()
7490 }
7491 }
7492
7493 #[derive(Debug)]
7497 struct ReparentHost {
7498 body: Option<WidgetId>,
7499 probe: std::rc::Rc<std::cell::Cell<Option<WidgetId>>>,
7500 }
7501 impl Widget for ReparentHost {
7502 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
7503 let body = match self.body {
7504 Some(id) => id,
7505 None => {
7506 let id = ctx.add(FillWidget::new());
7507 self.body = Some(id);
7508 self.probe.set(Some(id));
7509 id
7510 }
7511 };
7512 vec![ctx.add(Wrapper { child: body })]
7513 }
7514 fn layout_response(
7515 &self,
7516 p: SizeProposal,
7517 _c: &LayoutContext,
7518 ) -> crate::widget::LayoutResponse {
7519 p.resolve(10.0, 10.0).into()
7520 }
7521 fn preserves_children_on_rebuild(&self) -> bool {
7522 true
7523 }
7524 }
7525
7526 #[test]
7527 fn reconcile_spares_reparented_survivor() {
7528 let probe = std::rc::Rc::new(std::cell::Cell::new(None));
7535 let mut tree = WidgetTree::new();
7536 let host = tree.add(ReparentHost {
7537 body: None,
7538 probe: probe.clone(),
7539 });
7540 tree.layout(SizeProposal::exact(100.0, 100.0));
7541 let body = probe.get().expect("body mounted");
7542 let total0 = tree.arena.len();
7543 for _ in 0..5 {
7544 tree.arena_mark_needs_rebuild_for_testing(host);
7545 tree.layout(SizeProposal::exact(100.0, 100.0));
7546 }
7547 assert!(
7548 tree.arena.is_active(body),
7549 "the re-parented body must survive — it was moved into the new tree, \
7550 not swept with the dropped wrapper"
7551 );
7552 assert_eq!(
7553 tree.arena.len(),
7554 total0,
7555 "dropped wrappers reaped — no per-rebuild leak"
7556 );
7557 }
7558
7559 #[derive(Debug)]
7569 struct BelowContainer {
7570 child: Option<WidgetId>,
7571 offset: f32,
7572 }
7573
7574 impl crate::widget::Widget for BelowContainer {
7575 fn layout_response(
7576 &self,
7577 proposal: SizeProposal,
7578 _ctx: &crate::widget::LayoutContext,
7579 ) -> crate::widget::LayoutResponse {
7580 proposal.resolve(0.0, 0.0).into()
7581 }
7582 fn place_children(
7583 &self,
7584 bounds: Rect,
7585 _proposal: SizeProposal,
7586 children: &mut [crate::widget::WidgetPlacement],
7587 _ctx: &crate::widget::LayoutContext,
7588 ) {
7589 for c in children.iter_mut() {
7590 c.origin = Point::new(bounds.x, bounds.y + self.offset);
7591 c.size = bounds.size();
7592 }
7593 }
7594 fn children(&self) -> Vec<WidgetId> {
7595 self.child.into_iter().collect()
7596 }
7597 }
7598
7599 fn recording_scroll_container(
7602 tree: &mut WidgetTree,
7603 child: WidgetId,
7604 recorded: std::rc::Rc<std::cell::Cell<Option<Rect>>>,
7605 ) -> WidgetId {
7606 use crate::test_widgets::StackWidget;
7607 tree.add(
7608 StackWidget::new()
7609 .child(child)
7610 .on_scroll(move |ev, _ctx| match ev {
7611 WidgetEvent::ScrollIntoView { target_bounds, .. } => {
7612 recorded.set(Some(*target_bounds));
7613 EventResponse::Handled
7614 }
7615 _ => EventResponse::Ignored,
7616 })
7617 .clips_children(true),
7618 )
7619 }
7620
7621 #[test]
7622 fn ensure_visible_dispatches_scroll_into_view_to_clipping_ancestor() {
7623 use std::cell::Cell;
7624 use std::rc::Rc;
7625 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
7626 let mut tree = WidgetTree::new();
7627 let actor = tree.add(FillWidget::new());
7628 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
7629 tree.layout(SizeProposal::exact(100.0, 100.0));
7630
7631 let target = Rect::new(10.0, 500.0, 20.0, 15.0);
7634 let mut ctx = EventContext::new();
7635 ctx.ensure_visible(target);
7636 tree.collect_from_ctx(ctx, actor);
7637
7638 assert_eq!(
7639 recorded.get(),
7640 Some(target),
7641 "ensure_visible(rect) must dispatch ScrollIntoView with the exact rect \
7642 to the clips_children ancestor"
7643 );
7644 }
7645
7646 #[test]
7647 fn ensure_visible_is_noop_when_rect_already_visible() {
7648 use std::cell::Cell;
7649 use std::rc::Rc;
7650 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
7651 let mut tree = WidgetTree::new();
7652 let actor = tree.add(FillWidget::new());
7653 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
7654 tree.layout(SizeProposal::exact(100.0, 100.0));
7655
7656 let mut ctx = EventContext::new();
7659 ctx.ensure_visible(Rect::new(10.0, 10.0, 20.0, 15.0));
7660 tree.collect_from_ctx(ctx, actor);
7661
7662 assert_eq!(
7663 recorded.get(),
7664 None,
7665 "a rect already inside the viewport must not trigger a scroll"
7666 );
7667 }
7668
7669 #[test]
7670 fn ensure_visible_margin_forces_scroll_near_edge() {
7671 use std::cell::Cell;
7672 use std::rc::Rc;
7673 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
7674 let mut tree = WidgetTree::new();
7675 let actor = tree.add(FillWidget::new());
7676 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
7677 tree.layout(SizeProposal::exact(100.0, 100.0));
7678
7679 let rect = Rect::new(10.0, 95.0, 20.0, 4.0);
7682 let mut ctx = EventContext::new();
7683 ctx.ensure_visible_with_margin(rect, 10.0);
7684 tree.collect_from_ctx(ctx, actor);
7685
7686 assert_eq!(
7687 recorded.get(),
7688 Some(rect),
7689 "the margin must widen the visibility test so a near-edge rect scrolls"
7690 );
7691 }
7692
7693 fn recording_align_container(
7696 tree: &mut WidgetTree,
7697 child: WidgetId,
7698 recorded: std::rc::Rc<
7699 std::cell::Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>,
7700 >,
7701 ) -> WidgetId {
7702 use crate::test_widgets::StackWidget;
7703 tree.add(
7704 StackWidget::new()
7705 .child(child)
7706 .on_scroll(move |ev, _ctx| match ev {
7707 WidgetEvent::ScrollIntoView { align, motion, .. } => {
7708 recorded.set(Some((*align, *motion)));
7709 EventResponse::Handled
7710 }
7711 _ => EventResponse::Ignored,
7712 })
7713 .clips_children(true),
7714 )
7715 }
7716
7717 #[test]
7718 fn ensure_visible_aligned_scrolls_even_when_already_visible() {
7719 use std::cell::Cell;
7720 use std::rc::Rc;
7721 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
7722 let mut tree = WidgetTree::new();
7723 let actor = tree.add(FillWidget::new());
7724 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
7725 tree.layout(SizeProposal::exact(100.0, 100.0));
7726
7727 let target = Rect::new(10.0, 10.0, 20.0, 15.0);
7732 let mut ctx = EventContext::new();
7733 ctx.ensure_visible_aligned(target, 0.5, crate::event::ScrollMotion::Instant);
7734 tree.collect_from_ctx(ctx, actor);
7735
7736 assert_eq!(
7737 recorded.get(),
7738 Some(target),
7739 "an aligned reveal must dispatch even when the rect is already visible"
7740 );
7741 }
7742
7743 #[test]
7744 fn ensure_visible_aligned_forwards_fraction_and_motion() {
7745 use std::cell::Cell;
7746 use std::rc::Rc;
7747 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
7748 Rc::new(Cell::new(None));
7749 let mut tree = WidgetTree::new();
7750 let actor = tree.add(FillWidget::new());
7751 let _container = recording_align_container(&mut tree, actor, recorded.clone());
7752 tree.layout(SizeProposal::exact(100.0, 100.0));
7753
7754 let mut ctx = EventContext::new();
7755 ctx.ensure_visible_aligned(
7756 Rect::new(10.0, 10.0, 20.0, 15.0),
7757 0.25,
7758 crate::event::ScrollMotion::Smooth,
7759 );
7760 tree.collect_from_ctx(ctx, actor);
7761
7762 assert_eq!(
7763 recorded.get(),
7764 Some((
7765 crate::event::ScrollAlign::Fraction(0.25),
7766 crate::event::ScrollMotion::Smooth
7767 )),
7768 "the container must receive the requested fraction and motion verbatim"
7769 );
7770 }
7771
7772 #[test]
7773 fn ensure_visible_aligned_clamps_the_fraction() {
7774 use std::cell::Cell;
7775 use std::rc::Rc;
7776 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
7777 Rc::new(Cell::new(None));
7778 let mut tree = WidgetTree::new();
7779 let actor = tree.add(FillWidget::new());
7780 let _container = recording_align_container(&mut tree, actor, recorded.clone());
7781 tree.layout(SizeProposal::exact(100.0, 100.0));
7782
7783 let mut ctx = EventContext::new();
7784 ctx.ensure_visible_aligned(
7785 Rect::new(10.0, 10.0, 20.0, 15.0),
7786 4.2,
7787 crate::event::ScrollMotion::Instant,
7788 );
7789 tree.collect_from_ctx(ctx, actor);
7790
7791 assert_eq!(
7792 recorded.get().map(|(a, _)| a),
7793 Some(crate::event::ScrollAlign::Fraction(1.0)),
7794 "an out-of-range fraction must clamp rather than aim the pin off-screen"
7795 );
7796 }
7797
7798 #[test]
7799 fn plain_ensure_visible_requests_minimal_alignment() {
7800 use std::cell::Cell;
7801 use std::rc::Rc;
7802 let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
7803 Rc::new(Cell::new(None));
7804 let mut tree = WidgetTree::new();
7805 let actor = tree.add(FillWidget::new());
7806 let _container = recording_align_container(&mut tree, actor, recorded.clone());
7807 tree.layout(SizeProposal::exact(100.0, 100.0));
7808
7809 let mut ctx = EventContext::new();
7810 ctx.ensure_visible(Rect::new(10.0, 500.0, 20.0, 15.0));
7811 tree.collect_from_ctx(ctx, actor);
7812
7813 assert_eq!(
7814 recorded.get(),
7815 Some((
7816 crate::event::ScrollAlign::Minimal,
7817 crate::event::ScrollMotion::Instant
7818 )),
7819 "the pre-existing reveal API must keep its exact semantics"
7820 );
7821 }
7822
7823 #[test]
7824 fn only_the_innermost_container_aligns() {
7825 use std::cell::Cell;
7826 use std::rc::Rc;
7827 let inner_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
7828 Rc::new(Cell::new(None));
7829 let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
7830 Rc::new(Cell::new(None));
7831
7832 let mut tree = WidgetTree::new();
7833 let actor = tree.add(FillWidget::new());
7834 let inner = recording_align_container(&mut tree, actor, inner_rec.clone());
7835 let _outer = recording_align_container(&mut tree, inner, outer_rec.clone());
7836 tree.layout(SizeProposal::exact(100.0, 100.0));
7837
7838 let mut ctx = EventContext::new();
7841 ctx.ensure_visible_aligned(
7842 Rect::new(10.0, 500.0, 20.0, 15.0),
7843 0.5,
7844 crate::event::ScrollMotion::Instant,
7845 );
7846 tree.collect_from_ctx(ctx, actor);
7847
7848 assert_eq!(
7849 inner_rec.get().map(|(a, _)| a),
7850 Some(crate::event::ScrollAlign::Fraction(0.5)),
7851 "the innermost clipping ancestor owns the pin"
7852 );
7853 assert_eq!(
7854 outer_rec.get().map(|(a, _)| a),
7855 Some(crate::event::ScrollAlign::Minimal),
7856 "an outer container must only bring the inner viewport into view — a \
7857 fraction names a height in one viewport, not in every ancestor's"
7858 );
7859 }
7860
7861 #[test]
7862 fn a_clipper_that_does_not_scroll_passes_the_pin_on() {
7863 use crate::test_widgets::StackWidget;
7870 use std::cell::Cell;
7871 use std::rc::Rc;
7872 let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
7873 Rc::new(Cell::new(None));
7874
7875 let mut tree = WidgetTree::new();
7876 let actor = tree.add(FillWidget::new());
7877 let clipper = tree.add(StackWidget::new().child(actor).clips_children_on(true));
7878 let _scroller = recording_align_container(&mut tree, clipper, outer_rec.clone());
7879 tree.layout(SizeProposal::exact(100.0, 100.0));
7880
7881 let mut ctx = EventContext::new();
7884 ctx.ensure_visible_aligned(
7885 Rect::new(10.0, 10.0, 20.0, 15.0),
7886 0.5,
7887 crate::event::ScrollMotion::Instant,
7888 );
7889 tree.collect_from_ctx(ctx, actor);
7890
7891 assert_eq!(
7892 outer_rec.get().map(|(a, _)| a),
7893 Some(crate::event::ScrollAlign::Fraction(0.5)),
7894 "a clipping wrapper with no scroll handler must pass the pin through \
7895 to the scroll container above it"
7896 );
7897 }
7898
7899 #[test]
7900 fn a_scroller_that_declines_to_move_still_holds_the_pin() {
7901 use crate::test_widgets::StackWidget;
7906 use std::cell::Cell;
7907 use std::rc::Rc;
7908 let inner_rec: Rc<Cell<Option<crate::event::ScrollAlign>>> = Rc::new(Cell::new(None));
7909 let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
7910 Rc::new(Cell::new(None));
7911
7912 let mut tree = WidgetTree::new();
7913 let actor = tree.add(FillWidget::new());
7914 let rec = inner_rec.clone();
7915 let inner = tree.add(
7916 StackWidget::new()
7917 .child(actor)
7918 .on_scroll(move |ev, _ctx| match ev {
7919 WidgetEvent::ScrollIntoView { align, .. } => {
7920 rec.set(Some(*align));
7921 EventResponse::Ignored
7922 }
7923 _ => EventResponse::Ignored,
7924 })
7925 .clips_children(true),
7926 );
7927 let _outer = recording_align_container(&mut tree, inner, outer_rec.clone());
7928 tree.layout(SizeProposal::exact(100.0, 100.0));
7929
7930 let mut ctx = EventContext::new();
7932 ctx.ensure_visible_aligned(
7933 Rect::new(10.0, 500.0, 20.0, 15.0),
7934 0.5,
7935 crate::event::ScrollMotion::Instant,
7936 );
7937 tree.collect_from_ctx(ctx, actor);
7938
7939 assert_eq!(
7940 inner_rec.get(),
7941 Some(crate::event::ScrollAlign::Fraction(0.5)),
7942 "the innermost scroller is offered the pin"
7943 );
7944 assert_eq!(
7945 outer_rec.get().map(|(a, _)| a),
7946 Some(crate::event::ScrollAlign::Minimal),
7947 "declining to move must not pass the pin outward"
7948 );
7949 }
7950
7951 #[test]
7952 fn ensure_widget_visible_uses_target_arena_bounds() {
7953 use std::cell::Cell;
7954 use std::rc::Rc;
7955 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
7956 let mut tree = WidgetTree::new();
7957 let target = tree.add(FillWidget::new());
7959 let rec = recorded.clone();
7960 let container = tree.add(
7961 BelowContainer {
7962 child: Some(target),
7963 offset: 500.0,
7964 }
7965 .on_scroll(move |ev, _ctx| match ev {
7966 WidgetEvent::ScrollIntoView { target_bounds, .. } => {
7967 rec.set(Some(*target_bounds));
7968 EventResponse::Handled
7969 }
7970 _ => EventResponse::Ignored,
7971 })
7972 .clips_children(true),
7973 );
7974 tree.layout(SizeProposal::exact(100.0, 100.0));
7975
7976 let expected = tree.bounds(target);
7977 assert!(
7978 expected.y > 100.0,
7979 "fixture sanity: the target must sit below the viewport (y={})",
7980 expected.y
7981 );
7982
7983 let mut ctx = EventContext::new();
7986 ctx.ensure_widget_visible(target);
7987 tree.collect_from_ctx(ctx, container);
7988
7989 assert_eq!(
7990 recorded.get(),
7991 Some(expected),
7992 "ensure_widget_visible(id) must dispatch ScrollIntoView with the \
7993 target's current arena bounds"
7994 );
7995 }
7996
7997 #[test]
7998 fn ensure_widget_visible_ignores_missing_widget() {
7999 use std::cell::Cell;
8001 use std::rc::Rc;
8002 let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
8003 let mut tree = WidgetTree::new();
8004 let actor = tree.add(FillWidget::new());
8005 let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
8006 tree.layout(SizeProposal::exact(100.0, 100.0));
8007
8008 let mut ctx = EventContext::new();
8009 ctx.ensure_widget_visible(WidgetId::default());
8010 tree.collect_from_ctx(ctx, actor); assert_eq!(
8013 recorded.get(),
8014 None,
8015 "an unmounted id must not trigger a scroll"
8016 );
8017 }
8018
8019 #[test]
8020 fn context_menu_inside_a_modal_keeps_the_modal() {
8021 use crate::event::{Modifiers, PointerButton, WidgetEvent};
8025 use crate::overlay::{DismissBehavior, OverlayPlacement, OverlayRequest};
8026 use crate::test_widgets::{FillWidget, StackWidget};
8027
8028 let mut tree = WidgetTree::new();
8029 let modal_content = tree.add(StackWidget::new());
8032 let _editor = tree.add_child(
8033 modal_content,
8034 FillWidget::new()
8035 .context_menu(|_pos, _ctx| Some(Box::new(FillWidget::new()) as Box<dyn Widget>)),
8036 );
8037 tree.layout(SizeProposal::exact(200.0, 100.0));
8038
8039 let modal = tree.overlay_manager.show(OverlayRequest::new(
8040 modal_content,
8041 modal_content,
8042 OverlayPlacement::Centered,
8043 DismissBehavior::EscapeKey,
8044 ));
8045 tree.overlay_manager
8047 .stack
8048 .iter_mut()
8049 .find(|o| o.id == modal)
8050 .unwrap()
8051 .bounds = Rect::new(0.0, 0.0, 200.0, 100.0);
8052 assert_eq!(tree.overlay_manager.len(), 1);
8053
8054 tree.dispatch_event(WidgetEvent::pointer_down(
8056 Point::new(50.0, 25.0),
8057 PointerButton::Secondary,
8058 Modifiers::NONE,
8059 ));
8060
8061 assert!(
8062 tree.overlay_manager.active_ids().contains(&modal),
8063 "the modal must survive opening a context menu inside it"
8064 );
8065 assert_eq!(
8066 tree.overlay_manager.len(),
8067 2,
8068 "the context menu should now be open on top of the surviving modal"
8069 );
8070 }
8071}
8072
8073#[cfg(test)]
8076mod input_ingress_tests {
8077 use super::*;
8078 use crate::event::{EventResponse, Modifiers, PointerButton, ScrollDelta};
8079 use crate::pointer::{
8080 EventTime, PointerId, PointerInfo, PointerPhase, PointerSample, ScrollPhase, ScrollSample,
8081 ScrollSource,
8082 };
8083 use crate::test_widgets::FillWidget;
8084 use crate::widget::{LayoutContext, WidgetPlacement};
8085 use crate::widget_builder::WidgetBuilder;
8086 use std::cell::RefCell;
8087 use std::rc::Rc;
8088
8089 #[derive(Debug)]
8096 struct RowWidget {
8097 children: Vec<WidgetId>,
8098 }
8099
8100 impl crate::widget::Widget for RowWidget {
8101 fn layout_response(
8102 &self,
8103 proposal: SizeProposal,
8104 _ctx: &LayoutContext,
8105 ) -> crate::widget::LayoutResponse {
8106 proposal.resolve(0.0, 0.0).into()
8107 }
8108
8109 fn place_children(
8110 &self,
8111 bounds: Rect,
8112 _proposal: SizeProposal,
8113 children: &mut [WidgetPlacement],
8114 _ctx: &LayoutContext,
8115 ) {
8116 let n = children.len().max(1) as f32;
8117 let w = bounds.width / n;
8118 for (i, child) in children.iter_mut().enumerate() {
8119 child.origin = Point::new(bounds.x + w * i as f32, bounds.y);
8120 child.size = teksilo_canvas::Size::new(w, bounds.height);
8121 }
8122 }
8123
8124 fn children(&self) -> Vec<WidgetId> {
8125 self.children.clone()
8126 }
8127 }
8128
8129 fn record(drive: impl FnOnce(&mut WidgetTree)) -> Vec<String> {
8132 let log: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(Vec::new()));
8133
8134 let mut tree = WidgetTree::new();
8135 let pointer_log = log.clone();
8136 let hover_log = log.clone();
8137 tree.add(
8138 FillWidget::new()
8139 .on_pointer_event(move |event, _ctx| {
8140 pointer_log.borrow_mut().push(format!("{event:?}"));
8141 EventResponse::Ignored
8142 })
8143 .on_hover(move |entered, _ctx| {
8144 hover_log.borrow_mut().push(format!("hover({entered})"));
8145 }),
8146 );
8147 tree.layout(SizeProposal::exact(100.0, 100.0));
8148
8149 drive(&mut tree);
8150 log.borrow().clone()
8151 }
8152
8153 #[test]
8159 fn a_mouse_sample_reproduces_todays_event_stream() {
8160 let inside = Point::new(40.0, 40.0);
8161 let moved = Point::new(60.0, 55.0);
8162 let outside = Point::new(400.0, 400.0);
8163
8164 let legacy = record(|tree| {
8165 tree.dispatch_event(WidgetEvent::pointer_move(inside));
8166 tree.dispatch_event(WidgetEvent::pointer_down(
8167 inside,
8168 PointerButton::Primary,
8169 Modifiers::NONE,
8170 ));
8171 tree.dispatch_event(WidgetEvent::pointer_move(moved));
8172 tree.dispatch_event(WidgetEvent::pointer_up(
8173 moved,
8174 PointerButton::Primary,
8175 Modifiers::NONE,
8176 ));
8177 tree.dispatch_event(WidgetEvent::pointer_move(outside));
8178 });
8179
8180 let sampled = record(|tree| {
8181 let t = EventTime::ZERO;
8182 tree.dispatch_pointer(PointerSample::mouse(PointerPhase::Move, inside, t));
8183 tree.dispatch_pointer(
8184 PointerSample::mouse(PointerPhase::Down, inside, t)
8185 .with_button(PointerButton::Primary),
8186 );
8187 tree.dispatch_pointer(PointerSample::mouse(PointerPhase::Move, moved, t));
8188 tree.dispatch_pointer(
8189 PointerSample::mouse(PointerPhase::Up, moved, t)
8190 .with_button(PointerButton::Primary),
8191 );
8192 tree.dispatch_pointer(PointerSample::mouse(PointerPhase::Move, outside, t));
8193 });
8194
8195 assert_eq!(legacy, sampled);
8196 assert!(
8197 legacy.contains(&"hover(true)".to_string())
8198 && legacy.contains(&"hover(false)".to_string()),
8199 "the fixture must actually exercise enter and leave: {legacy:?}"
8200 );
8201 }
8202
8203 #[test]
8207 fn a_buttonless_press_lowers_to_primary() {
8208 let events = record(|tree| {
8209 tree.dispatch_pointer(PointerSample::mouse(
8210 PointerPhase::Down,
8211 Point::new(20.0, 20.0),
8212 EventTime::ZERO,
8213 ));
8214 });
8215 assert!(
8216 events.iter().any(|e| e.contains("button: Primary")),
8217 "{events:?}"
8218 );
8219 }
8220
8221 #[test]
8227 fn a_sampled_press_and_release_form_a_tap() {
8228 let taps = Rc::new(std::cell::Cell::new(0));
8229 let seen = taps.clone();
8230 let mut tree = WidgetTree::new();
8231 tree.add(FillWidget::new().on_tap(move |_event, _ctx| seen.set(seen.get() + 1)));
8232 tree.layout(SizeProposal::exact(100.0, 100.0));
8233
8234 let at = Point::new(50.0, 50.0);
8235 tree.dispatch_pointer(PointerSample::mouse(
8236 PointerPhase::Down,
8237 at,
8238 EventTime::from_millis(0),
8239 ));
8240 tree.dispatch_pointer(PointerSample::mouse(
8241 PointerPhase::Up,
8242 at,
8243 EventTime::from_millis(5),
8244 ));
8245
8246 assert_eq!(taps.get(), 1);
8247 }
8248
8249 fn scroll_fixture() -> (WidgetTree, Rc<RefCell<Vec<&'static str>>>) {
8254 let hits: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
8255 let mut tree = WidgetTree::new();
8256
8257 let leading_hits = hits.clone();
8258 let trailing_hits = hits.clone();
8259 let leading = tree.add(FillWidget::new().on_scroll(move |_event, _ctx| {
8260 leading_hits.borrow_mut().push("leading");
8261 EventResponse::Handled
8262 }));
8263 let trailing = tree.add(FillWidget::new().on_scroll(move |_event, _ctx| {
8264 trailing_hits.borrow_mut().push("trailing");
8265 EventResponse::Handled
8266 }));
8267 tree.add(RowWidget {
8268 children: vec![leading, trailing],
8269 });
8270 tree.layout(SizeProposal::exact(200.0, 100.0));
8271 (tree, hits)
8272 }
8273
8274 fn notch() -> ScrollDelta {
8275 ScrollDelta::Lines { x: 0.0, y: -1.0 }
8276 }
8277
8278 #[test]
8281 fn a_positionless_scroll_routes_by_hover() {
8282 let (mut tree, hits) = scroll_fixture();
8283
8284 tree.pointer_move(Point::new(150.0, 50.0)); tree.dispatch_event(WidgetEvent::scroll(notch(), Modifiers::NONE));
8286 assert_eq!(*hits.borrow(), vec!["trailing"]);
8287
8288 tree.pointer_move(Point::new(50.0, 50.0)); tree.dispatch_scroll(ScrollSample::wheel(
8290 notch(),
8291 Modifiers::NONE,
8292 EventTime::ZERO,
8293 ));
8294 assert_eq!(*hits.borrow(), vec!["trailing", "leading"]);
8295 }
8296
8297 #[test]
8302 fn a_positioned_scroll_routes_by_hit_test() {
8303 let (mut tree, hits) = scroll_fixture();
8304 tree.pointer_move(Point::new(150.0, 50.0)); tree.dispatch_event(WidgetEvent::scroll_at(
8307 notch(),
8308 Modifiers::NONE,
8309 Point::new(50.0, 50.0), ));
8311 assert_eq!(*hits.borrow(), vec!["leading"]);
8312
8313 tree.dispatch_scroll(
8314 ScrollSample::wheel(notch(), Modifiers::NONE, EventTime::ZERO)
8315 .at(Point::new(150.0, 50.0)),
8316 );
8317 assert_eq!(*hits.borrow(), vec!["leading", "trailing"]);
8318 }
8319
8320 #[test]
8324 fn a_positionless_scroll_with_no_hover_goes_nowhere() {
8325 let (mut tree, hits) = scroll_fixture();
8326 tree.dispatch_event(WidgetEvent::scroll(notch(), Modifiers::NONE));
8327 assert!(hits.borrow().is_empty());
8328 }
8329
8330 #[test]
8335 fn a_handler_sees_the_sample_it_is_serving() {
8336 let seen: Rc<RefCell<Vec<(ScrollPhase, ScrollSource, Option<Point>)>>> =
8337 Rc::new(RefCell::new(Vec::new()));
8338 let sink = seen.clone();
8339
8340 let mut tree = WidgetTree::new();
8341 tree.add(
8348 FillWidget::new()
8349 .scroll_container(crate::pointer::touch_action::PanAxes::BOTH)
8350 .on_scroll(move |_event, ctx| {
8351 sink.borrow_mut().push((
8352 ctx.scroll_phase(),
8353 ctx.scroll_source(),
8354 ctx.pointer_position(),
8355 ));
8356 EventResponse::Handled
8357 }),
8358 );
8359 tree.layout(SizeProposal::exact(100.0, 100.0));
8360
8361 tree.dispatch_scroll(ScrollSample {
8362 delta: notch(),
8363 position: Some(Point::new(50.0, 50.0)),
8364 phase: ScrollPhase::Momentum,
8365 source: ScrollSource::TouchPan,
8366 pointer: PointerInfo::mouse(EventTime::from_millis(12)),
8367 modifiers: Modifiers::NONE,
8368 });
8369
8370 assert_eq!(
8371 *seen.borrow(),
8372 vec![(
8373 ScrollPhase::Momentum,
8374 ScrollSource::TouchPan,
8375 Some(Point::new(50.0, 50.0))
8376 )]
8377 );
8378 }
8379
8380 #[test]
8383 fn a_legacy_event_reports_the_mouse_at_the_epoch() {
8384 let seen: Rc<RefCell<Option<(PointerId, ScrollPhase)>>> = Rc::new(RefCell::new(None));
8385 let sink = seen.clone();
8386
8387 let mut tree = WidgetTree::new();
8388 tree.add(FillWidget::new().on_pointer_event(move |_event, ctx| {
8389 *sink.borrow_mut() = Some((ctx.pointer().id, ctx.scroll_phase()));
8390 EventResponse::Ignored
8391 }));
8392 tree.layout(SizeProposal::exact(100.0, 100.0));
8393 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(50.0, 50.0)));
8394
8395 assert_eq!(
8396 *seen.borrow(),
8397 Some((PointerId::MOUSE, ScrollPhase::Discrete))
8398 );
8399 }
8400
8401 #[test]
8405 fn the_snapshot_is_restored_after_a_dispatch() {
8406 let mut tree = WidgetTree::new();
8407 tree.add(FillWidget::new());
8408 tree.layout(SizeProposal::exact(100.0, 100.0));
8409
8410 let before = tree.current_input.clone();
8411 tree.dispatch_scroll(
8412 ScrollSample::wheel(notch(), Modifiers::NONE, EventTime::ZERO)
8413 .at(Point::new(10.0, 10.0)),
8414 );
8415 assert_eq!(tree.current_input, before);
8416 }
8417}
8418
8419#[cfg(test)]
8420mod press_and_focus_tests {
8421 use std::cell::{Cell, RefCell};
8426 use std::rc::Rc;
8427 use teksilo_canvas::{Point, SizeProposal};
8428
8429 use crate::WidgetId;
8430 use crate::event::{EventResponse, Key, Modifiers, PointerButton, WidgetEvent};
8431 use crate::focus::FocusOrigin;
8432 use crate::pointer::clock::ManualClock;
8433 use crate::pointer::touch_action::PanClaim;
8434 use crate::pointer::{
8435 BackendDeviceKey, EventTime, PointerId, PointerIdAllocator, PointerInfo, PointerPhase,
8436 PointerSample,
8437 };
8438 use crate::test_widgets::{FillWidget, StackWidget};
8439 use crate::widget_builder::WidgetBuilder;
8440 use crate::widget_tree::WidgetTree;
8441
8442 fn contact_id() -> PointerId {
8448 use std::sync::atomic::{AtomicU64, Ordering};
8449 static NEXT: AtomicU64 = AtomicU64::new(1);
8450 PointerIdAllocator::global().begin(
8451 BackendDeviceKey::new(0x0B11),
8452 NEXT.fetch_add(1, Ordering::Relaxed),
8453 )
8454 }
8455
8456 fn contact(id: PointerId, phase: PointerPhase, at: Point, t: EventTime) -> PointerSample {
8457 PointerSample {
8458 pointer: PointerInfo::touch(id, t),
8459 phase,
8460 position: at,
8461 button: None,
8462 modifiers: Modifiers::NONE,
8463 coalesced: Vec::new(),
8464 }
8465 }
8466
8467 fn tree_on_a_clock() -> (WidgetTree, Rc<ManualClock>) {
8470 let mut tree = WidgetTree::new();
8471 let clock = Rc::new(ManualClock::new(EventTime::ZERO));
8472 tree.set_input_clock(clock.clone());
8473 (tree, clock)
8474 }
8475
8476 fn tappable(tree: &mut WidgetTree) -> (WidgetId, crate::signal::Signal<bool>) {
8483 let id = tree.add(FillWidget::new().focusable().on_tap(|_e, _c| {}));
8484 let signal = tree.pressed_signal(id);
8485 (id, signal)
8486 }
8487
8488 #[test]
8495 fn a_pointer_origin_names_its_device() {
8496 use teksilo_tokens::{PenKind, PointerKind};
8497
8498 assert!(FocusOrigin::Pointer(PointerKind::Touch).is_pointer());
8499 assert!(FocusOrigin::Pointer(PointerKind::Pen(PenKind::Pen)).is_pointer());
8500 assert!(FocusOrigin::POINTER.is_pointer());
8501 assert!(!FocusOrigin::Keyboard.is_pointer());
8502 assert!(!FocusOrigin::Programmatic.is_pointer());
8503 assert!(!FocusOrigin::Accessibility.is_pointer());
8504
8505 assert_eq!(
8506 FocusOrigin::Pointer(PointerKind::Touch).pointer_kind(),
8507 Some(PointerKind::Touch),
8508 );
8509 assert_eq!(FocusOrigin::Keyboard.pointer_kind(), None);
8510 assert_eq!(
8511 FocusOrigin::POINTER.pointer_kind(),
8512 Some(PointerKind::Unknown),
8513 "a widget deriving its own origin says so rather than naming a device it never saw",
8514 );
8515 }
8516
8517 #[test]
8521 fn only_a_real_navigation_declares_a_modality() {
8522 use teksilo_tokens::PointerKind;
8523
8524 assert_eq!(FocusOrigin::Keyboard.focus_visible(), Some(true));
8525 assert_eq!(FocusOrigin::Accessibility.focus_visible(), Some(true));
8526 assert_eq!(
8527 FocusOrigin::Pointer(PointerKind::Touch).focus_visible(),
8528 Some(false),
8529 );
8530 assert_eq!(FocusOrigin::Programmatic.focus_visible(), None);
8531 }
8532
8533 #[test]
8536 fn the_tier_three_configs_are_unchanged() {
8537 let origin: crate::signal::Signal<Option<FocusOrigin>> =
8538 crate::signal::Signal::new(Some(FocusOrigin::Keyboard));
8539 let slider: crate::signal::Signal<Option<FocusOrigin>> = origin.clone();
8540 let splitter: crate::signal::Signal<Option<FocusOrigin>> = origin.clone();
8541 let segmented: crate::signal::Signal<Option<FocusOrigin>> = origin.clone();
8542 for field in [slider, splitter, segmented] {
8543 assert_eq!(field.get(), Some(FocusOrigin::Keyboard));
8544 }
8545 origin.set(Some(FocusOrigin::POINTER));
8546 assert_ne!(
8547 origin.get(),
8548 Some(FocusOrigin::Keyboard),
8549 "the `== Some(Keyboard)` test every consumer makes still discriminates",
8550 );
8551 }
8552
8553 #[test]
8564 fn a_touch_tap_after_keyboard_focus_leaves_no_ring() {
8565 let (mut tree, _clock) = tree_on_a_clock();
8566 let a = tree.add(FillWidget::new().focusable());
8567 let b = tree.add(FillWidget::new().focusable());
8568 let root = tree.add(SideBySide {
8569 children: vec![a, b],
8570 });
8571 tree.layout(SizeProposal::exact(200.0, 100.0));
8572 let _ = root;
8573
8574 let visible = tree.focus_visible_signal();
8575 tree.press_key(Key::Tab, Modifiers::NONE);
8576 assert_eq!(tree.focused(), Some(a));
8577 assert!(visible.get(), "keyboard navigation reveals the ring");
8578
8579 let id = contact_id();
8580 let at = tree.bounds(b).center();
8581 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8582 tree.dispatch_pointer(contact(
8583 id,
8584 PointerPhase::Up,
8585 at,
8586 EventTime::from_millis(30),
8587 ));
8588
8589 assert_eq!(tree.focused(), Some(b), "the release moved focus");
8590 assert!(!visible.get(), "and the ring did not come with it");
8591 assert_eq!(
8592 tree.focus_origin(),
8593 Some(FocusOrigin::Pointer(teksilo_tokens::PointerKind::Touch)),
8594 );
8595 }
8596
8597 #[test]
8602 fn an_assistive_focus_reveals_the_ring() {
8603 let mut tree = WidgetTree::new();
8604 let a = tree.add(FillWidget::new().focusable());
8605 let b = tree.add(FillWidget::new().focusable());
8606 let root = tree.add(SideBySide {
8607 children: vec![a, b],
8608 });
8609 tree.layout(SizeProposal::exact(200.0, 50.0));
8610 let _ = root;
8611 let visible = tree.focus_visible_signal();
8612
8613 tree.click(a);
8614 assert_eq!(tree.focused(), Some(a));
8615 assert!(!visible.get(), "the click hid it");
8616
8617 tree.dispatch_event(WidgetEvent::AccessAction {
8618 target: Some(b),
8619 action: accesskit::Action::Focus,
8620 target_node: crate::accessibility::widget_id_to_node_id(b),
8621 data: None,
8622 });
8623 assert_eq!(tree.focused(), Some(b));
8624 assert!(visible.get(), "an assistive move is a navigation");
8625 assert_eq!(tree.focus_origin(), Some(FocusOrigin::Accessibility));
8626 }
8627
8628 struct ItemFocusFixture {
8632 tree: WidgetTree,
8633 other: WidgetId,
8634 group: WidgetId,
8636 owner: WidgetId,
8637 refused: WidgetId,
8640 taken: WidgetId,
8641 label: WidgetId,
8643 cursor: Rc<Cell<Option<accesskit::NodeId>>>,
8644 catch_all: Rc<Cell<u32>>,
8645 }
8646
8647 fn item_focus_fixture() -> ItemFocusFixture {
8648 let advertises_focus = |b: &mut crate::accessibility::AccessNodeBuilder| {
8649 b.add_action(accesskit::Action::Focus)
8650 };
8651 let mut tree = WidgetTree::new();
8652 let other = tree.add(FillWidget::new().focusable());
8653 let refused = tree.add(FillWidget::new().access_customize(advertises_focus));
8654 let taken = tree.add(FillWidget::new().access_customize(advertises_focus));
8655 let label = tree.add(FillWidget::new());
8656 let cursor: Rc<Cell<Option<accesskit::NodeId>>> = Rc::new(Cell::new(None));
8657 let owner_slot: Rc<Cell<Option<WidgetId>>> = Rc::new(Cell::new(None));
8658 let taken_node = crate::accessibility::widget_id_to_node_id(taken);
8659 let owner = tree.add(
8660 SideBySide {
8661 children: vec![refused, taken],
8662 }
8663 .focusable(true)
8664 .on_access_action_request({
8665 let cursor = cursor.clone();
8666 let owner_slot = owner_slot.clone();
8667 move |action, node, _data, ctx| {
8668 if action != accesskit::Action::Focus || node != taken_node {
8669 return EventResponse::Ignored;
8670 }
8671 cursor.set(Some(node));
8672 if let Some(owner) = owner_slot.get() {
8673 ctx.request_focus(owner);
8674 }
8675 EventResponse::Handled
8676 }
8677 }),
8678 );
8679 owner_slot.set(Some(owner));
8680 let group = tree.add(SideBySide {
8682 children: vec![owner],
8683 });
8684 let catch_all = Rc::new(Cell::new(0));
8685 let card = tree.add(
8686 SideBySide {
8687 children: vec![group, label],
8688 }
8689 .focusable(true)
8690 .on_access_action({
8691 let catch_all = catch_all.clone();
8692 move |action, _ctx| {
8693 if action != accesskit::Action::Focus {
8694 return EventResponse::Ignored;
8695 }
8696 catch_all.set(catch_all.get() + 1);
8697 EventResponse::Handled
8698 }
8699 }),
8700 );
8701 tree.add(SideBySide {
8702 children: vec![other, card],
8703 });
8704 tree.layout(SizeProposal::exact(400.0, 50.0));
8705 ItemFocusFixture {
8706 tree,
8707 other,
8708 group,
8709 owner,
8710 refused,
8711 taken,
8712 label,
8713 cursor,
8714 catch_all,
8715 }
8716 }
8717
8718 fn assistive_focus(tree: &mut WidgetTree, id: WidgetId) -> bool {
8719 tree.dispatch_access_action(
8720 crate::accessibility::widget_id_to_node_id(id),
8721 accesskit::Action::Focus,
8722 None,
8723 &mut crate::window::NoopWindowOps,
8724 )
8725 }
8726
8727 #[test]
8734 fn an_assistive_focus_on_an_item_goes_to_the_widget_that_points_at_it() {
8735 let mut f = item_focus_fixture();
8736 let visible = f.tree.focus_visible_signal();
8737 f.tree.click(f.other);
8738 assert!(!visible.get());
8739
8740 assert!(
8741 assistive_focus(&mut f.tree, f.taken),
8742 "the widget that points at the item took it"
8743 );
8744 let item = crate::accessibility::widget_id_to_node_id(f.taken);
8745 assert_eq!(f.cursor.get(), Some(item), "its cursor moved to the item");
8746 assert_eq!(
8747 f.tree.focused(),
8748 Some(f.owner),
8749 "the keyboard is on the widget, not on the item"
8750 );
8751 assert!(visible.get(), "an assistive move is a navigation");
8752 assert_eq!(f.tree.focus_origin(), Some(FocusOrigin::Accessibility));
8753 assert_eq!(
8754 f.catch_all.get(),
8755 0,
8756 "no ancestor above the owner was asked"
8757 );
8758 }
8759
8760 #[test]
8764 fn an_item_its_owner_refuses_takes_no_assistive_focus() {
8765 let mut f = item_focus_fixture();
8766 f.tree.click(f.other);
8767 assert!(!assistive_focus(&mut f.tree, f.refused));
8768 assert_eq!(f.tree.focused(), Some(f.other), "focus did not move");
8769 assert_eq!(f.cursor.get(), None);
8770 assert_eq!(f.catch_all.get(), 0);
8771 }
8772
8773 #[test]
8776 fn a_node_that_offers_no_focus_takes_no_assistive_focus() {
8777 let mut f = item_focus_fixture();
8778 f.tree.click(f.other);
8779 assert!(!assistive_focus(&mut f.tree, f.label));
8780 assert_eq!(f.tree.focused(), Some(f.other));
8781 assert_eq!(f.catch_all.get(), 0);
8782 }
8783
8784 #[test]
8788 fn a_greyed_item_reaches_its_owner_and_a_disabled_container_takes_nothing() {
8789 let mut f = item_focus_fixture();
8790 let item_enabled = crate::signal::Signal::new(false);
8791 f.tree.enabled_when(f.taken, item_enabled.clone());
8792 f.tree.layout(SizeProposal::exact(400.0, 50.0));
8793 f.tree.click(f.other);
8794 assert!(assistive_focus(&mut f.tree, f.taken), "a greyed item");
8795 assert_eq!(f.tree.focused(), Some(f.owner));
8796
8797 let mut f = item_focus_fixture();
8798 f.tree
8799 .enabled_when(f.group, crate::signal::Signal::new(false));
8800 f.tree.layout(SizeProposal::exact(400.0, 50.0));
8801 f.tree.click(f.other);
8802 assert!(
8803 !assistive_focus(&mut f.tree, f.taken),
8804 "a disabled container"
8805 );
8806 assert_eq!(f.tree.focused(), Some(f.other));
8807 assert_eq!(f.cursor.get(), None);
8808 assert_eq!(f.catch_all.get(), 0);
8809 }
8810
8811 #[test]
8815 fn a_composite_in_a_catch_all_ancestor_gets_its_stop() {
8816 let mut tree = WidgetTree::new();
8817 let other = tree.add(FillWidget::new().focusable());
8818 let leaf = tree.add(FillWidget::new().focusable());
8819 let composite = tree.add(
8820 SideBySide {
8821 children: vec![leaf],
8822 }
8823 .access_customize(|b| b.add_action(accesskit::Action::Focus)),
8824 );
8825 let catch_all = Rc::new(Cell::new(0));
8826 let card = tree.add(
8827 SideBySide {
8828 children: vec![composite],
8829 }
8830 .focusable(true)
8831 .on_access_action({
8832 let catch_all = catch_all.clone();
8833 move |action, _ctx| {
8834 if action == accesskit::Action::Focus {
8835 catch_all.set(catch_all.get() + 1);
8836 return EventResponse::Handled;
8837 }
8838 EventResponse::Ignored
8839 }
8840 }),
8841 );
8842 tree.add(SideBySide {
8843 children: vec![other, card],
8844 });
8845 tree.layout(SizeProposal::exact(400.0, 50.0));
8846 tree.click(other);
8847 assert!(assistive_focus(&mut tree, composite));
8848 assert_eq!(tree.focused(), Some(leaf), "the keyboard is on its stop");
8849 assert_eq!(catch_all.get(), 0);
8850 }
8851
8852 #[derive(Debug)]
8855 struct SyntheticOwner {
8856 advertises: bool,
8857 }
8858
8859 impl crate::widget::Widget for SyntheticOwner {
8860 fn layout_response(
8861 &self,
8862 proposal: SizeProposal,
8863 _ctx: &crate::widget::LayoutContext,
8864 ) -> crate::widget::LayoutResponse {
8865 proposal.resolve(0.0, 0.0).into()
8866 }
8867
8868 fn accessibility(&self, builder: &mut crate::accessibility::AccessNodeBuilder) {
8869 let advertises = self.advertises;
8870 builder.push_scene_child(1, crate::accessibility::SyntheticKind::ChartMark, |child| {
8871 child.set_role(accesskit::Role::GraphicsObject);
8872 if advertises {
8873 child.add_action(accesskit::Action::Focus);
8874 }
8875 });
8876 }
8877 }
8878
8879 fn catch_all_card(tree: &mut WidgetTree, child: WidgetId) -> (WidgetId, Rc<Cell<u32>>) {
8881 let catch_all = Rc::new(Cell::new(0));
8882 let card = tree.add(
8883 SideBySide {
8884 children: vec![child],
8885 }
8886 .focusable(true)
8887 .on_access_action({
8888 let catch_all = catch_all.clone();
8889 move |action, _ctx| {
8890 if action == accesskit::Action::Focus {
8891 catch_all.set(catch_all.get() + 1);
8892 return EventResponse::Handled;
8893 }
8894 EventResponse::Ignored
8895 }
8896 }),
8897 );
8898 (card, catch_all)
8899 }
8900
8901 #[test]
8905 fn a_disabled_synthetic_owner_or_composite_offers_nothing_upward() {
8906 let mut tree = WidgetTree::new();
8907 let other = tree.add(FillWidget::new().focusable());
8908 let owner = tree.add(SyntheticOwner { advertises: true }.focusable(true));
8909 let leaf = tree.add(FillWidget::new().focusable());
8910 let composite = tree.add(
8911 SideBySide {
8912 children: vec![leaf],
8913 }
8914 .access_customize(|b| b.add_action(accesskit::Action::Focus)),
8915 );
8916 let group = tree.add(SideBySide {
8917 children: vec![owner, composite],
8918 });
8919 let (card, catch_all) = catch_all_card(&mut tree, group);
8920 tree.add(SideBySide {
8921 children: vec![other, card],
8922 });
8923 tree.layout(SizeProposal::exact(400.0, 50.0));
8924 let _ = tree.sync_accessibility();
8925 tree.enabled_when(owner, crate::signal::Signal::new(false));
8926 tree.enabled_when(composite, crate::signal::Signal::new(false));
8927 tree.layout(SizeProposal::exact(400.0, 50.0));
8928 tree.click(other);
8929
8930 let mark = crate::accessibility::synthetic_node_id(
8931 owner,
8932 1,
8933 crate::accessibility::SyntheticKind::ChartMark,
8934 );
8935 let handled = tree.dispatch_access_action(
8936 mark,
8937 accesskit::Action::Focus,
8938 None,
8939 &mut crate::window::NoopWindowOps,
8940 );
8941 assert!(!handled, "a disabled owner's item");
8942 assert!(
8943 !assistive_focus(&mut tree, composite),
8944 "a disabled composite"
8945 );
8946 assert_eq!(tree.focused(), Some(other));
8947 assert_eq!(catch_all.get(), 0, "no ancestor was asked");
8948 }
8949
8950 #[test]
8953 fn a_synthetic_node_that_offers_no_focus_is_offered_to_no_one() {
8954 let mut tree = WidgetTree::new();
8955 let other = tree.add(FillWidget::new().focusable());
8956 let owner = tree.add(SyntheticOwner { advertises: false });
8958 let (card, catch_all) = catch_all_card(&mut tree, owner);
8959 tree.add(SideBySide {
8960 children: vec![other, card],
8961 });
8962 tree.layout(SizeProposal::exact(400.0, 50.0));
8963 let _ = tree.sync_accessibility();
8964 tree.click(other);
8965 let mark = crate::accessibility::synthetic_node_id(
8966 owner,
8967 1,
8968 crate::accessibility::SyntheticKind::ChartMark,
8969 );
8970 let _ = tree.dispatch_access_action(
8971 mark,
8972 accesskit::Action::Focus,
8973 None,
8974 &mut crate::window::NoopWindowOps,
8975 );
8976 assert_eq!(catch_all.get(), 0, "no handler was asked");
8977 }
8978
8979 #[test]
8983 fn an_answer_that_moves_no_focus_is_no_success() {
8984 let mut tree = WidgetTree::new();
8985 let other = tree.add(FillWidget::new().focusable());
8986 let item = tree
8987 .add(FillWidget::new().access_customize(|b| b.add_action(accesskit::Action::Focus)));
8988 let owner = tree.add(
8989 SideBySide {
8990 children: vec![item],
8991 }
8992 .focusable(true)
8993 .on_access_action(|action, _ctx| {
8994 if action == accesskit::Action::Focus {
8995 EventResponse::Handled
8996 } else {
8997 EventResponse::Ignored
8998 }
8999 }),
9000 );
9001 tree.add(SideBySide {
9002 children: vec![other, owner],
9003 });
9004 tree.layout(SizeProposal::exact(400.0, 50.0));
9005 let visible = tree.focus_visible_signal();
9006 tree.click(other);
9007 assert!(!assistive_focus(&mut tree, item), "reported unhandled");
9008 assert_eq!(tree.focused(), Some(other));
9009 assert!(!visible.get(), "no ring on the mouse-focused control");
9010 }
9011
9012 #[test]
9017 fn a_programmatic_focus_leaves_the_modality_alone() {
9018 let mut tree = WidgetTree::new();
9019 let a = tree.add(FillWidget::new().focusable());
9020 let b = tree.add(FillWidget::new().focusable());
9021 let root = tree.add(SideBySide {
9022 children: vec![a, b],
9023 });
9024 tree.layout(SizeProposal::exact(200.0, 100.0));
9025 let _ = root;
9026 let visible = tree.focus_visible_signal();
9027
9028 tree.press_key(Key::Tab, Modifiers::NONE);
9029 assert_eq!(tree.focused(), Some(a));
9030 assert!(visible.get());
9031 tree.focus(b);
9032 assert!(
9033 visible.get(),
9034 "a scripted focus does not hide a keyboard ring"
9035 );
9036
9037 tree.click(a);
9038 assert!(!visible.get());
9039 tree.focus(b);
9040 assert!(
9041 !visible.get(),
9042 "and does not reveal one after a click either",
9043 );
9044 }
9045
9046 #[test]
9052 fn a_mouse_still_focuses_on_press() {
9053 let mut tree = WidgetTree::new();
9054 let w = tree.add(FillWidget::new().focusable());
9055 tree.layout(SizeProposal::exact(100.0, 50.0));
9056 let center = tree.bounds(w).center();
9057
9058 tree.dispatch_event(WidgetEvent::pointer_down(
9059 center,
9060 PointerButton::Primary,
9061 Modifiers::NONE,
9062 ));
9063 assert_eq!(
9064 tree.focused(),
9065 Some(w),
9066 "focus lands on the press for an indirect pointer",
9067 );
9068 }
9069
9070 #[test]
9072 fn a_finger_focuses_on_release() {
9073 let (mut tree, _clock) = tree_on_a_clock();
9074 let w = tree.add(FillWidget::new().focusable());
9075 tree.layout(SizeProposal::exact(100.0, 50.0));
9076 let center = tree.bounds(w).center();
9077
9078 let id = contact_id();
9079 tree.dispatch_pointer(contact(id, PointerPhase::Down, center, EventTime::ZERO));
9080 assert_eq!(
9081 tree.focused(),
9082 None,
9083 "a finger that has only landed has chosen nothing yet",
9084 );
9085
9086 tree.dispatch_pointer(contact(
9087 id,
9088 PointerPhase::Up,
9089 center,
9090 EventTime::from_millis(40),
9091 ));
9092 assert_eq!(tree.focused(), Some(w));
9093 assert_eq!(
9094 tree.focus_origin().and_then(FocusOrigin::pointer_kind),
9095 Some(teksilo_tokens::PointerKind::Touch),
9096 "the origin names the device that delivered it",
9097 );
9098 }
9099
9100 #[test]
9104 fn a_release_on_a_different_focusable_moves_no_focus() {
9105 let (mut tree, _clock) = tree_on_a_clock();
9106 let a = tree.add(FillWidget::new().focusable());
9107 let b = tree.add(FillWidget::new().focusable());
9108 let root = tree.add(SideBySide {
9109 children: vec![a, b],
9110 });
9111 tree.layout(SizeProposal::exact(200.0, 50.0));
9112 let _ = root;
9113
9114 let on_a = tree.bounds(a).center();
9115 let on_b = tree.bounds(b).center();
9116 let id = contact_id();
9117 tree.dispatch_pointer(contact(id, PointerPhase::Down, on_a, EventTime::ZERO));
9118 tree.dispatch_pointer(contact(
9119 id,
9120 PointerPhase::Move,
9121 on_b,
9122 EventTime::from_millis(20),
9123 ));
9124 tree.dispatch_pointer(contact(
9125 id,
9126 PointerPhase::Up,
9127 on_b,
9128 EventTime::from_millis(40),
9129 ));
9130
9131 assert_eq!(
9132 tree.focused(),
9133 None,
9134 "the guard is `the release landed on the same focusable as the press`",
9135 );
9136 }
9137
9138 #[test]
9145 fn a_mouse_press_visual_is_unchanged() {
9146 let mut tree = WidgetTree::new();
9147 let (w, pressed) = tappable(&mut tree);
9148 tree.layout(SizeProposal::exact(100.0, 50.0));
9149 let center = tree.bounds(w).center();
9150
9151 assert!(!pressed.get(), "not pressed at rest");
9152 tree.dispatch_event(WidgetEvent::pointer_down(
9153 center,
9154 PointerButton::Primary,
9155 Modifiers::NONE,
9156 ));
9157 assert!(pressed.get(), "an indirect pointer lights up on the press");
9158 assert!(
9159 !tree.press_pending(w),
9160 "and never waits: a mouse opens no pan session, so there is no \
9161 ambiguity to wait out",
9162 );
9163 assert_eq!(tree.pressed_by(w), Some(PointerId::MOUSE));
9164
9165 tree.dispatch_event(WidgetEvent::pointer_up(
9166 center,
9167 PointerButton::Primary,
9168 Modifiers::NONE,
9169 ));
9170 assert!(!pressed.get(), "the release clears it");
9171 assert_eq!(tree.pressed_by(w), None);
9172 }
9173
9174 #[test]
9178 fn a_slide_off_clears_the_visual_and_re_entry_restores_it() {
9179 let (mut tree, _clock) = tree_on_a_clock();
9180 let (w, pressed) = tappable(&mut tree);
9181 tree.layout(SizeProposal::exact(100.0, 200.0));
9182 let inside = tree.bounds(w).center();
9183 let outside = Point::new(inside.x, inside.y + 400.0);
9184
9185 let id = contact_id();
9186 tree.dispatch_pointer(contact(id, PointerPhase::Down, inside, EventTime::ZERO));
9187 assert!(
9188 pressed.get(),
9189 "nothing here claims a pan, so no delay applies"
9190 );
9191
9192 tree.dispatch_pointer(contact(
9193 id,
9194 PointerPhase::Move,
9195 outside,
9196 EventTime::from_millis(20),
9197 ));
9198 assert!(!pressed.get(), "the press has left its target");
9199 assert!(!tree.press_is_inside(w));
9200 assert_eq!(
9201 tree.pressed_by(w),
9202 Some(id),
9203 "the contact still holds the press — it is the *visual* that is off",
9204 );
9205
9206 tree.dispatch_pointer(contact(
9207 id,
9208 PointerPhase::Move,
9209 inside,
9210 EventTime::from_millis(40),
9211 ));
9212 assert!(pressed.get(), "sliding back on restores it");
9213
9214 tree.dispatch_pointer(contact(
9215 id,
9216 PointerPhase::Up,
9217 inside,
9218 EventTime::from_millis(60),
9219 ));
9220 assert!(!pressed.get());
9221 }
9222
9223 #[test]
9226 fn a_second_contact_cannot_clear_the_first_visual() {
9227 let (mut tree, _clock) = tree_on_a_clock();
9228 let (w, pressed) = tappable(&mut tree);
9229 tree.layout(SizeProposal::exact(100.0, 50.0));
9230 let bounds = tree.bounds(w);
9231 let first_at = Point::new(bounds.x + 20.0, bounds.y + 25.0);
9232 let second_at = Point::new(bounds.x + 70.0, bounds.y + 25.0);
9233
9234 let first = contact_id();
9235 let second = contact_id();
9236 tree.dispatch_pointer(contact(
9237 first,
9238 PointerPhase::Down,
9239 first_at,
9240 EventTime::ZERO,
9241 ));
9242 assert!(pressed.get());
9243 assert_eq!(tree.pressed_by(w), Some(first));
9244
9245 tree.dispatch_pointer(contact(
9246 second,
9247 PointerPhase::Down,
9248 second_at,
9249 EventTime::from_millis(10),
9250 ));
9251 assert_eq!(
9252 tree.pressed_by(w),
9253 Some(first),
9254 "under `MultiContact::First` the second contact is terminated before \
9255 it reaches the arena at all",
9256 );
9257
9258 tree.dispatch_pointer(contact(
9259 second,
9260 PointerPhase::Up,
9261 second_at,
9262 EventTime::from_millis(20),
9263 ));
9264 assert!(
9265 pressed.get(),
9266 "so the second contact's release cannot clear a visual it never owned",
9267 );
9268 assert_eq!(tree.pressed_by(w), Some(first));
9269
9270 tree.dispatch_pointer(contact(
9271 first,
9272 PointerPhase::Up,
9273 first_at,
9274 EventTime::from_millis(30),
9275 ));
9276 assert!(!pressed.get(), "the owner's release does clear it");
9277 }
9278
9279 #[test]
9282 fn a_cancel_clears_the_visual() {
9283 let (mut tree, _clock) = tree_on_a_clock();
9284 let (w, pressed) = tappable(&mut tree);
9285 tree.layout(SizeProposal::exact(100.0, 50.0));
9286 let center = tree.bounds(w).center();
9287
9288 let id = contact_id();
9289 tree.dispatch_pointer(contact(id, PointerPhase::Down, center, EventTime::ZERO));
9290 assert!(pressed.get());
9291
9292 tree.dispatch_pointer(contact(
9293 id,
9294 PointerPhase::Cancel,
9295 center,
9296 EventTime::from_millis(20),
9297 ));
9298 assert!(!pressed.get(), "a press that was taken away is not painted");
9299 assert_eq!(tree.pressed_by(w), None);
9300 }
9301
9302 #[test]
9305 fn a_pan_claim_clears_the_visual() {
9306 let (mut tree, clock) = tree_on_a_clock();
9307 let (row, pressed) = tappable(&mut tree);
9308 let list = tree.add(
9309 StackWidget::new()
9310 .child(row)
9311 .pan_claim(PanClaim::vertical())
9312 .on_scroll(|_e, _c| EventResponse::Handled),
9313 );
9314 tree.layout(SizeProposal::exact(200.0, 400.0));
9315 let _ = list;
9316 let start = Point::new(100.0, 200.0);
9317
9318 let id = contact_id();
9319 tree.dispatch_pointer(contact(id, PointerPhase::Down, start, EventTime::ZERO));
9320 assert!(tree.press_pending(row));
9322 clock.set(EventTime::from_millis(100));
9323 tree.tick_gestures(std::time::Instant::now());
9324 assert!(pressed.get(), "…and appears once the delay has elapsed");
9325
9326 tree.dispatch_pointer(contact(
9328 id,
9329 PointerPhase::Move,
9330 Point::new(100.0, 260.0),
9331 EventTime::from_millis(120),
9332 ));
9333 assert!(
9334 !pressed.get(),
9335 "the row lost the press to the list and must stop advertising it",
9336 );
9337 assert_eq!(tree.pressed_by(row), None);
9338 }
9339
9340 #[test]
9349 fn a_button_the_control_cannot_act_on_raises_no_visual() {
9350 let taps = Rc::new(Cell::new(0u32));
9351 let count = taps.clone();
9352 let mut tree = WidgetTree::new();
9353 let w = tree.add(FillWidget::new().focusable().on_tap(move |_e, _c| {
9354 count.set(count.get() + 1);
9355 }));
9356 let pressed = tree.pressed_signal(w);
9357 tree.layout(SizeProposal::exact(100.0, 50.0));
9358 let center = tree.bounds(w).center();
9359
9360 for button in [
9361 PointerButton::Middle,
9362 PointerButton::Back,
9363 PointerButton::Forward,
9364 ] {
9365 tree.dispatch_event(WidgetEvent::pointer_down(center, button, Modifiers::NONE));
9366 assert!(
9367 !pressed.get(),
9368 "{button:?} cannot activate an `on_tap` node, so it must not light one up",
9369 );
9370 assert_eq!(tree.pressed_by(w), None, "and owns no visual to clear");
9371 tree.dispatch_event(WidgetEvent::pointer_up(center, button, Modifiers::NONE));
9372 assert_eq!(
9373 taps.get(),
9374 0,
9375 "the tap recognizer refuses {button:?} too — that is the point",
9376 );
9377 }
9378
9379 tree.dispatch_event(WidgetEvent::pointer_down(
9381 center,
9382 PointerButton::Primary,
9383 Modifiers::NONE,
9384 ));
9385 assert!(pressed.get(), "a primary press lights up as it always has");
9386 assert_eq!(tree.pressed_by(w), Some(PointerId::MOUSE));
9387 tree.dispatch_event(WidgetEvent::pointer_up(
9388 center,
9389 PointerButton::Primary,
9390 Modifiers::NONE,
9391 ));
9392 assert!(!pressed.get());
9393 assert_eq!(taps.get(), 1);
9394 }
9395
9396 #[test]
9400 fn a_secondary_press_with_no_context_menu_raises_no_visual() {
9401 let mut tree = WidgetTree::new();
9402 let (w, pressed) = tappable(&mut tree);
9403 tree.layout(SizeProposal::exact(100.0, 50.0));
9404 let center = tree.bounds(w).center();
9405
9406 tree.dispatch_event(WidgetEvent::pointer_down(
9407 center,
9408 PointerButton::Secondary,
9409 Modifiers::NONE,
9410 ));
9411 assert!(
9412 !pressed.get(),
9413 "nothing opened, and nothing may look pressed either",
9414 );
9415 assert_eq!(tree.pressed_by(w), None);
9416 assert!(!tree.press_is_inside(w));
9417 }
9418
9419 #[test]
9423 fn a_widened_mask_widens_the_visual_with_it() {
9424 use crate::event::ButtonMask;
9425
9426 let mut tree = WidgetTree::new();
9427 let w = tree.add(
9428 FillWidget::new()
9429 .on_tap(|_e, _c| {})
9430 .accept_tap_buttons(ButtonMask::PRIMARY | ButtonMask::MIDDLE),
9431 );
9432 let pressed = tree.pressed_signal(w);
9433 tree.layout(SizeProposal::exact(100.0, 50.0));
9434 let center = tree.bounds(w).center();
9435
9436 tree.dispatch_event(WidgetEvent::pointer_down(
9437 center,
9438 PointerButton::Middle,
9439 Modifiers::NONE,
9440 ));
9441 assert!(pressed.get(), "this node really does act on a middle-click");
9442
9443 tree.dispatch_event(WidgetEvent::pointer_up(
9444 center,
9445 PointerButton::Middle,
9446 Modifiers::NONE,
9447 ));
9448 assert!(!pressed.get());
9449 }
9450
9451 #[test]
9457 fn a_press_that_raises_no_visual_still_defers_its_focus() {
9458 use teksilo_tokens::{PenKind, PointerKind};
9459
9460 let (mut tree, _clock) = tree_on_a_clock();
9461 let (w, pressed) = tappable(&mut tree);
9462 tree.layout(SizeProposal::exact(100.0, 50.0));
9463 let center = tree.bounds(w).center();
9464
9465 let id = contact_id();
9466 let barrel = |phase, t| PointerSample {
9467 pointer: PointerInfo {
9468 kind: PointerKind::Pen(PenKind::Pen),
9469 ..PointerInfo::touch(id, t)
9470 },
9471 phase,
9472 position: center,
9473 button: Some(PointerButton::Secondary),
9474 modifiers: Modifiers::NONE,
9475 coalesced: Vec::new(),
9476 };
9477
9478 tree.dispatch_pointer(barrel(PointerPhase::Down, EventTime::ZERO));
9479 assert!(!pressed.get(), "the barrel button activates nothing here");
9480 assert_eq!(tree.pressed_by(w), None);
9481 assert_eq!(
9482 tree.focused(),
9483 None,
9484 "a direct pointer has chosen nothing until it lifts",
9485 );
9486
9487 tree.dispatch_pointer(barrel(PointerPhase::Up, EventTime::from_millis(40)));
9488 assert_eq!(
9489 tree.focused(),
9490 Some(w),
9491 "the deferral is not button-gated: the release landed where the press did",
9492 );
9493 assert_eq!(
9494 tree.focus_origin().and_then(FocusOrigin::pointer_kind),
9495 Some(PointerKind::Pen(PenKind::Pen)),
9496 );
9497 }
9498
9499 #[test]
9506 fn the_feedback_delay_applies_only_inside_a_claimant() {
9507 let (mut tree, _clock) = tree_on_a_clock();
9509 let (w, pressed) = tappable(&mut tree);
9510 tree.layout(SizeProposal::exact(200.0, 400.0));
9511 let id = contact_id();
9512 let at = tree.bounds(w).center();
9513 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
9514 assert!(
9515 !tree.press_pending(w),
9516 "no claimant above it, so nothing to rule out",
9517 );
9518 assert!(pressed.get());
9519
9520 let (mut tree, clock) = tree_on_a_clock();
9522 let (row, pressed) = tappable(&mut tree);
9523 let list = tree.add(
9524 StackWidget::new()
9525 .child(row)
9526 .pan_claim(PanClaim::vertical())
9527 .on_scroll(|_e, _c| EventResponse::Handled),
9528 );
9529 tree.layout(SizeProposal::exact(200.0, 400.0));
9530 let _ = list;
9531 let id = contact_id();
9532 let at = Point::new(100.0, 200.0);
9533 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
9534 assert!(tree.press_pending(row), "a finger might be about to scroll");
9535 assert!(!pressed.get(), "so the row does not flash");
9536 assert!(
9537 tree.press_is_inside(row),
9538 "the press is real; only its visual is being withheld",
9539 );
9540
9541 clock.set(EventTime::from_millis(99));
9542 tree.tick_gestures(std::time::Instant::now());
9543 assert!(!pressed.get(), "99 ms is under the 100 ms delay");
9544
9545 clock.set(EventTime::from_millis(100));
9546 tree.tick_gestures(std::time::Instant::now());
9547 assert!(pressed.get(), "and 100 ms is it");
9548 assert!(!tree.press_pending(row));
9549 }
9550
9551 #[test]
9554 fn a_mouse_inside_a_claimant_never_waits() {
9555 let mut tree = WidgetTree::new();
9556 let (row, pressed) = tappable(&mut tree);
9557 let list = tree.add(
9558 StackWidget::new()
9559 .child(row)
9560 .pan_claim(PanClaim::vertical())
9561 .on_scroll(|_e, _c| EventResponse::Handled),
9562 );
9563 tree.layout(SizeProposal::exact(200.0, 400.0));
9564 let _ = list;
9565
9566 tree.dispatch_event(WidgetEvent::pointer_down(
9567 Point::new(100.0, 200.0),
9568 PointerButton::Primary,
9569 Modifiers::NONE,
9570 ));
9571 assert!(!tree.press_pending(row));
9572 assert!(pressed.get(), "Compact with a mouse is exactly as it was");
9573 }
9574
9575 #[test]
9582 fn a_handler_can_read_the_press_it_is_inside() {
9583 let (mut tree, _clock) = tree_on_a_clock();
9584 let seen: Rc<RefCell<Vec<(bool, bool, bool)>>> = Rc::new(RefCell::new(Vec::new()));
9585 let log = seen.clone();
9586 let row = tree.add(FillWidget::new().on_tap(|_e, _c| {}).on_pointer_event(
9587 move |event, ctx| {
9588 if matches!(event, WidgetEvent::PointerUp { .. }) {
9589 log.borrow_mut().push((
9590 ctx.is_pressed(),
9591 ctx.press_is_inside(),
9592 ctx.press_pending(),
9593 ));
9594 }
9595 EventResponse::Ignored
9596 },
9597 ));
9598 let list = tree.add(
9599 StackWidget::new()
9600 .child(row)
9601 .pan_claim(PanClaim::vertical())
9602 .on_scroll(|_e, _c| EventResponse::Handled),
9603 );
9604 tree.layout(SizeProposal::exact(200.0, 400.0));
9605 let _ = list;
9606
9607 let id = contact_id();
9608 let at = Point::new(100.0, 200.0);
9609 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
9610 tree.dispatch_pointer(contact(
9611 id,
9612 PointerPhase::Up,
9613 at,
9614 EventTime::from_millis(30),
9615 ));
9616
9617 assert_eq!(
9618 seen.borrow().as_slice(),
9619 &[(false, true, true)],
9620 "released inside the claimant before the delay elapsed: inside, \
9621 pending, and therefore not yet showing",
9622 );
9623 }
9624
9625 #[test]
9628 fn a_handler_outside_a_press_reads_nothing() {
9629 let mut tree = WidgetTree::new();
9630 let asked = Rc::new(Cell::new(false));
9631 let flag = asked.clone();
9632 let w = tree.add(FillWidget::new().focusable().on_key(move |_e, ctx| {
9633 flag.set(ctx.is_pressed() || ctx.press_is_inside() || ctx.press_pending());
9634 EventResponse::Ignored
9635 }));
9636 tree.layout(SizeProposal::exact(100.0, 50.0));
9637 tree.focus(w);
9638 tree.press_key(Key::ArrowDown, Modifiers::NONE);
9639 assert!(!asked.get());
9640 }
9641
9642 #[test]
9646 fn a_completed_press_leaves_the_detector_clean() {
9647 let (mut tree, _clock) = tree_on_a_clock();
9648 let (w, pressed) = tappable(&mut tree);
9649 tree.layout(SizeProposal::exact(100.0, 50.0));
9650 let at = tree.bounds(w).center();
9651
9652 let id = contact_id();
9653 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
9654 assert!(pressed.get());
9655 tree.dispatch_pointer(contact(
9656 id,
9657 PointerPhase::Up,
9658 at,
9659 EventTime::from_millis(30),
9660 ));
9661 tree.assert_no_leaked_pointer_state();
9662 }
9663
9664 #[test]
9668 fn the_detector_reports_a_press_still_held() {
9669 let (mut tree, _clock) = tree_on_a_clock();
9670 let (w, _pressed) = tappable(&mut tree);
9671 tree.layout(SizeProposal::exact(100.0, 50.0));
9672 let at = tree.bounds(w).center();
9673
9674 let id = contact_id();
9675 tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
9676 let held = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
9677 tree.assert_no_leaked_pointer_state()
9678 }));
9679 let message = *held
9680 .expect_err("a live press is leaked state")
9681 .downcast::<String>()
9682 .expect("the detector panics with a message");
9683 assert!(
9684 message.contains("is still pressed by"),
9685 "the press is named in the report, got: {message}",
9686 );
9687 }
9688
9689 #[derive(Debug)]
9693 struct SideBySide {
9694 children: Vec<WidgetId>,
9695 }
9696
9697 impl crate::widget::Widget for SideBySide {
9698 fn layout_response(
9699 &self,
9700 proposal: SizeProposal,
9701 _ctx: &crate::widget::LayoutContext,
9702 ) -> crate::widget::LayoutResponse {
9703 proposal.resolve(0.0, 0.0).into()
9704 }
9705
9706 fn place_children(
9707 &self,
9708 bounds: teksilo_canvas::Rect,
9709 _proposal: SizeProposal,
9710 children: &mut [crate::widget::WidgetPlacement],
9711 _ctx: &crate::widget::LayoutContext,
9712 ) {
9713 let n = children.len().max(1) as f32;
9714 let w = bounds.width / n;
9715 for (i, child) in children.iter_mut().enumerate() {
9716 child.origin = Point::new(bounds.x + w * i as f32, bounds.y);
9717 child.size = teksilo_canvas::Size::new(w, bounds.height);
9718 }
9719 }
9720
9721 fn children(&self) -> Vec<WidgetId> {
9722 self.children.clone()
9723 }
9724 }
9725}
9726
9727#[cfg(test)]
9728mod overlay_release_dismissal_tests {
9729 use std::cell::Cell;
9739 use std::rc::Rc;
9740
9741 use teksilo_canvas::{Point, Rect, Size, SizeProposal};
9742
9743 use crate::WidgetId;
9744 use crate::event::{Modifiers, PointerButton, WidgetEvent};
9745 use crate::overlay::{DismissBehavior, OverlayPlacement, OverlayRequest};
9746 use crate::pointer::{
9747 BackendDeviceKey, EventTime, PointerId, PointerIdAllocator, PointerInfo, PointerPhase,
9748 PointerSample,
9749 };
9750 use crate::test_widgets::FillWidget;
9751 use crate::widget::{LayoutContext, LayoutResponse, Widget};
9752 use crate::widget_builder::WidgetBuilder;
9753 use crate::widget_tree::WidgetTree;
9754
9755 #[derive(Debug)]
9762 struct Panel(Size);
9763
9764 impl Widget for Panel {
9765 fn layout_response(&self, _proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
9766 self.0.into()
9767 }
9768 }
9769
9770 #[derive(Debug)]
9778 struct PageAndTrigger {
9779 children: Vec<WidgetId>,
9780 }
9781
9782 impl Widget for PageAndTrigger {
9783 fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
9784 proposal.resolve(0.0, 0.0).into()
9785 }
9786
9787 fn place_children(
9788 &self,
9789 bounds: Rect,
9790 _proposal: SizeProposal,
9791 children: &mut [crate::widget::WidgetPlacement],
9792 _ctx: &LayoutContext,
9793 ) {
9794 if let Some(page) = children.get_mut(0) {
9795 page.origin = bounds.origin();
9796 page.size = bounds.size();
9797 }
9798 if let Some(trigger) = children.get_mut(1) {
9799 trigger.origin = bounds.origin();
9800 trigger.size = Size::new(10.0, 10.0);
9801 }
9802 }
9803
9804 fn children(&self) -> Vec<WidgetId> {
9805 self.children.clone()
9806 }
9807 }
9808
9809 fn contact_id(n: u64) -> PointerId {
9810 PointerIdAllocator::global().begin(BackendDeviceKey::new(0x0FA2), n)
9811 }
9812
9813 fn touch(id: PointerId, phase: PointerPhase, at: Point) -> PointerSample {
9814 PointerSample {
9815 pointer: PointerInfo::touch(id, EventTime::ZERO),
9816 phase,
9817 position: at,
9818 button: None,
9819 modifiers: Modifiers::NONE,
9820 coalesced: Vec::new(),
9821 }
9822 }
9823
9824 fn page_with_a_menu() -> (WidgetTree, WidgetId, Rc<Cell<u32>>) {
9831 let mut tree = WidgetTree::new();
9832 let taps = Rc::new(Cell::new(0u32));
9833 let counter = taps.clone();
9834 let page = tree.add(
9835 FillWidget::new()
9836 .focusable()
9837 .on_tap(move |_e, _c| counter.set(counter.get() + 1)),
9838 );
9839 let trigger = tree.add(Panel(Size::new(10.0, 10.0)));
9842 let _root = tree.add(PageAndTrigger {
9843 children: vec![page, trigger],
9844 });
9845 let menu = tree.add(Panel(Size::new(200.0, 200.0)));
9846 tree.show_overlay(OverlayRequest::new(
9847 menu,
9848 trigger,
9849 OverlayPlacement::AtPointer(Point::new(100.0, 100.0)),
9850 DismissBehavior::ClickOutside,
9851 ));
9852 tree.layout(SizeProposal::exact(800.0, 600.0));
9853 assert_eq!(tree.active_overlays().len(), 1, "the menu is open");
9854 (tree, page, taps)
9855 }
9856
9857 fn outside() -> Point {
9858 Point::new(600.0, 500.0)
9859 }
9860
9861 fn inside_menu() -> Point {
9862 Point::new(150.0, 150.0)
9863 }
9864
9865 #[test]
9872 fn the_suppressed_down_leaves_nothing_pressed_or_captured_beneath() {
9873 let (mut tree, page, taps) = page_with_a_menu();
9874 let finger = contact_id(1);
9875 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
9876
9877 assert_eq!(
9878 tree.active_overlays().len(),
9879 1,
9880 "the press does not close the menu; the release does"
9881 );
9882 assert_eq!(tree.pressed_by(page), None, "nothing beneath is pressed");
9883 let entry = tree.pointers.get(finger).expect("the contact is live");
9884 assert_eq!(entry.captured_by, None, "nothing beneath captured it");
9885 assert!(entry.sequence.is_none(), "no arbitration was opened");
9886 assert_eq!(taps.get(), 0);
9887 }
9888
9889 #[test]
9891 fn a_touch_tap_outside_a_menu_closes_it_and_actuates_nothing() {
9892 let (mut tree, page, taps) = page_with_a_menu();
9893 let finger = contact_id(2);
9894 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
9895 tree.dispatch_pointer(touch(finger, PointerPhase::Up, outside()));
9896
9897 assert!(tree.active_overlays().is_empty(), "the menu closed");
9898 assert_eq!(taps.get(), 0, "the page beneath was never tapped");
9899 assert_eq!(tree.pressed_by(page), None);
9900 tree.assert_no_leaked_pointer_state();
9901 }
9902
9903 #[test]
9906 fn a_cancelled_press_aborts_the_arm() {
9907 let (mut tree, page, taps) = page_with_a_menu();
9908 let finger = contact_id(3);
9909 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
9910 tree.dispatch_pointer(touch(finger, PointerPhase::Cancel, outside()));
9911
9912 assert_eq!(tree.active_overlays().len(), 1, "the menu survives");
9913 assert_eq!(taps.get(), 0);
9914 assert_eq!(tree.pressed_by(page), None);
9915 tree.assert_no_leaked_pointer_state();
9916 }
9917
9918 #[test]
9922 fn a_press_slid_onto_the_menu_dismisses_nothing() {
9923 let (mut tree, _page, taps) = page_with_a_menu();
9924 let finger = contact_id(4);
9925 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
9926 tree.dispatch_pointer(touch(finger, PointerPhase::Move, inside_menu()));
9927 tree.dispatch_pointer(touch(finger, PointerPhase::Up, inside_menu()));
9928
9929 assert_eq!(tree.active_overlays().len(), 1, "the menu survives");
9930 assert_eq!(taps.get(), 0);
9931 tree.assert_no_leaked_pointer_state();
9932 }
9933
9934 #[test]
9937 fn a_second_contact_cannot_dismiss_what_the_first_is_manipulating() {
9938 let (mut tree, _page, _taps) = page_with_a_menu();
9939 let first = contact_id(5);
9940 let second = contact_id(6);
9941
9942 tree.dispatch_pointer(touch(first, PointerPhase::Down, inside_menu()));
9943 assert!(
9944 tree.pointers
9945 .get(first)
9946 .is_some_and(|e| e.sequence.is_some()),
9947 "the first contact holds a live press inside the menu"
9948 );
9949
9950 tree.dispatch_pointer(touch(second, PointerPhase::Down, outside()));
9951 tree.dispatch_pointer(touch(second, PointerPhase::Up, outside()));
9952 assert_eq!(
9953 tree.active_overlays().len(),
9954 1,
9955 "the menu the first finger is holding must not close under it"
9956 );
9957
9958 tree.dispatch_pointer(touch(first, PointerPhase::Up, inside_menu()));
9961 let third = contact_id(7);
9962 tree.dispatch_pointer(touch(third, PointerPhase::Down, outside()));
9963 tree.dispatch_pointer(touch(third, PointerPhase::Up, outside()));
9964 assert!(tree.active_overlays().is_empty());
9965 tree.assert_no_leaked_pointer_state();
9966 }
9967
9968 #[test]
9972 fn an_arm_is_not_itself_a_press_that_blocks_another_contact() {
9973 let (mut tree, _page, _taps) = page_with_a_menu();
9974 let first = contact_id(8);
9975 let second = contact_id(9);
9976
9977 tree.dispatch_pointer(touch(first, PointerPhase::Down, outside()));
9978 assert!(
9979 tree.overlay_manager().has_armed_dismiss(first),
9980 "the first contact armed"
9981 );
9982 assert!(tree.busy_press_points(second).is_empty());
9984
9985 tree.dispatch_pointer(touch(second, PointerPhase::Down, outside()));
9986 assert!(tree.overlay_manager().has_armed_dismiss(second));
9987 tree.dispatch_pointer(touch(second, PointerPhase::Up, outside()));
9988 assert!(tree.active_overlays().is_empty());
9989
9990 tree.dispatch_pointer(touch(first, PointerPhase::Up, outside()));
9993 tree.assert_no_leaked_pointer_state();
9994 }
9995
9996 #[test]
9999 fn a_touch_inside_the_menu_arms_nothing() {
10000 let (mut tree, _page, _taps) = page_with_a_menu();
10001 let finger = contact_id(10);
10002 tree.dispatch_pointer(touch(finger, PointerPhase::Down, inside_menu()));
10003 assert!(!tree.overlay_manager().has_armed_dismiss(finger));
10004 tree.dispatch_pointer(touch(finger, PointerPhase::Up, inside_menu()));
10005 assert_eq!(tree.active_overlays().len(), 1);
10006 tree.assert_no_leaked_pointer_state();
10007 }
10008
10009 #[test]
10019 fn a_contact_with_no_press_still_ends_when_the_platform_revokes_it() {
10020 let mut tree = WidgetTree::new();
10021 tree.layout(SizeProposal::exact(800.0, 600.0));
10022 let finger = contact_id(99);
10023 tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
10024 tree.dispatch_pointer(touch(finger, PointerPhase::Cancel, outside()));
10025 tree.assert_no_leaked_pointer_state();
10026 }
10027
10028 #[test]
10035 fn a_mouse_click_outside_a_menu_dismisses_on_the_press_and_falls_through() {
10036 let (mut tree, _page, taps) = page_with_a_menu();
10037 tree.dispatch_event(WidgetEvent::pointer_down(
10038 outside(),
10039 PointerButton::Primary,
10040 Modifiers::NONE,
10041 ));
10042 assert!(
10043 tree.active_overlays().is_empty(),
10044 "the mouse still dismisses on the press"
10045 );
10046 assert!(
10047 tree.pointers
10048 .get(PointerId::MOUSE)
10049 .is_some_and(|e| e.sequence.is_some()),
10050 "and the press still reaches the page beneath"
10051 );
10052 tree.dispatch_event(WidgetEvent::pointer_up(
10053 outside(),
10054 PointerButton::Primary,
10055 Modifiers::NONE,
10056 ));
10057 assert_eq!(taps.get(), 1, "the control beneath activated");
10058 assert!(
10059 !tree.overlay_manager().has_armed_dismiss(PointerId::MOUSE),
10060 "a mouse never arms"
10061 );
10062 tree.assert_no_leaked_pointer_state();
10063 }
10064
10065 #[test]
10072 fn a_context_menu_avoids_the_contact_that_opened_it() {
10073 fn menu_bounds(pointer: PointerInfo, at: Point) -> Rect {
10074 let mut tree = WidgetTree::new();
10075 let page = tree.add(
10076 FillWidget::new()
10077 .focusable()
10078 .context_menu(|_p, _ctx| Some(Box::new(Panel(Size::new(200.0, 160.0))))),
10079 );
10080 let _ = page;
10081 tree.layout(SizeProposal::exact(800.0, 600.0));
10082 tree.dispatch_pointer(PointerSample {
10083 pointer,
10084 phase: PointerPhase::Down,
10085 position: at,
10086 button: Some(PointerButton::Secondary),
10087 modifiers: Modifiers::NONE,
10088 coalesced: Vec::new(),
10089 });
10090 tree.layout(SizeProposal::exact(800.0, 600.0));
10091 let id = *tree
10092 .active_overlays()
10093 .first()
10094 .expect("the context menu opened");
10095 tree.overlay_manager().bounds_for(id).expect("bounds")
10096 }
10097
10098 let at = Point::new(400.0, 300.0);
10099 let finger = menu_bounds(PointerInfo::touch(contact_id(11), EventTime::ZERO), at);
10100 let contact = crate::overlay::rect_centred_on(at, crate::overlay::ASSUMED_CONTACT_PATCH);
10101 assert!(
10102 finger.x < contact.x && finger.right() <= contact.x,
10103 "a touch menu clears the contact patch: {finger:?} vs {contact:?}"
10104 );
10105
10106 let mouse = menu_bounds(PointerInfo::mouse(EventTime::ZERO), at);
10107 assert_eq!(
10108 (mouse.x, mouse.y),
10109 (at.x, at.y),
10110 "a mouse menu still opens with its corner on the pointer"
10111 );
10112 }
10113}
10114
10115#[cfg(test)]
10131mod pass_through_overlay_tests {
10132 use teksilo_canvas::{Point, Rect, SizeProposal};
10133
10134 use crate::WidgetId;
10135 use crate::overlay::{DismissBehavior, OverlayPlacement, OverlayRequest};
10136 use crate::test_widgets::FillWidget;
10137 use crate::widget::{LayoutContext, LayoutResponse, Widget};
10138 use crate::widget_builder::WidgetBuilder;
10139 use crate::widget_tree::WidgetTree;
10140
10141 #[derive(Debug)]
10144 struct HandleLayer {
10145 handle: WidgetId,
10146 at: Rect,
10147 }
10148
10149 impl Widget for HandleLayer {
10150 fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
10151 proposal.resolve(0.0, 0.0).into()
10152 }
10153
10154 fn place_children(
10155 &self,
10156 _bounds: Rect,
10157 _proposal: SizeProposal,
10158 children: &mut [crate::widget::WidgetPlacement],
10159 _ctx: &LayoutContext,
10160 ) {
10161 if let Some(child) = children.get_mut(0) {
10162 child.origin = self.at.origin();
10163 child.size = self.at.size();
10164 }
10165 }
10166
10167 fn children(&self) -> Vec<WidgetId> {
10168 vec![self.handle]
10169 }
10170 }
10171
10172 const HANDLE: Rect = Rect {
10173 x: 10.0,
10174 y: 10.0,
10175 width: 20.0,
10176 height: 20.0,
10177 };
10178
10179 #[derive(Clone, Copy, PartialEq)]
10181 enum RootFlag {
10182 PassThrough,
10184 HitTransparent,
10187 }
10188
10189 fn field_under_a_layer(flag: RootFlag) -> (WidgetTree, WidgetId, WidgetId) {
10195 let mut tree = WidgetTree::new();
10196 let field = tree.add(FillWidget::new());
10197 let handle = tree.add(FillWidget::new());
10198 let layer = HandleLayer { handle, at: HANDLE };
10199 let layer = match flag {
10200 RootFlag::PassThrough => tree.add(layer.event_pass_through(true)),
10201 RootFlag::HitTransparent => tree.add(layer.hit_transparent(true)),
10202 };
10203 tree.show_overlay(OverlayRequest::new(
10204 layer,
10205 field,
10206 OverlayPlacement::FullViewport,
10207 DismissBehavior::Manual,
10208 ));
10209 tree.layout(SizeProposal::exact(400.0, 200.0));
10210 assert_eq!(tree.active_overlays().len(), 1, "the layer is up");
10211 (tree, field, handle)
10212 }
10213
10214 #[test]
10216 fn a_pass_through_layer_hands_back_what_it_did_not_claim() {
10217 let (tree, field, handle) = field_under_a_layer(RootFlag::PassThrough);
10218
10219 assert_eq!(
10220 tree.hit_test(Point::new(15.0, 15.0)),
10221 Some(handle),
10222 "the layer must still win the point its own child covers"
10223 );
10224 assert_eq!(
10225 tree.hit_test(Point::new(200.0, 100.0)),
10226 Some(field),
10227 "a press the layer did not claim must reach the surface beneath it"
10228 );
10229 }
10230
10231 #[test]
10242 fn a_pass_through_root_still_hears_a_press_that_landed_on_its_child() {
10243 use std::cell::Cell;
10244 use std::rc::Rc;
10245
10246 let seen = Rc::new(Cell::new(0u32));
10247 let counter = seen.clone();
10248 let mut tree = WidgetTree::new();
10249 let field = tree.add(FillWidget::new());
10250 let handle = tree.add(FillWidget::new().on_tap(|_e, _c| {}));
10251 let layer = tree.add(
10252 HandleLayer { handle, at: HANDLE }
10253 .event_pass_through(true)
10254 .on_pointer_event(move |_event, _ctx| {
10255 counter.set(counter.get() + 1);
10256 crate::event::EventResponse::Ignored
10257 }),
10258 );
10259 tree.show_overlay(OverlayRequest::new(
10260 layer,
10261 field,
10262 OverlayPlacement::FullViewport,
10263 DismissBehavior::Manual,
10264 ));
10265 tree.layout(SizeProposal::exact(400.0, 200.0));
10266
10267 tree.pointer_down_button(Point::new(15.0, 15.0), crate::event::PointerButton::Primary);
10268 assert!(
10269 seen.get() > 0,
10270 "a pass-through root heard nothing about a press on its own child"
10271 );
10272
10273 let after_child = seen.get();
10274 tree.pointer_up_button(Point::new(15.0, 15.0), crate::event::PointerButton::Primary);
10275 tree.pointer_down_button(
10278 Point::new(200.0, 100.0),
10279 crate::event::PointerButton::Primary,
10280 );
10281 assert_eq!(
10282 seen.get(),
10283 after_child + 1,
10284 "the press-and-lift on the child accounts for the count, and the \
10285 press on the surface beneath added nothing"
10286 );
10287 }
10288
10289 #[test]
10303 fn a_hit_transparent_root_does_not_get_the_fall_through() {
10304 let (tree, field, _handle) = field_under_a_layer(RootFlag::HitTransparent);
10305
10306 assert_eq!(
10308 tree.hit_test(Point::new(15.0, 15.0)),
10309 None,
10310 "hit_transparent must exclude the subtree, or this fixture is not \
10311 testing the gate"
10312 );
10313 assert_eq!(
10314 tree.hit_test(Point::new(200.0, 100.0)),
10315 None,
10316 "the fall-through reached past an overlay that did not ask for it"
10317 );
10318 let _ = field;
10319 }
10320}
10321
10322fn note_refused_section(owner: WidgetId) {
10326 #[cfg(debug_assertions)]
10327 {
10328 thread_local! {
10329 static NOTED: std::cell::RefCell<std::collections::HashSet<WidgetId>> =
10330 std::cell::RefCell::new(std::collections::HashSet::new());
10331 }
10332 if NOTED.with(|noted| noted.borrow_mut().insert(owner)) {
10333 eprintln!(
10334 "teksilo: the context menu of {owner:?} takes no section \
10335 (Widget::append_context_menu_section), so a contribution was left out"
10336 );
10337 }
10338 }
10339 #[cfg(not(debug_assertions))]
10340 let _ = owner;
10341}
10342
10343fn logical_key_identity(key: Key) -> u64 {
10348 use std::hash::{Hash, Hasher};
10349 let key = match key {
10350 Key::Character(c) => Key::Character(c.to_ascii_lowercase()),
10351 other => other,
10352 };
10353 let mut hasher = std::collections::hash_map::DefaultHasher::new();
10354 key.hash(&mut hasher);
10355 hasher.finish()
10356}