1#[cfg(not(feature = "std"))]
4use alloc::string::{String, ToString};
5use alloc::{
6 boxed::Box,
7 collections::{btree_map::BTreeMap, btree_set::BTreeSet},
8 vec::Vec,
9};
10
11use azul_css::AzString;
12
13use crate::{
14 callbacks::Update,
15 dom::{DomId, DomNodeId, On},
16 geom::{LogicalPosition, LogicalRect},
17 hit_test::{FullHitTest, HitTestItem},
18 id::NodeId,
19 styled_dom::{ChangedCssProperty, NodeHierarchyItemId},
20 task::Instant,
21 OrderedMap,
22};
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum EasingFunction {
27 Linear,
28 EaseInOut,
29 EaseOut,
30 Spring,
36}
37
38pub type RestyleNodes = BTreeMap<NodeId, Vec<ChangedCssProperty>>;
39pub type RelayoutNodes = BTreeMap<NodeId, Vec<ChangedCssProperty>>;
40pub type RelayoutWords = BTreeMap<NodeId, AzString>;
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub struct FocusChange {
44 pub old: Option<DomNodeId>,
45 pub new: Option<DomNodeId>,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub struct CallbackToCall {
50 pub node_id: NodeId,
51 pub hit_test_item: Option<HitTestItem>,
52 pub event_filter: EventFilter,
53}
54
55impl CallbackToCall {
56 #[must_use] pub const fn new(
57 node_id: NodeId,
58 hit_test_item: Option<HitTestItem>,
59 event_filter: EventFilter,
60 ) -> Self {
61 Self { node_id, hit_test_item, event_filter }
62 }
63
64 #[must_use] pub fn from_hit_test(
68 hit_test: &FullHitTest,
69 dom_id: DomId,
70 event_filter: EventFilter,
71 ) -> Vec<Self> {
72 let Some(hit) = hit_test.hovered_nodes.get(&dom_id) else {
73 return Vec::new();
74 };
75 hit.regular_hit_test_nodes
76 .iter()
77 .map(|(node_id, item)| Self {
78 node_id: *node_id,
79 hit_test_item: Some(*item),
80 event_filter,
81 })
82 .collect()
83 }
84}
85
86#[derive(Debug, Copy, Clone, PartialEq, Eq)]
87#[must_use = "ProcessEventResult must be used to determine if relayout/repaint is needed"]
88pub enum ProcessEventResult {
89 DoNothing = 0,
90 ShouldReRenderCurrentWindow = 1,
91 ShouldUpdateDisplayListCurrentWindow = 2,
92 UpdateHitTesterAndProcessAgain = 3,
95 ShouldIncrementalRelayout = 4,
98 ShouldRegenerateDomCurrentWindow = 5,
100 ShouldRegenerateDomAllWindows = 6,
101}
102
103impl ProcessEventResult {
104 #[must_use] pub const fn order(&self) -> usize {
105 use self::ProcessEventResult::{DoNothing, ShouldReRenderCurrentWindow, ShouldUpdateDisplayListCurrentWindow, UpdateHitTesterAndProcessAgain, ShouldIncrementalRelayout, ShouldRegenerateDomCurrentWindow, ShouldRegenerateDomAllWindows};
106 match self {
107 DoNothing => 0,
108 ShouldReRenderCurrentWindow => 1,
109 ShouldUpdateDisplayListCurrentWindow => 2,
110 UpdateHitTesterAndProcessAgain => 3,
111 ShouldIncrementalRelayout => 4,
112 ShouldRegenerateDomCurrentWindow => 5,
113 ShouldRegenerateDomAllWindows => 6,
114 }
115 }
116}
117
118impl PartialOrd for ProcessEventResult {
119 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
120 self.order().partial_cmp(&other.order())
121 }
122}
123
124impl Ord for ProcessEventResult {
125 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
126 self.order().cmp(&other.order())
127 }
128}
129
130impl ProcessEventResult {
131 pub fn max_self(self, other: Self) -> Self {
132 self.max(other)
133 }
134}
135
136#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
141#[repr(C)]
142pub enum EventSource {
143 User,
145 Programmatic,
147 Synthetic,
149 Lifecycle,
151}
152
153#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
160#[repr(C)]
161#[derive(Default)]
162pub enum EventPhase {
163 Capture,
165 Target,
167 #[default]
169 Bubble,
170}
171
172
173#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
175#[repr(C)]
176pub enum MouseButton {
177 Left,
178 Middle,
179 Right,
180 Other(u8),
181}
182
183#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
185#[repr(C)]
186pub enum ScrollDeltaMode {
187 Pixel,
189 Line,
191 Page,
193}
194
195#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
197#[repr(C)]
198pub enum ScrollDirection {
199 Up,
200 Down,
201 Left,
202 Right,
203}
204
205#[repr(C)]
214#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
215pub struct ScrollIntoViewOptions {
216 pub block: ScrollLogicalPosition,
218 pub inline_axis: ScrollLogicalPosition,
221 pub behavior: ScrollIntoViewBehavior,
223}
224
225impl ScrollIntoViewOptions {
226 #[must_use] pub const fn nearest() -> Self {
228 Self {
229 block: ScrollLogicalPosition::Nearest,
230 inline_axis: ScrollLogicalPosition::Nearest,
231 behavior: ScrollIntoViewBehavior::Auto,
232 }
233 }
234
235 #[must_use] pub const fn center() -> Self {
237 Self {
238 block: ScrollLogicalPosition::Center,
239 inline_axis: ScrollLogicalPosition::Center,
240 behavior: ScrollIntoViewBehavior::Auto,
241 }
242 }
243
244 #[must_use] pub const fn start() -> Self {
246 Self {
247 block: ScrollLogicalPosition::Start,
248 inline_axis: ScrollLogicalPosition::Start,
249 behavior: ScrollIntoViewBehavior::Auto,
250 }
251 }
252
253 #[must_use] pub const fn end() -> Self {
255 Self {
256 block: ScrollLogicalPosition::End,
257 inline_axis: ScrollLogicalPosition::End,
258 behavior: ScrollIntoViewBehavior::Auto,
259 }
260 }
261
262 #[must_use] pub const fn with_instant(mut self) -> Self {
264 self.behavior = ScrollIntoViewBehavior::Instant;
265 self
266 }
267
268 #[must_use] pub const fn with_smooth(mut self) -> Self {
270 self.behavior = ScrollIntoViewBehavior::Smooth;
271 self
272 }
273}
274
275#[repr(C)]
280#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
281pub enum ScrollLogicalPosition {
282 Start,
284 Center,
286 End,
288 #[default]
290 Nearest,
291}
292
293#[repr(C)]
298#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
299pub enum ScrollIntoViewBehavior {
300 #[default]
302 Auto,
303 Instant,
305 Smooth,
307}
308
309#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
311#[repr(C)]
312pub enum LifecycleReason {
313 InitialMount,
315 Remount,
317 Resize,
319 Update,
321 Unmount,
323}
324
325#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)]
327#[repr(C)]
328pub struct KeyModifiers {
329 pub shift: bool,
330 pub ctrl: bool,
331 pub alt: bool,
332 pub meta: bool,
333}
334
335impl KeyModifiers {
336 #[must_use] pub fn new() -> Self {
337 Self::default()
338 }
339
340 #[must_use] pub const fn with_shift(mut self) -> Self {
341 self.shift = true;
342 self
343 }
344
345 #[must_use] pub const fn with_ctrl(mut self) -> Self {
346 self.ctrl = true;
347 self
348 }
349
350 #[must_use] pub const fn with_alt(mut self) -> Self {
351 self.alt = true;
352 self
353 }
354
355 #[must_use] pub const fn with_meta(mut self) -> Self {
356 self.meta = true;
357 self
358 }
359
360 #[must_use] pub const fn is_empty(&self) -> bool {
361 !self.shift && !self.ctrl && !self.alt && !self.meta
362 }
363}
364
365#[derive(Debug, Clone, Copy, PartialEq, Eq)]
367pub struct MouseEventData {
368 pub position: LogicalPosition,
370 pub button: MouseButton,
372 pub buttons: u8,
374 pub modifiers: KeyModifiers,
376}
377
378#[derive(Debug, Clone, Copy, PartialEq, Eq)]
380pub struct KeyboardEventData {
381 pub key_code: u32,
383 pub char_code: Option<char>,
385 pub modifiers: KeyModifiers,
387 pub repeat: bool,
389}
390
391#[derive(Debug, Clone, Copy, PartialEq, Eq)]
393pub struct ScrollEventData {
394 pub delta: LogicalPosition,
396 pub delta_mode: ScrollDeltaMode,
398}
399
400#[derive(Debug, Clone, Copy, PartialEq)]
402pub struct TouchEventData {
403 pub id: u64,
405 pub position: LogicalPosition,
407 pub force: f32,
409}
410
411#[derive(Debug, Clone, PartialEq, Eq)]
413pub struct ClipboardEventData {
414 pub content: Option<String>,
416}
417
418#[derive(Debug, Clone, Copy, PartialEq, Eq)]
420pub struct LifecycleEventData {
421 pub reason: LifecycleReason,
423 pub previous_bounds: Option<LogicalRect>,
425 pub current_bounds: LogicalRect,
427}
428
429#[derive(Debug, Clone, Copy, PartialEq, Eq)]
431pub struct WindowEventData {
432 pub size: Option<LogicalRect>,
434 pub position: Option<LogicalPosition>,
436}
437
438#[derive(Debug, Clone, PartialEq, Eq)]
446pub struct TextInputEventData {
447 pub inserted_text: String,
449 pub old_text: String,
451}
452
453#[derive(Debug, Clone, Copy, PartialEq, Eq)]
460pub struct DocumentEditEventData {
461 pub changeset_id: u64,
463}
464
465#[derive(Debug, Clone, PartialEq)]
467pub enum EventData {
468 Mouse(MouseEventData),
470 Keyboard(KeyboardEventData),
472 Scroll(ScrollEventData),
474 Touch(TouchEventData),
476 Clipboard(ClipboardEventData),
478 TextInput(TextInputEventData),
480 DocumentEdit(DocumentEditEventData),
482 Lifecycle(LifecycleEventData),
484 Window(WindowEventData),
486 None,
488}
489
490#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
496#[repr(C)]
497pub enum EventType {
498 MouseOver,
501 MouseEnter,
503 MouseLeave,
505 MouseOut,
507 MouseDown,
509 MouseUp,
511 Click,
513 DoubleClick,
515 ContextMenu,
517
518 KeyDown,
521 KeyUp,
523 KeyPress,
525
526 CompositionStart,
529 CompositionUpdate,
531 CompositionEnd,
533
534 Focus,
537 Blur,
539 FocusIn,
541 FocusOut,
543
544 Input,
547 Change,
549 Submit,
551 Reset,
553 Invalid,
555
556 Scroll,
559 ScrollStart,
561 ScrollEnd,
563
564 DragStart,
567 Drag,
569 DragEnd,
571 DragEnter,
573 DragOver,
575 DragLeave,
577 Drop,
579
580 TouchStart,
583 TouchMove,
585 TouchEnd,
587 TouchCancel,
589
590 PenDown,
593 PenMove,
595 PenUp,
597 PenEnter,
599 PenLeave,
601
602 LongPress,
605 SwipeLeft,
607 SwipeRight,
609 SwipeUp,
611 SwipeDown,
613 PinchIn,
615 PinchOut,
617 RotateClockwise,
619 RotateCounterClockwise,
621
622 Copy,
625 Cut,
627 Paste,
629
630 Play,
633 Pause,
635 Ended,
637 TimeUpdate,
639 VolumeChange,
641 MediaError,
643
644 Mount,
647 Unmount,
649 Update,
651 Resize,
653
654 WindowResize,
657 WindowMove,
659 WindowClose,
661 WindowFocusIn,
663 WindowFocusOut,
665 ThemeChange,
667 WindowDpiChanged,
669 WindowMonitorChanged,
671
672 MonitorConnected,
675 MonitorDisconnected,
677
678 FileHover,
681 FileDrop,
683 FileHoverCancel,
685
686 SensorChanged,
690 GamepadInput,
693
694 GeolocationFix,
700 GeolocationError,
703
704 PermissionChanged,
711 BiometricResult,
714 KeyringResult,
717
718 DocumentEdit,
728}
729
730#[derive(Debug, Clone, PartialEq)]
735pub struct SyntheticEvent {
736 pub event_type: EventType,
738
739 pub source: EventSource,
741
742 pub phase: EventPhase,
744
745 pub target: DomNodeId,
747
748 pub current_target: DomNodeId,
750
751 pub timestamp: Instant,
753
754 pub data: EventData,
756
757 pub stopped: bool,
759
760 pub stopped_immediate: bool,
762
763 pub prevented_default: bool,
765}
766
767impl SyntheticEvent {
768 #[must_use] pub const fn new(
773 event_type: EventType,
774 source: EventSource,
775 target: DomNodeId,
776 timestamp: Instant,
777 data: EventData,
778 ) -> Self {
779 Self {
780 event_type,
781 source,
782 phase: EventPhase::Target,
783 target,
784 current_target: target,
785 timestamp,
786 data,
787 stopped: false,
788 stopped_immediate: false,
789 prevented_default: false,
790 }
791 }
792
793 pub const fn stop_propagation(&mut self) {
798 self.stopped = true;
799 }
800
801 pub const fn stop_immediate_propagation(&mut self) {
806 self.stopped_immediate = true;
807 self.stopped = true;
808 }
809
810 pub const fn prevent_default(&mut self) {
815 self.prevented_default = true;
816 }
817
818 #[must_use] pub const fn is_propagation_stopped(&self) -> bool {
820 self.stopped
821 }
822
823 #[must_use] pub const fn is_immediate_propagation_stopped(&self) -> bool {
825 self.stopped_immediate
826 }
827
828 #[must_use] pub const fn is_default_prevented(&self) -> bool {
830 self.prevented_default
831 }
832}
833
834#[derive(Debug, Clone)]
836#[derive(Default)]
837pub struct PropagationResult {
838 pub callbacks_to_invoke: Vec<(NodeId, EventFilter)>,
840 pub default_prevented: bool,
842}
843
844#[must_use] pub fn get_dom_path(
851 node_hierarchy: &crate::id::NodeHierarchy,
852 target_node: NodeHierarchyItemId,
853) -> Vec<NodeId> {
854 let mut path = Vec::new();
855 let Some(target_node_id) = target_node.into_crate_internal() else {
856 return path;
857 };
858
859 let hier_ref = node_hierarchy.as_ref();
860
861 let node_count = hier_ref.len();
866 let mut visited: BTreeSet<NodeId> = BTreeSet::new();
867 let mut current = Some(target_node_id);
868 while let Some(node_id) = current {
869 if path.len() > node_count || !visited.insert(node_id) {
870 break;
872 }
873 path.push(node_id);
874 current = hier_ref.get(node_id).and_then(|node| node.parent);
875 }
876
877 path.reverse();
879 path
880}
881
882pub fn propagate_event(
896 event: &mut SyntheticEvent,
897 node_hierarchy: &crate::id::NodeHierarchy,
898 callbacks: &BTreeMap<NodeId, Vec<EventFilter>>,
899) -> PropagationResult {
900 let path = get_dom_path(node_hierarchy, event.target.node);
901 if path.is_empty() {
902 return PropagationResult::default();
903 }
904
905 let ancestors = &path[..path.len().saturating_sub(1)];
906 let target_node_id = *path.last().unwrap();
907
908 let mut result = PropagationResult::default();
909
910 propagate_phase(
912 event,
913 ancestors.iter().copied(),
914 EventPhase::Capture,
915 callbacks,
916 &mut result,
917 );
918
919 if !event.stopped {
921 propagate_target_phase(event, target_node_id, callbacks, &mut result);
922 }
923
924 if !event.stopped {
926 propagate_phase(
927 event,
928 ancestors.iter().rev().copied(),
929 EventPhase::Bubble,
930 callbacks,
931 &mut result,
932 );
933 }
934
935 result.default_prevented = event.prevented_default;
936 result
937}
938
939fn propagate_phase(
941 event: &mut SyntheticEvent,
942 nodes: impl Iterator<Item = NodeId>,
943 phase: EventPhase,
944 callbacks: &BTreeMap<NodeId, Vec<EventFilter>>,
945 result: &mut PropagationResult,
946) {
947 event.phase = phase;
948
949 for node_id in nodes {
950 if event.stopped_immediate || event.stopped {
951 return;
952 }
953
954 event.current_target = DomNodeId {
955 dom: event.target.dom,
956 node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
957 };
958
959 collect_matching_callbacks(event, node_id, phase, callbacks, result);
960 }
961}
962
963fn propagate_target_phase(
965 event: &mut SyntheticEvent,
966 target_node_id: NodeId,
967 callbacks: &BTreeMap<NodeId, Vec<EventFilter>>,
968 result: &mut PropagationResult,
969) {
970 event.phase = EventPhase::Target;
971 event.current_target = event.target;
972
973 collect_matching_callbacks(event, target_node_id, EventPhase::Target, callbacks, result);
974}
975
976fn collect_matching_callbacks(
978 event: &SyntheticEvent,
979 node_id: NodeId,
980 phase: EventPhase,
981 callbacks: &BTreeMap<NodeId, Vec<EventFilter>>,
982 result: &mut PropagationResult,
983) {
984 let Some(node_callbacks) = callbacks.get(&node_id) else {
985 return;
986 };
987
988 let matching = node_callbacks
989 .iter()
990 .take_while(|_| !event.stopped_immediate)
991 .filter(|filter| matches_filter_phase(**filter, event, phase))
992 .map(|filter| (node_id, *filter));
993
994 result.callbacks_to_invoke.extend(matching);
995}
996
997
998#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1017#[repr(C, u8)]
1018pub enum DefaultAction {
1019 FocusNext,
1021 FocusPrevious,
1023 FocusFirst,
1025 FocusLast,
1027 ClearFocus,
1029 ActivateFocusedElement {
1032 target: DomNodeId,
1033 },
1034 SubmitForm {
1036 form_node: DomNodeId,
1037 },
1038 CloseModal {
1040 modal_node: DomNodeId,
1041 },
1042 ScrollFocusedContainer {
1044 direction: ScrollDirection,
1045 amount: ScrollAmount,
1046 },
1047 SelectAllText,
1049 SplitBlockAtCursor {
1053 target: DomNodeId,
1054 },
1055 MergeWithPrevious {
1058 target: DomNodeId,
1059 },
1060 MergeWithNext {
1063 target: DomNodeId,
1064 },
1065 None,
1067}
1068
1069#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1071#[repr(C)]
1072pub enum ScrollAmount {
1073 Line,
1075 Page,
1077 Document,
1079}
1080
1081#[derive(Debug, Clone, Copy)]
1088#[repr(C)]
1089pub struct DefaultActionResult {
1090 pub action: DefaultAction,
1092 pub prevented: bool,
1094}
1095
1096impl Default for DefaultActionResult {
1097 fn default() -> Self {
1098 Self {
1099 action: DefaultAction::None,
1100 prevented: false,
1101 }
1102 }
1103}
1104
1105impl DefaultActionResult {
1106 #[must_use] pub const fn new(action: DefaultAction) -> Self {
1108 Self {
1109 action,
1110 prevented: false,
1111 }
1112 }
1113
1114 #[must_use] pub const fn prevented() -> Self {
1116 Self {
1117 action: DefaultAction::None,
1118 prevented: true,
1119 }
1120 }
1121
1122 #[must_use] pub const fn has_action(&self) -> bool {
1124 !self.prevented && !matches!(self.action, DefaultAction::None)
1125 }
1126}
1127
1128pub trait ActivationBehavior {
1140 fn has_activation_behavior(&self) -> bool;
1142
1143 fn is_activatable(&self) -> bool;
1146}
1147
1148pub trait Focusable {
1150 fn get_tabindex(&self) -> Option<i32>;
1152
1153 fn is_focusable(&self) -> bool;
1155
1156 fn is_in_tab_order(&self) -> bool {
1158 self.get_tabindex().map_or_else(|| self.is_naturally_focusable(), |i| i >= 0)
1159 }
1160
1161 fn is_naturally_focusable(&self) -> bool;
1164}
1165
1166fn matches_filter_phase(
1171 filter: EventFilter,
1172 event: &SyntheticEvent,
1173 current_phase: EventPhase,
1174) -> bool {
1175 if matches!(current_phase, EventPhase::Capture) {
1183 return false;
1184 }
1185
1186 match filter {
1187 EventFilter::Hover(hover_filter) => {
1188 matches_hover_filter(hover_filter, event, current_phase)
1189 }
1190 EventFilter::Focus(focus_filter) => {
1191 matches_focus_filter(focus_filter, event, current_phase)
1192 }
1193 EventFilter::Window(window_filter) => {
1194 matches_window_filter(window_filter, event, current_phase)
1195 }
1196 EventFilter::Component(component_filter) => {
1197 matches_component_filter(component_filter, event, current_phase)
1198 }
1199 EventFilter::Application(_) => {
1200 false
1202 }
1203 }
1204}
1205
1206const fn matches_component_filter(
1214 filter: ComponentEventFilter,
1215 event: &SyntheticEvent,
1216 _phase: EventPhase,
1217) -> bool {
1218 matches!(
1219 (filter, &event.event_type),
1220 (ComponentEventFilter::AfterMount, EventType::Mount)
1221 | (ComponentEventFilter::BeforeUnmount, EventType::Unmount)
1222 | (ComponentEventFilter::Updated, EventType::Update)
1223 | (ComponentEventFilter::NodeResized, EventType::Resize)
1224 )
1225}
1226
1227fn check_mouse_button(data: &EventData, expected: MouseButton) -> bool {
1229 if let EventData::Mouse(mouse_data) = data {
1230 mouse_data.button == expected
1231 } else {
1232 false
1233 }
1234}
1235
1236#[allow(clippy::match_same_arms)]
1241fn matches_hover_filter(
1242 filter: HoverEventFilter,
1243 event: &SyntheticEvent,
1244 _phase: EventPhase,
1245) -> bool {
1246 use HoverEventFilter::{MouseOver, MouseDown, LeftMouseDown, RightMouseDown, MiddleMouseDown, MouseUp, LeftMouseUp, RightMouseUp, MiddleMouseUp, MouseEnter, MouseLeave, Scroll, ScrollStart, ScrollEnd, TextInput, VirtualKeyDown, VirtualKeyUp, HoveredFile, DroppedFile, HoveredFileCancelled, TouchStart, TouchMove, TouchEnd, TouchCancel, PenDown, PenMove, PenUp, PenEnter, PenLeave, DragStart, Drag, DragEnd, DragEnter, DragOver, DragLeave, Drop, DoubleClick, SensorChanged, GamepadInput, GeolocationFix, GeolocationError, PermissionChanged, BiometricResult, KeyringResult};
1247
1248 match (filter, &event.event_type) {
1249 (MouseOver, EventType::MouseOver) => true,
1250 (MouseDown, EventType::MouseDown) => true,
1251 (LeftMouseDown, EventType::MouseDown) => check_mouse_button(&event.data, MouseButton::Left),
1252 (RightMouseDown, EventType::MouseDown) => {
1253 check_mouse_button(&event.data, MouseButton::Right)
1254 }
1255 (MiddleMouseDown, EventType::MouseDown) => {
1256 check_mouse_button(&event.data, MouseButton::Middle)
1257 }
1258 (MouseUp, EventType::MouseUp) => true,
1259 (LeftMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Left),
1260 (RightMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Right),
1261 (MiddleMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Middle),
1262 (MouseEnter, EventType::MouseEnter) => true,
1263 (MouseLeave, EventType::MouseLeave) => true,
1264 (Scroll, EventType::Scroll) => true,
1265 (ScrollStart, EventType::ScrollStart) => true,
1266 (ScrollEnd, EventType::ScrollEnd) => true,
1267 (TextInput, EventType::Input) => true,
1268 (VirtualKeyDown, EventType::KeyDown) => true,
1269 (VirtualKeyUp, EventType::KeyUp) => true,
1270 (HoveredFile, EventType::FileHover) => true,
1271 (DroppedFile, EventType::FileDrop) => true,
1272 (HoveredFileCancelled, EventType::FileHoverCancel) => true,
1273 (TouchStart, EventType::TouchStart) => true,
1274 (TouchMove, EventType::TouchMove) => true,
1275 (TouchEnd, EventType::TouchEnd) => true,
1276 (TouchCancel, EventType::TouchCancel) => true,
1277 (PenDown, EventType::PenDown) => true,
1278 (PenMove, EventType::PenMove) => true,
1279 (PenUp, EventType::PenUp) => true,
1280 (PenEnter, EventType::PenEnter) => true,
1281 (PenLeave, EventType::PenLeave) => true,
1282 (DragStart, EventType::DragStart) => true,
1283 (Drag, EventType::Drag) => true,
1284 (DragEnd, EventType::DragEnd) => true,
1285 (DragEnter, EventType::DragEnter) => true,
1286 (DragOver, EventType::DragOver) => true,
1287 (DragLeave, EventType::DragLeave) => true,
1288 (Drop, EventType::Drop) => true,
1289 (DoubleClick, EventType::DoubleClick) => true,
1290 (SensorChanged, EventType::SensorChanged) => true,
1291 (GamepadInput, EventType::GamepadInput) => true,
1292 (GeolocationFix, EventType::GeolocationFix) => true,
1293 (GeolocationError, EventType::GeolocationError) => true,
1294 (PermissionChanged, EventType::PermissionChanged) => true,
1295 (BiometricResult, EventType::BiometricResult) => true,
1296 (KeyringResult, EventType::KeyringResult) => true,
1297 _ => false,
1298 }
1299}
1300
1301#[allow(clippy::match_same_arms)]
1304fn matches_focus_filter(
1305 filter: FocusEventFilter,
1306 event: &SyntheticEvent,
1307 _phase: EventPhase,
1308) -> bool {
1309 use FocusEventFilter::{MouseOver, MouseDown, LeftMouseDown, RightMouseDown, MiddleMouseDown, MouseUp, LeftMouseUp, RightMouseUp, MiddleMouseUp, MouseEnter, MouseLeave, Scroll, ScrollStart, ScrollEnd, TextInput, VirtualKeyDown, VirtualKeyUp, FocusReceived, FocusLost, DragStart, Drag, DragEnd, DragEnter, DragOver, DragLeave, Drop};
1310
1311 match (filter, &event.event_type) {
1312 (MouseOver, EventType::MouseOver) => true,
1313 (MouseDown, EventType::MouseDown) => true,
1314 (LeftMouseDown, EventType::MouseDown) => check_mouse_button(&event.data, MouseButton::Left),
1315 (RightMouseDown, EventType::MouseDown) => {
1316 check_mouse_button(&event.data, MouseButton::Right)
1317 }
1318 (MiddleMouseDown, EventType::MouseDown) => {
1319 check_mouse_button(&event.data, MouseButton::Middle)
1320 }
1321 (MouseUp, EventType::MouseUp) => true,
1322 (LeftMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Left),
1323 (RightMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Right),
1324 (MiddleMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Middle),
1325 (MouseEnter, EventType::MouseEnter) => true,
1326 (MouseLeave, EventType::MouseLeave) => true,
1327 (Scroll, EventType::Scroll) => true,
1328 (ScrollStart, EventType::ScrollStart) => true,
1329 (ScrollEnd, EventType::ScrollEnd) => true,
1330 (TextInput, EventType::Input) => true,
1331 (FocusEventFilter::DocumentEdit, EventType::DocumentEdit) => true,
1332 (VirtualKeyDown, EventType::KeyDown) => true,
1333 (VirtualKeyUp, EventType::KeyUp) => true,
1334 (FocusReceived, EventType::Focus) => true,
1335 (FocusLost, EventType::Blur) => true,
1336 (DragStart, EventType::DragStart) => true,
1337 (Drag, EventType::Drag) => true,
1338 (DragEnd, EventType::DragEnd) => true,
1339 (DragEnter, EventType::DragEnter) => true,
1340 (DragOver, EventType::DragOver) => true,
1341 (DragLeave, EventType::DragLeave) => true,
1342 (Drop, EventType::Drop) => true,
1343 (FocusEventFilter::Copy, EventType::Copy) => true,
1347 (FocusEventFilter::Cut, EventType::Cut) => true,
1348 (FocusEventFilter::Paste, EventType::Paste) => true,
1349 _ => false,
1350 }
1351}
1352
1353#[allow(clippy::match_same_arms)]
1356fn matches_window_filter(
1357 filter: WindowEventFilter,
1358 event: &SyntheticEvent,
1359 _phase: EventPhase,
1360) -> bool {
1361 use WindowEventFilter::{MouseOver, MouseDown, LeftMouseDown, RightMouseDown, MiddleMouseDown, MouseUp, LeftMouseUp, RightMouseUp, MiddleMouseUp, MouseEnter, MouseLeave, Scroll, ScrollStart, ScrollEnd, TextInput, VirtualKeyDown, VirtualKeyUp, HoveredFile, DroppedFile, HoveredFileCancelled, Resized, Moved, TouchStart, TouchMove, TouchEnd, TouchCancel, PenDown, PenMove, PenUp, PenEnter, PenLeave, FocusReceived, FocusLost, CloseRequested, ThemeChanged, WindowFocusReceived, WindowFocusLost, SensorChanged, GamepadInput, GeolocationFix, GeolocationError, PermissionChanged, BiometricResult, KeyringResult, DragStart, Drag, DragEnd, DragEnter, DragOver, DragLeave, Drop};
1362
1363 match (filter, &event.event_type) {
1364 (MouseOver, EventType::MouseOver) => true,
1365 (MouseDown, EventType::MouseDown) => true,
1366 (LeftMouseDown, EventType::MouseDown) => check_mouse_button(&event.data, MouseButton::Left),
1367 (RightMouseDown, EventType::MouseDown) => {
1368 check_mouse_button(&event.data, MouseButton::Right)
1369 }
1370 (MiddleMouseDown, EventType::MouseDown) => {
1371 check_mouse_button(&event.data, MouseButton::Middle)
1372 }
1373 (MouseUp, EventType::MouseUp) => true,
1374 (LeftMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Left),
1375 (RightMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Right),
1376 (MiddleMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Middle),
1377 (MouseEnter, EventType::MouseEnter) => true,
1378 (MouseLeave, EventType::MouseLeave) => true,
1379 (Scroll, EventType::Scroll) => true,
1380 (ScrollStart, EventType::ScrollStart) => true,
1381 (ScrollEnd, EventType::ScrollEnd) => true,
1382 (TextInput, EventType::Input) => true,
1383 (VirtualKeyDown, EventType::KeyDown) => true,
1384 (VirtualKeyUp, EventType::KeyUp) => true,
1385 (HoveredFile, EventType::FileHover) => true,
1386 (DroppedFile, EventType::FileDrop) => true,
1387 (HoveredFileCancelled, EventType::FileHoverCancel) => true,
1388 (Resized, EventType::WindowResize) => true,
1389 (Moved, EventType::WindowMove) => true,
1390 (TouchStart, EventType::TouchStart) => true,
1391 (TouchMove, EventType::TouchMove) => true,
1392 (TouchEnd, EventType::TouchEnd) => true,
1393 (TouchCancel, EventType::TouchCancel) => true,
1394 (PenDown, EventType::PenDown) => true,
1395 (PenMove, EventType::PenMove) => true,
1396 (PenUp, EventType::PenUp) => true,
1397 (PenEnter, EventType::PenEnter) => true,
1398 (PenLeave, EventType::PenLeave) => true,
1399 (FocusReceived, EventType::Focus) => true,
1400 (FocusLost, EventType::Blur) => true,
1401 (CloseRequested, EventType::WindowClose) => true,
1402 (ThemeChanged, EventType::ThemeChange) => true,
1403 (WindowFocusReceived, EventType::WindowFocusIn) => true,
1404 (WindowFocusLost, EventType::WindowFocusOut) => true,
1405 (SensorChanged, EventType::SensorChanged) => true,
1406 (GamepadInput, EventType::GamepadInput) => true,
1407 (GeolocationFix, EventType::GeolocationFix) => true,
1408 (GeolocationError, EventType::GeolocationError) => true,
1409 (PermissionChanged, EventType::PermissionChanged) => true,
1410 (BiometricResult, EventType::BiometricResult) => true,
1411 (KeyringResult, EventType::KeyringResult) => true,
1412 (DragStart, EventType::DragStart) => true,
1413 (Drag, EventType::Drag) => true,
1414 (DragEnd, EventType::DragEnd) => true,
1415 (DragEnter, EventType::DragEnter) => true,
1416 (DragOver, EventType::DragOver) => true,
1417 (DragLeave, EventType::DragLeave) => true,
1418 (Drop, EventType::Drop) => true,
1419 _ => false,
1420 }
1421}
1422
1423#[allow(clippy::needless_pass_by_value)] #[must_use] pub fn detect_lifecycle_events(
1432 old_dom_id: DomId,
1433 new_dom_id: DomId,
1434 old_hierarchy: Option<&crate::id::NodeHierarchy>,
1435 new_hierarchy: Option<&crate::id::NodeHierarchy>,
1436 old_layout: Option<&BTreeMap<NodeId, LogicalRect>>,
1437 new_layout: Option<&BTreeMap<NodeId, LogicalRect>>,
1438 timestamp: Instant,
1439) -> Vec<SyntheticEvent> {
1440 let old_nodes = collect_node_ids(old_hierarchy);
1441 let new_nodes = collect_node_ids(new_hierarchy);
1442
1443 let mut events = Vec::new();
1444
1445 if let Some(layout) = new_layout {
1447 for &node_id in new_nodes.difference(&old_nodes) {
1448 events.push(create_mount_event(node_id, new_dom_id, layout, ×tamp));
1449 }
1450 }
1451
1452 if let Some(layout) = old_layout {
1454 for &node_id in old_nodes.difference(&new_nodes) {
1455 events.push(create_unmount_event(
1456 node_id, old_dom_id, layout, ×tamp,
1457 ));
1458 }
1459 }
1460
1461 if let (Some(old_l), Some(new_l)) = (old_layout, new_layout) {
1463 for &node_id in old_nodes.intersection(&new_nodes) {
1464 if let Some(ev) = create_resize_event(node_id, new_dom_id, old_l, new_l, ×tamp) {
1465 events.push(ev);
1466 }
1467 }
1468 }
1469
1470 events
1471}
1472
1473fn collect_node_ids(hierarchy: Option<&crate::id::NodeHierarchy>) -> BTreeSet<NodeId> {
1474 hierarchy
1475 .map(|h| h.as_ref().linear_iter().collect())
1476 .unwrap_or_default()
1477}
1478
1479fn create_lifecycle_event(
1480 event_type: EventType,
1481 node_id: NodeId,
1482 dom_id: DomId,
1483 timestamp: &Instant,
1484 data: LifecycleEventData,
1485) -> SyntheticEvent {
1486 let dom_node_id = DomNodeId {
1487 dom: dom_id,
1488 node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
1489 };
1490 SyntheticEvent {
1491 event_type,
1492 source: EventSource::Lifecycle,
1493 phase: EventPhase::Target,
1494 target: dom_node_id,
1495 current_target: dom_node_id,
1496 timestamp: timestamp.clone(),
1497 data: EventData::Lifecycle(data),
1498 stopped: false,
1499 stopped_immediate: false,
1500 prevented_default: false,
1501 }
1502}
1503
1504fn create_mount_event(
1505 node_id: NodeId,
1506 dom_id: DomId,
1507 layout: &BTreeMap<NodeId, LogicalRect>,
1508 timestamp: &Instant,
1509) -> SyntheticEvent {
1510 let current_bounds = layout.get(&node_id).copied().unwrap_or(LogicalRect::zero());
1511 create_lifecycle_event(
1512 EventType::Mount,
1513 node_id,
1514 dom_id,
1515 timestamp,
1516 LifecycleEventData {
1517 reason: LifecycleReason::InitialMount,
1518 previous_bounds: None,
1519 current_bounds,
1520 },
1521 )
1522}
1523
1524fn create_unmount_event(
1525 node_id: NodeId,
1526 dom_id: DomId,
1527 layout: &BTreeMap<NodeId, LogicalRect>,
1528 timestamp: &Instant,
1529) -> SyntheticEvent {
1530 let previous_bounds = layout.get(&node_id).copied().unwrap_or(LogicalRect::zero());
1531 create_lifecycle_event(
1532 EventType::Unmount,
1533 node_id,
1534 dom_id,
1535 timestamp,
1536 LifecycleEventData {
1537 reason: LifecycleReason::Unmount,
1538 previous_bounds: Some(previous_bounds),
1539 current_bounds: LogicalRect::zero(),
1540 },
1541 )
1542}
1543
1544fn size_changed(old: crate::geom::LogicalSize, new: crate::geom::LogicalSize) -> bool {
1548 fn dim_changed(a: f32, b: f32) -> bool {
1549 if a.is_nan() && b.is_nan() {
1550 return false;
1551 }
1552 #[allow(clippy::cast_possible_truncation)] let q = |v: f32| -> i64 {
1557 if v.is_nan() {
1558 i64::MIN
1559 } else {
1560 (v * 1000.0) as i64
1561 }
1562 };
1563 q(a) != q(b)
1564 }
1565 dim_changed(old.width, new.width) || dim_changed(old.height, new.height)
1566}
1567
1568fn create_resize_event(
1569 node_id: NodeId,
1570 dom_id: DomId,
1571 old_layout: &BTreeMap<NodeId, LogicalRect>,
1572 new_layout: &BTreeMap<NodeId, LogicalRect>,
1573 timestamp: &Instant,
1574) -> Option<SyntheticEvent> {
1575 let old_bounds = *old_layout.get(&node_id)?;
1576 let new_bounds = *new_layout.get(&node_id)?;
1577
1578 if !size_changed(old_bounds.size, new_bounds.size) {
1584 return None;
1585 }
1586
1587 Some(create_lifecycle_event(
1588 EventType::Resize,
1589 node_id,
1590 dom_id,
1591 timestamp,
1592 LifecycleEventData {
1593 reason: LifecycleReason::Resize,
1594 previous_bounds: Some(old_bounds),
1595 current_bounds: new_bounds,
1596 },
1597 ))
1598}
1599
1600#[derive(Debug, Clone, Default)]
1605pub struct LifecycleEventResult {
1606 pub events: Vec<SyntheticEvent>,
1608 pub node_id_mapping: OrderedMap<NodeId, NodeId>,
1611}
1612
1613#[must_use] pub fn detect_lifecycle_events_with_reconciliation(
1667 dom_id: DomId,
1668 old_node_data: &[crate::dom::NodeData],
1669 new_node_data: &[crate::dom::NodeData],
1670 old_hierarchy: &[crate::styled_dom::NodeHierarchyItem],
1671 new_hierarchy: &[crate::styled_dom::NodeHierarchyItem],
1672 old_layout: &OrderedMap<NodeId, LogicalRect>,
1673 new_layout: &OrderedMap<NodeId, LogicalRect>,
1674 timestamp: Instant,
1675) -> LifecycleEventResult {
1676 let diff_result = crate::diff::reconcile_dom(
1677 old_node_data,
1678 new_node_data,
1679 old_hierarchy,
1680 new_hierarchy,
1681 old_layout,
1682 new_layout,
1683 dom_id,
1684 timestamp,
1685 );
1686
1687 LifecycleEventResult {
1688 events: diff_result.events,
1689 node_id_mapping: crate::diff::create_migration_map(&diff_result.node_moves),
1690 }
1691}
1692
1693#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1695#[repr(C)]
1696pub enum HoverEventFilter {
1697 MouseOver,
1699 MouseDown,
1701 LeftMouseDown,
1703 RightMouseDown,
1705 MiddleMouseDown,
1707 MouseUp,
1709 LeftMouseUp,
1711 RightMouseUp,
1713 MiddleMouseUp,
1715 MouseEnter,
1717 MouseLeave,
1719 Scroll,
1721 ScrollStart,
1723 ScrollEnd,
1725 TextInput,
1727 VirtualKeyDown,
1729 VirtualKeyUp,
1731 HoveredFile,
1733 DroppedFile,
1735 HoveredFileCancelled,
1737 TouchStart,
1739 TouchMove,
1741 TouchEnd,
1743 TouchCancel,
1745 PenDown,
1747 PenMove,
1749 PenUp,
1751 PenEnter,
1753 PenLeave,
1755 PenSqueeze,
1760 PenDoubleTap,
1763 PenHover,
1769 GeolocationFix,
1773 GeolocationError,
1776 SensorChanged,
1779 GamepadInput,
1782 DragStart,
1784 Drag,
1786 DragEnd,
1788 DragEnter,
1790 DragOver,
1792 DragLeave,
1794 Drop,
1796 DoubleClick,
1798 LongPress,
1800 SwipeLeft,
1802 SwipeRight,
1804 SwipeUp,
1806 SwipeDown,
1808 PinchIn,
1810 PinchOut,
1812 RotateClockwise,
1814 RotateCounterClockwise,
1816
1817 MouseOut,
1820
1821 FocusIn,
1824 FocusOut,
1826
1827 CompositionStart,
1830 CompositionUpdate,
1832 CompositionEnd,
1834
1835 #[doc(hidden)]
1837 SystemTextSingleClick,
1839 #[doc(hidden)]
1840 SystemTextDoubleClick,
1842 #[doc(hidden)]
1843 SystemTextTripleClick,
1845
1846 PermissionChanged,
1850 BiometricResult,
1852 KeyringResult,
1854}
1855
1856impl HoverEventFilter {
1857 #[must_use] pub const fn is_system_internal(&self) -> bool {
1859 matches!(
1860 self,
1861 Self::SystemTextSingleClick
1862 | Self::SystemTextDoubleClick
1863 | Self::SystemTextTripleClick
1864 )
1865 }
1866
1867 #[allow(clippy::match_same_arms)]
1870 #[must_use] pub const fn to_focus_event_filter(&self) -> Option<FocusEventFilter> {
1871 match self {
1872 Self::MouseOver => Some(FocusEventFilter::MouseOver),
1873 Self::MouseDown => Some(FocusEventFilter::MouseDown),
1874 Self::LeftMouseDown => Some(FocusEventFilter::LeftMouseDown),
1875 Self::RightMouseDown => Some(FocusEventFilter::RightMouseDown),
1876 Self::MiddleMouseDown => Some(FocusEventFilter::MiddleMouseDown),
1877 Self::MouseUp => Some(FocusEventFilter::MouseUp),
1878 Self::LeftMouseUp => Some(FocusEventFilter::LeftMouseUp),
1879 Self::RightMouseUp => Some(FocusEventFilter::RightMouseUp),
1880 Self::MiddleMouseUp => Some(FocusEventFilter::MiddleMouseUp),
1881 Self::MouseEnter => Some(FocusEventFilter::MouseEnter),
1882 Self::MouseLeave => Some(FocusEventFilter::MouseLeave),
1883 Self::Scroll => Some(FocusEventFilter::Scroll),
1884 Self::ScrollStart => Some(FocusEventFilter::ScrollStart),
1885 Self::ScrollEnd => Some(FocusEventFilter::ScrollEnd),
1886 Self::TextInput => Some(FocusEventFilter::TextInput),
1887 Self::VirtualKeyDown => Some(FocusEventFilter::VirtualKeyDown),
1888 Self::VirtualKeyUp => Some(FocusEventFilter::VirtualKeyUp),
1889 Self::HoveredFile => None,
1890 Self::DroppedFile => None,
1891 Self::HoveredFileCancelled => None,
1892 Self::TouchStart => None,
1893 Self::TouchMove => None,
1894 Self::TouchEnd => None,
1895 Self::TouchCancel => None,
1896 Self::PenDown => Some(FocusEventFilter::PenDown),
1897 Self::PenMove => Some(FocusEventFilter::PenMove),
1898 Self::PenUp => Some(FocusEventFilter::PenUp),
1899 Self::PenEnter => None,
1900 Self::PenLeave => None,
1901 Self::PenSqueeze => None,
1902 Self::PenDoubleTap => None,
1903 Self::PenHover => None,
1904 Self::GeolocationFix => None,
1905 Self::GeolocationError => None,
1906 Self::SensorChanged => None,
1907 Self::GamepadInput => None,
1908 Self::DragStart => Some(FocusEventFilter::DragStart),
1909 Self::Drag => Some(FocusEventFilter::Drag),
1910 Self::DragEnd => Some(FocusEventFilter::DragEnd),
1911 Self::DragEnter => Some(FocusEventFilter::DragEnter),
1912 Self::DragOver => Some(FocusEventFilter::DragOver),
1913 Self::DragLeave => Some(FocusEventFilter::DragLeave),
1914 Self::Drop => Some(FocusEventFilter::Drop),
1915 Self::DoubleClick => Some(FocusEventFilter::DoubleClick),
1916 Self::LongPress => Some(FocusEventFilter::LongPress),
1917 Self::SwipeLeft => Some(FocusEventFilter::SwipeLeft),
1918 Self::SwipeRight => Some(FocusEventFilter::SwipeRight),
1919 Self::SwipeUp => Some(FocusEventFilter::SwipeUp),
1920 Self::SwipeDown => Some(FocusEventFilter::SwipeDown),
1921 Self::PinchIn => Some(FocusEventFilter::PinchIn),
1922 Self::PinchOut => Some(FocusEventFilter::PinchOut),
1923 Self::RotateClockwise => Some(FocusEventFilter::RotateClockwise),
1924 Self::RotateCounterClockwise => {
1925 Some(FocusEventFilter::RotateCounterClockwise)
1926 }
1927 Self::MouseOut => Some(FocusEventFilter::MouseLeave), Self::FocusIn => Some(FocusEventFilter::FocusIn),
1929 Self::FocusOut => Some(FocusEventFilter::FocusOut),
1930 Self::CompositionStart => Some(FocusEventFilter::CompositionStart),
1931 Self::CompositionUpdate => Some(FocusEventFilter::CompositionUpdate),
1932 Self::CompositionEnd => Some(FocusEventFilter::CompositionEnd),
1933 Self::SystemTextSingleClick => None,
1935 Self::SystemTextDoubleClick => None,
1936 Self::SystemTextTripleClick => None,
1937 Self::PermissionChanged => None,
1939 Self::BiometricResult => None,
1940 Self::KeyringResult => None,
1941 }
1942 }
1943}
1944
1945#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1950#[repr(C)]
1951pub enum FocusEventFilter {
1952 MouseOver,
1954 MouseDown,
1956 LeftMouseDown,
1958 RightMouseDown,
1960 MiddleMouseDown,
1962 MouseUp,
1964 LeftMouseUp,
1966 RightMouseUp,
1968 MiddleMouseUp,
1970 MouseEnter,
1972 MouseLeave,
1974 Scroll,
1976 ScrollStart,
1978 ScrollEnd,
1980 TextInput,
1982 VirtualKeyDown,
1984 VirtualKeyUp,
1986 FocusReceived,
1988 FocusLost,
1990 PenDown,
1992 PenMove,
1994 PenUp,
1996 DragStart,
1998 Drag,
2000 DragEnd,
2002 DragEnter,
2004 DragOver,
2006 DragLeave,
2008 Drop,
2010 DoubleClick,
2012 LongPress,
2014 SwipeLeft,
2016 SwipeRight,
2018 SwipeUp,
2020 SwipeDown,
2022 PinchIn,
2024 PinchOut,
2026 RotateClockwise,
2028 RotateCounterClockwise,
2030
2031 FocusIn,
2034 FocusOut,
2036
2037 CompositionStart,
2040 CompositionUpdate,
2042 CompositionEnd,
2044
2045 Copy,
2051 Cut,
2053 Paste,
2055 DocumentEdit,
2059}
2060
2061#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
2064#[repr(C)]
2065pub enum WindowEventFilter {
2066 MouseOver,
2068 MouseDown,
2070 LeftMouseDown,
2072 RightMouseDown,
2074 MiddleMouseDown,
2076 MouseUp,
2078 LeftMouseUp,
2080 RightMouseUp,
2082 MiddleMouseUp,
2084 MouseEnter,
2086 MouseLeave,
2088 Scroll,
2090 ScrollStart,
2092 ScrollEnd,
2094 TextInput,
2096 VirtualKeyDown,
2098 VirtualKeyUp,
2100 HoveredFile,
2102 DroppedFile,
2104 HoveredFileCancelled,
2106 Resized,
2108 Moved,
2110 TouchStart,
2112 TouchMove,
2114 TouchEnd,
2116 TouchCancel,
2118 FocusReceived,
2120 FocusLost,
2122 CloseRequested,
2124 ThemeChanged,
2126 WindowFocusReceived,
2128 WindowFocusLost,
2130 PenDown,
2132 PenMove,
2134 PenUp,
2136 PenEnter,
2138 PenLeave,
2140 PenSqueeze,
2143 PenDoubleTap,
2146 PenHover,
2149 GeolocationFix,
2157 GeolocationError,
2160 SensorChanged,
2163 GamepadInput,
2166 DragStart,
2168 Drag,
2170 DragEnd,
2172 DragEnter,
2174 DragOver,
2176 DragLeave,
2178 Drop,
2180 DoubleClick,
2182 LongPress,
2184 SwipeLeft,
2186 SwipeRight,
2188 SwipeUp,
2190 SwipeDown,
2192 PinchIn,
2194 PinchOut,
2196 RotateClockwise,
2198 RotateCounterClockwise,
2200 DpiChanged,
2203 MonitorChanged,
2206
2207 PermissionChanged,
2211 BiometricResult,
2213 KeyringResult,
2215}
2216
2217impl WindowEventFilter {
2218 #[allow(clippy::match_same_arms)]
2220 #[must_use] pub const fn to_hover_event_filter(&self) -> Option<HoverEventFilter> {
2221 match self {
2222 Self::MouseOver => Some(HoverEventFilter::MouseOver),
2223 Self::MouseDown => Some(HoverEventFilter::MouseDown),
2224 Self::LeftMouseDown => Some(HoverEventFilter::LeftMouseDown),
2225 Self::RightMouseDown => Some(HoverEventFilter::RightMouseDown),
2226 Self::MiddleMouseDown => Some(HoverEventFilter::MiddleMouseDown),
2227 Self::MouseUp => Some(HoverEventFilter::MouseUp),
2228 Self::LeftMouseUp => Some(HoverEventFilter::LeftMouseUp),
2229 Self::RightMouseUp => Some(HoverEventFilter::RightMouseUp),
2230 Self::MiddleMouseUp => Some(HoverEventFilter::MiddleMouseUp),
2231 Self::Scroll => Some(HoverEventFilter::Scroll),
2232 Self::ScrollStart => Some(HoverEventFilter::ScrollStart),
2233 Self::ScrollEnd => Some(HoverEventFilter::ScrollEnd),
2234 Self::TextInput => Some(HoverEventFilter::TextInput),
2235 Self::VirtualKeyDown => Some(HoverEventFilter::VirtualKeyDown),
2236 Self::VirtualKeyUp => Some(HoverEventFilter::VirtualKeyUp),
2237 Self::HoveredFile => Some(HoverEventFilter::HoveredFile),
2238 Self::DroppedFile => Some(HoverEventFilter::DroppedFile),
2239 Self::HoveredFileCancelled => Some(HoverEventFilter::HoveredFileCancelled),
2240 Self::MouseEnter => None,
2243 Self::MouseLeave => None,
2244 Self::Resized => None,
2245 Self::Moved => None,
2246 Self::TouchStart => Some(HoverEventFilter::TouchStart),
2247 Self::TouchMove => Some(HoverEventFilter::TouchMove),
2248 Self::TouchEnd => Some(HoverEventFilter::TouchEnd),
2249 Self::TouchCancel => Some(HoverEventFilter::TouchCancel),
2250 Self::FocusReceived => None,
2251 Self::FocusLost => None,
2252 Self::CloseRequested => None,
2253 Self::ThemeChanged => None,
2254 Self::WindowFocusReceived => None, Self::WindowFocusLost => None, Self::PenDown => Some(HoverEventFilter::PenDown),
2257 Self::PenMove => Some(HoverEventFilter::PenMove),
2258 Self::PenUp => Some(HoverEventFilter::PenUp),
2259 Self::PenEnter => Some(HoverEventFilter::PenEnter),
2260 Self::PenLeave => Some(HoverEventFilter::PenLeave),
2261 Self::PenSqueeze => Some(HoverEventFilter::PenSqueeze),
2262 Self::PenDoubleTap => Some(HoverEventFilter::PenDoubleTap),
2263 Self::PenHover => Some(HoverEventFilter::PenHover),
2264 Self::GeolocationFix => Some(HoverEventFilter::GeolocationFix),
2265 Self::GeolocationError => Some(HoverEventFilter::GeolocationError),
2266 Self::SensorChanged => Some(HoverEventFilter::SensorChanged),
2267 Self::GamepadInput => Some(HoverEventFilter::GamepadInput),
2268 Self::DragStart => Some(HoverEventFilter::DragStart),
2269 Self::Drag => Some(HoverEventFilter::Drag),
2270 Self::DragEnd => Some(HoverEventFilter::DragEnd),
2271 Self::DragEnter => Some(HoverEventFilter::DragEnter),
2272 Self::DragOver => Some(HoverEventFilter::DragOver),
2273 Self::DragLeave => Some(HoverEventFilter::DragLeave),
2274 Self::Drop => Some(HoverEventFilter::Drop),
2275 Self::DoubleClick => Some(HoverEventFilter::DoubleClick),
2276 Self::LongPress => Some(HoverEventFilter::LongPress),
2277 Self::SwipeLeft => Some(HoverEventFilter::SwipeLeft),
2278 Self::SwipeRight => Some(HoverEventFilter::SwipeRight),
2279 Self::SwipeUp => Some(HoverEventFilter::SwipeUp),
2280 Self::SwipeDown => Some(HoverEventFilter::SwipeDown),
2281 Self::PinchIn => Some(HoverEventFilter::PinchIn),
2282 Self::PinchOut => Some(HoverEventFilter::PinchOut),
2283 Self::RotateClockwise => Some(HoverEventFilter::RotateClockwise),
2284 Self::RotateCounterClockwise => {
2285 Some(HoverEventFilter::RotateCounterClockwise)
2286 }
2287 Self::DpiChanged => None,
2289 Self::MonitorChanged => None,
2290 Self::PermissionChanged => Some(HoverEventFilter::PermissionChanged),
2292 Self::BiometricResult => Some(HoverEventFilter::BiometricResult),
2293 Self::KeyringResult => Some(HoverEventFilter::KeyringResult),
2294 }
2295 }
2296}
2297
2298#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2300#[repr(C)]
2301pub enum ComponentEventFilter {
2302 AfterMount,
2304 BeforeUnmount,
2306 NodeResized,
2308 DefaultAction,
2310 Selected,
2312 Updated,
2314}
2315
2316#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2318#[repr(C)]
2319pub enum ApplicationEventFilter {
2320 DeviceConnected,
2322 DeviceDisconnected,
2324 MonitorConnected,
2327 MonitorDisconnected,
2329}
2330
2331#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
2336#[repr(C, u8)]
2337pub enum EventFilter {
2338 Hover(HoverEventFilter),
2341 Focus(FocusEventFilter),
2343 Window(WindowEventFilter),
2353 Component(ComponentEventFilter),
2355 Application(ApplicationEventFilter),
2357}
2358
2359impl EventFilter {
2360 #[must_use] pub const fn is_focus_callback(&self) -> bool {
2361 matches!(self, Self::Focus(_))
2362 }
2363 #[must_use] pub const fn is_window_callback(&self) -> bool {
2364 matches!(self, Self::Window(_))
2365 }
2366}
2367
2368macro_rules! get_single_enum_type {
2371 ($fn_name:ident, $enum_name:ident:: $variant:ident($return_type:ty)) => {
2372 #[must_use] pub const fn $fn_name(&self) -> Option<$return_type> {
2373 use self::$enum_name::*;
2374 match self {
2375 $variant(e) => Some(*e),
2376 _ => None,
2377 }
2378 }
2379 };
2380}
2381
2382impl EventFilter {
2383 get_single_enum_type!(as_hover_event_filter, EventFilter::Hover(HoverEventFilter));
2384 get_single_enum_type!(as_focus_event_filter, EventFilter::Focus(FocusEventFilter));
2385 get_single_enum_type!(
2386 as_window_event_filter,
2387 EventFilter::Window(WindowEventFilter)
2388 );
2389}
2390
2391impl From<On> for EventFilter {
2397 #[allow(clippy::match_same_arms)]
2401 fn from(input: On) -> Self {
2402 use crate::dom::On::{MouseOver, MouseDown, LeftMouseDown, MiddleMouseDown, RightMouseDown, MouseUp, LeftMouseUp, MiddleMouseUp, RightMouseUp, MouseEnter, MouseLeave, Scroll, TextInput, VirtualKeyDown, VirtualKeyUp, HoveredFile, DroppedFile, HoveredFileCancelled, FocusReceived, FocusLost, Default, Collapse, Expand, Increment, Decrement};
2403 match input {
2404 MouseOver => Self::Hover(HoverEventFilter::MouseOver),
2405 MouseDown => Self::Hover(HoverEventFilter::MouseDown),
2406 LeftMouseDown => Self::Hover(HoverEventFilter::LeftMouseDown),
2407 MiddleMouseDown => Self::Hover(HoverEventFilter::MiddleMouseDown),
2408 RightMouseDown => Self::Hover(HoverEventFilter::RightMouseDown),
2409 MouseUp => Self::Hover(HoverEventFilter::MouseUp),
2410 LeftMouseUp => Self::Hover(HoverEventFilter::LeftMouseUp),
2411 MiddleMouseUp => Self::Hover(HoverEventFilter::MiddleMouseUp),
2412 RightMouseUp => Self::Hover(HoverEventFilter::RightMouseUp),
2413
2414 MouseEnter => Self::Hover(HoverEventFilter::MouseEnter),
2415 MouseLeave => Self::Hover(HoverEventFilter::MouseLeave),
2416 Scroll => Self::Hover(HoverEventFilter::Scroll),
2417 TextInput => Self::Focus(FocusEventFilter::TextInput), On::DocumentEdit => Self::Focus(FocusEventFilter::DocumentEdit), VirtualKeyDown => Self::Window(WindowEventFilter::VirtualKeyDown), VirtualKeyUp => Self::Window(WindowEventFilter::VirtualKeyUp), HoveredFile => Self::Hover(HoverEventFilter::HoveredFile),
2422 DroppedFile => Self::Hover(HoverEventFilter::DroppedFile),
2423 HoveredFileCancelled => Self::Hover(HoverEventFilter::HoveredFileCancelled),
2424 FocusReceived => Self::Focus(FocusEventFilter::FocusReceived), FocusLost => Self::Focus(FocusEventFilter::FocusLost), Default => Self::Hover(HoverEventFilter::MouseUp), Collapse => Self::Hover(HoverEventFilter::MouseUp), Expand => Self::Hover(HoverEventFilter::MouseUp), Increment => Self::Hover(HoverEventFilter::MouseUp), Decrement => Self::Hover(HoverEventFilter::MouseUp), }
2434 }
2435}
2436
2437pub trait EventProvider {
2448 fn get_pending_events(&self, timestamp: Instant) -> Vec<SyntheticEvent>;
2460}
2461
2462#[must_use] pub fn deduplicate_synthetic_events(mut events: Vec<SyntheticEvent>) -> Vec<SyntheticEvent> {
2466 if events.len() <= 1 {
2467 return events;
2468 }
2469
2470 events.sort_by_key(|e| (e.target.dom, e.target.node, e.event_type));
2471
2472 let mut result = Vec::with_capacity(events.len());
2474 let mut iter = events.into_iter();
2475
2476 if let Some(mut prev) = iter.next() {
2477 for curr in iter {
2478 if prev.target == curr.target && prev.event_type == curr.event_type {
2479 prev = if curr.timestamp > prev.timestamp {
2481 curr
2482 } else {
2483 prev
2484 };
2485 } else {
2486 result.push(prev);
2487 prev = curr;
2488 }
2489 }
2490 result.push(prev);
2491 }
2492
2493 result
2494}
2495
2496
2497
2498#[allow(clippy::match_same_arms)]
2505#[must_use] pub fn event_type_to_filters(event_type: EventType, event_data: &EventData) -> Vec<EventFilter> {
2506 use EventFilter as EF;
2507 use EventType as E;
2508 use FocusEventFilter as F;
2509 use HoverEventFilter as H;
2510 use WindowEventFilter as W;
2511
2512 let button_specific_down = || -> Option<EventFilter> {
2514 match event_data {
2515 EventData::Mouse(m) => match m.button {
2516 MouseButton::Left => Some(EF::Hover(H::LeftMouseDown)),
2517 MouseButton::Right => Some(EF::Hover(H::RightMouseDown)),
2518 MouseButton::Middle => Some(EF::Hover(H::MiddleMouseDown)),
2519 MouseButton::Other(_) => None, },
2521 _ => Some(EF::Hover(H::LeftMouseDown)), }
2523 };
2524
2525 let button_specific_up = || -> Option<EventFilter> {
2526 match event_data {
2527 EventData::Mouse(m) => match m.button {
2528 MouseButton::Left => Some(EF::Hover(H::LeftMouseUp)),
2529 MouseButton::Right => Some(EF::Hover(H::RightMouseUp)),
2530 MouseButton::Middle => Some(EF::Hover(H::MiddleMouseUp)),
2531 MouseButton::Other(_) => None, },
2533 _ => Some(EF::Hover(H::LeftMouseUp)), }
2535 };
2536
2537 match event_type {
2538 E::MouseDown => {
2540 let mut v = vec![EF::Hover(H::MouseDown)];
2541 if let Some(f) = button_specific_down() { v.push(f); }
2542 v
2543 }
2544 E::MouseUp => {
2545 let mut v = vec![EF::Hover(H::MouseUp)];
2546 if let Some(f) = button_specific_up() { v.push(f); }
2547 v
2548 }
2549
2550 E::Click => vec![EF::Hover(H::LeftMouseUp)],
2555
2556 E::MouseOver => vec![EF::Hover(H::MouseOver)],
2558 E::MouseEnter => vec![EF::Hover(H::MouseEnter)],
2559 E::MouseLeave => vec![EF::Hover(H::MouseLeave)],
2560 E::MouseOut => vec![EF::Hover(H::MouseOut)],
2561
2562 E::DoubleClick => vec![EF::Hover(H::DoubleClick), EF::Window(W::DoubleClick)],
2563 E::ContextMenu => vec![EF::Hover(H::RightMouseDown)],
2564
2565 E::KeyDown => vec![EF::Focus(F::VirtualKeyDown)],
2567 E::KeyUp => vec![EF::Focus(F::VirtualKeyUp)],
2568 E::KeyPress => vec![EF::Focus(F::TextInput)],
2569
2570 E::CompositionStart => vec![EF::Hover(H::CompositionStart), EF::Focus(F::CompositionStart)],
2572 E::CompositionUpdate => vec![EF::Hover(H::CompositionUpdate), EF::Focus(F::CompositionUpdate)],
2573 E::CompositionEnd => vec![EF::Hover(H::CompositionEnd), EF::Focus(F::CompositionEnd)],
2574
2575 E::Focus => vec![EF::Focus(F::FocusReceived)],
2577 E::Blur => vec![EF::Focus(F::FocusLost)],
2578 E::FocusIn => vec![EF::Hover(H::FocusIn), EF::Focus(F::FocusIn)],
2579 E::FocusOut => vec![EF::Hover(H::FocusOut), EF::Focus(F::FocusOut)],
2580
2581 E::Input | E::Change => vec![EF::Focus(F::TextInput)],
2583
2584 E::Scroll | E::ScrollStart | E::ScrollEnd => vec![EF::Hover(H::Scroll)],
2586
2587 E::DragStart => vec![EF::Hover(H::DragStart), EF::Window(W::DragStart)],
2589 E::Drag => vec![EF::Hover(H::Drag), EF::Window(W::Drag)],
2590 E::DragEnd => vec![EF::Hover(H::DragEnd), EF::Window(W::DragEnd)],
2591 E::DragEnter => vec![EF::Hover(H::DragEnter), EF::Window(W::DragEnter)],
2592 E::DragOver => vec![EF::Hover(H::DragOver), EF::Window(W::DragOver)],
2593 E::DragLeave => vec![EF::Hover(H::DragLeave), EF::Window(W::DragLeave)],
2594 E::Drop => vec![EF::Hover(H::Drop), EF::Window(W::Drop)],
2595
2596 E::TouchStart => vec![EF::Hover(H::TouchStart)],
2598 E::TouchMove => vec![EF::Hover(H::TouchMove)],
2599 E::TouchEnd => vec![EF::Hover(H::TouchEnd)],
2600 E::TouchCancel => vec![EF::Hover(H::TouchCancel)],
2601
2602 E::WindowResize => vec![EF::Window(W::Resized)],
2604 E::WindowMove => vec![EF::Window(W::Moved)],
2605 E::WindowClose => vec![EF::Window(W::CloseRequested)],
2606 E::WindowFocusIn => vec![EF::Window(W::WindowFocusReceived)],
2607 E::WindowFocusOut => vec![EF::Window(W::WindowFocusLost)],
2608 E::ThemeChange => vec![EF::Window(W::ThemeChanged)],
2609 E::WindowDpiChanged => vec![EF::Window(W::DpiChanged)],
2610 E::WindowMonitorChanged => vec![EF::Window(W::MonitorChanged)],
2611
2612 E::MonitorConnected => vec![EF::Application(ApplicationEventFilter::MonitorConnected)],
2614 E::MonitorDisconnected => vec![EF::Application(ApplicationEventFilter::MonitorDisconnected)],
2615
2616 E::FileHover => vec![EF::Hover(H::HoveredFile), EF::Window(W::HoveredFile)],
2622 E::FileDrop => vec![EF::Hover(H::DroppedFile), EF::Window(W::DroppedFile)],
2623 E::FileHoverCancel => vec![
2624 EF::Hover(H::HoveredFileCancelled),
2625 EF::Window(W::HoveredFileCancelled),
2626 ],
2627
2628 E::Mount => vec![EF::Component(ComponentEventFilter::AfterMount)],
2633 E::Unmount => vec![EF::Component(ComponentEventFilter::BeforeUnmount)],
2634 E::Update => vec![EF::Component(ComponentEventFilter::Updated)],
2635 E::Resize => vec![EF::Component(ComponentEventFilter::NodeResized)],
2636
2637 E::SensorChanged => vec![EF::Hover(H::SensorChanged), EF::Window(W::SensorChanged)],
2640 E::GamepadInput => vec![EF::Hover(H::GamepadInput), EF::Window(W::GamepadInput)],
2641
2642 E::GeolocationFix => vec![EF::Hover(H::GeolocationFix), EF::Window(W::GeolocationFix)],
2646 E::GeolocationError => vec![EF::Hover(H::GeolocationError), EF::Window(W::GeolocationError)],
2647
2648 E::PermissionChanged => vec![EF::Hover(H::PermissionChanged), EF::Window(W::PermissionChanged)],
2652 E::BiometricResult => vec![EF::Hover(H::BiometricResult), EF::Window(W::BiometricResult)],
2653 E::KeyringResult => vec![EF::Hover(H::KeyringResult), EF::Window(W::KeyringResult)],
2654
2655 E::Copy => vec![EF::Focus(F::Copy)],
2659 E::Cut => vec![EF::Focus(F::Cut)],
2660 E::Paste => vec![EF::Focus(F::Paste)],
2661
2662 _ => vec![],
2664 }
2665}
2666
2667
2668
2669#[derive(Debug, Clone, PartialEq, Eq)]
2681#[must_use = "SystemChange must be processed through apply_system_change()"]
2682pub enum SystemChange {
2683 TextSelectionClick {
2687 position: LogicalPosition,
2688 timestamp: Instant,
2689 },
2690 TextSelectionDrag {
2692 start_position: LogicalPosition,
2693 current_position: LogicalPosition,
2694 },
2695 ApplySelectionOp {
2700 target: DomNodeId,
2701 op: SelectionOp,
2702 },
2703
2704 CopyToClipboard,
2708 CutToClipboard { target: DomNodeId },
2710 PasteFromClipboard,
2712 SelectAllText,
2714 UndoTextEdit { target: DomNodeId },
2716 RedoTextEdit { target: DomNodeId },
2718
2719 AddCursorAtClick {
2724 position: LogicalPosition,
2725 },
2726 SelectNextOccurrence {
2729 target: DomNodeId,
2730 },
2731
2732 ApplyPendingTextInput,
2736 ApplyTextChangeset,
2738
2739 ActivateNodeDrag {
2743 dom_id: DomId,
2744 node_id: NodeId,
2745 },
2746 ActivateWindowDrag,
2748 InitDragVisualState,
2750 SetDragOverState { target: DomNodeId, active: bool },
2752 UpdateDropTarget { target: DomNodeId },
2754 UpdateDragGpuTransform,
2756 DeactivateDrag,
2758
2759 SetFocus {
2765 new_focus: Option<DomNodeId>,
2766 old_focus: Option<DomNodeId>,
2767 },
2768 ClearAllSelections,
2770 FinalizePendingFocusChanges,
2772
2773 ScrollSelectionIntoView,
2777 ScrollNodeIntoView { target: DomNodeId },
2779 ScrollCursorIntoViewAfterTextInput,
2781
2782 StartAutoScrollTimer,
2786 StopAutoScrollTimer,
2788}
2789
2790impl_option!(
2791 SystemChange,
2792 OptionSystemChange,
2793 copy = false,
2794 clone = false,
2795 [Debug, Clone, PartialEq, Eq]
2796);
2797
2798impl_vec!(SystemChange, SystemChangeVec, SystemChangeVecDestructor, SystemChangeVecDestructorType, SystemChangeVecSlice, OptionSystemChange);
2799impl_vec_debug!(SystemChange, SystemChangeVec);
2800impl_vec_clone!(SystemChange, SystemChangeVec, SystemChangeVecDestructor);
2801impl_vec_partialeq!(SystemChange, SystemChangeVec);
2802
2803#[derive(Debug, Clone, PartialEq)]
2805pub struct PreCallbackFilterResult {
2806 pub system_changes: Vec<SystemChange>,
2808 pub user_events: Vec<SyntheticEvent>,
2810}
2811
2812#[derive(Debug, Clone, Copy)]
2814pub struct InputInterpreterState {
2815 pub focused_node: Option<DomNodeId>,
2816 pub click_count: u8,
2817 pub drag_start_position: Option<LogicalPosition>,
2818 pub has_selection: bool,
2819}
2820
2821#[derive(Debug)]
2826pub struct InputInterpreterInfo<'a> {
2827 pub events: &'a [SyntheticEvent],
2828 pub hit_test: Option<&'a FullHitTest>,
2829 pub keyboard_state: &'a crate::window::KeyboardState,
2830 pub mouse_state: &'a crate::window::MouseState,
2831 pub state: InputInterpreterState,
2832}
2833
2834pub type InputInterpreterCallbackType = extern "C" fn(
2844 crate::refany::RefAny,
2845 *const InputInterpreterInfo<'static>, ) -> PreCallbackFilterResult;
2847
2848#[repr(C)]
2858pub struct InputInterpreterCallback {
2859 pub cb: InputInterpreterCallbackType,
2860 pub ctx: crate::refany::OptionRefAny,
2861}
2862
2863impl_callback!(InputInterpreterCallback, InputInterpreterCallbackType);
2864
2865impl Default for InputInterpreterCallback {
2866 fn default() -> Self {
2867 Self {
2868 cb: default_input_interpreter_extern,
2869 ctx: crate::refany::OptionRefAny::None,
2870 }
2871 }
2872}
2873
2874pub type PostFilterCallbackType = extern "C" fn(
2876 crate::refany::RefAny,
2877 bool, SystemChangeVecSlice, DomNodeId, DomNodeId, ) -> SystemChangeVec;
2882
2883#[repr(C)]
2885pub struct PostFilterCallback {
2886 pub cb: PostFilterCallbackType,
2887 pub ctx: crate::refany::OptionRefAny,
2888}
2889
2890impl_callback!(PostFilterCallback, PostFilterCallbackType);
2891
2892impl Default for PostFilterCallback {
2893 fn default() -> Self {
2894 Self {
2895 cb: default_post_filter_extern,
2896 ctx: crate::refany::OptionRefAny::None,
2897 }
2898 }
2899}
2900
2901#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2907#[cfg_attr(feature = "serde-json", derive(serde::Serialize, serde::Deserialize))]
2908#[cfg_attr(feature = "serde-json", serde(rename_all = "lowercase"))]
2909pub enum E2eOpArgType {
2910 String,
2911 Number,
2912 Bool,
2913 Object,
2914 Array,
2915 Any,
2917}
2918
2919#[derive(Debug, Clone, PartialEq, Eq, Default)]
2921#[cfg_attr(feature = "serde-json", derive(serde::Serialize, serde::Deserialize))]
2922pub struct E2eOpArg {
2923 pub name: String,
2924 #[cfg_attr(feature = "serde-json", serde(rename = "type"))]
2925 pub arg_type: E2eOpArgType,
2926 pub required: bool,
2927 pub description: String,
2928}
2929
2930impl Default for E2eOpArgType {
2931 fn default() -> Self {
2932 Self::Any
2933 }
2934}
2935
2936#[derive(Debug, Clone, PartialEq, Default)]
2942#[cfg_attr(feature = "serde-json", derive(serde::Serialize, serde::Deserialize))]
2943pub struct E2eOpExample {
2944 pub description: String,
2945 pub args: crate::json::Json,
2952 pub returns: crate::json::Json,
2957}
2958
2959#[derive(Debug, Clone, PartialEq, Default)]
2961#[cfg_attr(feature = "serde-json", derive(serde::Serialize, serde::Deserialize))]
2962pub struct E2eOpDef {
2963 pub name: String,
2964 pub summary: String,
2966 pub description: String,
2968 pub args: Vec<E2eOpArg>,
2969 pub examples: Vec<E2eOpExample>,
2970}
2971
2972#[derive(Debug, Clone, PartialEq, Default)]
2980#[cfg_attr(feature = "serde-json", derive(serde::Serialize, serde::Deserialize))]
2981pub struct E2eOpSchema {
2982 pub ops: Vec<E2eOpDef>,
2983}
2984
2985#[allow(clippy::missing_const_for_fn)]
2994fn json_has_success_bool(v: &crate::json::Json) -> bool {
2995 #[cfg(feature = "serde-json")]
2996 {
2997 v
2998 .to_serde_value()
2999 .get("success")
3000 .is_some_and(serde_json::Value::is_boolean)
3001 }
3002 #[cfg(not(feature = "serde-json"))]
3003 {
3004 let _ = v;
3008 true
3009 }
3010}
3011
3012#[derive(Debug, Clone, PartialEq, Eq)]
3014pub enum E2eSchemaError {
3015 UnnamedOp { index: usize },
3017 DuplicateOpName { name: String },
3019 UnnamedArg { op: String, index: usize },
3021 ExampleMissingSuccess { op: String, index: usize },
3027}
3028
3029impl core::fmt::Display for E2eSchemaError {
3030 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
3031 match self {
3032 Self::UnnamedOp { index } => write!(f, "op #{index} has an empty name"),
3033 Self::DuplicateOpName { name } => {
3034 write!(f, "two ops are both named '{name}'; dispatch would be ambiguous")
3035 }
3036 Self::UnnamedArg { op, index } => {
3037 write!(f, "op '{op}' argument #{index} has an empty name")
3038 }
3039 Self::ExampleMissingSuccess { op, index } => write!(
3040 f,
3041 "op '{op}' example #{index}: `returns` has no `success` boolean. Every op \
3042 result must say whether it worked, or a failure is indistinguishable from a \
3043 success"
3044 ),
3045 }
3046 }
3047}
3048
3049impl E2eOpSchema {
3050 pub fn validate(&self) -> Result<(), E2eSchemaError> {
3065 let mut seen: Vec<&str> = Vec::new();
3066 for (i, op) in self.ops.iter().enumerate() {
3067 if op.name.trim().is_empty() {
3068 return Err(E2eSchemaError::UnnamedOp { index: i });
3069 }
3070 if seen.contains(&op.name.as_str()) {
3071 return Err(E2eSchemaError::DuplicateOpName { name: op.name.clone() });
3072 }
3073 seen.push(op.name.as_str());
3074 for (a, arg) in op.args.iter().enumerate() {
3075 if arg.name.trim().is_empty() {
3076 return Err(E2eSchemaError::UnnamedArg { op: op.name.clone(), index: a });
3077 }
3078 }
3079 for (e, ex) in op.examples.iter().enumerate() {
3080 if !json_has_success_bool(&ex.returns) {
3081 return Err(E2eSchemaError::ExampleMissingSuccess {
3082 op: op.name.clone(),
3083 index: e,
3084 });
3085 }
3086 }
3087 }
3088 Ok(())
3089 }
3090
3091 #[must_use]
3093 pub fn to_json(&self) -> crate::json::Json {
3094 #[cfg(feature = "serde-json")]
3095 {
3096 serde_json::to_string(self).ok().map_or_else(
3104 || crate::json::Json {
3105 value_type: crate::json::JsonType::Object,
3106 internal: crate::json::JsonInternal {
3107 string_value: AzString::from_const_str(r#"{"ops":[]}"#),
3108 ..Default::default()
3109 },
3110 },
3111 |text| crate::json::Json {
3112 value_type: crate::json::JsonType::Object,
3113 internal: crate::json::JsonInternal {
3114 string_value: AzString::from(text),
3115 ..Default::default()
3116 },
3117 },
3118 )
3119 }
3120 #[cfg(not(feature = "serde-json"))]
3121 crate::json::Json {
3122 value_type: crate::json::JsonType::Object,
3123 internal: crate::json::JsonInternal {
3124 string_value: AzString::from_const_str(r#"{"ops":[]}"#),
3125 ..Default::default()
3126 },
3127 }
3128 }
3129}
3130
3131#[repr(C)]
3133#[derive(Debug, Clone, PartialEq, Eq)]
3134pub struct CustomE2eOpResult {
3135 pub handled: bool,
3144 pub json: AzString,
3147}
3148
3149impl Default for CustomE2eOpResult {
3150 fn default() -> Self {
3154 Self { handled: false, json: AzString::from_const_str("") }
3155 }
3156}
3157
3158pub type CustomE2eOpCallbackType = extern "C" fn(
3165 crate::refany::RefAny, AzString, AzString, ) -> CustomE2eOpResult;
3169
3170#[repr(C)]
3172pub struct CustomE2eOpCallback {
3173 pub cb: CustomE2eOpCallbackType,
3174 pub ctx: crate::refany::OptionRefAny,
3175 pub op_schema: crate::json::Json,
3191}
3192
3193impl_callback_traits!(CustomE2eOpCallback);
3197
3198impl Clone for CustomE2eOpCallback {
3199 fn clone(&self) -> Self {
3200 Self {
3201 cb: self.cb,
3202 ctx: self.ctx.clone(),
3203 op_schema: self.op_schema.clone(),
3204 }
3205 }
3206}
3207
3208impl From<CustomE2eOpCallbackType> for CustomE2eOpCallback {
3209 fn from(cb: CustomE2eOpCallbackType) -> Self {
3216 Self {
3217 cb,
3218 ..Self::default()
3219 }
3220 }
3221}
3222
3223impl Default for CustomE2eOpCallback {
3224 fn default() -> Self {
3225 Self {
3226 cb: default_custom_e2e_op_extern,
3227 ctx: crate::refany::OptionRefAny::None,
3228 op_schema: E2eOpSchema::default().to_json(),
3237 }
3238 }
3239}
3240
3241#[must_use]
3248pub extern "C" fn default_custom_e2e_op_extern(
3249 _ctx: crate::refany::RefAny,
3250 _op: AzString,
3251 _args: AzString,
3252) -> CustomE2eOpResult {
3253 CustomE2eOpResult {
3254 handled: false,
3255 json: AzString::from_const_str(""),
3256 }
3257}
3258
3259pub type InputInterpreterFn = fn(
3261 info: &InputInterpreterInfo<'_>,
3262) -> PreCallbackFilterResult;
3263
3264pub type PostFilterFn = fn(
3265 prevent_default: bool,
3266 pre_changes: &[SystemChange],
3267 old_focus: Option<DomNodeId>,
3268 new_focus: Option<DomNodeId>,
3269) -> Vec<SystemChange>;
3270
3271#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3273pub struct MouseButtonState {
3274 pub left_down: bool,
3275 pub right_down: bool,
3276 pub middle_down: bool,
3277}
3278
3279#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
3281pub enum ArrowDirection {
3282 Left,
3283 Right,
3284 Up,
3285 Down,
3286 LineStart,
3288 LineEnd,
3290 DocumentStart,
3292 DocumentEnd,
3294}
3295
3296impl ArrowDirection {
3297 #[must_use] pub const fn from_key(vk: crate::window::VirtualKeyCode, ctrl: bool) -> Option<Self> {
3300 use crate::window::VirtualKeyCode::{Left, Right, Up, Down, Home, End};
3301 Some(match vk {
3302 Left => Self::Left,
3303 Right => Self::Right,
3304 Up => Self::Up,
3305 Down => Self::Down,
3306 Home if ctrl => Self::DocumentStart,
3307 Home => Self::LineStart,
3308 End if ctrl => Self::DocumentEnd,
3309 End => Self::LineEnd,
3310 _ => return None,
3311 })
3312 }
3313
3314 #[must_use] pub const fn to_selection(self, ctrl: bool) -> (SelectionDirection, SelectionStep) {
3317 match self {
3318 Self::Left if ctrl => (SelectionDirection::Backward, SelectionStep::Word),
3319 Self::Right if ctrl => (SelectionDirection::Forward, SelectionStep::Word),
3320 Self::Left => (SelectionDirection::Backward, SelectionStep::Character),
3321 Self::Right => (SelectionDirection::Forward, SelectionStep::Character),
3322 Self::Up => (SelectionDirection::Backward, SelectionStep::VisualLine),
3323 Self::Down => (SelectionDirection::Forward, SelectionStep::VisualLine),
3324 Self::LineStart => (SelectionDirection::Backward, SelectionStep::Line),
3325 Self::LineEnd => (SelectionDirection::Forward, SelectionStep::Line),
3326 Self::DocumentStart => (SelectionDirection::Backward, SelectionStep::Document),
3327 Self::DocumentEnd => (SelectionDirection::Forward, SelectionStep::Document),
3328 }
3329 }
3330}
3331
3332#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
3334#[repr(C)]
3335pub enum SelectionDirection {
3336 Forward,
3337 Backward,
3338}
3339
3340#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
3346#[repr(C)]
3347pub enum SelectionStep {
3348 Character,
3350 Word,
3352 Line,
3354 VisualLine,
3356 Document,
3358}
3359
3360#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
3362#[repr(C)]
3363pub enum SelectionMode {
3364 Move,
3366 Extend,
3368 Delete,
3371}
3372
3373#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
3385#[repr(C)]
3386pub struct SelectionOp {
3387 pub direction: SelectionDirection,
3388 pub step: SelectionStep,
3389 pub mode: SelectionMode,
3390 pub repeat: usize,
3391}
3392
3393impl SelectionOp {
3394 #[must_use] pub const fn new(direction: SelectionDirection, step: SelectionStep, mode: SelectionMode) -> Self {
3395 Self { direction, step, mode, repeat: 1 }
3396 }
3397}
3398
3399#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
3401pub enum KeyboardShortcut {
3402 Copy, Cut, Paste, SelectAll, Undo, Redo, }
3409
3410impl KeyboardShortcut {
3411 #[must_use] pub const fn from_key(vk: crate::window::VirtualKeyCode, primary: bool, shift: bool) -> Option<Self> {
3418 use crate::window::VirtualKeyCode::{C, X, V, A, Z, Y};
3419 if !primary {
3420 return None;
3421 }
3422 Some(match vk {
3423 C => Self::Copy,
3424 X => Self::Cut,
3425 V => Self::Paste,
3426 A => Self::SelectAll,
3427 Z if shift => Self::Redo,
3428 Z => Self::Undo,
3429 Y => Self::Redo,
3430 _ => return None,
3431 })
3432 }
3433}
3434
3435#[allow(clippy::not_unsafe_ptr_arg_deref)] #[must_use] pub extern "C" fn default_input_interpreter_extern(
3443 _user_data: crate::refany::RefAny,
3444 info_ptr: *const InputInterpreterInfo<'static>,
3445) -> PreCallbackFilterResult {
3446 if info_ptr.is_null() {
3447 return PreCallbackFilterResult {
3448 system_changes: Vec::new(),
3449 user_events: Vec::new(),
3450 };
3451 }
3452 let info = unsafe { &*info_ptr };
3453 default_input_interpreter(info)
3454}
3455
3456#[must_use] pub extern "C" fn default_post_filter_extern(
3458 _user_data: crate::refany::RefAny,
3459 prevent_default: bool,
3460 pre_changes: SystemChangeVecSlice,
3461 old_focus: DomNodeId,
3462 new_focus: DomNodeId,
3463) -> SystemChangeVec {
3464 let pre_changes_slice = pre_changes.as_slice();
3465 let old = old_focus.node.into_crate_internal().map(|_| old_focus);
3466 let new = new_focus.node.into_crate_internal().map(|_| new_focus);
3467 default_post_filter(prevent_default, pre_changes_slice, old, new).into()
3468}
3469
3470#[must_use] pub fn default_input_interpreter(
3471 info: &InputInterpreterInfo<'_>,
3472) -> PreCallbackFilterResult {
3473 let ctx = FilterContext {
3474 hit_test: info.hit_test,
3475 keyboard_state: info.keyboard_state,
3476 mouse_state: info.mouse_state,
3477 click_count: info.state.click_count,
3478 focused_node: info.state.focused_node,
3479 drag_start_position: info.state.drag_start_position,
3480 };
3481
3482 let (system_changes, user_events) = info.events.iter().fold(
3483 (Vec::new(), Vec::new()),
3484 |(mut internal, mut user), event| {
3485 match process_event_for_internal(&ctx, event) {
3486 Some(InternalEventAction::AddAndSkip(evt)) => {
3487 internal.push(evt);
3488 }
3489 Some(InternalEventAction::AddAndPass(evt)) => {
3490 internal.push(evt);
3491 user.push(event.clone());
3492 }
3493 None => {
3494 user.push(event.clone());
3495 }
3496 }
3497 (internal, user)
3498 },
3499 );
3500
3501 PreCallbackFilterResult {
3502 system_changes,
3503 user_events,
3504 }
3505}
3506
3507pub fn pre_callback_filter_internal_events<SM, FM>(
3509 events: &[SyntheticEvent],
3510 hit_test: Option<&FullHitTest>,
3511 keyboard_state: &crate::window::KeyboardState,
3512 mouse_state: &crate::window::MouseState,
3513 selection_manager: &SM,
3514 focus_manager: &FM,
3515) -> PreCallbackFilterResult
3516where
3517 SM: SelectionManagerQuery,
3518 FM: FocusManagerQuery,
3519{
3520 let info = InputInterpreterInfo {
3521 events,
3522 hit_test,
3523 keyboard_state,
3524 mouse_state,
3525 state: InputInterpreterState {
3526 focused_node: focus_manager.get_focused_node_id(),
3527 click_count: selection_manager.get_click_count(),
3528 drag_start_position: selection_manager.get_drag_start_position(),
3529 has_selection: selection_manager.has_selection(),
3530 },
3531 };
3532 default_input_interpreter(&info)
3533}
3534
3535struct FilterContext<'a> {
3537 hit_test: Option<&'a FullHitTest>,
3538 keyboard_state: &'a crate::window::KeyboardState,
3539 mouse_state: &'a crate::window::MouseState,
3540 click_count: u8,
3541 focused_node: Option<DomNodeId>,
3542 drag_start_position: Option<LogicalPosition>,
3543}
3544
3545fn process_event_for_internal(
3547 ctx: &FilterContext<'_>,
3548 event: &SyntheticEvent,
3549) -> Option<InternalEventAction> {
3550 match event.event_type {
3551 EventType::MouseDown => handle_mouse_down(event, ctx.hit_test, ctx.click_count, ctx.mouse_state, ctx.keyboard_state),
3552 EventType::MouseOver => handle_mouse_over(
3553 event,
3554 ctx.hit_test,
3555 ctx.mouse_state,
3556 ctx.drag_start_position,
3557 ),
3558 EventType::KeyDown => handle_key_down(
3559 event,
3560 ctx.keyboard_state,
3561 ctx.focused_node,
3562 ),
3563 _ => None,
3564 }
3565}
3566
3567enum InternalEventAction {
3569 AddAndSkip(SystemChange),
3571 AddAndPass(SystemChange),
3573}
3574
3575fn get_first_hovered_node(hit_test: Option<&FullHitTest>) -> Option<DomNodeId> {
3583 let ht = hit_test?;
3584 let mut best: Option<(DomId, NodeId, u32)> = None;
3585 for (dom_id, hit_data) in &ht.hovered_nodes {
3586 for (node_id, item) in &hit_data.regular_hit_test_nodes {
3587 let is_better = match best {
3588 None => true,
3589 Some((_, _, best_depth)) => item.hit_depth < best_depth,
3590 };
3591 if is_better {
3592 best = Some((*dom_id, *node_id, item.hit_depth));
3593 }
3594 }
3595 }
3596 let (dom_id, node_id, _) = best?;
3597 Some(DomNodeId {
3598 dom: dom_id,
3599 node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
3600 })
3601}
3602
3603fn get_mouse_position_with_fallback(
3605 event: &SyntheticEvent,
3606 mouse_state: &crate::window::MouseState,
3607) -> LogicalPosition {
3608 match &event.data {
3609 EventData::Mouse(mouse_data) => mouse_data.position,
3610 _ => {
3611 mouse_state.cursor_position.get_position().unwrap_or(LogicalPosition::zero())
3615 }
3616 }
3617}
3618
3619fn handle_mouse_down(
3621 event: &SyntheticEvent,
3622 hit_test: Option<&FullHitTest>,
3623 click_count: u8,
3624 mouse_state: &crate::window::MouseState,
3625 keyboard_state: &crate::window::KeyboardState,
3626) -> Option<InternalEventAction> {
3627 let effective_click_count = if click_count == 0 { 1 } else { click_count };
3628
3629 if effective_click_count > 3 {
3630 return None;
3631 }
3632
3633 let _target = get_first_hovered_node(hit_test)?;
3634 let position = get_mouse_position_with_fallback(event, mouse_state);
3635
3636 if keyboard_state.primary_down() && effective_click_count == 1 {
3641 return Some(InternalEventAction::AddAndPass(
3642 SystemChange::AddCursorAtClick { position },
3643 ));
3644 }
3645
3646 Some(InternalEventAction::AddAndPass(
3647 SystemChange::TextSelectionClick {
3648 position,
3649 timestamp: event.timestamp.clone(),
3650 },
3651 ))
3652}
3653
3654fn handle_mouse_over(
3656 event: &SyntheticEvent,
3657 hit_test: Option<&FullHitTest>,
3658 mouse_state: &crate::window::MouseState,
3659 drag_start_position: Option<LogicalPosition>,
3660) -> Option<InternalEventAction> {
3661 if !mouse_state.left_down {
3662 return None;
3663 }
3664
3665 let start_position = drag_start_position?;
3666
3667 let _target = get_first_hovered_node(hit_test)?;
3668 let current_position = get_mouse_position_with_fallback(event, mouse_state);
3669
3670 Some(InternalEventAction::AddAndPass(
3671 SystemChange::TextSelectionDrag {
3672 start_position,
3673 current_position,
3674 },
3675 ))
3676}
3677
3678fn handle_key_down(
3680 event: &SyntheticEvent,
3681 keyboard_state: &crate::window::KeyboardState,
3682 focused_node: Option<DomNodeId>,
3683) -> Option<InternalEventAction> {
3684 use crate::window::VirtualKeyCode;
3685
3686 let target = focused_node?;
3687 let EventData::Keyboard(kbd) = &event.data else {
3688 return None;
3689 };
3690
3691 let _ = keyboard_state;
3697
3698 let primary = if cfg!(target_os = "macos") {
3702 kbd.modifiers.meta
3703 } else {
3704 kbd.modifiers.ctrl
3705 };
3706 let word_mod = if cfg!(target_os = "macos") {
3707 kbd.modifiers.alt
3708 } else {
3709 kbd.modifiers.ctrl
3710 };
3711 let shift = kbd.modifiers.shift;
3712 let vk_owned = VirtualKeyCode::from_u32(kbd.key_code)?;
3713 let vk = &vk_owned;
3714
3715 if primary {
3720 if let Some(shortcut) = KeyboardShortcut::from_key(*vk, primary, shift) {
3721 let change = match shortcut {
3722 KeyboardShortcut::Copy => SystemChange::CopyToClipboard,
3723 KeyboardShortcut::Cut => SystemChange::CutToClipboard { target },
3724 KeyboardShortcut::Paste => SystemChange::PasteFromClipboard,
3725 KeyboardShortcut::SelectAll => SystemChange::SelectAllText,
3726 KeyboardShortcut::Undo => SystemChange::UndoTextEdit { target },
3727 KeyboardShortcut::Redo => SystemChange::RedoTextEdit { target },
3728 };
3729 return Some(InternalEventAction::AddAndSkip(change));
3730 }
3731 if matches!(vk, VirtualKeyCode::D) {
3732 return Some(InternalEventAction::AddAndSkip(
3733 SystemChange::SelectNextOccurrence { target },
3734 ));
3735 }
3736 }
3737
3738 let mode_for_shift = if shift { SelectionMode::Extend } else { SelectionMode::Move };
3740 let selection_op = if let Some(arrow) = ArrowDirection::from_key(*vk, word_mod) {
3741 let (direction, step) = arrow.to_selection(word_mod);
3742 SelectionOp::new(direction, step, mode_for_shift)
3743 } else {
3744 match vk {
3745 VirtualKeyCode::Back => SelectionOp::new(
3748 SelectionDirection::Backward,
3749 if word_mod { SelectionStep::Word } else { SelectionStep::Character },
3750 SelectionMode::Delete,
3751 ),
3752 VirtualKeyCode::Delete => SelectionOp::new(
3753 SelectionDirection::Forward,
3754 if word_mod { SelectionStep::Word } else { SelectionStep::Character },
3755 SelectionMode::Delete,
3756 ),
3757 _ => return None,
3758 }
3759 };
3760
3761 Some(InternalEventAction::AddAndSkip(
3762 SystemChange::ApplySelectionOp { target, op: selection_op },
3763 ))
3764}
3765
3766pub trait SelectionManagerQuery {
3771 fn get_click_count(&self) -> u8;
3773
3774 fn get_drag_start_position(&self) -> Option<LogicalPosition>;
3776
3777 fn has_selection(&self) -> bool;
3779}
3780
3781pub trait FocusManagerQuery {
3786 fn get_focused_node_id(&self) -> Option<DomNodeId>;
3788}
3789
3790#[must_use] pub fn default_post_filter(
3796 prevent_default: bool,
3797 pre_changes: &[SystemChange],
3798 old_focus: Option<DomNodeId>,
3799 new_focus: Option<DomNodeId>,
3800) -> Vec<SystemChange> {
3801 post_callback_filter_system_changes(prevent_default, pre_changes, old_focus, new_focus)
3802}
3803
3804#[allow(clippy::match_same_arms)]
3807#[must_use] pub fn post_callback_filter_system_changes(
3808 prevent_default: bool,
3809 pre_changes: &[SystemChange],
3810 old_focus: Option<DomNodeId>,
3811 new_focus: Option<DomNodeId>,
3812) -> Vec<SystemChange> {
3813 let mut changes = Vec::new();
3814
3815 if prevent_default {
3816 if old_focus != new_focus {
3818 changes.push(SystemChange::SetFocus { new_focus, old_focus });
3819 }
3820 return changes;
3821 }
3822
3823 changes.push(SystemChange::ApplyPendingTextInput);
3825
3826 for change in pre_changes {
3828 match change {
3829 SystemChange::TextSelectionClick { .. }
3830 | SystemChange::ApplySelectionOp { .. }
3831 | SystemChange::AddCursorAtClick { .. }
3832 | SystemChange::SelectNextOccurrence { .. } => {
3833 changes.push(SystemChange::ScrollSelectionIntoView);
3834 }
3835 SystemChange::TextSelectionDrag { .. } => {
3836 changes.push(SystemChange::StartAutoScrollTimer);
3837 }
3838 SystemChange::CutToClipboard { .. }
3839 | SystemChange::PasteFromClipboard
3840 | SystemChange::UndoTextEdit { .. }
3841 | SystemChange::RedoTextEdit { .. }
3842 | SystemChange::SelectAllText => {
3843 changes.push(SystemChange::ScrollSelectionIntoView);
3844 }
3845 _ => {}
3847 }
3848 }
3849
3850 if old_focus != new_focus {
3852 changes.push(SystemChange::SetFocus { new_focus, old_focus });
3853 }
3854
3855 changes
3856}
3857
3858
3859#[cfg(test)]
3860mod tests {
3861 use super::*;
3862 use azul_css::AzString;
3863 use crate::dom::{DomId, DomNodeId};
3864 use crate::styled_dom::NodeHierarchyItemId;
3865 use crate::id::NodeId;
3866 use crate::window::{KeyboardState, MouseState, VirtualKeyCode, VirtualKeyCodeVec, OptionVirtualKeyCode};
3867 use crate::geom::LogicalPosition;
3868 use crate::task::{Instant, SystemTick};
3869
3870 struct MockSelectionManager {
3871 click_count: u8,
3872 has_sel: bool,
3873 }
3874 impl SelectionManagerQuery for MockSelectionManager {
3875 fn get_click_count(&self) -> u8 { self.click_count }
3876 fn get_drag_start_position(&self) -> Option<LogicalPosition> { None }
3877 fn has_selection(&self) -> bool { self.has_sel }
3878 }
3879
3880 struct MockFocusManager(Option<DomNodeId>);
3881 impl FocusManagerQuery for MockFocusManager {
3882 fn get_focused_node_id(&self) -> Option<DomNodeId> { self.0 }
3883 }
3884
3885 fn focused_node(node_idx: usize) -> DomNodeId {
3886 DomNodeId {
3887 dom: DomId { inner: 0 },
3888 node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(node_idx))),
3889 }
3890 }
3891
3892 fn make_keyboard_state(vk: VirtualKeyCode) -> KeyboardState {
3893 KeyboardState {
3894 current_virtual_keycode: OptionVirtualKeyCode::Some(vk),
3895 pressed_virtual_keycodes: VirtualKeyCodeVec::from_vec(vec![vk]),
3896 ..KeyboardState::default()
3897 }
3898 }
3899
3900 fn make_keydown_event(target: DomNodeId) -> SyntheticEvent {
3901 SyntheticEvent::new(
3902 EventType::KeyDown,
3903 EventSource::User,
3904 target,
3905 Instant::Tick(SystemTick::new(0)),
3906 EventData::Keyboard(KeyboardEventData {
3907 key_code: VirtualKeyCode::Back as u32,
3908 char_code: None,
3909 modifiers: KeyModifiers::default(),
3910 repeat: false,
3911 }),
3912 )
3913 }
3914
3915 #[test]
3916 fn backspace_generates_delete_text_selection() {
3917 let target = focused_node(2);
3918 let events = vec![make_keydown_event(target)];
3919 let kb = make_keyboard_state(VirtualKeyCode::Back);
3920 let mouse = MouseState::default();
3921 let sel = MockSelectionManager { click_count: 0, has_sel: false };
3922 let focus = MockFocusManager(Some(target));
3923
3924 let result = pre_callback_filter_internal_events(
3925 &events, None, &kb, &mouse, &sel, &focus,
3926 );
3927
3928 let ops: Vec<_> = result.system_changes.iter()
3929 .filter(|c| matches!(c, SystemChange::ApplySelectionOp { .. }))
3930 .collect();
3931 assert_eq!(ops.len(), 1, "Backspace should generate ApplySelectionOp");
3932 match &ops[0] {
3933 SystemChange::ApplySelectionOp { op, .. } => {
3934 assert_eq!(op.direction, SelectionDirection::Backward);
3935 assert_eq!(op.step, SelectionStep::Character);
3936 assert_eq!(op.mode, SelectionMode::Delete);
3937 }
3938 _ => unreachable!(),
3939 }
3940 }
3941
3942 #[test]
3943 fn delete_key_generates_forward_deletion() {
3944 let target = focused_node(2);
3945 let event = SyntheticEvent::new(
3946 EventType::KeyDown, EventSource::User, target,
3947 Instant::Tick(SystemTick::new(0)),
3948 EventData::Keyboard(KeyboardEventData {
3949 key_code: VirtualKeyCode::Delete as u32,
3950 char_code: None, modifiers: KeyModifiers::default(), repeat: false,
3951 }),
3952 );
3953 let kb = make_keyboard_state(VirtualKeyCode::Delete);
3954 let mouse = MouseState::default();
3955 let sel = MockSelectionManager { click_count: 0, has_sel: false };
3956 let focus = MockFocusManager(Some(target));
3957 let result = pre_callback_filter_internal_events(&[event], None, &kb, &mouse, &sel, &focus);
3958 let ops: Vec<_> = result.system_changes.iter()
3959 .filter(|c| matches!(c, SystemChange::ApplySelectionOp { .. }))
3960 .collect();
3961 assert_eq!(ops.len(), 1);
3962 match &ops[0] {
3963 SystemChange::ApplySelectionOp { op, .. } => {
3964 assert_eq!(op.direction, SelectionDirection::Forward);
3965 assert_eq!(op.step, SelectionStep::Character);
3966 assert_eq!(op.mode, SelectionMode::Delete);
3967 }
3968 _ => unreachable!(),
3969 }
3970 }
3971
3972 #[test]
3973 fn arrow_left_generates_navigation() {
3974 let target = focused_node(2);
3975 let event = SyntheticEvent::new(
3976 EventType::KeyDown, EventSource::User, target,
3977 Instant::Tick(SystemTick::new(0)),
3978 EventData::Keyboard(KeyboardEventData {
3979 key_code: VirtualKeyCode::Left as u32,
3980 char_code: None, modifiers: KeyModifiers::default(), repeat: false,
3981 }),
3982 );
3983 let kb = make_keyboard_state(VirtualKeyCode::Left);
3984 let mouse = MouseState::default();
3985 let sel = MockSelectionManager { click_count: 0, has_sel: false };
3986 let focus = MockFocusManager(Some(target));
3987 let result = pre_callback_filter_internal_events(&[event], None, &kb, &mouse, &sel, &focus);
3988 let ops: Vec<_> = result.system_changes.iter()
3989 .filter(|c| matches!(c, SystemChange::ApplySelectionOp { .. }))
3990 .collect();
3991 assert_eq!(ops.len(), 1, "Left arrow should generate ApplySelectionOp");
3992 match &ops[0] {
3993 SystemChange::ApplySelectionOp { op, .. } => {
3994 assert_eq!(op.direction, SelectionDirection::Backward);
3995 assert_eq!(op.step, SelectionStep::Character);
3996 assert_eq!(op.mode, SelectionMode::Move);
3997 }
3998 _ => unreachable!(),
3999 }
4000 }
4001
4002 #[test]
4003 fn no_focused_node_means_no_keyboard_system_changes() {
4004 let target = focused_node(2);
4005 let event = make_keydown_event(target);
4006 let kb = make_keyboard_state(VirtualKeyCode::Back);
4007 let mouse = MouseState::default();
4008 let sel = MockSelectionManager { click_count: 0, has_sel: false };
4009 let focus = MockFocusManager(None); let result = pre_callback_filter_internal_events(
4012 &[event], None, &kb, &mouse, &sel, &focus,
4013 );
4014
4015 assert!(result.system_changes.is_empty(),
4016 "No system changes should be generated without focused node");
4017 }
4018
4019 #[test]
4020 fn keydown_without_keyboard_data_generates_no_system_change() {
4021 let target = focused_node(2);
4022 let event = SyntheticEvent::new(
4023 EventType::KeyDown,
4024 EventSource::User,
4025 target,
4026 Instant::Tick(SystemTick::new(0)),
4027 EventData::None, );
4029 let kb = make_keyboard_state(VirtualKeyCode::Back);
4030 let mouse = MouseState::default();
4031 let sel = MockSelectionManager { click_count: 0, has_sel: false };
4032 let focus = MockFocusManager(Some(target));
4033
4034 let result = pre_callback_filter_internal_events(
4035 &[event], None, &kb, &mouse, &sel, &focus,
4036 );
4037
4038 assert!(result.system_changes.is_empty(),
4041 "EventData::None should not generate system changes (documents the old bug)");
4042 }
4043
4044 #[test]
4045 fn ctrl_c_generates_copy() {
4046 let primary_key = if cfg!(target_os = "macos") {
4052 VirtualKeyCode::LWin
4053 } else {
4054 VirtualKeyCode::LControl
4055 };
4056 let target = focused_node(2);
4057 let event = SyntheticEvent::new(
4058 EventType::KeyDown,
4059 EventSource::User,
4060 target,
4061 Instant::Tick(SystemTick::new(0)),
4062 EventData::Keyboard(KeyboardEventData {
4063 key_code: VirtualKeyCode::C as u32,
4064 char_code: Some('c'),
4065 modifiers: KeyModifiers {
4066 ctrl: !cfg!(target_os = "macos"),
4067 shift: false,
4068 alt: false,
4069 meta: cfg!(target_os = "macos"),
4070 },
4071 repeat: false,
4072 }),
4073 );
4074 let mut kb = make_keyboard_state(VirtualKeyCode::C);
4075 kb.pressed_virtual_keycodes = VirtualKeyCodeVec::from_vec(
4076 vec![VirtualKeyCode::C, primary_key]
4077 );
4078 let mouse = MouseState::default();
4079 let sel = MockSelectionManager { click_count: 0, has_sel: false };
4080 let focus = MockFocusManager(Some(target));
4081
4082 let result = pre_callback_filter_internal_events(
4083 &[event], None, &kb, &mouse, &sel, &focus,
4084 );
4085
4086 let copy_changes = result.system_changes.iter()
4087 .filter(|c| matches!(c, SystemChange::CopyToClipboard))
4088 .count();
4089
4090 assert_eq!(copy_changes, 1, "primary+C should generate CopyToClipboard");
4091 }
4092
4093 fn make_hit_test_with_node(node_idx: usize) -> FullHitTest {
4094 use crate::hit_test::{FullHitTest, HitTest, HitTestItem};
4095 use crate::dom::OptionDomNodeId;
4096 use std::collections::BTreeMap;
4097
4098 let node_id = NodeId::new(node_idx);
4099 let dom_id = DomId { inner: 0 };
4100
4101 let mut regular = BTreeMap::new();
4102 regular.insert(node_id, HitTestItem {
4103 point_in_viewport: LogicalPosition::new(100.0, 200.0),
4104 point_relative_to_item: LogicalPosition::new(50.0, 30.0),
4105 is_focusable: true,
4106 is_virtual_view_hit: None,
4107 hit_depth: 0,
4108 });
4109
4110 let mut hovered = BTreeMap::new();
4111 hovered.insert(dom_id, HitTest {
4112 regular_hit_test_nodes: regular,
4113 scroll_hit_test_nodes: BTreeMap::new(),
4114 scrollbar_hit_test_nodes: BTreeMap::new(),
4115 cursor_hit_test_nodes: BTreeMap::new(),
4116 });
4117
4118 FullHitTest {
4119 hovered_nodes: hovered,
4120 focused_node: OptionDomNodeId::None,
4121 }
4122 }
4123
4124 #[test]
4125 fn mousedown_generates_text_selection_click() {
4126 let target = focused_node(2);
4127 let event = SyntheticEvent::new(
4128 EventType::MouseDown,
4129 EventSource::User,
4130 target,
4131 Instant::Tick(SystemTick::new(0)),
4132 EventData::Mouse(MouseEventData {
4133 position: LogicalPosition::new(100.0, 200.0),
4134 button: MouseButton::Left,
4135 buttons: 1,
4136 modifiers: KeyModifiers::default(),
4137 }),
4138 );
4139 let hit_test = make_hit_test_with_node(2);
4140 let kb = KeyboardState::default();
4141 let mouse = MouseState::default();
4142 let sel = MockSelectionManager { click_count: 1, has_sel: false };
4143 let focus = MockFocusManager(Some(target));
4144
4145 let result = pre_callback_filter_internal_events(
4146 &[event], Some(&hit_test), &kb, &mouse, &sel, &focus,
4147 );
4148
4149 let click_changes = result.system_changes.iter()
4150 .filter(|c| matches!(c, SystemChange::TextSelectionClick { .. }))
4151 .count();
4152
4153 assert_eq!(click_changes, 1, "MouseDown with hit_test should generate TextSelectionClick");
4154 }
4155
4156 #[test]
4157 fn process_event_result_max_self_picks_higher_variant() {
4158 let lo = ProcessEventResult::ShouldReRenderCurrentWindow;
4159 let hi = ProcessEventResult::ShouldRegenerateDomCurrentWindow;
4160 assert_eq!(lo.max_self(hi), hi);
4161 assert_eq!(hi.max_self(lo), hi);
4162 assert_eq!(lo.max_self(lo), lo);
4163 }
4164
4165 #[test]
4166 fn keyboard_shortcut_keys_off_primary_modifier() {
4167 use crate::window::VirtualKeyCode::{A, C, V, X, Z};
4168 assert_eq!(KeyboardShortcut::from_key(C, false, false), None);
4170 assert_eq!(KeyboardShortcut::from_key(Z, false, true), None);
4171 assert_eq!(KeyboardShortcut::from_key(C, true, false), Some(KeyboardShortcut::Copy));
4173 assert_eq!(KeyboardShortcut::from_key(X, true, false), Some(KeyboardShortcut::Cut));
4174 assert_eq!(KeyboardShortcut::from_key(V, true, false), Some(KeyboardShortcut::Paste));
4175 assert_eq!(KeyboardShortcut::from_key(A, true, false), Some(KeyboardShortcut::SelectAll));
4176 assert_eq!(KeyboardShortcut::from_key(Z, true, false), Some(KeyboardShortcut::Undo));
4177 assert_eq!(KeyboardShortcut::from_key(Z, true, true), Some(KeyboardShortcut::Redo));
4178 }
4179
4180 #[test]
4181 fn primary_modifier_is_platform_correct() {
4182 use crate::window::{KeyboardState, VirtualKeyCode};
4183 let cmd_held = KeyboardState {
4184 pressed_virtual_keycodes: vec![VirtualKeyCode::LWin].into(),
4185 ..Default::default()
4186 };
4187 assert_eq!(cmd_held.primary_down(), cfg!(target_os = "macos"));
4189
4190 let ctrl_held = KeyboardState {
4191 pressed_virtual_keycodes: vec![VirtualKeyCode::LControl].into(),
4192 ..Default::default()
4193 };
4194 assert_eq!(ctrl_held.primary_down(), !cfg!(target_os = "macos"));
4196 }
4197
4198 #[test]
4199 fn arrow_direction_from_key_maps_arrows_and_home_end() {
4200 use crate::window::VirtualKeyCode::*;
4201 assert_eq!(ArrowDirection::from_key(Left, false), Some(ArrowDirection::Left));
4202 assert_eq!(ArrowDirection::from_key(Right, false), Some(ArrowDirection::Right));
4203 assert_eq!(ArrowDirection::from_key(Up, false), Some(ArrowDirection::Up));
4204 assert_eq!(ArrowDirection::from_key(Down, false), Some(ArrowDirection::Down));
4205 assert_eq!(ArrowDirection::from_key(Home, false), Some(ArrowDirection::LineStart));
4206 assert_eq!(ArrowDirection::from_key(End, false), Some(ArrowDirection::LineEnd));
4207 assert_eq!(ArrowDirection::from_key(Home, true), Some(ArrowDirection::DocumentStart));
4208 assert_eq!(ArrowDirection::from_key(End, true), Some(ArrowDirection::DocumentEnd));
4209 assert_eq!(ArrowDirection::from_key(C, false), None);
4210 }
4211
4212 #[test]
4213 fn arrow_direction_to_selection_respects_ctrl() {
4214 let (d, s) = ArrowDirection::Left.to_selection(false);
4215 assert_eq!((d, s), (SelectionDirection::Backward, SelectionStep::Character));
4216 let (d, s) = ArrowDirection::Left.to_selection(true);
4217 assert_eq!((d, s), (SelectionDirection::Backward, SelectionStep::Word));
4218 let (d, s) = ArrowDirection::Up.to_selection(false);
4219 assert_eq!((d, s), (SelectionDirection::Backward, SelectionStep::VisualLine));
4220 let (d, s) = ArrowDirection::DocumentEnd.to_selection(false);
4221 assert_eq!((d, s), (SelectionDirection::Forward, SelectionStep::Document));
4222 }
4223
4224 #[test]
4225 fn keyboard_shortcut_from_key_recognizes_editing_combos() {
4226 use crate::window::VirtualKeyCode::*;
4227 assert_eq!(KeyboardShortcut::from_key(C, true, false), Some(KeyboardShortcut::Copy));
4228 assert_eq!(KeyboardShortcut::from_key(X, true, false), Some(KeyboardShortcut::Cut));
4229 assert_eq!(KeyboardShortcut::from_key(V, true, false), Some(KeyboardShortcut::Paste));
4230 assert_eq!(KeyboardShortcut::from_key(A, true, false), Some(KeyboardShortcut::SelectAll));
4231 assert_eq!(KeyboardShortcut::from_key(Z, true, false), Some(KeyboardShortcut::Undo));
4232 assert_eq!(KeyboardShortcut::from_key(Z, true, true), Some(KeyboardShortcut::Redo));
4233 assert_eq!(KeyboardShortcut::from_key(Y, true, false), Some(KeyboardShortcut::Redo));
4234 assert_eq!(KeyboardShortcut::from_key(C, false, false), None);
4236 assert_eq!(KeyboardShortcut::from_key(D, true, false), None);
4238 }
4239
4240 #[test]
4241 fn mouse_button_state_round_trips_from_mouse_state() {
4242 let ms = MouseState {
4243 left_down: true,
4244 middle_down: true,
4245 ..MouseState::default()
4246 };
4247 let bs: MouseButtonState = (&ms).into();
4248 assert!(bs.left_down);
4249 assert!(!bs.right_down);
4250 assert!(bs.middle_down);
4251 assert!(bs.any_down());
4252
4253 let none = MouseButtonState { left_down: false, right_down: false, middle_down: false };
4254 assert!(!none.any_down());
4255 }
4256
4257 #[test]
4258 fn callback_to_call_collects_hits_for_dom() {
4259 let dom_id = DomId { inner: 0 };
4260 let hit_test = make_hit_test_with_node(2);
4261 let filter = EventFilter::Hover(HoverEventFilter::MouseDown);
4262 let calls = CallbackToCall::from_hit_test(&hit_test, dom_id, filter);
4263 assert_eq!(calls.len(), 1);
4264 assert_eq!(calls[0].node_id, NodeId::new(2));
4265 assert_eq!(calls[0].event_filter, filter);
4266 assert!(calls[0].hit_test_item.is_some());
4267
4268 let other = CallbackToCall::from_hit_test(
4270 &hit_test,
4271 DomId { inner: 999 },
4272 EventFilter::Hover(HoverEventFilter::MouseUp),
4273 );
4274 assert!(other.is_empty());
4275
4276 let direct = CallbackToCall::new(
4278 NodeId::new(7),
4279 None,
4280 EventFilter::Focus(FocusEventFilter::FocusReceived),
4281 );
4282 assert_eq!(direct.node_id, NodeId::new(7));
4283 assert!(direct.hit_test_item.is_none());
4284 }
4285
4286 #[test]
4287 fn restyle_relayout_aliases_are_btreemap_compatible() {
4288 let restyle: RestyleNodes = BTreeMap::new();
4291 let relayout: RelayoutNodes = BTreeMap::new();
4292 assert!(restyle.is_empty());
4293 assert!(relayout.is_empty());
4294
4295 let mut words: RelayoutWords = BTreeMap::new();
4297 words.insert(NodeId::new(1), AzString::from_const_str("hello"));
4298 assert_eq!(words.get(&NodeId::new(1)).map(azul_css::AzString::as_str), Some("hello"));
4299 }
4300
4301 #[test]
4302 fn detect_lifecycle_events_with_reconciliation_is_callable() {
4303 let dom_id = DomId { inner: 0 };
4307 let old_data: Vec<crate::dom::NodeData> = Vec::new();
4308 let new_data: Vec<crate::dom::NodeData> = Vec::new();
4309 let old_hier: Vec<crate::styled_dom::NodeHierarchyItem> = Vec::new();
4310 let new_hier: Vec<crate::styled_dom::NodeHierarchyItem> = Vec::new();
4311 let old_layout = OrderedMap::default();
4312 let new_layout = OrderedMap::default();
4313 let result: LifecycleEventResult = detect_lifecycle_events_with_reconciliation(
4314 dom_id,
4315 &old_data,
4316 &new_data,
4317 &old_hier,
4318 &new_hier,
4319 &old_layout,
4320 &new_layout,
4321 Instant::Tick(SystemTick::new(0)),
4322 );
4323 assert!(result.events.is_empty());
4324 assert!(result.node_id_mapping.is_empty());
4325 }
4326
4327 #[test]
4328 fn nodedata_focusable_and_activation_traits_are_wired() {
4329 use crate::dom::{NodeData, NodeType};
4330 use crate::events::{ActivationBehavior as _, Focusable as _};
4331
4332 let btn = NodeData::create_node(NodeType::Button);
4334 assert!(<NodeData as Focusable>::is_naturally_focusable(&btn));
4335 assert!(<NodeData as Focusable>::is_focusable(&btn));
4336 assert!(<NodeData as ActivationBehavior>::has_activation_behavior(&btn));
4337 assert!(<NodeData as ActivationBehavior>::is_activatable(&btn));
4338
4339 let div = NodeData::create_node(NodeType::Div);
4341 assert!(!<NodeData as Focusable>::is_naturally_focusable(&div));
4342 assert!(!<NodeData as ActivationBehavior>::has_activation_behavior(&div));
4343
4344 let input = NodeData::create_node(NodeType::Input);
4346 assert!(<NodeData as Focusable>::is_naturally_focusable(&input));
4347 }
4348
4349 #[test]
4350 fn first_hovered_node_picks_frontmost_by_depth() {
4351 use crate::hit_test::{FullHitTest, HitTest, HitTestItem};
4352 use crate::dom::OptionDomNodeId;
4353 use std::collections::BTreeMap;
4354
4355 let item = |depth: u32| HitTestItem {
4356 point_in_viewport: LogicalPosition::zero(),
4357 point_relative_to_item: LogicalPosition::zero(),
4358 is_focusable: true,
4359 is_virtual_view_hit: None,
4360 hit_depth: depth,
4361 };
4362
4363 let mut regular = BTreeMap::new();
4367 regular.insert(NodeId::new(2), item(5));
4368 regular.insert(NodeId::new(5), item(0));
4369
4370 let mut hovered = BTreeMap::new();
4371 hovered.insert(DomId { inner: 0 }, HitTest {
4372 regular_hit_test_nodes: regular,
4373 scroll_hit_test_nodes: BTreeMap::new(),
4374 scrollbar_hit_test_nodes: BTreeMap::new(),
4375 cursor_hit_test_nodes: BTreeMap::new(),
4376 });
4377 let ht = FullHitTest { hovered_nodes: hovered, focused_node: OptionDomNodeId::None };
4378
4379 let got = get_first_hovered_node(Some(&ht)).unwrap();
4380 assert_eq!(got.node.into_crate_internal(), Some(NodeId::new(5)));
4381 }
4382
4383 #[test]
4384 fn size_changed_nan_guard_stops_resize_loop() {
4385 use crate::geom::LogicalSize;
4386 let a = LogicalSize::new(f32::NAN, 100.0);
4389 let b = LogicalSize::new(f32::NAN, 100.0);
4390 assert!(!size_changed(a, b));
4391 assert!(size_changed(LogicalSize::new(100.0, 100.0), LogicalSize::new(100.0, 120.0)));
4393 assert!(!size_changed(LogicalSize::new(100.0, 100.0), LogicalSize::new(100.00005, 100.0)));
4395 }
4396
4397 #[test]
4398 fn dom_path_terminates_on_parent_cycle() {
4399 use crate::id::{Node, NodeHierarchy};
4400 let nodes = vec![
4402 Node { parent: Some(NodeId::new(1)), ..Node::ROOT },
4403 Node { parent: Some(NodeId::new(0)), ..Node::ROOT },
4404 ];
4405 let hier = NodeHierarchy::new(nodes);
4406 let target = NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(0)));
4407 let path = get_dom_path(&hier, target);
4409 assert!(path.len() <= 2);
4410 }
4411
4412 #[test]
4413 fn click_event_maps_to_left_mouse_up() {
4414 let filters = event_type_to_filters(EventType::Click, &EventData::None);
4415 assert!(filters.contains(&EventFilter::Hover(HoverEventFilter::LeftMouseUp)));
4416 assert!(!filters.contains(&EventFilter::Hover(HoverEventFilter::LeftMouseDown)));
4417 }
4418}
4419
4420#[cfg(test)]
4421#[allow(clippy::float_cmp, clippy::too_many_lines)]
4422mod autotest_generated {
4423 use super::*;
4424 use crate::{
4425 dom::{DomId, DomNodeId, OptionDomNodeId},
4426 geom::{LogicalPosition, LogicalRect, LogicalSize},
4427 hit_test::{FullHitTest, HitTest, HitTestItem},
4428 id::{Node, NodeHierarchy, NodeId},
4429 styled_dom::NodeHierarchyItemId,
4430 task::{Instant, SystemTick},
4431 window::{CursorPosition, KeyboardState, MouseState, VirtualKeyCode, VirtualKeyCodeVec},
4432 };
4433
4434 fn tick(n: u64) -> Instant {
4437 Instant::Tick(SystemTick::new(n))
4438 }
4439
4440 fn dnid(dom: usize, node: usize) -> DomNodeId {
4441 DomNodeId {
4442 dom: DomId { inner: dom },
4443 node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(node))),
4444 }
4445 }
4446
4447 fn dnid_none(dom: usize) -> DomNodeId {
4449 DomNodeId {
4450 dom: DomId { inner: dom },
4451 node: NodeHierarchyItemId::NONE,
4452 }
4453 }
4454
4455 fn hit_item(depth: u32) -> HitTestItem {
4456 HitTestItem {
4457 point_in_viewport: LogicalPosition::new(1.0, 2.0),
4458 point_relative_to_item: LogicalPosition::new(3.0, 4.0),
4459 is_focusable: true,
4460 is_virtual_view_hit: None,
4461 hit_depth: depth,
4462 }
4463 }
4464
4465 fn hit_test_with(dom: usize, nodes: &[(usize, u32)]) -> FullHitTest {
4467 let mut regular = BTreeMap::new();
4468 for (idx, depth) in nodes {
4469 regular.insert(NodeId::new(*idx), hit_item(*depth));
4470 }
4471 let mut hovered = BTreeMap::new();
4472 hovered.insert(
4473 DomId { inner: dom },
4474 HitTest {
4475 regular_hit_test_nodes: regular,
4476 scroll_hit_test_nodes: BTreeMap::new(),
4477 scrollbar_hit_test_nodes: BTreeMap::new(),
4478 cursor_hit_test_nodes: BTreeMap::new(),
4479 },
4480 );
4481 FullHitTest {
4482 hovered_nodes: hovered,
4483 focused_node: OptionDomNodeId::None,
4484 }
4485 }
4486
4487 fn empty_hit_test() -> FullHitTest {
4488 FullHitTest {
4489 hovered_nodes: BTreeMap::new(),
4490 focused_node: OptionDomNodeId::None,
4491 }
4492 }
4493
4494 fn mouse_event(ty: EventType, button: MouseButton, pos: LogicalPosition) -> SyntheticEvent {
4495 SyntheticEvent::new(
4496 ty,
4497 EventSource::User,
4498 dnid(0, 0),
4499 tick(0),
4500 EventData::Mouse(MouseEventData {
4501 position: pos,
4502 button,
4503 buttons: 1,
4504 modifiers: KeyModifiers::default(),
4505 }),
4506 )
4507 }
4508
4509 fn key_event(key_code: u32, modifiers: KeyModifiers) -> SyntheticEvent {
4510 SyntheticEvent::new(
4511 EventType::KeyDown,
4512 EventSource::User,
4513 dnid(0, 0),
4514 tick(0),
4515 EventData::Keyboard(KeyboardEventData {
4516 key_code,
4517 char_code: None,
4518 modifiers,
4519 repeat: false,
4520 }),
4521 )
4522 }
4523
4524 fn hierarchy_chain(len: usize) -> NodeHierarchy {
4526 let nodes = (0..len)
4527 .map(|i| Node {
4528 parent: if i == 0 { None } else { Some(NodeId::new(i - 1)) },
4529 ..Node::ROOT
4530 })
4531 .collect::<Vec<_>>();
4532 NodeHierarchy::new(nodes)
4533 }
4534
4535 fn primary_modifiers() -> KeyModifiers {
4537 if cfg!(target_os = "macos") {
4538 KeyModifiers::new().with_meta()
4539 } else {
4540 KeyModifiers::new().with_ctrl()
4541 }
4542 }
4543
4544 fn keyboard_with_primary_held() -> KeyboardState {
4545 let key = if cfg!(target_os = "macos") {
4546 VirtualKeyCode::LWin
4547 } else {
4548 VirtualKeyCode::LControl
4549 };
4550 KeyboardState {
4551 pressed_virtual_keycodes: VirtualKeyCodeVec::from_vec(vec![key]),
4552 ..KeyboardState::default()
4553 }
4554 }
4555
4556 #[test]
4560 fn size_changed_zero_and_identity() {
4561 assert!(!size_changed(LogicalSize::zero(), LogicalSize::zero()));
4562 assert!(!size_changed(
4563 LogicalSize::new(0.0, 0.0),
4564 LogicalSize::new(-0.0, -0.0)
4565 ));
4566 assert!(size_changed(LogicalSize::zero(), LogicalSize::new(0.0, 1.0)));
4568 assert!(size_changed(LogicalSize::zero(), LogicalSize::new(1.0, 0.0)));
4569 }
4570
4571 #[test]
4572 fn size_changed_single_sided_nan_is_a_change() {
4573 assert!(size_changed(
4576 LogicalSize::new(f32::NAN, 10.0),
4577 LogicalSize::new(10.0, 10.0)
4578 ));
4579 assert!(size_changed(
4580 LogicalSize::new(10.0, 10.0),
4581 LogicalSize::new(10.0, f32::NAN)
4582 ));
4583 assert!(!size_changed(
4586 LogicalSize::new(f32::NAN, f32::NAN),
4587 LogicalSize::new(f32::NAN, f32::NAN)
4588 ));
4589 }
4590
4591 #[test]
4592 fn size_changed_negative_and_infinite_do_not_panic() {
4593 assert!(size_changed(
4595 LogicalSize::new(-100.0, 0.0),
4596 LogicalSize::new(100.0, 0.0)
4597 ));
4598 assert!(!size_changed(
4599 LogicalSize::new(-100.0, -50.0),
4600 LogicalSize::new(-100.0, -50.0)
4601 ));
4602 assert!(!size_changed(
4605 LogicalSize::new(f32::INFINITY, f32::INFINITY),
4606 LogicalSize::new(f32::INFINITY, f32::INFINITY)
4607 ));
4608 assert!(size_changed(
4609 LogicalSize::new(f32::INFINITY, 0.0),
4610 LogicalSize::new(f32::NEG_INFINITY, 0.0)
4611 ));
4612 assert!(!size_changed(
4616 LogicalSize::new(f32::MAX, 0.0),
4617 LogicalSize::new(f32::INFINITY, 0.0)
4618 ));
4619 }
4620
4621 #[test]
4622 fn size_changed_ignores_sub_quantum_jitter_but_sees_one_quantum() {
4623 assert!(!size_changed(
4625 LogicalSize::new(50.0, 50.0),
4626 LogicalSize::new(50.0004, 50.0)
4627 ));
4628 assert!(size_changed(
4630 LogicalSize::new(50.0, 50.0),
4631 LogicalSize::new(50.002, 50.0)
4632 ));
4633 }
4634
4635 #[test]
4638 fn create_mount_event_without_layout_entry_falls_back_to_zero_rect() {
4639 let layout: BTreeMap<NodeId, LogicalRect> = BTreeMap::new();
4640 let ev = create_mount_event(NodeId::new(3), DomId { inner: 0 }, &layout, &tick(7));
4641 assert_eq!(ev.event_type, EventType::Mount);
4642 assert_eq!(ev.source, EventSource::Lifecycle);
4643 assert_eq!(ev.phase, EventPhase::Target);
4644 assert_eq!(ev.target, ev.current_target);
4645 assert_eq!(ev.target.node.into_crate_internal(), Some(NodeId::new(3)));
4646 match ev.data {
4647 EventData::Lifecycle(d) => {
4648 assert_eq!(d.reason, LifecycleReason::InitialMount);
4649 assert!(d.previous_bounds.is_none());
4650 assert_eq!(d.current_bounds, LogicalRect::zero());
4651 }
4652 _ => panic!("mount event must carry lifecycle data"),
4653 }
4654 }
4655
4656 #[test]
4657 fn create_unmount_event_reports_previous_bounds_and_zero_current() {
4658 let mut layout = BTreeMap::new();
4659 let rect = LogicalRect::new(LogicalPosition::new(1.0, 2.0), LogicalSize::new(3.0, 4.0));
4660 layout.insert(NodeId::new(1), rect);
4661 let ev = create_unmount_event(NodeId::new(1), DomId { inner: 2 }, &layout, &tick(9));
4662 assert_eq!(ev.event_type, EventType::Unmount);
4663 match ev.data {
4664 EventData::Lifecycle(d) => {
4665 assert_eq!(d.reason, LifecycleReason::Unmount);
4666 assert_eq!(d.previous_bounds, Some(rect));
4667 assert_eq!(d.current_bounds, LogicalRect::zero());
4668 }
4669 _ => panic!("unmount event must carry lifecycle data"),
4670 }
4671 }
4672
4673 #[test]
4674 fn create_lifecycle_event_survives_extreme_node_ids() {
4675 let huge = NodeId::new(usize::MAX - 1);
4678 let layout: BTreeMap<NodeId, LogicalRect> = BTreeMap::new();
4679 let ev = create_mount_event(huge, DomId { inner: usize::MAX }, &layout, &tick(0));
4680 assert_eq!(ev.target.node.into_crate_internal(), Some(huge));
4681 assert_eq!(ev.target.dom, DomId { inner: usize::MAX });
4682
4683 let root = create_mount_event(NodeId::ZERO, DomId { inner: 0 }, &layout, &tick(0));
4686 assert_eq!(
4687 root.target.node.into_crate_internal(),
4688 Some(NodeId::ZERO),
4689 "NodeId 0 must not decode as `None`"
4690 );
4691 }
4692
4693 #[test]
4694 fn create_resize_event_returns_none_for_missing_or_unchanged_layout() {
4695 let dom = DomId { inner: 0 };
4696 let node = NodeId::new(1);
4697 let rect = LogicalRect::new(LogicalPosition::zero(), LogicalSize::new(10.0, 10.0));
4698
4699 let empty: BTreeMap<NodeId, LogicalRect> = BTreeMap::new();
4700 let mut one = BTreeMap::new();
4701 one.insert(node, rect);
4702
4703 assert!(create_resize_event(node, dom, &empty, &one, &tick(0)).is_none());
4705 assert!(create_resize_event(node, dom, &one, &empty, &tick(0)).is_none());
4706 assert!(create_resize_event(node, dom, &empty, &empty, &tick(0)).is_none());
4707 assert!(create_resize_event(node, dom, &one, &one, &tick(0)).is_none());
4709 }
4710
4711 #[test]
4712 fn create_resize_event_ignores_pure_origin_moves() {
4713 let dom = DomId { inner: 0 };
4716 let node = NodeId::new(0);
4717 let size = LogicalSize::new(10.0, 10.0);
4718 let mut old = BTreeMap::new();
4719 old.insert(node, LogicalRect::new(LogicalPosition::new(0.0, 0.0), size));
4720 let mut new = BTreeMap::new();
4721 new.insert(
4722 node,
4723 LogicalRect::new(LogicalPosition::new(500.0, 500.0), size),
4724 );
4725 assert!(create_resize_event(node, dom, &old, &new, &tick(0)).is_none());
4726 }
4727
4728 #[test]
4729 fn create_resize_event_nan_size_does_not_loop_forever() {
4730 let dom = DomId { inner: 0 };
4733 let node = NodeId::new(0);
4734 let nan_rect = LogicalRect::new(
4735 LogicalPosition::zero(),
4736 LogicalSize::new(f32::NAN, 100.0),
4737 );
4738 let mut old = BTreeMap::new();
4739 old.insert(node, nan_rect);
4740 let mut new = BTreeMap::new();
4741 new.insert(node, nan_rect);
4742 assert!(create_resize_event(node, dom, &old, &new, &tick(0)).is_none());
4743 }
4744
4745 #[test]
4746 fn create_resize_event_reports_both_bounds_on_real_change() {
4747 let dom = DomId { inner: 0 };
4748 let node = NodeId::new(0);
4749 let old_rect =
4750 LogicalRect::new(LogicalPosition::zero(), LogicalSize::new(10.0, 10.0));
4751 let new_rect =
4752 LogicalRect::new(LogicalPosition::zero(), LogicalSize::new(10.0, 20.0));
4753 let mut old = BTreeMap::new();
4754 old.insert(node, old_rect);
4755 let mut new = BTreeMap::new();
4756 new.insert(node, new_rect);
4757
4758 let ev = create_resize_event(node, dom, &old, &new, &tick(3))
4759 .expect("a real size change must emit a Resize");
4760 assert_eq!(ev.event_type, EventType::Resize);
4761 match ev.data {
4762 EventData::Lifecycle(d) => {
4763 assert_eq!(d.reason, LifecycleReason::Resize);
4764 assert_eq!(d.previous_bounds, Some(old_rect));
4765 assert_eq!(d.current_bounds, new_rect);
4766 }
4767 _ => panic!("resize event must carry lifecycle data"),
4768 }
4769 }
4770
4771 #[test]
4774 fn detect_lifecycle_events_all_none_is_empty() {
4775 let events = detect_lifecycle_events(
4776 DomId { inner: 0 },
4777 DomId { inner: 0 },
4778 None,
4779 None,
4780 None,
4781 None,
4782 tick(0),
4783 );
4784 assert!(events.is_empty());
4785 }
4786
4787 #[test]
4788 fn detect_lifecycle_events_without_layout_emits_nothing() {
4789 let old = hierarchy_chain(1);
4791 let new = hierarchy_chain(4);
4792 let events = detect_lifecycle_events(
4793 DomId { inner: 0 },
4794 DomId { inner: 0 },
4795 Some(&old),
4796 Some(&new),
4797 None,
4798 None,
4799 tick(0),
4800 );
4801 assert!(events.is_empty());
4802 }
4803
4804 #[test]
4805 fn detect_lifecycle_events_emits_mounts_unmounts_and_resizes() {
4806 let dom = DomId { inner: 0 };
4807 let old_hier = hierarchy_chain(2); let new_hier = hierarchy_chain(3); let r = |h: f32| LogicalRect::new(LogicalPosition::zero(), LogicalSize::new(10.0, h));
4811 let mut old_layout = BTreeMap::new();
4812 old_layout.insert(NodeId::new(0), r(10.0));
4813 old_layout.insert(NodeId::new(1), r(10.0));
4814 let mut new_layout = BTreeMap::new();
4815 new_layout.insert(NodeId::new(0), r(10.0)); new_layout.insert(NodeId::new(1), r(99.0)); new_layout.insert(NodeId::new(2), r(10.0)); let events = detect_lifecycle_events(
4820 dom,
4821 dom,
4822 Some(&old_hier),
4823 Some(&new_hier),
4824 Some(&old_layout),
4825 Some(&new_layout),
4826 tick(5),
4827 );
4828
4829 let mounts: Vec<_> = events
4830 .iter()
4831 .filter(|e| e.event_type == EventType::Mount)
4832 .collect();
4833 let resizes: Vec<_> = events
4834 .iter()
4835 .filter(|e| e.event_type == EventType::Resize)
4836 .collect();
4837 assert_eq!(mounts.len(), 1, "only node 2 is new");
4838 assert_eq!(
4839 mounts[0].target.node.into_crate_internal(),
4840 Some(NodeId::new(2))
4841 );
4842 assert_eq!(resizes.len(), 1, "only node 1 changed size");
4843 assert_eq!(
4844 resizes[0].target.node.into_crate_internal(),
4845 Some(NodeId::new(1))
4846 );
4847 assert!(
4848 !events.iter().any(|e| e.event_type == EventType::Unmount),
4849 "nothing was removed"
4850 );
4851 assert!(events.iter().all(|e| e.source == EventSource::Lifecycle));
4852
4853 let events = detect_lifecycle_events(
4855 dom,
4856 dom,
4857 Some(&new_hier),
4858 Some(&old_hier),
4859 Some(&new_layout),
4860 Some(&old_layout),
4861 tick(6),
4862 );
4863 let unmounts: Vec<_> = events
4864 .iter()
4865 .filter(|e| e.event_type == EventType::Unmount)
4866 .collect();
4867 assert_eq!(unmounts.len(), 1);
4868 assert_eq!(
4869 unmounts[0].target.node.into_crate_internal(),
4870 Some(NodeId::new(2))
4871 );
4872 }
4873
4874 #[test]
4875 fn detect_lifecycle_events_mount_of_node_missing_from_layout_uses_zero_rect() {
4876 let dom = DomId { inner: 0 };
4877 let new_hier = hierarchy_chain(2);
4878 let new_layout: BTreeMap<NodeId, LogicalRect> = BTreeMap::new(); let events = detect_lifecycle_events(
4880 dom,
4881 dom,
4882 None,
4883 Some(&new_hier),
4884 None,
4885 Some(&new_layout),
4886 tick(0),
4887 );
4888 assert_eq!(events.len(), 2);
4889 for ev in &events {
4890 match ev.data {
4891 EventData::Lifecycle(d) => assert_eq!(d.current_bounds, LogicalRect::zero()),
4892 _ => panic!("expected lifecycle data"),
4893 }
4894 }
4895 }
4896
4897 #[test]
4898 fn collect_node_ids_handles_none_and_empty_hierarchies() {
4899 assert!(collect_node_ids(None).is_empty());
4900 let empty = NodeHierarchy::new(Vec::new());
4901 assert!(collect_node_ids(Some(&empty)).is_empty());
4902 let three = hierarchy_chain(3);
4903 let ids = collect_node_ids(Some(&three));
4904 assert_eq!(ids.len(), 3);
4905 assert!(ids.contains(&NodeId::ZERO));
4906 assert!(ids.contains(&NodeId::new(2)));
4907 }
4908
4909 #[test]
4912 fn process_event_result_order_is_dense_and_monotonic() {
4913 let all = [
4914 ProcessEventResult::DoNothing,
4915 ProcessEventResult::ShouldReRenderCurrentWindow,
4916 ProcessEventResult::ShouldUpdateDisplayListCurrentWindow,
4917 ProcessEventResult::UpdateHitTesterAndProcessAgain,
4918 ProcessEventResult::ShouldIncrementalRelayout,
4919 ProcessEventResult::ShouldRegenerateDomCurrentWindow,
4920 ProcessEventResult::ShouldRegenerateDomAllWindows,
4921 ];
4922 for (i, r) in all.iter().enumerate() {
4923 assert_eq!(r.order(), i, "order() must match declaration index");
4924 }
4925 for a in all {
4927 for b in all {
4928 assert_eq!(a < b, a.order() < b.order());
4929 let joined = a.max_self(b);
4930 assert_eq!(joined.order(), a.order().max(b.order()));
4931 assert_eq!(joined, b.max_self(a), "max_self must be commutative");
4932 assert_eq!(a.max_self(a), a, "max_self must be idempotent");
4933 }
4934 }
4935 }
4936
4937 #[test]
4938 fn key_modifiers_builders_are_orthogonal_and_is_empty_tracks_them() {
4939 let empty = KeyModifiers::new();
4940 assert!(empty.is_empty());
4941 assert_eq!(empty, KeyModifiers::default());
4942
4943 assert_eq!(
4945 KeyModifiers::new().with_shift(),
4946 KeyModifiers { shift: true, ctrl: false, alt: false, meta: false }
4947 );
4948 assert_eq!(
4949 KeyModifiers::new().with_ctrl(),
4950 KeyModifiers { shift: false, ctrl: true, alt: false, meta: false }
4951 );
4952 assert_eq!(
4953 KeyModifiers::new().with_alt(),
4954 KeyModifiers { shift: false, ctrl: false, alt: true, meta: false }
4955 );
4956 assert_eq!(
4957 KeyModifiers::new().with_meta(),
4958 KeyModifiers { shift: false, ctrl: false, alt: false, meta: true }
4959 );
4960
4961 assert!(!KeyModifiers::new().with_shift().is_empty());
4963 assert!(!KeyModifiers::new().with_ctrl().is_empty());
4964 assert!(!KeyModifiers::new().with_alt().is_empty());
4965 assert!(!KeyModifiers::new().with_meta().is_empty());
4966 assert_eq!(
4967 KeyModifiers::new().with_ctrl().with_ctrl(),
4968 KeyModifiers::new().with_ctrl()
4969 );
4970 let all = KeyModifiers::new().with_shift().with_ctrl().with_alt().with_meta();
4971 assert!(!all.is_empty());
4972 assert!(all.shift && all.ctrl && all.alt && all.meta);
4973 }
4974
4975 #[test]
4976 fn scroll_into_view_options_presets_and_behavior_setters() {
4977 assert_eq!(
4978 ScrollIntoViewOptions::default(),
4979 ScrollIntoViewOptions::nearest(),
4980 "Default must be the `nearest`/`auto` preset"
4981 );
4982 for (opts, expected) in [
4983 (ScrollIntoViewOptions::nearest(), ScrollLogicalPosition::Nearest),
4984 (ScrollIntoViewOptions::center(), ScrollLogicalPosition::Center),
4985 (ScrollIntoViewOptions::start(), ScrollLogicalPosition::Start),
4986 (ScrollIntoViewOptions::end(), ScrollLogicalPosition::End),
4987 ] {
4988 assert_eq!(opts.block, expected);
4989 assert_eq!(opts.inline_axis, expected, "both axes must be aligned alike");
4990 assert_eq!(opts.behavior, ScrollIntoViewBehavior::Auto);
4991
4992 let instant = opts.with_instant();
4994 assert_eq!(instant.behavior, ScrollIntoViewBehavior::Instant);
4995 assert_eq!(instant.block, opts.block);
4996 assert_eq!(instant.inline_axis, opts.inline_axis);
4997
4998 let smooth = opts.with_smooth();
4999 assert_eq!(smooth.behavior, ScrollIntoViewBehavior::Smooth);
5000 assert_eq!(
5001 opts.with_instant().with_smooth().behavior,
5002 ScrollIntoViewBehavior::Smooth
5003 );
5004 assert_eq!(
5005 opts.with_smooth().with_instant().behavior,
5006 ScrollIntoViewBehavior::Instant
5007 );
5008 }
5009 }
5010
5011 #[test]
5012 fn default_action_result_has_action_predicate() {
5013 assert!(!DefaultActionResult::default().has_action());
5014 assert!(!DefaultActionResult::prevented().has_action());
5015 assert!(DefaultActionResult::prevented().prevented);
5016 assert_eq!(DefaultActionResult::prevented().action, DefaultAction::None);
5017
5018 let none = DefaultActionResult::new(DefaultAction::None);
5020 assert!(!none.prevented);
5021 assert!(!none.has_action());
5022
5023 for action in [
5025 DefaultAction::FocusNext,
5026 DefaultAction::FocusPrevious,
5027 DefaultAction::FocusFirst,
5028 DefaultAction::FocusLast,
5029 DefaultAction::ClearFocus,
5030 DefaultAction::SelectAllText,
5031 DefaultAction::ActivateFocusedElement { target: dnid(0, 1) },
5032 DefaultAction::SubmitForm { form_node: dnid(0, 1) },
5033 DefaultAction::CloseModal { modal_node: dnid(0, 1) },
5034 DefaultAction::ScrollFocusedContainer {
5035 direction: ScrollDirection::Down,
5036 amount: ScrollAmount::Page,
5037 },
5038 ] {
5039 let r = DefaultActionResult::new(action);
5040 assert_eq!(r.action, action);
5041 assert!(!r.prevented);
5042 assert!(r.has_action(), "{action:?} must be reported as actionable");
5043 }
5044 }
5045
5046 #[test]
5047 fn synthetic_event_constructor_invariants_and_flag_transitions() {
5048 let target = dnid(3, 7);
5049 let mut ev = SyntheticEvent::new(
5050 EventType::Click,
5051 EventSource::Programmatic,
5052 target,
5053 tick(42),
5054 EventData::None,
5055 );
5056 assert_eq!(ev.event_type, EventType::Click);
5058 assert_eq!(ev.source, EventSource::Programmatic);
5059 assert_eq!(ev.phase, EventPhase::Target);
5060 assert_eq!(ev.target, target);
5061 assert_eq!(ev.current_target, target);
5062 assert_eq!(ev.timestamp, tick(42));
5063 assert!(!ev.is_propagation_stopped());
5064 assert!(!ev.is_immediate_propagation_stopped());
5065 assert!(!ev.is_default_prevented());
5066
5067 ev.stop_propagation();
5069 assert!(ev.is_propagation_stopped());
5070 assert!(!ev.is_immediate_propagation_stopped());
5071
5072 let mut ev2 = SyntheticEvent::new(
5075 EventType::Click,
5076 EventSource::User,
5077 target,
5078 tick(0),
5079 EventData::None,
5080 );
5081 ev2.stop_immediate_propagation();
5082 assert!(ev2.is_immediate_propagation_stopped());
5083 assert!(
5084 ev2.is_propagation_stopped(),
5085 "immediate stop must also stop normal propagation"
5086 );
5087
5088 let mut ev3 = ev2.clone();
5090 ev3.stop_immediate_propagation();
5091 ev3.prevent_default();
5092 ev3.prevent_default();
5093 assert!(ev3.is_default_prevented());
5094 assert!(!ev.is_default_prevented(), "flags must not leak across events");
5095 }
5096
5097 #[test]
5098 fn hover_filter_is_system_internal_only_for_system_text_clicks() {
5099 for f in [
5100 HoverEventFilter::SystemTextSingleClick,
5101 HoverEventFilter::SystemTextDoubleClick,
5102 HoverEventFilter::SystemTextTripleClick,
5103 ] {
5104 assert!(f.is_system_internal(), "{f:?} is internal");
5105 assert!(
5106 f.to_focus_event_filter().is_none(),
5107 "internal filters must never be exposed as focus callbacks"
5108 );
5109 }
5110 for f in [
5111 HoverEventFilter::MouseOver,
5112 HoverEventFilter::MouseDown,
5113 HoverEventFilter::Drop,
5114 HoverEventFilter::KeyringResult,
5115 HoverEventFilter::MouseOut,
5116 ] {
5117 assert!(!f.is_system_internal(), "{f:?} is a user-visible filter");
5118 }
5119 }
5120
5121 #[test]
5122 fn event_filter_kind_predicates_are_mutually_exclusive() {
5123 let hover = EventFilter::Hover(HoverEventFilter::MouseDown);
5124 let focus = EventFilter::Focus(FocusEventFilter::FocusReceived);
5125 let window = EventFilter::Window(WindowEventFilter::Resized);
5126 let component = EventFilter::Component(ComponentEventFilter::AfterMount);
5127 let app = EventFilter::Application(ApplicationEventFilter::DeviceConnected);
5128
5129 assert!(focus.is_focus_callback());
5130 assert!(window.is_window_callback());
5131 for f in [hover, window, component, app] {
5132 assert!(!f.is_focus_callback(), "{f:?} is not a focus callback");
5133 }
5134 for f in [hover, focus, component, app] {
5135 assert!(!f.is_window_callback(), "{f:?} is not a window callback");
5136 }
5137 assert_eq!(hover.as_hover_event_filter(), Some(HoverEventFilter::MouseDown));
5139 assert_eq!(hover.as_focus_event_filter(), None);
5140 assert_eq!(hover.as_window_event_filter(), None);
5141 assert_eq!(focus.as_focus_event_filter(), Some(FocusEventFilter::FocusReceived));
5142 assert_eq!(window.as_window_event_filter(), Some(WindowEventFilter::Resized));
5143 assert_eq!(component.as_hover_event_filter(), None);
5144 }
5145
5146 #[test]
5149 fn window_to_hover_filter_mapping_never_yields_an_internal_filter() {
5150 for w in [
5153 WindowEventFilter::MouseOver,
5154 WindowEventFilter::MouseDown,
5155 WindowEventFilter::LeftMouseDown,
5156 WindowEventFilter::RightMouseDown,
5157 WindowEventFilter::MiddleMouseDown,
5158 WindowEventFilter::MouseUp,
5159 WindowEventFilter::LeftMouseUp,
5160 WindowEventFilter::RightMouseUp,
5161 WindowEventFilter::MiddleMouseUp,
5162 WindowEventFilter::Scroll,
5163 WindowEventFilter::TextInput,
5164 WindowEventFilter::VirtualKeyDown,
5165 WindowEventFilter::VirtualKeyUp,
5166 WindowEventFilter::HoveredFile,
5167 WindowEventFilter::DroppedFile,
5168 WindowEventFilter::HoveredFileCancelled,
5169 WindowEventFilter::TouchStart,
5170 WindowEventFilter::TouchEnd,
5171 WindowEventFilter::PenDown,
5172 WindowEventFilter::DragStart,
5173 WindowEventFilter::Drop,
5174 WindowEventFilter::DoubleClick,
5175 WindowEventFilter::PermissionChanged,
5176 WindowEventFilter::BiometricResult,
5177 WindowEventFilter::KeyringResult,
5178 ] {
5179 let hover = w
5180 .to_hover_event_filter()
5181 .unwrap_or_else(|| panic!("{w:?} should have a hover twin"));
5182 assert!(
5183 !hover.is_system_internal(),
5184 "{w:?} must not map onto an internal filter"
5185 );
5186 }
5187
5188 for w in [
5190 WindowEventFilter::MouseEnter,
5191 WindowEventFilter::MouseLeave,
5192 WindowEventFilter::Resized,
5193 WindowEventFilter::Moved,
5194 WindowEventFilter::FocusReceived,
5195 WindowEventFilter::FocusLost,
5196 WindowEventFilter::CloseRequested,
5197 WindowEventFilter::ThemeChanged,
5198 WindowEventFilter::WindowFocusReceived,
5199 WindowEventFilter::WindowFocusLost,
5200 WindowEventFilter::DpiChanged,
5201 WindowEventFilter::MonitorChanged,
5202 ] {
5203 assert_eq!(
5204 w.to_hover_event_filter(),
5205 None,
5206 "{w:?} is window-specific and must not map to a hover filter"
5207 );
5208 }
5209 }
5210
5211 #[test]
5212 fn window_hover_focus_filter_names_round_trip() {
5213 let pairs = [
5217 (
5218 WindowEventFilter::MouseOver,
5219 HoverEventFilter::MouseOver,
5220 Some(FocusEventFilter::MouseOver),
5221 ),
5222 (
5223 WindowEventFilter::LeftMouseDown,
5224 HoverEventFilter::LeftMouseDown,
5225 Some(FocusEventFilter::LeftMouseDown),
5226 ),
5227 (
5228 WindowEventFilter::RightMouseUp,
5229 HoverEventFilter::RightMouseUp,
5230 Some(FocusEventFilter::RightMouseUp),
5231 ),
5232 (
5233 WindowEventFilter::TextInput,
5234 HoverEventFilter::TextInput,
5235 Some(FocusEventFilter::TextInput),
5236 ),
5237 (
5238 WindowEventFilter::VirtualKeyDown,
5239 HoverEventFilter::VirtualKeyDown,
5240 Some(FocusEventFilter::VirtualKeyDown),
5241 ),
5242 (
5243 WindowEventFilter::DragStart,
5244 HoverEventFilter::DragStart,
5245 Some(FocusEventFilter::DragStart),
5246 ),
5247 (
5248 WindowEventFilter::Drop,
5249 HoverEventFilter::Drop,
5250 Some(FocusEventFilter::Drop),
5251 ),
5252 (
5254 WindowEventFilter::DroppedFile,
5255 HoverEventFilter::DroppedFile,
5256 None,
5257 ),
5258 (
5259 WindowEventFilter::TouchStart,
5260 HoverEventFilter::TouchStart,
5261 None,
5262 ),
5263 ];
5264 for (w, h, f) in pairs {
5265 assert_eq!(w.to_hover_event_filter(), Some(h), "window->hover for {w:?}");
5266 assert_eq!(h.to_focus_event_filter(), f, "hover->focus for {h:?}");
5267 }
5268 }
5269
5270 #[test]
5271 fn on_to_event_filter_conversion_is_stable() {
5272 use crate::dom::On;
5273 assert_eq!(
5276 EventFilter::from(On::TextInput),
5277 EventFilter::Focus(FocusEventFilter::TextInput)
5278 );
5279 assert_eq!(
5280 EventFilter::from(On::VirtualKeyDown),
5281 EventFilter::Window(WindowEventFilter::VirtualKeyDown)
5282 );
5283 assert_eq!(
5284 EventFilter::from(On::MouseOver),
5285 EventFilter::Hover(HoverEventFilter::MouseOver)
5286 );
5287 for on in [On::Default, On::Collapse, On::Expand, On::Increment, On::Decrement] {
5289 assert_eq!(
5290 EventFilter::from(on),
5291 EventFilter::Hover(HoverEventFilter::MouseUp),
5292 "{on:?} must map to the click filter"
5293 );
5294 }
5295 assert!(EventFilter::from(On::TextInput).is_focus_callback());
5296 assert!(EventFilter::from(On::VirtualKeyUp).is_window_callback());
5297 }
5298
5299 #[test]
5300 fn virtual_keycode_round_trips_for_every_key_events_rs_interprets() {
5301 for vk in [
5305 VirtualKeyCode::Left,
5306 VirtualKeyCode::Right,
5307 VirtualKeyCode::Up,
5308 VirtualKeyCode::Down,
5309 VirtualKeyCode::Home,
5310 VirtualKeyCode::End,
5311 VirtualKeyCode::Back,
5312 VirtualKeyCode::Delete,
5313 VirtualKeyCode::A,
5314 VirtualKeyCode::C,
5315 VirtualKeyCode::D,
5316 VirtualKeyCode::V,
5317 VirtualKeyCode::X,
5318 VirtualKeyCode::Y,
5319 VirtualKeyCode::Z,
5320 ] {
5321 assert_eq!(
5322 VirtualKeyCode::from_u32(vk as u32),
5323 Some(vk),
5324 "{vk:?} must survive the as-u32 / from_u32 round trip"
5325 );
5326 }
5327 assert_eq!(VirtualKeyCode::from_u32(u32::MAX), None);
5329 assert_eq!(VirtualKeyCode::from_u32(100_000), None);
5330 }
5331
5332 #[test]
5335 fn arrow_direction_from_key_is_total_over_every_decodable_key() {
5336 let nav = [
5339 VirtualKeyCode::Left,
5340 VirtualKeyCode::Right,
5341 VirtualKeyCode::Up,
5342 VirtualKeyCode::Down,
5343 VirtualKeyCode::Home,
5344 VirtualKeyCode::End,
5345 ];
5346 for raw in 0u32..1024 {
5347 let Some(vk) = VirtualKeyCode::from_u32(raw) else {
5348 continue;
5349 };
5350 for ctrl in [false, true] {
5351 let got = ArrowDirection::from_key(vk, ctrl);
5352 assert_eq!(
5353 got.is_some(),
5354 nav.contains(&vk),
5355 "{vk:?} (ctrl={ctrl}) must map to an ArrowDirection iff it is a nav key"
5356 );
5357 if let Some(dir) = got {
5358 let (_d, _s) = dir.to_selection(ctrl);
5360 }
5361 }
5362 }
5363 }
5364
5365 #[test]
5366 fn arrow_direction_ctrl_only_upgrades_horizontal_arrows_to_words() {
5367 for (dir, expect_no_ctrl, expect_ctrl) in [
5370 (
5371 ArrowDirection::Left,
5372 (SelectionDirection::Backward, SelectionStep::Character),
5373 (SelectionDirection::Backward, SelectionStep::Word),
5374 ),
5375 (
5376 ArrowDirection::Right,
5377 (SelectionDirection::Forward, SelectionStep::Character),
5378 (SelectionDirection::Forward, SelectionStep::Word),
5379 ),
5380 (
5381 ArrowDirection::Up,
5382 (SelectionDirection::Backward, SelectionStep::VisualLine),
5383 (SelectionDirection::Backward, SelectionStep::VisualLine),
5384 ),
5385 (
5386 ArrowDirection::Down,
5387 (SelectionDirection::Forward, SelectionStep::VisualLine),
5388 (SelectionDirection::Forward, SelectionStep::VisualLine),
5389 ),
5390 (
5391 ArrowDirection::LineStart,
5392 (SelectionDirection::Backward, SelectionStep::Line),
5393 (SelectionDirection::Backward, SelectionStep::Line),
5394 ),
5395 (
5396 ArrowDirection::DocumentEnd,
5397 (SelectionDirection::Forward, SelectionStep::Document),
5398 (SelectionDirection::Forward, SelectionStep::Document),
5399 ),
5400 ] {
5401 assert_eq!(dir.to_selection(false), expect_no_ctrl, "{dir:?} plain");
5402 assert_eq!(dir.to_selection(true), expect_ctrl, "{dir:?} + ctrl");
5403 }
5404 assert_eq!(
5406 ArrowDirection::from_key(VirtualKeyCode::Home, true),
5407 Some(ArrowDirection::DocumentStart)
5408 );
5409 assert_eq!(
5410 ArrowDirection::from_key(VirtualKeyCode::End, true),
5411 Some(ArrowDirection::DocumentEnd)
5412 );
5413 }
5414
5415 #[test]
5416 fn keyboard_shortcut_from_key_requires_primary_for_every_key() {
5417 for raw in 0u32..1024 {
5420 let Some(vk) = VirtualKeyCode::from_u32(raw) else {
5421 continue;
5422 };
5423 for shift in [false, true] {
5424 assert_eq!(
5425 KeyboardShortcut::from_key(vk, false, shift),
5426 None,
5427 "{vk:?} (shift={shift}) must need the primary modifier"
5428 );
5429 }
5430 }
5431 assert_eq!(
5433 KeyboardShortcut::from_key(VirtualKeyCode::Z, true, true),
5434 Some(KeyboardShortcut::Redo),
5435 "primary+shift+Z is Redo, not Undo"
5436 );
5437 assert_eq!(
5438 KeyboardShortcut::from_key(VirtualKeyCode::Y, true, true),
5439 Some(KeyboardShortcut::Redo),
5440 "shift must not disturb primary+Y"
5441 );
5442 assert_eq!(
5443 KeyboardShortcut::from_key(VirtualKeyCode::C, true, true),
5444 Some(KeyboardShortcut::Copy),
5445 "shift must not disturb primary+C"
5446 );
5447 assert_eq!(KeyboardShortcut::from_key(VirtualKeyCode::D, true, false), None);
5449 }
5450
5451 #[test]
5452 fn selection_op_new_defaults_to_a_single_repeat() {
5453 let op = SelectionOp::new(
5454 SelectionDirection::Forward,
5455 SelectionStep::Word,
5456 SelectionMode::Delete,
5457 );
5458 assert_eq!(op.direction, SelectionDirection::Forward);
5459 assert_eq!(op.step, SelectionStep::Word);
5460 assert_eq!(op.mode, SelectionMode::Delete);
5461 assert_eq!(op.repeat, 1, "a fresh op must apply exactly once");
5462 }
5463
5464 #[test]
5467 fn capture_phase_never_matches_any_filter() {
5468 let ev = mouse_event(EventType::MouseDown, MouseButton::Left, LogicalPosition::zero());
5471 for filter in [
5472 EventFilter::Hover(HoverEventFilter::MouseDown),
5473 EventFilter::Hover(HoverEventFilter::LeftMouseDown),
5474 EventFilter::Focus(FocusEventFilter::MouseDown),
5475 EventFilter::Window(WindowEventFilter::MouseDown),
5476 EventFilter::Component(ComponentEventFilter::AfterMount),
5477 EventFilter::Application(ApplicationEventFilter::DeviceConnected),
5478 ] {
5479 assert!(
5480 !matches_filter_phase(filter, &ev, EventPhase::Capture),
5481 "{filter:?} must not match in the capture phase"
5482 );
5483 }
5484 for phase in [EventPhase::Target, EventPhase::Bubble] {
5486 assert!(matches_filter_phase(
5487 EventFilter::Hover(HoverEventFilter::MouseDown),
5488 &ev,
5489 phase
5490 ));
5491 }
5492 }
5493
5494 #[test]
5495 fn application_filters_never_match_yet() {
5496 let ev = mouse_event(EventType::MouseDown, MouseButton::Left, LogicalPosition::zero());
5498 for phase in [EventPhase::Capture, EventPhase::Target, EventPhase::Bubble] {
5499 assert!(!matches_filter_phase(
5500 EventFilter::Application(ApplicationEventFilter::MonitorConnected),
5501 &ev,
5502 phase
5503 ));
5504 }
5505 }
5506
5507 #[test]
5508 fn check_mouse_button_is_false_for_every_non_mouse_payload() {
5509 for data in [
5510 EventData::None,
5511 EventData::Keyboard(KeyboardEventData {
5512 key_code: 0,
5513 char_code: None,
5514 modifiers: KeyModifiers::default(),
5515 repeat: false,
5516 }),
5517 EventData::Touch(TouchEventData {
5518 id: u64::MAX,
5519 position: LogicalPosition::zero(),
5520 force: f32::NAN,
5521 }),
5522 EventData::Clipboard(ClipboardEventData { content: None }),
5523 ] {
5524 for button in [MouseButton::Left, MouseButton::Right, MouseButton::Middle] {
5525 assert!(
5526 !check_mouse_button(&data, button),
5527 "non-mouse payload must never claim a button"
5528 );
5529 }
5530 }
5531 let other_max = EventData::Mouse(MouseEventData {
5533 position: LogicalPosition::zero(),
5534 button: MouseButton::Other(u8::MAX),
5535 buttons: u8::MAX,
5536 modifiers: KeyModifiers::default(),
5537 });
5538 assert!(check_mouse_button(&other_max, MouseButton::Other(u8::MAX)));
5539 assert!(!check_mouse_button(&other_max, MouseButton::Other(0)));
5540 assert!(!check_mouse_button(&other_max, MouseButton::Left));
5541 }
5542
5543 #[test]
5544 fn button_specific_filters_require_the_matching_button() {
5545 let left = mouse_event(EventType::MouseDown, MouseButton::Left, LogicalPosition::zero());
5546 let right = mouse_event(EventType::MouseDown, MouseButton::Right, LogicalPosition::zero());
5547 let middle = mouse_event(EventType::MouseDown, MouseButton::Middle, LogicalPosition::zero());
5548
5549 for ev in [&left, &right, &middle] {
5551 assert!(matches_hover_filter(
5552 HoverEventFilter::MouseDown,
5553 ev,
5554 EventPhase::Target
5555 ));
5556 }
5557 assert!(matches_hover_filter(HoverEventFilter::LeftMouseDown, &left, EventPhase::Target));
5559 assert!(!matches_hover_filter(HoverEventFilter::LeftMouseDown, &right, EventPhase::Target));
5560 assert!(matches_hover_filter(HoverEventFilter::RightMouseDown, &right, EventPhase::Target));
5561 assert!(!matches_hover_filter(HoverEventFilter::MiddleMouseDown, &right, EventPhase::Target));
5562 assert!(matches_hover_filter(HoverEventFilter::MiddleMouseDown, &middle, EventPhase::Target));
5563
5564 let up = mouse_event(EventType::MouseUp, MouseButton::Left, LogicalPosition::zero());
5566 assert!(!matches_hover_filter(HoverEventFilter::MouseDown, &up, EventPhase::Target));
5567 assert!(!matches_hover_filter(HoverEventFilter::MouseUp, &left, EventPhase::Target));
5568 assert!(matches_focus_filter(FocusEventFilter::LeftMouseUp, &up, EventPhase::Target));
5569 assert!(matches_window_filter(WindowEventFilter::LeftMouseUp, &up, EventPhase::Target));
5570
5571 let payloadless = SyntheticEvent::new(
5574 EventType::MouseDown,
5575 EventSource::Synthetic,
5576 dnid(0, 0),
5577 tick(0),
5578 EventData::None,
5579 );
5580 assert!(matches_hover_filter(HoverEventFilter::MouseDown, &payloadless, EventPhase::Target));
5581 assert!(!matches_hover_filter(
5582 HoverEventFilter::LeftMouseDown,
5583 &payloadless,
5584 EventPhase::Target
5585 ));
5586 }
5587
5588 #[test]
5589 fn component_filter_matches_only_its_own_lifecycle_event() {
5590 let lifecycle = |ty: EventType| {
5591 SyntheticEvent::new(ty, EventSource::Lifecycle, dnid(0, 0), tick(0), EventData::None)
5592 };
5593 let pairs = [
5594 (ComponentEventFilter::AfterMount, EventType::Mount),
5595 (ComponentEventFilter::BeforeUnmount, EventType::Unmount),
5596 (ComponentEventFilter::Updated, EventType::Update),
5597 (ComponentEventFilter::NodeResized, EventType::Resize),
5598 ];
5599 for (filter, ty) in pairs {
5600 let ev = lifecycle(ty);
5601 assert!(
5602 matches_component_filter(filter, &ev, EventPhase::Target),
5603 "{filter:?} must match {ty:?}"
5604 );
5605 for (_, other_ty) in pairs.iter().filter(|(_, t)| *t != ty) {
5607 assert!(
5608 !matches_component_filter(filter, &lifecycle(*other_ty), EventPhase::Target),
5609 "{filter:?} must not match {other_ty:?}"
5610 );
5611 }
5612 }
5613 for filter in [ComponentEventFilter::DefaultAction, ComponentEventFilter::Selected] {
5616 for (_, ty) in pairs {
5617 assert!(!matches_component_filter(filter, &lifecycle(ty), EventPhase::Target));
5618 }
5619 }
5620 }
5621
5622 #[test]
5623 fn event_type_to_filters_never_panics_and_stays_synced_with_the_hover_matcher() {
5624 const KNOWN_DESYNC: &[EventType] = &[
5633 EventType::Click, EventType::ContextMenu, EventType::MouseOut, EventType::ScrollStart, EventType::ScrollEnd, EventType::FocusIn, EventType::FocusOut, EventType::CompositionStart, EventType::CompositionUpdate, EventType::CompositionEnd, ];
5644
5645 let mouse_data = EventData::Mouse(MouseEventData {
5646 position: LogicalPosition::new(1.0, 1.0),
5647 button: MouseButton::Left,
5648 buttons: 1,
5649 modifiers: KeyModifiers::default(),
5650 });
5651
5652 let cases: Vec<(EventType, EventData)> = vec![
5653 (EventType::MouseOver, EventData::None),
5654 (EventType::MouseEnter, EventData::None),
5655 (EventType::MouseLeave, EventData::None),
5656 (EventType::MouseOut, EventData::None),
5657 (EventType::MouseDown, mouse_data.clone()),
5658 (EventType::MouseUp, mouse_data.clone()),
5659 (EventType::Click, mouse_data.clone()),
5660 (EventType::DoubleClick, mouse_data.clone()),
5661 (EventType::ContextMenu, mouse_data.clone()),
5662 (EventType::KeyDown, EventData::None),
5663 (EventType::KeyUp, EventData::None),
5664 (EventType::KeyPress, EventData::None),
5665 (EventType::CompositionStart, EventData::None),
5666 (EventType::CompositionUpdate, EventData::None),
5667 (EventType::CompositionEnd, EventData::None),
5668 (EventType::Focus, EventData::None),
5669 (EventType::Blur, EventData::None),
5670 (EventType::FocusIn, EventData::None),
5671 (EventType::FocusOut, EventData::None),
5672 (EventType::Input, EventData::None),
5673 (EventType::Change, EventData::None),
5674 (EventType::Scroll, EventData::None),
5675 (EventType::ScrollStart, EventData::None),
5676 (EventType::ScrollEnd, EventData::None),
5677 (EventType::DragStart, EventData::None),
5678 (EventType::Drag, EventData::None),
5679 (EventType::DragEnd, EventData::None),
5680 (EventType::DragEnter, EventData::None),
5681 (EventType::DragOver, EventData::None),
5682 (EventType::DragLeave, EventData::None),
5683 (EventType::Drop, EventData::None),
5684 (EventType::TouchStart, EventData::None),
5685 (EventType::TouchMove, EventData::None),
5686 (EventType::TouchEnd, EventData::None),
5687 (EventType::TouchCancel, EventData::None),
5688 (EventType::Mount, EventData::None),
5689 (EventType::Unmount, EventData::None),
5690 (EventType::Update, EventData::None),
5691 (EventType::Resize, EventData::None),
5692 (EventType::WindowResize, EventData::None),
5693 (EventType::WindowMove, EventData::None),
5694 (EventType::WindowClose, EventData::None),
5695 (EventType::ThemeChange, EventData::None),
5696 (EventType::FileHover, EventData::None),
5697 (EventType::FileDrop, EventData::None),
5698 (EventType::FileHoverCancel, EventData::None),
5699 (EventType::Copy, EventData::None),
5700 (EventType::Cut, EventData::None),
5701 (EventType::Paste, EventData::None),
5702 (EventType::SensorChanged, EventData::None),
5703 (EventType::GamepadInput, EventData::None),
5704 (EventType::GeolocationFix, EventData::None),
5705 (EventType::GeolocationError, EventData::None),
5706 (EventType::PermissionChanged, EventData::None),
5707 (EventType::BiometricResult, EventData::None),
5708 (EventType::KeyringResult, EventData::None),
5709 (EventType::LongPress, EventData::None),
5710 (EventType::Play, EventData::None),
5711 ];
5712
5713 for (ty, data) in cases {
5714 let filters = event_type_to_filters(ty, &data);
5715 let ev = SyntheticEvent::new(ty, EventSource::User, dnid(0, 0), tick(0), data);
5716
5717 let mut seen = BTreeSet::new();
5719 for f in &filters {
5720 assert!(seen.insert(*f), "{ty:?} emitted {f:?} twice");
5721 }
5722
5723 for f in &filters {
5724 if !matches!(f, EventFilter::Hover(_)) {
5725 continue; }
5727 if matches_filter_phase(*f, &ev, EventPhase::Target) {
5728 continue;
5729 }
5730 assert!(
5731 KNOWN_DESYNC.contains(&ty),
5732 "NEW DESYNC: event_type_to_filters({ty:?}) emits {f:?}, but \
5733 matches_filter_phase rejects it at the Target phase, so the \
5734 callback would be collected and then silently dropped"
5735 );
5736 }
5737 }
5738 }
5739
5740 #[test]
5741 fn event_type_to_filters_omits_button_specific_filter_for_exotic_buttons() {
5742 let data = EventData::Mouse(MouseEventData {
5744 position: LogicalPosition::zero(),
5745 button: MouseButton::Other(u8::MAX),
5746 buttons: 0,
5747 modifiers: KeyModifiers::default(),
5748 });
5749 let down = event_type_to_filters(EventType::MouseDown, &data);
5750 assert_eq!(down, vec![EventFilter::Hover(HoverEventFilter::MouseDown)]);
5751 let up = event_type_to_filters(EventType::MouseUp, &data);
5752 assert_eq!(up, vec![EventFilter::Hover(HoverEventFilter::MouseUp)]);
5753
5754 for ty in [
5756 EventType::Submit,
5757 EventType::Reset,
5758 EventType::Invalid,
5759 EventType::Play,
5760 EventType::Pause,
5761 EventType::Ended,
5762 EventType::TimeUpdate,
5763 EventType::VolumeChange,
5764 EventType::MediaError,
5765 EventType::PinchIn,
5766 EventType::RotateClockwise,
5767 EventType::SwipeLeft,
5768 ] {
5769 assert!(
5770 event_type_to_filters(ty, &EventData::None).is_empty(),
5771 "{ty:?} is unmapped and must yield no filters"
5772 );
5773 }
5774 }
5775
5776 #[test]
5779 fn get_dom_path_none_target_yields_empty_path() {
5780 let hier = hierarchy_chain(3);
5781 assert!(get_dom_path(&hier, NodeHierarchyItemId::NONE).is_empty());
5782 let empty = NodeHierarchy::new(Vec::new());
5784 let path = get_dom_path(&empty, NodeHierarchyItemId::from_crate_internal(Some(NodeId::ZERO)));
5785 assert_eq!(path, vec![NodeId::ZERO], "unknown nodes still path to themselves");
5786 }
5787
5788 #[test]
5789 fn get_dom_path_out_of_range_target_does_not_panic() {
5790 let hier = hierarchy_chain(3);
5791 let huge = NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(usize::MAX - 1)));
5792 let path = get_dom_path(&hier, huge);
5793 assert_eq!(path, vec![NodeId::new(usize::MAX - 1)]);
5794 }
5795
5796 #[test]
5797 fn get_dom_path_returns_root_to_target_order() {
5798 let hier = hierarchy_chain(4); let path = get_dom_path(&hier, NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(3))));
5800 assert_eq!(
5801 path,
5802 vec![NodeId::new(0), NodeId::new(1), NodeId::new(2), NodeId::new(3)],
5803 "path must run root -> target"
5804 );
5805 let root_path = get_dom_path(&hier, NodeHierarchyItemId::from_crate_internal(Some(NodeId::ZERO)));
5807 assert_eq!(root_path, vec![NodeId::ZERO]);
5808 }
5809
5810 #[test]
5811 fn get_dom_path_terminates_on_a_self_parent_cycle() {
5812 let hier = NodeHierarchy::new(vec![Node {
5814 parent: Some(NodeId::ZERO),
5815 ..Node::ROOT
5816 }]);
5817 let path = get_dom_path(&hier, NodeHierarchyItemId::from_crate_internal(Some(NodeId::ZERO)));
5818 assert_eq!(path, vec![NodeId::ZERO]);
5819 }
5820
5821 #[test]
5822 fn get_dom_path_handles_a_deep_chain_without_recursing() {
5823 let hier = hierarchy_chain(5000);
5826 let path = get_dom_path(
5827 &hier,
5828 NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(4999))),
5829 );
5830 assert_eq!(path.len(), 5000);
5831 assert_eq!(path[0], NodeId::ZERO);
5832 assert_eq!(path[4999], NodeId::new(4999));
5833 }
5834
5835 #[test]
5836 fn propagate_event_visits_each_node_exactly_once() {
5837 let hier = hierarchy_chain(3); let mut callbacks: BTreeMap<NodeId, Vec<EventFilter>> = BTreeMap::new();
5841 for i in 0..3 {
5842 callbacks.insert(
5843 NodeId::new(i),
5844 vec![EventFilter::Hover(HoverEventFilter::MouseDown)],
5845 );
5846 }
5847 let mut ev = SyntheticEvent::new(
5848 EventType::MouseDown,
5849 EventSource::User,
5850 dnid(0, 2),
5851 tick(0),
5852 EventData::Mouse(MouseEventData {
5853 position: LogicalPosition::zero(),
5854 button: MouseButton::Left,
5855 buttons: 1,
5856 modifiers: KeyModifiers::default(),
5857 }),
5858 );
5859
5860 let result = propagate_event(&mut ev, &hier, &callbacks);
5861 let nodes: Vec<NodeId> = result.callbacks_to_invoke.iter().map(|(n, _)| *n).collect();
5862 assert_eq!(
5863 nodes,
5864 vec![NodeId::new(2), NodeId::new(1), NodeId::new(0)],
5865 "target first, then bubbling up to the root — each node once"
5866 );
5867 assert!(!result.default_prevented);
5868 }
5869
5870 #[test]
5871 fn propagate_event_on_a_dangling_target_is_a_no_op() {
5872 let hier = hierarchy_chain(2);
5873 let callbacks: BTreeMap<NodeId, Vec<EventFilter>> = BTreeMap::new();
5874
5875 let mut ev = SyntheticEvent::new(
5878 EventType::MouseDown,
5879 EventSource::User,
5880 dnid_none(0),
5881 tick(0),
5882 EventData::None,
5883 );
5884 let result = propagate_event(&mut ev, &hier, &callbacks);
5885 assert!(result.callbacks_to_invoke.is_empty());
5886 assert!(!result.default_prevented);
5887
5888 let mut ev = SyntheticEvent::new(
5890 EventType::MouseDown,
5891 EventSource::User,
5892 dnid(0, 10_000),
5893 tick(0),
5894 EventData::None,
5895 );
5896 let result = propagate_event(&mut ev, &hier, &callbacks);
5897 assert!(result.callbacks_to_invoke.is_empty());
5898 }
5899
5900 #[test]
5901 fn propagate_event_respects_a_pre_stopped_event() {
5902 let hier = hierarchy_chain(3);
5903 let mut callbacks: BTreeMap<NodeId, Vec<EventFilter>> = BTreeMap::new();
5904 for i in 0..3 {
5905 callbacks.insert(
5906 NodeId::new(i),
5907 vec![EventFilter::Hover(HoverEventFilter::MouseOver)],
5908 );
5909 }
5910 let base = SyntheticEvent::new(
5911 EventType::MouseOver,
5912 EventSource::User,
5913 dnid(0, 2),
5914 tick(0),
5915 EventData::None,
5916 );
5917
5918 let mut stopped = base.clone();
5920 stopped.stop_propagation();
5921 let r = propagate_event(&mut stopped, &hier, &callbacks);
5922 assert!(r.callbacks_to_invoke.is_empty(), "a stopped event collects nothing");
5923
5924 let mut immediate = base.clone();
5926 immediate.stop_immediate_propagation();
5927 let r = propagate_event(&mut immediate, &hier, &callbacks);
5928 assert!(r.callbacks_to_invoke.is_empty());
5929
5930 let mut prevented = base;
5932 prevented.prevent_default();
5933 let r = propagate_event(&mut prevented, &hier, &callbacks);
5934 assert!(r.default_prevented);
5935 assert_eq!(r.callbacks_to_invoke.len(), 3, "preventDefault must not stop dispatch");
5936 }
5937
5938 #[test]
5939 fn propagate_event_ignores_filters_that_do_not_match_the_event() {
5940 let hier = hierarchy_chain(2);
5941 let mut callbacks: BTreeMap<NodeId, Vec<EventFilter>> = BTreeMap::new();
5942 callbacks.insert(
5943 NodeId::new(1),
5944 vec![
5945 EventFilter::Hover(HoverEventFilter::MouseUp), EventFilter::Hover(HoverEventFilter::RightMouseDown), EventFilter::Hover(HoverEventFilter::LeftMouseDown), ],
5949 );
5950 let mut ev = mouse_event(EventType::MouseDown, MouseButton::Left, LogicalPosition::zero());
5951 ev.target = dnid(0, 1);
5952 ev.current_target = ev.target;
5953
5954 let r = propagate_event(&mut ev, &hier, &callbacks);
5955 assert_eq!(
5956 r.callbacks_to_invoke,
5957 vec![(
5958 NodeId::new(1),
5959 EventFilter::Hover(HoverEventFilter::LeftMouseDown)
5960 )]
5961 );
5962 assert_eq!(ev.phase, EventPhase::Bubble);
5967 assert_eq!(ev.current_target, dnid(0, 0));
5968 assert_eq!(ev.target, dnid(0, 1), "the target itself must never be rewritten");
5969 }
5970
5971 #[test]
5972 fn collect_matching_callbacks_collects_nothing_once_immediate_stop_is_set() {
5973 let mut result = PropagationResult::default();
5974 let mut callbacks: BTreeMap<NodeId, Vec<EventFilter>> = BTreeMap::new();
5975 callbacks.insert(
5976 NodeId::ZERO,
5977 vec![EventFilter::Hover(HoverEventFilter::MouseOver)],
5978 );
5979 let mut ev = SyntheticEvent::new(
5980 EventType::MouseOver,
5981 EventSource::User,
5982 dnid(0, 0),
5983 tick(0),
5984 EventData::None,
5985 );
5986 ev.stop_immediate_propagation();
5987 collect_matching_callbacks(&ev, NodeId::ZERO, EventPhase::Target, &callbacks, &mut result);
5988 assert!(result.callbacks_to_invoke.is_empty());
5989
5990 let mut fresh = PropagationResult::default();
5992 let clean = SyntheticEvent::new(
5993 EventType::MouseOver,
5994 EventSource::User,
5995 dnid(0, 0),
5996 tick(0),
5997 EventData::None,
5998 );
5999 collect_matching_callbacks(&clean, NodeId::new(9), EventPhase::Target, &callbacks, &mut fresh);
6000 assert!(fresh.callbacks_to_invoke.is_empty());
6001 }
6002
6003 #[test]
6004 fn propagate_phase_over_an_empty_iterator_only_sets_the_phase() {
6005 let mut result = PropagationResult::default();
6006 let callbacks: BTreeMap<NodeId, Vec<EventFilter>> = BTreeMap::new();
6007 let mut ev = SyntheticEvent::new(
6008 EventType::MouseOver,
6009 EventSource::User,
6010 dnid(0, 0),
6011 tick(0),
6012 EventData::None,
6013 );
6014 propagate_phase(
6015 &mut ev,
6016 core::iter::empty(),
6017 EventPhase::Bubble,
6018 &callbacks,
6019 &mut result,
6020 );
6021 assert_eq!(ev.phase, EventPhase::Bubble);
6022 assert!(result.callbacks_to_invoke.is_empty());
6023
6024 propagate_target_phase(&mut ev, NodeId::ZERO, &callbacks, &mut result);
6026 assert_eq!(ev.phase, EventPhase::Target);
6027 assert_eq!(ev.current_target, ev.target);
6028 }
6029
6030 #[test]
6033 fn deduplicate_synthetic_events_handles_empty_and_single() {
6034 assert!(deduplicate_synthetic_events(Vec::new()).is_empty());
6035 let one = vec![SyntheticEvent::new(
6036 EventType::Scroll,
6037 EventSource::User,
6038 dnid(0, 0),
6039 tick(1),
6040 EventData::None,
6041 )];
6042 assert_eq!(deduplicate_synthetic_events(one).len(), 1);
6043 }
6044
6045 #[test]
6046 fn deduplicate_synthetic_events_keeps_the_latest_timestamp_per_target_and_type() {
6047 let mk = |node: usize, ty: EventType, t: u64| {
6048 SyntheticEvent::new(ty, EventSource::User, dnid(0, node), tick(t), EventData::None)
6049 };
6050 let events = vec![
6052 mk(1, EventType::Scroll, 5),
6053 mk(1, EventType::Scroll, 99),
6054 mk(1, EventType::Scroll, 1),
6055 ];
6056 let out = deduplicate_synthetic_events(events);
6057 assert_eq!(out.len(), 1);
6058 assert_eq!(out[0].timestamp, tick(99), "the newest event must survive");
6059
6060 let events = vec![
6062 mk(1, EventType::Scroll, 1),
6063 mk(2, EventType::Scroll, 1),
6064 mk(1, EventType::MouseOver, 1),
6065 ];
6066 assert_eq!(deduplicate_synthetic_events(events).len(), 3);
6067
6068 let a = SyntheticEvent::new(EventType::Scroll, EventSource::User, dnid(0, 1), tick(0), EventData::None);
6070 let b = SyntheticEvent::new(EventType::Scroll, EventSource::User, dnid(1, 1), tick(0), EventData::None);
6071 assert_eq!(deduplicate_synthetic_events(vec![a, b]).len(), 2);
6072 }
6073
6074 #[test]
6075 fn deduplicate_synthetic_events_collapses_a_large_duplicate_burst() {
6076 let events: Vec<SyntheticEvent> = (0..10_000u64)
6079 .map(|t| {
6080 SyntheticEvent::new(
6081 EventType::Scroll,
6082 EventSource::User,
6083 dnid(0, 0),
6084 tick(t),
6085 EventData::None,
6086 )
6087 })
6088 .collect();
6089 let out = deduplicate_synthetic_events(events);
6090 assert_eq!(out.len(), 1);
6091 assert_eq!(out[0].timestamp, tick(9_999));
6092 }
6093
6094 #[test]
6095 fn deduplicate_synthetic_events_preserves_unicode_payloads() {
6096 let text = "🦀 グラフ é\u{0301} مرحبا \u{1F1E6}\u{1F1F9}".repeat(200);
6099 let ev = SyntheticEvent::new(
6100 EventType::Input,
6101 EventSource::User,
6102 dnid(0, 0),
6103 tick(1),
6104 EventData::TextInput(TextInputEventData {
6105 inserted_text: text.clone(),
6106 old_text: String::new(),
6107 }),
6108 );
6109 let newer = SyntheticEvent::new(
6110 EventType::Input,
6111 EventSource::User,
6112 dnid(0, 0),
6113 tick(2),
6114 EventData::TextInput(TextInputEventData {
6115 inserted_text: text.clone(),
6116 old_text: text.clone(),
6117 }),
6118 );
6119 let out = deduplicate_synthetic_events(vec![ev, newer]);
6120 assert_eq!(out.len(), 1);
6121 match &out[0].data {
6122 EventData::TextInput(d) => {
6123 assert_eq!(d.inserted_text, text);
6124 assert_eq!(d.old_text, text, "the newer event won");
6125 }
6126 _ => panic!("payload must be preserved"),
6127 }
6128 }
6129
6130 #[test]
6133 fn get_first_hovered_node_on_empty_input() {
6134 assert!(get_first_hovered_node(None).is_none());
6135 assert!(
6136 get_first_hovered_node(Some(&empty_hit_test())).is_none(),
6137 "a hit test with no hovered DOMs has no front-most node"
6138 );
6139 let ht = hit_test_with(0, &[]);
6141 assert!(get_first_hovered_node(Some(&ht)).is_none());
6142 }
6143
6144 #[test]
6145 fn get_first_hovered_node_picks_minimum_depth_and_breaks_ties_deterministically() {
6146 let ht = hit_test_with(0, &[(2, 5), (5, 0), (9, 3)]);
6149 let got = get_first_hovered_node(Some(&ht)).unwrap();
6150 assert_eq!(got.node.into_crate_internal(), Some(NodeId::new(5)));
6151
6152 let ht = hit_test_with(0, &[(7, 2), (3, 2), (11, 2)]);
6155 let a = get_first_hovered_node(Some(&ht)).unwrap();
6156 let b = get_first_hovered_node(Some(&ht)).unwrap();
6157 assert_eq!(a, b, "tie-breaking must be deterministic");
6158 assert_eq!(a.node.into_crate_internal(), Some(NodeId::new(3)));
6159
6160 let ht = hit_test_with(0, &[(1, u32::MAX)]);
6162 let got = get_first_hovered_node(Some(&ht)).unwrap();
6163 assert_eq!(got.node.into_crate_internal(), Some(NodeId::new(1)));
6164 assert_eq!(got.dom, DomId { inner: 0 });
6165 }
6166
6167 #[test]
6168 fn get_mouse_position_with_fallback_prefers_the_event_payload() {
6169 let mouse = MouseState {
6170 cursor_position: CursorPosition::InWindow(LogicalPosition::new(9.0, 9.0)),
6171 ..MouseState::default()
6172 };
6173 let ev = mouse_event(
6174 EventType::MouseDown,
6175 MouseButton::Left,
6176 LogicalPosition::new(1.0, 2.0),
6177 );
6178 assert_eq!(
6179 get_mouse_position_with_fallback(&ev, &mouse),
6180 LogicalPosition::new(1.0, 2.0),
6181 "the event's own payload wins over the live cursor"
6182 );
6183
6184 let keyless = SyntheticEvent::new(
6186 EventType::MouseDown,
6187 EventSource::Synthetic,
6188 dnid(0, 0),
6189 tick(0),
6190 EventData::None,
6191 );
6192 assert_eq!(
6193 get_mouse_position_with_fallback(&keyless, &mouse),
6194 LogicalPosition::new(9.0, 9.0)
6195 );
6196
6197 for cursor in [
6200 CursorPosition::Uninitialized,
6201 CursorPosition::OutOfWindow(LogicalPosition::new(-5.0, -5.0)),
6202 ] {
6203 let ms = MouseState { cursor_position: cursor, ..MouseState::default() };
6204 assert_eq!(
6205 get_mouse_position_with_fallback(&keyless, &ms),
6206 LogicalPosition::zero()
6207 );
6208 }
6209 }
6210
6211 #[test]
6212 fn get_mouse_position_with_fallback_passes_through_extreme_coordinates() {
6213 let mouse = MouseState::default();
6214 for pos in [
6215 LogicalPosition::new(f32::NAN, f32::NAN),
6216 LogicalPosition::new(f32::INFINITY, f32::NEG_INFINITY),
6217 LogicalPosition::new(f32::MAX, f32::MIN),
6218 LogicalPosition::new(-0.0, 0.0),
6219 ] {
6220 let ev = mouse_event(EventType::MouseDown, MouseButton::Left, pos);
6221 let got = get_mouse_position_with_fallback(&ev, &mouse);
6222 assert_eq!(got.x.to_bits(), pos.x.to_bits());
6225 assert_eq!(got.y.to_bits(), pos.y.to_bits());
6226 }
6227 }
6228
6229 #[test]
6232 fn handle_mouse_down_treats_zero_click_count_as_one() {
6233 let ht = hit_test_with(0, &[(0, 0)]);
6234 let mouse = MouseState::default();
6235 let kb = KeyboardState::default();
6236 let ev = mouse_event(
6237 EventType::MouseDown,
6238 MouseButton::Left,
6239 LogicalPosition::new(4.0, 5.0),
6240 );
6241
6242 let action = handle_mouse_down(&ev, Some(&ht), 0, &mouse, &kb)
6244 .expect("click_count 0 must be treated as a single click");
6245 match action {
6246 InternalEventAction::AddAndPass(SystemChange::TextSelectionClick { position, .. }) => {
6247 assert_eq!(position, LogicalPosition::new(4.0, 5.0));
6248 }
6249 _ => panic!("expected a passed-through TextSelectionClick"),
6250 }
6251 }
6252
6253 #[test]
6254 fn handle_mouse_down_saturates_above_a_triple_click() {
6255 let ht = hit_test_with(0, &[(0, 0)]);
6256 let mouse = MouseState::default();
6257 let kb = KeyboardState::default();
6258 let ev = mouse_event(EventType::MouseDown, MouseButton::Left, LogicalPosition::zero());
6259
6260 for count in 1u8..=3 {
6262 assert!(
6263 handle_mouse_down(&ev, Some(&ht), count, &mouse, &kb).is_some(),
6264 "click_count {count} must produce a selection click"
6265 );
6266 }
6267 for count in [4u8, 5, 100, u8::MAX] {
6269 assert!(
6270 handle_mouse_down(&ev, Some(&ht), count, &mouse, &kb).is_none(),
6271 "click_count {count} must be ignored"
6272 );
6273 }
6274 }
6275
6276 #[test]
6277 fn handle_mouse_down_without_a_hit_test_is_a_no_op() {
6278 let mouse = MouseState::default();
6279 let kb = KeyboardState::default();
6280 let ev = mouse_event(EventType::MouseDown, MouseButton::Left, LogicalPosition::zero());
6281 assert!(handle_mouse_down(&ev, None, 1, &mouse, &kb).is_none());
6282 assert!(handle_mouse_down(&ev, Some(&empty_hit_test()), 1, &mouse, &kb).is_none());
6283 }
6284
6285 #[test]
6286 fn handle_mouse_down_with_primary_held_adds_a_cursor_only_on_a_single_click() {
6287 let ht = hit_test_with(0, &[(0, 0)]);
6288 let mouse = MouseState::default();
6289 let kb = keyboard_with_primary_held();
6290 let ev = mouse_event(
6291 EventType::MouseDown,
6292 MouseButton::Left,
6293 LogicalPosition::new(7.0, 8.0),
6294 );
6295
6296 match handle_mouse_down(&ev, Some(&ht), 1, &mouse, &kb) {
6298 Some(InternalEventAction::AddAndPass(SystemChange::AddCursorAtClick { position })) => {
6299 assert_eq!(position, LogicalPosition::new(7.0, 8.0));
6300 }
6301 _ => panic!("primary+click must add a cursor at the click position"),
6302 }
6303 match handle_mouse_down(&ev, Some(&ht), 2, &mouse, &kb) {
6305 Some(InternalEventAction::AddAndPass(SystemChange::TextSelectionClick { .. })) => {}
6306 _ => panic!("primary+double-click must not add a cursor"),
6307 }
6308 }
6309
6310 #[test]
6311 fn handle_mouse_over_requires_a_held_button_and_a_drag_origin() {
6312 let ht = hit_test_with(0, &[(0, 0)]);
6313 let start = LogicalPosition::new(1.0, 1.0);
6314 let ev = mouse_event(
6315 EventType::MouseOver,
6316 MouseButton::Left,
6317 LogicalPosition::new(50.0, 60.0),
6318 );
6319
6320 let up = MouseState::default();
6322 assert!(handle_mouse_over(&ev, Some(&ht), &up, Some(start)).is_none());
6323
6324 let down = MouseState { left_down: true, ..MouseState::default() };
6326 assert!(handle_mouse_over(&ev, Some(&ht), &down, None).is_none());
6327
6328 assert!(handle_mouse_over(&ev, None, &down, Some(start)).is_none());
6330 assert!(handle_mouse_over(&ev, Some(&empty_hit_test()), &down, Some(start)).is_none());
6331
6332 match handle_mouse_over(&ev, Some(&ht), &down, Some(start)) {
6334 Some(InternalEventAction::AddAndPass(SystemChange::TextSelectionDrag {
6335 start_position,
6336 current_position,
6337 })) => {
6338 assert_eq!(start_position, start);
6339 assert_eq!(current_position, LogicalPosition::new(50.0, 60.0));
6340 }
6341 _ => panic!("expected a TextSelectionDrag"),
6342 }
6343 }
6344
6345 #[test]
6346 fn handle_key_down_needs_a_focused_node_and_a_keyboard_payload() {
6347 let kb = KeyboardState::default();
6348 let ev = key_event(VirtualKeyCode::Back as u32, KeyModifiers::default());
6349 assert!(
6350 handle_key_down(&ev, &kb, None).is_none(),
6351 "no focus => no keyboard system change"
6352 );
6353
6354 let payloadless = SyntheticEvent::new(
6356 EventType::KeyDown,
6357 EventSource::User,
6358 dnid(0, 1),
6359 tick(0),
6360 EventData::None,
6361 );
6362 assert!(handle_key_down(&payloadless, &kb, Some(dnid(0, 1))).is_none());
6363 }
6364
6365 #[test]
6366 fn handle_key_down_rejects_undecodable_key_codes() {
6367 let kb = KeyboardState::default();
6368 let target = Some(dnid(0, 1));
6369 for code in [u32::MAX, u32::MAX - 1, 100_000, 9_999] {
6372 let ev = key_event(code, KeyModifiers::default());
6373 assert!(
6374 handle_key_down(&ev, &kb, target).is_none(),
6375 "key_code {code} must decode to None"
6376 );
6377 }
6378 }
6379
6380 #[test]
6381 fn handle_key_down_reads_modifiers_from_the_event_not_the_live_keyboard() {
6382 let kb = KeyboardState::default();
6386 let target = dnid(0, 1);
6387 let ev = key_event(VirtualKeyCode::C as u32, primary_modifiers());
6388 match handle_key_down(&ev, &kb, Some(target)) {
6389 Some(InternalEventAction::AddAndSkip(SystemChange::CopyToClipboard)) => {}
6390 _ => panic!("primary+C in the payload must copy, regardless of the live state"),
6391 }
6392
6393 let live = keyboard_with_primary_held();
6395 let plain = key_event(VirtualKeyCode::C as u32, KeyModifiers::default());
6396 assert!(
6397 handle_key_down(&plain, &live, Some(target)).is_none(),
6398 "an unmodified C is plain text input, not a copy"
6399 );
6400 }
6401
6402 #[test]
6403 fn handle_key_down_maps_backspace_and_delete_to_selection_ops() {
6404 let kb = KeyboardState::default();
6405 let target = dnid(0, 1);
6406
6407 let expect_op = |ev: &SyntheticEvent| -> SelectionOp {
6408 match handle_key_down(ev, &kb, Some(target)) {
6409 Some(InternalEventAction::AddAndSkip(SystemChange::ApplySelectionOp {
6410 target: t,
6411 op,
6412 })) => {
6413 assert_eq!(t, target);
6414 op
6415 }
6416 _ => panic!("expected an ApplySelectionOp"),
6417 }
6418 };
6419
6420 let back = expect_op(&key_event(VirtualKeyCode::Back as u32, KeyModifiers::default()));
6421 assert_eq!(back.direction, SelectionDirection::Backward);
6422 assert_eq!(back.step, SelectionStep::Character);
6423 assert_eq!(back.mode, SelectionMode::Delete);
6424
6425 let del = expect_op(&key_event(VirtualKeyCode::Delete as u32, KeyModifiers::default()));
6426 assert_eq!(del.direction, SelectionDirection::Forward);
6427 assert_eq!(del.step, SelectionStep::Character);
6428 assert_eq!(del.mode, SelectionMode::Delete);
6429
6430 let shift_right = expect_op(&key_event(
6432 VirtualKeyCode::Right as u32,
6433 KeyModifiers::new().with_shift(),
6434 ));
6435 assert_eq!(shift_right.mode, SelectionMode::Extend);
6436 assert_eq!(shift_right.step, SelectionStep::Character);
6437
6438 let word_mod = if cfg!(target_os = "macos") {
6440 KeyModifiers::new().with_alt()
6441 } else {
6442 KeyModifiers::new().with_ctrl()
6443 };
6444 let word_back = expect_op(&key_event(VirtualKeyCode::Back as u32, word_mod));
6445 assert_eq!(word_back.step, SelectionStep::Word);
6446 assert_eq!(word_back.mode, SelectionMode::Delete);
6447 }
6448
6449 #[test]
6450 fn handle_key_down_ignores_keys_it_does_not_interpret() {
6451 let kb = KeyboardState::default();
6452 let target = Some(dnid(0, 1));
6453 for vk in [VirtualKeyCode::B, VirtualKeyCode::Q, VirtualKeyCode::Space, VirtualKeyCode::F5] {
6455 let ev = key_event(vk as u32, KeyModifiers::default());
6456 assert!(
6457 handle_key_down(&ev, &kb, target).is_none(),
6458 "{vk:?} must not generate a system change"
6459 );
6460 }
6461 }
6462
6463 #[test]
6466 fn default_input_interpreter_with_no_events_produces_nothing() {
6467 let kb = KeyboardState::default();
6468 let mouse = MouseState::default();
6469 let info = InputInterpreterInfo {
6470 events: &[],
6471 hit_test: None,
6472 keyboard_state: &kb,
6473 mouse_state: &mouse,
6474 state: InputInterpreterState {
6475 focused_node: None,
6476 click_count: 0,
6477 drag_start_position: None,
6478 has_selection: false,
6479 },
6480 };
6481 let r = default_input_interpreter(&info);
6482 assert!(r.system_changes.is_empty());
6483 assert!(r.user_events.is_empty());
6484 }
6485
6486 #[test]
6487 fn default_input_interpreter_skips_shortcut_events_but_passes_clicks_through() {
6488 let kb = KeyboardState::default();
6489 let mouse = MouseState::default();
6490 let ht = hit_test_with(0, &[(0, 0)]);
6491 let target = dnid(0, 1);
6492
6493 let copy = key_event(VirtualKeyCode::C as u32, primary_modifiers());
6496 let click = mouse_event(EventType::MouseDown, MouseButton::Left, LogicalPosition::zero());
6498 let scroll = SyntheticEvent::new(
6500 EventType::Scroll,
6501 EventSource::User,
6502 target,
6503 tick(0),
6504 EventData::None,
6505 );
6506
6507 let events = vec![copy, click, scroll];
6508 let info = InputInterpreterInfo {
6509 events: &events,
6510 hit_test: Some(&ht),
6511 keyboard_state: &kb,
6512 mouse_state: &mouse,
6513 state: InputInterpreterState {
6514 focused_node: Some(target),
6515 click_count: 1,
6516 drag_start_position: None,
6517 has_selection: false,
6518 },
6519 };
6520 let r = default_input_interpreter(&info);
6521
6522 assert_eq!(r.system_changes.len(), 2, "copy + selection click");
6523 assert!(r.system_changes.contains(&SystemChange::CopyToClipboard));
6524 assert!(r
6525 .system_changes
6526 .iter()
6527 .any(|c| matches!(c, SystemChange::TextSelectionClick { .. })));
6528
6529 assert_eq!(r.user_events.len(), 2, "the consumed KeyDown must not be forwarded");
6530 assert!(!r.user_events.iter().any(|e| e.event_type == EventType::KeyDown));
6531 assert!(r.user_events.iter().any(|e| e.event_type == EventType::MouseDown));
6532 assert!(r.user_events.iter().any(|e| e.event_type == EventType::Scroll));
6533 }
6534
6535 #[test]
6536 fn default_input_interpreter_extern_survives_a_null_info_pointer() {
6537 let user_data = crate::refany::RefAny::new(0u8);
6539 let r = default_input_interpreter_extern(user_data, core::ptr::null());
6540 assert!(r.system_changes.is_empty());
6541 assert!(r.user_events.is_empty());
6542 }
6543
6544 #[test]
6547 fn post_filter_with_prevent_default_only_lets_focus_changes_through() {
6548 let old = Some(dnid(0, 1));
6549 let new = Some(dnid(0, 2));
6550 let pre = vec![
6551 SystemChange::TextSelectionClick {
6552 position: LogicalPosition::zero(),
6553 timestamp: tick(0),
6554 },
6555 SystemChange::PasteFromClipboard,
6556 ];
6557
6558 let out = default_post_filter(true, &pre, old, old);
6561 assert!(out.is_empty(), "preventDefault must suppress every side effect");
6562
6563 let out = default_post_filter(true, &pre, old, new);
6565 assert_eq!(out, vec![SystemChange::SetFocus { new_focus: new, old_focus: old }]);
6566 }
6567
6568 #[test]
6569 fn post_filter_maps_pre_changes_to_their_follow_ups() {
6570 let out = default_post_filter(false, &[], None, None);
6572 assert_eq!(out, vec![SystemChange::ApplyPendingTextInput]);
6573
6574 for change in [
6576 SystemChange::TextSelectionClick {
6577 position: LogicalPosition::zero(),
6578 timestamp: tick(0),
6579 },
6580 SystemChange::ApplySelectionOp {
6581 target: dnid(0, 1),
6582 op: SelectionOp::new(
6583 SelectionDirection::Forward,
6584 SelectionStep::Character,
6585 SelectionMode::Move,
6586 ),
6587 },
6588 SystemChange::AddCursorAtClick { position: LogicalPosition::zero() },
6589 SystemChange::SelectNextOccurrence { target: dnid(0, 1) },
6590 SystemChange::CutToClipboard { target: dnid(0, 1) },
6591 SystemChange::PasteFromClipboard,
6592 SystemChange::UndoTextEdit { target: dnid(0, 1) },
6593 SystemChange::RedoTextEdit { target: dnid(0, 1) },
6594 SystemChange::SelectAllText,
6595 ] {
6596 let out = default_post_filter(false, core::slice::from_ref(&change), None, None);
6597 assert!(
6598 out.contains(&SystemChange::ScrollSelectionIntoView),
6599 "{change:?} must schedule a scroll-into-view"
6600 );
6601 assert_eq!(out[0], SystemChange::ApplyPendingTextInput);
6602 }
6603
6604 let drag = SystemChange::TextSelectionDrag {
6606 start_position: LogicalPosition::zero(),
6607 current_position: LogicalPosition::new(1.0, 1.0),
6608 };
6609 let out = default_post_filter(false, core::slice::from_ref(&drag), None, None);
6610 assert!(out.contains(&SystemChange::StartAutoScrollTimer));
6611 assert!(!out.contains(&SystemChange::ScrollSelectionIntoView));
6612
6613 let out = default_post_filter(false, &[SystemChange::CopyToClipboard], None, None);
6615 assert_eq!(out, vec![SystemChange::ApplyPendingTextInput]);
6616 }
6617
6618 #[test]
6619 fn post_filter_emits_set_focus_only_when_focus_actually_moved() {
6620 let a = Some(dnid(0, 1));
6621 let b = Some(dnid(0, 2));
6622 for (old, new) in [(a, a), (None, None)] {
6624 let out = default_post_filter(false, &[], old, new);
6625 assert!(!out.iter().any(|c| matches!(c, SystemChange::SetFocus { .. })));
6626 }
6627 for (old, new) in [(a, b), (None, a), (a, None)] {
6629 let out = default_post_filter(false, &[], old, new);
6630 assert_eq!(
6631 out.last(),
6632 Some(&SystemChange::SetFocus { new_focus: new, old_focus: old })
6633 );
6634 assert_eq!(
6635 out.iter()
6636 .filter(|c| matches!(c, SystemChange::SetFocus { .. }))
6637 .count(),
6638 1
6639 );
6640 }
6641 }
6642
6643 #[test]
6644 fn post_filter_handles_a_large_pre_change_list_without_blowing_up() {
6645 let pre: Vec<SystemChange> = (0..5000)
6647 .map(|_| SystemChange::AddCursorAtClick { position: LogicalPosition::zero() })
6648 .collect();
6649 let out = default_post_filter(false, &pre, None, None);
6650 assert_eq!(out.len(), 5001);
6651 assert_eq!(out[0], SystemChange::ApplyPendingTextInput);
6652 assert!(out[1..]
6653 .iter()
6654 .all(|c| *c == SystemChange::ScrollSelectionIntoView));
6655 }
6656
6657 #[test]
6658 fn default_post_filter_delegates_to_post_callback_filter_system_changes() {
6659 let pre = vec![
6660 SystemChange::TextSelectionDrag {
6661 start_position: LogicalPosition::zero(),
6662 current_position: LogicalPosition::new(2.0, 2.0),
6663 },
6664 SystemChange::SelectAllText,
6665 ];
6666 for prevent in [false, true] {
6667 for (old, new) in [(None, None), (Some(dnid(0, 1)), Some(dnid(0, 2)))] {
6668 assert_eq!(
6669 default_post_filter(prevent, &pre, old, new),
6670 post_callback_filter_system_changes(prevent, &pre, old, new),
6671 "the two entry points must stay in lock-step"
6672 );
6673 }
6674 }
6675 }
6676
6677 #[test]
6683 fn default_op_schema_is_an_empty_list_not_a_null() {
6684 let cb = CustomE2eOpCallback::default();
6685 assert!(cb.op_schema.is_object(), "schema must be an object");
6686 assert!(!cb.op_schema.is_null());
6687 let text = cb.op_schema.internal.string_value.as_str();
6688 assert!(text.contains("\"ops\""), "got {text}");
6689
6690 let schema = E2eOpSchema {
6693 ops: alloc::vec![E2eOpDef {
6694 name: "load_document".to_string(),
6695 summary: "Open a file".to_string(),
6696 description: "Loads a markdown file into the editor.".to_string(),
6697 args: alloc::vec![E2eOpArg {
6698 name: "path".to_string(),
6699 arg_type: E2eOpArgType::String,
6700 required: true,
6701 description: "Absolute path.".to_string(),
6702 }],
6703 examples: alloc::vec![E2eOpExample {
6704 description: "Open big.md".to_string(),
6705 args: crate::json::Json::parse(r#"{"path":"/tmp/big.md"}"#).unwrap(),
6706 returns: crate::json::Json::parse(r#"{"success":true,"pages":40}"#)
6707 .unwrap(),
6708 }],
6709 }],
6710 };
6711 let j = schema.to_json();
6712 let t = j.internal.string_value.as_str();
6713 for needle in ["load_document", "Open a file", "\"type\":\"string\"", "big.md", "pages"] {
6714 assert!(t.contains(needle), "missing {needle} in {t}");
6715 }
6716 }
6717
6718 fn sample_op(returns: &str) -> E2eOpDef {
6719 E2eOpDef {
6720 name: "load_document".to_string(),
6721 summary: "Open a markdown file".to_string(),
6722 description: "Reads and paginates a file.".to_string(),
6723 args: alloc::vec![E2eOpArg {
6724 name: "path".to_string(),
6725 arg_type: E2eOpArgType::String,
6726 required: true,
6727 description: "Absolute path.".to_string(),
6728 }],
6729 examples: alloc::vec![E2eOpExample {
6730 description: "Open big.md".to_string(),
6731 args: crate::json::Json::parse(r#"{"path":"/tmp/big.md"}"#).unwrap(),
6732 returns: crate::json::Json::parse(returns).unwrap(),
6733 }],
6734 }
6735 }
6736
6737 #[test]
6739 fn schema_validation_requires_a_success_boolean_in_every_example() {
6740 let ok = E2eOpSchema { ops: alloc::vec![sample_op(r#"{"success":true,"pages":40}"#)] };
6741 assert_eq!(ok.validate(), Ok(()));
6742
6743 let missing = E2eOpSchema { ops: alloc::vec![sample_op(r#"{"pages":40}"#)] };
6745 assert!(matches!(
6746 missing.validate(),
6747 Err(E2eSchemaError::ExampleMissingSuccess { .. })
6748 ));
6749
6750 let stringy = E2eOpSchema { ops: alloc::vec![sample_op(r#"{"success":"yes"}"#)] };
6754 assert!(stringy.validate().is_err());
6755
6756 let dupe = E2eOpSchema {
6757 ops: alloc::vec![
6758 sample_op(r#"{"success":true}"#),
6759 sample_op(r#"{"success":true}"#),
6760 ],
6761 };
6762 assert!(matches!(dupe.validate(), Err(E2eSchemaError::DuplicateOpName { .. })));
6763 }
6764
6765 #[test]
6767 fn schema_json_keeps_declaration_order_and_nests_examples() {
6768 let schema = E2eOpSchema { ops: alloc::vec![sample_op(r#"{"success":true,"pages":40}"#)] };
6769 let text = schema.to_json().internal.string_value.as_str().to_string();
6770
6771 let name_at = text.find("\"name\"").expect("name present");
6772 let args_at = text.find("\"args\"").expect("args present");
6773 assert!(name_at < args_at, "identity fields must lead: {text}");
6774
6775 assert!(!text.contains("\\\""), "examples must nest, not escape: {text}");
6777 assert!(text.contains("\"success\":true"), "{text}");
6778 }
6779
6780 #[test]
6781 fn default_custom_op_handler_recognises_nothing() {
6782 let r = default_custom_e2e_op_extern(
6785 crate::refany::RefAny::new(0u8),
6786 AzString::from_const_str("anything"),
6787 AzString::from_const_str("{}"),
6788 );
6789 assert!(!r.handled);
6790 }
6791
6792 fn default_post_filter_extern_decodes_the_none_focus_sentinel() {
6793 let pre: Vec<SystemChange> = Vec::new();
6795 let slice = SystemChangeVecSlice {
6796 ptr: pre.as_ptr(),
6797 len: pre.len(),
6798 };
6799 let out = default_post_filter_extern(
6800 crate::refany::RefAny::new(0u8),
6801 false,
6802 slice,
6803 dnid_none(0),
6804 dnid(0, 4),
6805 );
6806 let changes = out.as_slice();
6807 assert_eq!(changes.first(), Some(&SystemChange::ApplyPendingTextInput));
6808 assert_eq!(
6809 changes.last(),
6810 Some(&SystemChange::SetFocus {
6811 new_focus: Some(dnid(0, 4)),
6812 old_focus: None,
6813 }),
6814 "a `NONE` node id must decode to `None`, not to node 0"
6815 );
6816
6817 let out = default_post_filter_extern(
6819 crate::refany::RefAny::new(0u8),
6820 true,
6821 SystemChangeVecSlice::empty(),
6822 dnid_none(0),
6823 dnid_none(0),
6824 );
6825 assert!(out.as_slice().is_empty());
6826 }
6827}