1
//! Event and callback filtering module
2

            
3
#[cfg(not(feature = "std"))]
4
use alloc::string::{String, ToString};
5
use alloc::{
6
    boxed::Box,
7
    collections::{btree_map::BTreeMap, btree_set::BTreeSet},
8
    vec::Vec,
9
};
10

            
11
use azul_css::AzString;
12

            
13
use 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)]
26
pub enum EasingFunction {
27
    Linear,
28
    EaseInOut,
29
    EaseOut,
30
}
31

            
32
pub type RestyleNodes = BTreeMap<NodeId, Vec<ChangedCssProperty>>;
33
pub type RelayoutNodes = BTreeMap<NodeId, Vec<ChangedCssProperty>>;
34
pub type RelayoutWords = BTreeMap<NodeId, AzString>;
35

            
36
#[derive(Debug, Clone, PartialEq)]
37
pub struct FocusChange {
38
    pub old: Option<DomNodeId>,
39
    pub new: Option<DomNodeId>,
40
}
41

            
42
#[derive(Debug, Clone, PartialEq)]
43
pub struct CallbackToCall {
44
    pub node_id: NodeId,
45
    pub hit_test_item: Option<HitTestItem>,
46
    pub event_filter: EventFilter,
47
}
48

            
49
impl CallbackToCall {
50
1
    pub fn new(
51
1
        node_id: NodeId,
52
1
        hit_test_item: Option<HitTestItem>,
53
1
        event_filter: EventFilter,
54
1
    ) -> Self {
55
1
        Self { node_id, hit_test_item, event_filter }
56
1
    }
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
2
    pub fn from_hit_test(
62
2
        hit_test: &FullHitTest,
63
2
        dom_id: DomId,
64
2
        event_filter: EventFilter,
65
2
    ) -> Vec<CallbackToCall> {
66
2
        let Some(hit) = hit_test.hovered_nodes.get(&dom_id) else {
67
1
            return Vec::new();
68
        };
69
1
        hit.regular_hit_test_nodes
70
1
            .iter()
71
1
            .map(|(node_id, item)| CallbackToCall {
72
1
                node_id: *node_id,
73
1
                hit_test_item: Some(item.clone()),
74
1
                event_filter: event_filter.clone(),
75
1
            })
76
1
            .collect()
77
2
    }
78
}
79

            
80
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
81
#[must_use = "ProcessEventResult must be used to determine if relayout/repaint is needed"]
82
pub 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

            
97
impl ProcessEventResult {
98
6
    pub fn order(&self) -> usize {
99
        use self::ProcessEventResult::*;
100
6
        match self {
101
            DoNothing => 0,
102
4
            ShouldReRenderCurrentWindow => 1,
103
            ShouldUpdateDisplayListCurrentWindow => 2,
104
            UpdateHitTesterAndProcessAgain => 3,
105
            ShouldIncrementalRelayout => 4,
106
2
            ShouldRegenerateDomCurrentWindow => 5,
107
            ShouldRegenerateDomAllWindows => 6,
108
        }
109
6
    }
110
}
111

            
112
impl PartialOrd for ProcessEventResult {
113
3
    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
114
3
        self.order().partial_cmp(&other.order())
115
3
    }
116
}
117

            
118
impl Ord for ProcessEventResult {
119
    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
120
        self.order().cmp(&other.order())
121
    }
