1
//! Unified Event Determination
2
//!
3
//! This module provides the single source of truth for what
4
//! events occurred in a frame. It combines window state changes
5
//! with events from all managers (scroll, text input, etc.).
6

            
7
use azul_core::{
8
    dom::{DomId, DomNodeId},
9
    events::{
10
        deduplicate_synthetic_events, EventData, EventProvider, EventSource, EventType,
11
        KeyModifiers, KeyboardEventData, MouseButton, MouseEventData, ScrollDeltaMode,
12
        ScrollEventData, SyntheticEvent, WindowEventData,
13
    },
14
    geom::{LogicalPosition, LogicalRect},
15
    id::NodeId,
16
    styled_dom::NodeHierarchyItemId,
17
    task::{Instant, SystemTick},
18
    window::{CursorPosition, WindowPosition},
19
};
20

            
21
use std::collections::BTreeSet;
22

            
23
use crate::window_state::FullWindowState;
24

            
25
/// Unified event determination from all sources.
26
///
27
/// This is the **single source of truth** for what events occurred in a frame.
28
///
29
/// It combines:
30
///
31
/// 1. Window state changes (resize, move, theme, etc.)
32
/// 2. Manager-reported events (scroll, text input, focus, hover)
33
/// 3. Deduplicates by node + event type
34
///
35
/// ## Architecture
36
///
37
/// ```text
38
/// Platform Input
39
///     ↓
40
/// Update Window State + Managers (record_sample, record_input, etc.)
41
///     ↓
42
/// determine_events_from_managers() ← Query all managers (immutable)
43
///     ↓
44
/// Vec<SyntheticEvent> (deduplicated)
45
///     ↓
46
/// dispatch_events() ← Route to callbacks
47
///     ↓
48
/// Invoke callbacks
49
/// ```
50
///
51
/// ## Arguments
52
///
53
/// All arguments are **immutable** references - no state is modified here.
54
/// State changes happen earlier via `record_sample()`, `record_input()`, etc.
55
///
56
/// - `current_state` - Current window state (after platform updates)
57
/// - `previous_state` - Window state from previous frame
58
/// - `managers` - All managers that can provide events
59
/// - `timestamp` - Current time for event timestamps
60
///
61
/// ## Returns
62
///
63
/// - Vector of `SyntheticEvents`, deduplicated and ready for dispatch.
64
// Instant is a ref-counted FFI clock handle threaded through the event loop by value.
65
#[allow(clippy::needless_pass_by_value)]
66
9
pub fn determine_events_from_managers(
67
9
    current_state: &FullWindowState,
68
9
    previous_state: &FullWindowState,
69
9
    managers: &[&dyn EventProvider],
70
9
    timestamp: Instant,
71
9
) -> Vec<SyntheticEvent> {
72
9
    let mut events = Vec::new();
73

            
74
    // 1. Detect window state changes (simple diffing)
75
9
    events.extend(detect_window_state_events(
76
9
        current_state,
77
9
        previous_state,
78
9
        timestamp.clone(),
79
    ));
80

            
81
    // 2. Query all managers for their pending events
82
275
    for manager in managers {
83
266
        events.extend(manager.get_pending_events(timestamp.clone()));
84
266
    }
85

            
86
    // 3. Deduplicate by (node, event_type)
87
9
    deduplicate_synthetic_events(events)
88
9
}
89

            
90
/// Detect window-level events by comparing states.
91
///
92
/// This is a simple sub-function that only handles window-level changes:
93
///
94
/// - Window resized
95
/// - Window moved
96
/// - Theme changed
97
/// - Mouse entered/left window
98
/// - Window focus changed
99
///
100
/// Node-level events (hover, focus, scroll, text) come from managers.
101
#[allow(clippy::cast_precision_loss)] // bounded graphics/coord/counter/fixed-point cast
102
25
fn detect_window_state_events(
103
25
    current: &FullWindowState,
104
25
    previous: &FullWindowState,
105
25
    timestamp: Instant,
106
25
) -> Vec<SyntheticEvent> {
107
25
    let mut events = Vec::new();
108

            
109
    // Window resized
110
25
    if current.size != previous.size {
111
6
        events.push(SyntheticEvent::new(
112
6
            EventType::WindowResize,
113
6
            EventSource::User,
114
6
            DomNodeId {
115
6
                dom: DomId { inner: 0 },
116
6
                node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::ZERO)),
117
6
            },
118
6
            timestamp.clone(),
119
6
            EventData::Window(WindowEventData {
120
6
                size: Some(LogicalRect {
121
6
                    origin: LogicalPosition { x: 0.0, y: 0.0 },
122
6
                    size: current.size.dimensions,
123
6
                }),
124
6
                position: None,
125
6
            }),
126
6
        ));
127
19
    }
128

            
129
    // Window moved
130
25
    if current.position != previous.position {
131
4
        let position = match current.position {
132
2
            WindowPosition::Initialized(phys_pos)
133
3
            | WindowPosition::RelativeToParentWindow(phys_pos) => Some(LogicalPosition {
134
3
                x: phys_pos.x as f32,
135
3
                y: phys_pos.y as f32,
136
3
            }),
137
1
            WindowPosition::Uninitialized => None,
138
        };
139

            
140
4
        if let Some(pos) = position {
141
3
            events.push(SyntheticEvent::new(
142
3
                EventType::WindowMove,
143
3
                EventSource::User,
144
3
                DomNodeId {
145
3
                    dom: DomId { inner: 0 },
146
3
                    node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::ZERO)),
147
3
                },
148
3
                timestamp.clone(),
149
3
                EventData::Window(WindowEventData {
150
3
                    size: None,
151
3
                    position: Some(pos),
152
3
                }),
153
3
            ));
154
3
        }
155
21
    }
156

            
157
    // Theme changed
158
25
    if current.theme != previous.theme {
159
3
        events.push(SyntheticEvent::new(
160
3
            EventType::ThemeChange,
161
3
            EventSource::User,
162
3
            DomNodeId {
163
3
                dom: DomId { inner: 0 },
164
3
                node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::ZERO)),
165
3
            },
166
3
            timestamp.clone(),
167
3
            EventData::None,
168
3
        ));
169
22
    }
170

            
171
    // Mouse entered window
172
25
    let prev_mouse_in_window = matches!(
173
25
        previous.mouse_state.cursor_position,
174
        CursorPosition::InWindow(_)
175
    );
176
25
    let curr_mouse_in_window = matches!(
177
25
        current.mouse_state.cursor_position,
178
        CursorPosition::InWindow(_)
179
    );
180

            
181
25
    if curr_mouse_in_window && !prev_mouse_in_window {
182
2
        events.push(SyntheticEvent::new(
183
2
            EventType::MouseEnter,
184
2
            EventSource::User,
185
2
            DomNodeId {
186
2
                dom: DomId { inner: 0 },
187
2
                node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::ZERO)),
188
2
            },
189
2
            timestamp.clone(),
190
2
            EventData::None,
191
2
        ));
192
23
    }
193

            
194
    // Mouse left window
195
25
    if !curr_mouse_in_window && prev_mouse_in_window {
196
1
        events.push(SyntheticEvent::new(
197
1
            EventType::MouseLeave,
198
1
            EventSource::User,
199
1
            DomNodeId {
200
1
                dom: DomId { inner: 0 },
201
1
                node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::ZERO)),
202
1
            },
203
1
            timestamp,
204
1
            EventData::None,
205
1
        ));
206
24
    }
207

            
208
25
    events
209
25
}
210

            
211
/// Get all hovered node IDs from the hover manager for a given frame.
212
///
213
/// `frame_index` 0 = current frame, 1 = previous frame, etc.
214
/// Returns a `BTreeSet` of all `NodeIds` that are hovered (the full hover chain).
215
377
fn get_all_hovered_nodes(
216
377
    hover_manager: &crate::managers::hover::HoverManager,
217
377
    frame_index: usize,
218
377
) -> BTreeSet<(DomId, NodeId)> {
219
    use crate::managers::hover::InputPointId;
220
    // MWA-C-hover: walk EVERY hit DOM, not just DomId 0 — the old
221
    // root-DOM-only read meant MouseEnter/MouseLeave never fired for nodes
222
    // inside VirtualView / iframe child DOMs.
223
377
    hover_manager
224
377
        .get_frame(&InputPointId::Mouse, frame_index)
225
377
        .map(|ht| {
226
63
            ht.hovered_nodes
227
63
                .iter()
228
63
                .flat_map(|(dom_id, hit)| {
229
62
                    hit.regular_hit_test_nodes
230
62
                        .keys()
231
132
                        .map(move |node_id| (*dom_id, *node_id))
232
62
                })
233
63
                .collect()
234
63
        })
235
377
        .unwrap_or_default()
236
377
}
237

            
238
/// Comprehensive event determination including mouse, keyboard, and gesture events.
239
///
240
/// This is the primary event determination function.
241
/// It generates `SyntheticEvents` for all event types including:
242
///
243
/// - Mouse button events (down/up for left/right/middle)
244
/// - Mouse movement (`MouseOver`)
245
/// - Keyboard events (VirtualKeyDown/Up)
246
/// - Window state changes (resize, move, theme, focus)
247
/// - Gesture events (`DragStart`, Drag, `DragEnd`, `DoubleClick`, `LongPress`, Swipe, Pinch, Rotate, Pen)
248
/// - File drop events (`HoveredFile`, `DroppedFile`)
249
///
250
/// ## Arguments
251
///
252
/// * `current_state` - Current window state
253
/// * `previous_state` - Previous window state
254
/// * `hover_manager` - For hover/mouse enter/leave detection
255
/// * `focus_manager` - For focus event detection
256
/// * `file_drop_manager` - For file drop detection
257
/// * `gesture_manager` - Optional gesture detection
258
/// * `managers` - Additional managers (scroll, text, etc.) that implement `EventProvider`
259
/// * `timestamp` - Current time
260
///
261
/// ## Returns
262
///
263
/// - Deduplicated vector of `SyntheticEvents` ready for dispatch
264
#[allow(clippy::cast_precision_loss)] // bounded graphics/coord/counter/fixed-point cast
265
// Instant is a ref-counted FFI clock handle threaded through the event loop by value.
266
#[allow(clippy::needless_pass_by_value)]
267
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
268
/// `EventProvider` for the window's pending STRUCTURAL edit (C11): emits one
269
/// `EventType::DocumentEdit` targeted at the changeset's node, once per
270
/// changeset id. Constructed per pass by
271
/// `LayoutWindow::document_edit_event_provider`; the dispatcher marks the
272
/// notification delivered after determination, so subsequent passes stay
273
/// silent while the same edit is pending.
274
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
275
pub struct DocumentEditEventProvider {
276
    /// (changeset target node, changeset id), if an edit is pending.
277
    pub pending: Option<(DomNodeId, u64)>,
278
    /// True once this changeset's notification has been delivered.
279
    pub already_notified: bool,
280
}
281

            
282
impl EventProvider for DocumentEditEventProvider {
283
36
    fn get_pending_events(&self, timestamp: Instant) -> Vec<SyntheticEvent> {
284
36
        match self.pending {
285
18
            Some((target, id)) if !self.already_notified => vec![SyntheticEvent::new(
286
18
                EventType::DocumentEdit,
287
18
                EventSource::User,
288
18
                target,
289
18
                timestamp,
290
18
                EventData::DocumentEdit(azul_core::events::DocumentEditEventData {
291
18
                    changeset_id: id,
292
18
                }),
293
            )],
294
18
            _ => Vec::new(),
295
        }
296
36
    }
297
}
298

            
299
182
pub fn determine_all_events(
300
182
    current_state: &FullWindowState,
301
182
    previous_state: &FullWindowState,
302
182
    hover_manager: &crate::managers::hover::HoverManager,
303
182
    focus_manager: &crate::managers::focus_cursor::FocusManager,
304
182
    file_drop_manager: &crate::managers::file_drop::FileDropManager,
305
182
    gesture_manager: Option<&crate::managers::gesture::GestureAndDragManager>,
306
182
    managers: &[&dyn EventProvider],
307
182
    wheel_delta: Option<LogicalPosition>,
308
182
    timestamp: Instant,
309
182
) -> Vec<SyntheticEvent> {
310
182
    let mut events = Vec::new();
311

            
312
    // Get root node for window-level events
313
182
    let root_node = DomNodeId {
314
182
        dom: DomId { inner: 0 },
315
182
        node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::ZERO)),
316
182
    };
317

            
318
    // Helper: get cursor position
319
182
    let cursor_pos = current_state
320
182
        .mouse_state
321
182
        .cursor_position
322
182
        .get_position()
323
182
        .unwrap_or(LogicalPosition { x: 0.0, y: 0.0 });
324

            
325
    // Helper: build key modifiers from keyboard state
326
182
    let modifiers = KeyModifiers {
327
182
        shift: current_state.keyboard_state.shift_down(),
328
182
        ctrl: current_state.keyboard_state.ctrl_down(),
329
182
        alt: current_state.keyboard_state.alt_down(),
330
182
        meta: current_state.keyboard_state.super_down(),
331
182
    };
332

            
333
    // Helper: compute mouse buttons bitmask
334
182
    let buttons: u8 = u8::from(current_state.mouse_state.left_down)
335
182
        | (if current_state.mouse_state.right_down { 2 } else { 0 })
336
182
        | (if current_state.mouse_state.middle_down { 4 } else { 0 });
337

            
338
    // Helper: get deepest hovered node as the event target for mouse events.
339
    // Multi-DOM aware: targets VirtualView / iframe child DOMs (higher DomId,
340
    // composited on top) when the cursor is over them, not just the root DOM.
341
    // For single-DOM apps only DomId 0 is ever hit, so this is unchanged.
342
182
    let mouse_target = hover_manager
343
182
        .current_hover_node_full()
