Skip to main content

azul_core/
events.rs

1//! Event and callback filtering module
2
3#[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/// Easing functions for smooth scroll animations
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum EasingFunction {
27    Linear,
28    EaseInOut,
29    EaseOut,
30}
31
32pub type RestyleNodes = BTreeMap<NodeId, Vec<ChangedCssProperty>>;
33pub type RelayoutNodes = BTreeMap<NodeId, Vec<ChangedCssProperty>>;
34pub type RelayoutWords = BTreeMap<NodeId, AzString>;
35
36#[derive(Debug, Clone, PartialEq)]
37pub struct FocusChange {
38    pub old: Option<DomNodeId>,
39    pub new: Option<DomNodeId>,
40}
41
42#[derive(Debug, Clone, PartialEq)]
43pub struct CallbackToCall {
44    pub node_id: NodeId,
45    pub hit_test_item: Option<HitTestItem>,
46    pub event_filter: EventFilter,
47}
48
49impl CallbackToCall {
50    pub fn new(
51        node_id: NodeId,
52        hit_test_item: Option<HitTestItem>,
53        event_filter: EventFilter,
54    ) -> Self {
55        Self { node_id, hit_test_item, event_filter }
56    }
57
58    /// Build a list of `CallbackToCall` entries for every node hit by the
59    /// given hit test under the given DOM, tagged with `event_filter`.
60    /// Returns an empty `Vec` when there is no hit test data for the DOM.
61    pub fn from_hit_test(
62        hit_test: &FullHitTest,
63        dom_id: DomId,
64        event_filter: EventFilter,
65    ) -> Vec<CallbackToCall> {
66        let Some(hit) = hit_test.hovered_nodes.get(&dom_id) else {
67            return Vec::new();
68        };
69        hit.regular_hit_test_nodes
70            .iter()
71            .map(|(node_id, item)| CallbackToCall {
72                node_id: *node_id,
73                hit_test_item: Some(item.clone()),
74                event_filter: event_filter.clone(),
75            })
76            .collect()
77    }
78}
79
80#[derive(Debug, Copy, Clone, PartialEq, Eq)]
81#[must_use = "ProcessEventResult must be used to determine if relayout/repaint is needed"]
82pub enum ProcessEventResult {
83    DoNothing = 0,
84    ShouldReRenderCurrentWindow = 1,
85    ShouldUpdateDisplayListCurrentWindow = 2,
86    // GPU transforms changed: do another hit-test and recurse
87    // until nothing has changed anymore
88    UpdateHitTesterAndProcessAgain = 3,
89    // Restyle or runtime edit changed layout-affecting properties:
90    // re-run layout on the EXISTING StyledDom (no DOM rebuild).
91    ShouldIncrementalRelayout = 4,
92    // Full DOM rebuild via user's layout_callback()
93    ShouldRegenerateDomCurrentWindow = 5,
94    ShouldRegenerateDomAllWindows = 6,
95}
96
97impl ProcessEventResult {
98    pub fn order(&self) -> usize {
99        use self::ProcessEventResult::*;
100        match self {
101            DoNothing => 0,
102            ShouldReRenderCurrentWindow => 1,
103            ShouldUpdateDisplayListCurrentWindow => 2,
104            UpdateHitTesterAndProcessAgain => 3,
105            ShouldIncrementalRelayout => 4,
106            ShouldRegenerateDomCurrentWindow => 5,
107            ShouldRegenerateDomAllWindows => 6,
108        }
109    }
110}
111
112impl PartialOrd for ProcessEventResult {
113    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
114        self.order().partial_cmp(&other.order())
115    }
116}
117
118impl Ord for ProcessEventResult {
119    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
120        self.order().cmp(&other.order())
121    }
122}
123
124impl ProcessEventResult {
125    pub fn max_self(self, other: Self) -> Self {
126        self.max(other)
127    }
128}
129
130/// Tracks the origin of an event for proper handling.
131///
132/// This allows the system to distinguish between user input, programmatic
133/// changes, and synthetic events generated by UI components.
134#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
135#[repr(C)]
136pub enum EventSource {
137    /// Direct user input (mouse, keyboard, touch, gamepad)
138    User,
139    /// API call (programmatic scroll, focus change, etc.)
140    Programmatic,
141    /// Generated from UI interaction (scrollbar drag, synthetic events)
142    Synthetic,
143    /// Generated from lifecycle hooks (mount, unmount, resize)
144    Lifecycle,
145}
146
147/// Event propagation phase (similar to DOM Level 2 Events).
148///
149/// Events can be intercepted at different phases:
150/// - **Capture**: Event travels from root down to target (rarely used)
151/// - **Target**: Event is at the target element
152/// - **Bubble**: Event travels from target back up to root (most common)
153#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
154#[repr(C)]
155pub enum EventPhase {
156    /// Event travels from root down to target
157    Capture,
158    /// Event is at the target element
159    Target,
160    /// Event bubbles from target back up to root
161    Bubble,
162}
163
164impl Default for EventPhase {
165    fn default() -> Self {
166        EventPhase::Bubble
167    }
168}
169
170/// Mouse button identifier for mouse events.
171#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
172#[repr(C)]
173pub enum MouseButton {
174    Left,
175    Middle,
176    Right,
177    Other(u8),
178}
179
180/// Scroll delta mode (how scroll deltas should be interpreted).
181#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
182#[repr(C)]
183pub enum ScrollDeltaMode {
184    /// Delta is in pixels
185    Pixel,
186    /// Delta is in lines (e.g., 3 lines of text)
187    Line,
188    /// Delta is in pages
189    Page,
190}
191
192/// Scroll direction for conditional event filtering.
193#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
194#[repr(C)]
195pub enum ScrollDirection {
196    Up,
197    Down,
198    Left,
199    Right,
200}
201
202// ============================================================================
203// W3C CSSOM View Module - Scroll Into View Types
204// ============================================================================
205
206/// W3C-compliant scroll-into-view options
207///
208/// These options control how an element is scrolled into view, following
209/// the CSSOM View Module specification.
210#[repr(C)]
211#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
212pub struct ScrollIntoViewOptions {
213    /// Vertical alignment: start, center, end, nearest (default: nearest)
214    pub block: ScrollLogicalPosition,
215    /// Horizontal alignment: start, center, end, nearest (default: nearest)
216    /// Note: Named `inline_axis` to avoid conflict with C keyword `inline`
217    pub inline_axis: ScrollLogicalPosition,
218    /// Animation behavior: auto, instant, smooth (default: auto)
219    pub behavior: ScrollIntoViewBehavior,
220}
221
222impl ScrollIntoViewOptions {
223    /// Create options with "nearest" alignment for both axes
224    pub fn nearest() -> Self {
225        Self {
226            block: ScrollLogicalPosition::Nearest,
227            inline_axis: ScrollLogicalPosition::Nearest,
228            behavior: ScrollIntoViewBehavior::Auto,
229        }
230    }
231    
232    /// Create options with "center" alignment for both axes
233    pub fn center() -> Self {
234        Self {
235            block: ScrollLogicalPosition::Center,
236            inline_axis: ScrollLogicalPosition::Center,
237            behavior: ScrollIntoViewBehavior::Auto,
238        }
239    }
240    
241    /// Create options with "start" alignment for both axes
242    pub fn start() -> Self {
243        Self {
244            block: ScrollLogicalPosition::Start,
245            inline_axis: ScrollLogicalPosition::Start,
246            behavior: ScrollIntoViewBehavior::Auto,
247        }
248    }
249    
250    /// Create options to align the end of the target with the end of the viewport
251    pub fn end() -> Self {
252        Self {
253            block: ScrollLogicalPosition::End,
254            inline_axis: ScrollLogicalPosition::End,
255            behavior: ScrollIntoViewBehavior::Auto,
256        }
257    }
258    
259    /// Set instant scroll behavior
260    pub fn with_instant(mut self) -> Self {
261        self.behavior = ScrollIntoViewBehavior::Instant;
262        self
263    }
264    
265    /// Set smooth scroll behavior
266    pub fn with_smooth(mut self) -> Self {
267        self.behavior = ScrollIntoViewBehavior::Smooth;
268        self
269    }
270}
271
272/// Scroll alignment for vertical (block) or horizontal (inline) axis
273///
274/// Determines where the target element should be positioned within
275/// the scroll container's visible area.
276#[repr(C)]
277#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
278pub enum ScrollLogicalPosition {
279    /// Align target's start edge with container's start edge
280    Start,
281    /// Center target within container
282    Center,
283    /// Align target's end edge with container's end edge
284    End,
285    /// Minimum scroll distance to make target fully visible (default)
286    #[default]
287    Nearest,
288}
289
290/// Scroll animation behavior for scrollIntoView API
291///
292/// This is distinct from the CSS `scroll-behavior` property, as it also
293/// supports the `Instant` option which CSS does not have.
294#[repr(C)]
295#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
296pub enum ScrollIntoViewBehavior {
297    /// Respect CSS scroll-behavior property (default)
298    #[default]
299    Auto,
300    /// Immediate jump without animation
301    Instant,
302    /// Animated smooth scroll
303    Smooth,
304}
305
306/// Reason why a lifecycle event was triggered.
307#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
308#[repr(C)]
309pub enum LifecycleReason {
310    /// First appearance in DOM
311    InitialMount,
312    /// Removed and re-added to DOM
313    Remount,
314    /// Layout bounds changed
315    Resize,
316    /// Props or state changed
317    Update,
318    /// Node was removed from DOM
319    Unmount,
320}
321
322/// Keyboard modifier keys state.
323#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)]
324#[repr(C)]
325pub struct KeyModifiers {
326    pub shift: bool,
327    pub ctrl: bool,
328    pub alt: bool,
329    pub meta: bool,
330}
331
332impl KeyModifiers {
333    pub fn new() -> Self {
334        Self::default()
335    }
336
337    pub fn with_shift(mut self) -> Self {
338        self.shift = true;
339        self
340    }
341
342    pub fn with_ctrl(mut self) -> Self {
343        self.ctrl = true;
344        self
345    }
346
347    pub fn with_alt(mut self) -> Self {
348        self.alt = true;
349        self
350    }
351
352    pub fn with_meta(mut self) -> Self {
353        self.meta = true;
354        self
355    }
356
357    pub fn is_empty(&self) -> bool {
358        !self.shift && !self.ctrl && !self.alt && !self.meta
359    }
360}
361
362/// Type-specific event data for mouse events.
363#[derive(Debug, Clone, PartialEq)]
364pub struct MouseEventData {
365    /// Position of the mouse cursor
366    pub position: LogicalPosition,
367    /// Which button was pressed/released
368    pub button: MouseButton,
369    /// Bitmask of currently pressed buttons
370    pub buttons: u8,
371    /// Modifier keys state
372    pub modifiers: KeyModifiers,
373}
374
375/// Type-specific event data for keyboard events.
376#[derive(Debug, Clone, PartialEq)]
377pub struct KeyboardEventData {
378    /// The virtual key code
379    pub key_code: u32,
380    /// The character produced (if any)
381    pub char_code: Option<char>,
382    /// Modifier keys state
383    pub modifiers: KeyModifiers,
384    /// Whether this is a repeat event
385    pub repeat: bool,
386}
387
388/// Type-specific event data for scroll events.
389#[derive(Debug, Clone, PartialEq)]
390pub struct ScrollEventData {
391    /// Scroll delta (dx, dy)
392    pub delta: LogicalPosition,
393    /// How the delta should be interpreted
394    pub delta_mode: ScrollDeltaMode,
395}
396
397/// Type-specific event data for touch events.
398#[derive(Debug, Clone, PartialEq)]
399pub struct TouchEventData {
400    /// Touch identifier
401    pub id: u64,
402    /// Touch position
403    pub position: LogicalPosition,
404    /// Touch force/pressure (0.0 - 1.0)
405    pub force: f32,
406}
407
408/// Type-specific event data for clipboard events.
409#[derive(Debug, Clone, PartialEq)]
410pub struct ClipboardEventData {
411    /// The clipboard content (for paste events)
412    pub content: Option<String>,
413}
414
415/// Type-specific event data for lifecycle events.
416#[derive(Debug, Clone, PartialEq)]
417pub struct LifecycleEventData {
418    /// Why this lifecycle event was triggered
419    pub reason: LifecycleReason,
420    /// Previous layout bounds (for resize events)
421    pub previous_bounds: Option<LogicalRect>,
422    /// Current layout bounds
423    pub current_bounds: LogicalRect,
424}
425
426/// Type-specific event data for window events.
427#[derive(Debug, Clone, PartialEq)]
428pub struct WindowEventData {
429    /// Window size (for resize events)
430    pub size: Option<LogicalRect>,
431    /// Window position (for move events)
432    pub position: Option<LogicalPosition>,
433}
434
435/// Union of all possible event data types.
436#[derive(Debug, Clone, PartialEq)]
437pub enum EventData {
438    /// Mouse event data
439    Mouse(MouseEventData),
440    /// Keyboard event data
441    Keyboard(KeyboardEventData),
442    /// Scroll event data
443    Scroll(ScrollEventData),
444    /// Touch event data
445    Touch(TouchEventData),
446    /// Clipboard event data
447    Clipboard(ClipboardEventData),
448    /// Lifecycle event data
449    Lifecycle(LifecycleEventData),
450    /// Window event data
451    Window(WindowEventData),
452    /// No additional data
453    None,
454}
455
456/// High-level event type classification.
457///
458/// This enum categorizes all possible events that can occur in the UI.
459/// It extends the existing event system with new event types for
460/// lifecycle, clipboard, media, and form handling.
461#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
462#[repr(C)]
463pub enum EventType {
464    // Mouse Events
465    /// Mouse cursor is over the element
466    MouseOver,
467    /// Mouse cursor entered the element
468    MouseEnter,
469    /// Mouse cursor left the element
470    MouseLeave,
471    /// Mouse left the element OR moved to a child element (W3C `mouseout`, bubbles)
472    MouseOut,
473    /// Mouse button pressed
474    MouseDown,
475    /// Mouse button released
476    MouseUp,
477    /// Mouse click (down + up on same element)
478    Click,
479    /// Mouse double-click
480    DoubleClick,
481    /// Right-click / context menu
482    ContextMenu,
483
484    // Keyboard Events
485    /// Key pressed down
486    KeyDown,
487    /// Key released
488    KeyUp,
489    /// Character input (respects locale/keyboard layout)
490    KeyPress,
491
492    // IME Composition Events
493    /// IME composition started
494    CompositionStart,
495    /// IME composition updated (intermediate text changed)
496    CompositionUpdate,
497    /// IME composition ended (final text committed)
498    CompositionEnd,
499
500    // Focus Events
501    /// Element received focus
502    Focus,
503    /// Element lost focus
504    Blur,
505    /// Focus entered element or its children
506    FocusIn,
507    /// Focus left element and its children
508    FocusOut,
509
510    // Input Events
511    /// Input value is being changed (fires on every keystroke)
512    Input,
513    /// Input value has changed (fires after editing complete)
514    Change,
515    /// Form submitted
516    Submit,
517    /// Form reset
518    Reset,
519    /// Form validation failed
520    Invalid,
521
522    // Scroll Events
523    /// Element is being scrolled
524    Scroll,
525    /// Scroll started
526    ScrollStart,
527    /// Scroll ended
528    ScrollEnd,
529
530    // Drag Events
531    /// Drag operation started
532    DragStart,
533    /// Element is being dragged
534    Drag,
535    /// Drag operation ended
536    DragEnd,
537    /// Dragged element entered drop target
538    DragEnter,
539    /// Dragged element is over drop target
540    DragOver,
541    /// Dragged element left drop target
542    DragLeave,
543    /// Element was dropped
544    Drop,
545
546    // Touch Events
547    /// Touch started
548    TouchStart,
549    /// Touch moved
550    TouchMove,
551    /// Touch ended
552    TouchEnd,
553    /// Touch cancelled
554    TouchCancel,
555
556    // Pen / Stylus Events (W3C PointerEvent, pointerType "pen")
557    /// Pen tip made contact (or pen entered while down)
558    PenDown,
559    /// Pen moved (in contact or hovering in range)
560    PenMove,
561    /// Pen tip lifted
562    PenUp,
563    /// Pen entered hover/sensing range (proximity in)
564    PenEnter,
565    /// Pen left hover/sensing range (proximity out)
566    PenLeave,
567
568    // Gesture Events
569    /// Long press detected (touch or mouse held down)
570    LongPress,
571    /// Swipe gesture to the left
572    SwipeLeft,
573    /// Swipe gesture to the right
574    SwipeRight,
575    /// Swipe gesture upward
576    SwipeUp,
577    /// Swipe gesture downward
578    SwipeDown,
579    /// Pinch-in gesture (zoom out)
580    PinchIn,
581    /// Pinch-out gesture (zoom in)
582    PinchOut,
583    /// Clockwise rotation gesture
584    RotateClockwise,
585    /// Counter-clockwise rotation gesture
586    RotateCounterClockwise,
587
588    // Clipboard Events
589    /// Content copied to clipboard
590    Copy,
591    /// Content cut to clipboard
592    Cut,
593    /// Content pasted from clipboard
594    Paste,
595
596    // Media Events
597    /// Media playback started
598    Play,
599    /// Media playback paused
600    Pause,
601    /// Media playback ended
602    Ended,
603    /// Media time updated
604    TimeUpdate,
605    /// Media volume changed
606    VolumeChange,
607    /// Media error occurred
608    MediaError,
609
610    // Lifecycle Events
611    /// Component was mounted to the DOM
612    Mount,
613    /// Component will be unmounted from the DOM
614    Unmount,
615    /// Component was updated
616    Update,
617    /// Component layout bounds changed
618    Resize,
619
620    // Window Events
621    /// Window resized
622    WindowResize,
623    /// Window moved
624    WindowMove,
625    /// Window close requested
626    WindowClose,
627    /// Window received focus
628    WindowFocusIn,
629    /// Window lost focus
630    WindowFocusOut,
631    /// System theme changed
632    ThemeChange,
633    /// Window DPI/scale factor changed (moved to different monitor)
634    WindowDpiChanged,
635    /// Window moved to a different monitor
636    WindowMonitorChanged,
637
638    // Application Events
639    /// A monitor/display was connected
640    MonitorConnected,
641    /// A monitor/display was disconnected
642    MonitorDisconnected,
643
644    // File Events
645    /// File is being hovered
646    FileHover,
647    /// File was dropped
648    FileDrop,
649    /// File hover cancelled
650    FileHoverCancel,
651
652    // Hardware input-device Events (P6 sensors / gamepad)
653    /// A motion-sensor reading (accelerometer / gyroscope / magnetometer)
654    /// changed. Read the value with `CallbackInfo::get_sensor_reading`.
655    SensorChanged,
656    /// A gamepad's buttons / axes changed, or one was (dis)connected. Read it
657    /// with `CallbackInfo::get_primary_gamepad` / `get_gamepad_state`.
658    GamepadInput,
659}
660
661/// Unified event wrapper (similar to React's SyntheticEvent).
662///
663/// All events in the system are wrapped in this structure, providing
664/// a consistent interface and enabling event propagation control.
665#[derive(Debug, Clone, PartialEq)]
666pub struct SyntheticEvent {
667    /// The type of event
668    pub event_type: EventType,
669
670    /// Where the event came from
671    pub source: EventSource,
672
673    /// Current propagation phase
674    pub phase: EventPhase,
675
676    /// Target node that the event was dispatched to
677    pub target: DomNodeId,
678
679    /// Current node in the propagation path
680    pub current_target: DomNodeId,
681
682    /// Timestamp when event was created
683    pub timestamp: Instant,
684
685    /// Type-specific event data
686    pub data: EventData,
687
688    /// Whether propagation has been stopped
689    pub stopped: bool,
690
691    /// Whether immediate propagation has been stopped
692    pub stopped_immediate: bool,
693
694    /// Whether default action has been prevented
695    pub prevented_default: bool,
696}
697
698impl SyntheticEvent {
699    /// Create a new synthetic event.
700    ///
701    /// # Parameters
702    /// - `timestamp`: Current time from `(system_callbacks.get_system_time_fn.cb)()`
703    pub fn new(
704        event_type: EventType,
705        source: EventSource,
706        target: DomNodeId,
707        timestamp: Instant,
708        data: EventData,
709    ) -> Self {
710        Self {
711            event_type,
712            source,
713            phase: EventPhase::Target,
714            target,
715            current_target: target,
716            timestamp,
717            data,
718            stopped: false,
719            stopped_immediate: false,
720            prevented_default: false,
721        }
722    }
723
724    /// Stop event propagation after the current phase completes.
725    ///
726    /// This prevents the event from reaching handlers in subsequent phases
727    /// (e.g., stopping during capture prevents bubble phase).
728    pub fn stop_propagation(&mut self) {
729        self.stopped = true;
730    }
731
732    /// Stop event propagation immediately.
733    ///
734    /// This prevents any further handlers from being called, even on the
735    /// current target element.
736    pub fn stop_immediate_propagation(&mut self) {
737        self.stopped_immediate = true;
738        self.stopped = true;
739    }
740
741    /// Prevent the default action associated with this event.
742    ///
743    /// For example, prevents form submission on Enter key, or prevents
744    /// text selection on drag.
745    pub fn prevent_default(&mut self) {
746        self.prevented_default = true;
747    }
748
749    /// Check if propagation was stopped.
750    pub fn is_propagation_stopped(&self) -> bool {
751        self.stopped
752    }
753
754    /// Check if immediate propagation was stopped.
755    pub fn is_immediate_propagation_stopped(&self) -> bool {
756        self.stopped_immediate
757    }
758
759    /// Check if default action was prevented.
760    pub fn is_default_prevented(&self) -> bool {
761        self.prevented_default
762    }
763}
764
765/// Result of event propagation through DOM tree.
766#[derive(Debug, Clone)]
767pub struct PropagationResult {
768    /// Callbacks that should be invoked, in order
769    pub callbacks_to_invoke: Vec<(NodeId, EventFilter)>,
770    /// Whether default action should be prevented
771    pub default_prevented: bool,
772}
773
774/// Get the path from root to target node in the DOM tree.
775///
776/// This is used for event propagation - we need to know which nodes
777/// are ancestors of the target to implement capture/bubble phases.
778///
779/// Returns nodes in order from root to target (inclusive).
780pub fn get_dom_path(
781    node_hierarchy: &crate::id::NodeHierarchy,
782    target_node: NodeHierarchyItemId,
783) -> Vec<NodeId> {
784    let mut path = Vec::new();
785    let target_node_id = match target_node.into_crate_internal() {
786        Some(id) => id,
787        None => return path,
788    };
789
790    let hier_ref = node_hierarchy.as_ref();
791
792    // Build path from target to root
793    let mut current = Some(target_node_id);
794    while let Some(node_id) = current {
795        path.push(node_id);
796        current = hier_ref.get(node_id).and_then(|node| node.parent);
797    }
798
799    // Reverse to get root → target order
800    path.reverse();
801    path
802}
803
804/// Propagate event through DOM tree with capture and bubble phases.
805///
806/// This implements DOM Level 2 event propagation:
807/// 1. **Capture Phase**: Event travels from root down to target
808/// 2. **Target Phase**: Event is at the target element
809/// 3. **Bubble Phase**: Event travels from target back up to root
810///
811/// The event can be stopped at any point via `stopPropagation()` or
812/// `stopImmediatePropagation()`.
813pub fn propagate_event(
814    event: &mut SyntheticEvent,
815    node_hierarchy: &crate::id::NodeHierarchy,
816    callbacks: &BTreeMap<NodeId, Vec<EventFilter>>,
817) -> PropagationResult {
818    let path = get_dom_path(node_hierarchy, event.target.node);
819    if path.is_empty() {
820        return PropagationResult::default();
821    }
822
823    let ancestors = &path[..path.len().saturating_sub(1)];
824    let target_node_id = *path.last().unwrap();
825
826    let mut result = PropagationResult::default();
827
828    // Phase 1: Capture (root → target)
829    propagate_phase(
830        event,
831        ancestors.iter().copied(),
832        EventPhase::Capture,
833        callbacks,
834        &mut result,
835    );
836
837    // Phase 2: Target
838    if !event.stopped {
839        propagate_target_phase(event, target_node_id, callbacks, &mut result);
840    }
841
842    // Phase 3: Bubble (target → root)
843    if !event.stopped {
844        propagate_phase(
845            event,
846            ancestors.iter().rev().copied(),
847            EventPhase::Bubble,
848            callbacks,
849            &mut result,
850        );
851    }
852
853    result.default_prevented = event.prevented_default;
854    result
855}
856
857/// Process a single propagation phase (Capture or Bubble)
858fn propagate_phase(
859    event: &mut SyntheticEvent,
860    nodes: impl Iterator<Item = NodeId>,
861    phase: EventPhase,
862    callbacks: &BTreeMap<NodeId, Vec<EventFilter>>,
863    result: &mut PropagationResult,
864) {
865    event.phase = phase;
866
867    for node_id in nodes {
868        if event.stopped_immediate || event.stopped {
869            return;
870        }
871
872        event.current_target = DomNodeId {
873            dom: event.target.dom,
874            node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
875        };
876
877        collect_matching_callbacks(event, node_id, phase, callbacks, result);
878    }
879}
880
881/// Process the target phase
882fn propagate_target_phase(
883    event: &mut SyntheticEvent,
884    target_node_id: NodeId,
885    callbacks: &BTreeMap<NodeId, Vec<EventFilter>>,
886    result: &mut PropagationResult,
887) {
888    event.phase = EventPhase::Target;
889    event.current_target = event.target;
890
891    collect_matching_callbacks(event, target_node_id, EventPhase::Target, callbacks, result);
892}
893
894/// Collect callbacks that match the current phase for a node
895fn collect_matching_callbacks(
896    event: &SyntheticEvent,
897    node_id: NodeId,
898    phase: EventPhase,
899    callbacks: &BTreeMap<NodeId, Vec<EventFilter>>,
900    result: &mut PropagationResult,
901) {
902    let Some(node_callbacks) = callbacks.get(&node_id) else {
903        return;
904    };
905
906    let matching = node_callbacks
907        .iter()
908        .take_while(|_| !event.stopped_immediate)
909        .filter(|filter| matches_filter_phase(filter, event, phase))
910        .map(|filter| (node_id, *filter));
911
912    result.callbacks_to_invoke.extend(matching);
913}
914
915impl Default for PropagationResult {
916    fn default() -> Self {
917        Self {
918            callbacks_to_invoke: Vec::new(),
919            default_prevented: false,
920        }
921    }
922}
923
924// =============================================================================
925// DEFAULT ACTIONS (W3C UI Events / HTML5 Activation Behavior)
926// =============================================================================
927
928/// Default actions are built-in behaviors that occur in response to events.
929///
930/// Per W3C DOM Event specification:
931/// > A default action is an action that the implementation is expected to take
932/// > in response to an event, unless that action is cancelled by the script.
933///
934/// Examples:
935/// - Tab key → move focus to next focusable element
936/// - Enter/Space on button → activate (click) the button
937/// - Escape → clear focus or close modal
938/// - Arrow keys in listbox → move selection
939///
940/// Default actions are processed AFTER all event callbacks have been invoked,
941/// and only if `event.prevent_default()` was NOT called.
942#[derive(Debug, Clone, PartialEq, Eq, Hash)]
943#[repr(C, u8)]
944pub enum DefaultAction {
945    /// Move focus to the next focusable element (Tab key)
946    FocusNext,
947    /// Move focus to the previous focusable element (Shift+Tab)
948    FocusPrevious,
949    /// Move focus to the first focusable element
950    FocusFirst,
951    /// Move focus to the last focusable element
952    FocusLast,
953    /// Clear focus from the currently focused element (Escape key)
954    ClearFocus,
955    /// Activate the focused element (Enter/Space on activatable elements)
956    /// This generates a synthetic Click event on the target
957    ActivateFocusedElement {
958        target: DomNodeId,
959    },
960    /// Submit the form containing the focused element (Enter in form input)
961    SubmitForm {
962        form_node: DomNodeId,
963    },
964    /// Close the current modal/dialog (Escape key when modal is open)
965    CloseModal {
966        modal_node: DomNodeId,
967    },
968    /// Scroll the focused scrollable container
969    ScrollFocusedContainer {
970        direction: ScrollDirection,
971        amount: ScrollAmount,
972    },
973    /// Select all text in the focused text input (Ctrl+A / Cmd+A)
974    SelectAllText,
975    /// No default action for this event
976    None,
977}
978
979/// Amount to scroll for keyboard-based scrolling
980#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
981#[repr(C)]
982pub enum ScrollAmount {
983    /// Scroll by one line (arrow keys)
984    Line,
985    /// Scroll by one page (Page Up/Down)
986    Page,
987    /// Scroll to start/end (Home/End)
988    Document,
989}
990
991/// Result of determining what default action should occur for an event.
992///
993/// This is computed AFTER event dispatch, based on:
994/// 1. The event type
995/// 2. The target element's type/role
996/// 3. Whether `prevent_default()` was called
997#[derive(Debug, Clone)]
998#[repr(C)]
999pub struct DefaultActionResult {
1000    /// The default action to perform (if any)
1001    pub action: DefaultAction,
1002    /// Whether the action was prevented by a callback
1003    pub prevented: bool,
1004}
1005
1006impl Default for DefaultActionResult {
1007    fn default() -> Self {
1008        Self {
1009            action: DefaultAction::None,
1010            prevented: false,
1011        }
1012    }
1013}
1014
1015impl DefaultActionResult {
1016    /// Create a new result with a specific action
1017    pub fn new(action: DefaultAction) -> Self {
1018        Self {
1019            action,
1020            prevented: false,
1021        }
1022    }
1023
1024    /// Create a prevented result (callback called prevent_default)
1025    pub fn prevented() -> Self {
1026        Self {
1027            action: DefaultAction::None,
1028            prevented: true,
1029        }
1030    }
1031
1032    /// Check if there's an action to perform
1033    pub fn has_action(&self) -> bool {
1034        !self.prevented && !matches!(self.action, DefaultAction::None)
1035    }
1036}
1037
1038/// Trait for elements that have activation behavior (can be "clicked" via keyboard).
1039///
1040/// Per HTML5 spec, elements with activation behavior include:
1041/// - `<button>` elements
1042/// - `<input type="submit">`, `<input type="button">`, `<input type="reset">`
1043/// - `<a>` elements with href
1044/// - `<area>` elements with href
1045/// - Any element with a click handler (implicit activation)
1046///
1047/// When an element with activation behavior is focused and the user presses
1048/// Enter or Space, a synthetic click event is generated.
1049pub trait ActivationBehavior {
1050    /// Returns true if this element can be activated via keyboard (Enter/Space)
1051    fn has_activation_behavior(&self) -> bool;
1052
1053    /// Returns true if this element is currently activatable
1054    /// (e.g., not disabled, not aria-disabled="true")
1055    fn is_activatable(&self) -> bool;
1056}
1057
1058/// Trait to query if a node is focusable for tab navigation
1059pub trait Focusable {
1060    /// Returns the tabindex value for this element (-1, 0, or positive)
1061    fn get_tabindex(&self) -> Option<i32>;
1062
1063    /// Returns true if this element can receive focus
1064    fn is_focusable(&self) -> bool;
1065
1066    /// Returns true if this element should be in the tab order
1067    fn is_in_tab_order(&self) -> bool {
1068        match self.get_tabindex() {
1069            None => self.is_naturally_focusable(),
1070            Some(i) => i >= 0,
1071        }
1072    }
1073
1074    /// Returns true if this element type is naturally focusable
1075    /// (button, input, select, textarea, a[href])
1076    fn is_naturally_focusable(&self) -> bool;
1077}
1078
1079/// Check if an event filter matches the given event in the current phase.
1080///
1081/// This is used during event propagation to determine which callbacks
1082/// should be invoked at each phase.
1083fn matches_filter_phase(
1084    filter: &EventFilter,
1085    event: &SyntheticEvent,
1086    current_phase: EventPhase,
1087) -> bool {
1088    // For now, we match based on the filter type
1089    // In the future, this will also check EventPhase and EventConditions
1090
1091    match filter {
1092        EventFilter::Hover(hover_filter) => {
1093            matches_hover_filter(hover_filter, event, current_phase)
1094        }
1095        EventFilter::Focus(focus_filter) => {
1096            matches_focus_filter(focus_filter, event, current_phase)
1097        }
1098        EventFilter::Window(window_filter) => {
1099            matches_window_filter(window_filter, event, current_phase)
1100        }
1101        EventFilter::Component(component_filter) => {
1102            matches_component_filter(component_filter, event, current_phase)
1103        }
1104        EventFilter::Application(_) => {
1105            // Application events - will be implemented in future
1106            false
1107        }
1108    }
1109}
1110
1111/// Check if a component (lifecycle) filter matches the event.
1112///
1113/// Lifecycle events produced by `diff::reconcile_dom` carry the target node in
1114/// `SyntheticEvent.target`, so dispatchers that bypass `propagate_event` and
1115/// invoke the target directly also need a way to compare. This predicate is
1116/// the single source of truth for that comparison; changing it without
1117/// updating `event_type_to_filters` will de-sync dispatch.
1118fn matches_component_filter(
1119    filter: &ComponentEventFilter,
1120    event: &SyntheticEvent,
1121    _phase: EventPhase,
1122) -> bool {
1123    matches!(
1124        (filter, &event.event_type),
1125        (ComponentEventFilter::AfterMount, EventType::Mount)
1126            | (ComponentEventFilter::BeforeUnmount, EventType::Unmount)
1127            | (ComponentEventFilter::Updated, EventType::Update)
1128            | (ComponentEventFilter::NodeResized, EventType::Resize)
1129    )
1130}
1131
1132/// Check if the event data contains a mouse event with the expected button.
1133fn check_mouse_button(data: &EventData, expected: MouseButton) -> bool {
1134    if let EventData::Mouse(mouse_data) = data {
1135        mouse_data.button == expected
1136    } else {
1137        false
1138    }
1139}
1140
1141/// Check if a hover filter matches the event.
1142fn matches_hover_filter(
1143    filter: &HoverEventFilter,
1144    event: &SyntheticEvent,
1145    _phase: EventPhase,
1146) -> bool {
1147    use HoverEventFilter::*;
1148
1149    match (filter, &event.event_type) {
1150        (MouseOver, EventType::MouseOver) => true,
1151        (MouseDown, EventType::MouseDown) => true,
1152        (LeftMouseDown, EventType::MouseDown) => check_mouse_button(&event.data, MouseButton::Left),
1153        (RightMouseDown, EventType::MouseDown) => {
1154            check_mouse_button(&event.data, MouseButton::Right)
1155        }
1156        (MiddleMouseDown, EventType::MouseDown) => {
1157            check_mouse_button(&event.data, MouseButton::Middle)
1158        }
1159        (MouseUp, EventType::MouseUp) => true,
1160        (LeftMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Left),
1161        (RightMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Right),
1162        (MiddleMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Middle),
1163        (MouseEnter, EventType::MouseEnter) => true,
1164        (MouseLeave, EventType::MouseLeave) => true,
1165        (Scroll, EventType::Scroll) => true,
1166        (ScrollStart, EventType::ScrollStart) => true,
1167        (ScrollEnd, EventType::ScrollEnd) => true,
1168        (TextInput, EventType::Input) => true,
1169        (VirtualKeyDown, EventType::KeyDown) => true,
1170        (VirtualKeyUp, EventType::KeyUp) => true,
1171        (HoveredFile, EventType::FileHover) => true,
1172        (DroppedFile, EventType::FileDrop) => true,
1173        (HoveredFileCancelled, EventType::FileHoverCancel) => true,
1174        (TouchStart, EventType::TouchStart) => true,
1175        (TouchMove, EventType::TouchMove) => true,
1176        (TouchEnd, EventType::TouchEnd) => true,
1177        (TouchCancel, EventType::TouchCancel) => true,
1178        (PenDown, EventType::PenDown) => true,
1179        (PenMove, EventType::PenMove) => true,
1180        (PenUp, EventType::PenUp) => true,
1181        (PenEnter, EventType::PenEnter) => true,
1182        (PenLeave, EventType::PenLeave) => true,
1183        (DragStart, EventType::DragStart) => true,
1184        (Drag, EventType::Drag) => true,
1185        (DragEnd, EventType::DragEnd) => true,
1186        (DragEnter, EventType::DragEnter) => true,
1187        (DragOver, EventType::DragOver) => true,
1188        (DragLeave, EventType::DragLeave) => true,
1189        (Drop, EventType::Drop) => true,
1190        (DoubleClick, EventType::DoubleClick) => true,
1191        (SensorChanged, EventType::SensorChanged) => true,
1192        (GamepadInput, EventType::GamepadInput) => true,
1193        _ => false,
1194    }
1195}
1196
1197/// Check if a focus filter matches the event.
1198fn matches_focus_filter(
1199    filter: &FocusEventFilter,
1200    event: &SyntheticEvent,
1201    _phase: EventPhase,
1202) -> bool {
1203    use FocusEventFilter::*;
1204
1205    match (filter, &event.event_type) {
1206        (MouseOver, EventType::MouseOver) => true,
1207        (MouseDown, EventType::MouseDown) => true,
1208        (LeftMouseDown, EventType::MouseDown) => check_mouse_button(&event.data, MouseButton::Left),
1209        (RightMouseDown, EventType::MouseDown) => {
1210            check_mouse_button(&event.data, MouseButton::Right)
1211        }
1212        (MiddleMouseDown, EventType::MouseDown) => {
1213            check_mouse_button(&event.data, MouseButton::Middle)
1214        }
1215        (MouseUp, EventType::MouseUp) => true,
1216        (LeftMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Left),
1217        (RightMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Right),
1218        (MiddleMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Middle),
1219        (MouseEnter, EventType::MouseEnter) => true,
1220        (MouseLeave, EventType::MouseLeave) => true,
1221        (Scroll, EventType::Scroll) => true,
1222        (ScrollStart, EventType::ScrollStart) => true,
1223        (ScrollEnd, EventType::ScrollEnd) => true,
1224        (TextInput, EventType::Input) => true,
1225        (VirtualKeyDown, EventType::KeyDown) => true,
1226        (VirtualKeyUp, EventType::KeyUp) => true,
1227        (FocusReceived, EventType::Focus) => true,
1228        (FocusLost, EventType::Blur) => true,
1229        (DragStart, EventType::DragStart) => true,
1230        (Drag, EventType::Drag) => true,
1231        (DragEnd, EventType::DragEnd) => true,
1232        (DragEnter, EventType::DragEnter) => true,
1233        (DragOver, EventType::DragOver) => true,
1234        (DragLeave, EventType::DragLeave) => true,
1235        (Drop, EventType::Drop) => true,
1236        _ => false,
1237    }
1238}
1239
1240/// Check if a window filter matches the event.
1241fn matches_window_filter(
1242    filter: &WindowEventFilter,
1243    event: &SyntheticEvent,
1244    _phase: EventPhase,
1245) -> bool {
1246    use WindowEventFilter::*;
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        (Resized, EventType::WindowResize) => true,
1274        (Moved, EventType::WindowMove) => true,
1275        (TouchStart, EventType::TouchStart) => true,
1276        (TouchMove, EventType::TouchMove) => true,
1277        (TouchEnd, EventType::TouchEnd) => true,
1278        (TouchCancel, EventType::TouchCancel) => true,
1279        (PenDown, EventType::PenDown) => true,
1280        (PenMove, EventType::PenMove) => true,
1281        (PenUp, EventType::PenUp) => true,
1282        (PenEnter, EventType::PenEnter) => true,
1283        (PenLeave, EventType::PenLeave) => true,
1284        (FocusReceived, EventType::Focus) => true,
1285        (FocusLost, EventType::Blur) => true,
1286        (CloseRequested, EventType::WindowClose) => true,
1287        (ThemeChanged, EventType::ThemeChange) => true,
1288        (WindowFocusReceived, EventType::WindowFocusIn) => true,
1289        (WindowFocusLost, EventType::WindowFocusOut) => true,
1290        (SensorChanged, EventType::SensorChanged) => true,
1291        (GamepadInput, EventType::GamepadInput) => true,
1292        (DragStart, EventType::DragStart) => true,
1293        (Drag, EventType::Drag) => true,
1294        (DragEnd, EventType::DragEnd) => true,
1295        (DragEnter, EventType::DragEnter) => true,
1296        (DragOver, EventType::DragOver) => true,
1297        (DragLeave, EventType::DragLeave) => true,
1298        (Drop, EventType::Drop) => true,
1299        _ => false,
1300    }
1301}
1302
1303/// Detect lifecycle events by comparing old and new DOM state.
1304///
1305/// This is the simple, index-based lifecycle detection that doesn't account for
1306/// node reordering. For more sophisticated reconciliation that can detect moves,
1307/// use `detect_lifecycle_events_with_reconciliation`.
1308///
1309/// Generates Mount, Unmount, and Resize events by comparing DOM hierarchies.
1310pub fn detect_lifecycle_events(
1311    old_dom_id: DomId,
1312    new_dom_id: DomId,
1313    old_hierarchy: Option<&crate::id::NodeHierarchy>,
1314    new_hierarchy: Option<&crate::id::NodeHierarchy>,
1315    old_layout: Option<&BTreeMap<NodeId, LogicalRect>>,
1316    new_layout: Option<&BTreeMap<NodeId, LogicalRect>>,
1317    timestamp: Instant,
1318) -> Vec<SyntheticEvent> {
1319    let old_nodes = collect_node_ids(old_hierarchy);
1320    let new_nodes = collect_node_ids(new_hierarchy);
1321
1322    let mut events = Vec::new();
1323
1324    // Mount events: nodes in new but not in old
1325    if let Some(layout) = new_layout {
1326        for &node_id in new_nodes.difference(&old_nodes) {
1327            events.push(create_mount_event(node_id, new_dom_id, layout, &timestamp));
1328        }
1329    }
1330
1331    // Unmount events: nodes in old but not in new
1332    if let Some(layout) = old_layout {
1333        for &node_id in old_nodes.difference(&new_nodes) {
1334            events.push(create_unmount_event(
1335                node_id, old_dom_id, layout, &timestamp,
1336            ));
1337        }
1338    }
1339
1340    // Resize events: nodes in both with changed bounds
1341    if let (Some(old_l), Some(new_l)) = (old_layout, new_layout) {
1342        for &node_id in old_nodes.intersection(&new_nodes) {
1343            if let Some(ev) = create_resize_event(node_id, new_dom_id, old_l, new_l, &timestamp) {
1344                events.push(ev);
1345            }
1346        }
1347    }
1348
1349    events
1350}
1351
1352fn collect_node_ids(hierarchy: Option<&crate::id::NodeHierarchy>) -> BTreeSet<NodeId> {
1353    hierarchy
1354        .map(|h| h.as_ref().linear_iter().collect())
1355        .unwrap_or_default()
1356}
1357
1358fn create_lifecycle_event(
1359    event_type: EventType,
1360    node_id: NodeId,
1361    dom_id: DomId,
1362    timestamp: &Instant,
1363    data: LifecycleEventData,
1364) -> SyntheticEvent {
1365    let dom_node_id = DomNodeId {
1366        dom: dom_id,
1367        node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
1368    };
1369    SyntheticEvent {
1370        event_type,
1371        source: EventSource::Lifecycle,
1372        phase: EventPhase::Target,
1373        target: dom_node_id,
1374        current_target: dom_node_id,
1375        timestamp: timestamp.clone(),
1376        data: EventData::Lifecycle(data),
1377        stopped: false,
1378        stopped_immediate: false,
1379        prevented_default: false,
1380    }
1381}
1382
1383fn create_mount_event(
1384    node_id: NodeId,
1385    dom_id: DomId,
1386    layout: &BTreeMap<NodeId, LogicalRect>,
1387    timestamp: &Instant,
1388) -> SyntheticEvent {
1389    let current_bounds = layout.get(&node_id).copied().unwrap_or(LogicalRect::zero());
1390    create_lifecycle_event(
1391        EventType::Mount,
1392        node_id,
1393        dom_id,
1394        timestamp,
1395        LifecycleEventData {
1396            reason: LifecycleReason::InitialMount,
1397            previous_bounds: None,
1398            current_bounds,
1399        },
1400    )
1401}
1402
1403fn create_unmount_event(
1404    node_id: NodeId,
1405    dom_id: DomId,
1406    layout: &BTreeMap<NodeId, LogicalRect>,
1407    timestamp: &Instant,
1408) -> SyntheticEvent {
1409    let previous_bounds = layout.get(&node_id).copied().unwrap_or(LogicalRect::zero());
1410    create_lifecycle_event(
1411        EventType::Unmount,
1412        node_id,
1413        dom_id,
1414        timestamp,
1415        LifecycleEventData {
1416            reason: LifecycleReason::Unmount,
1417            previous_bounds: Some(previous_bounds),
1418            current_bounds: LogicalRect::zero(),
1419        },
1420    )
1421}
1422
1423fn create_resize_event(
1424    node_id: NodeId,
1425    dom_id: DomId,
1426    old_layout: &BTreeMap<NodeId, LogicalRect>,
1427    new_layout: &BTreeMap<NodeId, LogicalRect>,
1428    timestamp: &Instant,
1429) -> Option<SyntheticEvent> {
1430    let old_bounds = *old_layout.get(&node_id)?;
1431    let new_bounds = *new_layout.get(&node_id)?;
1432
1433    if old_bounds.size == new_bounds.size {
1434        return None;
1435    }
1436
1437    Some(create_lifecycle_event(
1438        EventType::Resize,
1439        node_id,
1440        dom_id,
1441        timestamp,
1442        LifecycleEventData {
1443            reason: LifecycleReason::Resize,
1444            previous_bounds: Some(old_bounds),
1445            current_bounds: new_bounds,
1446        },
1447    ))
1448}
1449
1450/// Result of lifecycle event detection with reconciliation.
1451///
1452/// Contains both the generated lifecycle events and a mapping from old to new
1453/// node IDs for state migration (focus, scroll, etc.).
1454#[derive(Debug, Clone, Default)]
1455pub struct LifecycleEventResult {
1456    /// Lifecycle events (Mount, Unmount, Resize, Update)
1457    pub events: Vec<SyntheticEvent>,
1458    /// Maps old NodeId -> new NodeId for matched nodes.
1459    /// Use this to migrate focus, scroll state, and other node-specific state.
1460    pub node_id_mapping: crate::OrderedMap<NodeId, NodeId>,
1461}
1462
1463/// Detect lifecycle events using reconciliation with stable keys and content hashing.
1464///
1465/// This is the advanced lifecycle detection that can correctly identify:
1466/// - **Moves**: When a node changes position but keeps its identity (via key or hash)
1467/// - **Mounts**: When a new node appears
1468/// - **Unmounts**: When an existing node disappears
1469/// - **Resizes**: When a node's layout bounds change
1470/// - **Updates**: When a keyed node's content changes
1471///
1472/// The reconciliation strategy is:
1473/// 1. **Stable Key Match:** Nodes with `.with_reconciliation_key()` are matched by key (O(1))
1474/// 2. **Hash Match:** Nodes without keys are matched by content hash (enables reorder detection)
1475/// 3. **Fallback:** Unmatched nodes generate Mount/Unmount events
1476///
1477/// # Arguments
1478/// * `dom_id` - The DOM identifier
1479/// * `old_node_data` - Node data from the previous frame
1480/// * `new_node_data` - Node data from the current frame
1481/// * `old_layout` - Layout bounds from the previous frame
1482/// * `new_layout` - Layout bounds from the current frame
1483/// * `timestamp` - Current timestamp for events
1484///
1485/// # Returns
1486/// A `LifecycleEventResult` containing:
1487/// - `events`: Lifecycle events to dispatch
1488/// - `node_id_mapping`: Mapping from old to new NodeIds for state migration
1489///
1490/// # Example
1491/// ```rust,ignore
1492/// let result = detect_lifecycle_events_with_reconciliation(
1493///     dom_id,
1494///     &old_node_data,
1495///     &new_node_data,
1496///     &old_layout,
1497///     &new_layout,
1498///     timestamp,
1499/// );
1500///
1501/// // Dispatch lifecycle events
1502/// for event in result.events {
1503///     dispatch_event(event);
1504/// }
1505///
1506/// // Migrate focus to new node ID
1507/// if let Some(focused) = focus_manager.focused_node {
1508///     if let Some(&new_id) = result.node_id_mapping.get(&focused) {
1509///         focus_manager.focused_node = Some(new_id);
1510///     } else {
1511///         // Focused node was unmounted
1512///         focus_manager.focused_node = None;
1513///     }
1514/// }
1515/// ```
1516pub fn detect_lifecycle_events_with_reconciliation(
1517    dom_id: DomId,
1518    old_node_data: &[crate::dom::NodeData],
1519    new_node_data: &[crate::dom::NodeData],
1520    old_hierarchy: &[crate::styled_dom::NodeHierarchyItem],
1521    new_hierarchy: &[crate::styled_dom::NodeHierarchyItem],
1522    old_layout: &crate::OrderedMap<NodeId, LogicalRect>,
1523    new_layout: &crate::OrderedMap<NodeId, LogicalRect>,
1524    timestamp: Instant,
1525) -> LifecycleEventResult {
1526    let diff_result = crate::diff::reconcile_dom(
1527        old_node_data,
1528        new_node_data,
1529        old_hierarchy,
1530        new_hierarchy,
1531        old_layout,
1532        new_layout,
1533        dom_id,
1534        timestamp,
1535    );
1536
1537    LifecycleEventResult {
1538        events: diff_result.events,
1539        node_id_mapping: crate::diff::create_migration_map(&diff_result.node_moves),
1540    }
1541}
1542
1543/// Event filter that only fires when an element is hovered over.
1544#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1545#[repr(C)]
1546pub enum HoverEventFilter {
1547    /// Mouse moved over the hovered element
1548    MouseOver,
1549    /// Any mouse button pressed on the hovered element
1550    MouseDown,
1551    /// Left mouse button pressed on the hovered element
1552    LeftMouseDown,
1553    /// Right mouse button pressed on the hovered element
1554    RightMouseDown,
1555    /// Middle mouse button pressed on the hovered element
1556    MiddleMouseDown,
1557    /// Any mouse button released on the hovered element
1558    MouseUp,
1559    /// Left mouse button released on the hovered element
1560    LeftMouseUp,
1561    /// Right mouse button released on the hovered element
1562    RightMouseUp,
1563    /// Middle mouse button released on the hovered element
1564    MiddleMouseUp,
1565    /// Mouse entered the hovered element bounds
1566    MouseEnter,
1567    /// Mouse left the hovered element bounds
1568    MouseLeave,
1569    /// Scroll event on the hovered element
1570    Scroll,
1571    /// Scroll started on the hovered element
1572    ScrollStart,
1573    /// Scroll ended on the hovered element
1574    ScrollEnd,
1575    /// Text input received while element is hovered
1576    TextInput,
1577    /// Virtual key pressed while element is hovered
1578    VirtualKeyDown,
1579    /// Virtual key released while element is hovered
1580    VirtualKeyUp,
1581    /// File is being hovered over the element
1582    HoveredFile,
1583    /// File was dropped onto the element
1584    DroppedFile,
1585    /// File hover was cancelled
1586    HoveredFileCancelled,
1587    /// Touch started on the hovered element
1588    TouchStart,
1589    /// Touch moved on the hovered element
1590    TouchMove,
1591    /// Touch ended on the hovered element
1592    TouchEnd,
1593    /// Touch was cancelled on the hovered element
1594    TouchCancel,
1595    /// Pen/stylus made contact on the hovered element
1596    PenDown,
1597    /// Pen/stylus moved while in contact on the hovered element
1598    PenMove,
1599    /// Pen/stylus lifted from the hovered element
1600    PenUp,
1601    /// Pen/stylus entered proximity of the hovered element
1602    PenEnter,
1603    /// Pen/stylus left proximity of the hovered element
1604    PenLeave,
1605    /// Apple Pencil 2 / Surface Slim Pen 2 barrel squeeze on the hovered
1606    /// element. Fires once per squeeze. The matching W3C primitive is the
1607    /// `PointerEvent` with `pointerType: "pen"` and a transient
1608    /// `tangentialPressure` spike — most apps tie a tool-switch to it.
1609    PenSqueeze,
1610    /// Apple Pencil 2 side double-tap on the hovered element. Fires once
1611    /// per gesture. Usually mapped to "undo" or "switch eraser".
1612    PenDoubleTap,
1613    /// Pen/stylus is hovering above the hovered element (in proximity,
1614    /// not in contact). Continuous: fires per pen-axis update while the
1615    /// stylus is held above the surface. Maps to W3C
1616    /// `PointerEvent('pointermove')` with `buttons: 0` and
1617    /// `pointerType: 'pen'`.
1618    PenHover,
1619    /// New GPS / network location fix arrived for a `GeolocationProbe`
1620    /// in this node's subtree. Payload accessor:
1621    /// `CallbackInfo::get_geolocation_fix()`.
1622    GeolocationFix,
1623    /// Native geolocation subscription errored / was revoked /
1624    /// timed out.
1625    GeolocationError,
1626    /// A motion-sensor reading changed (P6). Window-level mirror:
1627    /// `WindowEventFilter::SensorChanged`. Read via `get_sensor_reading`.
1628    SensorChanged,
1629    /// A gamepad's state changed / it (dis)connected (P6). Read via
1630    /// `get_primary_gamepad` / `get_gamepad_state`.
1631    GamepadInput,
1632    /// Drag started on the hovered element
1633    DragStart,
1634    /// Drag in progress on the hovered element
1635    Drag,
1636    /// Drag ended on the hovered element
1637    DragEnd,
1638    /// Dragged element entered this element (drop target)
1639    DragEnter,
1640    /// Dragged element is over this element (drop target, fires continuously)
1641    DragOver,
1642    /// Dragged element left this element (drop target)
1643    DragLeave,
1644    /// Element was dropped on this element (drop target)
1645    Drop,
1646    /// Double-click detected on the hovered element
1647    DoubleClick,
1648    /// Long press detected on the hovered element
1649    LongPress,
1650    /// Swipe left gesture on the hovered element
1651    SwipeLeft,
1652    /// Swipe right gesture on the hovered element
1653    SwipeRight,
1654    /// Swipe up gesture on the hovered element
1655    SwipeUp,
1656    /// Swipe down gesture on the hovered element
1657    SwipeDown,
1658    /// Pinch-in (zoom out) gesture on the hovered element
1659    PinchIn,
1660    /// Pinch-out (zoom in) gesture on the hovered element
1661    PinchOut,
1662    /// Clockwise rotation gesture on the hovered element
1663    RotateClockwise,
1664    /// Counter-clockwise rotation gesture on the hovered element
1665    RotateCounterClockwise,
1666
1667    // W3C MouseOut event (bubbling version of MouseLeave)
1668    /// Mouse left the element OR moved to a child element (W3C `mouseout`, bubbles)
1669    MouseOut,
1670
1671    // W3C Focus events (bubbling versions)
1672    /// Focus is about to move INTO this element or a descendant (W3C `focusin`, bubbles)
1673    FocusIn,
1674    /// Focus is about to move OUT of this element or a descendant (W3C `focusout`, bubbles)
1675    FocusOut,
1676
1677    // IME Composition events
1678    /// IME composition started (W3C `compositionstart`)
1679    CompositionStart,
1680    /// IME composition updated (W3C `compositionupdate`)
1681    CompositionUpdate,
1682    /// IME composition ended (W3C `compositionend`)
1683    CompositionEnd,
1684
1685    // Internal System Events (not exposed to user callbacks)
1686    #[doc(hidden)]
1687    /// Internal: Single click for text cursor placement
1688    SystemTextSingleClick,
1689    #[doc(hidden)]
1690    /// Internal: Double click for word selection
1691    SystemTextDoubleClick,
1692    #[doc(hidden)]
1693    /// Internal: Triple click for paragraph/line selection
1694    SystemTextTripleClick,
1695}
1696
1697impl HoverEventFilter {
1698    /// Check if this is an internal system event that should not be exposed to user callbacks
1699    pub const fn is_system_internal(&self) -> bool {
1700        matches!(
1701            self,
1702            HoverEventFilter::SystemTextSingleClick
1703                | HoverEventFilter::SystemTextDoubleClick
1704                | HoverEventFilter::SystemTextTripleClick
1705        )
1706    }
1707
1708    pub fn to_focus_event_filter(&self) -> Option<FocusEventFilter> {
1709        match self {
1710            HoverEventFilter::MouseOver => Some(FocusEventFilter::MouseOver),
1711            HoverEventFilter::MouseDown => Some(FocusEventFilter::MouseDown),
1712            HoverEventFilter::LeftMouseDown => Some(FocusEventFilter::LeftMouseDown),
1713            HoverEventFilter::RightMouseDown => Some(FocusEventFilter::RightMouseDown),
1714            HoverEventFilter::MiddleMouseDown => Some(FocusEventFilter::MiddleMouseDown),
1715            HoverEventFilter::MouseUp => Some(FocusEventFilter::MouseUp),
1716            HoverEventFilter::LeftMouseUp => Some(FocusEventFilter::LeftMouseUp),
1717            HoverEventFilter::RightMouseUp => Some(FocusEventFilter::RightMouseUp),
1718            HoverEventFilter::MiddleMouseUp => Some(FocusEventFilter::MiddleMouseUp),
1719            HoverEventFilter::MouseEnter => Some(FocusEventFilter::MouseEnter),
1720            HoverEventFilter::MouseLeave => Some(FocusEventFilter::MouseLeave),
1721            HoverEventFilter::Scroll => Some(FocusEventFilter::Scroll),
1722            HoverEventFilter::ScrollStart => Some(FocusEventFilter::ScrollStart),
1723            HoverEventFilter::ScrollEnd => Some(FocusEventFilter::ScrollEnd),
1724            HoverEventFilter::TextInput => Some(FocusEventFilter::TextInput),
1725            HoverEventFilter::VirtualKeyDown => Some(FocusEventFilter::VirtualKeyDown),
1726            HoverEventFilter::VirtualKeyUp => Some(FocusEventFilter::VirtualKeyUp),
1727            HoverEventFilter::HoveredFile => None,
1728            HoverEventFilter::DroppedFile => None,
1729            HoverEventFilter::HoveredFileCancelled => None,
1730            HoverEventFilter::TouchStart => None,
1731            HoverEventFilter::TouchMove => None,
1732            HoverEventFilter::TouchEnd => None,
1733            HoverEventFilter::TouchCancel => None,
1734            HoverEventFilter::PenDown => Some(FocusEventFilter::PenDown),
1735            HoverEventFilter::PenMove => Some(FocusEventFilter::PenMove),
1736            HoverEventFilter::PenUp => Some(FocusEventFilter::PenUp),
1737            HoverEventFilter::PenEnter => None,
1738            HoverEventFilter::PenLeave => None,
1739            HoverEventFilter::PenSqueeze => None,
1740            HoverEventFilter::PenDoubleTap => None,
1741            HoverEventFilter::PenHover => None,
1742            HoverEventFilter::GeolocationFix => None,
1743            HoverEventFilter::GeolocationError => None,
1744            HoverEventFilter::SensorChanged => None,
1745            HoverEventFilter::GamepadInput => None,
1746            HoverEventFilter::DragStart => Some(FocusEventFilter::DragStart),
1747            HoverEventFilter::Drag => Some(FocusEventFilter::Drag),
1748            HoverEventFilter::DragEnd => Some(FocusEventFilter::DragEnd),
1749            HoverEventFilter::DragEnter => Some(FocusEventFilter::DragEnter),
1750            HoverEventFilter::DragOver => Some(FocusEventFilter::DragOver),
1751            HoverEventFilter::DragLeave => Some(FocusEventFilter::DragLeave),
1752            HoverEventFilter::Drop => Some(FocusEventFilter::Drop),
1753            HoverEventFilter::DoubleClick => Some(FocusEventFilter::DoubleClick),
1754            HoverEventFilter::LongPress => Some(FocusEventFilter::LongPress),
1755            HoverEventFilter::SwipeLeft => Some(FocusEventFilter::SwipeLeft),
1756            HoverEventFilter::SwipeRight => Some(FocusEventFilter::SwipeRight),
1757            HoverEventFilter::SwipeUp => Some(FocusEventFilter::SwipeUp),
1758            HoverEventFilter::SwipeDown => Some(FocusEventFilter::SwipeDown),
1759            HoverEventFilter::PinchIn => Some(FocusEventFilter::PinchIn),
1760            HoverEventFilter::PinchOut => Some(FocusEventFilter::PinchOut),
1761            HoverEventFilter::RotateClockwise => Some(FocusEventFilter::RotateClockwise),
1762            HoverEventFilter::RotateCounterClockwise => {
1763                Some(FocusEventFilter::RotateCounterClockwise)
1764            }
1765            HoverEventFilter::MouseOut => Some(FocusEventFilter::MouseLeave), // mouseout → closest focus equivalent
1766            HoverEventFilter::FocusIn => Some(FocusEventFilter::FocusIn),
1767            HoverEventFilter::FocusOut => Some(FocusEventFilter::FocusOut),
1768            HoverEventFilter::CompositionStart => Some(FocusEventFilter::CompositionStart),
1769            HoverEventFilter::CompositionUpdate => Some(FocusEventFilter::CompositionUpdate),
1770            HoverEventFilter::CompositionEnd => Some(FocusEventFilter::CompositionEnd),
1771            // System internal events - don't convert to focus events
1772            HoverEventFilter::SystemTextSingleClick => None,
1773            HoverEventFilter::SystemTextDoubleClick => None,
1774            HoverEventFilter::SystemTextTripleClick => None,
1775        }
1776    }
1777}
1778
1779/// Event filter similar to `HoverEventFilter` that only fires when the element is focused.
1780///
1781/// **Important**: In order for this to fire, the item must have a `tabindex` attribute
1782/// (to indicate that the item is focus-able).
1783#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1784#[repr(C)]
1785pub enum FocusEventFilter {
1786    /// Mouse moved over the focused element
1787    MouseOver,
1788    /// Any mouse button pressed on the focused element
1789    MouseDown,
1790    /// Left mouse button pressed on the focused element
1791    LeftMouseDown,
1792    /// Right mouse button pressed on the focused element
1793    RightMouseDown,
1794    /// Middle mouse button pressed on the focused element
1795    MiddleMouseDown,
1796    /// Any mouse button released on the focused element
1797    MouseUp,
1798    /// Left mouse button released on the focused element
1799    LeftMouseUp,
1800    /// Right mouse button released on the focused element
1801    RightMouseUp,
1802    /// Middle mouse button released on the focused element
1803    MiddleMouseUp,
1804    /// Mouse entered the focused element bounds
1805    MouseEnter,
1806    /// Mouse left the focused element bounds
1807    MouseLeave,
1808    /// Scroll event on the focused element
1809    Scroll,
1810    /// Scroll started on the focused element
1811    ScrollStart,
1812    /// Scroll ended on the focused element
1813    ScrollEnd,
1814    /// Text input received while element is focused
1815    TextInput,
1816    /// Virtual key pressed while element is focused
1817    VirtualKeyDown,
1818    /// Virtual key released while element is focused
1819    VirtualKeyUp,
1820    /// Element received keyboard focus
1821    FocusReceived,
1822    /// Element lost keyboard focus
1823    FocusLost,
1824    /// Pen/stylus made contact on the focused element
1825    PenDown,
1826    /// Pen/stylus moved while in contact on the focused element
1827    PenMove,
1828    /// Pen/stylus lifted from the focused element
1829    PenUp,
1830    /// Drag started on the focused element
1831    DragStart,
1832    /// Drag in progress on the focused element
1833    Drag,
1834    /// Drag ended on the focused element
1835    DragEnd,
1836    /// Dragged element entered this focused element (drop target)
1837    DragEnter,
1838    /// Dragged element is over this focused element (drop target)
1839    DragOver,
1840    /// Dragged element left this focused element (drop target)
1841    DragLeave,
1842    /// Element was dropped on this focused element (drop target)
1843    Drop,
1844    /// Double-click detected on the focused element
1845    DoubleClick,
1846    /// Long press detected on the focused element
1847    LongPress,
1848    /// Swipe left gesture on the focused element
1849    SwipeLeft,
1850    /// Swipe right gesture on the focused element
1851    SwipeRight,
1852    /// Swipe up gesture on the focused element
1853    SwipeUp,
1854    /// Swipe down gesture on the focused element
1855    SwipeDown,
1856    /// Pinch-in (zoom out) gesture on the focused element
1857    PinchIn,
1858    /// Pinch-out (zoom in) gesture on the focused element
1859    PinchOut,
1860    /// Clockwise rotation gesture on the focused element
1861    RotateClockwise,
1862    /// Counter-clockwise rotation gesture on the focused element
1863    RotateCounterClockwise,
1864
1865    // W3C Focus events (bubbling versions, fires on focused element when focus changes)
1866    /// Focus moved into this element or a descendant (W3C `focusin`)
1867    FocusIn,
1868    /// Focus moved out of this element or a descendant (W3C `focusout`)
1869    FocusOut,
1870
1871    // IME Composition events
1872    /// IME composition started (W3C `compositionstart`)
1873    CompositionStart,
1874    /// IME composition updated (W3C `compositionupdate`)
1875    CompositionUpdate,
1876    /// IME composition ended (W3C `compositionend`)
1877    CompositionEnd,
1878}
1879
1880/// Event filter that fires when any action fires on the entire window
1881/// (regardless of whether any element is hovered or focused over).
1882#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1883#[repr(C)]
1884pub enum WindowEventFilter {
1885    /// Mouse moved anywhere in window
1886    MouseOver,
1887    /// Any mouse button pressed anywhere in window
1888    MouseDown,
1889    /// Left mouse button pressed anywhere in window
1890    LeftMouseDown,
1891    /// Right mouse button pressed anywhere in window
1892    RightMouseDown,
1893    /// Middle mouse button pressed anywhere in window
1894    MiddleMouseDown,
1895    /// Any mouse button released anywhere in window
1896    MouseUp,
1897    /// Left mouse button released anywhere in window
1898    LeftMouseUp,
1899    /// Right mouse button released anywhere in window
1900    RightMouseUp,
1901    /// Middle mouse button released anywhere in window
1902    MiddleMouseUp,
1903    /// Mouse entered the window
1904    MouseEnter,
1905    /// Mouse left the window
1906    MouseLeave,
1907    /// Scroll event anywhere in window
1908    Scroll,
1909    /// Scroll started anywhere in window
1910    ScrollStart,
1911    /// Scroll ended anywhere in window
1912    ScrollEnd,
1913    /// Text input received in window
1914    TextInput,
1915    /// Virtual key pressed in window
1916    VirtualKeyDown,
1917    /// Virtual key released in window
1918    VirtualKeyUp,
1919    /// File is being hovered over the window
1920    HoveredFile,
1921    /// File was dropped onto the window
1922    DroppedFile,
1923    /// File hover was cancelled
1924    HoveredFileCancelled,
1925    /// Window was resized
1926    Resized,
1927    /// Window was moved
1928    Moved,
1929    /// Touch started anywhere in window
1930    TouchStart,
1931    /// Touch moved anywhere in window
1932    TouchMove,
1933    /// Touch ended anywhere in window
1934    TouchEnd,
1935    /// Touch was cancelled
1936    TouchCancel,
1937    /// Window received focus
1938    FocusReceived,
1939    /// Window lost focus
1940    FocusLost,
1941    /// Window close was requested
1942    CloseRequested,
1943    /// System theme changed (light/dark mode)
1944    ThemeChanged,
1945    /// Window received OS-level focus
1946    WindowFocusReceived,
1947    /// Window lost OS-level focus
1948    WindowFocusLost,
1949    /// Pen/stylus made contact anywhere in window
1950    PenDown,
1951    /// Pen/stylus moved while in contact anywhere in window
1952    PenMove,
1953    /// Pen/stylus lifted anywhere in window
1954    PenUp,
1955    /// Pen/stylus entered window proximity
1956    PenEnter,
1957    /// Pen/stylus left window proximity
1958    PenLeave,
1959    /// Pen barrel-squeeze gesture fired in the window. See
1960    /// [`HoverEventFilter::PenSqueeze`].
1961    PenSqueeze,
1962    /// Pen side double-tap gesture fired in the window. See
1963    /// [`HoverEventFilter::PenDoubleTap`].
1964    PenDoubleTap,
1965    /// Pen hover in the window (in proximity, not in contact). See
1966    /// [`HoverEventFilter::PenHover`].
1967    PenHover,
1968    /// New GPS / network location fix arrived. Payload accessor:
1969    /// `CallbackInfo::get_geolocation_fix()`. Window-level rather
1970    /// than per-node because the user's location isn't bound to any
1971    /// particular DOM node — but a node-level mirror
1972    /// (`HoverEventFilter::GeolocationFix`) fires on every
1973    /// `GeolocationProbe` in the tree as well, for the common
1974    /// "redraw this node when the location changes" pattern.
1975    GeolocationFix,
1976    /// Native geolocation subscription dropped or errored (signal
1977    /// lost, no provider, permission revoked mid-session).
1978    GeolocationError,
1979    /// A motion-sensor reading changed (P6). Fires window-level (the device
1980    /// isn't bound to a node); read via `CallbackInfo::get_sensor_reading`.
1981    SensorChanged,
1982    /// A gamepad's buttons / axes changed or it (dis)connected (P6); read via
1983    /// `get_primary_gamepad` / `get_gamepad_state`.
1984    GamepadInput,
1985    /// Drag started anywhere in window
1986    DragStart,
1987    /// Drag in progress anywhere in window
1988    Drag,
1989    /// Drag ended anywhere in window
1990    DragEnd,
1991    /// Dragged element entered a drop target in window
1992    DragEnter,
1993    /// Dragged element is over a drop target in window
1994    DragOver,
1995    /// Dragged element left a drop target in window
1996    DragLeave,
1997    /// Element was dropped on a drop target in window
1998    Drop,
1999    /// Double-click detected anywhere in window
2000    DoubleClick,
2001    /// Long press detected anywhere in window
2002    LongPress,
2003    /// Swipe left gesture anywhere in window
2004    SwipeLeft,
2005    /// Swipe right gesture anywhere in window
2006    SwipeRight,
2007    /// Swipe up gesture anywhere in window
2008    SwipeUp,
2009    /// Swipe down gesture anywhere in window
2010    SwipeDown,
2011    /// Pinch-in (zoom out) gesture anywhere in window
2012    PinchIn,
2013    /// Pinch-out (zoom in) gesture anywhere in window
2014    PinchOut,
2015    /// Clockwise rotation gesture anywhere in window
2016    RotateClockwise,
2017    /// Counter-clockwise rotation gesture anywhere in window
2018    RotateCounterClockwise,
2019    /// The window's DPI scale factor changed (e.g., moved to a monitor with
2020    /// different scaling). The new DPI is available via `CallbackInfo::get_hidpi_factor()`.
2021    DpiChanged,
2022    /// The window moved to a different monitor. The new monitor is available
2023    /// via `CallbackInfo::get_current_monitor()`.
2024    MonitorChanged,
2025}
2026
2027impl WindowEventFilter {
2028    pub fn to_hover_event_filter(&self) -> Option<HoverEventFilter> {
2029        match self {
2030            WindowEventFilter::MouseOver => Some(HoverEventFilter::MouseOver),
2031            WindowEventFilter::MouseDown => Some(HoverEventFilter::MouseDown),
2032            WindowEventFilter::LeftMouseDown => Some(HoverEventFilter::LeftMouseDown),
2033            WindowEventFilter::RightMouseDown => Some(HoverEventFilter::RightMouseDown),
2034            WindowEventFilter::MiddleMouseDown => Some(HoverEventFilter::MiddleMouseDown),
2035            WindowEventFilter::MouseUp => Some(HoverEventFilter::MouseUp),
2036            WindowEventFilter::LeftMouseUp => Some(HoverEventFilter::LeftMouseUp),
2037            WindowEventFilter::RightMouseUp => Some(HoverEventFilter::RightMouseUp),
2038            WindowEventFilter::MiddleMouseUp => Some(HoverEventFilter::MiddleMouseUp),
2039            WindowEventFilter::Scroll => Some(HoverEventFilter::Scroll),
2040            WindowEventFilter::ScrollStart => Some(HoverEventFilter::ScrollStart),
2041            WindowEventFilter::ScrollEnd => Some(HoverEventFilter::ScrollEnd),
2042            WindowEventFilter::TextInput => Some(HoverEventFilter::TextInput),
2043            WindowEventFilter::VirtualKeyDown => Some(HoverEventFilter::VirtualKeyDown),
2044            WindowEventFilter::VirtualKeyUp => Some(HoverEventFilter::VirtualKeyUp),
2045            WindowEventFilter::HoveredFile => Some(HoverEventFilter::HoveredFile),
2046            WindowEventFilter::DroppedFile => Some(HoverEventFilter::DroppedFile),
2047            WindowEventFilter::HoveredFileCancelled => Some(HoverEventFilter::HoveredFileCancelled),
2048            // MouseEnter and MouseLeave on the **window** - does not mean a mouseenter
2049            // and a mouseleave on the hovered element
2050            WindowEventFilter::MouseEnter => None,
2051            WindowEventFilter::MouseLeave => None,
2052            WindowEventFilter::Resized => None,
2053            WindowEventFilter::Moved => None,
2054            WindowEventFilter::TouchStart => Some(HoverEventFilter::TouchStart),
2055            WindowEventFilter::TouchMove => Some(HoverEventFilter::TouchMove),
2056            WindowEventFilter::TouchEnd => Some(HoverEventFilter::TouchEnd),
2057            WindowEventFilter::TouchCancel => Some(HoverEventFilter::TouchCancel),
2058            WindowEventFilter::FocusReceived => None,
2059            WindowEventFilter::FocusLost => None,
2060            WindowEventFilter::CloseRequested => None,
2061            WindowEventFilter::ThemeChanged => None,
2062            WindowEventFilter::WindowFocusReceived => None, // specific to window!
2063            WindowEventFilter::WindowFocusLost => None,     // specific to window!
2064            WindowEventFilter::PenDown => Some(HoverEventFilter::PenDown),
2065            WindowEventFilter::PenMove => Some(HoverEventFilter::PenMove),
2066            WindowEventFilter::PenUp => Some(HoverEventFilter::PenUp),
2067            WindowEventFilter::PenEnter => Some(HoverEventFilter::PenEnter),
2068            WindowEventFilter::PenLeave => Some(HoverEventFilter::PenLeave),
2069            WindowEventFilter::PenSqueeze => Some(HoverEventFilter::PenSqueeze),
2070            WindowEventFilter::PenDoubleTap => Some(HoverEventFilter::PenDoubleTap),
2071            WindowEventFilter::PenHover => Some(HoverEventFilter::PenHover),
2072            WindowEventFilter::GeolocationFix => Some(HoverEventFilter::GeolocationFix),
2073            WindowEventFilter::GeolocationError => Some(HoverEventFilter::GeolocationError),
2074            WindowEventFilter::SensorChanged => Some(HoverEventFilter::SensorChanged),
2075            WindowEventFilter::GamepadInput => Some(HoverEventFilter::GamepadInput),
2076            WindowEventFilter::DragStart => Some(HoverEventFilter::DragStart),
2077            WindowEventFilter::Drag => Some(HoverEventFilter::Drag),
2078            WindowEventFilter::DragEnd => Some(HoverEventFilter::DragEnd),
2079            WindowEventFilter::DragEnter => Some(HoverEventFilter::DragEnter),
2080            WindowEventFilter::DragOver => Some(HoverEventFilter::DragOver),
2081            WindowEventFilter::DragLeave => Some(HoverEventFilter::DragLeave),
2082            WindowEventFilter::Drop => Some(HoverEventFilter::Drop),
2083            WindowEventFilter::DoubleClick => Some(HoverEventFilter::DoubleClick),
2084            WindowEventFilter::LongPress => Some(HoverEventFilter::LongPress),
2085            WindowEventFilter::SwipeLeft => Some(HoverEventFilter::SwipeLeft),
2086            WindowEventFilter::SwipeRight => Some(HoverEventFilter::SwipeRight),
2087            WindowEventFilter::SwipeUp => Some(HoverEventFilter::SwipeUp),
2088            WindowEventFilter::SwipeDown => Some(HoverEventFilter::SwipeDown),
2089            WindowEventFilter::PinchIn => Some(HoverEventFilter::PinchIn),
2090            WindowEventFilter::PinchOut => Some(HoverEventFilter::PinchOut),
2091            WindowEventFilter::RotateClockwise => Some(HoverEventFilter::RotateClockwise),
2092            WindowEventFilter::RotateCounterClockwise => {
2093                Some(HoverEventFilter::RotateCounterClockwise)
2094            }
2095            // Window-specific events with no hover equivalent
2096            WindowEventFilter::DpiChanged => None,
2097            WindowEventFilter::MonitorChanged => None,
2098        }
2099    }
2100}
2101
2102/// Defines events related to the lifecycle of a DOM node itself.
2103#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2104#[repr(C)]
2105pub enum ComponentEventFilter {
2106    /// Fired after the component is first mounted into the DOM.
2107    AfterMount,
2108    /// Fired just before the component is removed from the DOM.
2109    BeforeUnmount,
2110    /// Fired when the node's layout rectangle has been resized.
2111    NodeResized,
2112    /// Fired to trigger the default action for an accessibility component.
2113    DefaultAction,
2114    /// Fired when the component becomes selected.
2115    Selected,
2116    /// Fired when a keyed component's content has changed (props/state update).
2117    Updated,
2118}
2119
2120/// Defines application-level events not tied to a specific window or node.
2121#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2122#[repr(C)]
2123pub enum ApplicationEventFilter {
2124    /// Fired when a new hardware device is connected.
2125    DeviceConnected,
2126    /// Fired when a hardware device is disconnected.
2127    DeviceDisconnected,
2128    /// Fired when a new monitor/display is connected to the system.
2129    /// Callback receives updated monitor list via `CallbackInfo::get_monitors()`.
2130    MonitorConnected,
2131    /// Fired when a monitor/display is disconnected from the system.
2132    MonitorDisconnected,
2133}
2134
2135/// Sets the target for what events can reach the callbacks specifically.
2136///
2137/// This determines the condition under which an event is fired, such as whether
2138/// the node is hovered, focused, or if the event is window-global.
2139#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
2140#[repr(C, u8)]
2141pub enum EventFilter {
2142    /// Calls the attached callback when the mouse is actively over the
2143    /// given element.
2144    Hover(HoverEventFilter),
2145    /// Calls the attached callback when the element is currently focused.
2146    Focus(FocusEventFilter),
2147    /// Calls the callback when anything related to the window is happening.
2148    /// The "hit item" will be the root item of the DOM.
2149    /// For example, this can be useful for tracking the mouse position
2150    /// (in relation to the window). In difference to `Desktop`, this only
2151    /// fires when the window is focused.
2152    ///
2153    /// This can also be good for capturing controller input, touch input
2154    /// (i.e. global gestures that aren't attached to any component, but rather
2155    /// the "window" itself).
2156    Window(WindowEventFilter),
2157    /// API stub: Something happened with the node itself (node resized, created or removed).
2158    Component(ComponentEventFilter),
2159    /// Something happened with the application (started, shutdown, device plugged in).
2160    Application(ApplicationEventFilter),
2161}
2162
2163impl EventFilter {
2164    pub const fn is_focus_callback(&self) -> bool {
2165        match self {
2166            EventFilter::Focus(_) => true,
2167            _ => false,
2168        }
2169    }
2170    pub const fn is_window_callback(&self) -> bool {
2171        match self {
2172            EventFilter::Window(_) => true,
2173            _ => false,
2174        }
2175    }
2176}
2177
2178/// Creates a function inside an impl <enum type> block that returns a single
2179/// variant if the enum is that variant.
2180macro_rules! get_single_enum_type {
2181    ($fn_name:ident, $enum_name:ident:: $variant:ident($return_type:ty)) => {
2182        pub fn $fn_name(&self) -> Option<$return_type> {
2183            use self::$enum_name::*;
2184            match self {
2185                $variant(e) => Some(*e),
2186                _ => None,
2187            }
2188        }
2189    };
2190}
2191
2192impl EventFilter {
2193    get_single_enum_type!(as_hover_event_filter, EventFilter::Hover(HoverEventFilter));
2194    get_single_enum_type!(as_focus_event_filter, EventFilter::Focus(FocusEventFilter));
2195    get_single_enum_type!(
2196        as_window_event_filter,
2197        EventFilter::Window(WindowEventFilter)
2198    );
2199}
2200
2201/// Convert from `On` enum to `EventFilter`.
2202///
2203/// This determines which specific filter variant is used based on the event type.
2204/// For example, `On::TextInput` becomes a Focus event filter, while `On::VirtualKeyDown`
2205/// becomes a Window event filter (since it's global to the window).
2206impl From<On> for EventFilter {
2207    fn from(input: On) -> EventFilter {
2208        use crate::dom::On::*;
2209        match input {
2210            MouseOver => EventFilter::Hover(HoverEventFilter::MouseOver),
2211            MouseDown => EventFilter::Hover(HoverEventFilter::MouseDown),
2212            LeftMouseDown => EventFilter::Hover(HoverEventFilter::LeftMouseDown),
2213            MiddleMouseDown => EventFilter::Hover(HoverEventFilter::MiddleMouseDown),
2214            RightMouseDown => EventFilter::Hover(HoverEventFilter::RightMouseDown),
2215            MouseUp => EventFilter::Hover(HoverEventFilter::MouseUp),
2216            LeftMouseUp => EventFilter::Hover(HoverEventFilter::LeftMouseUp),
2217            MiddleMouseUp => EventFilter::Hover(HoverEventFilter::MiddleMouseUp),
2218            RightMouseUp => EventFilter::Hover(HoverEventFilter::RightMouseUp),
2219
2220            MouseEnter => EventFilter::Hover(HoverEventFilter::MouseEnter),
2221            MouseLeave => EventFilter::Hover(HoverEventFilter::MouseLeave),
2222            Scroll => EventFilter::Hover(HoverEventFilter::Scroll),
2223            TextInput => EventFilter::Focus(FocusEventFilter::TextInput), // focus!
2224            VirtualKeyDown => EventFilter::Window(WindowEventFilter::VirtualKeyDown), // window!
2225            VirtualKeyUp => EventFilter::Window(WindowEventFilter::VirtualKeyUp), // window!
2226            HoveredFile => EventFilter::Hover(HoverEventFilter::HoveredFile),
2227            DroppedFile => EventFilter::Hover(HoverEventFilter::DroppedFile),
2228            HoveredFileCancelled => EventFilter::Hover(HoverEventFilter::HoveredFileCancelled),
2229            FocusReceived => EventFilter::Focus(FocusEventFilter::FocusReceived), // focus!
2230            FocusLost => EventFilter::Focus(FocusEventFilter::FocusLost),         // focus!
2231
2232            // Accessibility events - treat as hover events (element-specific)
2233            Default => EventFilter::Hover(HoverEventFilter::MouseUp), // Default action = click
2234            Collapse => EventFilter::Hover(HoverEventFilter::MouseUp), // Collapse = click
2235            Expand => EventFilter::Hover(HoverEventFilter::MouseUp),  // Expand = click
2236            Increment => EventFilter::Hover(HoverEventFilter::MouseUp), // Increment = click
2237            Decrement => EventFilter::Hover(HoverEventFilter::MouseUp), // Decrement = click
2238        }
2239    }
2240}
2241
2242// Cross-Platform Event Dispatch System
2243// NOTE: The old dispatch_synthetic_events / CallbackTarget / CallbackToInvoke / EventDispatchResult
2244// pipeline has been removed. Event dispatch now goes through dispatch_events_propagated() in
2245// event_v2.rs which uses propagate_event() for W3C Capture→Target→Bubble propagation.
2246
2247/// Trait for managers to provide their pending events.
2248///
2249/// Each manager (TextInputManager, ScrollManager, etc.) implements this to
2250/// report what events occurred since the last frame. This enables a unified,
2251/// lazy event determination system.
2252pub trait EventProvider {
2253    /// Get all pending events from this manager.
2254    ///
2255    /// Events should include:
2256    ///
2257    /// - `target`: The DomNodeId that was affected
2258    /// - `event_type`: What happened (Input, Scroll, Focus, etc.)
2259    /// - `source`: EventSource::User for input, EventSource::Programmatic for API calls
2260    /// - `data`: Type-specific event data
2261    ///
2262    /// After calling this, the manager should mark events as "read" so they
2263    /// aren't returned again next frame.
2264    fn get_pending_events(&self, timestamp: Instant) -> Vec<SyntheticEvent>;
2265}
2266
2267/// Deduplicate synthetic events by (target node, event type).
2268///
2269/// Groups by (target.dom, target.node, event_type), keeping the latest timestamp.
2270pub fn deduplicate_synthetic_events(mut events: Vec<SyntheticEvent>) -> Vec<SyntheticEvent> {
2271    if events.len() <= 1 {
2272        return events;
2273    }
2274
2275    events.sort_by_key(|e| (e.target.dom, e.target.node, e.event_type));
2276
2277    // Coalesce consecutive events with same target and event_type
2278    let mut result = Vec::with_capacity(events.len());
2279    let mut iter = events.into_iter();
2280
2281    if let Some(mut prev) = iter.next() {
2282        for curr in iter {
2283            if prev.target == curr.target && prev.event_type == curr.event_type {
2284                // Keep the one with later timestamp
2285                prev = if curr.timestamp > prev.timestamp {
2286                    curr
2287                } else {
2288                    prev
2289                };
2290            } else {
2291                result.push(prev);
2292                prev = curr;
2293            }
2294        }
2295        result.push(prev);
2296    }
2297
2298    result
2299}
2300
2301
2302
2303/// Convert EventType to EventFilters (returns multiple filters for generic + specific events)
2304///
2305/// For mouse button events, returns both generic (MouseUp) AND button-specific (LeftMouseUp/RightMouseUp).
2306/// The button-specific filter is derived from the EventData::Mouse payload.
2307pub fn event_type_to_filters(event_type: EventType, event_data: &EventData) -> Vec<EventFilter> {
2308    use EventFilter as EF;
2309    use EventType as E;
2310    use FocusEventFilter as F;
2311    use HoverEventFilter as H;
2312    use WindowEventFilter as W;
2313
2314    // Helper: get the button-specific MouseDown filter from EventData
2315    let button_specific_down = || -> Option<EventFilter> {
2316        match event_data {
2317            EventData::Mouse(m) => match m.button {
2318                MouseButton::Left => Some(EF::Hover(H::LeftMouseDown)),
2319                MouseButton::Right => Some(EF::Hover(H::RightMouseDown)),
2320                MouseButton::Middle => Some(EF::Hover(H::MiddleMouseDown)),
2321                MouseButton::Other(_) => None, // no specific filter for other buttons
2322            },
2323            _ => Some(EF::Hover(H::LeftMouseDown)), // fallback
2324        }
2325    };
2326
2327    let button_specific_up = || -> Option<EventFilter> {
2328        match event_data {
2329            EventData::Mouse(m) => match m.button {
2330                MouseButton::Left => Some(EF::Hover(H::LeftMouseUp)),
2331                MouseButton::Right => Some(EF::Hover(H::RightMouseUp)),
2332                MouseButton::Middle => Some(EF::Hover(H::MiddleMouseUp)),
2333                MouseButton::Other(_) => None, // no specific filter for other buttons
2334            },
2335            _ => Some(EF::Hover(H::LeftMouseUp)), // fallback
2336        }
2337    };
2338
2339    match event_type {
2340        // Mouse button events - return BOTH generic and button-specific
2341        E::MouseDown => {
2342            let mut v = vec![EF::Hover(H::MouseDown)];
2343            if let Some(f) = button_specific_down() { v.push(f); }
2344            v
2345        }
2346        E::MouseUp => {
2347            let mut v = vec![EF::Hover(H::MouseUp)];
2348            if let Some(f) = button_specific_up() { v.push(f); }
2349            v
2350        }
2351
2352        // Click uses LeftMouseDown (W3C: click is left-button only)
2353        E::Click => vec![EF::Hover(H::LeftMouseDown)],
2354
2355        // Other mouse events
2356        E::MouseOver => vec![EF::Hover(H::MouseOver)],
2357        E::MouseEnter => vec![EF::Hover(H::MouseEnter)],
2358        E::MouseLeave => vec![EF::Hover(H::MouseLeave)],
2359        E::MouseOut => vec![EF::Hover(H::MouseOut)],
2360
2361        E::DoubleClick => vec![EF::Hover(H::DoubleClick), EF::Window(W::DoubleClick)],
2362        E::ContextMenu => vec![EF::Hover(H::RightMouseDown)],
2363
2364        // Keyboard events
2365        E::KeyDown => vec![EF::Focus(F::VirtualKeyDown)],
2366        E::KeyUp => vec![EF::Focus(F::VirtualKeyUp)],
2367        E::KeyPress => vec![EF::Focus(F::TextInput)],
2368
2369        // IME Composition events
2370        E::CompositionStart => vec![EF::Hover(H::CompositionStart), EF::Focus(F::CompositionStart)],
2371        E::CompositionUpdate => vec![EF::Hover(H::CompositionUpdate), EF::Focus(F::CompositionUpdate)],
2372        E::CompositionEnd => vec![EF::Hover(H::CompositionEnd), EF::Focus(F::CompositionEnd)],
2373
2374        // Focus events
2375        E::Focus => vec![EF::Focus(F::FocusReceived)],
2376        E::Blur => vec![EF::Focus(F::FocusLost)],
2377        E::FocusIn => vec![EF::Hover(H::FocusIn), EF::Focus(F::FocusIn)],
2378        E::FocusOut => vec![EF::Hover(H::FocusOut), EF::Focus(F::FocusOut)],
2379
2380        // Input events
2381        E::Input | E::Change => vec![EF::Focus(F::TextInput)],
2382
2383        // Scroll events
2384        E::Scroll | E::ScrollStart | E::ScrollEnd => vec![EF::Hover(H::Scroll)],
2385
2386        // Drag events
2387        E::DragStart => vec![EF::Hover(H::DragStart), EF::Window(W::DragStart)],
2388        E::Drag => vec![EF::Hover(H::Drag), EF::Window(W::Drag)],
2389        E::DragEnd => vec![EF::Hover(H::DragEnd), EF::Window(W::DragEnd)],
2390        E::DragEnter => vec![EF::Hover(H::DragEnter), EF::Window(W::DragEnter)],
2391        E::DragOver => vec![EF::Hover(H::DragOver), EF::Window(W::DragOver)],
2392        E::DragLeave => vec![EF::Hover(H::DragLeave), EF::Window(W::DragLeave)],
2393        E::Drop => vec![EF::Hover(H::Drop), EF::Window(W::Drop)],
2394
2395        // Touch events
2396        E::TouchStart => vec![EF::Hover(H::TouchStart)],
2397        E::TouchMove => vec![EF::Hover(H::TouchMove)],
2398        E::TouchEnd => vec![EF::Hover(H::TouchEnd)],
2399        E::TouchCancel => vec![EF::Hover(H::TouchCancel)],
2400
2401        // Window events
2402        E::WindowResize => vec![EF::Window(W::Resized)],
2403        E::WindowMove => vec![EF::Window(W::Moved)],
2404        E::WindowClose => vec![EF::Window(W::CloseRequested)],
2405        E::WindowFocusIn => vec![EF::Window(W::WindowFocusReceived)],
2406        E::WindowFocusOut => vec![EF::Window(W::WindowFocusLost)],
2407        E::ThemeChange => vec![EF::Window(W::ThemeChanged)],
2408        E::WindowDpiChanged => vec![EF::Window(W::DpiChanged)],
2409        E::WindowMonitorChanged => vec![EF::Window(W::MonitorChanged)],
2410
2411        // Application events
2412        E::MonitorConnected => vec![EF::Application(ApplicationEventFilter::MonitorConnected)],
2413        E::MonitorDisconnected => vec![EF::Application(ApplicationEventFilter::MonitorDisconnected)],
2414
2415        // File events
2416        E::FileHover => vec![EF::Hover(H::HoveredFile)],
2417        E::FileDrop => vec![EF::Hover(H::DroppedFile)],
2418        E::FileHoverCancel => vec![EF::Hover(H::HoveredFileCancelled)],
2419
2420        // Lifecycle events — dispatched on the target node via EventFilter::Component.
2421        // Both Mount and Unmount map to their respective Component filters so that
2422        // `.add_callback(EventFilter::Component(ComponentEventFilter::AfterMount))`
2423        // actually fires after reconcile_dom emits a SyntheticEvent{EventType::Mount,..}.
2424        E::Mount => vec![EF::Component(ComponentEventFilter::AfterMount)],
2425        E::Unmount => vec![EF::Component(ComponentEventFilter::BeforeUnmount)],
2426        E::Update => vec![EF::Component(ComponentEventFilter::Updated)],
2427        E::Resize => vec![EF::Component(ComponentEventFilter::NodeResized)],
2428
2429        // Hardware input-device events (P6) — node-level Hover mirror + the
2430        // window-level filter (the device isn't bound to a node).
2431        E::SensorChanged => vec![EF::Hover(H::SensorChanged), EF::Window(W::SensorChanged)],
2432        E::GamepadInput => vec![EF::Hover(H::GamepadInput), EF::Window(W::GamepadInput)],
2433
2434        // Unsupported events
2435        _ => vec![],
2436    }
2437}
2438
2439
2440
2441// Internal System Event Processing
2442
2443/// Framework-determined side effects (system changes).
2444///
2445/// Unlike `CallbackChange` (from user callbacks), these are determined by the
2446/// framework's event analysis: hit tests, gesture detection, focus rules,
2447/// text selection, keyboard shortcuts, etc.
2448///
2449/// Both `CallbackChange` (user) and `SystemChange` (framework) are processed
2450/// through exhaustive match on `PlatformWindowV2` — adding a new variant
2451/// causes a compile error in `apply_system_change()`.
2452#[derive(Debug, Clone, PartialEq)]
2453#[must_use = "SystemChange must be processed through apply_system_change()"]
2454pub enum SystemChange {
2455    // === Text Selection ===
2456
2457    /// Process a mouse click for text selection (single/double/triple click).
2458    TextSelectionClick {
2459        position: LogicalPosition,
2460        timestamp: Instant,
2461    },
2462    /// Extend text selection via mouse drag.
2463    TextSelectionDrag {
2464        start_position: LogicalPosition,
2465        current_position: LogicalPosition,
2466    },
2467    /// Unified selection operation: cursor movement, selection extension, or deletion.
2468    ///
2469    /// Replaces the old ArrowKeyNavigation and DeleteTextSelection variants.
2470    /// Every keyboard shortcut maps to a single SelectionOp — see its docs.
2471    ApplySelectionOp {
2472        target: DomNodeId,
2473        op: SelectionOp,
2474    },
2475
2476    // === Keyboard Shortcuts ===
2477
2478    /// Copy selected text to system clipboard (Ctrl+C / Cmd+C).
2479    CopyToClipboard,
2480    /// Cut selected text to clipboard and delete (Ctrl+X / Cmd+X).
2481    CutToClipboard { target: DomNodeId },
2482    /// Paste text from system clipboard at cursor (Ctrl+V / Cmd+V).
2483    PasteFromClipboard,
2484    /// Select all text in focused node (Ctrl+A / Cmd+A).
2485    SelectAllText,
2486    /// Undo last text edit (Ctrl+Z / Cmd+Z).
2487    UndoTextEdit { target: DomNodeId },
2488    /// Redo last undone edit (Ctrl+Y / Ctrl+Shift+Z / Cmd+Shift+Z).
2489    RedoTextEdit { target: DomNodeId },
2490
2491    // === Multi-Cursor ===
2492
2493    /// Add a cursor at the clicked position (Ctrl+Click).
2494    /// The position will be hit-tested to find the text cursor location.
2495    AddCursorAtClick {
2496        position: LogicalPosition,
2497    },
2498    /// Select the next occurrence of the current selection's text (Ctrl+D).
2499    /// If the primary selection is a cursor, expand it to the word first.
2500    SelectNextOccurrence {
2501        target: DomNodeId,
2502    },
2503
2504    // === Text Input ===
2505
2506    /// Apply pending text input from platform (keyboard/IME).
2507    ApplyPendingTextInput,
2508    /// Apply text changeset (incremental relayout).
2509    ApplyTextChangeset,
2510
2511    // === Drag & Drop ===
2512
2513    /// Activate node drag on a draggable element.
2514    ActivateNodeDrag {
2515        dom_id: crate::dom::DomId,
2516        node_id: crate::id::NodeId,
2517    },
2518    /// Activate window drag (CSD titlebar).
2519    ActivateWindowDrag,
2520    /// Set up drag visual state (:dragging pseudo-state, GPU transform key, DragDropManager sync).
2521    InitDragVisualState,
2522    /// Set :drag-over pseudo-state on a target node.
2523    SetDragOverState { target: DomNodeId, active: bool },
2524    /// Update current drop target in drag context.
2525    UpdateDropTarget { target: DomNodeId },
2526    /// Update GPU transform for active node drag.
2527    UpdateDragGpuTransform,
2528    /// End drag: clear pseudo-states, remove GPU keys, end drag session.
2529    DeactivateDrag,
2530
2531    // === Focus ===
2532
2533    /// Change focus to a new target (or clear focus if None).
2534    /// Handles: set_focused_node, apply_focus_restyle, scroll_node_into_view,
2535    /// cursor_blink_timer start/stop.
2536    SetFocus {
2537        new_focus: Option<DomNodeId>,
2538        old_focus: Option<DomNodeId>,
2539    },
2540    /// Clear all text selections.
2541    ClearAllSelections,
2542    /// Finalize pending focus changes (cursor initialization after layout).
2543    FinalizePendingFocusChanges,
2544
2545    // === Scroll ===
2546
2547    /// Scroll cursor/selection into view.
2548    ScrollSelectionIntoView,
2549    /// Scroll a specific node into view.
2550    ScrollNodeIntoView { target: DomNodeId },
2551    /// Scroll cursor into view after text input (needs relayout first).
2552    ScrollCursorIntoViewAfterTextInput,
2553
2554    // === Auto-Scroll Timer ===
2555
2556    /// Start auto-scroll timer for drag-to-scroll (60Hz).
2557    StartAutoScrollTimer,
2558    /// Cancel auto-scroll timer.
2559    StopAutoScrollTimer,
2560}
2561
2562impl_option!(
2563    SystemChange,
2564    OptionSystemChange,
2565    copy = false,
2566    clone = false,
2567    [Debug, Clone, PartialEq]
2568);
2569
2570impl_vec!(SystemChange, SystemChangeVec, SystemChangeVecDestructor, SystemChangeVecDestructorType, SystemChangeVecSlice, OptionSystemChange);
2571impl_vec_debug!(SystemChange, SystemChangeVec);
2572impl_vec_clone!(SystemChange, SystemChangeVec, SystemChangeVecDestructor);
2573impl_vec_partialeq!(SystemChange, SystemChangeVec);
2574
2575/// Result of pre-callback internal event filtering
2576#[derive(Debug, Clone, PartialEq)]
2577pub struct PreCallbackFilterResult {
2578    /// System changes to process BEFORE user callbacks
2579    pub system_changes: Vec<SystemChange>,
2580    /// Regular events that will be passed to user callbacks
2581    pub user_events: Vec<SyntheticEvent>,
2582}
2583
2584/// Flattened focus/selection state for the input interpreter (replaces trait objects).
2585#[derive(Debug, Clone)]
2586pub struct InputInterpreterState {
2587    pub focused_node: Option<DomNodeId>,
2588    pub click_count: u8,
2589    pub drag_start_position: Option<LogicalPosition>,
2590    pub has_selection: bool,
2591}
2592
2593/// All context needed by the input interpreter to map events to system changes.
2594///
2595/// Passed to the interpreter callback. Contains references to the current
2596/// events and window state. The interpreter reads this and returns system changes.
2597pub struct InputInterpreterInfo<'a> {
2598    pub events: &'a [SyntheticEvent],
2599    pub hit_test: Option<&'a FullHitTest>,
2600    pub keyboard_state: &'a crate::window::KeyboardState,
2601    pub mouse_state: &'a crate::window::MouseState,
2602    pub state: InputInterpreterState,
2603}
2604
2605/// The `extern "C"` callback type for the input interpreter.
2606///
2607/// The first `RefAny` is the user data (vim mode, repeat counter, etc.)
2608/// held in `InputInterpreterCallback.ctx`. The `*const ()` is an opaque
2609/// pointer to `InputInterpreterInfo` — callers use the safe wrapper
2610/// methods to access event data. Returns a `PreCallbackFilterResult`.
2611///
2612/// For C/Python: the trampoline extracts the foreign callable from `RefAny.ctx`.
2613/// For Rust: use `InputInterpreterCallback::from(fn_ptr)` which sets ctx=None.
2614pub type InputInterpreterCallbackType = extern "C" fn(
2615    crate::refany::RefAny,
2616    *const InputInterpreterInfo<'static>,  // Opaque; actual lifetime managed by caller
2617) -> PreCallbackFilterResult;
2618
2619/// Configurable input interpreter callback.
2620///
2621/// Maps raw platform events + window state → semantic `SystemChange` actions.
2622/// The default (`default_input_interpreter`) handles standard desktop keybindings.
2623/// Replace this on `LayoutWindow` to implement vim, game controls, etc.
2624///
2625/// ## Pattern
2626/// - **Rust**: `InputInterpreterCallback::from(my_fn_ptr)` — `ctx` is None
2627/// - **Python/C**: Set `cb` to a trampoline, `ctx` to `RefAny` wrapping the foreign callable
2628#[repr(C)]
2629pub struct InputInterpreterCallback {
2630    pub cb: InputInterpreterCallbackType,
2631    pub ctx: crate::refany::OptionRefAny,
2632}
2633
2634impl_callback!(InputInterpreterCallback, InputInterpreterCallbackType);
2635
2636impl Default for InputInterpreterCallback {
2637    fn default() -> Self {
2638        Self {
2639            cb: default_input_interpreter_extern,
2640            ctx: crate::refany::OptionRefAny::None,
2641        }
2642    }
2643}
2644
2645/// The `extern "C"` callback type for the post-callback filter.
2646pub type PostFilterCallbackType = extern "C" fn(
2647    crate::refany::RefAny,
2648    bool,                    // prevent_default
2649    SystemChangeVecSlice,    // pre_changes (immutable slice)
2650    DomNodeId,               // old_focus (0xFFFF = None)
2651    DomNodeId,               // new_focus (0xFFFF = None)
2652) -> SystemChangeVec;
2653
2654/// Configurable post-callback filter.
2655#[repr(C)]
2656pub struct PostFilterCallback {
2657    pub cb: PostFilterCallbackType,
2658    pub ctx: crate::refany::OptionRefAny,
2659}
2660
2661impl_callback!(PostFilterCallback, PostFilterCallbackType);
2662
2663impl Default for PostFilterCallback {
2664    fn default() -> Self {
2665        Self {
2666            cb: default_post_filter_extern,
2667            ctx: crate::refany::OptionRefAny::None,
2668        }
2669    }
2670}
2671
2672// Keep simpler Rust fn pointer aliases for internal use
2673pub type InputInterpreterFn = fn(
2674    info: &InputInterpreterInfo,
2675) -> PreCallbackFilterResult;
2676
2677pub type PostFilterFn = fn(
2678    prevent_default: bool,
2679    pre_changes: &[SystemChange],
2680    old_focus: Option<DomNodeId>,
2681    new_focus: Option<DomNodeId>,
2682) -> Vec<SystemChange>;
2683
2684/// Mouse button state for drag tracking
2685#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2686pub struct MouseButtonState {
2687    pub left_down: bool,
2688    pub right_down: bool,
2689    pub middle_down: bool,
2690}
2691
2692/// Arrow key / cursor navigation directions
2693#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2694pub enum ArrowDirection {
2695    Left,
2696    Right,
2697    Up,
2698    Down,
2699    /// Home key: move to start of current line
2700    LineStart,
2701    /// End key: move to end of current line
2702    LineEnd,
2703    /// Ctrl+Home: move to start of document
2704    DocumentStart,
2705    /// Ctrl+End: move to end of document
2706    DocumentEnd,
2707}
2708
2709impl ArrowDirection {
2710    /// Map a `VirtualKeyCode` plus the `ctrl` modifier into an `ArrowDirection`.
2711    /// Returns `None` if the key is not a navigation key.
2712    pub fn from_key(vk: crate::window::VirtualKeyCode, ctrl: bool) -> Option<Self> {
2713        use crate::window::VirtualKeyCode::*;
2714        Some(match vk {
2715            Left => ArrowDirection::Left,
2716            Right => ArrowDirection::Right,
2717            Up => ArrowDirection::Up,
2718            Down => ArrowDirection::Down,
2719            Home if ctrl => ArrowDirection::DocumentStart,
2720            Home => ArrowDirection::LineStart,
2721            End if ctrl => ArrowDirection::DocumentEnd,
2722            End => ArrowDirection::LineEnd,
2723            _ => return None,
2724        })
2725    }
2726
2727    /// Convert to a `(SelectionDirection, SelectionStep)` pair for the
2728    /// selection-op interpreter. `ctrl` upgrades arrow keys to word jumps.
2729    pub fn to_selection(self, ctrl: bool) -> (SelectionDirection, SelectionStep) {
2730        match self {
2731            ArrowDirection::Left if ctrl => (SelectionDirection::Backward, SelectionStep::Word),
2732            ArrowDirection::Right if ctrl => (SelectionDirection::Forward, SelectionStep::Word),
2733            ArrowDirection::Left => (SelectionDirection::Backward, SelectionStep::Character),
2734            ArrowDirection::Right => (SelectionDirection::Forward, SelectionStep::Character),
2735            ArrowDirection::Up => (SelectionDirection::Backward, SelectionStep::VisualLine),
2736            ArrowDirection::Down => (SelectionDirection::Forward, SelectionStep::VisualLine),
2737            ArrowDirection::LineStart => (SelectionDirection::Backward, SelectionStep::Line),
2738            ArrowDirection::LineEnd => (SelectionDirection::Forward, SelectionStep::Line),
2739            ArrowDirection::DocumentStart => (SelectionDirection::Backward, SelectionStep::Document),
2740            ArrowDirection::DocumentEnd => (SelectionDirection::Forward, SelectionStep::Document),
2741        }
2742    }
2743}
2744
2745/// Direction of cursor movement or selection expansion.
2746#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2747#[repr(C)]
2748pub enum SelectionDirection {
2749    Forward,
2750    Backward,
2751}
2752
2753/// Granularity of cursor movement or selection expansion.
2754///
2755/// Combined with `SelectionDirection`, determines how far a cursor moves
2756/// or a selection expands. Reused for navigation, deletion, and visual
2757/// selection — a single code path for word boundaries etc.
2758#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2759#[repr(C)]
2760pub enum SelectionStep {
2761    /// One grapheme cluster (arrow keys, Backspace, Delete)
2762    Character,
2763    /// One word boundary (Ctrl+arrow, Ctrl+Backspace, Ctrl+Delete)
2764    Word,
2765    /// To line boundary (Home/End)
2766    Line,
2767    /// One visual line up/down (Up/Down arrows)
2768    VisualLine,
2769    /// To document boundary (Ctrl+Home/End)
2770    Document,
2771}
2772
2773/// What to do with the selection after moving.
2774#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2775#[repr(C)]
2776pub enum SelectionMode {
2777    /// Collapse selection to cursor, then move (plain arrow key).
2778    Move,
2779    /// Extend selection from anchor to new position (Shift+arrow).
2780    Extend,
2781    /// Expand cursor to range in the given direction, then delete the range
2782    /// (Backspace/Delete). If a range already exists, just delete it.
2783    Delete,
2784}
2785
2786/// A unified selection operation that replaces all cursor movement,
2787/// selection extension, and text deletion commands.
2788///
2789/// Every keyboard shortcut for cursor movement or deletion maps to this:
2790/// - Arrow Left = (Backward, Character, Move, 1)
2791/// - Shift+Right = (Forward, Character, Extend, 1)
2792/// - Ctrl+Backspace = (Backward, Word, Delete, 1)
2793/// - Home = (Backward, Line, Move, 1)
2794/// - Ctrl+End = (Forward, Document, Move, 1)
2795///
2796/// The `repeat` field enables vim-style commands: 3w = (Forward, Word, Move, 3).
2797#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2798#[repr(C)]
2799pub struct SelectionOp {
2800    pub direction: SelectionDirection,
2801    pub step: SelectionStep,
2802    pub mode: SelectionMode,
2803    pub repeat: usize,
2804}
2805
2806impl SelectionOp {
2807    pub fn new(direction: SelectionDirection, step: SelectionStep, mode: SelectionMode) -> Self {
2808        Self { direction, step, mode, repeat: 1 }
2809    }
2810}
2811
2812/// Keyboard shortcuts for text editing
2813#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2814pub enum KeyboardShortcut {
2815    Copy,      // Ctrl+C
2816    Cut,       // Ctrl+X
2817    Paste,     // Ctrl+V
2818    SelectAll, // Ctrl+A
2819    Undo,      // Ctrl+Z
2820    Redo,      // Ctrl+Y or Ctrl+Shift+Z
2821}
2822
2823impl KeyboardShortcut {
2824    /// Map a `(VirtualKeyCode, ctrl, shift)` triple to a text-editing shortcut.
2825    /// Returns `None` if the key combination is not a recognized shortcut or
2826    /// if `ctrl` is not held.
2827    pub fn from_key(vk: crate::window::VirtualKeyCode, ctrl: bool, shift: bool) -> Option<Self> {
2828        use crate::window::VirtualKeyCode::*;
2829        if !ctrl {
2830            return None;
2831        }
2832        Some(match vk {
2833            C => KeyboardShortcut::Copy,
2834            X => KeyboardShortcut::Cut,
2835            V => KeyboardShortcut::Paste,
2836            A => KeyboardShortcut::SelectAll,
2837            Z if shift => KeyboardShortcut::Redo,
2838            Z => KeyboardShortcut::Undo,
2839            Y => KeyboardShortcut::Redo,
2840            _ => return None,
2841        })
2842    }
2843}
2844
2845/// Default input interpreter: standard desktop keybindings.
2846///
2847/// This is the default `InputInterpreterFn` that handles arrow keys, Home/End,
2848/// Backspace/Delete, Ctrl+C/V/A/Z, mouse clicks, and drag selection.
2849/// Replace it on `LayoutWindow` to implement vim, game controls, etc.
2850/// `extern "C"` trampoline for `default_input_interpreter`.
2851pub extern "C" fn default_input_interpreter_extern(
2852    _user_data: crate::refany::RefAny,
2853    info_ptr: *const InputInterpreterInfo<'static>,
2854) -> PreCallbackFilterResult {
2855    if info_ptr.is_null() {
2856        return PreCallbackFilterResult {
2857            system_changes: Vec::new(),
2858            user_events: Vec::new(),
2859        };
2860    }
2861    let info = unsafe { &*info_ptr };
2862    default_input_interpreter(info)
2863}
2864
2865/// `extern "C"` trampoline for `default_post_filter`.
2866pub extern "C" fn default_post_filter_extern(
2867    _user_data: crate::refany::RefAny,
2868    prevent_default: bool,
2869    pre_changes: SystemChangeVecSlice,
2870    old_focus: DomNodeId,
2871    new_focus: DomNodeId,
2872) -> SystemChangeVec {
2873    let pre_changes_slice = pre_changes.as_slice();
2874    let old = old_focus.node.into_crate_internal().map(|_| old_focus);
2875    let new = new_focus.node.into_crate_internal().map(|_| new_focus);
2876    default_post_filter(prevent_default, pre_changes_slice, old, new).into()
2877}
2878
2879pub fn default_input_interpreter(
2880    info: &InputInterpreterInfo,
2881) -> PreCallbackFilterResult {
2882    let ctx = FilterContext {
2883        hit_test: info.hit_test,
2884        keyboard_state: info.keyboard_state,
2885        mouse_state: info.mouse_state,
2886        click_count: info.state.click_count,
2887        focused_node: info.state.focused_node,
2888        drag_start_position: info.state.drag_start_position,
2889    };
2890
2891    let (system_changes, user_events) = info.events.iter().fold(
2892        (Vec::new(), Vec::new()),
2893        |(mut internal, mut user), event| {
2894            match process_event_for_internal(&ctx, event) {
2895                Some(InternalEventAction::AddAndSkip(evt)) => {
2896                    internal.push(evt);
2897                }
2898                Some(InternalEventAction::AddAndPass(evt)) => {
2899                    internal.push(evt);
2900                    user.push(event.clone());
2901                }
2902                None => {
2903                    user.push(event.clone());
2904                }
2905            }
2906            (internal, user)
2907        },
2908    );
2909
2910    PreCallbackFilterResult {
2911        system_changes,
2912        user_events,
2913    }
2914}
2915
2916/// Backward-compatible wrapper that calls `default_input_interpreter`.
2917pub fn pre_callback_filter_internal_events<SM, FM>(
2918    events: &[SyntheticEvent],
2919    hit_test: Option<&FullHitTest>,
2920    keyboard_state: &crate::window::KeyboardState,
2921    mouse_state: &crate::window::MouseState,
2922    selection_manager: &SM,
2923    focus_manager: &FM,
2924) -> PreCallbackFilterResult
2925where
2926    SM: SelectionManagerQuery,
2927    FM: FocusManagerQuery,
2928{
2929    let info = InputInterpreterInfo {
2930        events,
2931        hit_test,
2932        keyboard_state,
2933        mouse_state,
2934        state: InputInterpreterState {
2935            focused_node: focus_manager.get_focused_node_id(),
2936            click_count: selection_manager.get_click_count(),
2937            drag_start_position: selection_manager.get_drag_start_position(),
2938            has_selection: selection_manager.has_selection(),
2939        },
2940    };
2941    default_input_interpreter(&info)
2942}
2943
2944/// Context for filtering internal events (used by default_input_interpreter)
2945struct FilterContext<'a> {
2946    hit_test: Option<&'a FullHitTest>,
2947    keyboard_state: &'a crate::window::KeyboardState,
2948    mouse_state: &'a crate::window::MouseState,
2949    click_count: u8,
2950    focused_node: Option<DomNodeId>,
2951    drag_start_position: Option<LogicalPosition>,
2952}
2953
2954/// Process a single event and determine if it generates an internal event
2955fn process_event_for_internal(
2956    ctx: &FilterContext<'_>,
2957    event: &SyntheticEvent,
2958) -> Option<InternalEventAction> {
2959    match event.event_type {
2960        EventType::MouseDown => handle_mouse_down(event, ctx.hit_test, ctx.click_count, ctx.mouse_state, ctx.keyboard_state),
2961        EventType::MouseOver => handle_mouse_over(
2962            event,
2963            ctx.hit_test,
2964            ctx.mouse_state,
2965            ctx.drag_start_position,
2966        ),
2967        EventType::KeyDown => handle_key_down(
2968            event,
2969            ctx.keyboard_state,
2970            ctx.focused_node,
2971        ),
2972        _ => None,
2973    }
2974}
2975
2976/// Action to take after processing an event for internal system events
2977enum InternalEventAction {
2978    /// Add system change and skip passing to user callbacks
2979    AddAndSkip(SystemChange),
2980    /// Add system change but also pass to user callbacks
2981    AddAndPass(SystemChange),
2982}
2983
2984/// Extract first hovered node from hit test
2985fn get_first_hovered_node(hit_test: Option<&FullHitTest>) -> Option<DomNodeId> {
2986    let ht = hit_test?;
2987    let (dom_id, hit_data) = ht.hovered_nodes.iter().next()?;
2988    let node_id = hit_data.regular_hit_test_nodes.keys().next()?;
2989    Some(DomNodeId {
2990        dom: *dom_id,
2991        node: NodeHierarchyItemId::from_crate_internal(Some(*node_id)),
2992    })
2993}
2994
2995/// Extract mouse position from event data, falling back to mouse_state if not available
2996fn get_mouse_position_with_fallback(
2997    event: &SyntheticEvent,
2998    mouse_state: &crate::window::MouseState,
2999) -> LogicalPosition {
3000    match &event.data {
3001        EventData::Mouse(mouse_data) => mouse_data.position,
3002        _ => {
3003            // Fallback: use current cursor position from mouse_state
3004            // This handles synthetic events from debug API and automation
3005            // where EventData may not contain the mouse position
3006            mouse_state.cursor_position.get_position().unwrap_or(LogicalPosition::zero())
3007        }
3008    }
3009}
3010
3011/// Handle MouseDown event - detect text selection clicks and Ctrl+Click for multi-cursor
3012fn handle_mouse_down(
3013    event: &SyntheticEvent,
3014    hit_test: Option<&FullHitTest>,
3015    click_count: u8,
3016    mouse_state: &crate::window::MouseState,
3017    keyboard_state: &crate::window::KeyboardState,
3018) -> Option<InternalEventAction> {
3019    let effective_click_count = if click_count == 0 { 1 } else { click_count };
3020
3021    if effective_click_count > 3 {
3022        return None;
3023    }
3024
3025    let _target = get_first_hovered_node(hit_test)?;
3026    let position = get_mouse_position_with_fallback(event, mouse_state);
3027
3028    // Ctrl+Click (or Cmd+Click on macOS): add cursor at click position
3029    if keyboard_state.ctrl_down() && effective_click_count == 1 {
3030        return Some(InternalEventAction::AddAndPass(
3031            SystemChange::AddCursorAtClick { position },
3032        ));
3033    }
3034
3035    Some(InternalEventAction::AddAndPass(
3036        SystemChange::TextSelectionClick {
3037            position,
3038            timestamp: event.timestamp.clone(),
3039        },
3040    ))
3041}
3042
3043/// Handle MouseOver event - detect drag selection
3044fn handle_mouse_over(
3045    event: &SyntheticEvent,
3046    hit_test: Option<&FullHitTest>,
3047    mouse_state: &crate::window::MouseState,
3048    drag_start_position: Option<LogicalPosition>,
3049) -> Option<InternalEventAction> {
3050    if !mouse_state.left_down {
3051        return None;
3052    }
3053
3054    let start_position = drag_start_position?;
3055
3056    let _target = get_first_hovered_node(hit_test)?;
3057    let current_position = get_mouse_position_with_fallback(event, mouse_state);
3058
3059    Some(InternalEventAction::AddAndPass(
3060        SystemChange::TextSelectionDrag {
3061            start_position,
3062            current_position,
3063        },
3064    ))
3065}
3066
3067/// Handle KeyDown event - detect shortcuts, arrow keys, and delete keys
3068fn handle_key_down(
3069    event: &SyntheticEvent,
3070    keyboard_state: &crate::window::KeyboardState,
3071    focused_node: Option<DomNodeId>,
3072) -> Option<InternalEventAction> {
3073    use crate::window::VirtualKeyCode;
3074
3075    let target = focused_node?;
3076    let EventData::Keyboard(_) = &event.data else {
3077        return None;
3078    };
3079
3080    let ctrl = keyboard_state.ctrl_down();
3081    let shift = keyboard_state.shift_down();
3082    let vk = keyboard_state.current_virtual_keycode.as_ref()?;
3083
3084    // Check keyboard shortcuts (Ctrl+key) → emit specific SystemChange variants.
3085    // Standard editing shortcuts are routed through the `KeyboardShortcut` enum,
3086    // and a couple of additional Azul-specific Ctrl combos are matched after.
3087    if ctrl {
3088        if let Some(shortcut) = KeyboardShortcut::from_key(*vk, ctrl, shift) {
3089            let change = match shortcut {
3090                KeyboardShortcut::Copy => SystemChange::CopyToClipboard,
3091                KeyboardShortcut::Cut => SystemChange::CutToClipboard { target },
3092                KeyboardShortcut::Paste => SystemChange::PasteFromClipboard,
3093                KeyboardShortcut::SelectAll => SystemChange::SelectAllText,
3094                KeyboardShortcut::Undo => SystemChange::UndoTextEdit { target },
3095                KeyboardShortcut::Redo => SystemChange::RedoTextEdit { target },
3096            };
3097            return Some(InternalEventAction::AddAndSkip(change));
3098        }
3099        if matches!(vk, VirtualKeyCode::D) {
3100            return Some(InternalEventAction::AddAndSkip(
3101                SystemChange::SelectNextOccurrence { target },
3102            ));
3103        }
3104    }
3105
3106    // Unified: arrow keys, Home/End, Backspace/Delete all map to SelectionOp.
3107    let mode_for_shift = if shift { SelectionMode::Extend } else { SelectionMode::Move };
3108    let selection_op = if let Some(arrow) = ArrowDirection::from_key(*vk, ctrl) {
3109        let (direction, step) = arrow.to_selection(ctrl);
3110        SelectionOp::new(direction, step, mode_for_shift)
3111    } else {
3112        match vk {
3113            // Backspace/Delete = Delete mode (Ctrl upgrades to Word)
3114            VirtualKeyCode::Back => SelectionOp::new(
3115                SelectionDirection::Backward,
3116                if ctrl { SelectionStep::Word } else { SelectionStep::Character },
3117                SelectionMode::Delete,
3118            ),
3119            VirtualKeyCode::Delete => SelectionOp::new(
3120                SelectionDirection::Forward,
3121                if ctrl { SelectionStep::Word } else { SelectionStep::Character },
3122                SelectionMode::Delete,
3123            ),
3124            _ => return None,
3125        }
3126    };
3127
3128    Some(InternalEventAction::AddAndSkip(
3129        SystemChange::ApplySelectionOp { target, op: selection_op },
3130    ))
3131}
3132
3133/// Trait for querying selection manager state.
3134///
3135/// This allows `pre_callback_filter_internal_events` to query manager state
3136/// without depending on the concrete `SelectionManager` type from layout crate.
3137pub trait SelectionManagerQuery {
3138    /// Get the current click count (1 = single, 2 = double, 3 = triple)
3139    fn get_click_count(&self) -> u8;
3140
3141    /// Get the drag start position if a drag is in progress
3142    fn get_drag_start_position(&self) -> Option<LogicalPosition>;
3143
3144    /// Check if any selection exists (click selection or drag selection)
3145    fn has_selection(&self) -> bool;
3146}
3147
3148/// Trait for querying focus manager state.
3149///
3150/// This allows `pre_callback_filter_internal_events` to query manager state
3151/// without depending on the concrete `FocusManager` type from layout crate.
3152pub trait FocusManagerQuery {
3153    /// Get the currently focused node ID
3154    fn get_focused_node_id(&self) -> Option<DomNodeId>;
3155}
3156
3157/// Post-callback filter: Determine additional system changes needed after user callbacks.
3158///
3159/// Takes the pre-callback system changes and focus state to determine what
3160/// post-callback system changes are needed (text input, scrolling, timers).
3161/// Default post-callback filter: scroll-into-view after cursor ops, auto-scroll during drag.
3162pub fn default_post_filter(
3163    prevent_default: bool,
3164    pre_changes: &[SystemChange],
3165    old_focus: Option<DomNodeId>,
3166    new_focus: Option<DomNodeId>,
3167) -> Vec<SystemChange> {
3168    post_callback_filter_system_changes(prevent_default, pre_changes, old_focus, new_focus)
3169}
3170
3171pub fn post_callback_filter_system_changes(
3172    prevent_default: bool,
3173    pre_changes: &[SystemChange],
3174    old_focus: Option<DomNodeId>,
3175    new_focus: Option<DomNodeId>,
3176) -> Vec<SystemChange> {
3177    let mut changes = Vec::new();
3178
3179    if prevent_default {
3180        // Only focus change passes through preventDefault
3181        if old_focus != new_focus {
3182            changes.push(SystemChange::SetFocus { new_focus, old_focus });
3183        }
3184        return changes;
3185    }
3186
3187    // Always apply pending text input
3188    changes.push(SystemChange::ApplyPendingTextInput);
3189
3190    // Determine post-callback actions based on pre-callback system changes
3191    for change in pre_changes {
3192        match change {
3193            SystemChange::TextSelectionClick { .. }
3194            | SystemChange::ApplySelectionOp { .. }
3195            | SystemChange::AddCursorAtClick { .. }
3196            | SystemChange::SelectNextOccurrence { .. } => {
3197                changes.push(SystemChange::ScrollSelectionIntoView);
3198            }
3199            SystemChange::TextSelectionDrag { .. } => {
3200                changes.push(SystemChange::StartAutoScrollTimer);
3201            }
3202            SystemChange::CutToClipboard { .. }
3203            | SystemChange::PasteFromClipboard
3204            | SystemChange::UndoTextEdit { .. }
3205            | SystemChange::RedoTextEdit { .. }
3206            | SystemChange::SelectAllText => {
3207                changes.push(SystemChange::ScrollSelectionIntoView);
3208            }
3209            // Other system changes don't generate post-callback actions
3210            _ => {}
3211        }
3212    }
3213
3214    // Focus changed during callbacks
3215    if old_focus != new_focus {
3216        changes.push(SystemChange::SetFocus { new_focus, old_focus });
3217    }
3218
3219    changes
3220}
3221
3222
3223#[cfg(test)]
3224mod tests {
3225    use super::*;
3226    use azul_css::AzString;
3227    use crate::dom::{DomId, DomNodeId};
3228    use crate::styled_dom::NodeHierarchyItemId;
3229    use crate::id::NodeId;
3230    use crate::window::{KeyboardState, MouseState, VirtualKeyCode, VirtualKeyCodeVec, OptionVirtualKeyCode};
3231    use crate::geom::LogicalPosition;
3232    use crate::task::{Instant, SystemTick};
3233
3234    struct MockSelectionManager {
3235        click_count: u8,
3236        has_sel: bool,
3237    }
3238    impl SelectionManagerQuery for MockSelectionManager {
3239        fn get_click_count(&self) -> u8 { self.click_count }
3240        fn get_drag_start_position(&self) -> Option<LogicalPosition> { None }
3241        fn has_selection(&self) -> bool { self.has_sel }
3242    }
3243
3244    struct MockFocusManager(Option<DomNodeId>);
3245    impl FocusManagerQuery for MockFocusManager {
3246        fn get_focused_node_id(&self) -> Option<DomNodeId> { self.0 }
3247    }
3248
3249    fn focused_node(node_idx: usize) -> DomNodeId {
3250        DomNodeId {
3251            dom: DomId { inner: 0 },
3252            node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(node_idx))),
3253        }
3254    }
3255
3256    fn make_keyboard_state(vk: VirtualKeyCode) -> KeyboardState {
3257        let mut ks = KeyboardState::default();
3258        ks.current_virtual_keycode = OptionVirtualKeyCode::Some(vk);
3259        ks.pressed_virtual_keycodes = VirtualKeyCodeVec::from_vec(vec![vk]);
3260        ks
3261    }
3262
3263    fn make_keydown_event(target: DomNodeId) -> SyntheticEvent {
3264        SyntheticEvent::new(
3265            EventType::KeyDown,
3266            EventSource::User,
3267            target,
3268            Instant::Tick(SystemTick::new(0)),
3269            EventData::Keyboard(KeyboardEventData {
3270                key_code: VirtualKeyCode::Back as u32,
3271                char_code: None,
3272                modifiers: KeyModifiers::default(),
3273                repeat: false,
3274            }),
3275        )
3276    }
3277
3278    #[test]
3279    fn backspace_generates_delete_text_selection() {
3280        let target = focused_node(2);
3281        let events = vec![make_keydown_event(target)];
3282        let kb = make_keyboard_state(VirtualKeyCode::Back);
3283        let mouse = MouseState::default();
3284        let sel = MockSelectionManager { click_count: 0, has_sel: false };
3285        let focus = MockFocusManager(Some(target));
3286
3287        let result = pre_callback_filter_internal_events(
3288            &events, None, &kb, &mouse, &sel, &focus,
3289        );
3290
3291        let ops: Vec<_> = result.system_changes.iter()
3292            .filter(|c| matches!(c, SystemChange::ApplySelectionOp { .. }))
3293            .collect();
3294        assert_eq!(ops.len(), 1, "Backspace should generate ApplySelectionOp");
3295        match &ops[0] {
3296            SystemChange::ApplySelectionOp { op, .. } => {
3297                assert_eq!(op.direction, SelectionDirection::Backward);
3298                assert_eq!(op.step, SelectionStep::Character);
3299                assert_eq!(op.mode, SelectionMode::Delete);
3300            }
3301            _ => unreachable!(),
3302        }
3303    }
3304
3305    #[test]
3306    fn delete_key_generates_forward_deletion() {
3307        let target = focused_node(2);
3308        let event = SyntheticEvent::new(
3309            EventType::KeyDown, EventSource::User, target,
3310            Instant::Tick(SystemTick::new(0)),
3311            EventData::Keyboard(KeyboardEventData {
3312                key_code: VirtualKeyCode::Delete as u32,
3313                char_code: None, modifiers: KeyModifiers::default(), repeat: false,
3314            }),
3315        );
3316        let kb = make_keyboard_state(VirtualKeyCode::Delete);
3317        let mouse = MouseState::default();
3318        let sel = MockSelectionManager { click_count: 0, has_sel: false };
3319        let focus = MockFocusManager(Some(target));
3320        let result = pre_callback_filter_internal_events(&[event], None, &kb, &mouse, &sel, &focus);
3321        let ops: Vec<_> = result.system_changes.iter()
3322            .filter(|c| matches!(c, SystemChange::ApplySelectionOp { .. }))
3323            .collect();
3324        assert_eq!(ops.len(), 1);
3325        match &ops[0] {
3326            SystemChange::ApplySelectionOp { op, .. } => {
3327                assert_eq!(op.direction, SelectionDirection::Forward);
3328                assert_eq!(op.step, SelectionStep::Character);
3329                assert_eq!(op.mode, SelectionMode::Delete);
3330            }
3331            _ => unreachable!(),
3332        }
3333    }
3334
3335    #[test]
3336    fn arrow_left_generates_navigation() {
3337        let target = focused_node(2);
3338        let event = SyntheticEvent::new(
3339            EventType::KeyDown, EventSource::User, target,
3340            Instant::Tick(SystemTick::new(0)),
3341            EventData::Keyboard(KeyboardEventData {
3342                key_code: VirtualKeyCode::Left as u32,
3343                char_code: None, modifiers: KeyModifiers::default(), repeat: false,
3344            }),
3345        );
3346        let kb = make_keyboard_state(VirtualKeyCode::Left);
3347        let mouse = MouseState::default();
3348        let sel = MockSelectionManager { click_count: 0, has_sel: false };
3349        let focus = MockFocusManager(Some(target));
3350        let result = pre_callback_filter_internal_events(&[event], None, &kb, &mouse, &sel, &focus);
3351        let ops: Vec<_> = result.system_changes.iter()
3352            .filter(|c| matches!(c, SystemChange::ApplySelectionOp { .. }))
3353            .collect();
3354        assert_eq!(ops.len(), 1, "Left arrow should generate ApplySelectionOp");
3355        match &ops[0] {
3356            SystemChange::ApplySelectionOp { op, .. } => {
3357                assert_eq!(op.direction, SelectionDirection::Backward);
3358                assert_eq!(op.step, SelectionStep::Character);
3359                assert_eq!(op.mode, SelectionMode::Move);
3360            }
3361            _ => unreachable!(),
3362        }
3363    }
3364
3365    #[test]
3366    fn no_focused_node_means_no_keyboard_system_changes() {
3367        let target = focused_node(2);
3368        let event = make_keydown_event(target);
3369        let kb = make_keyboard_state(VirtualKeyCode::Back);
3370        let mouse = MouseState::default();
3371        let sel = MockSelectionManager { click_count: 0, has_sel: false };
3372        let focus = MockFocusManager(None); // No focus!
3373
3374        let result = pre_callback_filter_internal_events(
3375            &[event], None, &kb, &mouse, &sel, &focus,
3376        );
3377
3378        assert!(result.system_changes.is_empty(),
3379            "No system changes should be generated without focused node");
3380    }
3381
3382    #[test]
3383    fn keydown_without_keyboard_data_generates_no_system_change() {
3384        let target = focused_node(2);
3385        let event = SyntheticEvent::new(
3386            EventType::KeyDown,
3387            EventSource::User,
3388            target,
3389            Instant::Tick(SystemTick::new(0)),
3390            EventData::None, // Bug: missing keyboard data
3391        );
3392        let kb = make_keyboard_state(VirtualKeyCode::Back);
3393        let mouse = MouseState::default();
3394        let sel = MockSelectionManager { click_count: 0, has_sel: false };
3395        let focus = MockFocusManager(Some(target));
3396
3397        let result = pre_callback_filter_internal_events(
3398            &[event], None, &kb, &mouse, &sel, &focus,
3399        );
3400
3401        // This test documents the bug we just fixed: EventData::None causes
3402        // the handle_key_down function to return None (early exit at line 2737)
3403        assert!(result.system_changes.is_empty(),
3404            "EventData::None should not generate system changes (documents the old bug)");
3405    }
3406
3407    #[test]
3408    fn ctrl_c_generates_copy() {
3409        let target = focused_node(2);
3410        let event = SyntheticEvent::new(
3411            EventType::KeyDown,
3412            EventSource::User,
3413            target,
3414            Instant::Tick(SystemTick::new(0)),
3415            EventData::Keyboard(KeyboardEventData {
3416                key_code: VirtualKeyCode::C as u32,
3417                char_code: Some('c'),
3418                modifiers: KeyModifiers { ctrl: true, shift: false, alt: false, meta: false },
3419                repeat: false,
3420            }),
3421        );
3422        let mut kb = make_keyboard_state(VirtualKeyCode::C);
3423        kb.pressed_virtual_keycodes = VirtualKeyCodeVec::from_vec(
3424            vec![VirtualKeyCode::C, VirtualKeyCode::LControl]
3425        );
3426        let mouse = MouseState::default();
3427        let sel = MockSelectionManager { click_count: 0, has_sel: false };
3428        let focus = MockFocusManager(Some(target));
3429
3430        let result = pre_callback_filter_internal_events(
3431            &[event], None, &kb, &mouse, &sel, &focus,
3432        );
3433
3434        let copy_changes: Vec<_> = result.system_changes.iter()
3435            .filter(|c| matches!(c, SystemChange::CopyToClipboard))
3436            .collect();
3437
3438        assert_eq!(copy_changes.len(), 1, "Ctrl+C should generate CopyToClipboard");
3439    }
3440
3441    fn make_hit_test_with_node(node_idx: usize) -> crate::hit_test::FullHitTest {
3442        use crate::hit_test::{FullHitTest, HitTest, HitTestItem};
3443        use crate::dom::OptionDomNodeId;
3444        use std::collections::BTreeMap;
3445
3446        let node_id = NodeId::new(node_idx);
3447        let dom_id = DomId { inner: 0 };
3448
3449        let mut regular = BTreeMap::new();
3450        regular.insert(node_id, HitTestItem {
3451            point_in_viewport: LogicalPosition::new(100.0, 200.0),
3452            point_relative_to_item: LogicalPosition::new(50.0, 30.0),
3453            is_focusable: true,
3454            is_virtual_view_hit: None,
3455            hit_depth: 0,
3456        });
3457
3458        let mut hovered = BTreeMap::new();
3459        hovered.insert(dom_id, HitTest {
3460            regular_hit_test_nodes: regular,
3461            scroll_hit_test_nodes: BTreeMap::new(),
3462            scrollbar_hit_test_nodes: BTreeMap::new(),
3463            cursor_hit_test_nodes: BTreeMap::new(),
3464        });
3465
3466        FullHitTest {
3467            hovered_nodes: hovered,
3468            focused_node: OptionDomNodeId::None,
3469        }
3470    }
3471
3472    #[test]
3473    fn mousedown_generates_text_selection_click() {
3474        let target = focused_node(2);
3475        let event = SyntheticEvent::new(
3476            EventType::MouseDown,
3477            EventSource::User,
3478            target,
3479            Instant::Tick(SystemTick::new(0)),
3480            EventData::Mouse(MouseEventData {
3481                position: LogicalPosition::new(100.0, 200.0),
3482                button: crate::events::MouseButton::Left,
3483                buttons: 1,
3484                modifiers: KeyModifiers::default(),
3485            }),
3486        );
3487        let hit_test = make_hit_test_with_node(2);
3488        let kb = KeyboardState::default();
3489        let mouse = MouseState::default();
3490        let sel = MockSelectionManager { click_count: 1, has_sel: false };
3491        let focus = MockFocusManager(Some(target));
3492
3493        let result = pre_callback_filter_internal_events(
3494            &[event], Some(&hit_test), &kb, &mouse, &sel, &focus,
3495        );
3496
3497        let click_changes: Vec<_> = result.system_changes.iter()
3498            .filter(|c| matches!(c, SystemChange::TextSelectionClick { .. }))
3499            .collect();
3500
3501        assert_eq!(click_changes.len(), 1, "MouseDown with hit_test should generate TextSelectionClick");
3502    }
3503
3504    #[test]
3505    fn process_event_result_max_self_picks_higher_variant() {
3506        let lo = ProcessEventResult::ShouldReRenderCurrentWindow;
3507        let hi = ProcessEventResult::ShouldRegenerateDomCurrentWindow;
3508        assert_eq!(lo.max_self(hi), hi);
3509        assert_eq!(hi.max_self(lo), hi);
3510        assert_eq!(lo.max_self(lo), lo);
3511    }
3512
3513    #[test]
3514    fn arrow_direction_from_key_maps_arrows_and_home_end() {
3515        use crate::window::VirtualKeyCode::*;
3516        assert_eq!(ArrowDirection::from_key(Left, false), Some(ArrowDirection::Left));
3517        assert_eq!(ArrowDirection::from_key(Right, false), Some(ArrowDirection::Right));
3518        assert_eq!(ArrowDirection::from_key(Up, false), Some(ArrowDirection::Up));
3519        assert_eq!(ArrowDirection::from_key(Down, false), Some(ArrowDirection::Down));
3520        assert_eq!(ArrowDirection::from_key(Home, false), Some(ArrowDirection::LineStart));
3521        assert_eq!(ArrowDirection::from_key(End, false), Some(ArrowDirection::LineEnd));
3522        assert_eq!(ArrowDirection::from_key(Home, true), Some(ArrowDirection::DocumentStart));
3523        assert_eq!(ArrowDirection::from_key(End, true), Some(ArrowDirection::DocumentEnd));
3524        assert_eq!(ArrowDirection::from_key(C, false), None);
3525    }
3526
3527    #[test]
3528    fn arrow_direction_to_selection_respects_ctrl() {
3529        let (d, s) = ArrowDirection::Left.to_selection(false);
3530        assert_eq!((d, s), (SelectionDirection::Backward, SelectionStep::Character));
3531        let (d, s) = ArrowDirection::Left.to_selection(true);
3532        assert_eq!((d, s), (SelectionDirection::Backward, SelectionStep::Word));
3533        let (d, s) = ArrowDirection::Up.to_selection(false);
3534        assert_eq!((d, s), (SelectionDirection::Backward, SelectionStep::VisualLine));
3535        let (d, s) = ArrowDirection::DocumentEnd.to_selection(false);
3536        assert_eq!((d, s), (SelectionDirection::Forward, SelectionStep::Document));
3537    }
3538
3539    #[test]
3540    fn keyboard_shortcut_from_key_recognizes_editing_combos() {
3541        use crate::window::VirtualKeyCode::*;
3542        assert_eq!(KeyboardShortcut::from_key(C, true, false), Some(KeyboardShortcut::Copy));
3543        assert_eq!(KeyboardShortcut::from_key(X, true, false), Some(KeyboardShortcut::Cut));
3544        assert_eq!(KeyboardShortcut::from_key(V, true, false), Some(KeyboardShortcut::Paste));
3545        assert_eq!(KeyboardShortcut::from_key(A, true, false), Some(KeyboardShortcut::SelectAll));
3546        assert_eq!(KeyboardShortcut::from_key(Z, true, false), Some(KeyboardShortcut::Undo));
3547        assert_eq!(KeyboardShortcut::from_key(Z, true, true), Some(KeyboardShortcut::Redo));
3548        assert_eq!(KeyboardShortcut::from_key(Y, true, false), Some(KeyboardShortcut::Redo));
3549        // Non-ctrl combos must not match
3550        assert_eq!(KeyboardShortcut::from_key(C, false, false), None);
3551        // Unknown keys
3552        assert_eq!(KeyboardShortcut::from_key(D, true, false), None);
3553    }
3554
3555    #[test]
3556    fn mouse_button_state_round_trips_from_mouse_state() {
3557        let mut ms = MouseState::default();
3558        ms.left_down = true;
3559        ms.middle_down = true;
3560        let bs: MouseButtonState = (&ms).into();
3561        assert!(bs.left_down);
3562        assert!(!bs.right_down);
3563        assert!(bs.middle_down);
3564        assert!(bs.any_down());
3565
3566        let none = MouseButtonState { left_down: false, right_down: false, middle_down: false };
3567        assert!(!none.any_down());
3568    }
3569
3570    #[test]
3571    fn callback_to_call_collects_hits_for_dom() {
3572        let dom_id = DomId { inner: 0 };
3573        let hit_test = make_hit_test_with_node(2);
3574        let filter = EventFilter::Hover(HoverEventFilter::MouseDown);
3575        let calls = CallbackToCall::from_hit_test(&hit_test, dom_id, filter.clone());
3576        assert_eq!(calls.len(), 1);
3577        assert_eq!(calls[0].node_id, NodeId::new(2));
3578        assert_eq!(calls[0].event_filter, filter);
3579        assert!(calls[0].hit_test_item.is_some());
3580
3581        // Unknown DOM id => empty list
3582        let other = CallbackToCall::from_hit_test(
3583            &hit_test,
3584            DomId { inner: 999 },
3585            EventFilter::Hover(HoverEventFilter::MouseUp),
3586        );
3587        assert!(other.is_empty());
3588
3589        // Direct constructor builds expected fields
3590        let direct = CallbackToCall::new(
3591            NodeId::new(7),
3592            None,
3593            EventFilter::Focus(FocusEventFilter::FocusReceived),
3594        );
3595        assert_eq!(direct.node_id, NodeId::new(7));
3596        assert!(direct.hit_test_item.is_none());
3597    }
3598
3599    #[test]
3600    fn restyle_relayout_aliases_are_btreemap_compatible() {
3601        // RestyleNodes / RelayoutNodes are aliases for BTreeMap<NodeId, Vec<ChangedCssProperty>>.
3602        // Confirm we can construct empty ones via the alias and that they accept the same keys.
3603        let restyle: RestyleNodes = BTreeMap::new();
3604        let relayout: RelayoutNodes = BTreeMap::new();
3605        assert!(restyle.is_empty());
3606        assert!(relayout.is_empty());
3607
3608        // RelayoutWords is BTreeMap<NodeId, AzString>.
3609        let mut words: RelayoutWords = BTreeMap::new();
3610        words.insert(NodeId::new(1), AzString::from_const_str("hello"));
3611        assert_eq!(words.get(&NodeId::new(1)).map(|s| s.as_str()), Some("hello"));
3612    }
3613
3614    #[test]
3615    fn detect_lifecycle_events_with_reconciliation_is_callable() {
3616        // Smoke test: empty old/new node data must produce no events and an
3617        // empty migration map. This proves the function is callable from
3618        // the public API and threads through `crate::diff::reconcile_dom`.
3619        let dom_id = DomId { inner: 0 };
3620        let old_data: Vec<crate::dom::NodeData> = Vec::new();
3621        let new_data: Vec<crate::dom::NodeData> = Vec::new();
3622        let old_hier: Vec<crate::styled_dom::NodeHierarchyItem> = Vec::new();
3623        let new_hier: Vec<crate::styled_dom::NodeHierarchyItem> = Vec::new();
3624        let old_layout = OrderedMap::default();
3625        let new_layout = OrderedMap::default();
3626        let result: LifecycleEventResult = detect_lifecycle_events_with_reconciliation(
3627            dom_id,
3628            &old_data,
3629            &new_data,
3630            &old_hier,
3631            &new_hier,
3632            &old_layout,
3633            &new_layout,
3634            Instant::Tick(SystemTick::new(0)),
3635        );
3636        assert!(result.events.is_empty());
3637        assert!(result.node_id_mapping.is_empty());
3638    }
3639
3640    #[test]
3641    fn nodedata_focusable_and_activation_traits_are_wired() {
3642        use crate::dom::{NodeData, NodeType};
3643        use crate::events::{ActivationBehavior as _, Focusable as _};
3644
3645        // <button> is naturally focusable and has activation behavior.
3646        let btn = NodeData::create_node(NodeType::Button);
3647        assert!(<NodeData as Focusable>::is_naturally_focusable(&btn));
3648        assert!(<NodeData as Focusable>::is_focusable(&btn));
3649        assert!(<NodeData as ActivationBehavior>::has_activation_behavior(&btn));
3650        assert!(<NodeData as ActivationBehavior>::is_activatable(&btn));
3651
3652        // A plain <div> is neither naturally focusable nor activatable.
3653        let div = NodeData::create_node(NodeType::Div);
3654        assert!(!<NodeData as Focusable>::is_naturally_focusable(&div));
3655        assert!(!<NodeData as ActivationBehavior>::has_activation_behavior(&div));
3656
3657        // <input> is naturally focusable.
3658        let input = NodeData::create_node(NodeType::Input);
3659        assert!(<NodeData as Focusable>::is_naturally_focusable(&input));
3660    }
3661}