122
}
123

            
124
impl ProcessEventResult {
125
3
    pub fn max_self(self, other: Self) -> Self {
126
3
        self.max(other)
127
3
    }
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)]
136
pub 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)]
155
pub 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

            
164
impl Default for EventPhase {
165
1
    fn default() -> Self {
166
1
        EventPhase::Bubble
167
1
    }
168
}
169

            
170
/// Mouse button identifier for mouse events.
171
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
172
#[repr(C)]
173
pub 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)]
183
pub 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)]
195
pub 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)]
212
pub 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

            
222
impl 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)]
278
pub 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)]
296
pub 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)]
309
pub 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)]
325
pub struct KeyModifiers {
326
    pub shift: bool,
327
    pub ctrl: bool,
328
    pub alt: bool,
329
    pub meta: bool,
330
}
331

            
332
impl KeyModifiers {
333
4
    pub fn new() -> Self {
334
4
        Self::default()
335
4
    }
336

            
337
1
    pub fn with_shift(mut self) -> Self {
338
1
        self.shift = true;
339
1
        self
340
1
    }
341

            
342
1
    pub fn with_ctrl(mut self) -> Self {
343
1
        self.ctrl = true;
344
1
        self
345
1
    }
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
2
    pub fn is_empty(&self) -> bool {
358
2
        !self.shift && !self.ctrl && !self.alt && !self.meta
359
2
    }
360
}
361

            
362
/// Type-specific event data for mouse events.
363
#[derive(Debug, Clone, PartialEq)]
364
pub 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)]
377
pub 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)]
390
pub 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)]
399
pub 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)]
410
pub 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)]
417
pub 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)]
428
pub 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)]
437
pub 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)]
463
pub 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)]
666
pub 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

            
698
impl SyntheticEvent {
699
    /// Create a new synthetic event.
700
    ///
701
    /// # Parameters
702
    /// - `timestamp`: Current time from `(system_callbacks.get_system_time_fn.cb)()`
703
373
    pub fn new(
704
373
        event_type: EventType,
705
373
        source: EventSource,
706
373
        target: DomNodeId,
707
373
        timestamp: Instant,
708
373
        data: EventData,
709
373
    ) -> Self {
710
373
        Self {
711
373
            event_type,
712
373
            source,
713
373
            phase: EventPhase::Target,
714
373
            target,
715
373
            current_target: target,
716
373
            timestamp,
717
373
            data,
718
373
            stopped: false,
719
373
            stopped_immediate: false,
720
373
            prevented_default: false,
721
373
        }
722
373
    }
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
1
    pub fn stop_propagation(&mut self) {
729
1
        self.stopped = true;
730
1
    }
731

            
732
    /// Stop event propagation immediately.
733
    ///
734
    /// This prevents any further handlers from being called, even on the
735
    /// current target element.
736
1
    pub fn stop_immediate_propagation(&mut self) {
737
1
        self.stopped_immediate = true;
738
1
        self.stopped = true;
739
1
    }
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
1
    pub fn prevent_default(&mut self) {
746
1
        self.prevented_default = true;
747
1
    }
748

            
749
    /// Check if propagation was stopped.
750
3
    pub fn is_propagation_stopped(&self) -> bool {
751
3
        self.stopped
752
3
    }
753

            
754
    /// Check if immediate propagation was stopped.
755
2
    pub fn is_immediate_propagation_stopped(&self) -> bool {
756
2
        self.stopped_immediate
757
2
    }
758

            
759
    /// Check if default action was prevented.
760
2
    pub fn is_default_prevented(&self) -> bool {
761
2
        self.prevented_default
762
2
    }
763
}
764

            
765
/// Result of event propagation through DOM tree.
766
#[derive(Debug, Clone)]
767
pub 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).
780
68
pub fn get_dom_path(
781
68
    node_hierarchy: &crate::id::NodeHierarchy,
782
68
    target_node: NodeHierarchyItemId,
783
68
) -> Vec<NodeId> {
784
68
    let mut path = Vec::new();
785
68
    let target_node_id = match target_node.into_crate_internal() {
786
68
        Some(id) => id,
787
        None => return path,
788
    };
789

            
790
68
    let hier_ref = node_hierarchy.as_ref();
791

            
792
    // Build path from target to root
793
68
    let mut current = Some(target_node_id);
794
221
    while let Some(node_id) = current {
795
153
        path.push(node_id);
796
153
        current = hier_ref.get(node_id).and_then(|node| node.parent);
797
    }
798

            
799
    // Reverse to get root → target order
800
68
    path.reverse();
801
68
    path
802
68
}
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()`.
813
17
pub fn propagate_event(
814
17
    event: &mut SyntheticEvent,
815
17
    node_hierarchy: &crate::id::NodeHierarchy,
816
17
    callbacks: &BTreeMap<NodeId, Vec<EventFilter>>,
817
17
) -> PropagationResult {
818
17
    let path = get_dom_path(node_hierarchy, event.target.node);
819
17
    if path.is_empty() {
820
        return PropagationResult::default();
821
17
    }
822

            
823
17
    let ancestors = &path[..path.len().saturating_sub(1)];
824
17
    let target_node_id = *path.last().unwrap();
825

            
826
17
    let mut result = PropagationResult::default();
827

            
828
    // Phase 1: Capture (root → target)
829
17
    propagate_phase(
830
17
        event,
831
17
        ancestors.iter().copied(),
832
17
        EventPhase::Capture,
833
17
        callbacks,
834
17
        &mut result,
835
    );
836

            
837
    // Phase 2: Target
838
17
    if !event.stopped {
839
17
        propagate_target_phase(event, target_node_id, callbacks, &mut result);
840
17
    }
841

            
842
    // Phase 3: Bubble (target → root)
843
17
    if !event.stopped {
844
17
        propagate_phase(
845
17
            event,
846
17
            ancestors.iter().rev().copied(),
847
17
            EventPhase::Bubble,
848
17
            callbacks,
849
17
            &mut result,
850
17
        );
851
17
    }
852

            
853
17
    result.default_prevented = event.prevented_default;
854
17
    result
855
17
}
856

            
857
/// Process a single propagation phase (Capture or Bubble)
858
34
fn propagate_phase(
859
34
    event: &mut SyntheticEvent,
860
34
    nodes: impl Iterator<Item = NodeId>,
861
34
    phase: EventPhase,
862
34
    callbacks: &BTreeMap<NodeId, Vec<EventFilter>>,
863
34
    result: &mut PropagationResult,
864
34
) {
865
34
    event.phase = phase;
866

            
867
102
    for node_id in nodes {
868
68
        if event.stopped_immediate || event.stopped {
869
            return;
870
68
        }
871

            
872
68
        event.current_target = DomNodeId {
873
68
            dom: event.target.dom,
874
68
            node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
875
68
        };
876

            
877
68
        collect_matching_callbacks(event, node_id, phase, callbacks, result);
878
    }
879
34
}
880

            
881
/// Process the target phase
882
17
fn propagate_target_phase(
883
17
    event: &mut SyntheticEvent,
884
17
    target_node_id: NodeId,
885
17
    callbacks: &BTreeMap<NodeId, Vec<EventFilter>>,
886
17
    result: &mut PropagationResult,
887
17
) {
888
17
    event.phase = EventPhase::Target;
889
17
    event.current_target = event.target;
890

            
891
17
    collect_matching_callbacks(event, target_node_id, EventPhase::Target, callbacks, result);
892
17
}
893

            
894
/// Collect callbacks that match the current phase for a node
895
85
fn collect_matching_callbacks(
896
85
    event: &SyntheticEvent,
897
85
    node_id: NodeId,
898
85
    phase: EventPhase,
899
85
    callbacks: &BTreeMap<NodeId, Vec<EventFilter>>,
900
85
    result: &mut PropagationResult,
901
85
) {
902
85
    let Some(node_callbacks) = callbacks.get(&node_id) else {
903
85
        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
85
}
914

            
915
impl Default for PropagationResult {
916
17
    fn default() -> Self {
917
17
        Self {
918
17
            callbacks_to_invoke: Vec::new(),
919
17
            default_prevented: false,
920
17
        }
921
17
    }
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)]
944
pub 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)]
982
pub 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)]
999
pub struct DefaultActionResult {
    /// The default action to perform (if any)
    pub action: DefaultAction,
    /// Whether the action was prevented by a callback
    pub prevented: bool,
}
impl Default for DefaultActionResult {
    fn default() -> Self {
        Self {
            action: DefaultAction::None,
            prevented: false,
        }
    }
}
impl DefaultActionResult {
    /// Create a new result with a specific action
108
    pub fn new(action: DefaultAction) -> Self {
108
        Self {
108
            action,
108
            prevented: false,
108
        }
108
    }
    /// Create a prevented result (callback called prevent_default)
36
    pub fn prevented() -> Self {
36
        Self {
36
            action: DefaultAction::None,
36
            prevented: true,
36
        }
36
    }
    /// Check if there's an action to perform
    pub fn has_action(&self) -> bool {
        !self.prevented && !matches!(self.action, DefaultAction::None)
    }
}
/// Trait for elements that have activation behavior (can be "clicked" via keyboard).
///
/// Per HTML5 spec, elements with activation behavior include:
/// - `<button>` elements
/// - `<input type="submit">`, `<input type="button">`, `<input type="reset">`
/// - `<a>` elements with href
/// - `<area>` elements with href
/// - Any element with a click handler (implicit activation)
///
/// When an element with activation behavior is focused and the user presses
/// Enter or Space, a synthetic click event is generated.
pub trait ActivationBehavior {
    /// Returns true if this element can be activated via keyboard (Enter/Space)
    fn has_activation_behavior(&self) -> bool;
    /// Returns true if this element is currently activatable
    /// (e.g., not disabled, not aria-disabled="true")
    fn is_activatable(&self) -> bool;
}
/// Trait to query if a node is focusable for tab navigation
pub trait Focusable {
    /// Returns the tabindex value for this element (-1, 0, or positive)
    fn get_tabindex(&self) -> Option<i32>;
    /// Returns true if this element can receive focus
    fn is_focusable(&self) -> bool;
    /// Returns true if this element should be in the tab order
    fn is_in_tab_order(&self) -> bool {
        match self.get_tabindex() {
            None => self.is_naturally_focusable(),
            Some(i) => i >= 0,
        }
    }
    /// Returns true if this element type is naturally focusable
    /// (button, input, select, textarea, a[href])
    fn is_naturally_focusable(&self) -> bool;
}
/// Check if an event filter matches the given event in the current phase.
///
/// This is used during event propagation to determine which callbacks
/// should be invoked at each phase.
fn matches_filter_phase(
    filter: &EventFilter,
    event: &SyntheticEvent,
    current_phase: EventPhase,
) -> bool {
    // For now, we match based on the filter type
    // In the future, this will also check EventPhase and EventConditions
    match filter {
        EventFilter::Hover(hover_filter) => {
            matches_hover_filter(hover_filter, event, current_phase)
        }
        EventFilter::Focus(focus_filter) => {
            matches_focus_filter(focus_filter, event, current_phase)
        }
        EventFilter::Window(window_filter) => {
            matches_window_filter(window_filter, event, current_phase)
        }
        EventFilter::Component(component_filter) => {
            matches_component_filter(component_filter, event, current_phase)
        }
        EventFilter::Application(_) => {
            // Application events - will be implemented in future
            false
        }
    }
}
/// Check if a component (lifecycle) filter matches the event.
///
/// Lifecycle events produced by `diff::reconcile_dom` carry the target node in
/// `SyntheticEvent.target`, so dispatchers that bypass `propagate_event` and
/// invoke the target directly also need a way to compare. This predicate is
/// the single source of truth for that comparison; changing it without
/// updating `event_type_to_filters` will de-sync dispatch.
fn matches_component_filter(
    filter: &ComponentEventFilter,
    event: &SyntheticEvent,
    _phase: EventPhase,
) -> bool {
    matches!(
        (filter, &event.event_type),
        (ComponentEventFilter::AfterMount, EventType::Mount)
            | (ComponentEventFilter::BeforeUnmount, EventType::Unmount)
            | (ComponentEventFilter::Updated, EventType::Update)
            | (ComponentEventFilter::NodeResized, EventType::Resize)
    )
}
/// Check if the event data contains a mouse event with the expected button.
fn check_mouse_button(data: &EventData, expected: MouseButton) -> bool {
    if let EventData::Mouse(mouse_data) = data {
        mouse_data.button == expected
    } else {
        false
    }
}
/// Check if a hover filter matches the event.
fn matches_hover_filter(
    filter: &HoverEventFilter,
    event: &SyntheticEvent,
    _phase: EventPhase,
) -> bool {
    use HoverEventFilter::*;
    match (filter, &event.event_type) {
        (MouseOver, EventType::MouseOver) => true,
        (MouseDown, EventType::MouseDown) => true,
        (LeftMouseDown, EventType::MouseDown) => check_mouse_button(&event.data, MouseButton::Left),
        (RightMouseDown, EventType::MouseDown) => {
            check_mouse_button(&event.data, MouseButton::Right)
        }
        (MiddleMouseDown, EventType::MouseDown) => {
            check_mouse_button(&event.data, MouseButton::Middle)
        }
        (MouseUp, EventType::MouseUp) => true,
        (LeftMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Left),
        (RightMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Right),
        (MiddleMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Middle),
        (MouseEnter, EventType::MouseEnter) => true,
        (MouseLeave, EventType::MouseLeave) => true,
        (Scroll, EventType::Scroll) => true,
        (ScrollStart, EventType::ScrollStart) => true,
        (ScrollEnd, EventType::ScrollEnd) => true,
        (TextInput, EventType::Input) => true,
        (VirtualKeyDown, EventType::KeyDown) => true,
        (VirtualKeyUp, EventType::KeyUp) => true,
        (HoveredFile, EventType::FileHover) => true,
        (DroppedFile, EventType::FileDrop) => true,
        (HoveredFileCancelled, EventType::FileHoverCancel) => true,
        (TouchStart, EventType::TouchStart) => true,
        (TouchMove, EventType::TouchMove) => true,
        (TouchEnd, EventType::TouchEnd) => true,
        (TouchCancel, EventType::TouchCancel) => true,
        (PenDown, EventType::PenDown) => true,
        (PenMove, EventType::PenMove) => true,
        (PenUp, EventType::PenUp) => true,
        (PenEnter, EventType::PenEnter) => true,
        (PenLeave, EventType::PenLeave) => true,
        (DragStart, EventType::DragStart) => true,
        (Drag, EventType::Drag) => true,
        (DragEnd, EventType::DragEnd) => true,
        (DragEnter, EventType::DragEnter) => true,
        (DragOver, EventType::DragOver) => true,
        (DragLeave, EventType::DragLeave) => true,
        (Drop, EventType::Drop) => true,
        (DoubleClick, EventType::DoubleClick) => true,
        (SensorChanged, EventType::SensorChanged) => true,
        (GamepadInput, EventType::GamepadInput) => true,
        _ => false,
    }
}
/// Check if a focus filter matches the event.
fn matches_focus_filter(
    filter: &FocusEventFilter,
    event: &SyntheticEvent,
    _phase: EventPhase,
) -> bool {
    use FocusEventFilter::*;
    match (filter, &event.event_type) {
        (MouseOver, EventType::MouseOver) => true,
        (MouseDown, EventType::MouseDown) => true,
        (LeftMouseDown, EventType::MouseDown) => check_mouse_button(&event.data, MouseButton::Left),
        (RightMouseDown, EventType::MouseDown) => {
            check_mouse_button(&event.data, MouseButton::Right)
        }
        (MiddleMouseDown, EventType::MouseDown) => {
            check_mouse_button(&event.data, MouseButton::Middle)
        }
        (MouseUp, EventType::MouseUp) => true,
        (LeftMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Left),
        (RightMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Right),
        (MiddleMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Middle),
        (MouseEnter, EventType::MouseEnter) => true,
        (MouseLeave, EventType::MouseLeave) => true,
        (Scroll, EventType::Scroll) => true,
        (ScrollStart, EventType::ScrollStart) => true,
        (ScrollEnd, EventType::ScrollEnd) => true,
        (TextInput, EventType::Input) => true,
        (VirtualKeyDown, EventType::KeyDown) => true,
        (VirtualKeyUp, EventType::KeyUp) => true,
        (FocusReceived, EventType::Focus) => true,
        (FocusLost, EventType::Blur) => true,
        (DragStart, EventType::DragStart) => true,
        (Drag, EventType::Drag) => true,
        (DragEnd, EventType::DragEnd) => true,
        (DragEnter, EventType::DragEnter) => true,
        (DragOver, EventType::DragOver) => true,
        (DragLeave, EventType::DragLeave) => true,
        (Drop, EventType::Drop) => true,
        _ => false,
    }
}
/// Check if a window filter matches the event.
fn matches_window_filter(
    filter: &WindowEventFilter,
    event: &SyntheticEvent,
    _phase: EventPhase,
) -> bool {
    use WindowEventFilter::*;
    match (filter, &event.event_type) {
        (MouseOver, EventType::MouseOver) => true,
        (MouseDown, EventType::MouseDown) => true,
        (LeftMouseDown, EventType::MouseDown) => check_mouse_button(&event.data, MouseButton::Left),
        (RightMouseDown, EventType::MouseDown) => {
            check_mouse_button(&event.data, MouseButton::Right)
        }
        (MiddleMouseDown, EventType::MouseDown) => {
            check_mouse_button(&event.data, MouseButton::Middle)
        }
        (MouseUp, EventType::MouseUp) => true,
        (LeftMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Left),
        (RightMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Right),
        (MiddleMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Middle),
        (MouseEnter, EventType::MouseEnter) => true,
        (MouseLeave, EventType::MouseLeave) => true,
        (Scroll, EventType::Scroll) => true,
        (ScrollStart, EventType::ScrollStart) => true,
        (ScrollEnd, EventType::ScrollEnd) => true,
        (TextInput, EventType::Input) => true,
        (VirtualKeyDown, EventType::KeyDown) => true,
        (VirtualKeyUp, EventType::KeyUp) => true,
        (HoveredFile, EventType::FileHover) => true,
        (DroppedFile, EventType::FileDrop) => true,
        (HoveredFileCancelled, EventType::FileHoverCancel) => true,
        (Resized, EventType::WindowResize) => true,
        (Moved, EventType::WindowMove) => true,
        (TouchStart, EventType::TouchStart) => true,
        (TouchMove, EventType::TouchMove) => true,
        (TouchEnd, EventType::TouchEnd) => true,
        (TouchCancel, EventType::TouchCancel) => true,
        (PenDown, EventType::PenDown) => true,
        (PenMove, EventType::PenMove) => true,
        (PenUp, EventType::PenUp) => true,
        (PenEnter, EventType::PenEnter) => true,
        (PenLeave, EventType::PenLeave) => true,
        (FocusReceived, EventType::Focus) => true,
        (FocusLost, EventType::Blur) => true,
        (CloseRequested, EventType::WindowClose) => true,
        (ThemeChanged, EventType::ThemeChange) => true,
        (WindowFocusReceived, EventType::WindowFocusIn) => true,
        (WindowFocusLost, EventType::WindowFocusOut) => true,
        (SensorChanged, EventType::SensorChanged) => true,
        (GamepadInput, EventType::GamepadInput) => true,
        (DragStart, EventType::DragStart) => true,
        (Drag, EventType::Drag) => true,
        (DragEnd, EventType::DragEnd) => true,
        (DragEnter, EventType::DragEnter) => true,
        (DragOver, EventType::DragOver) => true,
        (DragLeave, EventType::DragLeave) => true,
        (Drop, EventType::Drop) => true,
        _ => false,
    }
}
/// Detect lifecycle events by comparing old and new DOM state.
///
/// This is the simple, index-based lifecycle detection that doesn't account for
/// node reordering. For more sophisticated reconciliation that can detect moves,
/// use `detect_lifecycle_events_with_reconciliation`.
///
/// Generates Mount, Unmount, and Resize events by comparing DOM hierarchies.
51
pub fn detect_lifecycle_events(
51
    old_dom_id: DomId,
51
    new_dom_id: DomId,
51
    old_hierarchy: Option<&crate::id::NodeHierarchy>,
51
    new_hierarchy: Option<&crate::id::NodeHierarchy>,
51
    old_layout: Option<&BTreeMap<NodeId, LogicalRect>>,
51
    new_layout: Option<&BTreeMap<NodeId, LogicalRect>>,
51
    timestamp: Instant,
51
) -> Vec<SyntheticEvent> {
51
    let old_nodes = collect_node_ids(old_hierarchy);
51
    let new_nodes = collect_node_ids(new_hierarchy);
51
    let mut events = Vec::new();
    // Mount events: nodes in new but not in old
51
    if let Some(layout) = new_layout {
51
        for &node_id in new_nodes.difference(&old_nodes) {
51
            events.push(create_mount_event(node_id, new_dom_id, layout, &timestamp));
51
        }
17
    }
    // Unmount events: nodes in old but not in new
51
    if let Some(layout) = old_layout {
51
        for &node_id in old_nodes.difference(&new_nodes) {
51
            events.push(create_unmount_event(
51
                node_id, old_dom_id, layout, &timestamp,
51
            ));
51
        }
17
    }
    // Resize events: nodes in both with changed bounds
51
    if let (Some(old_l), Some(new_l)) = (old_layout, new_layout) {
51
        for &node_id in old_nodes.intersection(&new_nodes) {
51
            if let Some(ev) = create_resize_event(node_id, new_dom_id, old_l, new_l, &timestamp) {
17
                events.push(ev);
34
            }
        }
34
    }
51
    events
51
}
102
fn collect_node_ids(hierarchy: Option<&crate::id::NodeHierarchy>) -> BTreeSet<NodeId> {
102
    hierarchy
102
        .map(|h| h.as_ref().linear_iter().collect())
102
        .unwrap_or_default()
102
}
119
fn create_lifecycle_event(
119
    event_type: EventType,
119
    node_id: NodeId,
119
    dom_id: DomId,
119
    timestamp: &Instant,
119
    data: LifecycleEventData,
119
) -> SyntheticEvent {
119
    let dom_node_id = DomNodeId {
119
        dom: dom_id,
119
        node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
119
    };
119
    SyntheticEvent {
119
        event_type,
119
        source: EventSource::Lifecycle,
119
        phase: EventPhase::Target,
119
        target: dom_node_id,
119
        current_target: dom_node_id,
119
        timestamp: timestamp.clone(),
119
        data: EventData::Lifecycle(data),
119
        stopped: false,
119
        stopped_immediate: false,
119
        prevented_default: false,
119
    }
119
}
51
fn create_mount_event(
51
    node_id: NodeId,
51
    dom_id: DomId,
51
    layout: &BTreeMap<NodeId, LogicalRect>,
51
    timestamp: &Instant,
51
) -> SyntheticEvent {
51
    let current_bounds = layout.get(&node_id).copied().unwrap_or(LogicalRect::zero());
51
    create_lifecycle_event(
51
        EventType::Mount,
51
        node_id,
51
        dom_id,
51
        timestamp,
51
        LifecycleEventData {
51
            reason: LifecycleReason::InitialMount,
51
            previous_bounds: None,
51
            current_bounds,
51
        },
    )
51
}
51
fn create_unmount_event(
51
    node_id: NodeId,
51
    dom_id: DomId,
51
    layout: &BTreeMap<NodeId, LogicalRect>,
51
    timestamp: &Instant,
51
) -> SyntheticEvent {
51
    let previous_bounds = layout.get(&node_id).copied().unwrap_or(LogicalRect::zero());
51
    create_lifecycle_event(
51
        EventType::Unmount,
51
        node_id,
51
        dom_id,
51
        timestamp,
51
        LifecycleEventData {
51
            reason: LifecycleReason::Unmount,
51
            previous_bounds: Some(previous_bounds),
51
            current_bounds: LogicalRect::zero(),
51
        },
    )
51
}
51
fn create_resize_event(
51
    node_id: NodeId,
51
    dom_id: DomId,
51
    old_layout: &BTreeMap<NodeId, LogicalRect>,
51
    new_layout: &BTreeMap<NodeId, LogicalRect>,
51
    timestamp: &Instant,
51
) -> Option<SyntheticEvent> {
51
    let old_bounds = *old_layout.get(&node_id)?;
17
    let new_bounds = *new_layout.get(&node_id)?;
17
    if old_bounds.size == new_bounds.size {
        return None;
17
    }
17
    Some(create_lifecycle_event(
17
        EventType::Resize,
17
        node_id,
17
        dom_id,
17
        timestamp,
17
        LifecycleEventData {
17
            reason: LifecycleReason::Resize,
17
            previous_bounds: Some(old_bounds),
17
            current_bounds: new_bounds,
17
        },
17
    ))
51
}
/// Result of lifecycle event detection with reconciliation.
///
/// Contains both the generated lifecycle events and a mapping from old to new
/// node IDs for state migration (focus, scroll, etc.).
#[derive(Debug, Clone, Default)]
pub struct LifecycleEventResult {
    /// Lifecycle events (Mount, Unmount, Resize, Update)
    pub events: Vec<SyntheticEvent>,
    /// Maps old NodeId -> new NodeId for matched nodes.
    /// Use this to migrate focus, scroll state, and other node-specific state.
    pub node_id_mapping: crate::OrderedMap<NodeId, NodeId>,
}
/// Detect lifecycle events using reconciliation with stable keys and content hashing.
///
/// This is the advanced lifecycle detection that can correctly identify:
/// - **Moves**: When a node changes position but keeps its identity (via key or hash)
/// - **Mounts**: When a new node appears
/// - **Unmounts**: When an existing node disappears
/// - **Resizes**: When a node's layout bounds change
/// - **Updates**: When a keyed node's content changes
///
/// The reconciliation strategy is:
/// 1. **Stable Key Match:** Nodes with `.with_reconciliation_key()` are matched by key (O(1))
/// 2. **Hash Match:** Nodes without keys are matched by content hash (enables reorder detection)
/// 3. **Fallback:** Unmatched nodes generate Mount/Unmount events
///
/// # Arguments
/// * `dom_id` - The DOM identifier
/// * `old_node_data` - Node data from the previous frame
/// * `new_node_data` - Node data from the current frame
/// * `old_layout` - Layout bounds from the previous frame
/// * `new_layout` - Layout bounds from the current frame
/// * `timestamp` - Current timestamp for events
///
/// # Returns
/// A `LifecycleEventResult` containing:
/// - `events`: Lifecycle events to dispatch
/// - `node_id_mapping`: Mapping from old to new NodeIds for state migration
///
/// # Example
/// ```rust,ignore
/// let result = detect_lifecycle_events_with_reconciliation(
///     dom_id,
///     &old_node_data,
///     &new_node_data,
///     &old_layout,
///     &new_layout,
///     timestamp,
/// );
///
/// // Dispatch lifecycle events
/// for event in result.events {
///     dispatch_event(event);
/// }
///
/// // Migrate focus to new node ID
/// if let Some(focused) = focus_manager.focused_node {
///     if let Some(&new_id) = result.node_id_mapping.get(&focused) {
///         focus_manager.focused_node = Some(new_id);
///     } else {
///         // Focused node was unmounted
///         focus_manager.focused_node = None;
///     }
/// }
/// ```
1
pub fn detect_lifecycle_events_with_reconciliation(
1
    dom_id: DomId,
1
    old_node_data: &[crate::dom::NodeData],
1
    new_node_data: &[crate::dom::NodeData],
1
    old_hierarchy: &[crate::styled_dom::NodeHierarchyItem],
1
    new_hierarchy: &[crate::styled_dom::NodeHierarchyItem],
1
    old_layout: &crate::OrderedMap<NodeId, LogicalRect>,
1
    new_layout: &crate::OrderedMap<NodeId, LogicalRect>,
1
    timestamp: Instant,
1
) -> LifecycleEventResult {
1
    let diff_result = crate::diff::reconcile_dom(
1
        old_node_data,
1
        new_node_data,
1
        old_hierarchy,
1
        new_hierarchy,
1
        old_layout,
1
        new_layout,
1
        dom_id,
1
        timestamp,
    );
1
    LifecycleEventResult {
1
        events: diff_result.events,
1
        node_id_mapping: crate::diff::create_migration_map(&diff_result.node_moves),
1
    }
1
}
/// Event filter that only fires when an element is hovered over.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(C)]
pub enum HoverEventFilter {
    /// Mouse moved over the hovered element
    MouseOver,
    /// Any mouse button pressed on the hovered element
    MouseDown,
    /// Left mouse button pressed on the hovered element
    LeftMouseDown,
    /// Right mouse button pressed on the hovered element
    RightMouseDown,
    /// Middle mouse button pressed on the hovered element
    MiddleMouseDown,
    /// Any mouse button released on the hovered element
    MouseUp,
    /// Left mouse button released on the hovered element
    LeftMouseUp,
    /// Right mouse button released on the hovered element
    RightMouseUp,
    /// Middle mouse button released on the hovered element
    MiddleMouseUp,
    /// Mouse entered the hovered element bounds
    MouseEnter,
    /// Mouse left the hovered element bounds
    MouseLeave,
    /// Scroll event on the hovered element
    Scroll,
    /// Scroll started on the hovered element
    ScrollStart,
    /// Scroll ended on the hovered element
    ScrollEnd,
    /// Text input received while element is hovered
    TextInput,
    /// Virtual key pressed while element is hovered
    VirtualKeyDown,
    /// Virtual key released while element is hovered
    VirtualKeyUp,
    /// File is being hovered over the element
    HoveredFile,
    /// File was dropped onto the element
    DroppedFile,
    /// File hover was cancelled
    HoveredFileCancelled,
    /// Touch started on the hovered element
    TouchStart,
    /// Touch moved on the hovered element
    TouchMove,
    /// Touch ended on the hovered element
    TouchEnd,
    /// Touch was cancelled on the hovered element
    TouchCancel,
    /// Pen/stylus made contact on the hovered element
    PenDown,
    /// Pen/stylus moved while in contact on the hovered element
    PenMove,
    /// Pen/stylus lifted from the hovered element
    PenUp,
    /// Pen/stylus entered proximity of the hovered element
    PenEnter,
    /// Pen/stylus left proximity of the hovered element
    PenLeave,
    /// Apple Pencil 2 / Surface Slim Pen 2 barrel squeeze on the hovered
    /// element. Fires once per squeeze. The matching W3C primitive is the
    /// `PointerEvent` with `pointerType: "pen"` and a transient
    /// `tangentialPressure` spike — most apps tie a tool-switch to it.
    PenSqueeze,
    /// Apple Pencil 2 side double-tap on the hovered element. Fires once
    /// per gesture. Usually mapped to "undo" or "switch eraser".
    PenDoubleTap,
    /// Pen/stylus is hovering above the hovered element (in proximity,
    /// not in contact). Continuous: fires per pen-axis update while the
    /// stylus is held above the surface. Maps to W3C
    /// `PointerEvent('pointermove')` with `buttons: 0` and
    /// `pointerType: 'pen'`.
    PenHover,
    /// New GPS / network location fix arrived for a `GeolocationProbe`
    /// in this node's subtree. Payload accessor:
    /// `CallbackInfo::get_geolocation_fix()`.
    GeolocationFix,
    /// Native geolocation subscription errored / was revoked /
    /// timed out.
    GeolocationError,
    /// A motion-sensor reading changed (P6). Window-level mirror:
    /// `WindowEventFilter::SensorChanged`. Read via `get_sensor_reading`.
    SensorChanged,
    /// A gamepad's state changed / it (dis)connected (P6). Read via
    /// `get_primary_gamepad` / `get_gamepad_state`.
    GamepadInput,
    /// Drag started on the hovered element
    DragStart,
    /// Drag in progress on the hovered element
    Drag,
    /// Drag ended on the hovered element
    DragEnd,
    /// Dragged element entered this element (drop target)
    DragEnter,
    /// Dragged element is over this element (drop target, fires continuously)
    DragOver,
    /// Dragged element left this element (drop target)
    DragLeave,
    /// Element was dropped on this element (drop target)
    Drop,
    /// Double-click detected on the hovered element
    DoubleClick,
    /// Long press detected on the hovered element
    LongPress,
    /// Swipe left gesture on the hovered element
    SwipeLeft,
    /// Swipe right gesture on the hovered element
    SwipeRight,
    /// Swipe up gesture on the hovered element
    SwipeUp,
    /// Swipe down gesture on the hovered element
    SwipeDown,
    /// Pinch-in (zoom out) gesture on the hovered element
    PinchIn,
    /// Pinch-out (zoom in) gesture on the hovered element
    PinchOut,
    /// Clockwise rotation gesture on the hovered element
    RotateClockwise,
    /// Counter-clockwise rotation gesture on the hovered element
    RotateCounterClockwise,
    // W3C MouseOut event (bubbling version of MouseLeave)
    /// Mouse left the element OR moved to a child element (W3C `mouseout`, bubbles)
    MouseOut,
    // W3C Focus events (bubbling versions)
    /// Focus is about to move INTO this element or a descendant (W3C `focusin`, bubbles)
    FocusIn,
    /// Focus is about to move OUT of this element or a descendant (W3C `focusout`, bubbles)
    FocusOut,
    // IME Composition events
    /// IME composition started (W3C `compositionstart`)
    CompositionStart,
    /// IME composition updated (W3C `compositionupdate`)
    CompositionUpdate,
    /// IME composition ended (W3C `compositionend`)
    CompositionEnd,
    // Internal System Events (not exposed to user callbacks)
    #[doc(hidden)]
    /// Internal: Single click for text cursor placement
    SystemTextSingleClick,
    #[doc(hidden)]
    /// Internal: Double click for word selection
    SystemTextDoubleClick,
    #[doc(hidden)]
    /// Internal: Triple click for paragraph/line selection
    SystemTextTripleClick,
}
impl HoverEventFilter {
    /// Check if this is an internal system event that should not be exposed to user callbacks
    pub const fn is_system_internal(&self) -> bool {
        matches!(
            self,
            HoverEventFilter::SystemTextSingleClick
                | HoverEventFilter::SystemTextDoubleClick
                | HoverEventFilter::SystemTextTripleClick
        )
    }
    pub fn to_focus_event_filter(&self) -> Option<FocusEventFilter> {
        match self {
            HoverEventFilter::MouseOver => Some(FocusEventFilter::MouseOver),
            HoverEventFilter::MouseDown => Some(FocusEventFilter::MouseDown),
            HoverEventFilter::LeftMouseDown => Some(FocusEventFilter::LeftMouseDown),
            HoverEventFilter::RightMouseDown => Some(FocusEventFilter::RightMouseDown),
            HoverEventFilter::MiddleMouseDown => Some(FocusEventFilter::MiddleMouseDown),
            HoverEventFilter::MouseUp => Some(FocusEventFilter::MouseUp),
            HoverEventFilter::LeftMouseUp => Some(FocusEventFilter::LeftMouseUp),
            HoverEventFilter::RightMouseUp => Some(FocusEventFilter::RightMouseUp),
            HoverEventFilter::MiddleMouseUp => Some(FocusEventFilter::MiddleMouseUp),
            HoverEventFilter::MouseEnter => Some(FocusEventFilter::MouseEnter),
            HoverEventFilter::MouseLeave => Some(FocusEventFilter::MouseLeave),
            HoverEventFilter::Scroll => Some(FocusEventFilter::Scroll),
            HoverEventFilter::ScrollStart => Some(FocusEventFilter::ScrollStart),
            HoverEventFilter::ScrollEnd => Some(FocusEventFilter::ScrollEnd),
            HoverEventFilter::TextInput => Some(FocusEventFilter::TextInput),
            HoverEventFilter::VirtualKeyDown => Some(FocusEventFilter::VirtualKeyDown),
            HoverEventFilter::VirtualKeyUp => Some(FocusEventFilter::VirtualKeyUp),
            HoverEventFilter::HoveredFile => None,
            HoverEventFilter::DroppedFile => None,
            HoverEventFilter::HoveredFileCancelled => None,
            HoverEventFilter::TouchStart => None,
            HoverEventFilter::TouchMove => None,
            HoverEventFilter::TouchEnd => None,
            HoverEventFilter::TouchCancel => None,
            HoverEventFilter::PenDown => Some(FocusEventFilter::PenDown),
            HoverEventFilter::PenMove => Some(FocusEventFilter::PenMove),
            HoverEventFilter::PenUp => Some(FocusEventFilter::PenUp),
            HoverEventFilter::PenEnter => None,
            HoverEventFilter::PenLeave => None,
            HoverEventFilter::PenSqueeze => None,
            HoverEventFilter::PenDoubleTap => None,
            HoverEventFilter::PenHover => None,
            HoverEventFilter::GeolocationFix => None,
            HoverEventFilter::GeolocationError => None,
            HoverEventFilter::SensorChanged => None,
            HoverEventFilter::GamepadInput => None,
            HoverEventFilter::DragStart => Some(FocusEventFilter::DragStart),
            HoverEventFilter::Drag => Some(FocusEventFilter::Drag),
            HoverEventFilter::DragEnd => Some(FocusEventFilter::DragEnd),
            HoverEventFilter::DragEnter => Some(FocusEventFilter::DragEnter),
            HoverEventFilter::DragOver => Some(FocusEventFilter::DragOver),
            HoverEventFilter::DragLeave => Some(FocusEventFilter::DragLeave),
            HoverEventFilter::Drop => Some(FocusEventFilter::Drop),
            HoverEventFilter::DoubleClick => Some(FocusEventFilter::DoubleClick),
            HoverEventFilter::LongPress => Some(FocusEventFilter::LongPress),
            HoverEventFilter::SwipeLeft => Some(FocusEventFilter::SwipeLeft),
            HoverEventFilter::SwipeRight => Some(FocusEventFilter::SwipeRight),
            HoverEventFilter::SwipeUp => Some(FocusEventFilter::SwipeUp),
            HoverEventFilter::SwipeDown => Some(FocusEventFilter::SwipeDown),
            HoverEventFilter::PinchIn => Some(FocusEventFilter::PinchIn),
            HoverEventFilter::PinchOut => Some(FocusEventFilter::PinchOut),
            HoverEventFilter::RotateClockwise => Some(FocusEventFilter::RotateClockwise),
            HoverEventFilter::RotateCounterClockwise => {
                Some(FocusEventFilter::RotateCounterClockwise)
            }
            HoverEventFilter::MouseOut => Some(FocusEventFilter::MouseLeave), // mouseout → closest focus equivalent
            HoverEventFilter::FocusIn => Some(FocusEventFilter::FocusIn),
            HoverEventFilter::FocusOut => Some(FocusEventFilter::FocusOut),
            HoverEventFilter::CompositionStart => Some(FocusEventFilter::CompositionStart),
            HoverEventFilter::CompositionUpdate => Some(FocusEventFilter::CompositionUpdate),
            HoverEventFilter::CompositionEnd => Some(FocusEventFilter::CompositionEnd),
            // System internal events - don't convert to focus events
            HoverEventFilter::SystemTextSingleClick => None,
            HoverEventFilter::SystemTextDoubleClick => None,
            HoverEventFilter::SystemTextTripleClick => None,
        }
    }
}
/// Event filter similar to `HoverEventFilter` that only fires when the element is focused.
///
/// **Important**: In order for this to fire, the item must have a `tabindex` attribute
/// (to indicate that the item is focus-able).
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(C)]
pub enum FocusEventFilter {
    /// Mouse moved over the focused element
    MouseOver,
    /// Any mouse button pressed on the focused element
    MouseDown,
    /// Left mouse button pressed on the focused element
    LeftMouseDown,
    /// Right mouse button pressed on the focused element
    RightMouseDown,
    /// Middle mouse button pressed on the focused element
    MiddleMouseDown,
    /// Any mouse button released on the focused element
    MouseUp,
    /// Left mouse button released on the focused element
    LeftMouseUp,
    /// Right mouse button released on the focused element
    RightMouseUp,
    /// Middle mouse button released on the focused element
    MiddleMouseUp,
    /// Mouse entered the focused element bounds
    MouseEnter,
    /// Mouse left the focused element bounds
    MouseLeave,
    /// Scroll event on the focused element
    Scroll,
    /// Scroll started on the focused element
    ScrollStart,
    /// Scroll ended on the focused element
    ScrollEnd,
    /// Text input received while element is focused
    TextInput,
    /// Virtual key pressed while element is focused
    VirtualKeyDown,
    /// Virtual key released while element is focused
    VirtualKeyUp,
    /// Element received keyboard focus
    FocusReceived,
    /// Element lost keyboard focus
    FocusLost,
    /// Pen/stylus made contact on the focused element
    PenDown,
    /// Pen/stylus moved while in contact on the focused element
    PenMove,
    /// Pen/stylus lifted from the focused element
    PenUp,
    /// Drag started on the focused element
    DragStart,
    /// Drag in progress on the focused element
    Drag,
    /// Drag ended on the focused element
    DragEnd,
    /// Dragged element entered this focused element (drop target)
    DragEnter,
    /// Dragged element is over this focused element (drop target)
    DragOver,
    /// Dragged element left this focused element (drop target)
    DragLeave,
    /// Element was dropped on this focused element (drop target)
    Drop,
    /// Double-click detected on the focused element
    DoubleClick,
    /// Long press detected on the focused element
    LongPress,
    /// Swipe left gesture on the focused element
    SwipeLeft,
    /// Swipe right gesture on the focused element
    SwipeRight,
    /// Swipe up gesture on the focused element
    SwipeUp,
    /// Swipe down gesture on the focused element
    SwipeDown,
    /// Pinch-in (zoom out) gesture on the focused element
    PinchIn,
    /// Pinch-out (zoom in) gesture on the focused element
    PinchOut,
    /// Clockwise rotation gesture on the focused element
    RotateClockwise,
    /// Counter-clockwise rotation gesture on the focused element
    RotateCounterClockwise,
    // W3C Focus events (bubbling versions, fires on focused element when focus changes)
    /// Focus moved into this element or a descendant (W3C `focusin`)
    FocusIn,
    /// Focus moved out of this element or a descendant (W3C `focusout`)
    FocusOut,
    // IME Composition events
    /// IME composition started (W3C `compositionstart`)
    CompositionStart,
    /// IME composition updated (W3C `compositionupdate`)
    CompositionUpdate,
    /// IME composition ended (W3C `compositionend`)
    CompositionEnd,
}
/// Event filter that fires when any action fires on the entire window
/// (regardless of whether any element is hovered or focused over).
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(C)]
pub enum WindowEventFilter {
    /// Mouse moved anywhere in window
    MouseOver,
    /// Any mouse button pressed anywhere in window
    MouseDown,
    /// Left mouse button pressed anywhere in window
    LeftMouseDown,
    /// Right mouse button pressed anywhere in window
    RightMouseDown,
    /// Middle mouse button pressed anywhere in window
    MiddleMouseDown,
    /// Any mouse button released anywhere in window
    MouseUp,
    /// Left mouse button released anywhere in window
    LeftMouseUp,
    /// Right mouse button released anywhere in window
    RightMouseUp,
    /// Middle mouse button released anywhere in window
    MiddleMouseUp,
    /// Mouse entered the window
    MouseEnter,
    /// Mouse left the window
    MouseLeave,
    /// Scroll event anywhere in window
    Scroll,
    /// Scroll started anywhere in window
    ScrollStart,
    /// Scroll ended anywhere in window
    ScrollEnd,
    /// Text input received in window
    TextInput,
    /// Virtual key pressed in window
    VirtualKeyDown,
    /// Virtual key released in window
    VirtualKeyUp,
    /// File is being hovered over the window
    HoveredFile,
    /// File was dropped onto the window
    DroppedFile,
    /// File hover was cancelled
    HoveredFileCancelled,
    /// Window was resized
    Resized,
    /// Window was moved
    Moved,
    /// Touch started anywhere in window
    TouchStart,
    /// Touch moved anywhere in window
    TouchMove,
    /// Touch ended anywhere in window
    TouchEnd,
    /// Touch was cancelled
    TouchCancel,
    /// Window received focus
    FocusReceived,
    /// Window lost focus
    FocusLost,
    /// Window close was requested
    CloseRequested,
    /// System theme changed (light/dark mode)
    ThemeChanged,
    /// Window received OS-level focus
    WindowFocusReceived,
    /// Window lost OS-level focus
    WindowFocusLost,
    /// Pen/stylus made contact anywhere in window
    PenDown,
    /// Pen/stylus moved while in contact anywhere in window
    PenMove,
    /// Pen/stylus lifted anywhere in window
    PenUp,
    /// Pen/stylus entered window proximity
    PenEnter,
    /// Pen/stylus left window proximity
    PenLeave,
    /// Pen barrel-squeeze gesture fired in the window. See
    /// [`HoverEventFilter::PenSqueeze`].
    PenSqueeze,
    /// Pen side double-tap gesture fired in the window. See
    /// [`HoverEventFilter::PenDoubleTap`].
    PenDoubleTap,
    /// Pen hover in the window (in proximity, not in contact). See
    /// [`HoverEventFilter::PenHover`].
    PenHover,
    /// New GPS / network location fix arrived. Payload accessor:
    /// `CallbackInfo::get_geolocation_fix()`. Window-level rather
    /// than per-node because the user's location isn't bound to any
    /// particular DOM node — but a node-level mirror
    /// (`HoverEventFilter::GeolocationFix`) fires on every
    /// `GeolocationProbe` in the tree as well, for the common
    /// "redraw this node when the location changes" pattern.
    GeolocationFix,
    /// Native geolocation subscription dropped or errored (signal
    /// lost, no provider, permission revoked mid-session).
    GeolocationError,
    /// A motion-sensor reading changed (P6). Fires window-level (the device
    /// isn't bound to a node); read via `CallbackInfo::get_sensor_reading`.
    SensorChanged,
    /// A gamepad's buttons / axes changed or it (dis)connected (P6); read via
    /// `get_primary_gamepad` / `get_gamepad_state`.
    GamepadInput,
    /// Drag started anywhere in window
    DragStart,
    /// Drag in progress anywhere in window
    Drag,
    /// Drag ended anywhere in window
    DragEnd,
    /// Dragged element entered a drop target in window
    DragEnter,
    /// Dragged element is over a drop target in window
    DragOver,
    /// Dragged element left a drop target in window
    DragLeave,
    /// Element was dropped on a drop target in window
    Drop,
    /// Double-click detected anywhere in window
    DoubleClick,
    /// Long press detected anywhere in window
    LongPress,
    /// Swipe left gesture anywhere in window
    SwipeLeft,
    /// Swipe right gesture anywhere in window
    SwipeRight,
    /// Swipe up gesture anywhere in window
    SwipeUp,
    /// Swipe down gesture anywhere in window
    SwipeDown,
    /// Pinch-in (zoom out) gesture anywhere in window
    PinchIn,
    /// Pinch-out (zoom in) gesture anywhere in window
    PinchOut,
    /// Clockwise rotation gesture anywhere in window
    RotateClockwise,
    /// Counter-clockwise rotation gesture anywhere in window
    RotateCounterClockwise,
    /// The window's DPI scale factor changed (e.g., moved to a monitor with
    /// different scaling). The new DPI is available via `CallbackInfo::get_hidpi_factor()`.
    DpiChanged,
    /// The window moved to a different monitor. The new monitor is available
    /// via `CallbackInfo::get_current_monitor()`.
    MonitorChanged,
}
impl WindowEventFilter {
    pub fn to_hover_event_filter(&self) -> Option<HoverEventFilter> {
        match self {
            WindowEventFilter::MouseOver => Some(HoverEventFilter::MouseOver),
            WindowEventFilter::MouseDown => Some(HoverEventFilter::MouseDown),
            WindowEventFilter::LeftMouseDown => Some(HoverEventFilter::LeftMouseDown),
            WindowEventFilter::RightMouseDown => Some(HoverEventFilter::RightMouseDown),
            WindowEventFilter::MiddleMouseDown => Some(HoverEventFilter::MiddleMouseDown),
            WindowEventFilter::MouseUp => Some(HoverEventFilter::MouseUp),
            WindowEventFilter::LeftMouseUp => Some(HoverEventFilter::LeftMouseUp),
            WindowEventFilter::RightMouseUp => Some(HoverEventFilter::RightMouseUp),
            WindowEventFilter::MiddleMouseUp => Some(HoverEventFilter::MiddleMouseUp),
            WindowEventFilter::Scroll => Some(HoverEventFilter::Scroll),
            WindowEventFilter::ScrollStart => Some(HoverEventFilter::ScrollStart),
            WindowEventFilter::ScrollEnd => Some(HoverEventFilter::ScrollEnd),
            WindowEventFilter::TextInput => Some(HoverEventFilter::TextInput),
            WindowEventFilter::VirtualKeyDown => Some(HoverEventFilter::VirtualKeyDown),
            WindowEventFilter::VirtualKeyUp => Some(HoverEventFilter::VirtualKeyUp),
            WindowEventFilter::HoveredFile => Some(HoverEventFilter::HoveredFile),
            WindowEventFilter::DroppedFile => Some(HoverEventFilter::DroppedFile),
            WindowEventFilter::HoveredFileCancelled => Some(HoverEventFilter::HoveredFileCancelled),
            // MouseEnter and MouseLeave on the **window** - does not mean a mouseenter
            // and a mouseleave on the hovered element
            WindowEventFilter::MouseEnter => None,
            WindowEventFilter::MouseLeave => None,
            WindowEventFilter::Resized => None,
            WindowEventFilter::Moved => None,
            WindowEventFilter::TouchStart => Some(HoverEventFilter::TouchStart),
            WindowEventFilter::TouchMove => Some(HoverEventFilter::TouchMove),
            WindowEventFilter::TouchEnd => Some(HoverEventFilter::TouchEnd),
            WindowEventFilter::TouchCancel => Some(HoverEventFilter::TouchCancel),
            WindowEventFilter::FocusReceived => None,
            WindowEventFilter::FocusLost => None,
            WindowEventFilter::CloseRequested => None,
            WindowEventFilter::ThemeChanged => None,
            WindowEventFilter::WindowFocusReceived => None, // specific to window!
            WindowEventFilter::WindowFocusLost => None,     // specific to window!
            WindowEventFilter::PenDown => Some(HoverEventFilter::PenDown),
            WindowEventFilter::PenMove => Some(HoverEventFilter::PenMove),
            WindowEventFilter::PenUp => Some(HoverEventFilter::PenUp),
            WindowEventFilter::PenEnter => Some(HoverEventFilter::PenEnter),
            WindowEventFilter::PenLeave => Some(HoverEventFilter::PenLeave),
            WindowEventFilter::PenSqueeze => Some(HoverEventFilter::PenSqueeze),
            WindowEventFilter::PenDoubleTap => Some(HoverEventFilter::PenDoubleTap),
            WindowEventFilter::PenHover => Some(HoverEventFilter::PenHover),
            WindowEventFilter::GeolocationFix => Some(HoverEventFilter::GeolocationFix),
            WindowEventFilter::GeolocationError => Some(HoverEventFilter::GeolocationError),
            WindowEventFilter::SensorChanged => Some(HoverEventFilter::SensorChanged),
            WindowEventFilter::GamepadInput => Some(HoverEventFilter::GamepadInput),
            WindowEventFilter::DragStart => Some(HoverEventFilter::DragStart),
            WindowEventFilter::Drag => Some(HoverEventFilter::Drag),
            WindowEventFilter::DragEnd => Some(HoverEventFilter::DragEnd),
            WindowEventFilter::DragEnter => Some(HoverEventFilter::DragEnter),
            WindowEventFilter::DragOver => Some(HoverEventFilter::DragOver),
            WindowEventFilter::DragLeave => Some(HoverEventFilter::DragLeave),
            WindowEventFilter::Drop => Some(HoverEventFilter::Drop),
            WindowEventFilter::DoubleClick => Some(HoverEventFilter::DoubleClick),
            WindowEventFilter::LongPress => Some(HoverEventFilter::LongPress),
            WindowEventFilter::SwipeLeft => Some(HoverEventFilter::SwipeLeft),
            WindowEventFilter::SwipeRight => Some(HoverEventFilter::SwipeRight),
            WindowEventFilter::SwipeUp => Some(HoverEventFilter::SwipeUp),
            WindowEventFilter::SwipeDown => Some(HoverEventFilter::SwipeDown),
            WindowEventFilter::PinchIn => Some(HoverEventFilter::PinchIn),
            WindowEventFilter::PinchOut => Some(HoverEventFilter::PinchOut),
            WindowEventFilter::RotateClockwise => Some(HoverEventFilter::RotateClockwise),
            WindowEventFilter::RotateCounterClockwise => {
                Some(HoverEventFilter::RotateCounterClockwise)
            }
            // Window-specific events with no hover equivalent
            WindowEventFilter::DpiChanged => None,
            WindowEventFilter::MonitorChanged => None,
        }
    }
}
/// Defines events related to the lifecycle of a DOM node itself.
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub enum ComponentEventFilter {
    /// Fired after the component is first mounted into the DOM.
    AfterMount,
    /// Fired just before the component is removed from the DOM.
    BeforeUnmount,
    /// Fired when the node's layout rectangle has been resized.
    NodeResized,
    /// Fired to trigger the default action for an accessibility component.
    DefaultAction,
    /// Fired when the component becomes selected.
    Selected,
    /// Fired when a keyed component's content has changed (props/state update).
    Updated,
}
/// Defines application-level events not tied to a specific window or node.
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub enum ApplicationEventFilter {
    /// Fired when a new hardware device is connected.
    DeviceConnected,
    /// Fired when a hardware device is disconnected.
    DeviceDisconnected,
    /// Fired when a new monitor/display is connected to the system.
    /// Callback receives updated monitor list via `CallbackInfo::get_monitors()`.
    MonitorConnected,
    /// Fired when a monitor/display is disconnected from the system.
    MonitorDisconnected,
}
/// Sets the target for what events can reach the callbacks specifically.
///
/// This determines the condition under which an event is fired, such as whether
/// the node is hovered, focused, or if the event is window-global.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(C, u8)]
pub enum EventFilter {
    /// Calls the attached callback when the mouse is actively over the
    /// given element.
    Hover(HoverEventFilter),
    /// Calls the attached callback when the element is currently focused.
    Focus(FocusEventFilter),
    /// Calls the callback when anything related to the window is happening.
    /// The "hit item" will be the root item of the DOM.
    /// For example, this can be useful for tracking the mouse position
    /// (in relation to the window). In difference to `Desktop`, this only
    /// fires when the window is focused.
    ///
    /// This can also be good for capturing controller input, touch input
    /// (i.e. global gestures that aren't attached to any component, but rather
    /// the "window" itself).
    Window(WindowEventFilter),
    /// API stub: Something happened with the node itself (node resized, created or removed).
    Component(ComponentEventFilter),
    /// Something happened with the application (started, shutdown, device plugged in).
    Application(ApplicationEventFilter),
}
impl EventFilter {
    pub const fn is_focus_callback(&self) -> bool {
        match self {
            EventFilter::Focus(_) => true,
            _ => false,
        }
    }
    pub const fn is_window_callback(&self) -> bool {
        match self {
            EventFilter::Window(_) => true,
            _ => false,
        }
    }
}
/// Creates a function inside an impl <enum type> block that returns a single
/// variant if the enum is that variant.
macro_rules! get_single_enum_type {
    ($fn_name:ident, $enum_name:ident:: $variant:ident($return_type:ty)) => {
        pub fn $fn_name(&self) -> Option<$return_type> {
            use self::$enum_name::*;
            match self {
                $variant(e) => Some(*e),
                _ => None,
            }
        }
    };
}
impl EventFilter {
    get_single_enum_type!(as_hover_event_filter, EventFilter::Hover(HoverEventFilter));
    get_single_enum_type!(as_focus_event_filter, EventFilter::Focus(FocusEventFilter));
    get_single_enum_type!(
        as_window_event_filter,
        EventFilter::Window(WindowEventFilter)
    );
}
/// Convert from `On` enum to `EventFilter`.
///
/// This determines which specific filter variant is used based on the event type.
/// For example, `On::TextInput` becomes a Focus event filter, while `On::VirtualKeyDown`
/// becomes a Window event filter (since it's global to the window).
impl From<On> for EventFilter {
    fn from(input: On) -> EventFilter {
        use crate::dom::On::*;
        match input {
            MouseOver => EventFilter::Hover(HoverEventFilter::MouseOver),
            MouseDown => EventFilter::Hover(HoverEventFilter::MouseDown),
            LeftMouseDown => EventFilter::Hover(HoverEventFilter::LeftMouseDown),
            MiddleMouseDown => EventFilter::Hover(HoverEventFilter::MiddleMouseDown),
            RightMouseDown => EventFilter::Hover(HoverEventFilter::RightMouseDown),
            MouseUp => EventFilter::Hover(HoverEventFilter::MouseUp),
            LeftMouseUp => EventFilter::Hover(HoverEventFilter::LeftMouseUp),
            MiddleMouseUp => EventFilter::Hover(HoverEventFilter::MiddleMouseUp),
            RightMouseUp => EventFilter::Hover(HoverEventFilter::RightMouseUp),
            MouseEnter => EventFilter::Hover(HoverEventFilter::MouseEnter),
            MouseLeave => EventFilter::Hover(HoverEventFilter::MouseLeave),
            Scroll => EventFilter::Hover(HoverEventFilter::Scroll),
            TextInput => EventFilter::Focus(FocusEventFilter::TextInput), // focus!
            VirtualKeyDown => EventFilter::Window(WindowEventFilter::VirtualKeyDown), // window!
            VirtualKeyUp => EventFilter::Window(WindowEventFilter::VirtualKeyUp), // window!
            HoveredFile => EventFilter::Hover(HoverEventFilter::HoveredFile),
            DroppedFile => EventFilter::Hover(HoverEventFilter::DroppedFile),
            HoveredFileCancelled => EventFilter::Hover(HoverEventFilter::HoveredFileCancelled),
            FocusReceived => EventFilter::Focus(FocusEventFilter::FocusReceived), // focus!
            FocusLost => EventFilter::Focus(FocusEventFilter::FocusLost),         // focus!
            // Accessibility events - treat as hover events (element-specific)
            Default => EventFilter::Hover(HoverEventFilter::MouseUp), // Default action = click
            Collapse => EventFilter::Hover(HoverEventFilter::MouseUp), // Collapse = click
            Expand => EventFilter::Hover(HoverEventFilter::MouseUp),  // Expand = click
            Increment => EventFilter::Hover(HoverEventFilter::MouseUp), // Increment = click
            Decrement => EventFilter::Hover(HoverEventFilter::MouseUp), // Decrement = click
        }
    }
}
// Cross-Platform Event Dispatch System
// NOTE: The old dispatch_synthetic_events / CallbackTarget / CallbackToInvoke / EventDispatchResult
// pipeline has been removed. Event dispatch now goes through dispatch_events_propagated() in
// event_v2.rs which uses propagate_event() for W3C Capture→Target→Bubble propagation.
/// Trait for managers to provide their pending events.
///
/// Each manager (TextInputManager, ScrollManager, etc.) implements this to
/// report what events occurred since the last frame. This enables a unified,
/// lazy event determination system.
pub trait EventProvider {
    /// Get all pending events from this manager.
    ///
    /// Events should include:
    ///
    /// - `target`: The DomNodeId that was affected
    /// - `event_type`: What happened (Input, Scroll, Focus, etc.)
    /// - `source`: EventSource::User for input, EventSource::Programmatic for API calls
    /// - `data`: Type-specific event data
    ///
    /// After calling this, the manager should mark events as "read" so they
    /// aren't returned again next frame.
    fn get_pending_events(&self, timestamp: Instant) -> Vec<SyntheticEvent>;
}
/// Deduplicate synthetic events by (target node, event type).
///
/// Groups by (target.dom, target.node, event_type), keeping the latest timestamp.
462
pub fn deduplicate_synthetic_events(mut events: Vec<SyntheticEvent>) -> Vec<SyntheticEvent> {
462
    if events.len() <= 1 {
462
        return events;
    }
    events.sort_by_key(|e| (e.target.dom, e.target.node, e.event_type));
    // Coalesce consecutive events with same target and event_type
    let mut result = Vec::with_capacity(events.len());
    let mut iter = events.into_iter();
    if let Some(mut prev) = iter.next() {
        for curr in iter {
            if prev.target == curr.target && prev.event_type == curr.event_type {
                // Keep the one with later timestamp
                prev = if curr.timestamp > prev.timestamp {
                    curr
                } else {
                    prev
                };
            } else {
                result.push(prev);
                prev = curr;
            }
        }
        result.push(prev);
    }
    result
462
}
/// Convert EventType to EventFilters (returns multiple filters for generic + specific events)
///
/// For mouse button events, returns both generic (MouseUp) AND button-specific (LeftMouseUp/RightMouseUp).
/// The button-specific filter is derived from the EventData::Mouse payload.
126
pub fn event_type_to_filters(event_type: EventType, event_data: &EventData) -> Vec<EventFilter> {
    use EventFilter as EF;
    use EventType as E;
    use FocusEventFilter as F;
    use HoverEventFilter as H;
    use WindowEventFilter as W;
    // Helper: get the button-specific MouseDown filter from EventData
126
    let button_specific_down = || -> Option<EventFilter> {
        match event_data {
            EventData::Mouse(m) => match m.button {
                MouseButton::Left => Some(EF::Hover(H::LeftMouseDown)),
                MouseButton::Right => Some(EF::Hover(H::RightMouseDown)),
                MouseButton::Middle => Some(EF::Hover(H::MiddleMouseDown)),
                MouseButton::Other(_) => None, // no specific filter for other buttons
            },
            _ => Some(EF::Hover(H::LeftMouseDown)), // fallback
        }
    };
126
    let button_specific_up = || -> Option<EventFilter> {
        match event_data {
            EventData::Mouse(m) => match m.button {
                MouseButton::Left => Some(EF::Hover(H::LeftMouseUp)),
                MouseButton::Right => Some(EF::Hover(H::RightMouseUp)),
                MouseButton::Middle => Some(EF::Hover(H::MiddleMouseUp)),
                MouseButton::Other(_) => None, // no specific filter for other buttons
            },
            _ => Some(EF::Hover(H::LeftMouseUp)), // fallback
        }
    };
126
    match event_type {
        // Mouse button events - return BOTH generic and button-specific
        E::MouseDown => {
            let mut v = vec![EF::Hover(H::MouseDown)];
            if let Some(f) = button_specific_down() { v.push(f); }
            v
        }
        E::MouseUp => {
            let mut v = vec![EF::Hover(H::MouseUp)];
            if let Some(f) = button_specific_up() { v.push(f); }
            v
        }
        // Click uses LeftMouseDown (W3C: click is left-button only)
        E::Click => vec![EF::Hover(H::LeftMouseDown)],
        // Other mouse events
        E::MouseOver => vec![EF::Hover(H::MouseOver)],
        E::MouseEnter => vec![EF::Hover(H::MouseEnter)],
        E::MouseLeave => vec![EF::Hover(H::MouseLeave)],
        E::MouseOut => vec![EF::Hover(H::MouseOut)],
        E::DoubleClick => vec![EF::Hover(H::DoubleClick), EF::Window(W::DoubleClick)],
        E::ContextMenu => vec![EF::Hover(H::RightMouseDown)],
        // Keyboard events
        E::KeyDown => vec![EF::Focus(F::VirtualKeyDown)],
        E::KeyUp => vec![EF::Focus(F::VirtualKeyUp)],
        E::KeyPress => vec![EF::Focus(F::TextInput)],
        // IME Composition events
        E::CompositionStart => vec![EF::Hover(H::CompositionStart), EF::Focus(F::CompositionStart)],
        E::CompositionUpdate => vec![EF::Hover(H::CompositionUpdate), EF::Focus(F::CompositionUpdate)],
        E::CompositionEnd => vec![EF::Hover(H::CompositionEnd), EF::Focus(F::CompositionEnd)],
        // Focus events
        E::Focus => vec![EF::Focus(F::FocusReceived)],
        E::Blur => vec![EF::Focus(F::FocusLost)],
        E::FocusIn => vec![EF::Hover(H::FocusIn), EF::Focus(F::FocusIn)],
        E::FocusOut => vec![EF::Hover(H::FocusOut), EF::Focus(F::FocusOut)],
        // Input events
        E::Input | E::Change => vec![EF::Focus(F::TextInput)],
        // Scroll events
        E::Scroll | E::ScrollStart | E::ScrollEnd => vec![EF::Hover(H::Scroll)],
        // Drag events
        E::DragStart => vec![EF::Hover(H::DragStart), EF::Window(W::DragStart)],
        E::Drag => vec![EF::Hover(H::Drag), EF::Window(W::Drag)],
        E::DragEnd => vec![EF::Hover(H::DragEnd), EF::Window(W::DragEnd)],
        E::DragEnter => vec![EF::Hover(H::DragEnter), EF::Window(W::DragEnter)],
        E::DragOver => vec![EF::Hover(H::DragOver), EF::Window(W::DragOver)],
        E::DragLeave => vec![EF::Hover(H::DragLeave), EF::Window(W::DragLeave)],
        E::Drop => vec![EF::Hover(H::Drop), EF::Window(W::Drop)],
        // Touch events
42
        E::TouchStart => vec![EF::Hover(H::TouchStart)],
        E::TouchMove => vec![EF::Hover(H::TouchMove)],
        E::TouchEnd => vec![EF::Hover(H::TouchEnd)],
        E::TouchCancel => vec![EF::Hover(H::TouchCancel)],
        // Window events
        E::WindowResize => vec![EF::Window(W::Resized)],
        E::WindowMove => vec![EF::Window(W::Moved)],
        E::WindowClose => vec![EF::Window(W::CloseRequested)],
        E::WindowFocusIn => vec![EF::Window(W::WindowFocusReceived)],
        E::WindowFocusOut => vec![EF::Window(W::WindowFocusLost)],
        E::ThemeChange => vec![EF::Window(W::ThemeChanged)],
        E::WindowDpiChanged => vec![EF::Window(W::DpiChanged)],
        E::WindowMonitorChanged => vec![EF::Window(W::MonitorChanged)],
        // Application events
        E::MonitorConnected => vec![EF::Application(ApplicationEventFilter::MonitorConnected)],
        E::MonitorDisconnected => vec![EF::Application(ApplicationEventFilter::MonitorDisconnected)],
        // File events
        E::FileHover => vec![EF::Hover(H::HoveredFile)],
        E::FileDrop => vec![EF::Hover(H::DroppedFile)],
        E::FileHoverCancel => vec![EF::Hover(H::HoveredFileCancelled)],
        // Lifecycle events — dispatched on the target node via EventFilter::Component.
        // Both Mount and Unmount map to their respective Component filters so that
        // `.add_callback(EventFilter::Component(ComponentEventFilter::AfterMount))`
        // actually fires after reconcile_dom emits a SyntheticEvent{EventType::Mount,..}.
        E::Mount => vec![EF::Component(ComponentEventFilter::AfterMount)],
        E::Unmount => vec![EF::Component(ComponentEventFilter::BeforeUnmount)],
        E::Update => vec![EF::Component(ComponentEventFilter::Updated)],
        E::Resize => vec![EF::Component(ComponentEventFilter::NodeResized)],
        // Hardware input-device events (P6) — node-level Hover mirror + the
        // window-level filter (the device isn't bound to a node).
42
        E::SensorChanged => vec![EF::Hover(H::SensorChanged), EF::Window(W::SensorChanged)],
42
        E::GamepadInput => vec![EF::Hover(H::GamepadInput), EF::Window(W::GamepadInput)],
        // Unsupported events
        _ => vec![],
    }
126
}
// Internal System Event Processing
/// Framework-determined side effects (system changes).
///
/// Unlike `CallbackChange` (from user callbacks), these are determined by the
/// framework's event analysis: hit tests, gesture detection, focus rules,
/// text selection, keyboard shortcuts, etc.
///
/// Both `CallbackChange` (user) and `SystemChange` (framework) are processed
/// through exhaustive match on `PlatformWindowV2` — adding a new variant
/// causes a compile error in `apply_system_change()`.
#[derive(Debug, Clone, PartialEq)]
#[must_use = "SystemChange must be processed through apply_system_change()"]
pub enum SystemChange {
    // === Text Selection ===
    /// Process a mouse click for text selection (single/double/triple click).
    TextSelectionClick {
        position: LogicalPosition,
        timestamp: Instant,
    },
    /// Extend text selection via mouse drag.
    TextSelectionDrag {
        start_position: LogicalPosition,
        current_position: LogicalPosition,
    },
    /// Unified selection operation: cursor movement, selection extension, or deletion.
    ///
    /// Replaces the old ArrowKeyNavigation and DeleteTextSelection variants.
    /// Every keyboard shortcut maps to a single SelectionOp — see its docs.
    ApplySelectionOp {
        target: DomNodeId,
        op: SelectionOp,
    },
    // === Keyboard Shortcuts ===
    /// Copy selected text to system clipboard (Ctrl+C / Cmd+C).
    CopyToClipboard,
    /// Cut selected text to clipboard and delete (Ctrl+X / Cmd+X).
    CutToClipboard { target: DomNodeId },
    /// Paste text from system clipboard at cursor (Ctrl+V / Cmd+V).
    PasteFromClipboard,
    /// Select all text in focused node (Ctrl+A / Cmd+A).
    SelectAllText,
    /// Undo last text edit (Ctrl+Z / Cmd+Z).
    UndoTextEdit { target: DomNodeId },
    /// Redo last undone edit (Ctrl+Y / Ctrl+Shift+Z / Cmd+Shift+Z).
    RedoTextEdit { target: DomNodeId },
    // === Multi-Cursor ===
    /// Add a cursor at the clicked position (Ctrl+Click).
    /// The position will be hit-tested to find the text cursor location.
    AddCursorAtClick {
        position: LogicalPosition,
    },
    /// Select the next occurrence of the current selection's text (Ctrl+D).
    /// If the primary selection is a cursor, expand it to the word first.
    SelectNextOccurrence {
        target: DomNodeId,
    },
    // === Text Input ===
    /// Apply pending text input from platform (keyboard/IME).
    ApplyPendingTextInput,
    /// Apply text changeset (incremental relayout).
    ApplyTextChangeset,
    // === Drag & Drop ===
    /// Activate node drag on a draggable element.
    ActivateNodeDrag {
        dom_id: crate::dom::DomId,
        node_id: crate::id::NodeId,
    },
    /// Activate window drag (CSD titlebar).
    ActivateWindowDrag,
    /// Set up drag visual state (:dragging pseudo-state, GPU transform key, DragDropManager sync).
    InitDragVisualState,
    /// Set :drag-over pseudo-state on a target node.
    SetDragOverState { target: DomNodeId, active: bool },
    /// Update current drop target in drag context.
    UpdateDropTarget { target: DomNodeId },
    /// Update GPU transform for active node drag.
    UpdateDragGpuTransform,
    /// End drag: clear pseudo-states, remove GPU keys, end drag session.
    DeactivateDrag,
    // === Focus ===
    /// Change focus to a new target (or clear focus if None).
    /// Handles: set_focused_node, apply_focus_restyle, scroll_node_into_view,
    /// cursor_blink_timer start/stop.
    SetFocus {
        new_focus: Option<DomNodeId>,
        old_focus: Option<DomNodeId>,
    },
    /// Clear all text selections.
    ClearAllSelections,
    /// Finalize pending focus changes (cursor initialization after layout).
    FinalizePendingFocusChanges,
    // === Scroll ===
    /// Scroll cursor/selection into view.
    ScrollSelectionIntoView,
    /// Scroll a specific node into view.
    ScrollNodeIntoView { target: DomNodeId },
    /// Scroll cursor into view after text input (needs relayout first).
    ScrollCursorIntoViewAfterTextInput,
    // === Auto-Scroll Timer ===
    /// Start auto-scroll timer for drag-to-scroll (60Hz).
    StartAutoScrollTimer,
    /// Cancel auto-scroll timer.
    StopAutoScrollTimer,
}
impl_option!(
    SystemChange,
    OptionSystemChange,
    copy = false,
    clone = false,
    [Debug, Clone, PartialEq]
);
impl_vec!(SystemChange, SystemChangeVec, SystemChangeVecDestructor, SystemChangeVecDestructorType, SystemChangeVecSlice, OptionSystemChange);
impl_vec_debug!(SystemChange, SystemChangeVec);
impl_vec_clone!(SystemChange, SystemChangeVec, SystemChangeVecDestructor);
impl_vec_partialeq!(SystemChange, SystemChangeVec);
/// Result of pre-callback internal event filtering
#[derive(Debug, Clone, PartialEq)]
pub struct PreCallbackFilterResult {
    /// System changes to process BEFORE user callbacks
    pub system_changes: Vec<SystemChange>,
    /// Regular events that will be passed to user callbacks
    pub user_events: Vec<SyntheticEvent>,
}
/// Flattened focus/selection state for the input interpreter (replaces trait objects).
#[derive(Debug, Clone)]
pub struct InputInterpreterState {
    pub focused_node: Option<DomNodeId>,
    pub click_count: u8,
    pub drag_start_position: Option<LogicalPosition>,
    pub has_selection: bool,
}
/// All context needed by the input interpreter to map events to system changes.
///
/// Passed to the interpreter callback. Contains references to the current
/// events and window state. The interpreter reads this and returns system changes.
pub struct InputInterpreterInfo<'a> {
    pub events: &'a [SyntheticEvent],
    pub hit_test: Option<&'a FullHitTest>,
    pub keyboard_state: &'a crate::window::KeyboardState,
    pub mouse_state: &'a crate::window::MouseState,
    pub state: InputInterpreterState,
}
/// The `extern "C"` callback type for the input interpreter.
///
/// The first `RefAny` is the user data (vim mode, repeat counter, etc.)
/// held in `InputInterpreterCallback.ctx`. The `*const ()` is an opaque
/// pointer to `InputInterpreterInfo` — callers use the safe wrapper
/// methods to access event data. Returns a `PreCallbackFilterResult`.
///
/// For C/Python: the trampoline extracts the foreign callable from `RefAny.ctx`.
/// For Rust: use `InputInterpreterCallback::from(fn_ptr)` which sets ctx=None.
pub type InputInterpreterCallbackType = extern "C" fn(
    crate::refany::RefAny,
    *const InputInterpreterInfo<'static>,  // Opaque; actual lifetime managed by caller
) -> PreCallbackFilterResult;
/// Configurable input interpreter callback.
///
/// Maps raw platform events + window state → semantic `SystemChange` actions.
/// The default (`default_input_interpreter`) handles standard desktop keybindings.
/// Replace this on `LayoutWindow` to implement vim, game controls, etc.
///
/// ## Pattern
/// - **Rust**: `InputInterpreterCallback::from(my_fn_ptr)` — `ctx` is None
/// - **Python/C**: Set `cb` to a trampoline, `ctx` to `RefAny` wrapping the foreign callable
#[repr(C)]
pub struct InputInterpreterCallback {
    pub cb: InputInterpreterCallbackType,
    pub ctx: crate::refany::OptionRefAny,
}
impl_callback!(InputInterpreterCallback, InputInterpreterCallbackType);
impl Default for InputInterpreterCallback {
2772
    fn default() -> Self {
2772
        Self {
2772
            cb: default_input_interpreter_extern,
2772
            ctx: crate::refany::OptionRefAny::None,
2772
        }
2772
    }
}
/// The `extern "C"` callback type for the post-callback filter.
pub type PostFilterCallbackType = extern "C" fn(
    crate::refany::RefAny,
    bool,                    // prevent_default
    SystemChangeVecSlice,    // pre_changes (immutable slice)
    DomNodeId,               // old_focus (0xFFFF = None)
    DomNodeId,               // new_focus (0xFFFF = None)
) -> SystemChangeVec;
/// Configurable post-callback filter.
#[repr(C)]
pub struct PostFilterCallback {
    pub cb: PostFilterCallbackType,
    pub ctx: crate::refany::OptionRefAny,
}
impl_callback!(PostFilterCallback, PostFilterCallbackType);
impl Default for PostFilterCallback {
2772
    fn default() -> Self {
2772
        Self {
2772
            cb: default_post_filter_extern,
2772
            ctx: crate::refany::OptionRefAny::None,
2772
        }
2772
    }
}
// Keep simpler Rust fn pointer aliases for internal use
pub type InputInterpreterFn = fn(
    info: &InputInterpreterInfo,
) -> PreCallbackFilterResult;
pub type PostFilterFn = fn(
    prevent_default: bool,
    pre_changes: &[SystemChange],
    old_focus: Option<DomNodeId>,
    new_focus: Option<DomNodeId>,
) -> Vec<SystemChange>;
/// Mouse button state for drag tracking
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MouseButtonState {
    pub left_down: bool,
    pub right_down: bool,
    pub middle_down: bool,
}
/// Arrow key / cursor navigation directions
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ArrowDirection {
    Left,
    Right,
    Up,
    Down,
    /// Home key: move to start of current line
    LineStart,
    /// End key: move to end of current line
    LineEnd,
    /// Ctrl+Home: move to start of document
    DocumentStart,
    /// Ctrl+End: move to end of document
    DocumentEnd,
}
impl ArrowDirection {
    /// Map a `VirtualKeyCode` plus the `ctrl` modifier into an `ArrowDirection`.
    /// Returns `None` if the key is not a navigation key.
12
    pub fn from_key(vk: crate::window::VirtualKeyCode, ctrl: bool) -> Option<Self> {
        use crate::window::VirtualKeyCode::*;
2
        Some(match vk {
2
            Left => ArrowDirection::Left,
1
            Right => ArrowDirection::Right,
1
            Up => ArrowDirection::Up,
1
            Down => ArrowDirection::Down,
1
            Home if ctrl => ArrowDirection::DocumentStart,
1
            Home => ArrowDirection::LineStart,
1
            End if ctrl => ArrowDirection::DocumentEnd,
1
            End => ArrowDirection::LineEnd,
3
            _ => return None,
        })
12
    }
    /// Convert to a `(SelectionDirection, SelectionStep)` pair for the
    /// selection-op interpreter. `ctrl` upgrades arrow keys to word jumps.
5
    pub fn to_selection(self, ctrl: bool) -> (SelectionDirection, SelectionStep) {
3
        match self {
1
            ArrowDirection::Left if ctrl => (SelectionDirection::Backward, SelectionStep::Word),
            ArrowDirection::Right if ctrl => (SelectionDirection::Forward, SelectionStep::Word),
2
            ArrowDirection::Left => (SelectionDirection::Backward, SelectionStep::Character),
            ArrowDirection::Right => (SelectionDirection::Forward, SelectionStep::Character),
1
            ArrowDirection::Up => (SelectionDirection::Backward, SelectionStep::VisualLine),
            ArrowDirection::Down => (SelectionDirection::Forward, SelectionStep::VisualLine),
            ArrowDirection::LineStart => (SelectionDirection::Backward, SelectionStep::Line),
            ArrowDirection::LineEnd => (SelectionDirection::Forward, SelectionStep::Line),
            ArrowDirection::DocumentStart => (SelectionDirection::Backward, SelectionStep::Document),
1
            ArrowDirection::DocumentEnd => (SelectionDirection::Forward, SelectionStep::Document),
        }
5
    }
}
/// Direction of cursor movement or selection expansion.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(C)]
pub enum SelectionDirection {
    Forward,
    Backward,
}
/// Granularity of cursor movement or selection expansion.
///
/// Combined with `SelectionDirection`, determines how far a cursor moves
/// or a selection expands. Reused for navigation, deletion, and visual
/// selection — a single code path for word boundaries etc.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(C)]
pub enum SelectionStep {
    /// One grapheme cluster (arrow keys, Backspace, Delete)
    Character,
    /// One word boundary (Ctrl+arrow, Ctrl+Backspace, Ctrl+Delete)
    Word,
    /// To line boundary (Home/End)
    Line,
    /// One visual line up/down (Up/Down arrows)
    VisualLine,
    /// To document boundary (Ctrl+Home/End)
    Document,
}
/// What to do with the selection after moving.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(C)]
pub enum SelectionMode {
    /// Collapse selection to cursor, then move (plain arrow key).
    Move,
    /// Extend selection from anchor to new position (Shift+arrow).
    Extend,
    /// Expand cursor to range in the given direction, then delete the range
    /// (Backspace/Delete). If a range already exists, just delete it.
    Delete,
}
/// A unified selection operation that replaces all cursor movement,
/// selection extension, and text deletion commands.
///
/// Every keyboard shortcut for cursor movement or deletion maps to this:
/// - Arrow Left = (Backward, Character, Move, 1)
/// - Shift+Right = (Forward, Character, Extend, 1)
/// - Ctrl+Backspace = (Backward, Word, Delete, 1)
/// - Home = (Backward, Line, Move, 1)
/// - Ctrl+End = (Forward, Document, Move, 1)
///
/// The `repeat` field enables vim-style commands: 3w = (Forward, Word, Move, 3).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(C)]
pub struct SelectionOp {
    pub direction: SelectionDirection,
    pub step: SelectionStep,
    pub mode: SelectionMode,
    pub repeat: usize,
}
impl SelectionOp {
3
    pub fn new(direction: SelectionDirection, step: SelectionStep, mode: SelectionMode) -> Self {
3
        Self { direction, step, mode, repeat: 1 }
3
    }
}
/// Keyboard shortcuts for text editing
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum KeyboardShortcut {
    Copy,      // Ctrl+C
    Cut,       // Ctrl+X
    Paste,     // Ctrl+V
    SelectAll, // Ctrl+A
    Undo,      // Ctrl+Z
    Redo,      // Ctrl+Y or Ctrl+Shift+Z
}
impl KeyboardShortcut {
    /// Map a `(VirtualKeyCode, ctrl, shift)` triple to a text-editing shortcut.
    /// Returns `None` if the key combination is not a recognized shortcut or
    /// if `ctrl` is not held.
10
    pub fn from_key(vk: crate::window::VirtualKeyCode, ctrl: bool, shift: bool) -> Option<Self> {
        use crate::window::VirtualKeyCode::*;
10
        if !ctrl {
1
            return None;
9
        }
2
        Some(match vk {
2
            C => KeyboardShortcut::Copy,
1
            X => KeyboardShortcut::Cut,
1
            V => KeyboardShortcut::Paste,
1
            A => KeyboardShortcut::SelectAll,
1
            Z if shift => KeyboardShortcut::Redo,
1
            Z => KeyboardShortcut::Undo,
1
            Y => KeyboardShortcut::Redo,
1
            _ => return None,
        })
10
    }
}
/// Default input interpreter: standard desktop keybindings.
///
/// This is the default `InputInterpreterFn` that handles arrow keys, Home/End,
/// Backspace/Delete, Ctrl+C/V/A/Z, mouse clicks, and drag selection.
/// Replace it on `LayoutWindow` to implement vim, game controls, etc.
/// `extern "C"` trampoline for `default_input_interpreter`.
pub extern "C" fn default_input_interpreter_extern(
    _user_data: crate::refany::RefAny,
    info_ptr: *const InputInterpreterInfo<'static>,
) -> PreCallbackFilterResult {
    if info_ptr.is_null() {
        return PreCallbackFilterResult {
            system_changes: Vec::new(),
            user_events: Vec::new(),
        };
    }
    let info = unsafe { &*info_ptr };
    default_input_interpreter(info)
}
/// `extern "C"` trampoline for `default_post_filter`.
pub extern "C" fn default_post_filter_extern(
    _user_data: crate::refany::RefAny,
    prevent_default: bool,
    pre_changes: SystemChangeVecSlice,
    old_focus: DomNodeId,
    new_focus: DomNodeId,
) -> SystemChangeVec {
    let pre_changes_slice = pre_changes.as_slice();
    let old = old_focus.node.into_crate_internal().map(|_| old_focus);
    let new = new_focus.node.into_crate_internal().map(|_| new_focus);
    default_post_filter(prevent_default, pre_changes_slice, old, new).into()
}
7
pub fn default_input_interpreter(
7
    info: &InputInterpreterInfo,
7
) -> PreCallbackFilterResult {
7
    let ctx = FilterContext {
7
        hit_test: info.hit_test,
7
        keyboard_state: info.keyboard_state,
7
        mouse_state: info.mouse_state,
7
        click_count: info.state.click_count,
7
        focused_node: info.state.focused_node,
7
        drag_start_position: info.state.drag_start_position,
7
    };
7
    let (system_changes, user_events) = info.events.iter().fold(
7
        (Vec::new(), Vec::new()),
7
        |(mut internal, mut user), event| {
7
            match process_event_for_internal(&ctx, event) {
4
                Some(InternalEventAction::AddAndSkip(evt)) => {
4
                    internal.push(evt);
4
                }
1
                Some(InternalEventAction::AddAndPass(evt)) => {
1
                    internal.push(evt);
1
                    user.push(event.clone());
1
                }
2
                None => {
2
                    user.push(event.clone());
2
                }
            }
7
            (internal, user)
7
        },
    );
7
    PreCallbackFilterResult {
7
        system_changes,
7
        user_events,
7
    }
7
}
/// Backward-compatible wrapper that calls `default_input_interpreter`.
7
pub fn pre_callback_filter_internal_events<SM, FM>(
7
    events: &[SyntheticEvent],
7
    hit_test: Option<&FullHitTest>,
7
    keyboard_state: &crate::window::KeyboardState,
7
    mouse_state: &crate::window::MouseState,
7
    selection_manager: &SM,
7
    focus_manager: &FM,
7
) -> PreCallbackFilterResult
7
where
7
    SM: SelectionManagerQuery,