344
182
        .unwrap_or(root_node);
345

            
346
    // Helper: build MouseEventData for a specific button
347
182
    let make_mouse_data = |button: MouseButton| -> EventData {
348
43
        EventData::Mouse(MouseEventData {
349
43
            position: cursor_pos,
350
43
            button,
351
43
            buttons,
352
43
            modifiers,
353
43
        })
354
43
    };
355

            
356
    // ========================================================================
357
    // Mouse Button Events (with proper EventData::Mouse and per-button types)
358
    // ========================================================================
359

            
360
182
    let current_mouse_down = current_state.mouse_state.mouse_down();
361
182
    let previous_mouse_down = previous_state.mouse_state.mouse_down();
362

            
363
546
    for (curr_down, prev_down, button) in [
364
182
        (
365
182
            current_state.mouse_state.left_down,
366
182
            previous_state.mouse_state.left_down,
367
182
            MouseButton::Left,
368
182
        ),
369
182
        (
370
182
            current_state.mouse_state.right_down,
371
182
            previous_state.mouse_state.right_down,
372
182
            MouseButton::Right,
373
182
        ),
374
182
        (
375
182
            current_state.mouse_state.middle_down,
376
182
            previous_state.mouse_state.middle_down,
377
182
            MouseButton::Middle,
378
182
        ),
379
    ] {
380
546
        if curr_down && !prev_down {
381
10
            events.push(SyntheticEvent::new(
382
10
                EventType::MouseDown,
383
10
                EventSource::User,
384
10
                mouse_target,
385
10
                timestamp.clone(),
386
10
                make_mouse_data(button),
387
10
            ));
388
536
        }
389
546
        if !curr_down && prev_down {
390
11
            events.push(SyntheticEvent::new(
391
11
                EventType::MouseUp,
392
11
                EventSource::User,
393
11
                mouse_target,
394
11
                timestamp.clone(),
395
11
                make_mouse_data(button),
396
11
            ));
397
535
        }
398
    }
399

            
400
    // ========================================================================
401
    // Click synthesis: if left mouse released on the same node as down
402
    // ========================================================================
403
    // Note: proper click synthesis requires tracking mousedown target across frames.
404
    // For now, if left mouse was released and the hover node hasn't changed, emit Click.
405
182
    if !current_state.mouse_state.left_down && previous_state.mouse_state.left_down {
406
9
        let prev_hover = hover_manager.previous_hover_node_full();
407
9
        let curr_hover = hover_manager.current_hover_node_full();
408
9
        if prev_hover == curr_hover && curr_hover.is_some() {
409
5
            events.push(SyntheticEvent::new(
410
5
                EventType::Click,
411
5
                EventSource::User,
412
5
                mouse_target,
413
5
                timestamp.clone(),
414
5
                make_mouse_data(MouseButton::Left),
415
5
            ));
416
8
        }
417
173
    }
418

            
419
    // ========================================================================
420
    // Mouse Movement Events
421
    // ========================================================================
422

            
423
182
    let current_in_window = matches!(
424
182
        current_state.mouse_state.cursor_position,
425
        CursorPosition::InWindow(_)
426
    );
427
182
    let previous_in_window = matches!(
428
182
        previous_state.mouse_state.cursor_position,
429
        CursorPosition::InWindow(_)
430
    );
431

            
432
182
    if current_in_window {
433
33
        let current_pos = current_state.mouse_state.cursor_position.get_position();
434
33
        let previous_pos = previous_state.mouse_state.cursor_position.get_position();
435

            
436
        // MouseOver fires on ANY mouse movement (targeted at hovered node)
437
33
        if current_pos != previous_pos {
438
9
            events.push(SyntheticEvent::new(
439
9
                EventType::MouseOver,
440
9
                EventSource::User,
441
9
                mouse_target,
442
9
                timestamp.clone(),
443
9
                make_mouse_data(MouseButton::Left), // MouseOver doesn't care about button
444
9
            ));
445
24
        }
446
149
    }
447

            
448
    // ========================================================================
449
    // Touch Events  (TouchStart / TouchMove / TouchEnd)
450
    // ========================================================================
451
    //
452
    // `FullWindowState::touch_state` is what EVERY platform backend writes on a
453
    // touch — X11 `XI_TouchBegin/Update/End`, `wl_touch`, `WM_TOUCH`, UIKit,
454
    // Android `MotionEvent` — and NOTHING read it. `EventType::TouchStart` and
455
    // friends existed, `HoverEventFilter::TouchStart` was bound to real
456
    // handlers by the slider, split-pane and map widgets, and not one of them
457
    // could fire on any platform: the touch half of the event surface was
458
    // WRITE-ONLY. It is also why the headless E2E touch ops answered `ok` while
459
    // determining nothing at all.
460
    //
461
    // Targeting: a finger is a pointer of its own, so the target is the node
462
    // under THAT point (`HoverManager` keyed by `InputPointId::Touch(id)`) when
463
    // the host published a hit test for it, falling back to the mouse target —
464
    // the same fallback the pen path uses.
465
    //
466
    // `TouchCancel` is deliberately NOT derived here: `TouchState` carries no
467
    // cancel channel (a cancelled point and a lifted point are the same state
468
    // delta), so guessing one would put a fabricated event on the same footing
469
    // as a real one. The debug op refuses by name instead.
470
    //
471
    // KNOWN LIMIT: `deduplicate_synthetic_events` coalesces by (target,
472
    // event_type), so two fingers landing on the SAME node produce one
473
    // `TouchStart`, not two. That is pre-existing engine policy (it collapses
474
    // two simultaneous MouseDowns the same way) and is left alone here; a
475
    // multi-touch test must give each finger its own target, or read the
476
    // per-finger state off `GestureAndDragManager`, which does track sessions
477
    // individually.
478
    {
479
        use crate::managers::hover::InputPointId;
480
        use azul_core::events::TouchEventData;
481

            
482
182
        let previous_points = previous_state.touch_state.touch_points.as_ref();
483
182
        let current_points = current_state.touch_state.touch_points.as_ref();
484

            
485
182
        if !previous_points.is_empty() || !current_points.is_empty() {
486
6
            let target_of = |id: u64| {
487
6
                hover_manager
488
6
                    .hover_node_full_for(&InputPointId::Touch(id))
489
6
                    .unwrap_or(mouse_target)
490
6
            };
491
6
            let touch_data = |p: &azul_core::window::TouchPoint| {
492
6
                EventData::Touch(TouchEventData {
493
6
                    id: p.id,
494
6
                    position: p.position,
495
6
                    force: p.force,
496
6
                })
497
6
            };
498

            
499
11
            for point in current_points {
500
5
                match previous_points.iter().find(|q| q.id == point.id) {
501
1
                    None => events.push(SyntheticEvent::new(
502
1
                        EventType::TouchStart,
503
1
                        EventSource::User,
504
1
                        target_of(point.id),
505
1
                        timestamp.clone(),
506
1
                        touch_data(point),
507
                    )),
508
4
                    Some(before) if before.position != point.position => {
509
2
                        events.push(SyntheticEvent::new(
510
2
                            EventType::TouchMove,
511
2
                            EventSource::User,
512
2
                            target_of(point.id),
513
2
                            timestamp.clone(),
514
2
                            touch_data(point),
515
2
                        ));
516
2
                    }
517
2
                    Some(_) => {}
518
                }
519
            }
520
13
            for point in previous_points {
521
7
                if !current_points.iter().any(|p| p.id == point.id) {
522
3
                    events.push(SyntheticEvent::new(
523
3
                        EventType::TouchEnd,
524
3
                        EventSource::User,
525
3
                        target_of(point.id),
526
3
                        timestamp.clone(),
527
3
                        touch_data(point),
528
3
                    ));
529
4
                }
530
            }
531
176
        }
532
    }
533

            
534
    // ========================================================================
535
    // Wheel / trackpad scroll  →  Scroll event on the hovered node
536
    // ========================================================================
537
    // The platform recorded a wheel delta this pass. The scroll-physics path
538
    // handles scroll *containers*; here we additionally fire a W3C-style Scroll
539
    // event at the hovered node so widgets that treat the wheel specially (the
540
    // map zooms) get a `HoverEventFilter::Scroll` callback. The delta itself is
541
    // read back by the callback via `CallbackInfo::get_scroll_delta`.
542
182
    if let Some(delta) = wheel_delta {
543
6
        if delta.x != 0.0 || delta.y != 0.0 {
544
4
            events.push(SyntheticEvent::new(
545
4
                EventType::Scroll,
546
4
                EventSource::User,
547
4
                mouse_target,
548
4
                timestamp.clone(),
549
4
                EventData::Scroll(ScrollEventData {
550
4
                    delta,
551
4
                    delta_mode: ScrollDeltaMode::Pixel,
552
4
                }),
553
4
            ));
554
4
        }
555
176
    }
556

            
557
    // ========================================================================
558
    // Per-Node MouseEnter/MouseLeave (W3C compliant)
559
    // ========================================================================
560
    // Compare FULL hover chains between current and previous frames.
561
    // Nodes that gained hover get MouseEnter, nodes that lost hover get MouseLeave.
562
    {
563
182
        let current_hovered = get_all_hovered_nodes(hover_manager, 0);
564
182
        let previous_hovered = get_all_hovered_nodes(hover_manager, 1);
565

            
566
        // Nodes that lost hover -> MouseLeave
567
182
        for (dom_id, node_id) in previous_hovered.difference(&current_hovered) {
568
4
            events.push(SyntheticEvent::new(
569
4
                EventType::MouseLeave,
570
4
                EventSource::User,
571
4
                DomNodeId {
572
4
                    dom: *dom_id,
573
4
                    node: NodeHierarchyItemId::from_crate_internal(Some(*node_id)),
574
4
                },
575
4
                timestamp.clone(),
576
4
                EventData::None,
577
4
            ));
578
4
        }
579

            
580
        // Nodes that gained hover -> MouseEnter
581
182
        for (dom_id, node_id) in current_hovered.difference(&previous_hovered) {
582
19
            events.push(SyntheticEvent::new(
583
19
                EventType::MouseEnter,
584
19
                EventSource::User,
585
19
                DomNodeId {
586
19
                    dom: *dom_id,
587
19
                    node: NodeHierarchyItemId::from_crate_internal(Some(*node_id)),
588
19
                },
589
19
                timestamp.clone(),
590
19
                EventData::None,
591
19
            ));
592
19
        }
593
    }
594

            
595
    // Window-level mouse enter/leave (cursor enters/exits OS window)
596
182
    if current_in_window && !previous_in_window {
597
4
        events.push(SyntheticEvent::new(
598
4
            EventType::MouseEnter,
599
4
            EventSource::User,
600
4
            root_node,
601
4
            timestamp.clone(),
602
4
            EventData::None,
603
4
        ));
604
178
    }
605
182
    if !current_in_window && previous_in_window {
606
1
        events.push(SyntheticEvent::new(
607
1
            EventType::MouseLeave,
608
1
            EventSource::User,
609
1
            root_node,
610
1
            timestamp.clone(),
611
1
            EventData::None,
612
1
        ));
613
181
    }
614

            
615
    // Keyboard Events
616
    // W3C: Keyboard events target the focused element, falling back to root
617

            
618
182
    let focus_target = focus_manager
619
182
        .get_focused_node()
620
182
        .copied()
621
182
        .unwrap_or(root_node);
622

            
623
182
    let current_key = current_state
624
182
        .keyboard_state
625
182
        .current_virtual_keycode
626
182
        .into_option();
627
182
    let previous_key = previous_state
628
182
        .keyboard_state
629
182
        .current_virtual_keycode
630
182
        .into_option();
631

            
632
    // KeyDown: Fires when a new key is pressed
633
    // Case 1: New key pressed (current != previous)
634
    // Case 2: Same key pressed again after release (current.is_some() && previous.is_none())
635
182
    let keyboard_data = EventData::Keyboard(KeyboardEventData {
636
182
        key_code: current_key.map_or(0, |k| k as u32),
637
182
        char_code: None, // Character is available from the keyboard state
638
182
        modifiers,
639
        repeat: false,
640
    });
641

            
642
182
    if current_key.is_some() && (current_key != previous_key || previous_key.is_none()) {
643
20
        events.push(SyntheticEvent::new(
644
20
            EventType::KeyDown,
645
20
            EventSource::User,
646
20
            focus_target,
647
20
            timestamp.clone(),
648
20
            keyboard_data,
649
20
        ));
650
162
    }
651
182
    let key_up_data = EventData::Keyboard(KeyboardEventData {
652
182
        key_code: previous_key.map_or(0, |k| k as u32),
653
182
        char_code: None,
654
182
        modifiers,
655
        repeat: false,
656
    });
657

            
658
182
    if previous_key.is_some() && current_key.is_none() {
659
16
        events.push(SyntheticEvent::new(
660
16
            EventType::KeyUp,
661
16
            EventSource::User,
662
16
            focus_target,
663
16
            timestamp.clone(),
664
16
            key_up_data,
665
16
        ));
666
166
    }
667

            
668
    // Window State Events
669

            
670
    // Window resize
671
182
    if current_state.size.dimensions != previous_state.size.dimensions {
672
9
        events.push(SyntheticEvent::new(
673
9
            EventType::WindowResize,
674
9
            EventSource::User,
675
9
            root_node,
676
9
            timestamp.clone(),
677
9
            EventData::Window(WindowEventData {
678
9
                size: Some(LogicalRect {
679
9
                    origin: LogicalPosition { x: 0.0, y: 0.0 },
680
9
                    size: current_state.size.dimensions,
681
9
                }),
682
9
                position: None,
683
9
            }),
684
9
        ));
685
173
    }
686

            
687
    // Window moved
