Skip to main content

azul_core/
events.rs

1//! Event and callback filtering module
2
3#[cfg(not(feature = "std"))]
4use alloc::string::{String, ToString};
5use alloc::{
6    boxed::Box,
7    collections::{btree_map::BTreeMap, btree_set::BTreeSet},
8    vec::Vec,
9};
10
11use azul_css::AzString;
12
13use crate::{
14    callbacks::Update,
15    dom::{DomId, DomNodeId, On},
16    geom::{LogicalPosition, LogicalRect},
17    hit_test::{FullHitTest, HitTestItem},
18    id::NodeId,
19    styled_dom::{ChangedCssProperty, NodeHierarchyItemId},
20    task::Instant,
21    OrderedMap,
22};
23
24/// Easing functions for smooth scroll animations
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum EasingFunction {
27    Linear,
28    EaseInOut,
29    EaseOut,
30    /// Critically-damped spring settle (ledger #28, per the animation
31    /// design doc): position eases like a spring released toward the
32    /// target — fast initial pull, asymptote-free exact landing. Evaluated
33    /// analytically from normalized time (same curve family the scroll
34    /// physics integrates numerically).
35    Spring,
36}
37
38pub type RestyleNodes = BTreeMap<NodeId, Vec<ChangedCssProperty>>;
39pub type RelayoutNodes = BTreeMap<NodeId, Vec<ChangedCssProperty>>;
40pub type RelayoutWords = BTreeMap<NodeId, AzString>;
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub struct FocusChange {
44    pub old: Option<DomNodeId>,
45    pub new: Option<DomNodeId>,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub struct CallbackToCall {
50    pub node_id: NodeId,
51    pub hit_test_item: Option<HitTestItem>,
52    pub event_filter: EventFilter,
53}
54
55impl CallbackToCall {
56    #[must_use]
57    pub const fn new(
58        node_id: NodeId,
59        hit_test_item: Option<HitTestItem>,
60        event_filter: EventFilter,
61    ) -> Self {
62        Self {
63            node_id,
64            hit_test_item,
65            event_filter,
66        }
67    }
68
69    /// Build a list of `CallbackToCall` entries for every node hit by the
70    /// given hit test under the given DOM, tagged with `event_filter`.
71    /// Returns an empty `Vec` when there is no hit test data for the DOM.
72    #[must_use]
73    pub fn from_hit_test(
74        hit_test: &FullHitTest,
75        dom_id: DomId,
76        event_filter: EventFilter,
77    ) -> Vec<Self> {
78        let Some(hit) = hit_test.hovered_nodes.get(&dom_id) else {
79            return Vec::new();
80        };
81        hit.regular_hit_test_nodes
82            .iter()
83            .map(|(node_id, item)| Self {
84                node_id: *node_id,
85                hit_test_item: Some(*item),
86                event_filter,
87            })
88            .collect()
89    }
90}
91
92#[derive(Debug, Copy, Clone, PartialEq, Eq)]
93#[must_use = "ProcessEventResult must be used to determine if relayout/repaint is needed"]
94pub enum ProcessEventResult {
95    DoNothing = 0,
96    ShouldReRenderCurrentWindow = 1,
97    ShouldUpdateDisplayListCurrentWindow = 2,
98    // GPU transforms changed: do another hit-test and recurse
99    // until nothing has changed anymore
100    UpdateHitTesterAndProcessAgain = 3,
101    // Restyle or runtime edit changed layout-affecting properties:
102    // re-run layout on the EXISTING StyledDom (no DOM rebuild).
103    ShouldIncrementalRelayout = 4,
104    // Full DOM rebuild via user's layout_callback()
105    ShouldRegenerateDomCurrentWindow = 5,
106    ShouldRegenerateDomAllWindows = 6,
107}
108
109impl ProcessEventResult {
110    #[must_use]
111    pub const fn order(&self) -> usize {
112        use self::ProcessEventResult::{
113            DoNothing, ShouldIncrementalRelayout, ShouldReRenderCurrentWindow,
114            ShouldRegenerateDomAllWindows, ShouldRegenerateDomCurrentWindow,
115            ShouldUpdateDisplayListCurrentWindow, UpdateHitTesterAndProcessAgain,
116        };
117        match self {
118            DoNothing => 0,
119            ShouldReRenderCurrentWindow => 1,
120            ShouldUpdateDisplayListCurrentWindow => 2,
121            UpdateHitTesterAndProcessAgain => 3,
122            ShouldIncrementalRelayout => 4,
123            ShouldRegenerateDomCurrentWindow => 5,
124            ShouldRegenerateDomAllWindows => 6,
125        }
126    }
127}
128
129impl PartialOrd for ProcessEventResult {
130    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
131        self.order().partial_cmp(&other.order())
132    }
133}
134
135impl Ord for ProcessEventResult {
136    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
137        self.order().cmp(&other.order())
138    }
139}
140
141impl ProcessEventResult {
142    pub fn max_self(self, other: Self) -> Self {
143        self.max(other)
144    }
145}
146
147/// Tracks the origin of an event for proper handling.
148///
149/// This allows the system to distinguish between user input, programmatic
150/// changes, and synthetic events generated by UI components.
151#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
152#[repr(C)]
153pub enum EventSource {
154    /// Direct user input (mouse, keyboard, touch, gamepad)
155    User,
156    /// API call (programmatic scroll, focus change, etc.)
157    Programmatic,
158    /// Generated from UI interaction (scrollbar drag, synthetic events)
159    Synthetic,
160    /// Generated from lifecycle hooks (mount, unmount, resize)
161    Lifecycle,
162}
163
164/// Event propagation phase (similar to DOM Level 2 Events).
165///
166/// Events can be intercepted at different phases:
167/// - **Capture**: Event travels from root down to target (rarely used)
168/// - **Target**: Event is at the target element
169/// - **Bubble**: Event travels from target back up to root (most common)
170#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
171#[repr(C)]
172#[derive(Default)]
173pub enum EventPhase {
174    /// Event travels from root down to target
175    Capture,
176    /// Event is at the target element
177    Target,
178    /// Event bubbles from target back up to root
179    #[default]
180    Bubble,
181}
182
183/// Mouse button identifier for mouse events.
184#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
185#[repr(C)]
186pub enum MouseButton {
187    Left,
188    Middle,
189    Right,
190    Other(u8),
191}
192
193/// Scroll delta mode (how scroll deltas should be interpreted).
194#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
195#[repr(C)]
196pub enum ScrollDeltaMode {
197    /// Delta is in pixels
198    Pixel,
199    /// Delta is in lines (e.g., 3 lines of text)
200    Line,
201    /// Delta is in pages
202    Page,
203}
204
205/// Scroll direction for conditional event filtering.
206#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
207#[repr(C)]
208pub enum ScrollDirection {
209    Up,
210    Down,
211    Left,
212    Right,
213}
214
215// ============================================================================
216// W3C CSSOM View Module - Scroll Into View Types
217// ============================================================================
218
219/// W3C-compliant scroll-into-view options
220///
221/// These options control how an element is scrolled into view, following
222/// the CSSOM View Module specification.
223#[repr(C)]
224#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
225pub struct ScrollIntoViewOptions {
226    /// Vertical alignment: start, center, end, nearest (default: nearest)
227    pub block: ScrollLogicalPosition,
228    /// Horizontal alignment: start, center, end, nearest (default: nearest)
229    /// Note: Named `inline_axis` to avoid conflict with C keyword `inline`
230    pub inline_axis: ScrollLogicalPosition,
231    /// Animation behavior: auto, instant, smooth (default: auto)
232    pub behavior: ScrollIntoViewBehavior,
233}
234
235impl ScrollIntoViewOptions {
236    /// Create options with "nearest" alignment for both axes
237    #[must_use]
238    pub const fn nearest() -> Self {
239        Self {
240            block: ScrollLogicalPosition::Nearest,
241            inline_axis: ScrollLogicalPosition::Nearest,
242            behavior: ScrollIntoViewBehavior::Auto,
243        }
244    }
245
246    /// Create options with "center" alignment for both axes
247    #[must_use]
248    pub const fn center() -> Self {
249        Self {
250            block: ScrollLogicalPosition::Center,
251            inline_axis: ScrollLogicalPosition::Center,
252            behavior: ScrollIntoViewBehavior::Auto,
253        }
254    }
255
256    /// Create options with "start" alignment for both axes
257    #[must_use]
258    pub const fn start() -> Self {
259        Self {
260            block: ScrollLogicalPosition::Start,
261            inline_axis: ScrollLogicalPosition::Start,
262            behavior: ScrollIntoViewBehavior::Auto,
263        }
264    }
265
266    /// Create options to align the end of the target with the end of the viewport
267    #[must_use]
268    pub const fn end() -> Self {
269        Self {
270            block: ScrollLogicalPosition::End,
271            inline_axis: ScrollLogicalPosition::End,
272            behavior: ScrollIntoViewBehavior::Auto,
273        }
274    }
275
276    /// Set instant scroll behavior
277    #[must_use]
278    pub const fn with_instant(mut self) -> Self {
279        self.behavior = ScrollIntoViewBehavior::Instant;
280        self
281    }
282
283    /// Set smooth scroll behavior
284    #[must_use]
285    pub const fn with_smooth(mut self) -> Self {
286        self.behavior = ScrollIntoViewBehavior::Smooth;
287        self
288    }
289}
290
291/// Scroll alignment for vertical (block) or horizontal (inline) axis
292///
293/// Determines where the target element should be positioned within
294/// the scroll container's visible area.
295#[repr(C)]
296#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
297pub enum ScrollLogicalPosition {
298    /// Align target's start edge with container's start edge
299    Start,
300    /// Center target within container
301    Center,
302    /// Align target's end edge with container's end edge
303    End,
304    /// Minimum scroll distance to make target fully visible (default)
305    #[default]
306    Nearest,
307}
308
309/// Scroll animation behavior for scrollIntoView API
310///
311/// This is distinct from the CSS `scroll-behavior` property, as it also
312/// supports the `Instant` option which CSS does not have.
313#[repr(C)]
314#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
315pub enum ScrollIntoViewBehavior {
316    /// Respect CSS scroll-behavior property (default)
317    #[default]
318    Auto,
319    /// Immediate jump without animation
320    Instant,
321    /// Animated smooth scroll
322    Smooth,
323}
324
325/// Reason why a lifecycle event was triggered.
326#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
327#[repr(C)]
328pub enum LifecycleReason {
329    /// First appearance in DOM
330    InitialMount,
331    /// Removed and re-added to DOM
332    Remount,
333    /// Layout bounds changed
334    Resize,
335    /// Props or state changed
336    Update,
337    /// Node was removed from DOM
338    Unmount,
339    /// A `<transient-window>` was closed by the USER (outside click, Escape),
340    /// not by the app flipping `open`.
341    Dismiss,
342    /// A `<transient-window>` was torn off its anchor into a free toplevel
343    /// by the user's drag. `current_bounds` is where it went, in the parent.
344    TearOff,
345    /// A torn-off `<transient-window>` was docked back (onto its anchor or a
346    /// drop zone) by the user's drag.
347    Dock,
348}
349
350/// Keyboard modifier keys state.
351#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)]
352#[repr(C)]
353pub struct KeyModifiers {
354    pub shift: bool,
355    pub ctrl: bool,
356    pub alt: bool,
357    pub meta: bool,
358}
359
360impl KeyModifiers {
361    #[must_use]
362    pub fn new() -> Self {
363        Self::default()
364    }
365
366    #[must_use]
367    pub const fn with_shift(mut self) -> Self {
368        self.shift = true;
369        self
370    }
371
372    #[must_use]
373    pub const fn with_ctrl(mut self) -> Self {
374        self.ctrl = true;
375        self
376    }
377
378    #[must_use]
379    pub const fn with_alt(mut self) -> Self {
380        self.alt = true;
381        self
382    }
383
384    #[must_use]
385    pub const fn with_meta(mut self) -> Self {
386        self.meta = true;
387        self
388    }
389
390    #[must_use]
391    pub const fn is_empty(&self) -> bool {
392        !self.shift && !self.ctrl && !self.alt && !self.meta
393    }
394}
395
396/// What kind of device produced a pointer event.
397///
398/// The same discriminator the web calls `PointerEvent.pointerType`, GTK calls
399/// `GdkInputSource` and Qt calls `QInputDevice::DeviceType`. Azul splits Mouse,
400/// Touch and Pen into separate FILTERS, which is a stronger guarantee than a
401/// runtime tag — but it cannot express the distinctions WITHIN the pointer
402/// family, and those matter:
403///
404/// - A touchpad and a mouse both arrive as a synthesized pointer. Telling them
405///   apart is what lets an app know a pinch gesture is possible at all, and
406///   whether "scroll" means a wheel detent or a continuous drag.
407/// - A trackball and a trackpoint have their own acceleration and scroll
408///   semantics, which is why GTK and Android name them separately.
409/// - A pen reported through the pointer path (a stylus acting as a mouse, the
410///   Wayland tablet bridge) is not a finger and not a mouse.
411#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
412#[repr(C)]
413pub enum PointerSource {
414    /// The platform did not say.
415    Unknown,
416    /// A mouse.
417    Mouse,
418    /// An indirect touch surface — a laptop trackpad or a Magic Trackpad.
419    Touchpad,
420    /// A trackball.
421    Trackball,
422    /// A pointing stick (ThinkPad nub).
423    Trackpoint,
424    /// A direct touch surface reporting through the pointer path.
425    Touchscreen,
426    /// A stylus tip.
427    Pen,
428    /// The inverted end of a stylus.
429    Eraser,
430}
431
432/// Type-specific event data for mouse events.
433#[derive(Debug, Clone, Copy, PartialEq)]
434#[repr(C)]
435pub struct MouseEventData {
436    /// Position of the mouse cursor
437    pub position: LogicalPosition,
438    /// Which button was pressed/released
439    pub button: MouseButton,
440    /// Bitmask of currently pressed buttons
441    pub buttons: u8,
442    /// Modifier keys state
443    pub modifiers: KeyModifiers,
444    /// What produced this event. APPENDED for ABI stability.
445    pub source: PointerSource,
446    /// Which physical device produced it, or `0` when the platform does not
447    /// say. APPENDED for ABI stability.
448    ///
449    /// Only `PenState`, `GamepadState` and `TabletPadState` carried a device id
450    /// before, so a mouse event could not say WHICH of two mice moved — and
451    /// `MouseState` used to be a single global, which is why multi-seat (a
452    /// kiosk, a shared display, two people on one compositor) was not
453    /// expressible at all. The state fans out per seat since 9b-ii; this
454    /// field stays the PHYSICAL device, `seat_id` below says which cursor.
455    pub device_id: u64,
456    /// Which pointer SEAT - which independent cursor - produced it. APPENDED
457    /// (9b-ii). `PRIMARY_POINTER_SEAT` (0) for the ordinary mouse, which is
458    /// every event on a platform with one cursor; an X11 MPX master id or a
459    /// Wayland seat otherwise. A callback that receives a non-zero value
460    /// reads that seat's state through `get_pointer_seat_state`, because
461    /// `get_current_mouse_state` is the PRIMARY seat by definition.
462    pub seat_id: u64,
463}
464
465impl Default for MouseEventData {
466    fn default() -> Self {
467        Self {
468            position: LogicalPosition { x: 0.0, y: 0.0 },
469            button: MouseButton::Left,
470            buttons: 0,
471            modifiers: KeyModifiers::default(),
472            source: PointerSource::Unknown,
473            device_id: 0,
474            seat_id: crate::window::PRIMARY_POINTER_SEAT,
475        }
476    }
477}
478
479/// Type-specific event data for keyboard events.
480#[derive(Debug, Clone, Copy, PartialEq, Eq)]
481pub struct KeyboardEventData {
482    /// The virtual key code
483    pub key_code: u32,
484    /// The character produced (if any)
485    pub char_code: Option<char>,
486    /// Modifier keys state
487    pub modifiers: KeyModifiers,
488    /// Whether this is a repeat event
489    pub repeat: bool,
490    /// Which physical keyboard produced this event, or `0` when the platform
491    /// does not say. APPENDED for ABI stability.
492    ///
493    /// Same reasoning as `MouseEventData::device_id`: with two keyboards
494    /// attached — a laptop's built-in and an external, or a barcode scanner
495    /// presenting as a keyboard — nothing could tell their input apart.
496    pub device_id: u64,
497    /// Which KEYBOARD SEAT pressed (9b-ii-a-i): `PRIMARY_POINTER_SEAT` for
498    /// the ordinary keyboard, a seat id for a second person's - the same
499    /// number their pointer events carry.
500    pub seat_id: u64,
501}
502
503impl Default for KeyboardEventData {
504    fn default() -> Self {
505        Self {
506            key_code: 0,
507            char_code: None,
508            modifiers: KeyModifiers::default(),
509            repeat: false,
510            device_id: 0,
511            seat_id: crate::window::PRIMARY_POINTER_SEAT,
512        }
513    }
514}
515
516/// Type-specific event data for scroll events.
517#[derive(Debug, Clone, Copy, PartialEq, Eq)]
518pub struct ScrollEventData {
519    /// Scroll delta (dx, dy)
520    pub delta: LogicalPosition,
521    /// How the delta should be interpreted
522    pub delta_mode: ScrollDeltaMode,
523    /// Which pointer SEAT turned the wheel (9b-ii-b); `PRIMARY_POINTER_SEAT`
524    /// for the ordinary mouse. A second cursor's wheel scrolls the node under
525    /// THAT cursor, so the event names the seat the way a mouse event does.
526    pub seat_id: u64,
527}
528
529/// Type-specific event data for touch events.
530#[derive(Debug, Clone, Copy, PartialEq)]
531pub struct TouchEventData {
532    /// Touch identifier
533    pub id: u64,
534    /// Touch position
535    pub position: LogicalPosition,
536    /// Touch force/pressure (0.0 - 1.0)
537    pub force: f32,
538    /// The SEAT whose touchscreen this is (9b-ii-a-i-c); ids are per seat.
539    pub seat_id: u64,
540}
541
542/// Type-specific event data for clipboard events.
543#[derive(Debug, Clone, PartialEq, Eq)]
544pub struct ClipboardEventData {
545    /// The clipboard content (for paste events)
546    pub content: Option<String>,
547}
548
549/// Type-specific event data for lifecycle events.
550#[derive(Debug, Clone, Copy, PartialEq, Eq)]
551pub struct LifecycleEventData {
552    /// Why this lifecycle event was triggered
553    pub reason: LifecycleReason,
554    /// Previous layout bounds (for resize events)
555    pub previous_bounds: Option<LogicalRect>,
556    /// Current layout bounds
557    pub current_bounds: LogicalRect,
558}
559
560/// Type-specific event data for window events.
561#[derive(Debug, Clone, Copy, PartialEq, Eq)]
562pub struct WindowEventData {
563    /// Window size (for resize events)
564    pub size: Option<LogicalRect>,
565    /// Window position (for move events)
566    pub position: Option<LogicalPosition>,
567}
568
569/// Type-specific event data for text-input (editing) events.
570///
571/// Data carried by `EventType::RawMouseMotion`.
572///
573/// Raw motion is what a 3D viewport, a map orbit, an infinite-drag number
574/// scrubber and a first-person camera all need, and it is NOT the difference
575/// between two cursor positions. Absolute positions have already had the OS
576/// pointer-acceleration curve applied and are clamped to the screen, so
577/// differencing them gives accelerated motion that stops dead at the edge of
578/// the display. These deltas are pre-acceleration and unbounded.
579/// Where the caret sits INSIDE an open IME preedit.
580///
581/// Both offsets are into the preedit string, not into the document: the
582/// preedit is not committed text and has no position in the document until it
583/// is. An IME uses this to place its candidate window under the segment being
584/// edited, and a `begin != end` pair is a SELECTED segment - several IMEs
585/// (Japanese conversion especially) move a highlighted region through the
586/// preedit as the user cycles candidates.
587///
588/// A named struct rather than the `(usize, usize)` this used to be, because a
589/// non-empty tuple has no C representation - so the accessor could not be
590/// exposed at all and no binding could read an IME's caret.
591#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
592#[repr(C)]
593pub struct CompositionCursor {
594    /// Start offset in the preedit, in bytes.
595    pub begin: usize,
596    /// End offset. Equal to `begin` for a caret with no selected segment.
597    pub end: usize,
598}
599
600azul_css::impl_option!(
601    CompositionCursor,
602    OptionCompositionCursor,
603    [Debug, Copy, Clone, PartialEq, Eq]
604);
605
606/// `#[repr(C)]` because `CallbackInfo::get_raw_mouse_motion` hands this
607/// across the C API. Without it the layout is undefined at the boundary and
608/// the generated bindings read garbage fields.
609#[derive(Debug, Clone, Copy, PartialEq)]
610#[repr(C)]
611pub struct RawMotionEventData {
612    /// Horizontal motion in the device's own units. NOT logical pixels: a
613    /// mouse reports counts, whose size depends on its DPI, which is exactly
614    /// why a sensitivity setting exists.
615    pub dx: f64,
616    /// Vertical motion, same units.
617    pub dy: f64,
618    /// Which device moved, or `0` when the platform does not say.
619    pub device_id: u64,
620}
621
622azul_css::impl_option!(
623    RawMotionEventData,
624    OptionRawMotionEventData,
625    [Debug, Copy, Clone, PartialEq]
626);
627
628/// Data carried by a `MediaControl` event (9h-i-a-i-a, 9h-i-a-i-b): what the
629/// platform's media controls asked for. The URI of an `OpenUri` is not
630/// repeated here - `CallbackInfo::get_media_control_request` carries the
631/// whole request.
632#[derive(Debug, Clone, Copy, PartialEq)]
633#[repr(C)]
634pub struct MediaControlEventData {
635    /// Microseconds; relative for `SeekRelative`, absolute for `SeekAbsolute`.
636    pub position_us: i64,
637    pub kind: crate::media_session::MediaControlKind,
638    /// The requested volume for `SetVolume` (`0.0` silent, `1.0` full), `0.0`
639    /// for every other kind.
640    pub volume: f32,
641}
642
643/// Data carried by a `SystemAudioChange` event (9h-i-a-i-d-i): what the
644/// system did with the audio the app took over. Also readable through
645/// `CallbackInfo::get_system_audio_change`.
646#[derive(Debug, Clone, Copy, PartialEq, Eq)]
647#[repr(C)]
648pub struct SystemAudioEventData {
649    pub change: crate::media_session::SystemAudioChange,
650}
651
652/// Data carried by the three IME composition events.
653///
654/// Every desktop shell already runs a real IME client — Win32 `WM_IME_*` with
655/// candidate-window positioning, macOS `NSTextInputClient`, Wayland
656/// `zwp_text_input_v3` preedit/commit, X11 XIM plus the xkb Compose layer —
657/// and all of it terminated in the engine's text cache. The built-in text
658/// widgets therefore worked, but an app rendering its own composition (a code
659/// editor, a terminal, a chat box with inline candidates) had no way to see
660/// it, because the `Composition*` filters had no event to carry.
661///
662/// The shape is the intersection of what the platforms report, which is also
663/// what W3C `CompositionEvent` and Wayland `preedit_string` agree on.
664#[derive(Debug, Clone, PartialEq, Eq)]
665pub struct CompositionEventData {
666    /// The preedit (composition) text as it currently stands. Empty on
667    /// `CompositionStart`, and on `CompositionEnd` this is the committed
668    /// string — W3C defines `compositionend.data` as what was committed, not
669    /// what was pending.
670    pub data: String,
671    /// Byte offset of the selection/caret start within `data`. Wayland sends
672    /// this directly (`preedit_string(text, cursor_begin, cursor_end)`);
673    /// Win32 derives it from `GCS_CURSORPOS`; macOS from the `selectedRange`
674    /// passed to `setMarkedText:selectedRange:replacementRange:`.
675    pub cursor_begin: usize,
676    /// Byte offset of the selection/caret end within `data`. Equal to
677    /// `cursor_begin` for a collapsed caret.
678    pub cursor_end: usize,
679    /// The SEAT whose input method this is (9b-ii-a-i-d-ii-c-i); the primary
680    /// is `PRIMARY_POINTER_SEAT`. The Focus filter delivers a seat's
681    /// composition to that seat's focused node.
682    pub seat_id: u64,
683}
684
685/// Carried by `EventType::Input` events so that text-input callbacks can read
686/// the edit details directly off the event — matching how mouse/keyboard/scroll
687/// callbacks read their data — instead of having to reach into the
688/// `TextInputManager`'s pending changeset. The edited node is already available
689/// via `SyntheticEvent.target`.
690#[derive(Debug, Clone, PartialEq, Eq)]
691pub struct TextInputEventData {
692    /// The text inserted by this edit (empty for pure deletions).
693    pub inserted_text: String,
694    /// The text content of the node *before* this edit was applied.
695    pub old_text: String,
696}
697
698/// Identifies WHICH pending structural changeset a notification is for.
699///
700/// Carried by `EventType::DocumentEdit` events; the app acks with the same
701/// id via `mark_document_edit_applied`. The full changeset is intentionally
702/// NOT copied onto the event — it stays single-instance in the window
703/// (one-pending-changeset model).
704#[derive(Debug, Clone, Copy, PartialEq, Eq)]
705pub struct DocumentEditEventData {
706    /// The commit-handshake id of the recorded changeset.
707    pub changeset_id: u64,
708}
709
710/// Union of all possible event data types.
711#[derive(Debug, Clone, PartialEq)]
712pub enum EventData {
713    /// Mouse event data
714    Mouse(MouseEventData),
715    /// Keyboard event data
716    Keyboard(KeyboardEventData),
717    /// Scroll event data
718    Scroll(ScrollEventData),
719    /// Touch event data
720    Touch(TouchEventData),
721    /// Clipboard event data
722    Clipboard(ClipboardEventData),
723    /// Text-input (editing) event data
724    TextInput(TextInputEventData),
725    /// Structural document-edit notification data
726    DocumentEdit(DocumentEditEventData),
727    /// Lifecycle event data
728    Lifecycle(LifecycleEventData),
729    /// Window event data
730    Window(WindowEventData),
731    /// No additional data
732    None,
733    /// IME composition data. APPENDED at the end for ABI stability — this is
734    /// `#[repr(C, u8)]`-adjacent, so a variant inserted in the middle would
735    /// renumber every discriminant after it.
736    Composition(CompositionEventData),
737    /// Raw, pre-acceleration pointer motion. APPENDED at the end.
738    RawMotion(RawMotionEventData),
739    /// A media seek (9h-i-a-i-a). APPENDED for ABI stability.
740    MediaControl(MediaControlEventData),
741    SystemAudio(SystemAudioEventData),
742}
743
744/// High-level event type classification.
745///
746/// This enum categorizes all possible events that can occur in the UI.
747/// It extends the existing event system with new event types for
748/// lifecycle, clipboard, media, and form handling.
749#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
750#[repr(C)]
751pub enum EventType {
752    // Mouse Events
753    /// Mouse cursor is over the element
754    MouseOver,
755    /// Mouse cursor entered the element
756    MouseEnter,
757    /// Mouse cursor left the element
758    MouseLeave,
759    /// Mouse left the element OR moved to a child element (W3C `mouseout`, bubbles)
760    MouseOut,
761    /// Mouse button pressed
762    MouseDown,
763    /// Mouse button released
764    MouseUp,
765    /// Mouse click (down + up on same element)
766    Click,
767    /// Mouse double-click
768    DoubleClick,
769    /// Right-click / context menu
770    ContextMenu,
771
772    // Keyboard Events
773    /// Key pressed down
774    KeyDown,
775    /// Key released
776    KeyUp,
777    /// Character input (respects locale/keyboard layout)
778    KeyPress,
779
780    // IME Composition Events
781    /// IME composition started
782    CompositionStart,
783    /// IME composition updated (intermediate text changed)
784    CompositionUpdate,
785    /// IME composition ended (final text committed)
786    CompositionEnd,
787
788    // Focus Events
789    /// Element received focus
790    Focus,
791    /// Element lost focus
792    Blur,
793    /// Focus entered element or its children
794    FocusIn,
795    /// Focus left element and its children
796    FocusOut,
797
798    // Input Events
799    /// Input value is being changed (fires on every keystroke)
800    Input,
801    /// Input value has changed (fires after editing complete)
802    Change,
803    /// Form submitted
804    Submit,
805    /// Form reset
806    Reset,
807    /// Form validation failed
808    Invalid,
809
810    // Scroll Events
811    /// Element is being scrolled
812    Scroll,
813    /// Scroll started
814    ScrollStart,
815    /// Scroll ended
816    ScrollEnd,
817
818    // Drag Events
819    /// Drag operation started
820    DragStart,
821    /// Element is being dragged
822    Drag,
823    /// Drag operation ended
824    DragEnd,
825    /// Dragged element entered drop target
826    DragEnter,
827    /// Dragged element is over drop target
828    DragOver,
829    /// Dragged element left drop target
830    DragLeave,
831    /// Element was dropped
832    Drop,
833
834    // Touch Events
835    /// Touch started
836    TouchStart,
837    /// Touch moved
838    TouchMove,
839    /// Touch ended
840    TouchEnd,
841    /// Touch cancelled
842    TouchCancel,
843
844    // Pen / Stylus Events (W3C PointerEvent, pointerType "pen")
845    /// Pen tip made contact (or pen entered while down)
846    PenDown,
847    /// Pen moved (in contact or hovering in range)
848    PenMove,
849    /// Pen tip lifted
850    PenUp,
851    /// Pen entered hover/sensing range (proximity in)
852    PenEnter,
853    /// Pen left hover/sensing range (proximity out)
854    PenLeave,
855
856    // Gesture Events
857    /// Long press detected (touch or mouse held down)
858    LongPress,
859    /// Swipe gesture to the left
860    SwipeLeft,
861    /// Swipe gesture to the right
862    SwipeRight,
863    /// Swipe gesture upward
864    SwipeUp,
865    /// Swipe gesture downward
866    SwipeDown,
867    /// Pinch-in gesture (zoom out)
868    PinchIn,
869    /// Pinch-out gesture (zoom in)
870    PinchOut,
871    /// Clockwise rotation gesture
872    RotateClockwise,
873    /// Counter-clockwise rotation gesture
874    RotateCounterClockwise,
875
876    // Clipboard Events
877    /// Content copied to clipboard
878    Copy,
879    /// Content cut to clipboard
880    Cut,
881    /// Content pasted from clipboard
882    Paste,
883
884    // Media Events
885    /// Media playback started
886    Play,
887    /// Media playback paused
888    Pause,
889    /// Media playback ended
890    Ended,
891    /// Media time updated
892    TimeUpdate,
893    /// Media volume changed
894    VolumeChange,
895    /// Media error occurred
896    MediaError,
897
898    // Lifecycle Events
899    /// Component was mounted to the DOM
900    Mount,
901    /// Component will be unmounted from the DOM
902    Unmount,
903    /// Component was updated
904    Update,
905    /// Component layout bounds changed
906    Resize,
907    /// A `<transient-window>` was dismissed by the user (outside click or
908    /// Escape). Fired on the transient node so the app can drop its `open`
909    /// flag; the surface is already gone by the time this runs.
910    Dismiss,
911    /// A `<transient-window>` was torn off by the user's drag and is now a
912    /// free toplevel. Fired on the transient node; the lifecycle data's
913    /// `current_bounds` is the toplevel's rect in the parent's coordinates,
914    /// for apps that persist their palette layout.
915    TearOff,
916    /// A torn-off `<transient-window>` was docked back by the user's drag.
917    Dock,
918
919    // Window Events
920    /// Window resized
921    WindowResize,
922    /// Window moved
923    WindowMove,
924    /// Window close requested
925    WindowClose,
926    /// The window's frame state changed — minimized, maximized, restored to
927    /// normal, or entered/left fullscreen.
928    ///
929    /// Read the new state from `flags.frame` on the current window state; the
930    /// event carries no payload of its own because the flag IS the payload and
931    /// a callback that cares will already be reading window state.
932    WindowFrameChanged,
933    /// Window received focus
934    WindowFocusIn,
935    /// Window lost focus
936    WindowFocusOut,
937    /// System theme changed
938    ThemeChange,
939    /// Window DPI/scale factor changed (moved to different monitor)
940    WindowDpiChanged,
941    /// Window moved to a different monitor
942    WindowMonitorChanged,
943
944    // Application Events
945    /// A monitor/display was connected
946    MonitorConnected,
947    /// A monitor/display was disconnected
948    MonitorDisconnected,
949
950    // File Events
951    /// File is being hovered
952    FileHover,
953    /// File was dropped
954    FileDrop,
955    /// File hover cancelled
956    FileHoverCancel,
957
958    // Hardware input-device Events (P6 sensors / gamepad)
959    /// A motion-sensor reading (accelerometer / gyroscope / magnetometer)
960    /// changed. Read the value with `CallbackInfo::get_sensor_reading`.
961    SensorChanged,
962    /// A gamepad's buttons / axes changed, or one was (dis)connected. Read it
963    /// with `CallbackInfo::get_primary_gamepad` / `get_gamepad_state`.
964    GamepadInput,
965
966    // Geolocation Events (MWA-A1 — synthesized by the capability pump's
967    // GeolocationManager EventProvider; both filter enums already carried
968    // the matching variants, only this dispatch type was missing them).
969    /// A new GPS / network location fix arrived. Read it with
970    /// `CallbackInfo::get_geolocation_fix`.
971    GeolocationFix,
972    /// The native geolocation subscription errored, timed out, or was
973    /// revoked.
974    GeolocationError,
975
976    // Async capability outcomes (MWA-A1b — synthesized by the capability
977    // pump's manager EventProviders so idle apps observe prompt results).
978    /// A permission's OS-observed state changed (granted / denied /
979    /// revoked / restricted). Targeted at the capability's most recent
980    /// subscriber node when known, else the root. Read the new state via
981    /// `CallbackInfo` permission accessors.
982    PermissionChanged,
983    /// A biometric authentication prompt completed. Read the outcome via
984    /// `CallbackInfo::get_biometric_result`.
985    BiometricResult,
986    /// The screen eyedropper started by `CallbackInfo::pick_screen_color`
987    /// finished - the user picked a pixel, or cancelled. Window-level (the
988    /// pick is not bound to a node); read the colour via
989    /// `CallbackInfo::get_picked_screen_color` (`None` = cancelled).
990    ScreenColorPicked,
991    /// A keyring store / get / delete operation completed. Read the outcome
992    /// via `CallbackInfo::get_keyring_result`.
993    KeyringResult,
994
995    // Structural document editing (C11 — synthesized once per recorded
996    // changeset by the LayoutWindow's document-edit EventProvider).
997    /// A STRUCTURAL document edit (Enter split / Backspace merge / wrap /
998    /// selection-spanning replace…) was recorded and awaits the app's
999    /// apply-and-ack. Fired ONCE per changeset so the app's apply loop is
1000    /// prompt instead of polling `get_pending_document_edit()` on its next
1001    /// unrelated callback. The changeset id rides on
1002    /// `EventData::DocumentEdit`; the full changeset is read via
1003    /// `CallbackInfo` / `LayoutWindow::get_pending_document_edit()`.
1004    DocumentEdit,
1005
1006    /// The text of an editable node CHANGED - fired once per commit, AFTER
1007    /// the edit is applied (typing, IME commit, deletion, paste, undo/redo:
1008    /// everything that writes the text overlay).
1009    ///
1010    /// `Input` is the `beforeinput` of this pair: it fires BEFORE the
1011    /// pending insertion is applied so a listener can `prevent_default`,
1012    /// which means a model that reads `get_unsynced_text_edits` from an
1013    /// `Input` callback is always one keystroke behind. A listener that
1014    /// wants the committed text - a word counter, a dirty flag, a live
1015    /// preview - listens here instead; the edits are already in the sync
1016    /// channel when this runs. Deletions used to be the only edits notified
1017    /// post-commit (through a second `Input`); this is the uniform spelling.
1018    TextChanged,
1019    /// An input device was attached. APPENDED at the end for ABI stability.
1020    ///
1021    /// `ApplicationEventFilter` has carried `DeviceConnected` since it was
1022    /// introduced, with no `EventType` able to reach it. Hotplug is observable
1023    /// on every backend — gilrs already reports gamepad arrive/leave through
1024    /// the capability pump, Wayland has `wl_seat.capabilities` and the
1025    /// `zwp_tablet_seat_v2` add/remove events, X11 has `XI_HierarchyChanged`,
1026    /// Win32 has `WM_DEVICECHANGE` — so the filter was the only thing missing.
1027    DeviceConnected,
1028    /// An input device was detached. See [`EventType::DeviceConnected`].
1029    DeviceDisconnected,
1030    /// The pen barrel was squeezed. APPENDED at the end for ABI stability.
1031    ///
1032    /// Apple Pencil Pro's squeeze, surfaced by `UIPencilInteraction`. The
1033    /// Hover and Window filter variants have existed with no `EventType` able
1034    /// to reach them.
1035    PenSqueeze,
1036    /// The pen was double-tapped on its barrel — Apple Pencil 2 and later,
1037    /// also `UIPencilInteraction`. See [`EventType::PenSqueeze`].
1038    PenDoubleTap,
1039    /// The pen is in range but not touching. Wayland reports it as
1040    /// `proximity_in` plus `distance`, Win32 as `POINTER_FLAG_INRANGE` without
1041    /// `POINTER_FLAG_INCONTACT`, Android as `ACTION_HOVER_MOVE` with a stylus
1042    /// tool type, macOS as an `NSEventSubtype::TabletProximity` subtype on the
1043    /// ordinary mouse-event path. See [`EventType::PenSqueeze`].
1044    PenHover,
1045    /// A component's default action was invoked — Enter/Space on a focused
1046    /// control, or the accessibility `Default` action. Distinct from `Click`:
1047    /// that is the pointer spelling, this is the component-level one that
1048    /// `ComponentEventFilter::DefaultAction` subscribes to.
1049    DefaultAction,
1050    /// A component's selection changed. `ComponentEventFilter::Selected` has
1051    /// existed with no `EventType` and no match arm.
1052    Selected,
1053    /// A HID device delivered an input report. APPENDED at the end.
1054    ///
1055    /// The escape hatch for devices azul does not model — flight sticks,
1056    /// wheels, 6-DOF mice, pedals, Stream Decks. Read the bytes via
1057    /// `CallbackInfo::get_hid_reports`.
1058    HidReport,
1059    /// A modifier or lock key changed state. APPENDED at the end.
1060    ///
1061    /// Modifiers arrive today only as ordinary key events, so an app tracking
1062    /// "is Shift down" has to watch every KeyDown and KeyUp and reconstruct
1063    /// it — and gets it wrong across a focus loss, because the KeyUp for a
1064    /// modifier released while another window had focus never arrives. This
1065    /// event carries the resolved state instead.
1066    ModifiersChanged,
1067    /// Raw pointer motion, pre-acceleration and unclamped. APPENDED at the
1068    /// end for ABI stability.
1069    ///
1070    /// `MouseState.is_cursor_locked` has existed since the beginning,
1071    /// documented as "important for applications like games", with no event
1072    /// carrying a delta behind it — so pointer lock was a flag that did
1073    /// nothing. This is the event it was waiting for.
1074    RawMouseMotion,
1075    /// A dial or rotary encoder turned. APPENDED at the end for ABI stability.
1076    ///
1077    /// The Surface Dial, a tablet pad's dial, a Wear crown, an Apple Digital
1078    /// Crown. `DialState` (rotation, detents, pressed, contact point) has been
1079    /// readable through `CallbackInfo::get_dial_state()` since the type
1080    /// landed, with no event to subscribe to — so a dial could only be POLLED
1081    /// from an unrelated callback that happened to run.
1082    DialRotate,
1083    /// A dial with a physical click was pressed. APPENDED at the end.
1084    DialClick,
1085    /// The pointer MOVED while over this node. APPENDED at the end.
1086    ///
1087    /// W3C `mousemove`. This is the event `MouseOver` used to be: until
1088    /// now azul had no movement event at all, and `MouseOver` was emitted
1089    /// on every cursor move - `mousemove` semantics under the `mouseover`
1090    /// name. `MouseOver` now means what the spec says (the pointer
1091    /// ENTERED the node) and movement lives here.
1092    MouseMove,
1093    /// The platform's media controls asked for a SEEK (9h-i-a-i-a): a desktop
1094    /// scrubber, a lock-screen slider, `playerctl position`. APPENDED at the
1095    /// end for ABI stability. Unlike Play/Pause/Next, which arrive as media
1096    /// KEY events, a seek carries a position - read it with
1097    /// `CallbackInfo::get_media_control_request`.
1098    MediaControl,
1099    /// The pointer lock changed hands (9d-ii-c): the app took or released it,
1100    /// or the platform ended it - every grab-based backend drops the grab
1101    /// when the window loses focus, and a Wayland compositor may end a
1102    /// constraint at any time. Both directions, like the browser's
1103    /// `pointerlockchange`; read `mouse_state.is_cursor_locked` for which.
1104    /// Nothing re-takes a lost lock on focus return (USER RULING
1105    /// 2026-09-03): an app that wants it back asks again from
1106    /// `WindowFocusReceived`.
1107    PointerLockChange,
1108    /// The system did something with the audio the app took over
1109    /// (9h-i-a-i-d-i): an interruption began or ended, focus was ducked,
1110    /// granted, or lost. Application-level, at the root; the payload is
1111    /// `EventData::SystemAudio` and `CallbackInfo::get_system_audio_change`.
1112    SystemAudioChange,
1113}
1114
1115/// Unified event wrapper (similar to React's `SyntheticEvent`).
1116///
1117/// All events in the system are wrapped in this structure, providing
1118/// a consistent interface and enabling event propagation control.
1119#[derive(Debug, Clone, PartialEq)]
1120#[allow(clippy::struct_excessive_bools)] // four independent propagation flags — W3C's own model
1121pub struct SyntheticEvent {
1122    /// The type of event
1123    pub event_type: EventType,
1124
1125    /// Where the event came from
1126    pub source: EventSource,
1127
1128    /// Current propagation phase
1129    pub phase: EventPhase,
1130
1131    /// Target node that the event was dispatched to
1132    pub target: DomNodeId,
1133
1134    /// Current node in the propagation path
1135    pub current_target: DomNodeId,
1136
1137    /// Timestamp when event was created
1138    pub timestamp: Instant,
1139
1140    /// Type-specific event data
1141    pub data: EventData,
1142
1143    /// Whether propagation has been stopped
1144    pub stopped: bool,
1145
1146    /// Whether immediate propagation has been stopped
1147    pub stopped_immediate: bool,
1148
1149    /// Whether default action has been prevented
1150    pub prevented_default: bool,
1151
1152    /// Deliver at the target ONLY — no capture, no bubble.
1153    ///
1154    /// Set on the release synthesised for the node that was PRESSED when the
1155    /// pointer releases somewhere else (press-target capture, see
1156    /// `HoverManager::apply_press_target_capture`): that node owes a
1157    /// `MouseUp`, but its ancestors already see the real release through the
1158    /// hovered node's propagation path and must not see it twice.
1159    pub at_target_only: bool,
1160}
1161
1162impl SyntheticEvent {
1163    /// Create a new synthetic event.
1164    ///
1165    /// # Parameters
1166    /// - `timestamp`: Current time from `(system_callbacks.get_system_time_fn.cb)()`
1167    #[must_use]
1168    pub const fn new(
1169        event_type: EventType,
1170        source: EventSource,
1171        target: DomNodeId,
1172        timestamp: Instant,
1173        data: EventData,
1174    ) -> Self {
1175        Self {
1176            event_type,
1177            source,
1178            phase: EventPhase::Target,
1179            target,
1180            current_target: target,
1181            timestamp,
1182            data,
1183            stopped: false,
1184            stopped_immediate: false,
1185            prevented_default: false,
1186            at_target_only: false,
1187        }
1188    }
1189
1190    /// This event, delivered at its target only (see [`Self::at_target_only`]).
1191    #[must_use]
1192    pub const fn at_target_only(mut self) -> Self {
1193        self.at_target_only = true;
1194        self
1195    }
1196
1197    /// Stop event propagation after the current phase completes.
1198    ///
1199    /// This prevents the event from reaching handlers in subsequent phases
1200    /// (e.g., stopping during capture prevents bubble phase).
1201    pub const fn stop_propagation(&mut self) {
1202        self.stopped = true;
1203    }
1204
1205    /// Stop event propagation immediately.
1206    ///
1207    /// This prevents any further handlers from being called, even on the
1208    /// current target element.
1209    pub const fn stop_immediate_propagation(&mut self) {
1210        self.stopped_immediate = true;
1211        self.stopped = true;
1212    }
1213
1214    /// Prevent the default action associated with this event.
1215    ///
1216    /// For example, prevents form submission on Enter key, or prevents
1217    /// text selection on drag.
1218    pub const fn prevent_default(&mut self) {
1219        self.prevented_default = true;
1220    }
1221
1222    /// Check if propagation was stopped.
1223    #[must_use]
1224    pub const fn is_propagation_stopped(&self) -> bool {
1225        self.stopped
1226    }
1227
1228    /// Check if immediate propagation was stopped.
1229    #[must_use]
1230    pub const fn is_immediate_propagation_stopped(&self) -> bool {
1231        self.stopped_immediate
1232    }
1233
1234    /// Check if default action was prevented.
1235    #[must_use]
1236    pub const fn is_default_prevented(&self) -> bool {
1237        self.prevented_default
1238    }
1239}
1240
1241/// Result of event propagation through DOM tree.
1242#[derive(Debug, Clone, Default)]
1243pub struct PropagationResult {
1244    /// Callbacks that should be invoked, in order
1245    pub callbacks_to_invoke: Vec<(NodeId, EventFilter)>,
1246    /// Whether default action should be prevented
1247    pub default_prevented: bool,
1248}
1249
1250/// Get the path from root to target node in the DOM tree.
1251///
1252/// This is used for event propagation - we need to know which nodes
1253/// are ancestors of the target to implement capture/bubble phases.
1254///
1255/// Returns nodes in order from root to target (inclusive).
1256#[must_use]
1257pub fn get_dom_path(
1258    node_hierarchy: &crate::id::NodeHierarchy,
1259    target_node: NodeHierarchyItemId,
1260) -> Vec<NodeId> {
1261    let mut path = Vec::new();
1262    let Some(target_node_id) = target_node.into_crate_internal() else {
1263        return path;
1264    };
1265
1266    let hier_ref = node_hierarchy.as_ref();
1267
1268    // Build path from target to root. Bounded by the node count and guarded by a
1269    // visited-set: a corrupt hierarchy with a parent cycle (or a parent chain
1270    // longer than the arena) would otherwise loop forever / OOM here, and this
1271    // runs on every event dispatch.
1272    let node_count = hier_ref.len();
1273    let mut visited: BTreeSet<NodeId> = BTreeSet::new();
1274    let mut current = Some(target_node_id);
1275    while let Some(node_id) = current {
1276        if path.len() > node_count || !visited.insert(node_id) {
1277            // Cycle or overrun detected: stop rather than spin forever.
1278            break;
1279        }
1280        path.push(node_id);
1281        current = hier_ref.get(node_id).and_then(|node| node.parent);
1282    }
1283
1284    // Reverse to get root → target order
1285    path.reverse();
1286    path
1287}
1288
1289/// Propagate event through DOM tree with capture and bubble phases.
1290///
1291/// This implements DOM Level 2 event propagation:
1292/// 1. **Capture Phase**: Event travels from root down to target
1293/// 2. **Target Phase**: Event is at the target element
1294/// 3. **Bubble Phase**: Event travels from target back up to root
1295///
1296/// The event can be stopped at any point via `stopPropagation()` or
1297/// `stopImmediatePropagation()`.
1298///
1299/// # Panics
1300///
1301/// Panics if `path` is empty; it must contain at least the target node.
1302pub fn propagate_event(
1303    event: &mut SyntheticEvent,
1304    node_hierarchy: &crate::id::NodeHierarchy,
1305    callbacks: &BTreeMap<NodeId, Vec<EventFilter>>,
1306) -> PropagationResult {
1307    let path = get_dom_path(node_hierarchy, event.target.node);
1308    if path.is_empty() {
1309        return PropagationResult::default();
1310    }
1311
1312    let ancestors = &path[..path.len().saturating_sub(1)];
1313    let target_node_id = *path.last().unwrap();
1314
1315    let mut result = PropagationResult::default();
1316
1317    // A captured release (see `SyntheticEvent::at_target_only`) reaches its
1318    // target and nothing else: the ancestors get the real release through
1319    // the hovered node's path.
1320    if event.at_target_only {
1321        propagate_target_phase(event, target_node_id, callbacks, &mut result);
1322        result.default_prevented = event.prevented_default;
1323        return result;
1324    }
1325
1326    // Phase 1: Capture (root → target)
1327    propagate_phase(
1328        event,
1329        ancestors.iter().copied(),
1330        EventPhase::Capture,
1331        callbacks,
1332        &mut result,
1333    );
1334
1335    // Phase 2: Target
1336    if !event.stopped {
1337        propagate_target_phase(event, target_node_id, callbacks, &mut result);
1338    }
1339
1340    // Phase 3: Bubble (target → root) — unless the event type does not bubble.
1341    if !event.stopped && event.event_type.bubbles() {
1342        propagate_phase(
1343            event,
1344            ancestors.iter().rev().copied(),
1345            EventPhase::Bubble,
1346            callbacks,
1347            &mut result,
1348        );
1349    }
1350
1351    result.default_prevented = event.prevented_default;
1352    result
1353}
1354
1355impl EventType {
1356    /// Whether the event reaches the target's ANCESTORS in the bubble phase.
1357    ///
1358    /// Enter/leave events do not bubble (`W3C` `mouseenter`/`mouseleave`,
1359    /// `pointerenter`/`pointerleave`): each node that gained or lost hover
1360    /// gets its OWN event (`event_determination` generates them per node), so
1361    /// bubbling a child's leave to its parent tells the parent the pointer
1362    /// left IT while it is still inside — which is how a slider's drag ended
1363    /// the moment the pointer slid off the thumb, a map pan ended on every
1364    /// tile crossing, and a split-pane drag ended on its first motion.
1365    /// `dragenter`/`dragleave` DO bubble in the `W3C` model and keep doing so.
1366    #[must_use]
1367    pub const fn bubbles(self) -> bool {
1368        !matches!(
1369            self,
1370            Self::MouseEnter | Self::MouseLeave | Self::PenEnter | Self::PenLeave
1371        )
1372    }
1373}
1374
1375/// Process a single propagation phase (Capture or Bubble)
1376fn propagate_phase(
1377    event: &mut SyntheticEvent,
1378    nodes: impl Iterator<Item = NodeId>,
1379    phase: EventPhase,
1380    callbacks: &BTreeMap<NodeId, Vec<EventFilter>>,
1381    result: &mut PropagationResult,
1382) {
1383    event.phase = phase;
1384
1385    for node_id in nodes {
1386        if event.stopped_immediate || event.stopped {
1387            return;
1388        }
1389
1390        event.current_target = DomNodeId {
1391            dom: event.target.dom,
1392            node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
1393        };
1394
1395        collect_matching_callbacks(event, node_id, phase, callbacks, result);
1396    }
1397}
1398
1399/// Process the target phase
1400fn propagate_target_phase(
1401    event: &mut SyntheticEvent,
1402    target_node_id: NodeId,
1403    callbacks: &BTreeMap<NodeId, Vec<EventFilter>>,
1404    result: &mut PropagationResult,
1405) {
1406    event.phase = EventPhase::Target;
1407    event.current_target = event.target;
1408
1409    collect_matching_callbacks(event, target_node_id, EventPhase::Target, callbacks, result);
1410}
1411
1412/// Collect callbacks that match the current phase for a node
1413fn collect_matching_callbacks(
1414    event: &SyntheticEvent,
1415    node_id: NodeId,
1416    phase: EventPhase,
1417    callbacks: &BTreeMap<NodeId, Vec<EventFilter>>,
1418    result: &mut PropagationResult,
1419) {
1420    let Some(node_callbacks) = callbacks.get(&node_id) else {
1421        return;
1422    };
1423
1424    let matching = node_callbacks
1425        .iter()
1426        .take_while(|_| !event.stopped_immediate)
1427        .filter(|filter| matches_filter_phase(**filter, event, phase))
1428        .map(|filter| (node_id, *filter));
1429
1430    result.callbacks_to_invoke.extend(matching);
1431}
1432
1433// =============================================================================
1434// DEFAULT ACTIONS (W3C UI Events / HTML5 Activation Behavior)
1435// =============================================================================
1436
1437/// Default actions are built-in behaviors that occur in response to events.
1438///
1439/// Per W3C DOM Event specification:
1440/// > A default action is an action that the implementation is expected to take
1441/// > in response to an event, unless that action is cancelled by the script.
1442///
1443/// Examples:
1444/// - Tab key → move focus to next focusable element
1445/// - Enter/Space on button → activate (click) the button
1446/// - Escape → clear focus or close modal
1447/// - Arrow keys in listbox → move selection
1448///
1449/// Default actions are processed AFTER all event callbacks have been invoked,
1450/// and only if `event.prevent_default()` was NOT called.
1451#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1452#[repr(C, u8)]
1453pub enum DefaultAction {
1454    /// Move focus to the next focusable element (Tab key)
1455    FocusNext,
1456    /// Move focus to the previous focusable element (Shift+Tab)
1457    FocusPrevious,
1458    /// Move focus to the first focusable element
1459    FocusFirst,
1460    /// Move focus to the last focusable element
1461    FocusLast,
1462    /// Move focus SPATIALLY — to the nearest focusable element in that
1463    /// direction, rather than the next one in document order.
1464    ///
1465    /// Tab order is a single sequence; a D-pad, a TV remote and spatial
1466    /// navigation all ask a different question ("what is above this?"), which
1467    /// `FocusTarget::Directional` already answered and nothing could request.
1468    ///
1469    /// NOT bound to the arrow keys: those scroll, and taking them
1470    /// unconditionally would break every scroll container. That binding waits
1471    /// on the CSS opt-out (item 9a-i); a D-pad has no such conflict.
1472    FocusUp,
1473    FocusDown,
1474    FocusLeft,
1475    FocusRight,
1476    /// Clear focus from the currently focused element (Escape key)
1477    ClearFocus,
1478    /// Activate the focused element (Enter/Space on activatable elements)
1479    /// This generates a synthetic Click event on the target
1480    ActivateFocusedElement { target: DomNodeId },
1481    /// Submit the form containing the focused element (Enter in form input)
1482    SubmitForm { form_node: DomNodeId },
1483    /// Close the current modal/dialog (Escape key when modal is open)
1484    CloseModal { modal_node: DomNodeId },
1485    /// Scroll the focused scrollable container
1486    ScrollFocusedContainer {
1487        direction: ScrollDirection,
1488        amount: ScrollAmount,
1489    },
1490    /// Select all text in the focused text input (Ctrl+A / Cmd+A)
1491    SelectAllText,
1492    /// Enter in a contenteditable host: record a STRUCTURAL split-block
1493    /// changeset for the app to apply to its model (azul never mutates the
1494    /// DOM). Execution = `LayoutWindow::record_structural_default_action`.
1495    SplitBlockAtCursor { target: DomNodeId },
1496    /// Backspace at block start in a contenteditable host: record a
1497    /// merge-with-previous-block changeset (same record-only semantics).
1498    MergeWithPrevious { target: DomNodeId },
1499    /// Delete at block end in a contenteditable host: record a
1500    /// merge-with-next-block changeset (same record-only semantics).
1501    MergeWithNext { target: DomNodeId },
1502    /// No default action for this event
1503    None,
1504    /// Enter in a PLAIN-TEXT editing context (the editing host's computed
1505    /// `white-space` preserves newlines: pre / pre-wrap / break-spaces /
1506    /// pre-line), and Shift+Enter in ANY contenteditable host: insert a
1507    /// literal `"\n"` through the standard text-input pipeline (which brings
1508    /// veto, undo, and caret-follow along) instead of recording a structural
1509    /// block split. APPENDED at the enum tail for ABI stability.
1510    InsertLineBreakAtCursor { target: DomNodeId },
1511    /// Reset a form to its controls' default values (a `type="reset"` control
1512    /// was activated). APPENDED at the enum tail for ABI stability.
1513    ///
1514    /// The sibling of [`Self::SubmitForm`]: both name the FORM rather than the
1515    /// control that triggered them, because a reset handler belongs on the
1516    /// form and the button is an implementation detail of how the user asked.
1517    ResetForm { form_node: DomNodeId },
1518}
1519
1520/// Amount to scroll for keyboard-based scrolling
1521#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1522#[repr(C)]
1523pub enum ScrollAmount {
1524    /// Scroll by one line (arrow keys)
1525    Line,
1526    /// Scroll by one page (Page Up/Down)
1527    Page,
1528    /// Scroll to start/end (Home/End)
1529    Document,
1530}
1531
1532/// Result of determining what default action should occur for an event.
1533///
1534/// This is computed AFTER event dispatch, based on:
1535/// 1. The event type
1536/// 2. The target element's type/role
1537/// 3. Whether `prevent_default()` was called
1538#[derive(Debug, Clone, Copy)]
1539#[repr(C)]
1540pub struct DefaultActionResult {
1541    /// The default action to perform (if any)
1542    pub action: DefaultAction,
1543    /// Whether the action was prevented by a callback
1544    pub prevented: bool,
1545}
1546
1547impl Default for DefaultActionResult {
1548    fn default() -> Self {
1549        Self {
1550            action: DefaultAction::None,
1551            prevented: false,
1552        }
1553    }
1554}
1555
1556impl DefaultActionResult {
1557    /// Create a new result with a specific action
1558    #[must_use]
1559    pub const fn new(action: DefaultAction) -> Self {
1560        Self {
1561            action,
1562            prevented: false,
1563        }
1564    }
1565
1566    /// Create a prevented result (callback called `prevent_default`)
1567    #[must_use]
1568    pub const fn prevented() -> Self {
1569        Self {
1570            action: DefaultAction::None,
1571            prevented: true,
1572        }
1573    }
1574
1575    /// Check if there's an action to perform
1576    #[must_use]
1577    pub const fn has_action(&self) -> bool {
1578        !self.prevented && !matches!(self.action, DefaultAction::None)
1579    }
1580}
1581
1582/// Trait for elements that have activation behavior (can be "clicked" via keyboard).
1583///
1584/// Per HTML5 spec, elements with activation behavior include:
1585/// - `<button>` elements
1586/// - `<input type="submit">`, `<input type="button">`, `<input type="reset">`
1587/// - `<a>` elements with href
1588/// - `<area>` elements with href
1589/// - Any element with a click handler (implicit activation)
1590///
1591/// When an element with activation behavior is focused and the user presses
1592/// Enter or Space, a synthetic click event is generated.
1593pub trait ActivationBehavior {
1594    /// Returns true if this element can be activated via keyboard (Enter/Space)
1595    fn has_activation_behavior(&self) -> bool;
1596
1597    /// Returns true if this element is currently activatable
1598    /// (e.g., not disabled, not aria-disabled="true")
1599    fn is_activatable(&self) -> bool;
1600}
1601
1602/// Trait to query if a node is focusable for tab navigation
1603pub trait Focusable {
1604    /// Returns the tabindex value for this element (-1, 0, or positive)
1605    fn get_tabindex(&self) -> Option<i32>;
1606
1607    /// Returns true if this element can receive focus
1608    fn is_focusable(&self) -> bool;
1609
1610    /// Returns true if this element should be in the tab order
1611    fn is_in_tab_order(&self) -> bool {
1612        self.get_tabindex()
1613            .map_or_else(|| self.is_naturally_focusable(), |i| i >= 0)
1614    }
1615
1616    /// Returns true if this element type is naturally focusable
1617    /// (button, input, select, textarea, a[href])
1618    fn is_naturally_focusable(&self) -> bool;
1619}
1620
1621/// Check if an event filter matches the given event in the current phase.
1622///
1623/// This is used during event propagation to determine which callbacks
1624/// should be invoked at each phase.
1625fn matches_filter_phase(
1626    filter: EventFilter,
1627    event: &SyntheticEvent,
1628    current_phase: EventPhase,
1629) -> bool {
1630
1631    // azul has no capture-phase listeners (no `addEventListener(…, capture=true)`
1632    // equivalent): every `EventFilter` is a bubble-phase listener, which by the W3C
1633    // model fires only in the Target and Bubble phases — never Capture. Without this
1634    // guard an ancestor node's Hover/Focus callback was collected in BOTH the capture
1635    // and the bubble walk, so it fired TWICE whenever the hit target was a descendant
1636    // (e.g. a menubar item, hit via its text child, opened two stacked popups; any
1637    // button containing a text/child node ran its MouseUp callback twice).
1638    if matches!(current_phase, EventPhase::Capture) {
1639        return false;
1640    }
1641
1642    match filter {
1643        EventFilter::Hover(hover_filter) => {
1644            matches_hover_filter(hover_filter, event, current_phase)
1645        }
1646        EventFilter::Focus(focus_filter) => {
1647            matches_focus_filter(focus_filter, event, current_phase)
1648        }
1649        EventFilter::Window(window_filter) => {
1650            matches_window_filter(window_filter, event, current_phase)
1651        }
1652        EventFilter::Component(component_filter) => {
1653            matches_component_filter(component_filter, event, current_phase)
1654        }
1655        EventFilter::Application(application_filter) => {
1656            matches_application_filter(application_filter, event, current_phase)
1657        }
1658        EventFilter::External(external_filter) => {
1659            matches_external_filter(external_filter, event, current_phase)
1660        }
1661    }
1662}
1663
1664/// Check if a component (lifecycle) filter matches the event.
1665///
1666/// Lifecycle events produced by `diff::reconcile_dom` carry the target node in
1667/// `SyntheticEvent.target`, so dispatchers that bypass `propagate_event` and
1668/// invoke the target directly also need a way to compare. This predicate is
1669/// the single source of truth for that comparison; changing it without
1670/// updating `event_type_to_filters` will de-sync dispatch.
1671const fn matches_component_filter(
1672    filter: ComponentEventFilter,
1673    event: &SyntheticEvent,
1674    _phase: EventPhase,
1675) -> bool {
1676    matches!(
1677        (filter, &event.event_type),
1678        (ComponentEventFilter::AfterMount, EventType::Mount)
1679            | (ComponentEventFilter::BeforeUnmount, EventType::Unmount)
1680            | (ComponentEventFilter::Updated, EventType::Update)
1681            | (ComponentEventFilter::NodeResized, EventType::Resize)
1682            | (ComponentEventFilter::Dismissed, EventType::Dismiss)
1683            | (ComponentEventFilter::TornOff, EventType::TearOff)
1684            | (ComponentEventFilter::Docked, EventType::Dock)
1685            // These two were simply absent from the match. Both filter
1686            // variants shipped in `ComponentEventFilter`, so a component
1687            // subscribing to either was collected and then dropped.
1688            | (ComponentEventFilter::DefaultAction, EventType::DefaultAction)
1689            | (ComponentEventFilter::Selected, EventType::Selected)
1690    )
1691}
1692
1693/// Check if the event data contains a mouse event with the expected button.
1694/// `MouseButton::Other(BACK)` — the thumb "back" button, button 4 on a mouse
1695/// and index 3 zero-based, which is the numbering the web and every shell
1696/// already use (Win32 `XBUTTON1`, X11 button 8, macOS `otherMouseDown:` 3).
1697pub const MOUSE_BUTTON_BACK: u8 = 3;
1698/// `MouseButton::Other(FORWARD)` — the thumb "forward" button.
1699pub const MOUSE_BUTTON_FORWARD: u8 = 4;
1700
1701/// Bit for the back button in [`MouseState::other_down`].
1702pub const MOUSE_OTHER_MASK_BACK: u8 = 1 << 0;
1703/// Bit for the forward button in [`MouseState::other_down`].
1704pub const MOUSE_OTHER_MASK_FORWARD: u8 = 1 << 1;
1705
1706fn check_mouse_button(data: &EventData, expected: MouseButton) -> bool {
1707    if let EventData::Mouse(mouse_data) = data {
1708        mouse_data.button == expected
1709    } else {
1710        false
1711    }
1712}
1713
1714/// Check if a hover filter matches the event.
1715// Exhaustive (filter, event-type) truth table: many distinct pairs share the
1716// `=> true` body. One arm per pair is intentional; merging into giant or-patterns
1717// would destroy the table's readability/maintainability.
1718#[allow(clippy::match_same_arms)]
1719fn matches_hover_filter(
1720    filter: HoverEventFilter,
1721    event: &SyntheticEvent,
1722    _phase: EventPhase,
1723) -> bool {
1724    use HoverEventFilter::{
1725        BiometricResult, DoubleClick, Drag, DragEnd, DragEnter, DragLeave, DragOver, DragStart,
1726        Drop, DroppedFile, GamepadInput, GeolocationError, GeolocationFix, HoveredFile,
1727        HoveredFileCancelled, KeyringResult, LeftMouseDown, LeftMouseUp, MiddleMouseDown,
1728        MiddleMouseUp, MouseDown, MouseEnter, MouseLeave, MouseMove, MouseOver, MouseUp,
1729        PenDoubleTap, PenDown,
1730        PenEnter, PenHover, PenLeave, PenMove, PenSqueeze, PenUp, PermissionChanged, RightMouseDown,
1731        RightMouseUp,
1732        ScreenColorPicked, Scroll, ScrollEnd, ScrollStart, SensorChanged, TextInput, TouchCancel,
1733        TouchEnd, TouchMove, TouchStart, VirtualKeyDown, VirtualKeyUp,
1734    };
1735
1736    match (filter, &event.event_type) {
1737        (MouseOver, EventType::MouseOver) => true,
1738        (MouseMove, EventType::MouseMove) => true,
1739        (MouseDown, EventType::MouseDown) => true,
1740        (LeftMouseDown, EventType::MouseDown) => check_mouse_button(&event.data, MouseButton::Left),
1741        (RightMouseDown, EventType::MouseDown) => {
1742            check_mouse_button(&event.data, MouseButton::Right)
1743        }
1744        (MiddleMouseDown, EventType::MouseDown) => {
1745            check_mouse_button(&event.data, MouseButton::Middle)
1746        }
1747        (MouseUp, EventType::MouseUp) => true,
1748        // KEYBOARD ACTIVATION (Enter / Space on a focused element) dispatches
1749        // a synthetic `EventType::Click`. Every widget in the toolkit listens
1750        // on MouseUp / LeftMouseUp - the pointer spelling of "activate" - so
1751        // without this arm the synthetic event matched NOTHING and Enter /
1752        // Space silently did nothing on every focusable widget (device
1753        // report, 2026-08-31). Nothing else in the engine emits
1754        // `EventType::Click`, so this cannot double-fire a real pointer
1755        // click: a real click arrives as MouseDown + MouseUp.
1756        //
1757        // A keyboard activation counts as a LEFT activation - it carries no
1758        // button data, so `check_mouse_button` cannot answer for it - which
1759        // is the W3C activation behaviour Enter and Space implement.
1760        (HoverEventFilter::Click, EventType::Click) => true,
1761        (LeftMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Left),
1762        (RightMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Right),
1763        (MiddleMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Middle),
1764        (MouseEnter, EventType::MouseEnter) => true,
1765        (MouseLeave, EventType::MouseLeave) => true,
1766        (Scroll, EventType::Scroll) => true,
1767        (ScrollStart, EventType::ScrollStart) => true,
1768        (ScrollEnd, EventType::ScrollEnd) => true,
1769        (TextInput, EventType::Input) => true,
1770        // `KeyPress` (a key that produced a character) and `Change` (a value
1771        // committed on blur) both plan onto the `TextInput` filter, which is
1772        // what an app registers for "text arrived here". Planning named that
1773        // filter and this table then rejected it, so both dispatched to
1774        // nothing.
1775        (TextInput, EventType::KeyPress) => true,
1776        (TextInput, EventType::Change) => true,
1777        // A context-menu request is not a `MouseDown`, so the button-payload
1778        // check on the `RightMouseDown` arm cannot answer for it. Routing it
1779        // to the same filter means the Menu/Apps key, Shift+F10 and the
1780        // accessibility `ShowContextMenu` action all reach the handler an app
1781        // already registered for right-click, without inventing a new filter
1782        // variant for each spelling of "open the context menu".
1783        (RightMouseDown, EventType::ContextMenu) => true,
1784        (VirtualKeyDown, EventType::KeyDown) => true,
1785        (VirtualKeyUp, EventType::KeyUp) => true,
1786        (HoveredFile, EventType::FileHover) => true,
1787        (DroppedFile, EventType::FileDrop) => true,
1788        (HoveredFileCancelled, EventType::FileHoverCancel) => true,
1789        (TouchStart, EventType::TouchStart) => true,
1790        (TouchMove, EventType::TouchMove) => true,
1791        (TouchEnd, EventType::TouchEnd) => true,
1792        (TouchCancel, EventType::TouchCancel) => true,
1793        (PenDown, EventType::PenDown) => true,
1794        (PenMove, EventType::PenMove) => true,
1795        (PenUp, EventType::PenUp) => true,
1796        (PenEnter, EventType::PenEnter) => true,
1797        (PenLeave, EventType::PenLeave) => true,
1798        (DragStart, EventType::DragStart) => true,
1799        (Drag, EventType::Drag) => true,
1800        (DragEnd, EventType::DragEnd) => true,
1801        (DragEnter, EventType::DragEnter) => true,
1802        (DragOver, EventType::DragOver) => true,
1803        (DragLeave, EventType::DragLeave) => true,
1804        (Drop, EventType::Drop) => true,
1805        (DoubleClick, EventType::DoubleClick) => true,
1806        (SensorChanged, EventType::SensorChanged) => true,
1807        (GamepadInput, EventType::GamepadInput) => true,
1808        (GeolocationFix, EventType::GeolocationFix) => true,
1809        (GeolocationError, EventType::GeolocationError) => true,
1810        (PermissionChanged, EventType::PermissionChanged) => true,
1811        (BiometricResult, EventType::BiometricResult) => true,
1812        (ScreenColorPicked, EventType::ScreenColorPicked) => true,
1813        (KeyringResult, EventType::KeyringResult) => true,
1814        // Gestures. These filters existed, the detectors produced the events,
1815        // and this table had no arm for them — a `PinchOut` handler on a map
1816        // could never fire, whatever the gesture manager saw.
1817        (HoverEventFilter::LongPress, EventType::LongPress) => true,
1818        (HoverEventFilter::SwipeLeft, EventType::SwipeLeft) => true,
1819        (HoverEventFilter::SwipeRight, EventType::SwipeRight) => true,
1820        (HoverEventFilter::SwipeUp, EventType::SwipeUp) => true,
1821        (HoverEventFilter::SwipeDown, EventType::SwipeDown) => true,
1822        (HoverEventFilter::PinchIn, EventType::PinchIn) => true,
1823        (HoverEventFilter::PinchOut, EventType::PinchOut) => true,
1824        (HoverEventFilter::RotateClockwise, EventType::RotateClockwise) => true,
1825        (HoverEventFilter::RotateCounterClockwise, EventType::RotateCounterClockwise) => true,
1826        (HoverEventFilter::MouseOut, EventType::MouseOut) => true,
1827        (HoverEventFilter::FocusIn, EventType::FocusIn) => true,
1828        (HoverEventFilter::FocusOut, EventType::FocusOut) => true,
1829        (HoverEventFilter::CompositionStart, EventType::CompositionStart) => true,
1830        (HoverEventFilter::CompositionUpdate, EventType::CompositionUpdate) => true,
1831        (HoverEventFilter::CompositionEnd, EventType::CompositionEnd) => true,
1832        // Pen barrel gestures and hover. The filter variants shipped long
1833        // before any `EventType` existed that could reach them.
1834        (PenSqueeze, EventType::PenSqueeze) => true,
1835        (PenDoubleTap, EventType::PenDoubleTap) => true,
1836        (PenHover, EventType::PenHover) => true,
1837        // Thumb buttons. Gated on the payload like every other
1838        // button-specific arm — a payloadless event must not claim a button.
1839        (HoverEventFilter::BackMouseDown, EventType::MouseDown) => {
1840            check_mouse_button(&event.data, MouseButton::Other(MOUSE_BUTTON_BACK))
1841        }
1842        (HoverEventFilter::BackMouseUp, EventType::MouseUp) => {
1843            check_mouse_button(&event.data, MouseButton::Other(MOUSE_BUTTON_BACK))
1844        }
1845        (HoverEventFilter::ForwardMouseDown, EventType::MouseDown) => {
1846            check_mouse_button(&event.data, MouseButton::Other(MOUSE_BUTTON_FORWARD))
1847        }
1848        (HoverEventFilter::ForwardMouseUp, EventType::MouseUp) => {
1849            check_mouse_button(&event.data, MouseButton::Other(MOUSE_BUTTON_FORWARD))
1850        }
1851        (HoverEventFilter::Submit, EventType::Submit) => true,
1852        (HoverEventFilter::Change, EventType::Change) => true,
1853        (HoverEventFilter::Reset, EventType::Reset) => true,
1854        (HoverEventFilter::Invalid, EventType::Invalid) => true,
1855        // The dial. A NODE-scoped dial event only makes sense for a device
1856        // that reports a contact point (a Surface Dial ON the display); every
1857        // other dial reaches the app through the Window filter.
1858        (HoverEventFilter::DialRotate, EventType::DialRotate) => true,
1859        (HoverEventFilter::DialClick, EventType::DialClick) => true,
1860        _ => false,
1861    }
1862}
1863
1864/// Check if a focus filter matches the event.
1865// Exhaustive (filter, event-type) truth table — see matches_hover_filter.
1866#[allow(clippy::match_same_arms)]
1867fn matches_focus_filter(
1868    filter: FocusEventFilter,
1869    event: &SyntheticEvent,
1870    _phase: EventPhase,
1871) -> bool {
1872    use FocusEventFilter::{
1873        Drag, DragEnd, DragEnter, DragLeave, DragOver, DragStart, Drop, FocusLost, FocusReceived,
1874        LeftMouseDown, LeftMouseUp, MiddleMouseDown, MiddleMouseUp, MouseDown, MouseEnter,
1875        MouseLeave, MouseMove, MouseOver, MouseUp, RightMouseDown, RightMouseUp, Scroll, ScrollEnd,
1876        ScrollStart, TextInput, VirtualKeyDown, VirtualKeyUp,
1877    };
1878
1879    match (filter, &event.event_type) {
1880        (MouseOver, EventType::MouseOver) => true,
1881        (MouseMove, EventType::MouseMove) => true,
1882        (MouseDown, EventType::MouseDown) => true,
1883        (LeftMouseDown, EventType::MouseDown) => check_mouse_button(&event.data, MouseButton::Left),
1884        (RightMouseDown, EventType::MouseDown) => {
1885            check_mouse_button(&event.data, MouseButton::Right)
1886        }
1887        (MiddleMouseDown, EventType::MouseDown) => {
1888            check_mouse_button(&event.data, MouseButton::Middle)
1889        }
1890        (MouseUp, EventType::MouseUp) => true,
1891        (LeftMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Left),
1892        (RightMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Right),
1893        (MiddleMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Middle),
1894        (MouseEnter, EventType::MouseEnter) => true,
1895        (MouseLeave, EventType::MouseLeave) => true,
1896        (Scroll, EventType::Scroll) => true,
1897        (ScrollStart, EventType::ScrollStart) => true,
1898        (ScrollEnd, EventType::ScrollEnd) => true,
1899        (TextInput, EventType::Input) => true,
1900        (FocusEventFilter::DocumentEdit, EventType::DocumentEdit) => true,
1901        (FocusEventFilter::TextChanged, EventType::TextChanged) => true,
1902        (VirtualKeyDown, EventType::KeyDown) => true,
1903        (VirtualKeyUp, EventType::KeyUp) => true,
1904        (FocusReceived, EventType::Focus) => true,
1905        (FocusLost, EventType::Blur) => true,
1906        (DragStart, EventType::DragStart) => true,
1907        (Drag, EventType::Drag) => true,
1908        (DragEnd, EventType::DragEnd) => true,
1909        (DragEnter, EventType::DragEnter) => true,
1910        (DragOver, EventType::DragOver) => true,
1911        (DragLeave, EventType::DragLeave) => true,
1912        (Drop, EventType::Drop) => true,
1913        // MWA-C-clipboard: W3C clipboard events on the focused element
1914        // (qualified paths — `use FocusEventFilter::Copy` would shadow the
1915        // `Copy` trait in this scope).
1916        (FocusEventFilter::Copy, EventType::Copy) => true,
1917        (FocusEventFilter::Cut, EventType::Cut) => true,
1918        (FocusEventFilter::Paste, EventType::Paste) => true,
1919        // Gestures — same gap as the hover table.
1920        (FocusEventFilter::LongPress, EventType::LongPress) => true,
1921        (FocusEventFilter::SwipeLeft, EventType::SwipeLeft) => true,
1922        (FocusEventFilter::SwipeRight, EventType::SwipeRight) => true,
1923        (FocusEventFilter::SwipeUp, EventType::SwipeUp) => true,
1924        (FocusEventFilter::SwipeDown, EventType::SwipeDown) => true,
1925        (FocusEventFilter::PinchIn, EventType::PinchIn) => true,
1926        (FocusEventFilter::PinchOut, EventType::PinchOut) => true,
1927        (FocusEventFilter::RotateClockwise, EventType::RotateClockwise) => true,
1928        (FocusEventFilter::RotateCounterClockwise, EventType::RotateCounterClockwise) => true,
1929        // Pen — `FocusEventFilter` has carried Down/Move/Up since the filter
1930        // was introduced, but this table had no arm for any of them, so a
1931        // focused node could never receive a pen event even once dispatch
1932        // planning named the filter. (Enter/Leave have no Focus twin: a pen
1933        // crossing a node's bounds is not a focus change.)
1934        (FocusEventFilter::PenDown, EventType::PenDown) => true,
1935        (FocusEventFilter::PenMove, EventType::PenMove) => true,
1936        (FocusEventFilter::PenUp, EventType::PenUp) => true,
1937        // Thumb buttons. Gated on the payload like every other
1938        // button-specific arm — a payloadless event must not claim a button.
1939        (FocusEventFilter::BackMouseDown, EventType::MouseDown) => {
1940            check_mouse_button(&event.data, MouseButton::Other(MOUSE_BUTTON_BACK))
1941        }
1942        (FocusEventFilter::BackMouseUp, EventType::MouseUp) => {
1943            check_mouse_button(&event.data, MouseButton::Other(MOUSE_BUTTON_BACK))
1944        }
1945        (FocusEventFilter::ForwardMouseDown, EventType::MouseDown) => {
1946            check_mouse_button(&event.data, MouseButton::Other(MOUSE_BUTTON_FORWARD))
1947        }
1948        (FocusEventFilter::ForwardMouseUp, EventType::MouseUp) => {
1949            check_mouse_button(&event.data, MouseButton::Other(MOUSE_BUTTON_FORWARD))
1950        }
1951        (FocusEventFilter::Submit, EventType::Submit) => true,
1952        (FocusEventFilter::Change, EventType::Change) => true,
1953        (FocusEventFilter::Reset, EventType::Reset) => true,
1954        (FocusEventFilter::Invalid, EventType::Invalid) => true,
1955        _ => false,
1956    }
1957}
1958
1959/// Check if an application filter matches the event.
1960///
1961/// Application events are not hit-tested and have no meaningful target: a
1962/// device arriving or a monitor being unplugged belongs to the application,
1963/// not to whichever node happened to be under the cursor. They are dispatched
1964/// at the root and reach any subscriber by propagation, which is why this
1965/// table ignores the phase exactly as the window table does.
1966///
1967/// This arm returned a flat `false` — "will be implemented in future" — so all
1968/// four variants were structurally unreachable no matter what a shell emitted.
1969/// Every other layer was already in place: `event_type_to_filters` had named
1970/// `EF::Application(..)` for the monitor pair the whole time.
1971// Exhaustive (filter, event-type) truth table — see matches_hover_filter.
1972#[allow(clippy::match_same_arms)]
1973fn matches_application_filter(
1974    filter: ApplicationEventFilter,
1975    event: &SyntheticEvent,
1976    _phase: EventPhase,
1977) -> bool {
1978    use ApplicationEventFilter::{
1979        DeviceConnected, DeviceDisconnected, MediaControl, MonitorConnected, MonitorDisconnected,
1980        SystemAudioChange,
1981    };
1982
1983    match (filter, &event.event_type) {
1984        (MediaControl, EventType::MediaControl) => true,
1985        (SystemAudioChange, EventType::SystemAudioChange) => true,
1986        (DeviceConnected, EventType::DeviceConnected) => true,
1987        (DeviceDisconnected, EventType::DeviceDisconnected) => true,
1988        (MonitorConnected, EventType::MonitorConnected) => true,
1989        (MonitorDisconnected, EventType::MonitorDisconnected) => true,
1990        _ => false,
1991    }
1992}
1993
1994/// Check if an external filter matches the event (11c).
1995///
1996/// Media events are not hit-tested: the player is not a device with a
1997/// position, so there is no node "under" a `TimeUpdate`. Like the window and
1998/// application tables this therefore ignores the phase; the event's `target`
1999/// still names the media node, for callbacks that serve more than one.
2000// Exhaustive (filter, event-type) truth table — see matches_hover_filter.
2001#[allow(clippy::match_same_arms)]
2002const fn matches_external_filter(
2003    filter: ExternalEventFilter,
2004    event: &SyntheticEvent,
2005    _phase: EventPhase,
2006) -> bool {
2007    matches!(
2008        (filter, &event.event_type),
2009        (ExternalEventFilter::Play, EventType::Play)
2010            | (ExternalEventFilter::Pause, EventType::Pause)
2011            | (ExternalEventFilter::Ended, EventType::Ended)
2012            | (ExternalEventFilter::TimeUpdate, EventType::TimeUpdate)
2013            | (ExternalEventFilter::VolumeChange, EventType::VolumeChange)
2014            | (ExternalEventFilter::MediaError, EventType::MediaError)
2015    )
2016}
2017
2018/// Check if a window filter matches the event.
2019// Exhaustive (filter, event-type) truth table — see matches_hover_filter.
2020#[allow(clippy::match_same_arms)]
2021fn matches_window_filter(
2022    filter: WindowEventFilter,
2023    event: &SyntheticEvent,
2024    _phase: EventPhase,
2025) -> bool {
2026    use WindowEventFilter::{
2027        BiometricResult, CloseRequested, Drag, DragEnd, DragEnter, DragLeave, DragOver, DragStart,
2028        Drop, DroppedFile, FocusLost, FocusReceived, FrameChanged, GamepadInput, GeolocationError,
2029        GeolocationFix, HoveredFile, HoveredFileCancelled, KeyringResult, LeftMouseDown,
2030        LeftMouseUp, MiddleMouseDown, MiddleMouseUp, MouseDown, MouseEnter, MouseLeave, MouseMove,
2031        MouseOver,
2032        MouseUp, Moved, PenDoubleTap, PenDown, PenEnter, PenHover, PenLeave, PenMove, PenSqueeze,
2033        ModifiersChanged, PenUp, PermissionChanged, PointerLockChange, RawMouseMotion, Resized,
2034        RightMouseDown, RightMouseUp, ScreenColorPicked, Scroll, ScrollEnd, ScrollStart,
2035        SensorChanged, TextInput, ThemeChanged, TouchCancel, TouchEnd, TouchMove, TouchStart,
2036        VirtualKeyDown, VirtualKeyUp, WindowFocusLost, WindowFocusReceived,
2037    };
2038
2039    match (filter, &event.event_type) {
2040        (MouseOver, EventType::MouseOver) => true,
2041        (MouseMove, EventType::MouseMove) => true,
2042        (MouseDown, EventType::MouseDown) => true,
2043        (LeftMouseDown, EventType::MouseDown) => check_mouse_button(&event.data, MouseButton::Left),
2044        (RightMouseDown, EventType::MouseDown) => {
2045            check_mouse_button(&event.data, MouseButton::Right)
2046        }
2047        (MiddleMouseDown, EventType::MouseDown) => {
2048            check_mouse_button(&event.data, MouseButton::Middle)
2049        }
2050        (MouseUp, EventType::MouseUp) => true,
2051        (LeftMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Left),
2052        (RightMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Right),
2053        (MiddleMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Middle),
2054        (MouseEnter, EventType::MouseEnter) => true,
2055        (MouseLeave, EventType::MouseLeave) => true,
2056        (Scroll, EventType::Scroll) => true,
2057        (ScrollStart, EventType::ScrollStart) => true,
2058        (ScrollEnd, EventType::ScrollEnd) => true,
2059        (TextInput, EventType::Input) => true,
2060        // `KeyPress` (a key that produced a character) and `Change` (a value
2061        // committed on blur) both plan onto the `TextInput` filter, which is
2062        // what an app registers for "text arrived here". Planning named that
2063        // filter and this table then rejected it, so both dispatched to
2064        // nothing.
2065        (TextInput, EventType::KeyPress) => true,
2066        (TextInput, EventType::Change) => true,
2067        // A context-menu request is not a `MouseDown`, so the button-payload
2068        // check on the `RightMouseDown` arm cannot answer for it. Routing it
2069        // to the same filter means the Menu/Apps key, Shift+F10 and the
2070        // accessibility `ShowContextMenu` action all reach the handler an app
2071        // already registered for right-click, without inventing a new filter
2072        // variant for each spelling of "open the context menu".
2073        (RightMouseDown, EventType::ContextMenu) => true,
2074        (VirtualKeyDown, EventType::KeyDown) => true,
2075        (VirtualKeyUp, EventType::KeyUp) => true,
2076        (HoveredFile, EventType::FileHover) => true,
2077        (DroppedFile, EventType::FileDrop) => true,
2078        (HoveredFileCancelled, EventType::FileHoverCancel) => true,
2079        (Resized, EventType::WindowResize) => true,
2080        (FrameChanged, EventType::WindowFrameChanged) => true,
2081        (Moved, EventType::WindowMove) => true,
2082        (TouchStart, EventType::TouchStart) => true,
2083        (TouchMove, EventType::TouchMove) => true,
2084        (TouchEnd, EventType::TouchEnd) => true,
2085        (TouchCancel, EventType::TouchCancel) => true,
2086        (PenDown, EventType::PenDown) => true,
2087        (PenMove, EventType::PenMove) => true,
2088        (PenUp, EventType::PenUp) => true,
2089        (PenEnter, EventType::PenEnter) => true,
2090        (PenLeave, EventType::PenLeave) => true,
2091        (FocusReceived, EventType::Focus) => true,
2092        (FocusLost, EventType::Blur) => true,
2093        (CloseRequested, EventType::WindowClose) => true,
2094        (ThemeChanged, EventType::ThemeChange) => true,
2095        (WindowFocusReceived, EventType::WindowFocusIn) => true,
2096        (WindowFocusLost, EventType::WindowFocusOut) => true,
2097        (PointerLockChange, EventType::PointerLockChange) => true,
2098        (SensorChanged, EventType::SensorChanged) => true,
2099        (GamepadInput, EventType::GamepadInput) => true,
2100        (GeolocationFix, EventType::GeolocationFix) => true,
2101        (GeolocationError, EventType::GeolocationError) => true,
2102        (PermissionChanged, EventType::PermissionChanged) => true,
2103        (BiometricResult, EventType::BiometricResult) => true,
2104        (ScreenColorPicked, EventType::ScreenColorPicked) => true,
2105        (KeyringResult, EventType::KeyringResult) => true,
2106        (DragStart, EventType::DragStart) => true,
2107        (Drag, EventType::Drag) => true,
2108        (DragEnd, EventType::DragEnd) => true,
2109        (DragEnter, EventType::DragEnter) => true,
2110        (DragOver, EventType::DragOver) => true,
2111        (DragLeave, EventType::DragLeave) => true,
2112        (Drop, EventType::Drop) => true,
2113        // Gestures — same gap as the hover table.
2114        (WindowEventFilter::LongPress, EventType::LongPress) => true,
2115        (WindowEventFilter::SwipeLeft, EventType::SwipeLeft) => true,
2116        (WindowEventFilter::SwipeRight, EventType::SwipeRight) => true,
2117        (WindowEventFilter::SwipeUp, EventType::SwipeUp) => true,
2118        (WindowEventFilter::SwipeDown, EventType::SwipeDown) => true,
2119        (WindowEventFilter::PinchIn, EventType::PinchIn) => true,
2120        (WindowEventFilter::PinchOut, EventType::PinchOut) => true,
2121        (WindowEventFilter::RotateClockwise, EventType::RotateClockwise) => true,
2122        (WindowEventFilter::RotateCounterClockwise, EventType::RotateCounterClockwise) => true,
2123        // Pen barrel gestures and hover. The filter variants shipped long
2124        // before any `EventType` existed that could reach them.
2125        (PenSqueeze, EventType::PenSqueeze) => true,
2126        (PenDoubleTap, EventType::PenDoubleTap) => true,
2127        (PenHover, EventType::PenHover) => true,
2128        // Thumb buttons. Gated on the payload like every other
2129        // button-specific arm — a payloadless event must not claim a button.
2130        (WindowEventFilter::BackMouseDown, EventType::MouseDown) => {
2131            check_mouse_button(&event.data, MouseButton::Other(MOUSE_BUTTON_BACK))
2132        }
2133        (WindowEventFilter::BackMouseUp, EventType::MouseUp) => {
2134            check_mouse_button(&event.data, MouseButton::Other(MOUSE_BUTTON_BACK))
2135        }
2136        (WindowEventFilter::ForwardMouseDown, EventType::MouseDown) => {
2137            check_mouse_button(&event.data, MouseButton::Other(MOUSE_BUTTON_FORWARD))
2138        }
2139        (WindowEventFilter::ForwardMouseUp, EventType::MouseUp) => {
2140            check_mouse_button(&event.data, MouseButton::Other(MOUSE_BUTTON_FORWARD))
2141        }
2142        (RawMouseMotion, EventType::RawMouseMotion) => true,
2143        (ModifiersChanged, EventType::ModifiersChanged) => true,
2144        (WindowEventFilter::HidReport, EventType::HidReport) => true,
2145        (WindowEventFilter::DialRotate, EventType::DialRotate) => true,
2146        (WindowEventFilter::DialClick, EventType::DialClick) => true,
2147        // Media (11c). These six `EventType`s shipped with no filter in ANY
2148        // family, so they planned an empty list and dispatched to nothing.
2149        (WindowEventFilter::Play, EventType::Play) => true,
2150        (WindowEventFilter::Pause, EventType::Pause) => true,
2151        (WindowEventFilter::Ended, EventType::Ended) => true,
2152        (WindowEventFilter::TimeUpdate, EventType::TimeUpdate) => true,
2153        (WindowEventFilter::VolumeChange, EventType::VolumeChange) => true,
2154        (WindowEventFilter::MediaError, EventType::MediaError) => true,
2155        _ => false,
2156    }
2157}
2158
2159/// Detect lifecycle events by comparing old and new DOM state.
2160///
2161/// This is the simple, index-based lifecycle detection that doesn't account for
2162/// node reordering. For more sophisticated reconciliation that can detect moves,
2163/// use `detect_lifecycle_events_with_reconciliation`.
2164///
2165/// Generates Mount, Unmount, and Resize events by comparing DOM hierarchies.
2166#[allow(clippy::needless_pass_by_value)] // owned azul value taken by value (public API / ownership-transfer convention)
2167#[must_use]
2168pub fn detect_lifecycle_events(
2169    old_dom_id: DomId,
2170    new_dom_id: DomId,
2171    old_hierarchy: Option<&crate::id::NodeHierarchy>,
2172    new_hierarchy: Option<&crate::id::NodeHierarchy>,
2173    old_layout: Option<&BTreeMap<NodeId, LogicalRect>>,
2174    new_layout: Option<&BTreeMap<NodeId, LogicalRect>>,
2175    timestamp: Instant,
2176) -> Vec<SyntheticEvent> {
2177    let old_nodes = collect_node_ids(old_hierarchy);
2178    let new_nodes = collect_node_ids(new_hierarchy);
2179
2180    let mut events = Vec::new();
2181
2182    // Mount events: nodes in new but not in old
2183    if let Some(layout) = new_layout {
2184        for &node_id in new_nodes.difference(&old_nodes) {
2185            events.push(create_mount_event(node_id, new_dom_id, layout, &timestamp));
2186        }
2187    }
2188
2189    // Unmount events: nodes in old but not in new
2190    if let Some(layout) = old_layout {
2191        for &node_id in old_nodes.difference(&new_nodes) {
2192            events.push(create_unmount_event(
2193                node_id, old_dom_id, layout, &timestamp,
2194            ));
2195        }
2196    }
2197
2198    // Resize events: nodes in both with changed bounds
2199    if let (Some(old_l), Some(new_l)) = (old_layout, new_layout) {
2200        for &node_id in old_nodes.intersection(&new_nodes) {
2201            if let Some(ev) = create_resize_event(node_id, new_dom_id, old_l, new_l, &timestamp) {
2202                events.push(ev);
2203            }
2204        }
2205    }
2206
2207    events
2208}
2209
2210fn collect_node_ids(hierarchy: Option<&crate::id::NodeHierarchy>) -> BTreeSet<NodeId> {
2211    hierarchy
2212        .map(|h| h.as_ref().linear_iter().collect())
2213        .unwrap_or_default()
2214}
2215
2216fn create_lifecycle_event(
2217    event_type: EventType,
2218    node_id: NodeId,
2219    dom_id: DomId,
2220    timestamp: &Instant,
2221    data: LifecycleEventData,
2222) -> SyntheticEvent {
2223    let dom_node_id = DomNodeId {
2224        dom: dom_id,
2225        node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
2226    };
2227    SyntheticEvent {
2228        event_type,
2229        source: EventSource::Lifecycle,
2230        phase: EventPhase::Target,
2231        target: dom_node_id,
2232        current_target: dom_node_id,
2233        timestamp: timestamp.clone(),
2234        data: EventData::Lifecycle(data),
2235        stopped: false,
2236        stopped_immediate: false,
2237        prevented_default: false,
2238        at_target_only: false,
2239    }
2240}
2241
2242fn create_mount_event(
2243    node_id: NodeId,
2244    dom_id: DomId,
2245    layout: &BTreeMap<NodeId, LogicalRect>,
2246    timestamp: &Instant,
2247) -> SyntheticEvent {
2248    let current_bounds = layout.get(&node_id).copied().unwrap_or(LogicalRect::zero());
2249    create_lifecycle_event(
2250        EventType::Mount,
2251        node_id,
2252        dom_id,
2253        timestamp,
2254        LifecycleEventData {
2255            reason: LifecycleReason::InitialMount,
2256            previous_bounds: None,
2257            current_bounds,
2258        },
2259    )
2260}
2261
2262fn create_unmount_event(
2263    node_id: NodeId,
2264    dom_id: DomId,
2265    layout: &BTreeMap<NodeId, LogicalRect>,
2266    timestamp: &Instant,
2267) -> SyntheticEvent {
2268    let previous_bounds = layout.get(&node_id).copied().unwrap_or(LogicalRect::zero());
2269    create_lifecycle_event(
2270        EventType::Unmount,
2271        node_id,
2272        dom_id,
2273        timestamp,
2274        LifecycleEventData {
2275            reason: LifecycleReason::Unmount,
2276            previous_bounds: Some(previous_bounds),
2277            current_bounds: LogicalRect::zero(),
2278        },
2279    )
2280}
2281
2282/// Returns `true` iff the two logical sizes differ after fixed-point
2283/// quantization (~0.001 tolerance), treating a dimension that is NaN on *both*
2284/// sides as unchanged so a degenerate layout cannot emit a Resize every frame.
2285fn size_changed(old: crate::geom::LogicalSize, new: crate::geom::LogicalSize) -> bool {
2286    fn dim_changed(a: f32, b: f32) -> bool {
2287        if a.is_nan() && b.is_nan() {
2288            return false;
2289        }
2290        // Fixed-point quantization mirrors `LogicalSize`'s `Ord`/`Hash`.
2291        // `f32 as i64` saturates on overflow (no wasm32 wraparound); a lone
2292        // NaN quantizes to `i64::MIN` and so registers as changed.
2293        #[allow(clippy::cast_possible_truncation)]
2294        // intentional fixed-point quantization; saturates
2295        let q = |v: f32| -> i64 {
2296            if v.is_nan() {
2297                i64::MIN
2298            } else {
2299                (v * 1000.0) as i64
2300            }
2301        };
2302        q(a) != q(b)
2303    }
2304    dim_changed(old.width, new.width) || dim_changed(old.height, new.height)
2305}
2306
2307fn create_resize_event(
2308    node_id: NodeId,
2309    dom_id: DomId,
2310    old_layout: &BTreeMap<NodeId, LogicalRect>,
2311    new_layout: &BTreeMap<NodeId, LogicalRect>,
2312    timestamp: &Instant,
2313) -> Option<SyntheticEvent> {
2314    let old_bounds = *old_layout.get(&node_id)?;
2315    let new_bounds = *new_layout.get(&node_id)?;
2316
2317    // Quantized/tolerance compare with an explicit NaN guard. A raw `==` on
2318    // `LogicalSize` used to compare f32 bit patterns, so a single NaN dimension
2319    // made `old != new` true *every frame forever* -> an endless Resize-event
2320    // loop. `size_changed` treats a NaN dimension present on both sides as
2321    // "unchanged" and otherwise compares fixed-point-quantized values.
2322    if !size_changed(old_bounds.size, new_bounds.size) {
2323        return None;
2324    }
2325
2326    Some(create_lifecycle_event(
2327        EventType::Resize,
2328        node_id,
2329        dom_id,
2330        timestamp,
2331        LifecycleEventData {
2332            reason: LifecycleReason::Resize,
2333            previous_bounds: Some(old_bounds),
2334            current_bounds: new_bounds,
2335        },
2336    ))
2337}
2338
2339/// A `Resize` lifecycle event (`ComponentEventFilter::NodeResized`) for
2340/// `node_id` whose layout box went from `old` to `new`.
2341///
2342/// `None` when its SIZE did not change: a position-only move is not a
2343/// resize, and a NaN dimension present on both sides reads as unchanged (see
2344/// `size_changed`; a raw `!=` once produced a Resize every frame forever).
2345///
2346/// This is the constructor the layout tail uses after EVERY solve (full
2347/// rebuild, pre-cascade relayout, window-resize fast path). The older
2348/// reconcile-time emitter compared layout maps that production always
2349/// passed EMPTY, so `NodeResized` had never fired in a running app.
2350#[must_use]
2351pub fn resize_event_for_bounds(
2352    dom_id: DomId,
2353    node_id: NodeId,
2354    old: LogicalRect,
2355    new: LogicalRect,
2356    timestamp: &Instant,
2357) -> Option<SyntheticEvent> {
2358    if !size_changed(old.size, new.size) {
2359        return None;
2360    }
2361    Some(create_lifecycle_event(
2362        EventType::Resize,
2363        node_id,
2364        dom_id,
2365        timestamp,
2366        LifecycleEventData {
2367            reason: LifecycleReason::Resize,
2368            previous_bounds: Some(old),
2369            current_bounds: new,
2370        },
2371    ))
2372}
2373
2374/// Result of lifecycle event detection with reconciliation.
2375///
2376/// Contains both the generated lifecycle events and a mapping from old to new
2377/// node IDs for state migration (focus, scroll, etc.).
2378#[derive(Debug, Clone, Default)]
2379pub struct LifecycleEventResult {
2380    /// Lifecycle events (Mount, Unmount, Resize, Update)
2381    pub events: Vec<SyntheticEvent>,
2382    /// Maps old `NodeId` -> new `NodeId` for matched nodes.
2383    /// Use this to migrate focus, scroll state, and other node-specific state.
2384    pub node_id_mapping: OrderedMap<NodeId, NodeId>,
2385}
2386
2387/// Detect lifecycle events using reconciliation with stable keys and content hashing.
2388///
2389/// This is the advanced lifecycle detection that can correctly identify:
2390/// - **Moves**: When a node changes position but keeps its identity (via key or hash)
2391/// - **Mounts**: When a new node appears
2392/// - **Unmounts**: When an existing node disappears
2393/// - **Resizes**: When a node's layout bounds change
2394/// - **Updates**: When a keyed node's content changes
2395///
2396/// The reconciliation strategy is:
2397/// 1. **Stable Key Match:** Nodes with `.with_reconciliation_key()` are matched by key (O(1))
2398/// 2. **Hash Match:** Nodes without keys are matched by content hash (enables reorder detection)
2399/// 3. **Fallback:** Unmatched nodes generate Mount/Unmount events
2400///
2401/// # Arguments
2402/// * `dom_id` - The DOM identifier
2403/// * `old_node_data` - Node data from the previous frame
2404/// * `new_node_data` - Node data from the current frame
2405/// * `old_layout` - Layout bounds from the previous frame
2406/// * `new_layout` - Layout bounds from the current frame
2407/// * `timestamp` - Current timestamp for events
2408///
2409/// # Returns
2410/// A `LifecycleEventResult` containing:
2411/// - `events`: Lifecycle events to dispatch
2412/// - `node_id_mapping`: Mapping from old to new `NodeIds` for state migration
2413///
2414/// # Example
2415/// ```rust,ignore
2416/// let result = detect_lifecycle_events_with_reconciliation(
2417///     dom_id,
2418///     &old_node_data,
2419///     &new_node_data,
2420///     &old_layout,
2421///     &new_layout,
2422///     timestamp,
2423/// );
2424///
2425/// // Dispatch lifecycle events
2426/// for event in result.events {
2427///     dispatch_event(event);
2428/// }
2429///
2430/// // Migrate focus to new node ID
2431/// if let Some(focused) = focus_manager.focused_node {
2432///     if let Some(&new_id) = result.node_id_mapping.get(&focused) {
2433///         focus_manager.focused_node = Some(new_id);
2434///     } else {
2435///         // Focused node was unmounted
2436///         focus_manager.focused_node = None;
2437///     }
2438/// }
2439/// ```
2440#[must_use]
2441pub fn detect_lifecycle_events_with_reconciliation(
2442    dom_id: DomId,
2443    old_node_data: &[crate::dom::NodeData],
2444    new_node_data: &[crate::dom::NodeData],
2445    old_hierarchy: &[crate::styled_dom::NodeHierarchyItem],
2446    new_hierarchy: &[crate::styled_dom::NodeHierarchyItem],
2447    old_layout: &OrderedMap<NodeId, LogicalRect>,
2448    new_layout: &OrderedMap<NodeId, LogicalRect>,
2449    timestamp: Instant,
2450) -> LifecycleEventResult {
2451    let diff_result = crate::diff::reconcile_dom(
2452        old_node_data,
2453        new_node_data,
2454        old_hierarchy,
2455        new_hierarchy,
2456        old_layout,
2457        new_layout,
2458        dom_id,
2459        timestamp,
2460    );
2461
2462    LifecycleEventResult {
2463        events: diff_result.events,
2464        node_id_mapping: crate::diff::create_migration_map(&diff_result.node_moves),
2465    }
2466}
2467
2468/// Event filter that only fires when an element is hovered over.
2469#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
2470#[repr(C)]
2471pub enum HoverEventFilter {
2472    /// Mouse moved over the hovered element
2473    MouseOver,
2474    /// Any mouse button pressed on the hovered element
2475    MouseDown,
2476    /// Left mouse button pressed on the hovered element
2477    LeftMouseDown,
2478    /// Right mouse button pressed on the hovered element
2479    RightMouseDown,
2480    /// Middle mouse button pressed on the hovered element
2481    MiddleMouseDown,
2482    /// A completed ACTIVATION on this element: press and release on the same
2483    /// node, per W3C `click`. This - not `MouseUp` - is what "the user
2484    /// activated this control" means, and it is what Enter/Space on a focused
2485    /// element and an assistive technology's default action also dispatch.
2486    /// `MouseUp` stays the raw pointer-release event.
2487    Click,
2488    /// Any mouse button released on the hovered element
2489    MouseUp,
2490    /// Left mouse button released on the hovered element
2491    LeftMouseUp,
2492    /// Right mouse button released on the hovered element
2493    RightMouseUp,
2494    /// Middle mouse button released on the hovered element
2495    MiddleMouseUp,
2496    /// Mouse entered the hovered element bounds
2497    MouseEnter,
2498    /// Mouse left the hovered element bounds
2499    MouseLeave,
2500    /// Scroll event on the hovered element
2501    Scroll,
2502    /// Scroll started on the hovered element
2503    ScrollStart,
2504    /// Scroll ended on the hovered element
2505    ScrollEnd,
2506    /// Text input received while element is hovered
2507    TextInput,
2508    /// Virtual key pressed while element is hovered
2509    VirtualKeyDown,
2510    /// Virtual key released while element is hovered
2511    VirtualKeyUp,
2512    /// File is being hovered over the element
2513    HoveredFile,
2514    /// File was dropped onto the element
2515    DroppedFile,
2516    /// File hover was cancelled
2517    HoveredFileCancelled,
2518    /// Touch started on the hovered element
2519    TouchStart,
2520    /// Touch moved on the hovered element
2521    TouchMove,
2522    /// Touch ended on the hovered element
2523    TouchEnd,
2524    /// Touch was cancelled on the hovered element
2525    TouchCancel,
2526    /// Pen/stylus made contact on the hovered element
2527    PenDown,
2528    /// Pen/stylus moved while in contact on the hovered element
2529    PenMove,
2530    /// Pen/stylus lifted from the hovered element
2531    PenUp,
2532    /// Pen/stylus entered proximity of the hovered element
2533    PenEnter,
2534    /// Pen/stylus left proximity of the hovered element
2535    PenLeave,
2536    /// Apple Pencil 2 / Surface Slim Pen 2 barrel squeeze on the hovered
2537    /// element. Fires once per squeeze. The matching W3C primitive is the
2538    /// `PointerEvent` with `pointerType: "pen"` and a transient
2539    /// `tangentialPressure` spike — most apps tie a tool-switch to it.
2540    PenSqueeze,
2541    /// Apple Pencil 2 side double-tap on the hovered element. Fires once
2542    /// per gesture. Usually mapped to "undo" or "switch eraser".
2543    PenDoubleTap,
2544    /// Pen/stylus is hovering above the hovered element (in proximity,
2545    /// not in contact). Continuous: fires per pen-axis update while the
2546    /// stylus is held above the surface. Maps to W3C
2547    /// `PointerEvent('pointermove')` with `buttons: 0` and
2548    /// `pointerType: 'pen'`.
2549    PenHover,
2550    /// New GPS / network location fix arrived for a `GeolocationProbe`
2551    /// in this node's subtree. Payload accessor:
2552    /// `CallbackInfo::get_geolocation_fix()`.
2553    GeolocationFix,
2554    /// Native geolocation subscription errored / was revoked /
2555    /// timed out.
2556    GeolocationError,
2557    /// A motion-sensor reading changed (P6). Window-level mirror:
2558    /// `WindowEventFilter::SensorChanged`. Read via `get_sensor_reading`.
2559    SensorChanged,
2560    /// A gamepad's state changed / it (dis)connected (P6). Read via
2561    /// `get_primary_gamepad` / `get_gamepad_state`.
2562    GamepadInput,
2563    /// Drag started on the hovered element
2564    DragStart,
2565    /// Drag in progress on the hovered element
2566    Drag,
2567    /// Drag ended on the hovered element
2568    DragEnd,
2569    /// Dragged element entered this element (drop target)
2570    DragEnter,
2571    /// Dragged element is over this element (drop target, fires continuously)
2572    DragOver,
2573    /// Dragged element left this element (drop target)
2574    DragLeave,
2575    /// Element was dropped on this element (drop target)
2576    Drop,
2577    /// Double-click detected on the hovered element
2578    DoubleClick,
2579    /// Long press detected on the hovered element
2580    LongPress,
2581    /// Swipe left gesture on the hovered element
2582    SwipeLeft,
2583    /// Swipe right gesture on the hovered element
2584    SwipeRight,
2585    /// Swipe up gesture on the hovered element
2586    SwipeUp,
2587    /// Swipe down gesture on the hovered element
2588    SwipeDown,
2589    /// Pinch-in (zoom out) gesture on the hovered element
2590    PinchIn,
2591    /// Pinch-out (zoom in) gesture on the hovered element
2592    PinchOut,
2593    /// Clockwise rotation gesture on the hovered element
2594    RotateClockwise,
2595    /// Counter-clockwise rotation gesture on the hovered element
2596    RotateCounterClockwise,
2597
2598    // W3C MouseOut event (bubbling version of MouseLeave)
2599    /// Mouse left the element OR moved to a child element (W3C `mouseout`, bubbles)
2600    MouseOut,
2601
2602    // W3C Focus events (bubbling versions)
2603    /// Focus is about to move INTO this element or a descendant (W3C `focusin`, bubbles)
2604    FocusIn,
2605    /// Focus is about to move OUT of this element or a descendant (W3C `focusout`, bubbles)
2606    FocusOut,
2607
2608    // IME Composition events
2609    /// IME composition started (W3C `compositionstart`)
2610    CompositionStart,
2611    /// IME composition updated (W3C `compositionupdate`)
2612    CompositionUpdate,
2613    /// IME composition ended (W3C `compositionend`)
2614    CompositionEnd,
2615
2616    // Internal System Events (not exposed to user callbacks)
2617    #[doc(hidden)]
2618    /// Internal: Single click for text cursor placement
2619    SystemTextSingleClick,
2620    #[doc(hidden)]
2621    /// Internal: Double click for word selection
2622    SystemTextDoubleClick,
2623    #[doc(hidden)]
2624    /// Internal: Triple click for paragraph/line selection
2625    SystemTextTripleClick,
2626
2627    // Async capability outcomes (MWA-A1b)
2628    /// A permission's OS-observed state changed while this node (the
2629    /// capability's most recent subscriber) is in the target chain.
2630    PermissionChanged,
2631    /// A biometric authentication prompt completed.
2632    BiometricResult,
2633    /// The screen eyedropper finished (picked or cancelled).
2634    ScreenColorPicked,
2635    /// A keyring store / get / delete operation completed.
2636    KeyringResult,
2637    /// The thumb "back" button was pressed. APPENDED at the end for ABI
2638    /// stability.
2639    ///
2640    /// Every shell already routes this button — Win32 `WM_XBUTTONDOWN`, macOS
2641    /// `otherMouseDown:`, X11 button 8 — into `MouseButton::Other(3)`, and it
2642    /// died there because no filter named it. Nearly every mouse sold in a
2643    /// decade has the pair, and browsers, IDEs and file managers all bind them
2644    /// to navigate back/forward.
2645    BackMouseDown,
2646    /// The thumb "back" button was released.
2647    BackMouseUp,
2648    /// The thumb "forward" button was pressed.
2649    ForwardMouseDown,
2650    /// The thumb "forward" button was released.
2651    ForwardMouseUp,
2652    /// A form was submitted — Enter on a focused control, or a submit
2653    /// button activated. APPENDED at the end for ABI stability.
2654    Submit,
2655    /// A control's value was COMMITTED, which is not the same as edited.
2656    /// APPENDED at the end.
2657    ///
2658    /// `TextInput` fires on every keystroke; this fires once, when the value
2659    /// settles — on blur, or on Enter. A field that validates on `TextInput`
2660    /// scolds the user halfway through typing an email address; one that
2661    /// validates on `Change` waits until they have finished.
2662    Change,
2663    /// A form was reset to its initial values. APPENDED at the end.
2664    Reset,
2665    /// A control failed validation. APPENDED at the end.
2666    Invalid,
2667    /// A dial turned over this node. APPENDED at the end.
2668    DialRotate,
2669    /// A dial was clicked over this node. APPENDED at the end.
2670    DialClick,
2671    /// The pointer moved while over this node. APPENDED at the end.
2672    MouseMove,
2673}
2674
2675impl HoverEventFilter {
2676    /// Check if this is an internal system event that should not be exposed to user callbacks
2677    #[must_use]
2678    pub const fn is_system_internal(&self) -> bool {
2679        matches!(
2680            self,
2681            Self::SystemTextSingleClick | Self::SystemTextDoubleClick | Self::SystemTextTripleClick
2682        )
2683    }
2684
2685    // Exhaustive On -> Option<FocusEventFilter> mapping table; the several `=> None`
2686    // rows (window-only events) are intentional 1:1 rows — merging would collapse the table.
2687    #[allow(clippy::match_same_arms)]
2688    #[must_use]
2689    pub const fn to_focus_event_filter(&self) -> Option<FocusEventFilter> {
2690        match self {
2691            // No focus mirror: a dial is delivered by POSITION (hover) or to
2692            // the window, never by focus.
2693            Self::DialRotate | Self::DialClick => None,
2694            Self::MouseOver => Some(FocusEventFilter::MouseOver),
2695            Self::MouseMove => Some(FocusEventFilter::MouseMove),
2696            Self::MouseDown => Some(FocusEventFilter::MouseDown),
2697            Self::LeftMouseDown => Some(FocusEventFilter::LeftMouseDown),
2698            Self::RightMouseDown => Some(FocusEventFilter::RightMouseDown),
2699            Self::MiddleMouseDown => Some(FocusEventFilter::MiddleMouseDown),
2700            // Activation is HOVER-scoped: it targets the node that was
2701            // activated, and dispatch already walks that node's ancestors, so
2702            // there is no separate focus-scoped spelling to map onto.
2703            Self::Click => None,
2704            Self::MouseUp => Some(FocusEventFilter::MouseUp),
2705            Self::LeftMouseUp => Some(FocusEventFilter::LeftMouseUp),
2706            Self::RightMouseUp => Some(FocusEventFilter::RightMouseUp),
2707            Self::MiddleMouseUp => Some(FocusEventFilter::MiddleMouseUp),
2708            Self::MouseEnter => Some(FocusEventFilter::MouseEnter),
2709            Self::MouseLeave => Some(FocusEventFilter::MouseLeave),
2710            Self::Scroll => Some(FocusEventFilter::Scroll),
2711            Self::ScrollStart => Some(FocusEventFilter::ScrollStart),
2712            Self::ScrollEnd => Some(FocusEventFilter::ScrollEnd),
2713            Self::TextInput => Some(FocusEventFilter::TextInput),
2714            Self::VirtualKeyDown => Some(FocusEventFilter::VirtualKeyDown),
2715            Self::VirtualKeyUp => Some(FocusEventFilter::VirtualKeyUp),
2716            Self::HoveredFile => None,
2717            Self::DroppedFile => None,
2718            Self::HoveredFileCancelled => None,
2719            Self::TouchStart => None,
2720            Self::TouchMove => None,
2721            Self::TouchEnd => None,
2722            Self::TouchCancel => None,
2723            Self::PenDown => Some(FocusEventFilter::PenDown),
2724            Self::PenMove => Some(FocusEventFilter::PenMove),
2725            Self::PenUp => Some(FocusEventFilter::PenUp),
2726            Self::PenEnter => None,
2727            Self::PenLeave => None,
2728            Self::PenSqueeze => None,
2729            Self::PenDoubleTap => None,
2730            Self::PenHover => None,
2731            Self::GeolocationFix => None,
2732            Self::GeolocationError => None,
2733            Self::SensorChanged => None,
2734            Self::GamepadInput => None,
2735            Self::DragStart => Some(FocusEventFilter::DragStart),
2736            Self::Drag => Some(FocusEventFilter::Drag),
2737            Self::DragEnd => Some(FocusEventFilter::DragEnd),
2738            Self::DragEnter => Some(FocusEventFilter::DragEnter),
2739            Self::DragOver => Some(FocusEventFilter::DragOver),
2740            Self::DragLeave => Some(FocusEventFilter::DragLeave),
2741            Self::Drop => Some(FocusEventFilter::Drop),
2742            Self::DoubleClick => Some(FocusEventFilter::DoubleClick),
2743            Self::LongPress => Some(FocusEventFilter::LongPress),
2744            Self::SwipeLeft => Some(FocusEventFilter::SwipeLeft),
2745            Self::SwipeRight => Some(FocusEventFilter::SwipeRight),
2746            Self::SwipeUp => Some(FocusEventFilter::SwipeUp),
2747            Self::SwipeDown => Some(FocusEventFilter::SwipeDown),
2748            Self::PinchIn => Some(FocusEventFilter::PinchIn),
2749            Self::PinchOut => Some(FocusEventFilter::PinchOut),
2750            Self::RotateClockwise => Some(FocusEventFilter::RotateClockwise),
2751            Self::RotateCounterClockwise => Some(FocusEventFilter::RotateCounterClockwise),
2752            Self::MouseOut => Some(FocusEventFilter::MouseLeave), // mouseout → closest focus equivalent
2753            Self::FocusIn => Some(FocusEventFilter::FocusIn),
2754            Self::FocusOut => Some(FocusEventFilter::FocusOut),
2755            Self::CompositionStart => Some(FocusEventFilter::CompositionStart),
2756            Self::CompositionUpdate => Some(FocusEventFilter::CompositionUpdate),
2757            Self::CompositionEnd => Some(FocusEventFilter::CompositionEnd),
2758            // System internal events - don't convert to focus events
2759            Self::SystemTextSingleClick => None,
2760            Self::SystemTextDoubleClick => None,
2761            Self::SystemTextTripleClick => None,
2762            // Async capability outcomes — no focus-filter equivalents
2763            Self::PermissionChanged => None,
2764            Self::BiometricResult => None,
2765            Self::ScreenColorPicked => None,
2766            Self::KeyringResult => None,
2767            // Thumb buttons have a Focus twin; the rest of the additions do
2768            // not (a HID report and a modifier change belong to the window).
2769            Self::BackMouseDown => Some(FocusEventFilter::BackMouseDown),
2770            Self::BackMouseUp => Some(FocusEventFilter::BackMouseUp),
2771            Self::ForwardMouseDown => Some(FocusEventFilter::ForwardMouseDown),
2772            Self::ForwardMouseUp => Some(FocusEventFilter::ForwardMouseUp),
2773            // Form events have a Focus twin: they belong to the control that
2774            // was being edited, not to whatever is hovered.
2775            Self::Submit => Some(FocusEventFilter::Submit),
2776            Self::Change => Some(FocusEventFilter::Change),
2777            Self::Reset => Some(FocusEventFilter::Reset),
2778            Self::Invalid => Some(FocusEventFilter::Invalid),
2779        }
2780    }
2781}
2782
2783/// Event filter similar to `HoverEventFilter` that only fires when the element is focused.
2784///
2785/// **Important**: In order for this to fire, the item must have a `tabindex` attribute
2786/// (to indicate that the item is focus-able).
2787#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
2788#[repr(C)]
2789pub enum FocusEventFilter {
2790    /// Mouse moved over the focused element
2791    MouseOver,
2792    /// Any mouse button pressed on the focused element
2793    MouseDown,
2794    /// Left mouse button pressed on the focused element
2795    LeftMouseDown,
2796    /// Right mouse button pressed on the focused element
2797    RightMouseDown,
2798    /// Middle mouse button pressed on the focused element
2799    MiddleMouseDown,
2800    /// Any mouse button released on the focused element
2801    MouseUp,
2802    /// Left mouse button released on the focused element
2803    LeftMouseUp,
2804    /// Right mouse button released on the focused element
2805    RightMouseUp,
2806    /// Middle mouse button released on the focused element
2807    MiddleMouseUp,
2808    /// Mouse entered the focused element bounds
2809    MouseEnter,
2810    /// Mouse left the focused element bounds
2811    MouseLeave,
2812    /// Scroll event on the focused element
2813    Scroll,
2814    /// Scroll started on the focused element
2815    ScrollStart,
2816    /// Scroll ended on the focused element
2817    ScrollEnd,
2818    /// Text input received while element is focused
2819    TextInput,
2820    /// Virtual key pressed while element is focused
2821    VirtualKeyDown,
2822    /// Virtual key released while element is focused
2823    VirtualKeyUp,
2824    /// Element received keyboard focus
2825    FocusReceived,
2826    /// Element lost keyboard focus
2827    FocusLost,
2828    /// Pen/stylus made contact on the focused element
2829    PenDown,
2830    /// Pen/stylus moved while in contact on the focused element
2831    PenMove,
2832    /// Pen/stylus lifted from the focused element
2833    PenUp,
2834    /// Drag started on the focused element
2835    DragStart,
2836    /// Drag in progress on the focused element
2837    Drag,
2838    /// Drag ended on the focused element
2839    DragEnd,
2840    /// Dragged element entered this focused element (drop target)
2841    DragEnter,
2842    /// Dragged element is over this focused element (drop target)
2843    DragOver,
2844    /// Dragged element left this focused element (drop target)
2845    DragLeave,
2846    /// Element was dropped on this focused element (drop target)
2847    Drop,
2848    /// Double-click detected on the focused element
2849    DoubleClick,
2850    /// Long press detected on the focused element
2851    LongPress,
2852    /// Swipe left gesture on the focused element
2853    SwipeLeft,
2854    /// Swipe right gesture on the focused element
2855    SwipeRight,
2856    /// Swipe up gesture on the focused element
2857    SwipeUp,
2858    /// Swipe down gesture on the focused element
2859    SwipeDown,
2860    /// Pinch-in (zoom out) gesture on the focused element
2861    PinchIn,
2862    /// Pinch-out (zoom in) gesture on the focused element
2863    PinchOut,
2864    /// Clockwise rotation gesture on the focused element
2865    RotateClockwise,
2866    /// Counter-clockwise rotation gesture on the focused element
2867    RotateCounterClockwise,
2868
2869    // W3C Focus events (bubbling versions, fires on focused element when focus changes)
2870    /// Focus moved into this element or a descendant (W3C `focusin`)
2871    FocusIn,
2872    /// Focus moved out of this element or a descendant (W3C `focusout`)
2873    FocusOut,
2874
2875    // IME Composition events
2876    /// IME composition started (W3C `compositionstart`)
2877    CompositionStart,
2878    /// IME composition updated (W3C `compositionupdate`)
2879    CompositionUpdate,
2880    /// IME composition ended (W3C `compositionend`)
2881    CompositionEnd,
2882
2883    // Clipboard events (W3C clipboard-events; MWA-C-clipboard: fire on the
2884    // focused element BEFORE the OS default action, which preventDefault
2885    // suppresses). APPENDED at the end for ABI stability — sync to api.json
2886    // via azul-doc autofix in Phase D.
2887    /// Content is about to be copied from the focused element (W3C `copy`)
2888    Copy,
2889    /// Content is about to be cut from the focused element (W3C `cut`)
2890    Cut,
2891    /// Content is about to be pasted into the focused element (W3C `paste`)
2892    Paste,
2893    /// A structural document edit was recorded on (or under) the focused
2894    /// element and awaits the app's apply-and-ack (see
2895    /// `EventType::DocumentEdit`). APPENDED at the end for ABI stability.
2896    DocumentEdit,
2897    /// The focused editable's text was committed (see
2898    /// `EventType::TextChanged`): the post-commit counterpart of `TextInput`,
2899    /// which fires before the pending insertion is applied. APPENDED at the
2900    /// end for ABI stability.
2901    TextChanged,
2902    /// The thumb "back" button was pressed. APPENDED at the end for ABI
2903    /// stability.
2904    ///
2905    /// Every shell already routes this button — Win32 `WM_XBUTTONDOWN`, macOS
2906    /// `otherMouseDown:`, X11 button 8 — into `MouseButton::Other(3)`, and it
2907    /// died there because no filter named it. Nearly every mouse sold in a
2908    /// decade has the pair, and browsers, IDEs and file managers all bind them
2909    /// to navigate back/forward.
2910    BackMouseDown,
2911    /// The thumb "back" button was released.
2912    BackMouseUp,
2913    /// The thumb "forward" button was pressed.
2914    ForwardMouseDown,
2915    /// The thumb "forward" button was released.
2916    ForwardMouseUp,
2917    /// A form was submitted — Enter on a focused control, or a submit
2918    /// button activated. APPENDED at the end for ABI stability.
2919    Submit,
2920    /// A control's value was COMMITTED, which is not the same as edited.
2921    /// APPENDED at the end.
2922    ///
2923    /// `TextInput` fires on every keystroke; this fires once, when the value
2924    /// settles — on blur, or on Enter. A field that validates on `TextInput`
2925    /// scolds the user halfway through typing an email address; one that
2926    /// validates on `Change` waits until they have finished.
2927    Change,
2928    /// A form was reset to its initial values. APPENDED at the end.
2929    Reset,
2930    /// A control failed validation. APPENDED at the end.
2931    Invalid,
2932    /// The pointer moved while this node had focus. APPENDED at the end.
2933    MouseMove,
2934}
2935
2936/// Event filter that fires when any action fires on the entire window
2937/// (regardless of whether any element is hovered or focused over).
2938#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
2939#[repr(C)]
2940pub enum WindowEventFilter {
2941    /// Mouse moved anywhere in window
2942    MouseOver,
2943    /// Any mouse button pressed anywhere in window
2944    MouseDown,
2945    /// Left mouse button pressed anywhere in window
2946    LeftMouseDown,
2947    /// Right mouse button pressed anywhere in window
2948    RightMouseDown,
2949    /// Middle mouse button pressed anywhere in window
2950    MiddleMouseDown,
2951    /// Any mouse button released anywhere in window
2952    MouseUp,
2953    /// Left mouse button released anywhere in window
2954    LeftMouseUp,
2955    /// Right mouse button released anywhere in window
2956    RightMouseUp,
2957    /// Middle mouse button released anywhere in window
2958    MiddleMouseUp,
2959    /// Mouse entered the window
2960    MouseEnter,
2961    /// Mouse left the window
2962    MouseLeave,
2963    /// Scroll event anywhere in window
2964    Scroll,
2965    /// Scroll started anywhere in window
2966    ScrollStart,
2967    /// Scroll ended anywhere in window
2968    ScrollEnd,
2969    /// Text input received in window
2970    TextInput,
2971    /// Virtual key pressed in window
2972    VirtualKeyDown,
2973    /// Virtual key released in window
2974    VirtualKeyUp,
2975    /// File is being hovered over the window
2976    HoveredFile,
2977    /// File was dropped onto the window
2978    DroppedFile,
2979    /// File hover was cancelled
2980    HoveredFileCancelled,
2981    /// Window was resized
2982    Resized,
2983    /// Window was moved
2984    Moved,
2985    /// Window was minimized, maximized, restored, or toggled fullscreen
2986    FrameChanged,
2987    /// Touch started anywhere in window
2988    TouchStart,
2989    /// Touch moved anywhere in window
2990    TouchMove,
2991    /// Touch ended anywhere in window
2992    TouchEnd,
2993    /// Touch was cancelled
2994    TouchCancel,
2995    /// Window received focus
2996    FocusReceived,
2997    /// Window lost focus
2998    FocusLost,
2999    /// Window close was requested
3000    CloseRequested,
3001    /// System theme changed (light/dark mode)
3002    ThemeChanged,
3003    /// Window received OS-level focus
3004    WindowFocusReceived,
3005    /// Window lost OS-level focus
3006    WindowFocusLost,
3007    /// Pen/stylus made contact anywhere in window
3008    PenDown,
3009    /// Pen/stylus moved while in contact anywhere in window
3010    PenMove,
3011    /// Pen/stylus lifted anywhere in window
3012    PenUp,
3013    /// Pen/stylus entered window proximity
3014    PenEnter,
3015    /// Pen/stylus left window proximity
3016    PenLeave,
3017    /// Pen barrel-squeeze gesture fired in the window. See
3018    /// [`HoverEventFilter::PenSqueeze`].
3019    PenSqueeze,
3020    /// Pen side double-tap gesture fired in the window. See
3021    /// [`HoverEventFilter::PenDoubleTap`].
3022    PenDoubleTap,
3023    /// Pen hover in the window (in proximity, not in contact). See
3024    /// [`HoverEventFilter::PenHover`].
3025    PenHover,
3026    /// New GPS / network location fix arrived. Payload accessor:
3027    /// `CallbackInfo::get_geolocation_fix()`. Window-level rather
3028    /// than per-node because the user's location isn't bound to any
3029    /// particular DOM node — but a node-level mirror
3030    /// (`HoverEventFilter::GeolocationFix`) fires on every
3031    /// `GeolocationProbe` in the tree as well, for the common
3032    /// "redraw this node when the location changes" pattern.
3033    GeolocationFix,
3034    /// Native geolocation subscription dropped or errored (signal
3035    /// lost, no provider, permission revoked mid-session).
3036    GeolocationError,
3037    /// A motion-sensor reading changed (P6). Fires window-level (the device
3038    /// isn't bound to a node); read via `CallbackInfo::get_sensor_reading`.
3039    SensorChanged,
3040    /// A gamepad's buttons / axes changed or it (dis)connected (P6); read via
3041    /// `get_primary_gamepad` / `get_gamepad_state`.
3042    GamepadInput,
3043    /// Drag started anywhere in window
3044    DragStart,
3045    /// Drag in progress anywhere in window
3046    Drag,
3047    /// Drag ended anywhere in window
3048    DragEnd,
3049    /// Dragged element entered a drop target in window
3050    DragEnter,
3051    /// Dragged element is over a drop target in window
3052    DragOver,
3053    /// Dragged element left a drop target in window
3054    DragLeave,
3055    /// Element was dropped on a drop target in window
3056    Drop,
3057    /// Double-click detected anywhere in window
3058    DoubleClick,
3059    /// Long press detected anywhere in window
3060    LongPress,
3061    /// Swipe left gesture anywhere in window
3062    SwipeLeft,
3063    /// Swipe right gesture anywhere in window
3064    SwipeRight,
3065    /// Swipe up gesture anywhere in window
3066    SwipeUp,
3067    /// Swipe down gesture anywhere in window
3068    SwipeDown,
3069    /// Pinch-in (zoom out) gesture anywhere in window
3070    PinchIn,
3071    /// Pinch-out (zoom in) gesture anywhere in window
3072    PinchOut,
3073    /// Clockwise rotation gesture anywhere in window
3074    RotateClockwise,
3075    /// Counter-clockwise rotation gesture anywhere in window
3076    RotateCounterClockwise,
3077    /// The window's DPI scale factor changed (e.g., moved to a monitor with
3078    /// different scaling). The new DPI is available via `CallbackInfo::get_hidpi_factor()`.
3079    DpiChanged,
3080    /// The window moved to a different monitor. The new monitor is available
3081    /// via `CallbackInfo::get_current_monitor()`.
3082    MonitorChanged,
3083
3084    // Async capability outcomes (MWA-A1b) — window-level mirrors (the
3085    // outcome isn't inherently bound to a node).
3086    /// A permission's OS-observed state changed.
3087    PermissionChanged,
3088    /// A biometric authentication prompt completed.
3089    BiometricResult,
3090    /// The screen eyedropper finished (picked or cancelled).
3091    ScreenColorPicked,
3092    /// A keyring store / get / delete operation completed.
3093    KeyringResult,
3094    /// The thumb "back" button was pressed. APPENDED at the end for ABI
3095    /// stability.
3096    ///
3097    /// Every shell already routes this button — Win32 `WM_XBUTTONDOWN`, macOS
3098    /// `otherMouseDown:`, X11 button 8 — into `MouseButton::Other(3)`, and it
3099    /// died there because no filter named it. Nearly every mouse sold in a
3100    /// decade has the pair, and browsers, IDEs and file managers all bind them
3101    /// to navigate back/forward.
3102    BackMouseDown,
3103    /// The thumb "back" button was released.
3104    BackMouseUp,
3105    /// The thumb "forward" button was pressed.
3106    ForwardMouseDown,
3107    /// The thumb "forward" button was released.
3108    ForwardMouseUp,
3109    /// Raw, pre-acceleration pointer motion. APPENDED at the end for ABI
3110    /// stability.
3111    ///
3112    /// Window-scoped rather than hover-scoped because raw motion has no
3113    /// position: there is no node under it to hit-test, and a locked pointer
3114    /// is a window-wide mode.
3115    RawMouseMotion,
3116    /// A modifier or lock key changed state. APPENDED at the end.
3117    ModifiersChanged,
3118    /// A HID device delivered an input report. APPENDED at the end.
3119    ///
3120    /// Window-scoped like `RawMouseMotion`: a HID report has no position, so
3121    /// there is no node it belongs to.
3122    HidReport,
3123    /// A dial turned. APPENDED at the end.
3124    ///
3125    /// Window-scoped as well as node-scoped: a dial used OFF the screen (every
3126    /// device except a Surface Dial on a Surface Studio) reports no contact
3127    /// point, so no node is under it and only the window can receive it.
3128    DialRotate,
3129    /// A dial with a physical click was pressed. APPENDED at the end.
3130    DialClick,
3131    /// The pointer moved anywhere in the window. APPENDED at the end.
3132    MouseMove,
3133    /// The pointer lock was taken, released, or ended by the platform
3134    /// (9d-ii-c). Window-level: the lock belongs to the window, not a node.
3135    PointerLockChange,
3136    /// Media playback started (11c). APPENDED at the end for ABI stability.
3137    ///
3138    /// The window mirror of [`ExternalEventFilter::Play`]. Media state is
3139    /// reported by an EXTERNAL player, not by a device the window hit-tests,
3140    /// so `External` is its home; this pair exists because a media event
3141    /// belongs to the window in exactly the sense a monitor being unplugged
3142    /// does, and an app that already listens window-wide should not have to
3143    /// learn a second family to hear it.
3144    Play,
3145    /// Media playback paused. APPENDED at the end.
3146    Pause,
3147    /// Media playback reached the end. APPENDED at the end.
3148    Ended,
3149    /// The media position advanced (throttled — see
3150    /// `managers::media_player::TIME_UPDATE_INTERVAL_S`). APPENDED at the end.
3151    TimeUpdate,
3152    /// The media volume or mute state changed. APPENDED at the end.
3153    VolumeChange,
3154    /// The media pipeline failed. APPENDED at the end.
3155    MediaError,
3156}
3157
3158impl WindowEventFilter {
3159    // Exhaustive On -> Option<HoverEventFilter> mapping table (see to_focus_event_filter).
3160    #[allow(clippy::match_same_arms)]
3161    #[must_use]
3162    pub const fn to_hover_event_filter(&self) -> Option<HoverEventFilter> {
3163        match self {
3164            Self::DialRotate => Some(HoverEventFilter::DialRotate),
3165            Self::DialClick => Some(HoverEventFilter::DialClick),
3166            Self::MouseOver => Some(HoverEventFilter::MouseOver),
3167            Self::MouseMove => Some(HoverEventFilter::MouseMove),
3168            Self::MouseDown => Some(HoverEventFilter::MouseDown),
3169            Self::LeftMouseDown => Some(HoverEventFilter::LeftMouseDown),
3170            Self::RightMouseDown => Some(HoverEventFilter::RightMouseDown),
3171            Self::MiddleMouseDown => Some(HoverEventFilter::MiddleMouseDown),
3172            Self::MouseUp => Some(HoverEventFilter::MouseUp),
3173            Self::LeftMouseUp => Some(HoverEventFilter::LeftMouseUp),
3174            Self::RightMouseUp => Some(HoverEventFilter::RightMouseUp),
3175            Self::MiddleMouseUp => Some(HoverEventFilter::MiddleMouseUp),
3176            Self::Scroll => Some(HoverEventFilter::Scroll),
3177            Self::ScrollStart => Some(HoverEventFilter::ScrollStart),
3178            Self::ScrollEnd => Some(HoverEventFilter::ScrollEnd),
3179            Self::TextInput => Some(HoverEventFilter::TextInput),
3180            Self::VirtualKeyDown => Some(HoverEventFilter::VirtualKeyDown),
3181            Self::VirtualKeyUp => Some(HoverEventFilter::VirtualKeyUp),
3182            Self::HoveredFile => Some(HoverEventFilter::HoveredFile),
3183            Self::DroppedFile => Some(HoverEventFilter::DroppedFile),
3184            Self::HoveredFileCancelled => Some(HoverEventFilter::HoveredFileCancelled),
3185            // MouseEnter and MouseLeave on the **window** - does not mean a mouseenter
3186            // and a mouseleave on the hovered element
3187            Self::MouseEnter => None,
3188            Self::MouseLeave => None,
3189            Self::Resized => None,
3190            Self::Moved => None,
3191            // A frame transition is a WINDOW fact; there is no per-element
3192            // hover equivalent to map it onto.
3193            Self::FrameChanged => None,
3194            Self::TouchStart => Some(HoverEventFilter::TouchStart),
3195            Self::TouchMove => Some(HoverEventFilter::TouchMove),
3196            Self::TouchEnd => Some(HoverEventFilter::TouchEnd),
3197            Self::TouchCancel => Some(HoverEventFilter::TouchCancel),
3198            Self::FocusReceived => None,
3199            Self::FocusLost => None,
3200            Self::CloseRequested => None,
3201            Self::ThemeChanged => None,
3202            Self::WindowFocusReceived => None, // specific to window!
3203            Self::WindowFocusLost => None,     // specific to window!
3204            Self::PointerLockChange => None,   // specific to window!
3205            Self::PenDown => Some(HoverEventFilter::PenDown),
3206            Self::PenMove => Some(HoverEventFilter::PenMove),
3207            Self::PenUp => Some(HoverEventFilter::PenUp),
3208            Self::PenEnter => Some(HoverEventFilter::PenEnter),
3209            Self::PenLeave => Some(HoverEventFilter::PenLeave),
3210            Self::PenSqueeze => Some(HoverEventFilter::PenSqueeze),
3211            Self::PenDoubleTap => Some(HoverEventFilter::PenDoubleTap),
3212            Self::PenHover => Some(HoverEventFilter::PenHover),
3213            Self::GeolocationFix => Some(HoverEventFilter::GeolocationFix),
3214            Self::GeolocationError => Some(HoverEventFilter::GeolocationError),
3215            Self::SensorChanged => Some(HoverEventFilter::SensorChanged),
3216            Self::GamepadInput => Some(HoverEventFilter::GamepadInput),
3217            Self::DragStart => Some(HoverEventFilter::DragStart),
3218            Self::Drag => Some(HoverEventFilter::Drag),
3219            Self::DragEnd => Some(HoverEventFilter::DragEnd),
3220            Self::DragEnter => Some(HoverEventFilter::DragEnter),
3221            Self::DragOver => Some(HoverEventFilter::DragOver),
3222            Self::DragLeave => Some(HoverEventFilter::DragLeave),
3223            Self::Drop => Some(HoverEventFilter::Drop),
3224            Self::DoubleClick => Some(HoverEventFilter::DoubleClick),
3225            Self::LongPress => Some(HoverEventFilter::LongPress),
3226            Self::SwipeLeft => Some(HoverEventFilter::SwipeLeft),
3227            Self::SwipeRight => Some(HoverEventFilter::SwipeRight),
3228            Self::SwipeUp => Some(HoverEventFilter::SwipeUp),
3229            Self::SwipeDown => Some(HoverEventFilter::SwipeDown),
3230            Self::PinchIn => Some(HoverEventFilter::PinchIn),
3231            Self::PinchOut => Some(HoverEventFilter::PinchOut),
3232            Self::RotateClockwise => Some(HoverEventFilter::RotateClockwise),
3233            Self::RotateCounterClockwise => Some(HoverEventFilter::RotateCounterClockwise),
3234            // Window-specific events with no hover equivalent
3235            Self::DpiChanged => None,
3236            Self::MonitorChanged => None,
3237            // Async capability outcomes — mirror to the hover twin
3238            Self::PermissionChanged => Some(HoverEventFilter::PermissionChanged),
3239            Self::BiometricResult => Some(HoverEventFilter::BiometricResult),
3240            Self::ScreenColorPicked => Some(HoverEventFilter::ScreenColorPicked),
3241            Self::KeyringResult => Some(HoverEventFilter::KeyringResult),
3242            Self::BackMouseDown => Some(HoverEventFilter::BackMouseDown),
3243            Self::BackMouseUp => Some(HoverEventFilter::BackMouseUp),
3244            Self::ForwardMouseDown => Some(HoverEventFilter::ForwardMouseDown),
3245            Self::ForwardMouseUp => Some(HoverEventFilter::ForwardMouseUp),
3246            // Window-only: no position to hit-test, so no hover twin.
3247            Self::RawMouseMotion | Self::ModifiersChanged | Self::HidReport => None,
3248            // Media state (11c) has no hover twin either: it is reported by an
3249            // external player, and there is no pointer position that could
3250            // decide which node it "happened over".
3251            Self::Play
3252            | Self::Pause
3253            | Self::Ended
3254            | Self::TimeUpdate
3255            | Self::VolumeChange
3256            | Self::MediaError => None,
3257        }
3258    }
3259}
3260
3261/// Defines events related to the lifecycle of a DOM node itself.
3262#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3263#[repr(C)]
3264pub enum ComponentEventFilter {
3265    /// Fired after the component is first mounted into the DOM.
3266    AfterMount,
3267    /// Fired just before the component is removed from the DOM.
3268    BeforeUnmount,
3269    /// Fired when the node's layout rectangle has been resized.
3270    NodeResized,
3271    /// Fired to trigger the default action for an accessibility component.
3272    DefaultAction,
3273    /// Fired when the component becomes selected.
3274    Selected,
3275    /// Fired when a keyed component's content has changed (props/state update).
3276    Updated,
3277    /// Fired on a `<transient-window>` the user dismissed (outside click or
3278    /// Escape). The popup is closed by the engine regardless; this is where
3279    /// the app clears its own `open` state so the next layout agrees.
3280    Dismissed,
3281    /// Fired on a `<transient-window>` the user tore off its anchor by
3282    /// dragging its `-azul-app-region: drag` strip; it is a free toplevel
3283    /// now. The event's `current_bounds` is its rect in the parent.
3284    TornOff,
3285    /// Fired on a torn-off `<transient-window>` the user docked back - onto
3286    /// its anchor, or onto a `tearoff-zone` node, which is its anchor from
3287    /// here on.
3288    Docked,
3289}
3290
3291/// Defines application-level events not tied to a specific window or node.
3292#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3293#[repr(C)]
3294pub enum ApplicationEventFilter {
3295    /// Fired when a new hardware device is connected.
3296    DeviceConnected,
3297    /// Fired when a hardware device is disconnected.
3298    DeviceDisconnected,
3299    /// Fired when a new monitor/display is connected to the system.
3300    /// Callback receives updated monitor list via `CallbackInfo::get_monitors()`.
3301    MonitorConnected,
3302    /// Fired when a monitor/display is disconnected from the system.
3303    MonitorDisconnected,
3304    /// The platform's media controls asked for a seek (9h-i-a-i-a). APPENDED
3305    /// at the end for ABI stability.
3306    MediaControl,
3307    /// The system changed the app's hold on the audio (9h-i-a-i-d-i).
3308    SystemAudioChange,
3309}
3310
3311/// Events reported by something OUTSIDE the input pipeline (11c).
3312///
3313/// The other four families answer "where did this land?" — hovered, focused,
3314/// window-wide, on this component. A media player answers none of those: the
3315/// state change happens in an external player (a decoder thread, a platform
3316/// media service, the app's own transport calls) and is then reported IN. It
3317/// is the same shape as a monitor being unplugged, one level closer to the
3318/// app.
3319///
3320/// Dispatched like a window filter: every node carrying a matching callback
3321/// is invoked, and the event's `target` names the media node so a callback
3322/// serving several players can tell them apart.
3323#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3324#[repr(C)]
3325pub enum ExternalEventFilter {
3326    /// Media playback started.
3327    Play,
3328    /// Media playback paused.
3329    Pause,
3330    /// Media playback reached the end of its known duration. Fires exactly
3331    /// once per arrival at the end (reaching it also stops the transport).
3332    Ended,
3333    /// The media position advanced. THROTTLED to one event per 250 ms of
3334    /// media time (`managers::media_player::TIME_UPDATE_INTERVAL_S`) — the
3335    /// web's ~4/s budget — because a playing video would otherwise raise one
3336    /// per frame for its entire length.
3337    TimeUpdate,
3338    /// The media volume or mute state changed.
3339    VolumeChange,
3340    /// The media pipeline reported a failure; the transport is stopped.
3341    MediaError,
3342}
3343
3344/// Sets the target for what events can reach the callbacks specifically.
3345///
3346/// This determines the condition under which an event is fired, such as whether
3347/// the node is hovered, focused, or if the event is window-global.
3348#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
3349#[repr(C, u8)]
3350pub enum EventFilter {
3351    /// Calls the attached callback when the mouse is actively over the
3352    /// given element.
3353    Hover(HoverEventFilter),
3354    /// Calls the attached callback when the element is currently focused.
3355    Focus(FocusEventFilter),
3356    /// Calls the callback when anything related to the window is happening.
3357    /// The "hit item" will be the root item of the DOM.
3358    /// For example, this can be useful for tracking the mouse position
3359    /// (in relation to the window). In difference to `Desktop`, this only
3360    /// fires when the window is focused.
3361    ///
3362    /// This can also be good for capturing controller input, touch input
3363    /// (i.e. global gestures that aren't attached to any component, but rather
3364    /// the "window" itself).
3365    Window(WindowEventFilter),
3366    /// API stub: Something happened with the node itself (node resized, created or removed).
3367    Component(ComponentEventFilter),
3368    /// Something happened with the application (started, shutdown, device plugged in).
3369    Application(ApplicationEventFilter),
3370    /// Something was reported by an external source that the hit test cannot
3371    /// place — media playback state, today. APPENDED at the end for ABI
3372    /// stability (`EventFilter` is `repr(C, u8)`; a variant inserted anywhere
3373    /// but the tail would renumber every discriminant after it).
3374    External(ExternalEventFilter),
3375}
3376
3377impl EventFilter {
3378    #[must_use]
3379    pub const fn is_focus_callback(&self) -> bool {
3380        matches!(self, Self::Focus(_))
3381    }
3382    #[must_use]
3383    pub const fn is_window_callback(&self) -> bool {
3384        matches!(self, Self::Window(_))
3385    }
3386}
3387
3388/// Creates a function inside an impl <enum type> block that returns a single
3389/// variant if the enum is that variant.
3390macro_rules! get_single_enum_type {
3391    ($fn_name:ident, $enum_name:ident:: $variant:ident($return_type:ty)) => {
3392        #[must_use]
3393        pub const fn $fn_name(&self) -> Option<$return_type> {
3394            use self::$enum_name::*;
3395            match self {
3396                $variant(e) => Some(*e),
3397                _ => None,
3398            }
3399        }
3400    };
3401}
3402
3403impl EventFilter {
3404    get_single_enum_type!(as_hover_event_filter, EventFilter::Hover(HoverEventFilter));
3405    get_single_enum_type!(as_focus_event_filter, EventFilter::Focus(FocusEventFilter));
3406    get_single_enum_type!(
3407        as_window_event_filter,
3408        EventFilter::Window(WindowEventFilter)
3409    );
3410}
3411
3412/// Convert from `On` enum to `EventFilter`.
3413///
3414/// This determines which specific filter variant is used based on the event type.
3415/// For example, `On::TextInput` becomes a Focus event filter, while `On::VirtualKeyDown`
3416/// becomes a Window event filter (since it's global to the window).
3417impl From<On> for EventFilter {
3418    // Exhaustive On -> EventFilter mapping table; the a11y events (Default/Collapse/
3419    // Expand/Increment/Decrement) all map to MouseUp ("click") as intentional 1:1
3420    // documented rows — merging would drop the per-row rationale comments.
3421    #[allow(clippy::match_same_arms)]
3422    fn from(input: On) -> Self {
3423        use crate::dom::On::{
3424            Collapse, Decrement, Default, DroppedFile, Expand, FocusLost, FocusReceived,
3425            HoveredFile, HoveredFileCancelled, Increment, LeftMouseDown, LeftMouseUp,
3426            MiddleMouseDown, MiddleMouseUp, MouseDown, MouseEnter, MouseLeave, MouseMove,
3427            MouseOver, MouseUp, RightMouseDown, RightMouseUp, Scroll, TextInput, VirtualKeyDown,
3428            VirtualKeyUp,
3429        };
3430        match input {
3431            MouseOver => Self::Hover(HoverEventFilter::MouseOver),
3432            MouseMove => Self::Hover(HoverEventFilter::MouseMove),
3433            MouseDown => Self::Hover(HoverEventFilter::MouseDown),
3434            LeftMouseDown => Self::Hover(HoverEventFilter::LeftMouseDown),
3435            MiddleMouseDown => Self::Hover(HoverEventFilter::MiddleMouseDown),
3436            RightMouseDown => Self::Hover(HoverEventFilter::RightMouseDown),
3437            On::Click => Self::Hover(HoverEventFilter::Click),
3438            MouseUp => Self::Hover(HoverEventFilter::MouseUp),
3439            LeftMouseUp => Self::Hover(HoverEventFilter::LeftMouseUp),
3440            MiddleMouseUp => Self::Hover(HoverEventFilter::MiddleMouseUp),
3441            RightMouseUp => Self::Hover(HoverEventFilter::RightMouseUp),
3442
3443            MouseEnter => Self::Hover(HoverEventFilter::MouseEnter),
3444            MouseLeave => Self::Hover(HoverEventFilter::MouseLeave),
3445            Scroll => Self::Hover(HoverEventFilter::Scroll),
3446            TextInput => Self::Focus(FocusEventFilter::TextInput), // focus!
3447            On::DocumentEdit => Self::Focus(FocusEventFilter::DocumentEdit), // focus!
3448            On::TextChanged => Self::Focus(FocusEventFilter::TextChanged), // focus!
3449            VirtualKeyDown => Self::Window(WindowEventFilter::VirtualKeyDown), // window!
3450            VirtualKeyUp => Self::Window(WindowEventFilter::VirtualKeyUp), // window!
3451            HoveredFile => Self::Hover(HoverEventFilter::HoveredFile),
3452            DroppedFile => Self::Hover(HoverEventFilter::DroppedFile),
3453            HoveredFileCancelled => Self::Hover(HoverEventFilter::HoveredFileCancelled),
3454            FocusReceived => Self::Focus(FocusEventFilter::FocusReceived), // focus!
3455            FocusLost => Self::Focus(FocusEventFilter::FocusLost),         // focus!
3456
3457            // Accessibility events are ACTIVATIONS, so they map to `Click` -
3458            // the same filter a pointer click and a keyboard Enter/Space
3459            // reach. They used to map to `MouseUp`, which conflated
3460            // activation with a raw pointer release.
3461            Default => Self::Hover(HoverEventFilter::Click),
3462            Collapse => Self::Hover(HoverEventFilter::Click),
3463            Expand => Self::Hover(HoverEventFilter::Click),
3464            Increment => Self::Hover(HoverEventFilter::Click),
3465            Decrement => Self::Hover(HoverEventFilter::Click),
3466        }
3467    }
3468}
3469
3470// Cross-Platform Event Dispatch System
3471// NOTE: The old dispatch_synthetic_events / CallbackTarget / CallbackToInvoke / EventDispatchResult
3472// pipeline has been removed. Event dispatch now goes through dispatch_events_propagated() in
3473// event_v2.rs which uses propagate_event() for W3C Capture→Target→Bubble propagation.
3474
3475/// Trait for managers to provide their pending events.
3476///
3477/// Each manager (`TextInputManager`, `ScrollManager`, etc.) implements this to
3478/// report what events occurred since the last frame. This enables a unified,
3479/// lazy event determination system.
3480pub trait EventProvider {
3481    /// Get all pending events from this manager.
3482    ///
3483    /// Events should include:
3484    ///
3485    /// - `target`: The `DomNodeId` that was affected
3486    /// - `event_type`: What happened (Input, Scroll, Focus, etc.)
3487    /// - `source`: `EventSource::User` for input, `EventSource::Programmatic` for API calls
3488    /// - `data`: Type-specific event data
3489    ///
3490    /// After calling this, the manager should mark events as "read" so they
3491    /// aren't returned again next frame.
3492    fn get_pending_events(&self, timestamp: Instant) -> Vec<SyntheticEvent>;
3493}
3494
3495/// Deduplicate synthetic events by (target node, event type).
3496///
3497/// Groups by (target.dom, target.node, `event_type`), keeping the latest timestamp.
3498#[must_use]
3499pub fn deduplicate_synthetic_events(mut events: Vec<SyntheticEvent>) -> Vec<SyntheticEvent> {
3500    if events.len() <= 1 {
3501        return events;
3502    }
3503
3504    // The pointer SEAT is part of the key (9b-ii-b): two cursors pressing the
3505    // same node are two presses, not one. Non-pointer events key on the
3506    // primary and coalesce exactly as before.
3507    let seat_of = |e: &SyntheticEvent| match &e.data {
3508        EventData::Mouse(m) => m.seat_id,
3509        EventData::Scroll(s) => s.seat_id,
3510        EventData::Touch(t) => t.seat_id,
3511        EventData::Composition(c) => c.seat_id,
3512        _ => crate::window::PRIMARY_POINTER_SEAT,
3513    };
3514    events.sort_by_key(|e| (e.target.dom, e.target.node, e.event_type, seat_of(e)));
3515
3516    // Coalesce consecutive events with same target, event_type and seat
3517    let mut result = Vec::with_capacity(events.len());
3518    let mut iter = events.into_iter();
3519
3520    if let Some(mut prev) = iter.next() {
3521        for curr in iter {
3522            if prev.target == curr.target
3523                && prev.event_type == curr.event_type
3524                && seat_of(&prev) == seat_of(&curr)
3525            {
3526                // Keep the one with later timestamp
3527                prev = if curr.timestamp > prev.timestamp {
3528                    curr
3529                } else {
3530                    prev
3531                };
3532            } else {
3533                result.push(prev);
3534                prev = curr;
3535            }
3536        }
3537        result.push(prev);
3538    }
3539
3540    result
3541}
3542
3543/// Every `HoverEventFilter` variant, so planning can be derived from matching.
3544static ALL_HOVER: &[HoverEventFilter] = &[
3545    HoverEventFilter::MouseOver,
3546    HoverEventFilter::MouseMove,
3547    HoverEventFilter::MouseDown,
3548    HoverEventFilter::LeftMouseDown,
3549    HoverEventFilter::RightMouseDown,
3550    HoverEventFilter::MiddleMouseDown,
3551    HoverEventFilter::Click,
3552    HoverEventFilter::MouseUp,
3553    HoverEventFilter::LeftMouseUp,
3554    HoverEventFilter::RightMouseUp,
3555    HoverEventFilter::MiddleMouseUp,
3556    HoverEventFilter::MouseEnter,
3557    HoverEventFilter::MouseLeave,
3558    HoverEventFilter::Scroll,
3559    HoverEventFilter::ScrollStart,
3560    HoverEventFilter::ScrollEnd,
3561    HoverEventFilter::TextInput,
3562    HoverEventFilter::VirtualKeyDown,
3563    HoverEventFilter::VirtualKeyUp,
3564    HoverEventFilter::HoveredFile,
3565    HoverEventFilter::DroppedFile,
3566    HoverEventFilter::HoveredFileCancelled,
3567    HoverEventFilter::TouchStart,
3568    HoverEventFilter::TouchMove,
3569    HoverEventFilter::TouchEnd,
3570    HoverEventFilter::TouchCancel,
3571    HoverEventFilter::PenDown,
3572    HoverEventFilter::PenMove,
3573    HoverEventFilter::PenUp,
3574    HoverEventFilter::PenEnter,
3575    HoverEventFilter::PenLeave,
3576    HoverEventFilter::PenSqueeze,
3577    HoverEventFilter::PenDoubleTap,
3578    HoverEventFilter::PenHover,
3579    HoverEventFilter::GeolocationFix,
3580    HoverEventFilter::GeolocationError,
3581    HoverEventFilter::SensorChanged,
3582    HoverEventFilter::GamepadInput,
3583    HoverEventFilter::DragStart,
3584    HoverEventFilter::Drag,
3585    HoverEventFilter::DragEnd,
3586    HoverEventFilter::DragEnter,
3587    HoverEventFilter::DragOver,
3588    HoverEventFilter::DragLeave,
3589    HoverEventFilter::Drop,
3590    HoverEventFilter::DoubleClick,
3591    HoverEventFilter::LongPress,
3592    HoverEventFilter::SwipeLeft,
3593    HoverEventFilter::SwipeRight,
3594    HoverEventFilter::SwipeUp,
3595    HoverEventFilter::SwipeDown,
3596    HoverEventFilter::PinchIn,
3597    HoverEventFilter::PinchOut,
3598    HoverEventFilter::RotateClockwise,
3599    HoverEventFilter::RotateCounterClockwise,
3600    HoverEventFilter::MouseOut,
3601    HoverEventFilter::FocusIn,
3602    HoverEventFilter::FocusOut,
3603    HoverEventFilter::CompositionStart,
3604    HoverEventFilter::CompositionUpdate,
3605    HoverEventFilter::CompositionEnd,
3606    HoverEventFilter::SystemTextSingleClick,
3607    HoverEventFilter::SystemTextDoubleClick,
3608    HoverEventFilter::SystemTextTripleClick,
3609    HoverEventFilter::PermissionChanged,
3610    HoverEventFilter::BiometricResult,
3611    HoverEventFilter::ScreenColorPicked,
3612    HoverEventFilter::KeyringResult,
3613    // Form events. These have matcher arms and planning arms, but were absent
3614    // from THIS list — and planning is DERIVED by probing it, so a filter the
3615    // list does not name can never be planned and dispatches to nothing. Same
3616    // dead-filter shape the arc exists to close, reintroduced by the arc.
3617    HoverEventFilter::Submit,
3618    HoverEventFilter::Change,
3619    HoverEventFilter::Reset,
3620    HoverEventFilter::Invalid,
3621    // Planning is DERIVED by probing this list, so a filter missing from it
3622    // can never be planned — the failure this arc has hit repeatedly.
3623    HoverEventFilter::DialRotate,
3624    HoverEventFilter::DialClick,
3625];
3626
3627/// Every `FocusEventFilter` variant, so planning can be derived from matching.
3628static ALL_FOCUS: &[FocusEventFilter] = &[
3629    FocusEventFilter::MouseOver,
3630    FocusEventFilter::MouseMove,
3631    FocusEventFilter::MouseDown,
3632    FocusEventFilter::LeftMouseDown,
3633    FocusEventFilter::RightMouseDown,
3634    FocusEventFilter::MiddleMouseDown,
3635    FocusEventFilter::MouseUp,
3636    FocusEventFilter::LeftMouseUp,
3637    FocusEventFilter::RightMouseUp,
3638    FocusEventFilter::MiddleMouseUp,
3639    FocusEventFilter::MouseEnter,
3640    FocusEventFilter::MouseLeave,
3641    FocusEventFilter::Scroll,
3642    FocusEventFilter::ScrollStart,
3643    FocusEventFilter::ScrollEnd,
3644    FocusEventFilter::TextInput,
3645    FocusEventFilter::VirtualKeyDown,
3646    FocusEventFilter::VirtualKeyUp,
3647    FocusEventFilter::FocusReceived,
3648    FocusEventFilter::FocusLost,
3649    FocusEventFilter::PenDown,
3650    FocusEventFilter::PenMove,
3651    FocusEventFilter::PenUp,
3652    FocusEventFilter::DragStart,
3653    FocusEventFilter::Drag,
3654    FocusEventFilter::DragEnd,
3655    FocusEventFilter::DragEnter,
3656    FocusEventFilter::DragOver,
3657    FocusEventFilter::DragLeave,
3658    FocusEventFilter::Drop,
3659    FocusEventFilter::DoubleClick,
3660    FocusEventFilter::LongPress,
3661    FocusEventFilter::SwipeLeft,
3662    FocusEventFilter::SwipeRight,
3663    FocusEventFilter::SwipeUp,
3664    FocusEventFilter::SwipeDown,
3665    FocusEventFilter::PinchIn,
3666    FocusEventFilter::PinchOut,
3667    FocusEventFilter::RotateClockwise,
3668    FocusEventFilter::RotateCounterClockwise,
3669    FocusEventFilter::FocusIn,
3670    FocusEventFilter::FocusOut,
3671    FocusEventFilter::CompositionStart,
3672    FocusEventFilter::CompositionUpdate,
3673    FocusEventFilter::CompositionEnd,
3674    FocusEventFilter::Copy,
3675    FocusEventFilter::Cut,
3676    FocusEventFilter::Paste,
3677    FocusEventFilter::DocumentEdit,
3678    FocusEventFilter::TextChanged,
3679    // Form events. These have matcher arms and planning arms, but were absent
3680    // from THIS list — and planning is DERIVED by probing it, so a filter the
3681    // list does not name can never be planned and dispatches to nothing. Same
3682    // dead-filter shape the arc exists to close, reintroduced by the arc.
3683    FocusEventFilter::Submit,
3684    FocusEventFilter::Change,
3685    FocusEventFilter::Reset,
3686    FocusEventFilter::Invalid,
3687];
3688
3689/// Every `WindowEventFilter` variant, so planning can be derived from matching.
3690static ALL_WINDOW: &[WindowEventFilter] = &[
3691    WindowEventFilter::MouseOver,
3692    WindowEventFilter::MouseMove,
3693    WindowEventFilter::MouseDown,
3694    WindowEventFilter::LeftMouseDown,
3695    WindowEventFilter::RightMouseDown,
3696    WindowEventFilter::MiddleMouseDown,
3697    WindowEventFilter::MouseUp,
3698    WindowEventFilter::LeftMouseUp,
3699    WindowEventFilter::RightMouseUp,
3700    WindowEventFilter::MiddleMouseUp,
3701    WindowEventFilter::MouseEnter,
3702    WindowEventFilter::MouseLeave,
3703    WindowEventFilter::Scroll,
3704    WindowEventFilter::ScrollStart,
3705    WindowEventFilter::ScrollEnd,
3706    WindowEventFilter::TextInput,
3707    WindowEventFilter::VirtualKeyDown,
3708    WindowEventFilter::VirtualKeyUp,
3709    WindowEventFilter::HoveredFile,
3710    WindowEventFilter::DroppedFile,
3711    WindowEventFilter::HoveredFileCancelled,
3712    WindowEventFilter::Resized,
3713    WindowEventFilter::Moved,
3714    WindowEventFilter::FrameChanged,
3715    WindowEventFilter::TouchStart,
3716    WindowEventFilter::TouchMove,
3717    WindowEventFilter::TouchEnd,
3718    WindowEventFilter::TouchCancel,
3719    WindowEventFilter::FocusReceived,
3720    WindowEventFilter::FocusLost,
3721    WindowEventFilter::CloseRequested,
3722    WindowEventFilter::ThemeChanged,
3723    WindowEventFilter::WindowFocusReceived,
3724    WindowEventFilter::WindowFocusLost,
3725    WindowEventFilter::PointerLockChange,
3726    WindowEventFilter::PenDown,
3727    WindowEventFilter::PenMove,
3728    WindowEventFilter::PenUp,
3729    WindowEventFilter::PenEnter,
3730    WindowEventFilter::PenLeave,
3731    WindowEventFilter::PenSqueeze,
3732    WindowEventFilter::PenDoubleTap,
3733    WindowEventFilter::PenHover,
3734    WindowEventFilter::GeolocationFix,
3735    WindowEventFilter::GeolocationError,
3736    WindowEventFilter::SensorChanged,
3737    WindowEventFilter::GamepadInput,
3738    WindowEventFilter::DragStart,
3739    WindowEventFilter::Drag,
3740    WindowEventFilter::DragEnd,
3741    WindowEventFilter::DragEnter,
3742    WindowEventFilter::DragOver,
3743    WindowEventFilter::DragLeave,
3744    WindowEventFilter::Drop,
3745    WindowEventFilter::DoubleClick,
3746    WindowEventFilter::LongPress,
3747    WindowEventFilter::SwipeLeft,
3748    WindowEventFilter::SwipeRight,
3749    WindowEventFilter::SwipeUp,
3750    WindowEventFilter::SwipeDown,
3751    WindowEventFilter::PinchIn,
3752    WindowEventFilter::PinchOut,
3753    WindowEventFilter::RotateClockwise,
3754    WindowEventFilter::RotateCounterClockwise,
3755    WindowEventFilter::DpiChanged,
3756    WindowEventFilter::MonitorChanged,
3757    WindowEventFilter::PermissionChanged,
3758    WindowEventFilter::BiometricResult,
3759    WindowEventFilter::ScreenColorPicked,
3760    WindowEventFilter::KeyringResult,
3761    WindowEventFilter::DialRotate,
3762    WindowEventFilter::DialClick,
3763    // Media (11c). Planning is DERIVED by probing this list, so a filter the
3764    // list does not name can never be planned, whatever its matcher says.
3765    WindowEventFilter::Play,
3766    WindowEventFilter::Pause,
3767    WindowEventFilter::Ended,
3768    WindowEventFilter::TimeUpdate,
3769    WindowEventFilter::VolumeChange,
3770    WindowEventFilter::MediaError,
3771];
3772
3773/// Every `ComponentEventFilter` variant, so planning can be derived from
3774/// matching. This is the family the derivation FORGOT when it replaced the
3775/// hand-written table: with no component probe, `Mount` planned zero filters
3776/// and every `AfterMount` / `Updated` / `NodeResized` / `Dismissed` /
3777/// `TornOff` / `Docked` callback in the engine went silent — the reconcile
3778/// diff kept emitting the events, the dispatcher's `Component` arm simply
3779/// never saw one (`headless_lifecycle` caught it: mount=0 on the first frame).
3780/// `DefaultAction` and `Selected` have no event type, so probing them is a
3781/// no-op today; they are listed so the table stays the whole enum.
3782static ALL_COMPONENT: &[ComponentEventFilter] = &[
3783    ComponentEventFilter::AfterMount,
3784    ComponentEventFilter::BeforeUnmount,
3785    ComponentEventFilter::NodeResized,
3786    ComponentEventFilter::DefaultAction,
3787    ComponentEventFilter::Selected,
3788    ComponentEventFilter::Updated,
3789    ComponentEventFilter::Dismissed,
3790    ComponentEventFilter::TornOff,
3791    ComponentEventFilter::Docked,
3792];
3793
3794
3795/// Every `ExternalEventFilter`, for planning to probe. See [`ALL_COMPONENT`].
3796static ALL_EXTERNAL: &[ExternalEventFilter] = &[
3797    ExternalEventFilter::Play,
3798    ExternalEventFilter::Pause,
3799    ExternalEventFilter::Ended,
3800    ExternalEventFilter::TimeUpdate,
3801    ExternalEventFilter::VolumeChange,
3802    ExternalEventFilter::MediaError,
3803];
3804
3805/// Every `ApplicationEventFilter`, for planning to probe. See [`ALL_COMPONENT`].
3806static ALL_APPLICATION: &[ApplicationEventFilter] = &[
3807    ApplicationEventFilter::DeviceConnected,
3808    ApplicationEventFilter::DeviceDisconnected,
3809    ApplicationEventFilter::MonitorConnected,
3810    ApplicationEventFilter::MonitorDisconnected,
3811    ApplicationEventFilter::MediaControl,
3812    ApplicationEventFilter::SystemAudioChange,
3813];
3814
3815/// Which listeners an event should reach.
3816///
3817/// DERIVED from `matches_filter_phase`, the phase-matching table, so the two
3818/// can no longer disagree. They used to be two hand-written tables that had
3819/// to be kept in step by discipline, and an exhaustive cross-product found 61
3820/// pairs where they had drifted - in BOTH directions. A filter the matcher
3821/// accepts but planning never emitted produced a listener that could never
3822/// fire (pen enter/leave, scroll start/end, every focus-scoped drag event);
3823/// a filter planned but not matched collected a callback and dropped it. The
3824/// visible symptoms were Enter/Space activating nothing and, briefly, a
3825/// pointer click activating a control TWICE.
3826///
3827/// Dispatch sites use this to decide which callbacks to COLLECT and then ask
3828/// the matcher again per callback, so "collected" and "fired" now answer to
3829/// one table.
3830///
3831/// The universe is the four `ALL_*` tables - Hover, Focus, Window, Component
3832/// - and it has to be ALL of them: a family missing from the probe is a whole
3833/// class of listeners that can never fire, and nothing else in the pipeline
3834/// notices (the events are still produced, the callbacks still registered).
3835/// That is how every lifecycle callback went dead when the derivation first
3836/// shipped without `ALL_COMPONENT`; the cross-product test
3837/// `planning_and_matching_agree_for_every_event_and_filter` now covers all
3838/// four families so that the next omission fails there.
3839///
3840/// Cost: one pass over the filter universe per EVENT (not per node, not per
3841/// callback) - a few hundred `match` arms, nanoseconds, and it happens once
3842/// where the old table was also built once.
3843#[must_use]
3844pub fn event_type_to_filters(event_type: EventType, event_data: &EventData) -> Vec<EventFilter> {
3845    // Built through the public constructor so a new field on `SyntheticEvent`
3846    // cannot silently change what planning probes with.
3847    let probe = SyntheticEvent::new(
3848        event_type,
3849        EventSource::User,
3850        DomNodeId {
3851            dom: DomId::ROOT_ID,
3852            node: crate::styled_dom::NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(0))),
3853        },
3854        crate::task::Instant::Tick(crate::task::SystemTick::new(0)),
3855        event_data.clone(),
3856    );
3857
3858    let mut out = Vec::new();
3859    for f in ALL_HOVER {
3860        if matches_filter_phase(EventFilter::Hover(*f), &probe, EventPhase::Bubble) {
3861            out.push(EventFilter::Hover(*f));
3862        }
3863    }
3864    for f in ALL_FOCUS {
3865        if matches_filter_phase(EventFilter::Focus(*f), &probe, EventPhase::Bubble) {
3866            out.push(EventFilter::Focus(*f));
3867        }
3868    }
3869    for f in ALL_WINDOW {
3870        if matches_filter_phase(EventFilter::Window(*f), &probe, EventPhase::Bubble) {
3871            out.push(EventFilter::Window(*f));
3872        }
3873    }
3874    // Component and Application were simply not probed here. Their matcher
3875    // arms (`matches_component_filter` / `matches_application_filter`) were
3876    // correct — but planning never asked them anything, so NO lifecycle or
3877    // application event could ever be planned, and every
3878    // `EventFilter::Component(..)` callback in every app was dead.
3879    for f in ALL_COMPONENT {
3880        if matches_filter_phase(EventFilter::Component(*f), &probe, EventPhase::Bubble) {
3881            out.push(EventFilter::Component(*f));
3882        }
3883    }
3884    for f in ALL_APPLICATION {
3885        if matches_filter_phase(EventFilter::Application(*f), &probe, EventPhase::Bubble) {
3886            out.push(EventFilter::Application(*f));
3887        }
3888    }
3889    for f in ALL_EXTERNAL {
3890        if matches_filter_phase(EventFilter::External(*f), &probe, EventPhase::Bubble) {
3891            out.push(EventFilter::External(*f));
3892        }
3893    }
3894    out
3895}
3896
3897// Internal System Event Processing
3898
3899/// Framework-determined side effects (system changes).
3900///
3901/// Unlike `CallbackChange` (from user callbacks), these are determined by the
3902/// framework's event analysis: hit tests, gesture detection, focus rules,
3903/// text selection, keyboard shortcuts, etc.
3904///
3905/// Both `CallbackChange` (user) and `SystemChange` (framework) are processed
3906/// through exhaustive match on `PlatformWindowV2` — adding a new variant
3907/// causes a compile error in `apply_system_change()`.
3908#[derive(Debug, Clone, PartialEq, Eq)]
3909#[must_use = "SystemChange must be processed through apply_system_change()"]
3910pub enum SystemChange {
3911    /// Advance layout animations by an EXACT step, bypassing the wall clock.
3912    ///
3913    /// Only the E2E surface emits this. A headless scenario cannot sample real
3914    /// time: the same test would land on a different point of the curve on a
3915    /// fast machine than a slow one, so any assertion mid-flight would be
3916    /// flaky. Stepping by a fixed `dt` makes the trajectory a pure function of
3917    /// how many times this ran.
3918    ///
3919    /// A `SystemChange` rather than a direct call because `CallbackInfo` hands
3920    /// out `&LayoutWindow` only — mutation is required to go through this
3921    /// channel, which is also where the mutable window actually exists.
3922    // === Text Selection ===
3923
3924    /// Process a mouse click for text selection (single/double/triple click).
3925    TextSelectionClick {
3926        position: LogicalPosition,
3927        timestamp: Instant,
3928    },
3929    /// Extend text selection via mouse drag.
3930    TextSelectionDrag {
3931        start_position: LogicalPosition,
3932        current_position: LogicalPosition,
3933    },
3934    /// Unified selection operation: cursor movement, selection extension, or deletion.
3935    ///
3936    /// Replaces the old `ArrowKeyNavigation` and `DeleteTextSelection` variants.
3937    /// Every keyboard shortcut maps to a single `SelectionOp` — see its docs.
3938    ApplySelectionOp {
3939        target: DomNodeId,
3940        op: SelectionOp,
3941        /// The SEAT whose key this was (9b-ii-a-i-d-ii-b): the op acts on that
3942        /// seat's caret. Seat 0 is the primary.
3943        seat_id: u64,
3944    },
3945
3946    // === Keyboard Shortcuts ===
3947    /// Copy selected text to system clipboard (Ctrl+C / Cmd+C).
3948    CopyToClipboard,
3949    /// Cut selected text to clipboard and delete (Ctrl+X / Cmd+X).
3950    CutToClipboard { target: DomNodeId },
3951    /// Paste text from system clipboard at cursor (Ctrl+V / Cmd+V).
3952    PasteFromClipboard,
3953    /// Select all text in focused node (Ctrl+A / Cmd+A).
3954    SelectAllText,
3955    /// Undo last text edit (Ctrl+Z / Cmd+Z).
3956    UndoTextEdit { target: DomNodeId },
3957    /// Redo last undone edit (Ctrl+Y / Ctrl+Shift+Z / Cmd+Shift+Z).
3958    RedoTextEdit { target: DomNodeId },
3959
3960    // === Multi-Cursor ===
3961    /// Add a cursor at the clicked position (Ctrl+Click).
3962    /// The position will be hit-tested to find the text cursor location.
3963    AddCursorAtClick { position: LogicalPosition },
3964    /// Select the next occurrence of the current selection's text (Ctrl+D).
3965    /// If the primary selection is a cursor, expand it to the word first.
3966    SelectNextOccurrence { target: DomNodeId },
3967    /// A NON-primary seat's editing shortcut (9b-ii-a-i-d-ii-b-i): Copy /
3968    /// Cut / Paste / Select-all / Undo / Redo on THAT seat's focus and caret.
3969    /// The primary's shortcuts stay the dedicated variants above.
3970    SeatShortcut {
3971        seat_id: u64,
3972        target: DomNodeId,
3973        shortcut: KeyboardShortcut,
3974    },
3975
3976    // === Text Input ===
3977    /// Apply pending text input from platform (keyboard/IME).
3978    ApplyPendingTextInput,
3979    /// Apply text changeset (incremental relayout).
3980    ApplyTextChangeset,
3981
3982    // === Drag & Drop ===
3983    /// Activate node drag on a draggable element.
3984    ActivateNodeDrag { dom_id: DomId, node_id: NodeId },
3985    /// Activate window drag (CSD titlebar).
3986    ActivateWindowDrag,
3987    /// Set up drag visual state (:dragging pseudo-state, GPU transform key).
3988    InitDragVisualState,
3989    /// Set :drag-over pseudo-state on a target node.
3990    SetDragOverState { target: DomNodeId, active: bool },
3991    /// Update current drop target in drag context.
3992    UpdateDropTarget { target: DomNodeId },
3993    /// Update GPU transform for active node drag.
3994    UpdateDragGpuTransform,
3995    /// End drag: clear pseudo-states, remove GPU keys, end drag session.
3996    DeactivateDrag,
3997
3998    // === Focus ===
3999    /// Move a NON-primary seat's focus (9b-ii-a-i-d): only that seat's
4000    /// entry changes - no `:focus` restyle, no caret / blink timer, no soft
4001    /// keyboard, which all follow the primary's focus (the text-edit session
4002    /// is one). A primary-seat change is `SetFocus`.
4003    SetSeatFocus {
4004        seat_id: u64,
4005        new_focus: Option<DomNodeId>,
4006        old_focus: Option<DomNodeId>,
4007    },
4008    /// Change focus to a new target (or clear focus if None).
4009    /// Handles: `set_focused_node`, `apply_focus_restyle`, `scroll_node_into_view`,
4010    /// `cursor_blink_timer` start/stop.
4011    SetFocus {
4012        new_focus: Option<DomNodeId>,
4013        old_focus: Option<DomNodeId>,
4014        /// Should this focus be INDICATED - the W3C `:focus-visible` question.
4015        ///
4016        /// It travels WITH the change because the handler restyles and
4017        /// regenerates the display list, and the focus ring is emitted by that
4018        /// regeneration. Setting the flag afterwards (which the shell used to
4019        /// do for the keyboard route) means the frame that was just built did
4020        /// not know focus was indicated, so the ring only appeared on whatever
4021        /// repaint happened NEXT - a click, a resize. On the device that read
4022        /// as "Tab does not ring anything until you click something", while the
4023        /// E2E harness - which happened to set the flag first - showed a ring
4024        /// and passed (device report, 2026-09-01).
4025        visible: bool,
4026    },
4027    /// Clear all text selections.
4028    ClearAllSelections,
4029    /// Finalize pending focus changes (cursor initialization after layout).
4030    FinalizePendingFocusChanges,
4031
4032    // === Scroll ===
4033    /// Scroll cursor/selection into view.
4034    ScrollSelectionIntoView,
4035    /// Scroll a specific node into view.
4036    ScrollNodeIntoView { target: DomNodeId },
4037    /// Scroll cursor into view after text input (needs relayout first).
4038    ScrollCursorIntoViewAfterTextInput,
4039
4040    // === Auto-Scroll Timer ===
4041    /// Start auto-scroll timer for drag-to-scroll (60Hz).
4042    StartAutoScrollTimer,
4043    /// Cancel auto-scroll timer.
4044    StopAutoScrollTimer,
4045}
4046
4047impl_option!(
4048    SystemChange,
4049    OptionSystemChange,
4050    copy = false,
4051    clone = false,
4052    [Debug, Clone, PartialEq, Eq]
4053);
4054
4055impl_vec!(
4056    SystemChange,
4057    SystemChangeVec,
4058    SystemChangeVecDestructor,
4059    SystemChangeVecDestructorType,
4060    SystemChangeVecSlice,
4061    OptionSystemChange
4062);
4063impl_vec_debug!(SystemChange, SystemChangeVec);
4064impl_vec_clone!(SystemChange, SystemChangeVec, SystemChangeVecDestructor);
4065impl_vec_partialeq!(SystemChange, SystemChangeVec);
4066
4067/// Result of pre-callback internal event filtering
4068#[derive(Debug, Clone, PartialEq)]
4069pub struct PreCallbackFilterResult {
4070    /// System changes to process BEFORE user callbacks
4071    pub system_changes: Vec<SystemChange>,
4072    /// Regular events that will be passed to user callbacks
4073    pub user_events: Vec<SyntheticEvent>,
4074}
4075
4076/// Flattened focus/selection state for the input interpreter (replaces trait objects).
4077#[derive(Debug, Clone, Copy)]
4078pub struct InputInterpreterState {
4079    pub focused_node: Option<DomNodeId>,
4080    pub click_count: u8,
4081    pub drag_start_position: Option<LogicalPosition>,
4082    pub has_selection: bool,
4083    /// Whether focus sits in a TEXT-EDITING context (a contenteditable host,
4084    /// a text input) - the only place arrow keys mean "move the caret".
4085    ///
4086    /// Without this the interpreter claimed every arrow key for a caret op
4087    /// and returned `AddAndSkip`, which SWALLOWS the event: it never reached
4088    /// user callbacks at all. A focused Slider or colour picker therefore
4089    /// could not implement arrow keys - the handler was never called (device
4090    /// report, 2026-09-01: "the arrow keys for sliders do not work at all,
4091    /// nor the four arrow keys for navigating the color gradient").
4092    pub focus_is_editable: bool,
4093}
4094
4095/// All context needed by the input interpreter to map events to system changes.
4096///
4097/// Passed to the interpreter callback. Contains references to the current
4098/// events and window state. The interpreter reads this and returns system changes.
4099#[derive(Debug)]
4100pub struct InputInterpreterInfo<'a> {
4101    pub events: &'a [SyntheticEvent],
4102    pub hit_test: Option<&'a FullHitTest>,
4103    pub keyboard_state: &'a crate::window::KeyboardState,
4104    pub mouse_state: &'a crate::window::MouseState,
4105    pub state: InputInterpreterState,
4106    /// The focused node of each NON-primary keyboard seat present in
4107    /// `events` (9b-ii-a-i-d), from `seat_focus_of_events`; a seat missing
4108    /// here resolves to the primary's `state.focused_node`.
4109    pub seat_focus: &'a [(u64, Option<DomNodeId>)],
4110}
4111
4112/// The `extern "C"` callback type for the input interpreter.
4113///
4114/// The first `RefAny` is the user data (vim mode, repeat counter, etc.)
4115/// held in `InputInterpreterCallback.ctx`. The `*const ()` is an opaque
4116/// pointer to `InputInterpreterInfo` — callers use the safe wrapper
4117/// methods to access event data. Returns a `PreCallbackFilterResult`.
4118///
4119/// For C/Python: the trampoline extracts the foreign callable from `RefAny.ctx`.
4120/// For Rust: use `InputInterpreterCallback::from(fn_ptr)` which sets ctx=None.
4121pub type InputInterpreterCallbackType = extern "C" fn(
4122    crate::refany::RefAny,
4123    *const InputInterpreterInfo<'static>, // Opaque; actual lifetime managed by caller
4124) -> PreCallbackFilterResult;
4125
4126/// Configurable input interpreter callback.
4127///
4128/// Maps raw platform events + window state → semantic `SystemChange` actions.
4129/// The default (`default_input_interpreter`) handles standard desktop keybindings.
4130/// Replace this on `LayoutWindow` to implement vim, game controls, etc.
4131///
4132/// ## Pattern
4133/// - **Rust**: `InputInterpreterCallback::from(my_fn_ptr)` — `ctx` is None
4134/// - **Python/C**: Set `cb` to a trampoline, `ctx` to `RefAny` wrapping the foreign callable
4135#[repr(C)]
4136pub struct InputInterpreterCallback {
4137    pub cb: InputInterpreterCallbackType,
4138    pub ctx: crate::refany::OptionRefAny,
4139}
4140
4141impl_callback!(InputInterpreterCallback, InputInterpreterCallbackType);
4142
4143impl Default for InputInterpreterCallback {
4144    fn default() -> Self {
4145        Self {
4146            cb: default_input_interpreter_extern,
4147            ctx: crate::refany::OptionRefAny::None,
4148        }
4149    }
4150}
4151
4152/// The `extern "C"` callback type for the post-callback filter.
4153pub type PostFilterCallbackType = extern "C" fn(
4154    crate::refany::RefAny,
4155    bool,                 // prevent_default
4156    SystemChangeVecSlice, // pre_changes (immutable slice)
4157    DomNodeId,            // old_focus (0xFFFF = None)
4158    DomNodeId,            // new_focus (0xFFFF = None)
4159) -> SystemChangeVec;
4160
4161/// Configurable post-callback filter.
4162#[repr(C)]
4163pub struct PostFilterCallback {
4164    pub cb: PostFilterCallbackType,
4165    pub ctx: crate::refany::OptionRefAny,
4166}
4167
4168impl_callback!(PostFilterCallback, PostFilterCallbackType);
4169
4170impl Default for PostFilterCallback {
4171    fn default() -> Self {
4172        Self {
4173            cb: default_post_filter_extern,
4174            ctx: crate::refany::OptionRefAny::None,
4175        }
4176    }
4177}
4178
4179/// What JSON type an op argument expects.
4180///
4181/// Spelled out rather than left to prose, because this is read by machines:
4182/// an agent choosing arguments and a UI validating a macro form both need the
4183/// type, not a sentence describing it.
4184#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4185#[cfg_attr(feature = "serde-json", derive(serde::Serialize, serde::Deserialize))]
4186#[cfg_attr(feature = "serde-json", serde(rename_all = "lowercase"))]
4187pub enum E2eOpArgType {
4188    String,
4189    Number,
4190    Bool,
4191    Object,
4192    Array,
4193    /// Any JSON value is acceptable.
4194    Any,
4195}
4196
4197/// One argument of one op.
4198#[derive(Debug, Clone, PartialEq, Eq, Default)]
4199#[cfg_attr(feature = "serde-json", derive(serde::Serialize, serde::Deserialize))]
4200pub struct E2eOpArg {
4201    pub name: String,
4202    #[cfg_attr(feature = "serde-json", serde(rename = "type"))]
4203    pub arg_type: E2eOpArgType,
4204    pub required: bool,
4205    pub description: String,
4206}
4207
4208impl Default for E2eOpArgType {
4209    fn default() -> Self {
4210        Self::Any
4211    }
4212}
4213
4214/// A worked example: what to send, and what comes back.
4215///
4216/// Both halves matter. The arguments alone tell a caller how to invoke the op;
4217/// the RETURN tells it what it can then assert on, which is what a scenario
4218/// author and an agent each need before committing to a call.
4219#[derive(Debug, Clone, PartialEq, Default)]
4220#[cfg_attr(feature = "serde-json", derive(serde::Serialize, serde::Deserialize))]
4221pub struct E2eOpExample {
4222    pub description: String,
4223    /// Example arguments — a real JSON value, not a string containing JSON.
4224    ///
4225    /// These were `String` first, which serialized as escaped JSON inside
4226    /// JSON: every consumer parsed twice and nothing checked the inner text
4227    /// was even well-formed, so a malformed example sat in the schema looking
4228    /// fine.
4229    pub args: crate::json::Json,
4230    /// What the op returns for those arguments.
4231    ///
4232    /// MUST contain a `success` boolean — validated when the schema is
4233    /// installed, see `E2eOpSchema::validate`.
4234    pub returns: crate::json::Json,
4235}
4236
4237/// One op the application answers.
4238#[derive(Debug, Clone, PartialEq, Default)]
4239#[cfg_attr(feature = "serde-json", derive(serde::Serialize, serde::Deserialize))]
4240pub struct E2eOpDef {
4241    pub name: String,
4242    /// One line, for a list or a picker.
4243    pub summary: String,
4244    /// The long form, for a tooltip or an agent's context.
4245    pub description: String,
4246    pub args: Vec<E2eOpArg>,
4247    pub examples: Vec<E2eOpExample>,
4248}
4249
4250/// Everything an application advertises about its ops.
4251///
4252/// Built in memory as a normal Rust struct and serialized to `Json` at the
4253/// boundary — on BOTH sides, framework and application. The `Json` hop is a
4254/// deliberate interim bridge so the shape can be iterated on without an ABI
4255/// break each time; these types get exposed through api.json later and the
4256/// bridge goes away.
4257#[derive(Debug, Clone, PartialEq, Default)]
4258#[cfg_attr(feature = "serde-json", derive(serde::Serialize, serde::Deserialize))]
4259pub struct E2eOpSchema {
4260    pub ops: Vec<E2eOpDef>,
4261}
4262
4263/// Does this value have a top-level `success` boolean?
4264///
4265/// Deliberately requires a BOOLEAN, not merely the key: `"success": "yes"` and
4266/// `"success": null` are the shapes a hand-written schema actually produces,
4267/// and each would otherwise pass while telling a consumer nothing.
4268// Only the `not(serde-json)` arm is const-eligible (it is a literal `true`);
4269// the serde arm calls `to_serde_value`, which allocates. Marking the fn `const`
4270// would therefore stop compiling in the configuration that actually parses.
4271#[allow(clippy::missing_const_for_fn)]
4272fn json_has_success_bool(v: &crate::json::Json) -> bool {
4273    #[cfg(feature = "serde-json")]
4274    {
4275        v.to_serde_value()
4276            .get("success")
4277            .is_some_and(serde_json::Value::is_boolean)
4278    }
4279    #[cfg(not(feature = "serde-json"))]
4280    {
4281        // Without serde there is no parser here. Returning TRUE would silently
4282        // pass every schema; the honest fallback is a textual check that can
4283        // only reject things that are definitely wrong.
4284        let _ = v;
4285        true
4286    }
4287}
4288
4289/// Why an advertised schema is unusable.
4290#[derive(Debug, Clone, PartialEq, Eq)]
4291pub enum E2eSchemaError {
4292    /// An op has no name.
4293    UnnamedOp { index: usize },
4294    /// Two ops share a name, so dispatch by name is ambiguous.
4295    DuplicateOpName { name: String },
4296    /// An argument has no name, or no usable type.
4297    UnnamedArg { op: String, index: usize },
4298    /// An example's `returns` has no `success` boolean.
4299    ///
4300    /// The contract is that every op result says whether it worked. An
4301    /// example that omits it is advertising a result shape the runtime is
4302    /// required to reject.
4303    ExampleMissingSuccess { op: String, index: usize },
4304}
4305
4306impl core::fmt::Display for E2eSchemaError {
4307    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
4308        match self {
4309            Self::UnnamedOp { index } => write!(f, "op #{index} has an empty name"),
4310            Self::DuplicateOpName { name } => {
4311                write!(
4312                    f,
4313                    "two ops are both named '{name}'; dispatch would be ambiguous"
4314                )
4315            }
4316            Self::UnnamedArg { op, index } => {
4317                write!(f, "op '{op}' argument #{index} has an empty name")
4318            }
4319            Self::ExampleMissingSuccess { op, index } => write!(
4320                f,
4321                "op '{op}' example #{index}: `returns` has no `success` boolean. Every op \
4322                 result must say whether it worked, or a failure is indistinguishable from a \
4323                 success"
4324            ),
4325        }
4326    }
4327}
4328
4329impl E2eOpSchema {
4330    /// Check the schema is usable, BEFORE anything reads or dispatches it.
4331    ///
4332    /// Called when the schema is installed, not on first invocation. The whole
4333    /// point of advertising a schema is that a plugin, an MCP server or an
4334    /// agent reads it before calling anything — a schema that only proves
4335    /// malformed when an op is finally invoked has failed at its one job. An
4336    /// app shipping an unusable advertisement is a bug in the app, and its own
4337    /// startup is the cheapest place to catch it.
4338    ///
4339    /// # Errors
4340    ///
4341    /// Returns [`E2eSchemaError`] if the schema is malformed: a duplicate or
4342    /// empty op name, or an argument whose declared type is not a valid JSON
4343    /// type name.
4344    pub fn validate(&self) -> Result<(), E2eSchemaError> {
4345        let mut seen: Vec<&str> = Vec::new();
4346        for (i, op) in self.ops.iter().enumerate() {
4347            if op.name.trim().is_empty() {
4348                return Err(E2eSchemaError::UnnamedOp { index: i });
4349            }
4350            if seen.contains(&op.name.as_str()) {
4351                return Err(E2eSchemaError::DuplicateOpName {
4352                    name: op.name.clone(),
4353                });
4354            }
4355            seen.push(op.name.as_str());
4356            for (a, arg) in op.args.iter().enumerate() {
4357                if arg.name.trim().is_empty() {
4358                    return Err(E2eSchemaError::UnnamedArg {
4359                        op: op.name.clone(),
4360                        index: a,
4361                    });
4362                }
4363            }
4364            for (e, ex) in op.examples.iter().enumerate() {
4365                if !json_has_success_bool(&ex.returns) {
4366                    return Err(E2eSchemaError::ExampleMissingSuccess {
4367                        op: op.name.clone(),
4368                        index: e,
4369                    });
4370                }
4371            }
4372        }
4373        Ok(())
4374    }
4375
4376    /// Serialize to the `Json` that crosses the C ABI.
4377    #[must_use]
4378    pub fn to_json(&self) -> crate::json::Json {
4379        #[cfg(feature = "serde-json")]
4380        {
4381            // Falling back to an empty-op object rather than to null: a
4382            // consumer must never be handed something that reads as "not
4383            // parseable" when the truth is "serialization failed".
4384            // serde_json preserves declaration order; `Json::parse` does NOT
4385            // (it re-serializes sorted, which buried `name` and `summary`
4386            // under `args`/`description`/`examples`). Keep the serialized text
4387            // and wrap it, rather than round-tripping through the parser.
4388            serde_json::to_string(self).ok().map_or_else(
4389                || crate::json::Json {
4390                    value_type: crate::json::JsonType::Object,
4391                    internal: crate::json::JsonInternal {
4392                        string_value: AzString::from_const_str(r#"{"ops":[]}"#),
4393                        ..Default::default()
4394                    },
4395                },
4396                |text| crate::json::Json {
4397                    value_type: crate::json::JsonType::Object,
4398                    internal: crate::json::JsonInternal {
4399                        string_value: AzString::from(text),
4400                        ..Default::default()
4401                    },
4402                },
4403            )
4404        }
4405        #[cfg(not(feature = "serde-json"))]
4406        crate::json::Json {
4407            value_type: crate::json::JsonType::Object,
4408            internal: crate::json::JsonInternal {
4409                string_value: AzString::from_const_str(r#"{"ops":[]}"#),
4410                ..Default::default()
4411            },
4412        }
4413    }
4414}
4415
4416/// Outcome of a user-defined E2E op.
4417#[repr(C)]
4418#[derive(Debug, Clone, PartialEq, Eq)]
4419pub struct CustomE2eOpResult {
4420    /// Whether the application RECOGNISED this op name.
4421    ///
4422    /// This is not "did it succeed" — it is "was this op mine at all". The
4423    /// debug server turns `false` into an explicit unknown-op error rather
4424    /// than an OK. Without it, a hook that returns a result for every name
4425    /// makes a typo'd op in a scenario indistinguishable from one that ran:
4426    /// non-assert ops produce no output of their own, so a silent success and
4427    /// a silent miss look identical from the outside.
4428    pub handled: bool,
4429    /// Result payload, JSON. Reported back to the scenario as-is, so a
4430    /// scenario can assert on it. Empty string is a valid empty result.
4431    pub json: AzString,
4432}
4433
4434impl Default for CustomE2eOpResult {
4435    /// "Not my op": `handled: false` with an empty payload — the value a
4436    /// bridge (e.g. the generated Python trampoline) returns when no user
4437    /// handler can run. Mirrors the debug server's unknown-op semantics.
4438    fn default() -> Self {
4439        Self {
4440            handled: false,
4441            json: AzString::from_const_str(""),
4442        }
4443    }
4444}
4445
4446/// The `extern "C"` callback type for a user-defined E2E op.
4447///
4448/// Receives the op name and its arguments as a JSON string, exactly as they
4449/// appeared in the scenario, and returns a `CustomE2eOpResult`. This is the
4450/// hook for driving application-level actions from a scenario — "now load the
4451/// document" — that the engine has no way to express on the app's behalf.
4452pub type CustomE2eOpCallbackType = extern "C" fn(
4453    crate::refany::RefAny, // ctx
4454    AzString,              // op name
4455    AzString,              // arguments, JSON
4456) -> CustomE2eOpResult;
4457
4458/// Application-provided handler for E2E ops the engine does not implement.
4459#[repr(C)]
4460pub struct CustomE2eOpCallback {
4461    pub cb: CustomE2eOpCallbackType,
4462    pub ctx: crate::refany::OptionRefAny,
4463    /// Describes every op `cb` answers: name, summary, description, the JSON
4464    /// type each argument expects, usage examples, and example returns.
4465    ///
4466    /// DATA, not a second callback — discovery is a field read, so it needs
4467    /// no invocation and no debug HTTP server. That is what lets a plugin
4468    /// enumerate host capabilities, a locally-spawned MCP server expose the
4469    /// app to an agent that would otherwise drive it by screenshot, or a
4470    /// `script.json` be handed straight to the binary.
4471    ///
4472    /// The examples and types are not documentation garnish: they are what a
4473    /// macro picker renders and what an agent reads in place of a screenshot.
4474    ///
4475    /// `Json` deliberately, not typed structs — the schema can then follow an
4476    /// OpenAPI-style operation shape and gain fields without an ABI break.
4477    /// Typed structs come later.
4478    pub op_schema: crate::json::Json,
4479}
4480
4481// `impl_callback_traits!` only ever reads `self.cb`, so it is correct here.
4482// The full `impl_callback!` is NOT usable: its generated `Clone` and `From`
4483// construct `Self { cb, ctx }` literally, and this struct has a third field.
4484impl_callback_traits!(CustomE2eOpCallback);
4485
4486impl Clone for CustomE2eOpCallback {
4487    fn clone(&self) -> Self {
4488        Self {
4489            cb: self.cb,
4490            ctx: self.ctx.clone(),
4491            op_schema: self.op_schema.clone(),
4492        }
4493    }
4494}
4495
4496impl From<CustomE2eOpCallbackType> for CustomE2eOpCallback {
4497    /// Installs a handler that advertises NOTHING.
4498    ///
4499    /// A bare fn pointer carries no schema, so this cannot invent one. An app
4500    /// converting from a fn pointer gets a working handler whose ops are
4501    /// undiscoverable until it sets `op_schema` — visible in a plugin listing
4502    /// as an empty op list, which is the honest answer rather than a guess.
4503    fn from(cb: CustomE2eOpCallbackType) -> Self {
4504        Self {
4505            cb,
4506            ..Self::default()
4507        }
4508    }
4509}
4510
4511impl Default for CustomE2eOpCallback {
4512    fn default() -> Self {
4513        Self {
4514            cb: default_custom_e2e_op_extern,
4515            ctx: crate::refany::OptionRefAny::None,
4516            // An empty LIST, not an empty string or a null. A consumer must
4517            // be able to tell "this app advertises no ops" from "this app
4518            // returned nothing parseable"; those mean different things to a
4519            // plugin deciding whether the host is usable at all.
4520            // An empty LIST, not an empty string or a null. A consumer must
4521            // be able to tell "this app advertises no ops" from "this app
4522            // returned nothing parseable"; those mean different things to a
4523            // plugin deciding whether the host is usable at all.
4524            op_schema: E2eOpSchema::default().to_json(),
4525        }
4526    }
4527}
4528
4529/// Default handler: recognises NOTHING.
4530///
4531/// `handled: false` is the load-bearing part. An app that has not installed a
4532/// handler must make a scenario referencing a custom op FAIL, not pass
4533/// quietly — the default has to be the safe answer, because it is the one
4534/// that ships when nobody thought about this.
4535#[must_use]
4536pub extern "C" fn default_custom_e2e_op_extern(
4537    _ctx: crate::refany::RefAny,
4538    _op: AzString,
4539    _args: AzString,
4540) -> CustomE2eOpResult {
4541    CustomE2eOpResult {
4542        handled: false,
4543        json: AzString::from_const_str(""),
4544    }
4545}
4546
4547// Keep simpler Rust fn pointer aliases for internal use
4548pub type InputInterpreterFn = fn(info: &InputInterpreterInfo<'_>) -> PreCallbackFilterResult;
4549
4550pub type PostFilterFn = fn(
4551    prevent_default: bool,
4552    pre_changes: &[SystemChange],
4553    old_focus: Option<DomNodeId>,
4554    new_focus: Option<DomNodeId>,
4555) -> Vec<SystemChange>;
4556
4557/// Mouse button state for drag tracking
4558#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4559pub struct MouseButtonState {
4560    pub left_down: bool,
4561    pub right_down: bool,
4562    pub middle_down: bool,
4563}
4564
4565/// Arrow key / cursor navigation directions
4566#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4567pub enum ArrowDirection {
4568    Left,
4569    Right,
4570    Up,
4571    Down,
4572    /// Home key: move to start of current line
4573    LineStart,
4574    /// End key: move to end of current line
4575    LineEnd,
4576    /// Ctrl+Home: move to start of document
4577    DocumentStart,
4578    /// Ctrl+End: move to end of document
4579    DocumentEnd,
4580}
4581
4582impl ArrowDirection {
4583    /// Map a `VirtualKeyCode` plus the `ctrl` modifier into an `ArrowDirection`.
4584    /// Returns `None` if the key is not a navigation key.
4585    #[must_use]
4586    pub const fn from_key(vk: crate::window::VirtualKeyCode, ctrl: bool) -> Option<Self> {
4587        use crate::window::VirtualKeyCode::{Down, End, Home, Left, Right, Up};
4588        Some(match vk {
4589            Left => Self::Left,
4590            Right => Self::Right,
4591            Up => Self::Up,
4592            Down => Self::Down,
4593            Home if ctrl => Self::DocumentStart,
4594            Home => Self::LineStart,
4595            End if ctrl => Self::DocumentEnd,
4596            End => Self::LineEnd,
4597            _ => return None,
4598        })
4599    }
4600
4601    /// Convert to a `(SelectionDirection, SelectionStep)` pair for the
4602    /// selection-op interpreter. `ctrl` upgrades arrow keys to word jumps.
4603    #[must_use]
4604    pub const fn to_selection(self, ctrl: bool) -> (SelectionDirection, SelectionStep) {
4605        match self {
4606            Self::Left if ctrl => (SelectionDirection::Backward, SelectionStep::Word),
4607            Self::Right if ctrl => (SelectionDirection::Forward, SelectionStep::Word),
4608            Self::Left => (SelectionDirection::Backward, SelectionStep::Character),
4609            Self::Right => (SelectionDirection::Forward, SelectionStep::Character),
4610            Self::Up => (SelectionDirection::Backward, SelectionStep::VisualLine),
4611            Self::Down => (SelectionDirection::Forward, SelectionStep::VisualLine),
4612            Self::LineStart => (SelectionDirection::Backward, SelectionStep::Line),
4613            Self::LineEnd => (SelectionDirection::Forward, SelectionStep::Line),
4614            Self::DocumentStart => (SelectionDirection::Backward, SelectionStep::Document),
4615            Self::DocumentEnd => (SelectionDirection::Forward, SelectionStep::Document),
4616        }
4617    }
4618}
4619
4620/// Direction of cursor movement or selection expansion.
4621#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4622#[repr(C)]
4623pub enum SelectionDirection {
4624    Forward,
4625    Backward,
4626}
4627
4628/// Granularity of cursor movement or selection expansion.
4629///
4630/// Combined with `SelectionDirection`, determines how far a cursor moves
4631/// or a selection expands. Reused for navigation, deletion, and visual
4632/// selection — a single code path for word boundaries etc.
4633#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4634#[repr(C)]
4635pub enum SelectionStep {
4636    /// One grapheme cluster (arrow keys, Backspace, Delete)
4637    Character,
4638    /// One word boundary (Ctrl+arrow, Ctrl+Backspace, Ctrl+Delete)
4639    Word,
4640    /// To line boundary (Home/End)
4641    Line,
4642    /// One visual line up/down (Up/Down arrows)
4643    VisualLine,
4644    /// To document boundary (Ctrl+Home/End)
4645    Document,
4646}
4647
4648/// What to do with the selection after moving.
4649#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4650#[repr(C)]
4651pub enum SelectionMode {
4652    /// Collapse selection to cursor, then move (plain arrow key).
4653    Move,
4654    /// Extend selection from anchor to new position (Shift+arrow).
4655    Extend,
4656    /// Expand cursor to range in the given direction, then delete the range
4657    /// (Backspace/Delete). If a range already exists, just delete it.
4658    Delete,
4659}
4660
4661/// A unified selection operation that replaces all cursor movement,
4662/// selection extension, and text deletion commands.
4663///
4664/// Every keyboard shortcut for cursor movement or deletion maps to this:
4665/// - Arrow Left = (Backward, Character, Move, 1)
4666/// - Shift+Right = (Forward, Character, Extend, 1)
4667/// - Ctrl+Backspace = (Backward, Word, Delete, 1)
4668/// - Home = (Backward, Line, Move, 1)
4669/// - Ctrl+End = (Forward, Document, Move, 1)
4670///
4671/// The `repeat` field enables vim-style commands: 3w = (Forward, Word, Move, 3).
4672#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4673#[repr(C)]
4674pub struct SelectionOp {
4675    pub direction: SelectionDirection,
4676    pub step: SelectionStep,
4677    pub mode: SelectionMode,
4678    pub repeat: usize,
4679}
4680
4681impl SelectionOp {
4682    #[must_use]
4683    pub const fn new(
4684        direction: SelectionDirection,
4685        step: SelectionStep,
4686        mode: SelectionMode,
4687    ) -> Self {
4688        Self {
4689            direction,
4690            step,
4691            mode,
4692            repeat: 1,
4693        }
4694    }
4695}
4696
4697/// Keyboard shortcuts for text editing
4698#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4699pub enum KeyboardShortcut {
4700    Copy,      // Ctrl+C
4701    Cut,       // Ctrl+X
4702    Paste,     // Ctrl+V
4703    SelectAll, // Ctrl+A
4704    Undo,      // Ctrl+Z
4705    Redo,      // Ctrl+Y or Ctrl+Shift+Z
4706}
4707
4708impl KeyboardShortcut {
4709    /// Map a `(VirtualKeyCode, primary, shift)` triple to a text-editing
4710    /// shortcut. Returns `None` if the key combination is not a recognized
4711    /// shortcut or if `primary` is not held. `primary` is the platform's
4712    /// primary modifier — Cmd on macOS, Ctrl elsewhere — obtained from
4713    /// `KeyboardState::primary_down()` (MWA-A2: hardcoding Ctrl here made
4714    /// every editing shortcut dead on macOS).
4715    #[must_use]
4716    pub const fn from_key(
4717        vk: crate::window::VirtualKeyCode,
4718        primary: bool,
4719        shift: bool,
4720    ) -> Option<Self> {
4721        use crate::window::VirtualKeyCode::{A, C, V, X, Y, Z};
4722        if !primary {
4723            return None;
4724        }
4725        Some(match vk {
4726            C => Self::Copy,
4727            X => Self::Cut,
4728            V => Self::Paste,
4729            A => Self::SelectAll,
4730            Z if shift => Self::Redo,
4731            Z => Self::Undo,
4732            Y => Self::Redo,
4733            _ => return None,
4734        })
4735    }
4736}
4737
4738/// Default input interpreter: standard desktop keybindings.
4739///
4740/// This is the default `InputInterpreterFn` that handles arrow keys, Home/End,
4741/// Backspace/Delete, Ctrl+C/V/A/Z, mouse clicks, and drag selection.
4742/// Replace it on `LayoutWindow` to implement vim, game controls, etc.
4743/// `extern "C"` trampoline for `default_input_interpreter`.
4744#[allow(clippy::not_unsafe_ptr_arg_deref)]
4745// SAFETY/FFI: `*const T` is the C-ABI signature; the fn null-checks then derefs under the documented caller contract (C guarantees a valid ptr/len). Marking it `unsafe fn` would force unsafe blocks into the generated dll bindings.
4746#[must_use]
4747pub extern "C" fn default_input_interpreter_extern(
4748    _user_data: crate::refany::RefAny,
4749    info_ptr: *const InputInterpreterInfo<'static>,
4750) -> PreCallbackFilterResult {
4751    if info_ptr.is_null() {
4752        return PreCallbackFilterResult {
4753            system_changes: Vec::new(),
4754            user_events: Vec::new(),
4755        };
4756    }
4757    let info = unsafe { &*info_ptr };
4758    default_input_interpreter(info)
4759}
4760
4761/// `extern "C"` trampoline for `default_post_filter`.
4762#[must_use]
4763pub extern "C" fn default_post_filter_extern(
4764    _user_data: crate::refany::RefAny,
4765    prevent_default: bool,
4766    pre_changes: SystemChangeVecSlice,
4767    old_focus: DomNodeId,
4768    new_focus: DomNodeId,
4769) -> SystemChangeVec {
4770    let pre_changes_slice = pre_changes.as_slice();
4771    let old = old_focus.node.into_crate_internal().map(|_| old_focus);
4772    let new = new_focus.node.into_crate_internal().map(|_| new_focus);
4773    default_post_filter(prevent_default, pre_changes_slice, old, new).into()
4774}
4775
4776#[must_use]
4777pub fn default_input_interpreter(info: &InputInterpreterInfo<'_>) -> PreCallbackFilterResult {
4778    let ctx = FilterContext {
4779        hit_test: info.hit_test,
4780        keyboard_state: info.keyboard_state,
4781        mouse_state: info.mouse_state,
4782        click_count: info.state.click_count,
4783        focused_node: info.state.focused_node,
4784        drag_start_position: info.state.drag_start_position,
4785        focus_is_editable: info.state.focus_is_editable,
4786        seat_focus: info.seat_focus,
4787    };
4788
4789    let (system_changes, user_events) = info.events.iter().fold(
4790        (Vec::new(), Vec::new()),
4791        |(mut internal, mut user), event| {
4792            match process_event_for_internal(&ctx, event) {
4793                Some(InternalEventAction::AddAndSkip(evt)) => {
4794                    internal.push(evt);
4795                }
4796                Some(InternalEventAction::AddAndPass(evt)) => {
4797                    internal.push(evt);
4798                    user.push(event.clone());
4799                }
4800                None => {
4801                    user.push(event.clone());
4802                }
4803            }
4804            (internal, user)
4805        },
4806    );
4807
4808    PreCallbackFilterResult {
4809        system_changes,
4810        user_events,
4811    }
4812}
4813
4814/// Backward-compatible wrapper that calls `default_input_interpreter`.
4815pub fn pre_callback_filter_internal_events<SM, FM>(
4816    events: &[SyntheticEvent],
4817    hit_test: Option<&FullHitTest>,
4818    keyboard_state: &crate::window::KeyboardState,
4819    mouse_state: &crate::window::MouseState,
4820    selection_manager: &SM,
4821    focus_manager: &FM,
4822    // See `InputInterpreterState::focus_is_editable` - only a text-editing
4823    // focus owns the arrow keys.
4824    focus_is_editable: bool,
4825) -> PreCallbackFilterResult
4826where
4827    SM: SelectionManagerQuery,
4828    FM: FocusManagerQuery,
4829{
4830    let seat_focus = seat_focus_of_events(events, focus_manager);
4831    let info = InputInterpreterInfo {
4832        events,
4833        hit_test,
4834        keyboard_state,
4835        mouse_state,
4836        seat_focus: &seat_focus,
4837        state: InputInterpreterState {
4838            focused_node: focus_manager.get_focused_node_id(),
4839            click_count: selection_manager.get_click_count(),
4840            drag_start_position: selection_manager.get_drag_start_position(),
4841            has_selection: selection_manager.has_selection(),
4842            focus_is_editable,
4843        },
4844    };
4845    default_input_interpreter(&info)
4846}
4847
4848/// Context for filtering internal events (used by `default_input_interpreter`)
4849struct FilterContext<'a> {
4850    hit_test: Option<&'a FullHitTest>,
4851    keyboard_state: &'a crate::window::KeyboardState,
4852    mouse_state: &'a crate::window::MouseState,
4853    click_count: u8,
4854    focused_node: Option<DomNodeId>,
4855    drag_start_position: Option<LogicalPosition>,
4856    /// See `InputInterpreterState::focus_is_editable`.
4857    focus_is_editable: bool,
4858    /// See `InputInterpreterInfo::seat_focus`.
4859    seat_focus: &'a [(u64, Option<DomNodeId>)],
4860}
4861
4862impl FilterContext<'_> {
4863    /// The focus a key event of seat `seat_id` acts on (9b-ii-a-i-d).
4864    fn focused_node_for(&self, seat_id: u64) -> Option<DomNodeId> {
4865        if seat_id == crate::window::PRIMARY_POINTER_SEAT {
4866            return self.focused_node;
4867        }
4868        self.seat_focus
4869            .iter()
4870            .find(|(seat, _)| *seat == seat_id)
4871            .map_or(self.focused_node, |(_, focus)| *focus)
4872    }
4873}
4874
4875/// Process a single event and determine if it generates an internal event
4876fn process_event_for_internal(
4877    ctx: &FilterContext<'_>,
4878    event: &SyntheticEvent,
4879) -> Option<InternalEventAction> {
4880    match event.event_type {
4881        EventType::MouseDown => handle_mouse_down(
4882            event,
4883            ctx.hit_test,
4884            ctx.click_count,
4885            ctx.mouse_state,
4886            ctx.keyboard_state,
4887        ),
4888        // Drag-selection needs MOVEMENT while the button is down, so it
4889        // follows `MouseMove`. Leaving it on `MouseOver` after that event
4890        // became entry-only would have silently killed drag-to-select: the
4891        // handler would fire once on entry and never again.
4892        EventType::MouseMove => handle_mouse_move(
4893            event,
4894            ctx.hit_test,
4895            ctx.mouse_state,
4896            ctx.drag_start_position,
4897        ),
4898        EventType::KeyDown => {
4899            // A seat's key acts on THAT seat's focus (9b-ii-a-i-d).
4900            let seat_id = match &event.data {
4901                EventData::Keyboard(k) => k.seat_id,
4902                _ => crate::window::PRIMARY_POINTER_SEAT,
4903            };
4904            handle_key_down(
4905                event,
4906                ctx.keyboard_state,
4907                ctx.focused_node_for(seat_id),
4908                ctx.focus_is_editable,
4909            )
4910        }
4911        EventType::MouseUp => Some(handle_mouse_up()),
4912        _ => None,
4913    }
4914}
4915
4916/// Releasing the button ends a text-selection drag, so the autoscroll timer
4917/// has to go.
4918///
4919/// `StopAutoScrollTimer` existed and was handled, but NOTHING emitted it:
4920/// teardown relied entirely on the 60Hz callback noticing on its next tick
4921/// that the button had been released and terminating itself. Any path that
4922/// loses the release — a grab broken by the window manager, a crossing that
4923/// swallows it — left a timer running at 60Hz for the life of the window.
4924///
4925/// It passes to callbacks: a `MouseUp` is a user event, and stopping an
4926/// internal timer must not swallow it.
4927const fn handle_mouse_up() -> InternalEventAction {
4928    InternalEventAction::AddAndPass(SystemChange::StopAutoScrollTimer)
4929}
4930
4931/// Action to take after processing an event for internal system events
4932enum InternalEventAction {
4933    /// Add system change and skip passing to user callbacks
4934    AddAndSkip(SystemChange),
4935    /// Add system change but also pass to user callbacks
4936    AddAndPass(SystemChange),
4937}
4938
4939/// Extract the front-most hovered node from a hit test.
4940///
4941/// Picks the node with the minimum `hit_depth` (0 = frontmost/topmost in
4942/// z-order) across every hovered DOM. The previous implementation took the
4943/// first entry of the `BTreeMap` (lowest `NodeId`), which ignored z-order
4944/// entirely and targeted the back-most node under overlapping elements.
4945/// Ties are broken deterministically by (`DomId`, `NodeId`) iteration order.
4946fn get_first_hovered_node(hit_test: Option<&FullHitTest>) -> Option<DomNodeId> {
4947    let ht = hit_test?;
4948    let mut best: Option<(DomId, NodeId, u32)> = None;
4949    for (dom_id, hit_data) in &ht.hovered_nodes {
4950        for (node_id, item) in &hit_data.regular_hit_test_nodes {
4951            let is_better = match best {
4952                None => true,
4953                Some((_, _, best_depth)) => item.hit_depth < best_depth,
4954            };
4955            if is_better {
4956                best = Some((*dom_id, *node_id, item.hit_depth));
4957            }
4958        }
4959    }
4960    let (dom_id, node_id, _) = best?;
4961    Some(DomNodeId {
4962        dom: dom_id,
4963        node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
4964    })
4965}
4966
4967/// Extract mouse position from event data, falling back to `mouse_state` if not available
4968fn get_mouse_position_with_fallback(
4969    event: &SyntheticEvent,
4970    mouse_state: &crate::window::MouseState,
4971) -> LogicalPosition {
4972    match &event.data {
4973        EventData::Mouse(mouse_data) => mouse_data.position,
4974        _ => {
4975            // Fallback: use current cursor position from mouse_state
4976            // This handles synthetic events from debug API and automation
4977            // where EventData may not contain the mouse position
4978            mouse_state
4979                .cursor_position
4980                .get_position()
4981                .unwrap_or(LogicalPosition::zero())
4982        }
4983    }
4984}
4985
4986/// Handle `MouseDown` event - detect text selection clicks and Ctrl+Click for multi-cursor
4987fn handle_mouse_down(
4988    event: &SyntheticEvent,
4989    hit_test: Option<&FullHitTest>,
4990    click_count: u8,
4991    mouse_state: &crate::window::MouseState,
4992    keyboard_state: &crate::window::KeyboardState,
4993) -> Option<InternalEventAction> {
4994    let effective_click_count = if click_count == 0 { 1 } else { click_count };
4995
4996    if effective_click_count > 3 {
4997        return None;
4998    }
4999
5000    let _target = get_first_hovered_node(hit_test)?;
5001    let position = get_mouse_position_with_fallback(event, mouse_state);
5002
5003    // Ctrl+Click (or Cmd+Click on macOS): add cursor at click position.
5004    // Use the platform PRIMARY modifier so this fires on Cmd on macOS
5005    // (where Ctrl+Click is the secondary-click gesture) — `ctrl_down()`
5006    // was wrong there.
5007    if keyboard_state.primary_down() && effective_click_count == 1 {
5008        return Some(InternalEventAction::AddAndPass(
5009            SystemChange::AddCursorAtClick { position },
5010        ));
5011    }
5012
5013    Some(InternalEventAction::AddAndPass(
5014        SystemChange::TextSelectionClick {
5015            position,
5016            timestamp: event.timestamp.clone(),
5017        },
5018    ))
5019}
5020
5021/// Handle `MouseMove` event - detect drag selection
5022fn handle_mouse_move(
5023    event: &SyntheticEvent,
5024    _hit_test: Option<&FullHitTest>,
5025    mouse_state: &crate::window::MouseState,
5026    drag_start_position: Option<LogicalPosition>,
5027) -> Option<InternalEventAction> {
5028    if !mouse_state.left_down {
5029        return None;
5030    }
5031
5032    let start_position = drag_start_position?;
5033
5034    // Deliberately NOT gated on a hovered hit node. A drag that leaves the
5035    // text — into the container's padding, over a gap, past the last line —
5036    // still extends the selection in every native editor, and the endpoint is
5037    // resolved from the pointer position against the ANCHOR block's layout
5038    // (`process_mouse_drag_for_selection`), not from whatever happens to be
5039    // under the cursor. Requiring a hit node froze the selection exactly where
5040    // the user was reaching for more of it.
5041    let current_position = get_mouse_position_with_fallback(event, mouse_state);
5042
5043    Some(InternalEventAction::AddAndPass(
5044        SystemChange::TextSelectionDrag {
5045            start_position,
5046            current_position,
5047        },
5048    ))
5049}
5050
5051/// Handle `KeyDown` event - detect shortcuts, arrow keys, and delete keys
5052fn handle_key_down(
5053    event: &SyntheticEvent,
5054    keyboard_state: &crate::window::KeyboardState,
5055    focused_node: Option<DomNodeId>,
5056    focus_is_editable: bool,
5057) -> Option<InternalEventAction> {
5058    use crate::window::VirtualKeyCode;
5059
5060    let target = focused_node?;
5061    let EventData::Keyboard(kbd) = &event.data else {
5062        return None;
5063    };
5064
5065    // Read the key and modifiers from THIS event's payload, not from the live
5066    // `keyboard_state`. The live state can have advanced (another key pressed /
5067    // released) between when the event was queued and when it is dispatched, so
5068    // reading it here could act on the wrong key/modifiers. `keyboard_state` is
5069    // retained only for the platform where the event does not carry a key.
5070    let _ = keyboard_state;
5071
5072    // MWA-A2: standard shortcuts key off the PRIMARY modifier (Cmd on
5073    // macOS, Ctrl elsewhere); word-jump / word-delete keys off the
5074    // platform's word modifier (Option on macOS, Ctrl elsewhere).
5075    let primary = if cfg!(target_os = "macos") {
5076        kbd.modifiers.meta
5077    } else {
5078        kbd.modifiers.ctrl
5079    };
5080    let word_mod = if cfg!(target_os = "macos") {
5081        kbd.modifiers.alt
5082    } else {
5083        kbd.modifiers.ctrl
5084    };
5085    let shift = kbd.modifiers.shift;
5086    let vk_owned = VirtualKeyCode::from_u32(kbd.key_code)?;
5087    let vk = &vk_owned;
5088
5089    // Check keyboard shortcuts (primary+key) → emit specific SystemChange
5090    // variants. Standard editing shortcuts are routed through the
5091    // `KeyboardShortcut` enum, and a couple of additional Azul-specific
5092    // primary-modifier combos are matched after.
5093    if primary {
5094        if let Some(shortcut) = KeyboardShortcut::from_key(*vk, primary, shift) {
5095            // A second seat's shortcut acts on ITS caret (9b-ii-a-i-d-ii-b-i).
5096            if kbd.seat_id != crate::window::PRIMARY_POINTER_SEAT {
5097                return Some(InternalEventAction::AddAndSkip(SystemChange::SeatShortcut {
5098                    seat_id: kbd.seat_id,
5099                    target,
5100                    shortcut,
5101                }));
5102            }
5103            let change = match shortcut {
5104                KeyboardShortcut::Copy => SystemChange::CopyToClipboard,
5105                KeyboardShortcut::Cut => SystemChange::CutToClipboard { target },
5106                KeyboardShortcut::Paste => SystemChange::PasteFromClipboard,
5107                KeyboardShortcut::SelectAll => SystemChange::SelectAllText,
5108                KeyboardShortcut::Undo => SystemChange::UndoTextEdit { target },
5109                KeyboardShortcut::Redo => SystemChange::RedoTextEdit { target },
5110            };
5111            return Some(InternalEventAction::AddAndSkip(change));
5112        }
5113        if matches!(vk, VirtualKeyCode::D) {
5114            // Ctrl+D adds a multi-cursor, which is the primary's alone
5115            // (9b-ii-a-i-d-ii-b-i): a seat's passes through to callbacks.
5116            if kbd.seat_id != crate::window::PRIMARY_POINTER_SEAT {
5117                return None;
5118            }
5119            return Some(InternalEventAction::AddAndSkip(
5120                SystemChange::SelectNextOccurrence { target },
5121            ));
5122        }
5123    }
5124
5125    // Unified: arrow keys, Home/End, Backspace/Delete all map to SelectionOp.
5126    let mode_for_shift = if shift {
5127        SelectionMode::Extend
5128    } else {
5129        SelectionMode::Move
5130    };
5131    let selection_op = if let Some(arrow) = ArrowDirection::from_key(*vk, word_mod) {
5132        // ONLY a text-editing context owns the arrows. Anywhere else they
5133        // belong to the focused widget (a slider steps its value, a colour
5134        // plane moves its marker) and, failing that, to the scroll default
5135        // action. Claiming them here returned `AddAndSkip`, which swallowed
5136        // the key before any callback could see it.
5137        if !focus_is_editable {
5138            return None;
5139        }
5140        let (direction, step) = arrow.to_selection(word_mod);
5141        SelectionOp::new(direction, step, mode_for_shift)
5142    } else {
5143        match vk {
5144            // Backspace/Delete = Delete mode (word modifier upgrades to
5145            // Word: Option+Backspace on macOS, Ctrl+Backspace elsewhere)
5146            VirtualKeyCode::Back => SelectionOp::new(
5147                SelectionDirection::Backward,
5148                if word_mod {
5149                    SelectionStep::Word
5150                } else {
5151                    SelectionStep::Character
5152                },
5153                SelectionMode::Delete,
5154            ),
5155            VirtualKeyCode::Delete => SelectionOp::new(
5156                SelectionDirection::Forward,
5157                if word_mod {
5158                    SelectionStep::Word
5159                } else {
5160                    SelectionStep::Character
5161                },
5162                SelectionMode::Delete,
5163            ),
5164            _ => return None,
5165        }
5166    };
5167
5168    Some(InternalEventAction::AddAndSkip(
5169        SystemChange::ApplySelectionOp {
5170            target,
5171            op: selection_op,
5172            seat_id: kbd.seat_id,
5173        },
5174    ))
5175}
5176
5177/// Trait for querying selection manager state.
5178///
5179/// This allows `pre_callback_filter_internal_events` to query manager state
5180/// without depending on the concrete `SelectionManager` type from layout crate.
5181pub trait SelectionManagerQuery {
5182    /// Get the current click count (1 = single, 2 = double, 3 = triple)
5183    fn get_click_count(&self) -> u8;
5184
5185    /// Get the drag start position if a drag is in progress
5186    fn get_drag_start_position(&self) -> Option<LogicalPosition>;
5187
5188    /// Check if any selection exists (click selection or drag selection)
5189    fn has_selection(&self) -> bool;
5190}
5191
5192/// Trait for querying focus manager state.
5193///
5194/// This allows `pre_callback_filter_internal_events` to query manager state
5195/// without depending on the concrete `FocusManager` type from layout crate.
5196pub trait FocusManagerQuery {
5197    /// Get the currently focused node ID
5198    fn get_focused_node_id(&self) -> Option<DomNodeId>;
5199
5200    /// The node seat `seat_id` focuses (9b-ii-a-i-d). A manager without
5201    /// per-seat focus answers the shared one for every seat.
5202    fn get_focused_node_for_seat(&self, seat_id: u64) -> Option<DomNodeId> {
5203        let _ = seat_id;
5204        self.get_focused_node_id()
5205    }
5206}
5207
5208/// The focused node of every NON-primary keyboard seat that `events` carry
5209/// (9b-ii-a-i-d), for `InputInterpreterInfo::seat_focus`: the interpreter
5210/// resolves a seat's key against that seat's focus, not the primary's.
5211#[must_use]
5212pub fn seat_focus_of_events<FM: FocusManagerQuery + ?Sized>(
5213    events: &[SyntheticEvent],
5214    focus_manager: &FM,
5215) -> Vec<(u64, Option<DomNodeId>)> {
5216    let mut out: Vec<(u64, Option<DomNodeId>)> = Vec::new();
5217    for event in events {
5218        let EventData::Keyboard(k) = &event.data else {
5219            continue;
5220        };
5221        if k.seat_id == crate::window::PRIMARY_POINTER_SEAT
5222            || out.iter().any(|(seat, _)| *seat == k.seat_id)
5223        {
5224            continue;
5225        }
5226        out.push((k.seat_id, focus_manager.get_focused_node_for_seat(k.seat_id)));
5227    }
5228    out
5229}
5230
5231/// Post-callback filter: Determine additional system changes needed after user callbacks.
5232///
5233/// Takes the pre-callback system changes and focus state to determine what
5234/// post-callback system changes are needed (text input, scrolling, timers).
5235/// Default post-callback filter: scroll-into-view after cursor ops, auto-scroll during drag.
5236#[must_use]
5237pub fn default_post_filter(
5238    prevent_default: bool,
5239    pre_changes: &[SystemChange],
5240    old_focus: Option<DomNodeId>,
5241    new_focus: Option<DomNodeId>,
5242) -> Vec<SystemChange> {
5243    post_callback_filter_system_changes(prevent_default, pre_changes, old_focus, new_focus)
5244}
5245
5246// SystemChange dispatch table; a few arms incidentally push the same follow-up
5247// change but are kept as distinct documented cases.
5248#[allow(clippy::match_same_arms)]
5249#[must_use]
5250pub fn post_callback_filter_system_changes(
5251    prevent_default: bool,
5252    pre_changes: &[SystemChange],
5253    old_focus: Option<DomNodeId>,
5254    new_focus: Option<DomNodeId>,
5255) -> Vec<SystemChange> {
5256    let mut changes = Vec::new();
5257
5258    if prevent_default {
5259        // Only focus change passes through preventDefault
5260        if old_focus != new_focus {
5261            changes.push(SystemChange::SetFocus {
5262                new_focus,
5263                old_focus,
5264                // A focus move a callback made (or that survived
5265                // preventDefault) is programmatic, not a keyboard walk: focused
5266                // but not indicated, like `autofocus`.
5267                visible: false,
5268            });
5269        }
5270        return changes;
5271    }
5272
5273    // Always apply pending text input
5274    changes.push(SystemChange::ApplyPendingTextInput);
5275
5276    // Determine post-callback actions based on pre-callback system changes
5277    for change in pre_changes {
5278        match change {
5279            SystemChange::TextSelectionClick { .. }
5280            | SystemChange::ApplySelectionOp { .. }
5281            | SystemChange::AddCursorAtClick { .. }
5282            | SystemChange::SelectNextOccurrence { .. } => {
5283                changes.push(SystemChange::ScrollSelectionIntoView);
5284            }
5285            SystemChange::TextSelectionDrag { .. } => {
5286                changes.push(SystemChange::StartAutoScrollTimer);
5287            }
5288            SystemChange::CutToClipboard { .. }
5289            | SystemChange::PasteFromClipboard
5290            | SystemChange::UndoTextEdit { .. }
5291            | SystemChange::RedoTextEdit { .. }
5292            | SystemChange::SelectAllText => {
5293                changes.push(SystemChange::ScrollSelectionIntoView);
5294            }
5295            // Other system changes don't generate post-callback actions
5296            _ => {}
5297        }
5298    }
5299
5300    // Focus changed during callbacks
5301    if old_focus != new_focus {
5302        changes.push(SystemChange::SetFocus {
5303            new_focus,
5304            old_focus,
5305            visible: false,
5306        });
5307    }
5308
5309    changes
5310}
5311
5312#[cfg(test)]
5313#[path = "events_test.rs"]
5314mod events_test;