7
    FM: FocusManagerQuery,
{
7
    let info = InputInterpreterInfo {
7
        events,
7
        hit_test,
7
        keyboard_state,
7
        mouse_state,
7
        state: InputInterpreterState {
7
            focused_node: focus_manager.get_focused_node_id(),
7
            click_count: selection_manager.get_click_count(),
7
            drag_start_position: selection_manager.get_drag_start_position(),
7
            has_selection: selection_manager.has_selection(),
7
        },
7
    };
7
    default_input_interpreter(&info)
7
}
/// Context for filtering internal events (used by default_input_interpreter)
struct FilterContext<'a> {
    hit_test: Option<&'a FullHitTest>,
    keyboard_state: &'a crate::window::KeyboardState,
    mouse_state: &'a crate::window::MouseState,
    click_count: u8,
    focused_node: Option<DomNodeId>,
    drag_start_position: Option<LogicalPosition>,
}
/// Process a single event and determine if it generates an internal event
7
fn process_event_for_internal(
7
    ctx: &FilterContext<'_>,
7
    event: &SyntheticEvent,
7
) -> Option<InternalEventAction> {
7
    match event.event_type {
1
        EventType::MouseDown => handle_mouse_down(event, ctx.hit_test, ctx.click_count, ctx.mouse_state, ctx.keyboard_state),
        EventType::MouseOver => handle_mouse_over(
            event,
            ctx.hit_test,
            ctx.mouse_state,
            ctx.drag_start_position,
        ),
6
        EventType::KeyDown => handle_key_down(
6
            event,
6
            ctx.keyboard_state,
6
            ctx.focused_node,
        ),
        _ => None,
    }
7
}
/// Action to take after processing an event for internal system events
enum InternalEventAction {
    /// Add system change and skip passing to user callbacks
    AddAndSkip(SystemChange),
    /// Add system change but also pass to user callbacks
    AddAndPass(SystemChange),
}
/// Extract first hovered node from hit test
1
fn get_first_hovered_node(hit_test: Option<&FullHitTest>) -> Option<DomNodeId> {
1
    let ht = hit_test?;
1
    let (dom_id, hit_data) = ht.hovered_nodes.iter().next()?;
1
    let node_id = hit_data.regular_hit_test_nodes.keys().next()?;
1
    Some(DomNodeId {
1
        dom: *dom_id,
1
        node: NodeHierarchyItemId::from_crate_internal(Some(*node_id)),
1
    })
1
}
/// Extract mouse position from event data, falling back to mouse_state if not available
1
fn get_mouse_position_with_fallback(
1
    event: &SyntheticEvent,
1
    mouse_state: &crate::window::MouseState,
1
) -> LogicalPosition {
1
    match &event.data {
1
        EventData::Mouse(mouse_data) => mouse_data.position,
        _ => {
            // Fallback: use current cursor position from mouse_state
            // This handles synthetic events from debug API and automation
            // where EventData may not contain the mouse position
            mouse_state.cursor_position.get_position().unwrap_or(LogicalPosition::zero())
        }
    }
1
}
/// Handle MouseDown event - detect text selection clicks and Ctrl+Click for multi-cursor
1
fn handle_mouse_down(
1
    event: &SyntheticEvent,
1
    hit_test: Option<&FullHitTest>,
1
    click_count: u8,
1
    mouse_state: &crate::window::MouseState,
1
    keyboard_state: &crate::window::KeyboardState,
1
) -> Option<InternalEventAction> {
1
    let effective_click_count = if click_count == 0 { 1 } else { click_count };
1
    if effective_click_count > 3 {
        return None;
1
    }
1
    let _target = get_first_hovered_node(hit_test)?;
1
    let position = get_mouse_position_with_fallback(event, mouse_state);
    // Ctrl+Click (or Cmd+Click on macOS): add cursor at click position
1
    if keyboard_state.ctrl_down() && effective_click_count == 1 {
        return Some(InternalEventAction::AddAndPass(
            SystemChange::AddCursorAtClick { position },
        ));
1
    }
1
    Some(InternalEventAction::AddAndPass(
1
        SystemChange::TextSelectionClick {
1
            position,
1
            timestamp: event.timestamp.clone(),
1
        },
1
    ))
1
}
/// Handle MouseOver event - detect drag selection
fn handle_mouse_over(
    event: &SyntheticEvent,
    hit_test: Option<&FullHitTest>,
    mouse_state: &crate::window::MouseState,
    drag_start_position: Option<LogicalPosition>,
) -> Option<InternalEventAction> {
    if !mouse_state.left_down {
        return None;
    }
    let start_position = drag_start_position?;
    let _target = get_first_hovered_node(hit_test)?;
    let current_position = get_mouse_position_with_fallback(event, mouse_state);
    Some(InternalEventAction::AddAndPass(
        SystemChange::TextSelectionDrag {
            start_position,
            current_position,
        },
    ))
}
/// Handle KeyDown event - detect shortcuts, arrow keys, and delete keys
6
fn handle_key_down(
6
    event: &SyntheticEvent,
6
    keyboard_state: &crate::window::KeyboardState,
6
    focused_node: Option<DomNodeId>,
6
) -> Option<InternalEventAction> {
    use crate::window::VirtualKeyCode;
6
    let target = focused_node?;
5
    let EventData::Keyboard(_) = &event.data else {
1
        return None;
    };
4
    let ctrl = keyboard_state.ctrl_down();
4
    let shift = keyboard_state.shift_down();
4
    let vk = keyboard_state.current_virtual_keycode.as_ref()?;
    // Check keyboard shortcuts (Ctrl+key) → emit specific SystemChange variants.
    // Standard editing shortcuts are routed through the `KeyboardShortcut` enum,
    // and a couple of additional Azul-specific Ctrl combos are matched after.
4
    if ctrl {
1
        if let Some(shortcut) = KeyboardShortcut::from_key(*vk, ctrl, shift) {
1
            let change = match shortcut {
1
                KeyboardShortcut::Copy => SystemChange::CopyToClipboard,
                KeyboardShortcut::Cut => SystemChange::CutToClipboard { target },
                KeyboardShortcut::Paste => SystemChange::PasteFromClipboard,
                KeyboardShortcut::SelectAll => SystemChange::SelectAllText,
                KeyboardShortcut::Undo => SystemChange::UndoTextEdit { target },
                KeyboardShortcut::Redo => SystemChange::RedoTextEdit { target },
            };
1
            return Some(InternalEventAction::AddAndSkip(change));
        }
        if matches!(vk, VirtualKeyCode::D) {
            return Some(InternalEventAction::AddAndSkip(
                SystemChange::SelectNextOccurrence { target },
            ));
        }
3
    }
    // Unified: arrow keys, Home/End, Backspace/Delete all map to SelectionOp.
3
    let mode_for_shift = if shift { SelectionMode::Extend } else { SelectionMode::Move };
3
    let selection_op = if let Some(arrow) = ArrowDirection::from_key(*vk, ctrl) {
1
        let (direction, step) = arrow.to_selection(ctrl);
1
        SelectionOp::new(direction, step, mode_for_shift)
    } else {
2
        match vk {
            // Backspace/Delete = Delete mode (Ctrl upgrades to Word)
1
            VirtualKeyCode::Back => SelectionOp::new(
1
                SelectionDirection::Backward,
1
                if ctrl { SelectionStep::Word } else { SelectionStep::Character },
1
                SelectionMode::Delete,
            ),
1
            VirtualKeyCode::Delete => SelectionOp::new(
1
                SelectionDirection::Forward,
1
                if ctrl { SelectionStep::Word } else { SelectionStep::Character },
1
                SelectionMode::Delete,
            ),
            _ => return None,
        }
    };
3
    Some(InternalEventAction::AddAndSkip(
3
        SystemChange::ApplySelectionOp { target, op: selection_op },
3
    ))
6
}
/// Trait for querying selection manager state.
///
/// This allows `pre_callback_filter_internal_events` to query manager state
/// without depending on the concrete `SelectionManager` type from layout crate.
pub trait SelectionManagerQuery {
    /// Get the current click count (1 = single, 2 = double, 3 = triple)
    fn get_click_count(&self) -> u8;
    /// Get the drag start position if a drag is in progress
    fn get_drag_start_position(&self) -> Option<LogicalPosition>;
    /// Check if any selection exists (click selection or drag selection)
    fn has_selection(&self) -> bool;
}
/// Trait for querying focus manager state.
///
/// This allows `pre_callback_filter_internal_events` to query manager state
/// without depending on the concrete `FocusManager` type from layout crate.
pub trait FocusManagerQuery {
    /// Get the currently focused node ID
    fn get_focused_node_id(&self) -> Option<DomNodeId>;
}
/// Post-callback filter: Determine additional system changes needed after user callbacks.
///
/// Takes the pre-callback system changes and focus state to determine what
/// post-callback system changes are needed (text input, scrolling, timers).
/// Default post-callback filter: scroll-into-view after cursor ops, auto-scroll during drag.
pub fn default_post_filter(
    prevent_default: bool,
    pre_changes: &[SystemChange],
    old_focus: Option<DomNodeId>,
    new_focus: Option<DomNodeId>,
) -> Vec<SystemChange> {
    post_callback_filter_system_changes(prevent_default, pre_changes, old_focus, new_focus)
}
pub fn post_callback_filter_system_changes(
    prevent_default: bool,
    pre_changes: &[SystemChange],
    old_focus: Option<DomNodeId>,
    new_focus: Option<DomNodeId>,
) -> Vec<SystemChange> {
    let mut changes = Vec::new();
    if prevent_default {
        // Only focus change passes through preventDefault
        if old_focus != new_focus {
            changes.push(SystemChange::SetFocus { new_focus, old_focus });
        }
        return changes;
    }
    // Always apply pending text input
    changes.push(SystemChange::ApplyPendingTextInput);
    // Determine post-callback actions based on pre-callback system changes
    for change in pre_changes {
        match change {
            SystemChange::TextSelectionClick { .. }
            | SystemChange::ApplySelectionOp { .. }
            | SystemChange::AddCursorAtClick { .. }
            | SystemChange::SelectNextOccurrence { .. } => {
                changes.push(SystemChange::ScrollSelectionIntoView);
            }
            SystemChange::TextSelectionDrag { .. } => {
                changes.push(SystemChange::StartAutoScrollTimer);
            }
            SystemChange::CutToClipboard { .. }
            | SystemChange::PasteFromClipboard
            | SystemChange::UndoTextEdit { .. }
            | SystemChange::RedoTextEdit { .. }
            | SystemChange::SelectAllText => {
                changes.push(SystemChange::ScrollSelectionIntoView);
            }
            // Other system changes don't generate post-callback actions
            _ => {}
        }
    }
    // Focus changed during callbacks
    if old_focus != new_focus {
        changes.push(SystemChange::SetFocus { new_focus, old_focus });
    }
    changes
}
#[cfg(test)]
mod tests {
    use super::*;
    use azul_css::AzString;
    use crate::dom::{DomId, DomNodeId};
    use crate::styled_dom::NodeHierarchyItemId;
    use crate::id::NodeId;
    use crate::window::{KeyboardState, MouseState, VirtualKeyCode, VirtualKeyCodeVec, OptionVirtualKeyCode};
    use crate::geom::LogicalPosition;
    use crate::task::{Instant, SystemTick};
    struct MockSelectionManager {
        click_count: u8,
        has_sel: bool,
    }
    impl SelectionManagerQuery for MockSelectionManager {
7
        fn get_click_count(&self) -> u8 { self.click_count }
7
        fn get_drag_start_position(&self) -> Option<LogicalPosition> { None }
7
        fn has_selection(&self) -> bool { self.has_sel }
    }
    struct MockFocusManager(Option<DomNodeId>);
    impl FocusManagerQuery for MockFocusManager {
7
        fn get_focused_node_id(&self) -> Option<DomNodeId> { self.0 }
    }
7
    fn focused_node(node_idx: usize) -> DomNodeId {
7
        DomNodeId {
7
            dom: DomId { inner: 0 },
7
            node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(node_idx))),
7
        }