688
182
    if current_state.position != previous_state.position {
689
3
        if let WindowPosition::Initialized(phys_pos) = current_state.position {
690
2
            events.push(SyntheticEvent::new(
691
2
                EventType::WindowMove,
692
2
                EventSource::User,
693
2
                root_node,
694
2
                timestamp.clone(),
695
2
                EventData::Window(WindowEventData {
696
2
                    size: None,
697
2
                    position: Some(LogicalPosition {
698
2
                        x: phys_pos.x as f32,
699
2
                        y: phys_pos.y as f32,
700
2
                    }),
701
2
                }),
702
2
            ));
703
3
        }
704
179
    }
705

            
706
    // Window frame transition (minimize / maximize / restore / fullscreen).
707
    //
708
    // The backends have kept `flags.frame` in sync for a long time, and both
709
    // the Win32 and macOS handlers carried a comment claiming the app's
710
    // Maximize/Restore callbacks fired as a result. They did not: nothing
711
    // derived an event from this field, so no callback could ever observe a
712
    // frame transition.
713
182
    if current_state.flags.frame != previous_state.flags.frame {
714
5
        events.push(SyntheticEvent::new(
715
5
            EventType::WindowFrameChanged,
716
5
            EventSource::User,
717
5
            root_node,
718
5
            timestamp.clone(),
719
5
            EventData::None,
720
5
        ));
721
177
    }
722

            
723
    // Window close requested
724
182
    if current_state.flags.close_requested && !previous_state.flags.close_requested {
725
1
        events.push(SyntheticEvent::new(
726
1
            EventType::WindowClose,
727
1
            EventSource::User,
728
1
            root_node,
729
1
            timestamp.clone(),
730
1
            EventData::None,
731
1
        ));
732
181
    }
733

            
734
    // Window focus changed
735
182
    if current_state.window_focused && !previous_state.window_focused {
736
2
        events.push(SyntheticEvent::new(
737
2
            EventType::WindowFocusIn,
738
2
            EventSource::User,
739
2
            root_node,
740
2
            timestamp.clone(),
741
2
            EventData::None,
742
2
        ));
743
180
    }
744
182
    if !current_state.window_focused && previous_state.window_focused {
745
3
        events.push(SyntheticEvent::new(
746
3
            EventType::WindowFocusOut,
747
3
            EventSource::User,
748
3
            root_node,
749
3
            timestamp.clone(),
750
3
            EventData::None,
751
3
        ));
752
179
    }
753

            
754
    // Theme changed
755
182
    if current_state.theme != previous_state.theme {
756
1
        events.push(SyntheticEvent::new(
757
1
            EventType::ThemeChange,
758
1
            EventSource::User,
759
1
            root_node,
760
1
            timestamp.clone(),
761
1
            EventData::None,
762
1
        ));
763
181
    }
764

            
765
    // DPI changed (moved to a different-DPI monitor, or system DPI setting changed)
766
182
    if current_state.size.dpi != previous_state.size.dpi {
767
3
        events.push(SyntheticEvent::new(
768
3
            EventType::WindowDpiChanged,
769
3
            EventSource::User,
770
3
            root_node,
771
3
            timestamp.clone(),
772
3
            EventData::None,
773
3
        ));
774
179
    }
775

            
776
    // Monitor changed (window moved to a different monitor)
777
182
    if current_state.monitor_id != previous_state.monitor_id
778
3
        && current_state.monitor_id != azul_css::corety::OptionU32::None
779
2
        && previous_state.monitor_id != azul_css::corety::OptionU32::None
780
1
    {
781
1
        events.push(SyntheticEvent::new(
782
1
            EventType::WindowMonitorChanged,
783
1
            EventSource::User,
784
1
            root_node,
785
1
            timestamp.clone(),
786
1
            EventData::None,
787
1
        ));
788
181
    }
789

            
790
    // File Drop Events
791
    //
792
    // FileHover fires while a file is hovered over the window; FileHoverCancel
793
    // fires once when that hover ends without a drop. Both depend on platform
794
    // backends calling `file_drop_manager.set_hovered_file(Some/None)` from
795
    // their drag-enter / drag-leave handlers (macOS `draggingEntered` /
796
    // `draggingExited`, Windows OLE `IDropTarget::DragEnter` / `DragLeave`).
797

            
798
    // MWA-B7: target the node UNDER the drag position (mouse_target already
799
    // falls back to the root when nothing is hovered). Targeting the root
800
    // unconditionally made per-node drop targets impossible — a DroppedFile
801
    // callback registered on any non-root node could never fire.
802
182
    if file_drop_manager.get_hovered_file().is_some() {
803
3
        events.push(SyntheticEvent::new(
804
3
            EventType::FileHover,
805
3
            EventSource::User,
806
3
            mouse_target,
807
3
            timestamp.clone(),
808
3
            EventData::None,
809
3
        ));
810
179
    } else if file_drop_manager.hover_was_cancelled() {
811
1
        events.push(SyntheticEvent::new(
812
1
            EventType::FileHoverCancel,
813
1
            EventSource::User,
814
1
            mouse_target,
815
1
            timestamp.clone(),
816
1
            EventData::None,
817
1
        ));
818
178
    }
819

            
820
182
    if file_drop_manager.get_dropped_file().is_some() {
821
3
        events.push(SyntheticEvent::new(
822
3
            EventType::FileDrop,
823
3
            EventSource::User,
824
3
            mouse_target,
825
3
            timestamp.clone(),
826
3
            EventData::None,
827
3
        ));
828
179
    }
829

            
830
    // Pen / Stylus Events (W3C PointerEvent, pointerType "pen")
831
    // Diff the gesture manager's pen state; `pen_event_pending` gates one diff per
832
    // update (the event loop clears it after this pass, like the sensor manager).
833
    // Targets the hovered node (pen-as-pointer); full pen data via CallbackInfo.
834
182
    if let Some(manager) = gesture_manager {
835
106
        if manager.pen_event_pending {
836
6
            let pen_data = make_mouse_data(MouseButton::Left);
837
6
            match (manager.get_previous_pen_state(), manager.get_pen_state()) {
838
1
                (None, Some(_)) => events.push(SyntheticEvent::new(
839
1
                    EventType::PenEnter,
840
1
                    EventSource::User,
841
1
                    mouse_target,
842
1
                    timestamp.clone(),
843
1
                    pen_data,
844
                )),
845
1
                (Some(_), None) => events.push(SyntheticEvent::new(
846
1
                    EventType::PenLeave,
847
1
                    EventSource::User,
848
1
                    mouse_target,
849
1
                    timestamp.clone(),
850
1
                    pen_data,
851
                )),
852
4
                (Some(p), Some(c)) => {
853
4
                    if !p.in_contact && c.in_contact {
854
1
                        events.push(SyntheticEvent::new(
855
1
                            EventType::PenDown,
856
1
                            EventSource::User,
857
1
                            mouse_target,
858
1
                            timestamp.clone(),
859
1
                            pen_data.clone(),
860
1
                        ));
861
3
                    } else if p.in_contact && !c.in_contact {
862
1
                        events.push(SyntheticEvent::new(
863
1
                            EventType::PenUp,
864
1
                            EventSource::User,
865
1
                            mouse_target,
866
1
                            timestamp.clone(),
867
1
                            pen_data.clone(),
868
1
                        ));
869
2
                    }
870
4
                    if p.position != c.position {
871
1
                        events.push(SyntheticEvent::new(
872
1
                            EventType::PenMove,
873
1
                            EventSource::User,
874
1
                            mouse_target,
875
1
                            timestamp.clone(),
876
1
                            pen_data,
877
1
                        ));
878
3
                    }
879
                }
880
                (None, None) => {}
881
            }
882
100
        }
883
76
    }
884

            
885
    // Gesture Events
886

            
887
182
    if let Some(manager) = gesture_manager {
888
106
        let event_was_mouse_release = !current_mouse_down && previous_mouse_down;
889

            
890
        // Detect DragStart (targeted at hovered node)
891
106
        if manager.detect_drag().is_some()
892
            && !manager.is_dragging() {
893
                events.push(SyntheticEvent::new(
894
                    EventType::DragStart,
895
                    EventSource::User,
896
                    mouse_target,
897
                    timestamp.clone(),
898
                    make_mouse_data(MouseButton::Left),
899
                ));
900
106
            }
901

            
902
        // Detect Drag (continuous movement, targeted at hovered node)
903
106
        if manager.is_dragging() && current_mouse_down {
904
            let current_pos = current_state.mouse_state.cursor_position.get_position();
905
            let previous_pos = previous_state.mouse_state.cursor_position.get_position();
906

            
907
            if current_pos != previous_pos {
908
                events.push(SyntheticEvent::new(
909
                    EventType::Drag,
910
                    EventSource::User,
911
                    mouse_target,
912
                    timestamp.clone(),
913
                    make_mouse_data(MouseButton::Left),
914
                ));
915
            }
916
106
        }
917

            
918
        // Detect DragEnd (targeted at hovered node)
919
106
        if manager.is_dragging() && event_was_mouse_release {
920
            events.push(SyntheticEvent::new(
921
                EventType::DragEnd,
922
                EventSource::User,
923
                mouse_target,
924
                timestamp.clone(),
925
                make_mouse_data(MouseButton::Left),
926
            ));
927

            
928
            // When mouse is released during a node drag, generate a Drop event
929
            // on the current drop target (the node under the cursor)
930
            if manager.is_node_drag_active() {
931
                events.push(SyntheticEvent::new(
932
                    EventType::Drop,
933
                    EventSource::User,
934
                    mouse_target, // W3C: Drop targets the node under cursor
935
                    timestamp.clone(),
936
                    make_mouse_data(MouseButton::Left),
937
                ));
938
            }
939
106
        }
940

            
941
        // Detect DragEnter/DragOver/DragLeave events on drop targets
942
        // W3C: These fire ON the drop target node (the node UNDER the cursor)
943
106
        if manager.is_node_drag_active() && current_mouse_down {
944
            let current_hover = hover_manager.current_hover_node();
945
            let previous_hover = hover_manager.previous_hover_node();
946

            
947
            // If the hover node changed, generate DragLeave on old + DragEnter on new
948
            if current_hover != previous_hover {
949
                if let Some(prev_node) = previous_hover {
950
                    events.push(SyntheticEvent::new(
951
                        EventType::DragLeave,
952
                        EventSource::User,
953
                        DomNodeId {
954
                            dom: root_node.dom,
955
                            node: NodeHierarchyItemId::from_crate_internal(Some(prev_node)),
956
                        },
957
                        timestamp.clone(),
958
                        EventData::None,
959
                    ));
960
                }
961
                if let Some(curr_node) = current_hover {
962
                    events.push(SyntheticEvent::new(
963
                        EventType::DragEnter,
964
                        EventSource::User,
965
                        DomNodeId {
966
                            dom: root_node.dom,
967
                            node: NodeHierarchyItemId::from_crate_internal(Some(curr_node)),
968
                        },
969
                        timestamp.clone(),
970
                        EventData::None,
971
                    ));
972
                }
973
            }
974

            
975
            // DragOver fires continuously while hovering a drop target
976
            if let Some(curr_node) = current_hover {
977
                events.push(SyntheticEvent::new(
978
                    EventType::DragOver,
979
                    EventSource::User,
980
                    DomNodeId {
981
                        dom: root_node.dom,
982
                        node: NodeHierarchyItemId::from_crate_internal(Some(curr_node)),
983
                    },
984
                    timestamp.clone(),
985
                    EventData::None,
986
                ));
987
            }
988
106
        }
989

            
990
        // Detect DoubleClick (targeted at hovered node)
991
106
        if manager.detect_double_click() {
992
1
            events.push(SyntheticEvent::new(
993
1
                EventType::DoubleClick,
994
1
                EventSource::User,
995
1
                mouse_target,
996
1
                timestamp.clone(),
997
1
                make_mouse_data(MouseButton::Left),
998
1
            ));
999
105
        }
        // Detect LongPress (targeted at hovered node)
106
        if let Some(long_press) = manager.detect_long_press() {
2
            if !long_press.callback_invoked {
1
                events.push(SyntheticEvent::new(
1
                    EventType::LongPress,
1
                    EventSource::User,
1
                    mouse_target,
1
                    timestamp.clone(),
1
                    make_mouse_data(MouseButton::Left),
1
                ));
1
            }
104
        }
        // Detect Swipe gestures (targeted at hovered node)
106
        if let Some(direction) = manager.detect_swipe_direction() {
            use crate::managers::gesture::GestureDirection;
4
            let event_type = match direction {
1
                GestureDirection::Left => EventType::SwipeLeft,
1
                GestureDirection::Right => EventType::SwipeRight,
1
                GestureDirection::Up => EventType::SwipeUp,
1
                GestureDirection::Down => EventType::SwipeDown,
            };
4
            events.push(SyntheticEvent::new(
4
                event_type,
4
                EventSource::User,
4
                mouse_target,
4
                timestamp.clone(),
4
                EventData::None,
            ));
102
        }
        // Detect Pinch gestures (targeted at hovered node)
106
        if let Some(pinch) = manager.detect_pinch() {
8
            let event_type = if pinch.scale < 1.0 {
4
                EventType::PinchIn
            } else {
4
                EventType::PinchOut
            };
8
            events.push(SyntheticEvent::new(
8
                event_type,
8
                EventSource::User,
8
                mouse_target,
8
                timestamp.clone(),
8
                EventData::None,
            ));
98
        }
        // Detect Rotation gestures (targeted at hovered node)
106
        if let Some(rotation) = manager.detect_rotation() {
7
            let event_type = if rotation.angle_radians > 0.0 {
2
                EventType::RotateClockwise
            } else {
5
                EventType::RotateCounterClockwise
            };
7
            events.push(SyntheticEvent::new(
7
                event_type,
7
                EventSource::User,
7
                mouse_target,
7
                timestamp.clone(),
7
                EventData::None,
            ));
99
        }
        // Detect Pen events (targeted at hovered node)
106
        if let Some(pen_state) = manager.get_pen_state() {
7
            if pen_state.in_contact {
5
                let current_pos = current_state.mouse_state.cursor_position.get_position();
5
                let previous_pos = previous_state.mouse_state.cursor_position.get_position();
5
                if current_pos != previous_pos {
1
                    events.push(SyntheticEvent::new(
1
                        EventType::TouchMove, // Use TouchMove as fallback
1
                        EventSource::User,
1
                        mouse_target,
1
                        timestamp.clone(),
1
                        EventData::None,
1
                    ));
4
                }
2
            }
99
        }
76
    }
    // Manager Events (Scroll, Text Input, etc.)