7
    }
6
    fn make_keyboard_state(vk: VirtualKeyCode) -> KeyboardState {
6
        let mut ks = KeyboardState::default();
6
        ks.current_virtual_keycode = OptionVirtualKeyCode::Some(vk);
6
        ks.pressed_virtual_keycodes = VirtualKeyCodeVec::from_vec(vec![vk]);
6
        ks
6
    }
2
    fn make_keydown_event(target: DomNodeId) -> SyntheticEvent {
2
        SyntheticEvent::new(
2
            EventType::KeyDown,
2
            EventSource::User,
2
            target,
2
            Instant::Tick(SystemTick::new(0)),
2
            EventData::Keyboard(KeyboardEventData {
2
                key_code: VirtualKeyCode::Back as u32,
2
                char_code: None,
2
                modifiers: KeyModifiers::default(),
2
                repeat: false,
2
            }),
        )
2
    }
    #[test]
1
    fn backspace_generates_delete_text_selection() {
1
        let target = focused_node(2);
1
        let events = vec![make_keydown_event(target)];
1
        let kb = make_keyboard_state(VirtualKeyCode::Back);
1
        let mouse = MouseState::default();
1
        let sel = MockSelectionManager { click_count: 0, has_sel: false };
1
        let focus = MockFocusManager(Some(target));
1
        let result = pre_callback_filter_internal_events(
1
            &events, None, &kb, &mouse, &sel, &focus,
        );
1
        let ops: Vec<_> = result.system_changes.iter()
1
            .filter(|c| matches!(c, SystemChange::ApplySelectionOp { .. }))
1
            .collect();
1
        assert_eq!(ops.len(), 1, "Backspace should generate ApplySelectionOp");
1
        match &ops[0] {
1
            SystemChange::ApplySelectionOp { op, .. } => {
1
                assert_eq!(op.direction, SelectionDirection::Backward);
1
                assert_eq!(op.step, SelectionStep::Character);
1
                assert_eq!(op.mode, SelectionMode::Delete);
            }
            _ => unreachable!(),
        }
1
    }
    #[test]
1
    fn delete_key_generates_forward_deletion() {
1
        let target = focused_node(2);
1
        let event = SyntheticEvent::new(
1
            EventType::KeyDown, EventSource::User, target,
1
            Instant::Tick(SystemTick::new(0)),
1
            EventData::Keyboard(KeyboardEventData {
1
                key_code: VirtualKeyCode::Delete as u32,
1
                char_code: None, modifiers: KeyModifiers::default(), repeat: false,
1
            }),
        );
1
        let kb = make_keyboard_state(VirtualKeyCode::Delete);
1
        let mouse = MouseState::default();
1
        let sel = MockSelectionManager { click_count: 0, has_sel: false };
1
        let focus = MockFocusManager(Some(target));
1
        let result = pre_callback_filter_internal_events(&[event], None, &kb, &mouse, &sel, &focus);
1
        let ops: Vec<_> = result.system_changes.iter()
1
            .filter(|c| matches!(c, SystemChange::ApplySelectionOp { .. }))
1
            .collect();
1
        assert_eq!(ops.len(), 1);
1
        match &ops[0] {
1
            SystemChange::ApplySelectionOp { op, .. } => {
1
                assert_eq!(op.direction, SelectionDirection::Forward);
1
                assert_eq!(op.step, SelectionStep::Character);
1
                assert_eq!(op.mode, SelectionMode::Delete);
            }
            _ => unreachable!(),
        }
1
    }
    #[test]