703
    for manager in managers {
521
        events.extend(manager.get_pending_events(timestamp.clone()));
521
    }
    // Deduplication
182
    deduplicate_synthetic_events(events)
182
}
#[cfg(test)]
mod tests {
    use super::*;
    use azul_core::window::{
        VirtualKeyCode, VirtualKeyCodeVec,
        OptionVirtualKeyCode,
    };
18
    fn ts() -> Instant { Instant::Tick(SystemTick::new(0)) }
30
    fn default_state() -> FullWindowState {
30
        FullWindowState::default()
30
    }
10
    fn state_with_key(vk: VirtualKeyCode) -> FullWindowState {
10
        let mut s = default_state();
10
        s.keyboard_state.current_virtual_keycode = OptionVirtualKeyCode::Some(vk);
10
        s.keyboard_state.pressed_virtual_keycodes = VirtualKeyCodeVec::from_vec(vec![vk]);
10
        s
10
    }
2
    fn state_with_left_down(x: f32, y: f32) -> FullWindowState {
2
        let mut s = default_state();
2
        s.mouse_state.cursor_position = CursorPosition::InWindow(LogicalPosition::new(x, y));
2
        s.mouse_state.left_down = true;
2
        s
2
    }
4
    fn state_with_cursor(x: f32, y: f32) -> FullWindowState {
4
        let mut s = default_state();
4
        s.mouse_state.cursor_position = CursorPosition::InWindow(LogicalPosition::new(x, y));
4
        s
4
    }
18
    fn empty_providers() -> Vec<&'static dyn EventProvider> { vec![] }
18
    fn run_determine(
18
        current: &FullWindowState,
18
        previous: &FullWindowState,
18
    ) -> Vec<SyntheticEvent> {
18
        let focus = crate::managers::focus_cursor::FocusManager::new();
18
        let hover = crate::managers::hover::HoverManager::new();
18
        let filedrop = crate::managers::file_drop::FileDropManager::new();
18
        let providers = empty_providers();
18
        determine_all_events(current, previous, &hover, &focus, &filedrop, None, &providers, None, ts())
18
    }
    // === Touch tests ===
    //
    // Regression guard for a hole that made the whole touch surface
    // write-only: every backend wrote `FullWindowState.touch_state` and
    // `determine_all_events` never read it, so `HoverEventFilter::TouchStart`
    // handlers (slider, split-pane, map) could not fire on ANY platform.
9
    fn n_of(events: &[SyntheticEvent], ty: EventType) -> usize {
9
        events.iter().filter(|e| e.event_type == ty).count()
9
    }
6
    fn state_with_touch(points: &[(u64, f32, f32)]) -> FullWindowState {
        use azul_core::window::{TouchPoint, TouchPointVec};
6
        let mut s = default_state();
6
        let pts: Vec<TouchPoint> = points
6
            .iter()
6
            .map(|(id, x, y)| TouchPoint {
8
                id: *id,
8
                position: LogicalPosition::new(*x, *y),
                force: 0.5,
8
            })
6
            .collect();
6
        s.touch_state.num_touches = pts.len();
6
        s.touch_state.touch_points = TouchPointVec::from_vec(pts);
6
        s
6
    }
    #[test]
1
    fn touch_start_fires_for_a_newly_down_point() {
1
        let events = run_determine(&state_with_touch(&[(1, 10.0, 20.0)]), &default_state());
1
        let ts: Vec<_> = events
1
            .iter()
1
            .filter(|e| e.event_type == EventType::TouchStart)
1
            .collect();
1
        assert_eq!(ts.len(), 1);
1
        match &ts[0].data {
1
            EventData::Touch(t) => {
1
                assert_eq!(t.id, 1);
1
                assert_eq!(t.position, LogicalPosition::new(10.0, 20.0));
            }
            other => panic!("TouchStart must carry EventData::Touch, got {other:?}"),
        }
1
    }
    #[test]
1
    fn touch_move_fires_only_when_the_point_moved() {
1
        let held = state_with_touch(&[(1, 10.0, 20.0)]);
1
        assert_eq!(n_of(&run_determine(&held, &held), EventType::TouchMove), 0);
1
        let moved = state_with_touch(&[(1, 11.0, 20.0)]);
1
        assert_eq!(n_of(&run_determine(&moved, &held), EventType::TouchMove), 1);
1
        assert_eq!(n_of(&run_determine(&moved, &held), EventType::TouchStart), 0);
1
    }
    #[test]
1
    fn touch_end_fires_for_a_point_that_disappeared() {
1
        let down = state_with_touch(&[(1, 10.0, 20.0), (2, 30.0, 40.0)]);
1
        let one_left = state_with_touch(&[(2, 30.0, 40.0)]);
1
        let events = run_determine(&one_left, &down);
1
        assert_eq!(n_of(&events, EventType::TouchEnd), 1);
1
        let ended = events
1
            .iter()
1
            .find(|e| e.event_type == EventType::TouchEnd)
1
            .expect("a TouchEnd exists");
1
        match &ended.data {
1
            EventData::Touch(t) => assert_eq!(t.id, 1),
            other => panic!("TouchEnd must carry EventData::Touch, got {other:?}"),
        }
1
    }
    #[test]
1
    fn no_touch_events_without_a_touch_state_delta() {
1
        let events = run_determine(&default_state(), &default_state());
1
        assert_eq!(n_of(&events, EventType::TouchStart), 0);
1
        assert_eq!(n_of(&events, EventType::TouchMove), 0);
1
        assert_eq!(n_of(&events, EventType::TouchEnd), 0);
1
    }
    #[test]
1
    fn touch_cancel_is_never_synthesised() {
        // `TouchState` has no cancel channel; lifting every point is
        // indistinguishable from a cancel, so the determination emits TouchEnd
        // and the debug op refuses `touch_cancel` by name rather than letting a
        // cancellation test go green on end semantics.
        //
        // Both fingers resolve to the SAME target here (no per-touch hit test
        // is published in this fixture, so both fall back to the root), and
        // `deduplicate_synthetic_events` coalesces by (target, event_type) —
        // pre-existing engine policy, the same thing that collapses two
        // simultaneous MouseDowns. So one TouchEnd survives, and zero
        // TouchCancels, which is the property under test.
1
        let down = state_with_touch(&[(1, 10.0, 20.0), (2, 30.0, 40.0)]);
1
        let events = run_determine(&default_state(), &down);
1
        assert_eq!(n_of(&events, EventType::TouchEnd), 1);
1
        assert_eq!(n_of(&events, EventType::TouchCancel), 0);
1
    }
    // === Keyboard tests ===
    #[test]
1
    fn keydown_fires_when_key_newly_pressed() {
1
        let events = run_determine(&state_with_key(VirtualKeyCode::A), &default_state());
1
        let kd: Vec<_> = events.iter().filter(|e| e.event_type == EventType::KeyDown).collect();
1
        assert_eq!(kd.len(), 1);
1
        assert!(matches!(kd[0].data, EventData::Keyboard(_)));
1
    }
    #[test]
1
    fn keydown_skipped_when_same_key_held() {
1
        let s = state_with_key(VirtualKeyCode::A);
1
        let events = run_determine(&s, &s);
1
        let kd = events.iter().filter(|e| e.event_type == EventType::KeyDown).count();
1
        assert_eq!(kd, 0, "KeyDown should not fire when same key held");
1
    }
    #[test]
1
    fn keydown_fires_for_different_key() {
1
        let events = run_determine(
1
            &state_with_key(VirtualKeyCode::B),
1
            &state_with_key(VirtualKeyCode::A),
        );
1
        let kd = events.iter().filter(|e| e.event_type == EventType::KeyDown).count();
1
        assert_eq!(kd, 1);
1
    }
    #[test]
1
    fn keyup_fires_when_key_released() {
1
        let events = run_determine(&default_state(), &state_with_key(VirtualKeyCode::A));
1
        let ku: Vec<_> = events.iter().filter(|e| e.event_type == EventType::KeyUp).collect();
1
        assert_eq!(ku.len(), 1);
1
        assert!(matches!(ku[0].data, EventData::Keyboard(_)));
1
    }
    #[test]
1
    fn backspace_keydown_has_keyboard_data() {
1
        let events = run_determine(&state_with_key(VirtualKeyCode::Back), &default_state());
1
        let kd: Vec<_> = events.iter().filter(|e| e.event_type == EventType::KeyDown).collect();
1
        assert_eq!(kd.len(), 1);
1
        match &kd[0].data {
1
            EventData::Keyboard(kb) => assert_eq!(kb.key_code, VirtualKeyCode::Back as u32),
            other => panic!("Expected Keyboard data, got {other:?}"),
        }
1
    }
    // === Mouse tests ===
    #[test]
1
    fn mousedown_fires_on_left_press() {
1
        let events = run_determine(
1
            &state_with_left_down(100.0, 200.0),
1
            &state_with_cursor(100.0, 200.0),
        );
1
        let md = events.iter().filter(|e| e.event_type == EventType::MouseDown).count();
1
        assert_eq!(md, 1);
1
    }
    #[test]
1
    fn mouseup_fires_on_left_release() {
1
        let events = run_determine(
1
            &state_with_cursor(100.0, 200.0),
1
            &state_with_left_down(100.0, 200.0),
        );
1
        let mu = events.iter().filter(|e| e.event_type == EventType::MouseUp).count();
1
        assert_eq!(mu, 1);
1
    }
    #[test]
1
    fn no_events_when_state_unchanged() {
1
        let s = default_state();
1
        let events = run_determine(&s, &s);
1
        assert!(events.is_empty(), "Got {} events when state unchanged", events.len());
1
    }
    #[test]
1
    fn mouseover_fires_on_cursor_move() {
1
        let events = run_determine(
1
            &state_with_cursor(150.0, 250.0),
1
            &state_with_cursor(100.0, 200.0),
        );
1
        let mo = events.iter().filter(|e| e.event_type == EventType::MouseOver).count();
1
        assert_eq!(mo, 1);
1
    }
    #[test]
1
    fn key_repeat_fires_keydown_when_previous_cleared() {
        // Simulates what the platform layer does for key repeat:
        // previous has current_virtual_keycode=None (cleared by platform),
        // current has Some(Left). This should fire KeyDown even though
        // the key was already pressed in the previous frame.
1
        let mut previous = state_with_key(VirtualKeyCode::Left);
        // Platform clears this for repeat detection:
1
        previous.keyboard_state.current_virtual_keycode = OptionVirtualKeyCode::None;
1
        let current = state_with_key(VirtualKeyCode::Left);
1
        let events = run_determine(&current, &previous);
1
        let kd = events.iter().filter(|e| e.event_type == EventType::KeyDown).count();
1
        assert_eq!(kd, 1, "Key repeat should fire KeyDown when previous is cleared");
1
    }
    #[test]
1
    fn key_repeat_skipped_when_previous_not_cleared() {
        // Without the platform fix: both previous and current have Same(Left).
        // KeyDown should NOT fire (this documents the limitation that the
        // platform layer MUST clear previous for repeats).
1
        let previous = state_with_key(VirtualKeyCode::Left);
1
        let current = state_with_key(VirtualKeyCode::Left);
1
        let events = run_determine(&current, &previous);
1
        let kd = events.iter().filter(|e| e.event_type == EventType::KeyDown).count();
1
        assert_eq!(kd, 0, "Without platform clearing, repeat is not detected");
1
    }
}
#[cfg(test)]
#[allow(clippy::float_cmp, clippy::cast_precision_loss)]
mod autotest_generated {
    use azul_core::{
        geom::{LogicalSize, PhysicalPositionI32},
        hit_test::{FullHitTest, HitTest, HitTestItem},
        window::{
            OptionVirtualKeyCode, VirtualKeyCode, VirtualKeyCodeVec, WindowTheme,
        },
    };
    use azul_css::{corety::OptionU32, AzString};
    use super::*;
    use crate::managers::{
        file_drop::FileDropManager,
        focus_cursor::FocusManager,
        gesture::{
            DetectedLongPress, DetectedPinch, DetectedRotation, GestureAndDragManager,
            GestureDirection, NativeGestureEvent,
        },
        hover::{HoverManager, InputPointId},
    };
    // ------------------------------------------------------------------
    // Fixtures
    // ------------------------------------------------------------------
    fn ts(tick: u64) -> Instant {
        Instant::Tick(SystemTick::new(tick))
    }
    fn node(dom: usize, n: usize) -> DomNodeId {
        DomNodeId {
            dom: DomId { inner: dom },
            node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(n))),
        }
    }
    fn root() -> DomNodeId {
        node(0, 0)
    }
    fn ev(event_type: EventType, target: DomNodeId, tick: u64) -> SyntheticEvent {
        SyntheticEvent::new(
            event_type,
            EventSource::User,
            target,
            ts(tick),
            EventData::None,
        )
    }
    fn hit_item() -> HitTestItem {
        HitTestItem {
            point_in_viewport: LogicalPosition::new(0.0, 0.0),
            point_relative_to_item: LogicalPosition::new(0.0, 0.0),
            is_focusable: false,
            is_virtual_view_hit: None,
            hit_depth: 0,
        }
    }
    /// A hit test where `(dom, node)` pairs are "regular" hits.
    fn hits(pairs: &[(usize, usize)]) -> FullHitTest {
        let mut ht = FullHitTest::empty(None);
        for (dom, n) in pairs {
            ht.hovered_nodes
                .entry(DomId { inner: *dom })
                .or_insert_with(HitTest::empty)
                .regular_hit_test_nodes
                .insert(NodeId::new(*n), hit_item());
        }
        ht
    }
    /// Hover manager with `previous` recorded first, so it lands at frame 1.
    fn hover_with(previous: FullHitTest, current: FullHitTest) -> HoverManager {
        let mut hm = HoverManager::new();
        hm.push_hit_test(InputPointId::Mouse, previous);
        hm.push_hit_test(InputPointId::Mouse, current);
        hm
    }
    fn state() -> FullWindowState {
        FullWindowState::default()
    }
    fn cursor_at(x: f32, y: f32) -> FullWindowState {
        let mut s = state();
        s.mouse_state.cursor_position = CursorPosition::InWindow(LogicalPosition::new(x, y));
        s
    }
    /// Test-only `EventProvider` that replays a fixed list of events.
    struct StaticProvider(Vec<SyntheticEvent>);
    impl EventProvider for StaticProvider {
        fn get_pending_events(&self, _timestamp: Instant) -> Vec<SyntheticEvent> {
            self.0.clone()
        }
    }
    /// Test-only `EventProvider` that stamps events with the timestamp it is handed.
    struct EchoProvider(DomNodeId);
    impl EventProvider for EchoProvider {
        fn get_pending_events(&self, timestamp: Instant) -> Vec<SyntheticEvent> {
            vec![SyntheticEvent::new(
                EventType::Scroll,
                EventSource::User,
                self.0,
                timestamp,
                EventData::None,
            )]
        }
    }
    fn count(events: &[SyntheticEvent], ty: EventType) -> usize {
        events.iter().filter(|e| e.event_type == ty).count()
    }
    fn only(events: &[SyntheticEvent], ty: EventType) -> SyntheticEvent {
        let matching: Vec<_> = events.iter().filter(|e| e.event_type == ty).collect();
        assert_eq!(
            matching.len(),
            1,
            "expected exactly one {ty:?}, got {matching:?}"
        );
        matching[0].clone()
    }
    /// `determine_all_events` with default managers and no providers.
    fn run(
        current: &FullWindowState,
        previous: &FullWindowState,
        hover: &HoverManager,
        gesture: Option<&GestureAndDragManager>,
        wheel: Option<LogicalPosition>,
    ) -> Vec<SyntheticEvent> {
        let focus = FocusManager::new();
        let file_drop = FileDropManager::new();
        let providers: Vec<&dyn EventProvider> = Vec::new();
        determine_all_events(
            current,
            previous,
            hover,
            &focus,
            &file_drop,
            gesture,
            &providers,
            wheel,
            ts(0),
        )
    }
    /// `determine_all_events` with an empty hover manager and no gestures.
    fn run_plain(current: &FullWindowState, previous: &FullWindowState) -> Vec<SyntheticEvent> {
        run(current, previous, &HoverManager::new(), None, None)
    }
    // ==================================================================
    // get_all_hovered_nodes  (numeric: zero / min_max / overflow)
    // ==================================================================
    #[test]
    fn hovered_nodes_empty_manager_is_empty_at_frame_zero() {
        let hm = HoverManager::new();
        assert!(get_all_hovered_nodes(&hm, 0).is_empty());
    }
    #[test]
    fn hovered_nodes_frame_index_usize_max_does_not_panic() {
        let hm = hover_with(hits(&[(0, 1)]), hits(&[(0, 2)]));
        // VecDeque::get(usize::MAX) must return None, not index out of bounds.
        assert!(get_all_hovered_nodes(&hm, usize::MAX).is_empty());
        assert!(get_all_hovered_nodes(&hm, usize::MAX - 1).is_empty());
    }
    #[test]
    fn hovered_nodes_frame_index_past_history_is_empty() {
        let hm = hover_with(hits(&[(0, 1)]), hits(&[(0, 2)]));
        assert_eq!(get_all_hovered_nodes(&hm, 0).len(), 1);
        assert_eq!(get_all_hovered_nodes(&hm, 1).len(), 1);
        assert!(get_all_hovered_nodes(&hm, 2).is_empty());
        assert!(get_all_hovered_nodes(&hm, 100).is_empty());
    }
    #[test]
    fn hovered_nodes_history_is_capped_at_five_frames() {
        let mut hm = HoverManager::new();
        for i in 0..8_usize {
            hm.push_hit_test(InputPointId::Mouse, hits(&[(0, i)]));
        }
        // Most recent push is frame 0.
        assert!(get_all_hovered_nodes(&hm, 0).contains(&(DomId { inner: 0 }, NodeId::new(7))));
        // Ring buffer keeps 5 frames: nodes 7,6,5,4,3.
        assert!(get_all_hovered_nodes(&hm, 4).contains(&(DomId { inner: 0 }, NodeId::new(3))));
        assert!(
            get_all_hovered_nodes(&hm, 5).is_empty(),
            "frame 5 must have been evicted by the 5-frame cap"
        );
    }
    #[test]
    fn hovered_nodes_walks_every_hit_dom_not_just_the_root() {
        let hm = hover_with(
            FullHitTest::empty(None),
            hits(&[(0, 1), (0, 2), (7, 3), (usize::MAX, 4)]),
        );
        let set = get_all_hovered_nodes(&hm, 0);
        assert_eq!(set.len(), 4);
        assert!(set.contains(&(DomId { inner: 0 }, NodeId::new(1))));
        assert!(set.contains(&(DomId { inner: 7 }, NodeId::new(3))));
        assert!(set.contains(&(DomId { inner: usize::MAX }, NodeId::new(4))));
    }
    #[test]
    fn hovered_nodes_accepts_extreme_node_ids() {
        // NodeId is a plain usize index here; no encoding happens in this
        // function, so even usize::MAX must round-trip untouched.
        let hm = hover_with(
            FullHitTest::empty(None),
            hits(&[(0, 0), (0, usize::MAX), (0, usize::MAX - 1)]),
        );
        let set = get_all_hovered_nodes(&hm, 0);
        assert_eq!(set.len(), 3);
        assert!(set.contains(&(DomId { inner: 0 }, NodeId::new(usize::MAX))));
        assert!(set.contains(&(DomId { inner: 0 }, NodeId::ZERO)));
    }
    #[test]
    fn hovered_nodes_ignores_non_regular_hits() {
        let mut ht = FullHitTest::empty(None);
        let mut hit = HitTest::empty();
        hit.cursor_hit_test_nodes.insert(
            NodeId::new(9),
            azul_core::hit_test::CursorHitTestItem {
                cursor_type: azul_core::hit_test::CursorType::Default,
                hit_depth: 0,
                point_in_viewport: LogicalPosition::new(0.0, 0.0),
            },
        );
        ht.hovered_nodes.insert(DomId { inner: 0 }, hit);
        let mut hm = HoverManager::new();
        hm.push_hit_test(InputPointId::Mouse, ht);
        assert!(
            get_all_hovered_nodes(&hm, 0).is_empty(),
            "only regular hits count as hovered"
        );
    }
    // ==================================================================
    // detect_window_state_events
    // ==================================================================
    #[test]
    fn window_state_identical_states_yield_no_events() {
        let s = state();
        assert!(detect_window_state_events(&s, &s, ts(0)).is_empty());
    }
    #[test]
    fn window_resize_reports_the_current_dimensions() {
        let mut current = state();
        current.size.dimensions = LogicalSize::new(1234.0, 5678.0);
        let events = detect_window_state_events(&current, &state(), ts(0));
        let e = only(&events, EventType::WindowResize);
        match e.data {
            EventData::Window(w) => {
                let rect = w.size.expect("resize carries a size");
                assert_eq!(rect.origin.x, 0.0);
                assert_eq!(rect.size.width, 1234.0);
                assert_eq!(rect.size.height, 5678.0);
            }
            other => panic!("expected Window data, got {other:?}"),
        }
    }
    #[test]
    fn window_resize_below_the_quantization_step_is_not_a_resize() {
        // LogicalSize::eq quantizes to 1/1000, so a sub-milli-pixel change is
        // *not* a size change and must not spam WindowResize every frame.
        let previous = state();
        let mut current = state();
        current.size.dimensions = LogicalSize::new(640.0004, 480.0);
        assert!(detect_window_state_events(&current, &previous, ts(0)).is_empty());
        current.size.dimensions = LogicalSize::new(640.5, 480.0);
        assert_eq!(
            count(
                &detect_window_state_events(&current, &previous, ts(0)),
                EventType::WindowResize
            ),
            1
        );
    }
    #[test]
    fn window_resize_nan_dimensions_are_stable_and_do_not_panic() {
        let mut nan = state();
        nan.size.dimensions = LogicalSize::new(f32::NAN, f32::NAN);
        // quantize() maps NaN to a single sentinel, so NaN == NaN: a window
        // stuck at NaN must not emit a resize on every single frame.
        assert!(
            detect_window_state_events(&nan, &nan.clone(), ts(0)).is_empty(),
            "NaN size compared against itself must not fire a resize"
        );
        // ...but a real -> NaN transition still reports (and forwards the NaN).
        let events = detect_window_state_events(&nan, &state(), ts(0));
        let e = only(&events, EventType::WindowResize);
        match e.data {
            EventData::Window(w) => {
                assert!(w.size.expect("size").size.width.is_nan());
            }
            other => panic!("expected Window data, got {other:?}"),
        }
    }
    #[test]
    fn window_resize_infinite_dimensions_do_not_panic() {
        let mut current = state();
        current.size.dimensions = LogicalSize::new(f32::INFINITY, f32::NEG_INFINITY);
        let events = detect_window_state_events(&current, &state(), ts(0));
        assert_eq!(count(&events, EventType::WindowResize), 1);
    }
    #[test]
    fn window_move_saturates_extreme_i32_coordinates_into_f32() {
        let mut current = state();
        current.position = WindowPosition::Initialized(PhysicalPositionI32 {
            x: i32::MIN,
            y: i32::MAX,
        });
        let events = detect_window_state_events(&current, &state(), ts(0));
        let e = only(&events, EventType::WindowMove);
        match e.data {
            EventData::Window(w) => {
                let pos = w.position.expect("move carries a position");
                assert_eq!(pos.x, i32::MIN as f32);
                // i32::MAX is not representable in f32: it rounds *up* to 2^31.
                assert_eq!(pos.y, 2_147_483_648.0_f32);
            }
            other => panic!("expected Window data, got {other:?}"),
        }
    }
    #[test]
    fn window_move_to_uninitialized_emits_nothing() {
        let previous = {
            let mut s = state();
            s.position = WindowPosition::Initialized(PhysicalPositionI32 { x: 5, y: 5 });
            s
        };
        // current.position == Uninitialized: state *changed*, but there is no
        // position to report, so no event.
        let events = detect_window_state_events(&state(), &previous, ts(0));
        assert_eq!(count(&events, EventType::WindowMove), 0);
    }
    #[test]
    fn window_move_fires_for_relative_child_window_positions() {
        let mut current = state();
        current.position =
            WindowPosition::RelativeToParentWindow(PhysicalPositionI32 { x: -3, y: 9 });
        let events = detect_window_state_events(&current, &state(), ts(0));
        let e = only(&events, EventType::WindowMove);
        match e.data {
            EventData::Window(w) => {
                let pos = w.position.expect("position");
                assert_eq!(pos.x, -3.0);
                assert_eq!(pos.y, 9.0);
            }
            other => panic!("expected Window data, got {other:?}"),
        }
    }
    #[test]
    fn window_theme_change_emits_exactly_one_theme_event() {
        let mut current = state();
        current.theme = WindowTheme::DarkMode;
        let events = detect_window_state_events(&current, &state(), ts(0));
        assert_eq!(events.len(), 1);
        assert_eq!(events[0].event_type, EventType::ThemeChange);
        assert_eq!(events[0].target, root());
    }
    #[test]
    fn window_mouse_enter_leave_transitions_are_exclusive() {
        let inside = cursor_at(1.0, 1.0);
        let outside = {
            let mut s = state();
            s.mouse_state.cursor_position =
                CursorPosition::OutOfWindow(LogicalPosition::new(1.0, 1.0));
            s
        };
        let uninit = state();
        let enter = detect_window_state_events(&inside, &uninit, ts(0));
        assert_eq!(count(&enter, EventType::MouseEnter), 1);
        assert_eq!(count(&enter, EventType::MouseLeave), 0);
        let leave = detect_window_state_events(&outside, &inside, ts(0));
        assert_eq!(count(&leave, EventType::MouseLeave), 1);
        assert_eq!(count(&leave, EventType::MouseEnter), 0);
        // OutOfWindow -> Uninitialized: neither is "in window", so nothing fires.
        let neither = detect_window_state_events(&uninit, &outside, ts(0));
        assert!(neither.is_empty());
    }
    #[test]
    fn window_state_events_all_target_the_root_node() {
        let mut current = cursor_at(10.0, 10.0);
        current.size.dimensions = LogicalSize::new(800.0, 600.0);
        current.position = WindowPosition::Initialized(PhysicalPositionI32 { x: 1, y: 2 });
        current.theme = WindowTheme::DarkMode;
        let events = detect_window_state_events(&current, &state(), ts(7));
        assert_eq!(events.len(), 4, "resize + move + theme + mouse-enter");
        assert!(events.iter().all(|e| e.target == root()));
        assert!(events.iter().all(|e| e.timestamp == ts(7)));
        assert!(events.iter().all(|e| e.source == EventSource::User));
    }
    #[test]
    fn min_dimensions_change_resizes_in_one_detector_but_not_the_other() {
        // detect_window_state_events compares the whole WindowSize (incl. the
        // min/max constraints), determine_all_events compares only `.dimensions`.
        // Documenting the divergence: a min-size-only change resizes in one and
        // not in the other.
        let mut current = state();
        current.size.min_dimensions = Some(LogicalSize::new(100.0, 100.0)).into();
        let differ = detect_window_state_events(&current, &state(), ts(0));
        assert_eq!(count(&differ, EventType::WindowResize), 1);
        let all = run_plain(&current, &state());
        assert_eq!(
            count(&all, EventType::WindowResize),
            0,
            "determine_all_events only diffs `size.dimensions`"
        );
    }
    // ==================================================================
    // determine_events_from_managers
    // ==================================================================
    #[test]
    fn from_managers_no_change_no_providers_is_empty() {
        let s = state();
        let providers: Vec<&dyn EventProvider> = Vec::new();
        assert!(determine_events_from_managers(&s, &s, &providers, ts(0)).is_empty());
    }
    #[test]
    fn from_managers_collapses_a_flood_of_identical_events() {
        let s = state();
        let target = node(0, 4);
        let provider = StaticProvider((0..5_000).map(|i| ev(EventType::Scroll, target, i)).collect());
        let providers: Vec<&dyn EventProvider> = vec![&provider];
        let events = determine_events_from_managers(&s, &s, &providers, ts(0));
        assert_eq!(events.len(), 1, "same (target, type) must coalesce");
        assert_eq!(events[0].event_type, EventType::Scroll);
    }
    #[test]
    fn from_managers_dedup_keeps_the_latest_timestamp() {
        let s = state();
        let target = node(0, 4);
        let provider = StaticProvider(vec![
            ev(EventType::Scroll, target, 1),
            ev(EventType::Scroll, target, 9),
            ev(EventType::Scroll, target, 5),
        ]);
        let providers: Vec<&dyn EventProvider> = vec![&provider];
        let events = determine_events_from_managers(&s, &s, &providers, ts(0));
        assert_eq!(events.len(), 1);
        assert_eq!(events[0].timestamp, ts(9));
    }
    #[test]
    fn from_managers_dedup_survives_u64_max_timestamps() {
        let s = state();
        let target = node(0, 4);
        let provider = StaticProvider(vec![
            ev(EventType::Scroll, target, u64::MAX),
            ev(EventType::Scroll, target, 0),
        ]);
        let providers: Vec<&dyn EventProvider> = vec![&provider];
        let events = determine_events_from_managers(&s, &s, &providers, ts(0));
        assert_eq!(events.len(), 1);
        assert_eq!(events[0].timestamp, ts(u64::MAX));
    }
    #[test]
    fn from_managers_provider_event_dedups_against_the_window_event() {
        let mut current = state();
        current.theme = WindowTheme::DarkMode;
        // The provider claims the exact same (root, ThemeChange) slot.
        let provider = StaticProvider(vec![ev(EventType::ThemeChange, root(), 3)]);
        let providers: Vec<&dyn EventProvider> = vec![&provider];
        let events = determine_events_from_managers(&current, &state(), &providers, ts(0));
        assert_eq!(events.len(), 1);
        assert_eq!(events[0].event_type, EventType::ThemeChange);
        assert_eq!(events[0].timestamp, ts(3), "the later timestamp wins");
    }
    /// PORTED 2026-08-20 from the deleted `layout/tests/event_determination.rs`
    /// (`test_determine_events_deduplicates`). That file was gated behind
    /// `DISABLED_event_tests`, a feature no job enabled, and it could not have
    /// compiled anyway — it glob-imported `azul_layout::event_determination::*`
    /// for `EventProvider`/`SyntheticEvent`/`EventType`, which live in
    /// `azul_core::events`. Its scenario lives here instead, where it runs in
    /// `cargo test --lib`: TWO DISTINCT provider objects claiming the same
    /// (target, event_type) slot must collapse to one event. The sibling
    /// `from_managers_dedup_keeps_the_latest_timestamp` covers one provider
    /// emitting duplicates; this covers the across-providers case.
    #[test]
    fn two_distinct_providers_colliding_on_one_slot_dedup() {
        let s = state();
        let a = StaticProvider(vec![ev(EventType::Input, root(), 0)]);
        let b = StaticProvider(vec![ev(EventType::Input, root(), 0)]);
        let providers: Vec<&dyn EventProvider> = vec![&a, &b];
        let events = determine_events_from_managers(&s, &s, &providers, ts(0));
        assert_eq!(events.len(), 1, "two providers, one slot: {events:?}");
        assert_eq!(events[0].event_type, EventType::Input);
    }
    #[test]
    fn from_managers_forwards_its_timestamp_to_every_provider() {
        let s = state();
        let provider = EchoProvider(node(0, 2));
        let providers: Vec<&dyn EventProvider> = vec![&provider];
        let events = determine_events_from_managers(&s, &s, &providers, ts(4242));
        assert_eq!(events.len(), 1);
        assert_eq!(events[0].timestamp, ts(4242));
    }
    #[test]
    fn from_managers_many_providers_with_distinct_targets_all_survive() {
        let s = state();
        let owned: Vec<StaticProvider> = (0..256_usize)
            .map(|i| StaticProvider(vec![ev(EventType::Scroll, node(i % 4, i), 0)]))
            .collect();
        let providers: Vec<&dyn EventProvider> =
            owned.iter().map(|p| p as &dyn EventProvider).collect();
        let events = determine_events_from_managers(&s, &s, &providers, ts(0));
        assert_eq!(events.len(), 256);
    }
    #[test]
    fn from_managers_empty_provider_lists_are_harmless() {
        let s = state();
        let empty = StaticProvider(Vec::new());
        let providers: Vec<&dyn EventProvider> = vec![&empty, &empty, &empty];
        assert!(determine_events_from_managers(&s, &s, &providers, ts(0)).is_empty());
    }
    // ==================================================================
    // determine_all_events - mouse buttons / bitmask
    // ==================================================================
    #[test]
    fn all_buttons_pressed_at_once_reports_a_full_bitmask() {
        let mut current = cursor_at(10.0, 10.0);
        current.mouse_state.left_down = true;
        current.mouse_state.right_down = true;
        current.mouse_state.middle_down = true;
        let events = run_plain(&current, &cursor_at(10.0, 10.0));
        // NOTE: the three per-button MouseDowns share (target, MouseDown), so
        // deduplication collapses them into a single event - the first (Left).
        let down = only(&events, EventType::MouseDown);
        match down.data {
            EventData::Mouse(m) => {
                assert_eq!(m.buttons, 0b0000_0111, "left|right|middle bitmask");
                assert_eq!(m.button, MouseButton::Left);
                assert_eq!(m.position, LogicalPosition::new(10.0, 10.0));
            }
            other => panic!("expected Mouse data, got {other:?}"),
        }
    }
    #[test]
    fn releasing_every_button_clears_the_bitmask() {
        let mut previous = cursor_at(10.0, 10.0);
        previous.mouse_state.left_down = true;
        previous.mouse_state.right_down = true;
        previous.mouse_state.middle_down = true;
        let events = run_plain(&cursor_at(10.0, 10.0), &previous);
        let up = only(&events, EventType::MouseUp);
        match up.data {
            EventData::Mouse(m) => assert_eq!(m.buttons, 0),
            other => panic!("expected Mouse data, got {other:?}"),
        }
    }
    #[test]
    fn mouse_events_carry_the_live_modifier_state() {
        let mut current = cursor_at(0.0, 0.0);
        current.keyboard_state.pressed_virtual_keycodes = VirtualKeyCodeVec::from_vec(vec![
            VirtualKeyCode::LShift,
            VirtualKeyCode::RControl,
            VirtualKeyCode::LAlt,
            VirtualKeyCode::LWin,
        ]);
        current.mouse_state.left_down = true;
        let events = run_plain(&current, &cursor_at(0.0, 0.0));
        let down = only(&events, EventType::MouseDown);
        match down.data {
            EventData::Mouse(m) => {
                assert!(m.modifiers.shift && m.modifiers.ctrl && m.modifiers.alt && m.modifiers.meta);
            }
            other => panic!("expected Mouse data, got {other:?}"),
        }
    }
    #[test]
    fn mouse_position_falls_back_to_origin_when_the_cursor_is_unknown() {
        // cursor_position is Uninitialized -> get_position() is None -> (0,0).
        let mut current = state();
        current.mouse_state.left_down = true;
        let events = run_plain(&current, &state());
        let down = only(&events, EventType::MouseDown);
        match down.data {
            EventData::Mouse(m) => assert_eq!(m.position, LogicalPosition::new(0.0, 0.0)),
            other => panic!("expected Mouse data, got {other:?}"),
        }
    }
    // ==================================================================
    // determine_all_events - wheel delta (numeric edges)
    // ==================================================================
    #[test]
    fn wheel_delta_none_emits_no_scroll() {
        let s = state();
        assert_eq!(
            count(&run(&s, &s, &HoverManager::new(), None, None), EventType::Scroll),
            0
        );
    }
    #[test]
    fn wheel_delta_zero_emits_no_scroll() {
        let s = state();
        let events = run(
            &s,
            &s,
            &HoverManager::new(),
            None,
            Some(LogicalPosition::new(0.0, 0.0)),
        );
        assert_eq!(count(&events, EventType::Scroll), 0);
    }
    #[test]
    fn wheel_delta_negative_zero_emits_no_scroll() {
        // IEEE-754: -0.0 == 0.0, so a signed-zero delta must stay silent.
        let s = state();
        let events = run(
            &s,
            &s,
            &HoverManager::new(),
            None,
            Some(LogicalPosition::new(-0.0, -0.0)),
        );
        assert_eq!(count(&events, EventType::Scroll), 0);
    }
    #[test]
    fn wheel_delta_nan_still_emits_a_scroll_and_forwards_the_nan() {
        // NaN != 0.0 is true, so a NaN delta *does* fire. Assert it is passed
        // through untouched rather than silently turning into 0.
        let s = state();
        let events = run(
            &s,
            &s,
            &HoverManager::new(),
            None,
            Some(LogicalPosition::new(f32::NAN, f32::NAN)),
        );
        let scroll = only(&events, EventType::Scroll);
        match scroll.data {
            EventData::Scroll(sd) => {
                assert!(sd.delta.x.is_nan() && sd.delta.y.is_nan());
                assert_eq!(sd.delta_mode, ScrollDeltaMode::Pixel);
            }
            other => panic!("expected Scroll data, got {other:?}"),
        }
    }
    #[test]
    fn wheel_delta_infinite_is_forwarded_without_saturation() {
        let s = state();
        let events = run(
            &s,
            &s,
            &HoverManager::new(),
            None,
            Some(LogicalPosition::new(f32::INFINITY, f32::NEG_INFINITY)),
        );
        let scroll = only(&events, EventType::Scroll);
        match scroll.data {
            EventData::Scroll(sd) => {
                assert_eq!(sd.delta.x, f32::INFINITY);
                assert_eq!(sd.delta.y, f32::NEG_INFINITY);
            }
            other => panic!("expected Scroll data, got {other:?}"),
        }
    }
    #[test]
    fn wheel_scroll_targets_the_hovered_node_not_the_root() {
        let s = state();
        let hm = hover_with(hits(&[(2, 6)]), hits(&[(2, 6)]));
        let events = run(
            &s,
            &s,
            &hm,
            None,
            Some(LogicalPosition::new(0.0, -120.0)),
        );
        let scroll = only(&events, EventType::Scroll);
        assert_eq!(scroll.target, node(2, 6));
    }
    // ==================================================================
    // determine_all_events - cursor movement + f32 quantization
    // ==================================================================
    #[test]
    fn mouseover_ignores_sub_quantum_cursor_jitter() {
        // LogicalPosition::eq quantizes to 1/1000 px: a 0.4 micro-pixel move is
        // no move at all, and must not wake the whole event pipeline.
        let events = run_plain(&cursor_at(100.0004, 0.0), &cursor_at(100.0, 0.0));
        assert_eq!(count(&events, EventType::MouseOver), 0);
        let events = run_plain(&cursor_at(100.5, 0.0), &cursor_at(100.0, 0.0));
        assert_eq!(count(&events, EventType::MouseOver), 1);
    }
    #[test]
    fn mouseover_treats_saturating_far_coordinates_as_unchanged() {
        // quantize() saturates on f32 -> i64: 1e30 and f32::MAX both land on
        // i64::MAX, so this "move" is invisible to the comparison. Deterministic
        // and panic-free is what matters here.
        let events = run_plain(&cursor_at(1e30, 0.0), &cursor_at(f32::MAX, 0.0));
        assert!(events.is_empty(), "got {events:?}");
    }
    #[test]
    fn mouseover_nan_cursor_positions_do_not_panic_or_thrash() {
        let events = run_plain(&cursor_at(f32::NAN, f32::NAN), &cursor_at(f32::NAN, f32::NAN));
        assert_eq!(
            count(&events, EventType::MouseOver),
            0,
            "a NaN cursor stuck in place must not fire MouseOver every frame"
        );
        // NaN -> real coordinate is a genuine move.
        let events = run_plain(&cursor_at(5.0, 5.0), &cursor_at(f32::NAN, f32::NAN));
        assert_eq!(count(&events, EventType::MouseOver), 1);
    }
    #[test]
    fn mouseover_never_fires_while_the_cursor_is_outside_the_window() {
        let mut current = state();
        current.mouse_state.cursor_position =
            CursorPosition::OutOfWindow(LogicalPosition::new(9.0, 9.0));
        let events = run_plain(&current, &state());
        assert_eq!(count(&events, EventType::MouseOver), 0);
    }
    #[test]
    fn entering_the_window_emits_mouse_enter_at_the_root() {
        let events = run_plain(&cursor_at(3.0, 3.0), &state());
        let enter = only(&events, EventType::MouseEnter);
        assert_eq!(enter.target, root());
        // Uninitialized -> InWindow is also a position change.
        assert_eq!(count(&events, EventType::MouseOver), 1);
    }
    #[test]
    fn leaving_the_window_emits_mouse_leave_at_the_root() {
        let mut current = state();
        current.mouse_state.cursor_position =
            CursorPosition::OutOfWindow(LogicalPosition::new(3.0, 3.0));
        let events = run_plain(&current, &cursor_at(3.0, 3.0));
        let leave = only(&events, EventType::MouseLeave);
        assert_eq!(leave.target, root());
    }
    // ==================================================================
    // determine_all_events - per-node hover chain (W3C enter/leave)
    // ==================================================================
    #[test]
    fn hover_chain_diff_emits_leave_for_lost_and_enter_for_gained_nodes() {
        let s = state();
        let hm = hover_with(hits(&[(0, 1), (0, 3)]), hits(&[(0, 1), (0, 5)]));
        let events = run(&s, &s, &hm, None, None);
        let leave = only(&events, EventType::MouseLeave);
        assert_eq!(leave.target, node(0, 3));
        let enter = only(&events, EventType::MouseEnter);
        assert_eq!(enter.target, node(0, 5));
        // Node 1 stayed hovered -> no event for it.
        assert_eq!(events.len(), 2);
    }
    #[test]
    fn hover_chain_diff_spans_child_doms() {
        let s = state();
        let hm = hover_with(FullHitTest::empty(None), hits(&[(0, 1), (9, 2)]));
        let events = run(&s, &s, &hm, None, None);
        assert_eq!(count(&events, EventType::MouseEnter), 2);
        assert!(events.iter().any(|e| e.target == node(9, 2)));
        assert!(events.iter().any(|e| e.target == node(0, 1)));
    }
    #[test]
    fn hover_chain_enter_targets_round_trip_through_the_1_based_encoding() {
        // NodeHierarchyItemId stores `index + 1`; make sure the edge indices
        // survive the encode/decode round-trip in the emitted target.
        for id in [0_usize, 1, 12_345, usize::MAX - 1] {
            let s = state();
            let hm = hover_with(FullHitTest::empty(None), hits(&[(0, id)]));
            let events = run(&s, &s, &hm, None, None);
            let enter = only(&events, EventType::MouseEnter);
            assert_eq!(
                enter.target.node.into_crate_internal(),
                Some(NodeId::new(id)),
                "target NodeId must decode back to {id}"
            );
        }
    }
    #[test]
    fn node_level_and_window_level_enter_collapse_at_the_root() {
        // Node 0 of DOM 0 *is* the root target, so its per-node MouseEnter and
        // the window-level MouseEnter share a dedup slot.
        let hm = hover_with(FullHitTest::empty(None), hits(&[(0, 0)]));
        let events = run(&cursor_at(1.0, 1.0), &state(), &hm, None, None);
        assert_eq!(count(&events, EventType::MouseEnter), 1);
    }
    #[test]
    fn an_unchanged_hover_chain_emits_no_enter_or_leave() {
        let s = state();
        let hm = hover_with(hits(&[(0, 1), (4, 2)]), hits(&[(0, 1), (4, 2)]));
        let events = run(&s, &s, &hm, None, None);
        assert!(events.is_empty(), "got {events:?}");
    }
    // ==================================================================
    // determine_all_events - click synthesis
    // ==================================================================
    #[test]
    fn click_is_synthesized_when_release_lands_on_the_press_node() {
        let mut previous = cursor_at(20.0, 20.0);
        previous.mouse_state.left_down = true;
        let hm = hover_with(hits(&[(0, 8)]), hits(&[(0, 8)]));
        let events = run(&cursor_at(20.0, 20.0), &previous, &hm, None, None);
        let click = only(&events, EventType::Click);
        assert_eq!(click.target, node(0, 8));
        assert_eq!(count(&events, EventType::MouseUp), 1);
    }
    #[test]
    fn no_click_when_the_hover_node_changed_between_press_and_release() {
        let mut previous = cursor_at(20.0, 20.0);
        previous.mouse_state.left_down = true;
        let hm = hover_with(hits(&[(0, 3)]), hits(&[(0, 5)]));
        let events = run(&cursor_at(20.0, 20.0), &previous, &hm, None, None);
        assert_eq!(count(&events, EventType::Click), 0);
    }
    #[test]
    fn no_click_when_nothing_is_hovered() {
        let mut previous = cursor_at(20.0, 20.0);
        previous.mouse_state.left_down = true;
        // Empty hover manager: prev == curr == None, but None is not a node.
        let events = run_plain(&cursor_at(20.0, 20.0), &previous);
        assert_eq!(count(&events, EventType::Click), 0);
        assert_eq!(count(&events, EventType::MouseUp), 1);
    }
    // ==================================================================
    // determine_all_events - keyboard
    // ==================================================================
    #[test]
    fn keyboard_events_target_the_focused_node() {
        let mut current = state();
        current.keyboard_state.current_virtual_keycode =
            OptionVirtualKeyCode::Some(VirtualKeyCode::A);
        let mut focus = FocusManager::new();
        focus.set_focused_node(Some(node(3, 11)));
        let hover = HoverManager::new();
        let file_drop = FileDropManager::new();
        let providers: Vec<&dyn EventProvider> = Vec::new();
        let events = determine_all_events(
            &current,
            &state(),
            &hover,
            &focus,
            &file_drop,
            None,
            &providers,
            None,
            ts(0),
        );
        let down = only(&events, EventType::KeyDown);
        assert_eq!(down.target, node(3, 11));
    }
    #[test]
    fn keyup_reports_the_previous_keycode_not_zero() {
        let mut previous = state();
        previous.keyboard_state.current_virtual_keycode =
            OptionVirtualKeyCode::Some(VirtualKeyCode::F12);
        let events = run_plain(&state(), &previous);
        let up = only(&events, EventType::KeyUp);
        match up.data {
            EventData::Keyboard(k) => {
                assert_eq!(k.key_code, VirtualKeyCode::F12 as u32);
                assert!(!k.repeat);
                assert!(k.char_code.is_none());
            }
            other => panic!("expected Keyboard data, got {other:?}"),
        }
    }
    #[test]
    fn no_keyboard_events_when_no_key_is_involved() {
        let s = state();
        let events = run_plain(&s, &s);
        assert_eq!(count(&events, EventType::KeyDown), 0);
        assert_eq!(count(&events, EventType::KeyUp), 0);
    }
    #[test]
    fn swapping_keys_in_one_frame_emits_keydown_but_no_keyup() {
        // Documented behaviour: KeyUp only fires on Some -> None. A direct
        // A -> B swap therefore emits KeyDown(B) and *drops* the KeyUp(A).
        let mut current = state();
        current.keyboard_state.current_virtual_keycode =
            OptionVirtualKeyCode::Some(VirtualKeyCode::B);
        let mut previous = state();
        previous.keyboard_state.current_virtual_keycode =
            OptionVirtualKeyCode::Some(VirtualKeyCode::A);
        let events = run_plain(&current, &previous);
        assert_eq!(count(&events, EventType::KeyDown), 1);
        assert_eq!(count(&events, EventType::KeyUp), 0);
    }
    // ==================================================================
    // determine_all_events - window state
    // ==================================================================
    #[test]
    fn dpi_change_emits_a_dpi_event_at_the_u32_extremes() {
        let mut current = state();
        current.size.dpi = u32::MAX;
        let mut previous = state();
        previous.size.dpi = 0;
        let events = run_plain(&current, &previous);
        assert_eq!(count(&events, EventType::WindowDpiChanged), 1);
        assert_eq!(count(&events, EventType::WindowResize), 0);
    }
    #[test]
    fn monitor_change_needs_a_known_monitor_on_both_sides() {
        let mut current = state();
        current.monitor_id = OptionU32::Some(2);
        // None -> Some(2): the window learned its monitor, that is not a move.
        let events = run_plain(&current, &state());
        assert_eq!(count(&events, EventType::WindowMonitorChanged), 0);
        let mut previous = state();
        previous.monitor_id = OptionU32::Some(1);
        let events = run_plain(&current, &previous);
        assert_eq!(count(&events, EventType::WindowMonitorChanged), 1);
        // Some(u32::MAX) -> None: still not a monitor change.
        let mut max = state();
        max.monitor_id = OptionU32::Some(u32::MAX);
        let events = run_plain(&state(), &max);
        assert_eq!(count(&events, EventType::WindowMonitorChanged), 0);
    }
    /// A maximize / restore / minimize / fullscreen transition must reach the
    /// app. Nothing derived an event from `flags.frame` before, so no callback
    /// could observe one — while two backends carried comments claiming they
    /// could.
    #[test]
    fn a_frame_transition_emits_exactly_one_event_per_change() {
        use azul_core::window::WindowFrame;
        let normal = state();
        let mut maximized = state();
        maximized.flags.frame = WindowFrame::Maximized;
        let events = run_plain(&maximized, &normal);
        assert_eq!(count(&events, EventType::WindowFrameChanged), 1);
        // Still maximized on the next frame -> no repeat.
        let events = run_plain(&maximized, &maximized.clone());
        assert_eq!(count(&events, EventType::WindowFrameChanged), 0);
        // And back again is its own transition.
        let events = run_plain(&normal, &maximized);
        assert_eq!(count(&events, EventType::WindowFrameChanged), 1);
        // Every distinct frame reachable from Normal is a transition.
        for frame in [
            WindowFrame::Minimized,
            WindowFrame::Maximized,
            WindowFrame::Fullscreen,
        ] {
            let mut to = state();
            to.flags.frame = frame;
            assert_eq!(
                count(&run_plain(&to, &normal), EventType::WindowFrameChanged),
                1,
                "Normal -> {frame:?} must be one event"
            );
        }
    }
    #[test]
    fn close_request_emits_window_close_once() {
        let mut current = state();
        current.flags.close_requested = true;
        let events = run_plain(&current, &state());
        assert_eq!(count(&events, EventType::WindowClose), 1);
        // Still requested on the next frame -> no repeat.
        let events = run_plain(&current, &current.clone());
        assert_eq!(count(&events, EventType::WindowClose), 0);
    }
    #[test]
    fn window_focus_transitions_emit_focus_in_and_out() {
        let unfocused = {
            let mut s = state();
            s.window_focused = false;
            s
        };
        let events = run_plain(&unfocused, &state());
        assert_eq!(count(&events, EventType::WindowFocusOut), 1);
        let events = run_plain(&state(), &unfocused);
        assert_eq!(count(&events, EventType::WindowFocusIn), 1);
    }
    #[test]
    fn determine_all_events_ignores_relative_child_window_moves() {
        // Divergence from detect_window_state_events, which *does* report it.
        let mut current = state();
        current.position =
            WindowPosition::RelativeToParentWindow(PhysicalPositionI32 { x: 4, y: 4 });
        let events = run_plain(&current, &state());
        assert_eq!(count(&events, EventType::WindowMove), 0);
    }
    #[test]
    fn determine_all_events_accepts_a_u64_max_timestamp() {
        let mut current = state();
        current.theme = WindowTheme::DarkMode;
        let hover = HoverManager::new();
        let focus = FocusManager::new();
        let file_drop = FileDropManager::new();
        let providers: Vec<&dyn EventProvider> = Vec::new();
        let events = determine_all_events(
            &current,
            &state(),
            &hover,
            &focus,
            &file_drop,
            None,
            &providers,
            None,
            ts(u64::MAX),
        );
        let theme = only(&events, EventType::ThemeChange);
        assert_eq!(theme.timestamp, ts(u64::MAX));
    }
    // ==================================================================
    // determine_all_events - file drop
    // ==================================================================
    fn run_with_file_drop(file_drop: &FileDropManager) -> Vec<SyntheticEvent> {
        let s = state();
        let hover = HoverManager::new();
        let focus = FocusManager::new();
        let providers: Vec<&dyn EventProvider> = Vec::new();
        determine_all_events(
            &s,
            &s,
            &hover,
            &focus,
            file_drop,
            None,
            &providers,
            None,
            ts(0),
        )
    }
    #[test]
    fn hovering_a_file_emits_file_hover_and_suppresses_the_cancel() {
        let mut fd = FileDropManager::new();
        fd.set_hovered_file(Some(AzString::from(String::from("/tmp/a.png"))));
        fd.set_hovered_file(None); // latches hover_cancelled
        fd.set_hovered_file(Some(AzString::from(String::from("/tmp/b.png"))));
        assert!(fd.hover_was_cancelled(), "flag is still latched");
        let events = run_with_file_drop(&fd);
        assert_eq!(count(&events, EventType::FileHover), 1);
        assert_eq!(
            count(&events, EventType::FileHoverCancel),
            0,
            "an active hover must win over the stale cancel flag"
        );
    }
    #[test]
    fn a_cancelled_hover_emits_file_hover_cancel() {
        let mut fd = FileDropManager::new();
        fd.set_hovered_file(Some(AzString::from(String::from("/tmp/a.png"))));
        fd.set_hovered_file(None);
        let events = run_with_file_drop(&fd);
        assert_eq!(count(&events, EventType::FileHoverCancel), 1);
        assert_eq!(count(&events, EventType::FileHover), 0);
    }
    #[test]
    fn an_empty_path_still_counts_as_a_hovered_file() {
        let mut fd = FileDropManager::new();
        fd.set_hovered_file(Some(AzString::from(String::new())));
        let events = run_with_file_drop(&fd);
        assert_eq!(count(&events, EventType::FileHover), 1);
    }
    #[test]
    fn pathological_file_paths_do_not_panic() {
        // 256 KiB of text, an interior NUL, an RTL override and astral-plane
        // codepoints - the event pass must not touch the bytes at all.
        let mut path = "\u{202e}🎉\u{0}".repeat(4);
        path.push_str(&"ß".repeat(256 * 1024));
        let mut fd = FileDropManager::new();
        fd.set_dropped_file(Some(AzString::from(path)));
        fd.set_hovered_file(Some(AzString::from(String::from("\u{feff}"))));
        let events = run_with_file_drop(&fd);
        assert_eq!(count(&events, EventType::FileDrop), 1);
        assert_eq!(count(&events, EventType::FileHover), 1);
    }
    #[test]
    fn a_multi_file_drop_still_emits_exactly_one_file_drop_event() {
        let mut fd = FileDropManager::new();
        fd.set_dropped_files(
            (0..1000)
                .map(|i| AzString::from(format!("/tmp/{i}.bin")))
                .collect(),
        );
        let events = run_with_file_drop(&fd);
        assert_eq!(count(&events, EventType::FileDrop), 1);
    }
    #[test]
    fn file_events_target_the_hovered_node() {
        let s = state();
        let hover = hover_with(hits(&[(1, 4)]), hits(&[(1, 4)]));
        let focus = FocusManager::new();
        let mut fd = FileDropManager::new();
        fd.set_dropped_file(Some(AzString::from(String::from("/tmp/x"))));
        let providers: Vec<&dyn EventProvider> = Vec::new();
        let events = determine_all_events(
            &s, &s, &hover, &focus, &fd, None, &providers, None, ts(0),
        );
        let drop = only(&events, EventType::FileDrop);
        assert_eq!(drop.target, node(1, 4));
    }
    // ==================================================================
    // determine_all_events - gestures (native injection)
    // ==================================================================
    fn pinch(scale: f32) -> GestureAndDragManager {
        let mut g = GestureAndDragManager::new();
        g.inject_native_gesture(NativeGestureEvent::Pinch(DetectedPinch {
            scale,
            center: LogicalPosition::new(0.0, 0.0),
            initial_distance: 0.0,
            current_distance: 0.0,
            duration_ms: 0,
        }));
        g
    }
    fn rotation(angle_radians: f32) -> GestureAndDragManager {
        let mut g = GestureAndDragManager::new();
        g.inject_native_gesture(NativeGestureEvent::Rotation(DetectedRotation {
            angle_radians,
            center: LogicalPosition::new(0.0, 0.0),
            duration_ms: 0,
        }));
        g
    }
    #[test]
    fn pinch_scale_below_one_is_pinch_in_at_and_above_one_is_pinch_out() {
        let s = state();
        for (scale, expected) in [
            (0.0_f32, EventType::PinchIn),
            (0.999_999, EventType::PinchIn),
            (f32::NEG_INFINITY, EventType::PinchIn),
            (1.0, EventType::PinchOut),
            (1.000_001, EventType::PinchOut),
            (f32::INFINITY, EventType::PinchOut),
        ] {
            let g = pinch(scale);
            let events = run(&s, &s, &HoverManager::new(), Some(&g), None);
            assert_eq!(count(&events, expected), 1, "scale {scale} -> {expected:?}");
        }
    }
    #[test]
    fn pinch_scale_nan_is_deterministic_and_does_not_panic() {
        // `pinch.scale < 1.0` is false for NaN, so NaN classifies as PinchOut.
        // The point is that it is *deterministic* and never panics.
        let s = state();
        let g = pinch(f32::NAN);
        let events = run(&s, &s, &HoverManager::new(), Some(&g), None);
        assert_eq!(count(&events, EventType::PinchOut), 1);
        assert_eq!(count(&events, EventType::PinchIn), 0);
    }
    #[test]
    fn rotation_sign_selects_the_direction_including_signed_zero_and_nan() {
        let s = state();
        for (angle, expected) in [
            (0.1_f32, EventType::RotateClockwise),
            (f32::INFINITY, EventType::RotateClockwise),
            (-0.1, EventType::RotateCounterClockwise),
            (0.0, EventType::RotateCounterClockwise),
            (-0.0, EventType::RotateCounterClockwise),
            (f32::NAN, EventType::RotateCounterClockwise),
            (f32::NEG_INFINITY, EventType::RotateCounterClockwise),
        ] {
            let g = rotation(angle);
            let events = run(&s, &s, &HoverManager::new(), Some(&g), None);
            assert_eq!(count(&events, expected), 1, "angle {angle} -> {expected:?}");
        }
    }
    #[test]
    fn every_swipe_direction_maps_to_its_own_event_type() {
        let s = state();
        for (dir, expected) in [
            (GestureDirection::Left, EventType::SwipeLeft),
            (GestureDirection::Right, EventType::SwipeRight),
            (GestureDirection::Up, EventType::SwipeUp),
            (GestureDirection::Down, EventType::SwipeDown),
        ] {
            let mut g = GestureAndDragManager::new();
            g.inject_native_gesture(NativeGestureEvent::Swipe(dir));
            let events = run(&s, &s, &HoverManager::new(), Some(&g), None);
            assert_eq!(count(&events, expected), 1, "{dir:?} -> {expected:?}");
            assert_eq!(events.len(), 1);
        }
    }
    #[test]
    fn long_press_is_suppressed_once_the_callback_ran() {
        let s = state();
        for (invoked, expected) in [(false, 1_usize), (true, 0)] {
            let mut g = GestureAndDragManager::new();
            g.inject_native_gesture(NativeGestureEvent::LongPress(DetectedLongPress {
                position: LogicalPosition::new(1.0, 1.0),
                duration_ms: u64::MAX,
                callback_invoked: invoked,
                session_id: 0,
            }));
            let events = run(&s, &s, &HoverManager::new(), Some(&g), None);
            assert_eq!(count(&events, EventType::LongPress), expected);
        }
    }
    #[test]
    fn a_double_click_fires_exactly_one_event() {
        let s = state();
        let mut g = GestureAndDragManager::new();
        g.inject_native_gesture(NativeGestureEvent::DoubleClick);
        let events = run(&s, &s, &HoverManager::new(), Some(&g), None);
        assert_eq!(count(&events, EventType::DoubleClick), 1);
    }
    #[test]
    fn a_fresh_gesture_manager_produces_no_gesture_events() {
        let s = state();
        let g = GestureAndDragManager::new();
        let events = run(&s, &s, &HoverManager::new(), Some(&g), None);
        assert!(events.is_empty(), "got {events:?}");
    }
    #[test]
    fn gesture_events_target_the_hovered_node() {
        let s = state();
        let hm = hover_with(hits(&[(3, 12)]), hits(&[(3, 12)]));
        let g = pinch(0.5);
        let events = run(&s, &s, &hm, Some(&g), None);
        let e = only(&events, EventType::PinchIn);
        assert_eq!(e.target, node(3, 12));
    }
    // ==================================================================
    // determine_all_events - pen / stylus diffing
    // ==================================================================
    #[test]
    fn pen_events_require_the_pending_flag() {
        let s = state();
        let mut g = GestureAndDragManager::new();
        g.update_pen_state(LogicalPosition::new(1.0, 1.0), 0.5, (0.0, 0.0), true, false, false, 1);
        g.clear_pen_event_pending();
        let events = run(&s, &s, &HoverManager::new(), Some(&g), None);
        assert_eq!(count(&events, EventType::PenEnter), 0);
        assert_eq!(count(&events, EventType::PenDown), 0);
    }
    #[test]
    fn pen_entering_proximity_emits_pen_enter() {
        let s = state();
        let mut g = GestureAndDragManager::new();
        g.update_pen_state(LogicalPosition::new(1.0, 1.0), 0.0, (0.0, 0.0), false, false, false, 1);
        let events = run(&s, &s, &HoverManager::new(), Some(&g), None);
        assert_eq!(count(&events, EventType::PenEnter), 1);
        assert_eq!(count(&events, EventType::PenLeave), 0);
    }
    #[test]
    fn pen_leaving_proximity_emits_pen_leave() {
        let s = state();
        let mut g = GestureAndDragManager::new();
        g.update_pen_state(LogicalPosition::new(1.0, 1.0), 0.0, (0.0, 0.0), false, false, false, 1);
        g.clear_pen_state();
        let events = run(&s, &s, &HoverManager::new(), Some(&g), None);
        assert_eq!(count(&events, EventType::PenLeave), 1);
        assert_eq!(count(&events, EventType::PenEnter), 0);
    }
    #[test]
    fn pen_contact_transitions_emit_pen_down_then_pen_up() {
        let s = state();
        let mut g = GestureAndDragManager::new();
        g.update_pen_state(LogicalPosition::new(1.0, 1.0), 0.0, (0.0, 0.0), false, false, false, 1);
        g.update_pen_state(LogicalPosition::new(1.0, 1.0), 1.0, (0.0, 0.0), true, false, false, 1);
        let down = run(&s, &s, &HoverManager::new(), Some(&g), None);
        assert_eq!(count(&down, EventType::PenDown), 1);
        assert_eq!(count(&down, EventType::PenMove), 0, "position did not change");
        g.update_pen_state(LogicalPosition::new(1.0, 1.0), 0.0, (0.0, 0.0), false, false, false, 1);
        let up = run(&s, &s, &HoverManager::new(), Some(&g), None);
        assert_eq!(count(&up, EventType::PenUp), 1);
        assert_eq!(count(&up, EventType::PenDown), 0);
    }
    #[test]
    fn pen_movement_emits_pen_move() {
        let s = state();
        let mut g = GestureAndDragManager::new();
        g.update_pen_state(LogicalPosition::new(1.0, 1.0), 1.0, (0.0, 0.0), true, false, false, 1);
        g.update_pen_state(LogicalPosition::new(40.0, 90.0), 1.0, (0.0, 0.0), true, false, false, 1);
        let events = run(&s, &s, &HoverManager::new(), Some(&g), None);
        assert_eq!(count(&events, EventType::PenMove), 1);
    }
    #[test]
    fn a_pen_stuck_at_nan_does_not_emit_a_phantom_pen_move() {
        // LogicalPosition::eq quantizes NaN to a single sentinel, so NaN == NaN
        // and a stuck-at-NaN pen must stay quiet instead of firing every frame.
        let s = state();
        let mut g = GestureAndDragManager::new();
        let nan = LogicalPosition::new(f32::NAN, f32::NAN);
        g.update_pen_state(nan, 1.0, (f32::NAN, f32::NAN), true, false, false, 1);
        g.update_pen_state(nan, 1.0, (f32::NAN, f32::NAN), true, false, false, 1);
        let events = run(&s, &s, &HoverManager::new(), Some(&g), None);
        assert_eq!(count(&events, EventType::PenMove), 0);
        assert!(events.is_empty(), "got {events:?}");
    }
    #[test]
    fn a_pen_in_contact_emits_touch_move_when_the_cursor_moves() {
        let mut g = GestureAndDragManager::new();
        g.update_pen_state(LogicalPosition::new(1.0, 1.0), 1.0, (0.0, 0.0), true, false, false, 1);
        g.clear_pen_event_pending(); // isolate the TouchMove path from PenEnter
        let events = run(
            &cursor_at(50.0, 50.0),
            &cursor_at(10.0, 10.0),
            &HoverManager::new(),
            Some(&g),
            None,
        );
        assert_eq!(count(&events, EventType::TouchMove), 1);
    }
    // ==================================================================
    // determine_all_events - manager providers + dedup
    // ==================================================================
    #[test]
    fn provider_events_are_appended_and_deduplicated() {
        let s = state();
        let hover = HoverManager::new();
        let focus = FocusManager::new();
        let file_drop = FileDropManager::new();
        let target = node(0, 42);
        let provider = StaticProvider(vec![
            ev(EventType::Scroll, target, 2),
            ev(EventType::Scroll, target, 8),
            ev(EventType::KeyDown, target, 1),
        ]);
        let providers: Vec<&dyn EventProvider> = vec![&provider];
        let events = determine_all_events(
            &s,
            &s,
            &hover,
            &focus,
            &file_drop,
            None,
            &providers,
            None,
            ts(0),
        );
        assert_eq!(events.len(), 2);
        assert_eq!(only(&events, EventType::Scroll).timestamp, ts(8));
    }
    #[test]
    fn a_provider_can_collide_with_a_wheel_scroll_on_the_same_node() {
        let s = state();
        let hover = HoverManager::new();
        let focus = FocusManager::new();
        let file_drop = FileDropManager::new();
        // Same (root, Scroll) slot as the wheel-generated event.
        let provider = StaticProvider(vec![ev(EventType::Scroll, root(), u64::MAX)]);
        let providers: Vec<&dyn EventProvider> = vec![&provider];
        let events = determine_all_events(
            &s,
            &s,
            &hover,
            &focus,
            &file_drop,
            None,
            &providers,
            Some(LogicalPosition::new(0.0, -1.0)),
            ts(0),
        );
        assert_eq!(count(&events, EventType::Scroll), 1);
        assert_eq!(
            events[0].timestamp,
            ts(u64::MAX),
            "dedup keeps the later timestamp"
        );
    }
    #[test]
    fn a_completely_idle_frame_produces_nothing() {
        let s = state();
        let g = GestureAndDragManager::new();
        let hover = HoverManager::new();
        let focus = FocusManager::new();
        let file_drop = FileDropManager::new();
        let empty = StaticProvider(Vec::new());
        let providers: Vec<&dyn EventProvider> = vec![&empty];
        let events = determine_all_events(
            &s,
            &s,
            &hover,
            &focus,
            &file_drop,
            Some(&g),
            &providers,
            None,
            ts(0),
        );
        assert!(events.is_empty(), "got {events:?}");
    }
}