1
    fn arrow_left_generates_navigation() {
1
        let target = focused_node(2);
1
        let event = SyntheticEvent::new(
1
            EventType::KeyDown, EventSource::User, target,
1
            Instant::Tick(SystemTick::new(0)),
1
            EventData::Keyboard(KeyboardEventData {
1
                key_code: VirtualKeyCode::Left as u32,
1
                char_code: None, modifiers: KeyModifiers::default(), repeat: false,
1
            }),
        );
1
        let kb = make_keyboard_state(VirtualKeyCode::Left);
1
        let mouse = MouseState::default();
1
        let sel = MockSelectionManager { click_count: 0, has_sel: false };
1
        let focus = MockFocusManager(Some(target));
1
        let result = pre_callback_filter_internal_events(&[event], None, &kb, &mouse, &sel, &focus);
1
        let ops: Vec<_> = result.system_changes.iter()
1
            .filter(|c| matches!(c, SystemChange::ApplySelectionOp { .. }))
1
            .collect();
1
        assert_eq!(ops.len(), 1, "Left arrow should generate ApplySelectionOp");
1
        match &ops[0] {
1
            SystemChange::ApplySelectionOp { op, .. } => {
1
                assert_eq!(op.direction, SelectionDirection::Backward);
1
                assert_eq!(op.step, SelectionStep::Character);
1
                assert_eq!(op.mode, SelectionMode::Move);
            }
            _ => unreachable!(),
        }
1
    }
    #[test]
1
    fn no_focused_node_means_no_keyboard_system_changes() {
1
        let target = focused_node(2);
1
        let event = make_keydown_event(target);
1
        let kb = make_keyboard_state(VirtualKeyCode::Back);
1
        let mouse = MouseState::default();
1
        let sel = MockSelectionManager { click_count: 0, has_sel: false };
1
        let focus = MockFocusManager(None); // No focus!
1
        let result = pre_callback_filter_internal_events(
1
            &[event], None, &kb, &mouse, &sel, &focus,
        );
1
        assert!(result.system_changes.is_empty(),
            "No system changes should be generated without focused node");
1
    }
    #[test]
1
    fn keydown_without_keyboard_data_generates_no_system_change() {
1
        let target = focused_node(2);
1
        let event = SyntheticEvent::new(
1
            EventType::KeyDown,
1
            EventSource::User,
1
            target,
1
            Instant::Tick(SystemTick::new(0)),
1
            EventData::None, // Bug: missing keyboard data
        );
1
        let kb = make_keyboard_state(VirtualKeyCode::Back);
1
        let mouse = MouseState::default();
1
        let sel = MockSelectionManager { click_count: 0, has_sel: false };
1
        let focus = MockFocusManager(Some(target));
1
        let result = pre_callback_filter_internal_events(
1
            &[event], None, &kb, &mouse, &sel, &focus,
        );
        // This test documents the bug we just fixed: EventData::None causes
        // the handle_key_down function to return None (early exit at line 2737)
1
        assert!(result.system_changes.is_empty(),
            "EventData::None should not generate system changes (documents the old bug)");
1
    }
    #[test]
1
    fn ctrl_c_generates_copy() {
1
        let target = focused_node(2);
1
        let event = SyntheticEvent::new(
1
            EventType::KeyDown,
1
            EventSource::User,
1
            target,
1
            Instant::Tick(SystemTick::new(0)),
1
            EventData::Keyboard(KeyboardEventData {
1
                key_code: VirtualKeyCode::C as u32,
1
                char_code: Some('c'),
1
                modifiers: KeyModifiers { ctrl: true, shift: false, alt: false, meta: false },
1
                repeat: false,
1
            }),
        );
1
        let mut kb = make_keyboard_state(VirtualKeyCode::C);
1
        kb.pressed_virtual_keycodes = VirtualKeyCodeVec::from_vec(
1
            vec![VirtualKeyCode::C, VirtualKeyCode::LControl]
        );
1
        let mouse = MouseState::default();
1
        let sel = MockSelectionManager { click_count: 0, has_sel: false };
1
        let focus = MockFocusManager(Some(target));
1
        let result = pre_callback_filter_internal_events(
1
            &[event], None, &kb, &mouse, &sel, &focus,
        );
1
        let copy_changes: Vec<_> = result.system_changes.iter()
1
            .filter(|c| matches!(c, SystemChange::CopyToClipboard))
1
            .collect();
1
        assert_eq!(copy_changes.len(), 1, "Ctrl+C should generate CopyToClipboard");
1
    }
2
    fn make_hit_test_with_node(node_idx: usize) -> crate::hit_test::FullHitTest {
        use crate::hit_test::{FullHitTest, HitTest, HitTestItem};
        use crate::dom::OptionDomNodeId;
        use std::collections::BTreeMap;
2
        let node_id = NodeId::new(node_idx);
2
        let dom_id = DomId { inner: 0 };
2
        let mut regular = BTreeMap::new();
2
        regular.insert(node_id, HitTestItem {
2
            point_in_viewport: LogicalPosition::new(100.0, 200.0),
2
            point_relative_to_item: LogicalPosition::new(50.0, 30.0),
2
            is_focusable: true,
2
            is_virtual_view_hit: None,
2
            hit_depth: 0,
2
        });
2
        let mut hovered = BTreeMap::new();
2
        hovered.insert(dom_id, HitTest {
2
            regular_hit_test_nodes: regular,
2
            scroll_hit_test_nodes: BTreeMap::new(),
2
            scrollbar_hit_test_nodes: BTreeMap::new(),
2
            cursor_hit_test_nodes: BTreeMap::new(),
2
        });
2
        FullHitTest {
2
            hovered_nodes: hovered,
2
            focused_node: OptionDomNodeId::None,
2
        }
2
    }
    #[test]
1
    fn mousedown_generates_text_selection_click() {
1
        let target = focused_node(2);
1
        let event = SyntheticEvent::new(
1
            EventType::MouseDown,
1
            EventSource::User,
1
            target,
1
            Instant::Tick(SystemTick::new(0)),
1
            EventData::Mouse(MouseEventData {
1
                position: LogicalPosition::new(100.0, 200.0),
1
                button: crate::events::MouseButton::Left,
1
                buttons: 1,
1
                modifiers: KeyModifiers::default(),
1
            }),
        );
1
        let hit_test = make_hit_test_with_node(2);
1
        let kb = KeyboardState::default();
1
        let mouse = MouseState::default();
1
        let sel = MockSelectionManager { click_count: 1, has_sel: false };
1
        let focus = MockFocusManager(Some(target));
1
        let result = pre_callback_filter_internal_events(
1
            &[event], Some(&hit_test), &kb, &mouse, &sel, &focus,
        );
1
        let click_changes: Vec<_> = result.system_changes.iter()
1
            .filter(|c| matches!(c, SystemChange::TextSelectionClick { .. }))
1
            .collect();
1
        assert_eq!(click_changes.len(), 1, "MouseDown with hit_test should generate TextSelectionClick");
1
    }
    #[test]
1
    fn process_event_result_max_self_picks_higher_variant() {
1
        let lo = ProcessEventResult::ShouldReRenderCurrentWindow;
1
        let hi = ProcessEventResult::ShouldRegenerateDomCurrentWindow;
1
        assert_eq!(lo.max_self(hi), hi);
1
        assert_eq!(hi.max_self(lo), hi);
1
        assert_eq!(lo.max_self(lo), lo);
1
    }
    #[test]
1
    fn arrow_direction_from_key_maps_arrows_and_home_end() {
        use crate::window::VirtualKeyCode::*;
1
        assert_eq!(ArrowDirection::from_key(Left, false), Some(ArrowDirection::Left));
1
        assert_eq!(ArrowDirection::from_key(Right, false), Some(ArrowDirection::Right));
1
        assert_eq!(ArrowDirection::from_key(Up, false), Some(ArrowDirection::Up));
1
        assert_eq!(ArrowDirection::from_key(Down, false), Some(ArrowDirection::Down));
1
        assert_eq!(ArrowDirection::from_key(Home, false), Some(ArrowDirection::LineStart));
1
        assert_eq!(ArrowDirection::from_key(End, false), Some(ArrowDirection::LineEnd));
1
        assert_eq!(ArrowDirection::from_key(Home, true), Some(ArrowDirection::DocumentStart));
1
        assert_eq!(ArrowDirection::from_key(End, true), Some(ArrowDirection::DocumentEnd));
1
        assert_eq!(ArrowDirection::from_key(C, false), None);
1
    }
    #[test]
1
    fn arrow_direction_to_selection_respects_ctrl() {
1
        let (d, s) = ArrowDirection::Left.to_selection(false);
1
        assert_eq!((d, s), (SelectionDirection::Backward, SelectionStep::Character));
1
        let (d, s) = ArrowDirection::Left.to_selection(true);
1
        assert_eq!((d, s), (SelectionDirection::Backward, SelectionStep::Word));
1
        let (d, s) = ArrowDirection::Up.to_selection(false);
1
        assert_eq!((d, s), (SelectionDirection::Backward, SelectionStep::VisualLine));
1
        let (d, s) = ArrowDirection::DocumentEnd.to_selection(false);
1
        assert_eq!((d, s), (SelectionDirection::Forward, SelectionStep::Document));
1
    }
    #[test]
1
    fn keyboard_shortcut_from_key_recognizes_editing_combos() {
        use crate::window::VirtualKeyCode::*;
1
        assert_eq!(KeyboardShortcut::from_key(C, true, false), Some(KeyboardShortcut::Copy));
1
        assert_eq!(KeyboardShortcut::from_key(X, true, false), Some(KeyboardShortcut::Cut));
1
        assert_eq!(KeyboardShortcut::from_key(V, true, false), Some(KeyboardShortcut::Paste));
1
        assert_eq!(KeyboardShortcut::from_key(A, true, false), Some(KeyboardShortcut::SelectAll));
1
        assert_eq!(KeyboardShortcut::from_key(Z, true, false), Some(KeyboardShortcut::Undo));
1
        assert_eq!(KeyboardShortcut::from_key(Z, true, true), Some(KeyboardShortcut::Redo));
1
        assert_eq!(KeyboardShortcut::from_key(Y, true, false), Some(KeyboardShortcut::Redo));
        // Non-ctrl combos must not match
1
        assert_eq!(KeyboardShortcut::from_key(C, false, false), None);
        // Unknown keys
1
        assert_eq!(KeyboardShortcut::from_key(D, true, false), None);
1
    }
    #[test]
1
    fn mouse_button_state_round_trips_from_mouse_state() {
1
        let mut ms = MouseState::default();
1
        ms.left_down = true;
1
        ms.middle_down = true;
1
        let bs: MouseButtonState = (&ms).into();
1
        assert!(bs.left_down);
1
        assert!(!bs.right_down);
1
        assert!(bs.middle_down);
1
        assert!(bs.any_down());
1
        let none = MouseButtonState { left_down: false, right_down: false, middle_down: false };
1
        assert!(!none.any_down());
1
    }
    #[test]
1
    fn callback_to_call_collects_hits_for_dom() {
1
        let dom_id = DomId { inner: 0 };
1
        let hit_test = make_hit_test_with_node(2);
1
        let filter = EventFilter::Hover(HoverEventFilter::MouseDown);
1
        let calls = CallbackToCall::from_hit_test(&hit_test, dom_id, filter.clone());
1
        assert_eq!(calls.len(), 1);
1
        assert_eq!(calls[0].node_id, NodeId::new(2));
1
        assert_eq!(calls[0].event_filter, filter);
1
        assert!(calls[0].hit_test_item.is_some());
        // Unknown DOM id => empty list
1
        let other = CallbackToCall::from_hit_test(
1
            &hit_test,
1
            DomId { inner: 999 },
1
            EventFilter::Hover(HoverEventFilter::MouseUp),
        );
1
        assert!(other.is_empty());
        // Direct constructor builds expected fields
1
        let direct = CallbackToCall::new(
1
            NodeId::new(7),
1
            None,
1
            EventFilter::Focus(FocusEventFilter::FocusReceived),
        );
1
        assert_eq!(direct.node_id, NodeId::new(7));
1
        assert!(direct.hit_test_item.is_none());
1
    }
    #[test]
1
    fn restyle_relayout_aliases_are_btreemap_compatible() {
        // RestyleNodes / RelayoutNodes are aliases for BTreeMap<NodeId, Vec<ChangedCssProperty>>.
        // Confirm we can construct empty ones via the alias and that they accept the same keys.
1
        let restyle: RestyleNodes = BTreeMap::new();
1
        let relayout: RelayoutNodes = BTreeMap::new();
1
        assert!(restyle.is_empty());
1
        assert!(relayout.is_empty());
        // RelayoutWords is BTreeMap<NodeId, AzString>.
1
        let mut words: RelayoutWords = BTreeMap::new();
1
        words.insert(NodeId::new(1), AzString::from_const_str("hello"));
1
        assert_eq!(words.get(&NodeId::new(1)).map(|s| s.as_str()), Some("hello"));
1
    }
    #[test]
1
    fn detect_lifecycle_events_with_reconciliation_is_callable() {
        // Smoke test: empty old/new node data must produce no events and an
        // empty migration map. This proves the function is callable from
        // the public API and threads through `crate::diff::reconcile_dom`.
1
        let dom_id = DomId { inner: 0 };
1
        let old_data: Vec<crate::dom::NodeData> = Vec::new();
1
        let new_data: Vec<crate::dom::NodeData> = Vec::new();
1
        let old_hier: Vec<crate::styled_dom::NodeHierarchyItem> = Vec::new();
1
        let new_hier: Vec<crate::styled_dom::NodeHierarchyItem> = Vec::new();
1
        let old_layout = OrderedMap::default();
1
        let new_layout = OrderedMap::default();
1
        let result: LifecycleEventResult = detect_lifecycle_events_with_reconciliation(
1
            dom_id,
1
            &old_data,
1
            &new_data,
1
            &old_hier,
1
            &new_hier,
1
            &old_layout,
1
            &new_layout,
1
            Instant::Tick(SystemTick::new(0)),
        );
1
        assert!(result.events.is_empty());
1
        assert!(result.node_id_mapping.is_empty());
1
    }
    #[test]
1
    fn nodedata_focusable_and_activation_traits_are_wired() {
        use crate::dom::{NodeData, NodeType};
        use crate::events::{ActivationBehavior as _, Focusable as _};
        // <button> is naturally focusable and has activation behavior.
1
        let btn = NodeData::create_node(NodeType::Button);
1
        assert!(<NodeData as Focusable>::is_naturally_focusable(&btn));
1
        assert!(<NodeData as Focusable>::is_focusable(&btn));
1
        assert!(<NodeData as ActivationBehavior>::has_activation_behavior(&btn));
1
        assert!(<NodeData as ActivationBehavior>::is_activatable(&btn));
        // A plain <div> is neither naturally focusable nor activatable.
1
        let div = NodeData::create_node(NodeType::Div);
1
        assert!(!<NodeData as Focusable>::is_naturally_focusable(&div));
1
        assert!(!<NodeData as ActivationBehavior>::has_activation_behavior(&div));
        // <input> is naturally focusable.
1
        let input = NodeData::create_node(NodeType::Input);
1
        assert!(<NodeData as Focusable>::is_naturally_focusable(&input));
1
    }
}