Skip to main content

dioxus_elements/
event.rs

1//! Shared event payload types bridging ArkUI native events into dioxus.
2//!
3//! When an ArkUI event fires, the renderer wraps the payload in an
4//! [`ArkEventData`] and forwards it to `Runtime::handle_event`. The listener
5//! installed by `impl_event!` downcasts the `Rc<dyn Any>` back to
6//! `ArkEventData` and converts it into the specific event data type
7//! (e.g. [`ClickData`], [`ChangeData`]).
8//!
9//! ## Payload carrying
10//! Each typed data type ([`ChangeData`], [`ScrollData`], ...) reads fields from
11//! [`ArkEventData::payload`]. Native payloads are populated at the ArkUI event
12//! boundary; `None` remains the explicit representation for payload-free
13//! events such as refresh and submit-fire.
14
15/// Semantic identity of an ArkUI event listener.
16///
17/// Dioxus passes listener names to renderers after removing the leading
18/// `on`, while the public RSX surface intentionally accepts both compact
19/// (`onclick`) and readable (`on_click`) spellings. Keeping this classification
20/// in the event-owning crate prevents the renderer and runtime from developing
21/// different alias tables.
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
23pub enum ArkEventKind {
24    AccessibilityAction,
25    Click,
26    LongPress,
27    Change,
28    Submit,
29    Scroll,
30    ReachEnd,
31    SwiperChange,
32    Refresh,
33    AreaChange,
34    Focus,
35    Blur,
36    FocusIn,
37    FocusOut,
38    Hover,
39    HoverMove,
40    DragStart,
41    DragMove,
42    DragEnd,
43    DragLeave,
44    DragEnter,
45    Touch,
46    Key,
47    KeyPreIme,
48    Mouse,
49    Axis,
50}
51
52impl ArkEventKind {
53    /// Whether Dioxus should propagate this event through ancestor listeners.
54    pub const fn bubbles(self) -> bool {
55        matches!(
56            self,
57            Self::Click
58                | Self::LongPress
59                | Self::Touch
60                | Self::Key
61                | Self::KeyPreIme
62                | Self::Mouse
63                | Self::FocusIn
64                | Self::FocusOut
65        )
66    }
67}
68
69/// Classify an RSX listener name passed by Dioxus to a renderer.
70///
71/// Canonical listener names use the RSX spelling: `onclick`, `onchange`,
72/// `onscroll`, `onarea`, and so on. Dioxus strips the leading `on` before
73/// handing a listener name to the renderer, so the compact spelling (`click`)
74/// resolves to the same identity.
75///
76/// The relation is generated from the listener declarations in
77/// [`crate::events`], which is also what defines the listener functions
78/// themselves — a name that exists there always classifies, and one that does
79/// not never will.
80pub fn classify_event_name(name: &str) -> Option<ArkEventKind> {
81    crate::events::EVENT_KINDS
82        .iter()
83        .find_map(|(canonical, kind)| {
84            (*canonical == name || canonical.get("on".len()..) == Some(name)).then_some(*kind)
85        })
86}
87
88/// Typed payload carried by an [`ArkEventData`].
89///
90/// Variants mirror the value shapes the ArkUI native event API exposes
91/// (`i32_value`, `f32_value`, `string_value`, ...). The renderer populates this
92/// when it registers a native event callback; the typed `*Data` structs read
93/// from it in their `From<&ArkEventData>` impls.
94#[derive(Default, Clone, Debug)]
95pub enum ArkEventPayload {
96    /// No payload (click, refresh, submit-fire, etc.).
97    #[default]
98    None,
99    /// A boolean value (checkbox/toggle/radio checked state).
100    Bool(bool),
101    /// A single float (slider value, refresh offset).
102    Float(f32),
103    /// A single integer (swiper index, submit return code).
104    Int(i32),
105    /// An ArkUI accessibility action bit (`click`, `long click`, `cut`,
106    /// `copy`, or `paste`).
107    AccessibilityAction(u32),
108    /// A string value (text input/area change).
109    String(String),
110    /// A scroll-index payload (list/grid/water-flow visible range).
111    ScrollIndex(ScrollIndexPayload),
112    /// A physical scroll offset payload (generic Scroll and scroll observers).
113    ScrollOffset(ScrollOffsetPayload),
114    /// Layout frame payload for element-bound area/layout change events.
115    Layout(LayoutPayload),
116    /// Pointer payload for input events such as click, touch, and hover move.
117    Pointer(PointerPayload),
118    /// Physical keyboard input delivered to the focused node.
119    Key(KeyPayload),
120    /// Raw mouse input delivered to a desktop-capable node.
121    Mouse(MousePayload),
122    Axis(AxisPayload),
123}
124
125/// Scroll-index payload shared by list/grid/water-flow `on_scroll` events.
126#[derive(Default, Clone, Copy, Debug)]
127pub struct ScrollIndexPayload {
128    /// First visible item index.
129    pub first: i32,
130    /// Last visible item index.
131    pub last: i32,
132    /// Center index (list only; 0 for grid/water-flow).
133    pub center: i32,
134}
135
136#[derive(Default, Clone, Copy, Debug, PartialEq)]
137pub struct ScrollOffsetPayload {
138    /// Horizontal movement for this scroll frame, in vp.
139    pub x: f32,
140    /// Vertical movement for this scroll frame, in vp.
141    pub y: f32,
142}
143
144/// Element layout frame in physical pixels, relative to the window.
145#[derive(Default, Clone, Copy, Debug, PartialEq)]
146pub struct LayoutPayload {
147    pub x: f32,
148    pub y: f32,
149    pub width: f32,
150    pub height: f32,
151}
152
153impl LayoutPayload {
154    pub fn is_measured(self) -> bool {
155        self.width > 0.0 && self.height > 0.0
156    }
157}
158
159/// Pointer coordinates and target bounds carried by ArkUI input events.
160#[derive(Default, Clone, Copy, Debug, PartialEq)]
161pub struct PointerPayload {
162    pub source: PointerSource,
163    /// Native touch phase. Click/drag events that do not expose a touch phase
164    /// use [`PointerAction::Unknown`].
165    pub action: PointerAction,
166    /// Monotonic platform event timestamp in nanoseconds, or zero when the
167    /// native event does not expose one.
168    pub timestamp_nanos: u64,
169    /// Stable native contact identifier for the current pointer.
170    pub pointer_id: i32,
171    /// Pressed mouse/stylus buttons represented as a platform bit mask.
172    pub buttons: u64,
173    /// Contact pressure in the platform-normalized range when available.
174    pub pressure: f32,
175    /// Pointer x relative to the event target.
176    pub x: f32,
177    /// Pointer y relative to the event target.
178    pub y: f32,
179    /// Pointer x relative to the window.
180    pub window_x: f32,
181    /// Pointer y relative to the window.
182    pub window_y: f32,
183    /// Event target x relative to the window/global display.
184    pub target_x: f32,
185    /// Event target y relative to the window/global display.
186    pub target_y: f32,
187    /// Event target width.
188    pub target_width: f32,
189    /// Event target height.
190    pub target_height: f32,
191}
192
193#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
194pub enum PointerSource {
195    #[default]
196    Unknown,
197    Touch,
198    Mouse,
199}
200
201/// Platform-neutral pointer phase used by touch-capable components.
202#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
203pub enum PointerAction {
204    #[default]
205    Unknown,
206    Cancel,
207    Down,
208    Move,
209    Up,
210}
211
212/// Platform-neutral mouse button identity.
213#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
214pub enum MouseButton {
215    #[default]
216    None,
217    Primary,
218    Secondary,
219    Middle,
220    Back,
221    Forward,
222    Side,
223    Extra,
224    Task,
225    Other(i32),
226}
227
228/// Raw mouse values captured during the native callback.
229#[derive(Default, Clone, Copy, Debug, PartialEq)]
230pub struct MousePayload {
231    pub button: MouseButton,
232    pub action: PointerAction,
233    pub x: f32,
234    pub y: f32,
235    pub window_x: f32,
236    pub window_y: f32,
237    pub target_x: f32,
238    pub target_y: f32,
239    pub target_width: f32,
240    pub target_height: f32,
241    pub modifiers: KeyModifiers,
242}
243
244impl From<MousePayload> for PointerPayload {
245    fn from(mouse: MousePayload) -> Self {
246        Self {
247            source: PointerSource::Mouse,
248            action: mouse.action,
249            x: mouse.x,
250            y: mouse.y,
251            window_x: mouse.window_x,
252            window_y: mouse.window_y,
253            target_x: mouse.target_x,
254            target_y: mouse.target_y,
255            target_width: mouse.target_width,
256            target_height: mouse.target_height,
257            ..Default::default()
258        }
259    }
260}
261
262#[derive(Default, Clone, Copy, Debug, PartialEq)]
263pub struct AxisPayload {
264    pub horizontal: f64,
265    pub vertical: f64,
266    pub x: f32,
267    pub y: f32,
268    pub modifiers: KeyModifiers,
269}
270
271#[derive(Default, Clone, Copy, Debug)]
272pub struct AxisData {
273    pub horizontal: f64,
274    pub vertical: f64,
275    pub x: f32,
276    pub y: f32,
277    pub modifiers: KeyModifiers,
278}
279
280impl From<ArkEventData> for AxisData {
281    fn from(data: ArkEventData) -> Self {
282        Self::from(&data)
283    }
284}
285impl From<&ArkEventData> for AxisData {
286    fn from(data: &ArkEventData) -> Self {
287        match data.payload {
288            ArkEventPayload::Axis(axis) => Self {
289                horizontal: axis.horizontal,
290                vertical: axis.vertical,
291                x: axis.x,
292                y: axis.y,
293                modifiers: axis.modifiers,
294            },
295            _ => Self::default(),
296        }
297    }
298}
299
300/// Platform-neutral key identity used by desktop-capable components.
301#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
302pub enum KeyboardKey {
303    #[default]
304    Unknown,
305    Enter,
306    Space,
307    Tab,
308    Escape,
309    ArrowUp,
310    ArrowDown,
311    ArrowLeft,
312    ArrowRight,
313    Home,
314    End,
315    PageUp,
316    PageDown,
317    Delete,
318    Backspace,
319    Menu,
320    F10,
321    Function(u8),
322    Character(char),
323    Other(i32),
324}
325
326/// Directional-key policy for lists, tabs, and calendar grids.
327#[derive(Debug, Clone, Copy, PartialEq, Eq)]
328pub enum KeyboardNavigation {
329    Horizontal,
330    Vertical,
331    Grid(usize),
332}
333
334pub fn keyboard_navigation_target(
335    current: usize,
336    key: KeyboardKey,
337    navigation: KeyboardNavigation,
338    enabled: &[bool],
339) -> Option<usize> {
340    let count = enabled.len();
341    if count == 0 {
342        return None;
343    }
344    if key == KeyboardKey::Home {
345        return enabled.iter().position(|enabled| *enabled);
346    }
347    if key == KeyboardKey::End {
348        return enabled.iter().rposition(|enabled| *enabled);
349    }
350    let step = match (navigation, key) {
351        (KeyboardNavigation::Horizontal | KeyboardNavigation::Grid(_), KeyboardKey::ArrowLeft)
352        | (KeyboardNavigation::Vertical, KeyboardKey::ArrowUp) => -1,
353        (KeyboardNavigation::Horizontal | KeyboardNavigation::Grid(_), KeyboardKey::ArrowRight)
354        | (KeyboardNavigation::Vertical, KeyboardKey::ArrowDown) => 1,
355        (KeyboardNavigation::Grid(columns), KeyboardKey::ArrowUp) => -(columns.max(1) as isize),
356        (KeyboardNavigation::Grid(columns), KeyboardKey::ArrowDown) => columns.max(1) as isize,
357        _ => return None,
358    };
359    let mut next = current.min(count - 1) as isize;
360    for _ in 0..count {
361        next += step;
362        match navigation {
363            KeyboardNavigation::Grid(_) if next < 0 || next >= count as isize => return None,
364            KeyboardNavigation::Grid(_) => {}
365            _ => next = next.rem_euclid(count as isize),
366        }
367        if enabled[next as usize] {
368            return Some(next as usize);
369        }
370    }
371    None
372}
373
374/// Native key lifecycle normalized across OpenHarmony keyboard sources.
375#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
376pub enum KeyAction {
377    #[default]
378    Unknown,
379    Down,
380    Up,
381    Repeat,
382    Click,
383}
384
385/// Modifier snapshot attached to a physical key event.
386#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
387pub struct KeyModifiers {
388    pub ctrl: bool,
389    pub shift: bool,
390    pub alt: bool,
391    pub function: bool,
392}
393
394/// Raw and normalized keyboard values captured during the native callback.
395#[derive(Default, Clone, Debug, PartialEq, Eq)]
396pub struct KeyPayload {
397    pub key: KeyboardKey,
398    pub action: KeyAction,
399    pub raw_code: i32,
400    pub text: String,
401    pub modifiers: KeyModifiers,
402}
403
404/// Declarative native key ownership. Unclaimed keys retain platform behavior.
405#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
406pub struct KeyboardCapture(u32);
407
408impl KeyboardCapture {
409    pub fn from_names(names: &str) -> Self {
410        let mut mask = 0;
411        for name in names.split([' ', ',', '|']).filter(|name| !name.is_empty()) {
412            mask |= match name {
413                "all" => u32::MAX,
414                "enter" => 1,
415                "space" => 2,
416                "tab" => 4,
417                "escape" => 8,
418                "up" => 16,
419                "down" => 32,
420                "left" => 64,
421                "right" => 128,
422                "home" => 256,
423                "end" => 512,
424                "page_up" => 1024,
425                "page_down" => 2048,
426                _ => 0,
427            };
428        }
429        Self(mask)
430    }
431
432    pub fn captures(self, key: &KeyPayload) -> bool {
433        if self.0 == u32::MAX {
434            return true;
435        }
436        if key.modifiers.ctrl || key.modifiers.alt {
437            return false;
438        }
439        let bit = match key.key {
440            KeyboardKey::Enter => 1,
441            KeyboardKey::Space => 2,
442            KeyboardKey::Tab => 4,
443            KeyboardKey::Escape => 8,
444            KeyboardKey::ArrowUp => 16,
445            KeyboardKey::ArrowDown => 32,
446            KeyboardKey::ArrowLeft => 64,
447            KeyboardKey::ArrowRight => 128,
448            KeyboardKey::Home => 256,
449            KeyboardKey::End => 512,
450            KeyboardKey::PageUp => 1024,
451            KeyboardKey::PageDown => 2048,
452            _ => 0,
453        };
454        self.0 & bit != 0
455    }
456}
457
458impl PointerPayload {
459    pub fn has_target_bounds(self) -> bool {
460        self.target_x.is_finite()
461            && self.target_y.is_finite()
462            && self.target_width.is_finite()
463            && self.target_height.is_finite()
464            && self.target_width > 0.0
465            && self.target_height > 0.0
466    }
467
468    pub fn has_window_position(self) -> bool {
469        self.window_x.is_finite()
470            && self.window_y.is_finite()
471            && (self.window_x != 0.0 || self.window_y != 0.0)
472    }
473}
474
475/// Platform event payload carrying an ArkUI native event.
476#[derive(Default)]
477pub struct ArkEventData {
478    /// Typed native event payload. `None` variant when the runtime sink did not
479    /// populate it (foundation runtime); the typed `*Data` structs fall back to
480    /// defaults in that case.
481    pub payload: ArkEventPayload,
482}
483
484impl ArkEventData {
485    pub fn new() -> Self {
486        Self::default()
487    }
488
489    pub fn with_payload(payload: ArkEventPayload) -> Self {
490        Self { payload }
491    }
492}
493
494/// Data for a click/press event.
495#[derive(Default, Clone, Copy, Debug)]
496pub struct ClickData {
497    pub pointer: Option<PointerPayload>,
498}
499
500/// Action requested by an accessibility service.
501#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
502pub struct AccessibilityActionData {
503    /// ArkUI action bit: click=1, long-click=2, cut=4, copy=8, paste=16.
504    pub action: u32,
505}
506
507impl From<ArkEventData> for AccessibilityActionData {
508    fn from(data: ArkEventData) -> Self {
509        Self::from(&data)
510    }
511}
512
513impl From<&ArkEventData> for AccessibilityActionData {
514    fn from(data: &ArkEventData) -> Self {
515        match &data.payload {
516            ArkEventPayload::AccessibilityAction(action) => Self { action: *action },
517            _ => Self::default(),
518        }
519    }
520}
521
522impl From<ArkEventData> for ClickData {
523    fn from(data: ArkEventData) -> Self {
524        Self::from(&data)
525    }
526}
527
528impl From<&ArkEventData> for ClickData {
529    fn from(data: &ArkEventData) -> Self {
530        let pointer = match &data.payload {
531            ArkEventPayload::Pointer(pointer) => Some(*pointer),
532            _ => None,
533        };
534        ClickData { pointer }
535    }
536}
537
538/// Data for a hover event. `is_hovering` is true on pointer enter, false on
539/// exit.
540#[derive(Default, Clone, Copy, Debug)]
541pub struct HoverData {
542    pub is_hovering: bool,
543}
544
545/// Data for native focus and blur events.
546#[derive(Default, Clone, Copy, Debug)]
547pub struct FocusData {
548    pub focused: bool,
549}
550
551impl From<ArkEventData> for FocusData {
552    fn from(data: ArkEventData) -> Self {
553        Self::from(&data)
554    }
555}
556
557impl From<&ArkEventData> for FocusData {
558    fn from(data: &ArkEventData) -> Self {
559        let focused = match &data.payload {
560            ArkEventPayload::Bool(focused) => *focused,
561            _ => false,
562        };
563        Self { focused }
564    }
565}
566
567impl From<ArkEventData> for HoverData {
568    fn from(data: ArkEventData) -> Self {
569        Self::from(&data)
570    }
571}
572
573impl From<&ArkEventData> for HoverData {
574    fn from(data: &ArkEventData) -> Self {
575        let is_hovering = match &data.payload {
576            ArkEventPayload::Bool(b) => *b,
577            _ => false,
578        };
579        HoverData { is_hovering }
580    }
581}
582
583/// Data for a touch or pointer movement event. Drag lifecycle events currently
584/// carry `None` because ArkUI exposes `ArkUI_DragEvent` rather than an input
585/// event; use `NodeBuilder`'s raw drag callbacks when the native object is
586/// required.
587#[derive(Default, Clone, Copy, Debug)]
588pub struct PointerData {
589    pub pointer: Option<PointerPayload>,
590}
591
592impl From<ArkEventData> for PointerData {
593    fn from(data: ArkEventData) -> Self {
594        Self::from(&data)
595    }
596}
597
598impl From<&ArkEventData> for PointerData {
599    fn from(data: &ArkEventData) -> Self {
600        let pointer = match &data.payload {
601            ArkEventPayload::Pointer(pointer) => Some(*pointer),
602            _ => None,
603        };
604        PointerData { pointer }
605    }
606}
607
608/// Data for a native mouse event.
609pub type MouseData = MousePayload;
610
611impl MouseData {
612    pub fn secondary_down(self) -> bool {
613        self.button == MouseButton::Secondary && self.action == PointerAction::Down
614    }
615}
616
617impl From<ArkEventData> for MouseData {
618    fn from(data: ArkEventData) -> Self {
619        Self::from(&data)
620    }
621}
622
623impl From<&ArkEventData> for MouseData {
624    fn from(data: &ArkEventData) -> Self {
625        match data.payload {
626            ArkEventPayload::Mouse(payload) => payload,
627            _ => Self::default(),
628        }
629    }
630}
631
632/// Data for a physical keyboard event.
633#[derive(Default, Clone, Debug, PartialEq, Eq)]
634pub struct KeyData {
635    pub key: KeyboardKey,
636    pub action: KeyAction,
637    pub raw_code: i32,
638    pub text: String,
639    pub modifiers: KeyModifiers,
640}
641
642impl KeyData {
643    pub fn is_down(&self) -> bool {
644        matches!(self.action, KeyAction::Down | KeyAction::Repeat)
645    }
646
647    pub fn activates(&self) -> bool {
648        self.action == KeyAction::Down
649            && !self.modifiers.ctrl
650            && !self.modifiers.alt
651            && matches!(self.key, KeyboardKey::Enter | KeyboardKey::Space)
652    }
653}
654
655impl From<ArkEventData> for KeyData {
656    fn from(data: ArkEventData) -> Self {
657        Self::from(&data)
658    }
659}
660
661impl From<&ArkEventData> for KeyData {
662    fn from(data: &ArkEventData) -> Self {
663        match &data.payload {
664            ArkEventPayload::Key(payload) => Self {
665                key: payload.key,
666                action: payload.action,
667                raw_code: payload.raw_code,
668                text: payload.text.clone(),
669                modifiers: payload.modifiers,
670            },
671            _ => Self::default(),
672        }
673    }
674}
675
676/// Data for a value-change event (checkbox/toggle/radio/slider/text input).
677///
678/// `bool_value`/`float_value`/`string_value` are populated depending on the
679/// source component; the others remain at their defaults.
680#[derive(Default, Clone, Debug)]
681pub struct ChangeData {
682    /// Checked state for checkbox/toggle/radio.
683    pub bool_value: bool,
684    /// Numeric value for slider/progress.
685    pub float_value: f32,
686    /// Text value for text input/area.
687    pub string_value: String,
688}
689
690impl From<ArkEventData> for ChangeData {
691    fn from(data: ArkEventData) -> Self {
692        Self::from(&data)
693    }
694}
695
696impl From<&ArkEventData> for ChangeData {
697    fn from(data: &ArkEventData) -> Self {
698        let mut out = ChangeData::default();
699        match &data.payload {
700            ArkEventPayload::Bool(b) => out.bool_value = *b,
701            ArkEventPayload::Float(f) => out.float_value = *f,
702            ArkEventPayload::Int(i) => out.float_value = *i as f32,
703            ArkEventPayload::String(s) => out.string_value = s.clone(),
704            ArkEventPayload::ScrollIndex(_)
705            | ArkEventPayload::ScrollOffset(_)
706            | ArkEventPayload::Layout(_)
707            | ArkEventPayload::Pointer(_)
708            | ArkEventPayload::AccessibilityAction(_)
709            | ArkEventPayload::Key(_)
710            | ArkEventPayload::Mouse(_)
711            | ArkEventPayload::Axis(_)
712            | ArkEventPayload::None => {}
713        }
714        out
715    }
716}
717
718/// Data for an element-bound area/layout change event.
719#[derive(Default, Clone, Copy, Debug)]
720pub struct AreaData {
721    pub frame: LayoutPayload,
722}
723
724impl From<ArkEventData> for AreaData {
725    fn from(data: ArkEventData) -> Self {
726        Self::from(&data)
727    }
728}
729
730impl From<&ArkEventData> for AreaData {
731    fn from(data: &ArkEventData) -> Self {
732        match &data.payload {
733            ArkEventPayload::Layout(frame) => AreaData { frame: *frame },
734            _ => AreaData::default(),
735        }
736    }
737}
738
739/// Data for a scroll event (list/grid/water-flow/scroll).
740#[derive(Default, Clone, Copy, Debug)]
741pub struct ScrollData {
742    /// First visible item index (scroll-index events).
743    pub first_index: i32,
744    /// Last visible item index (scroll-index events).
745    pub last_index: i32,
746    /// Center visible item index (list only).
747    pub center_index: i32,
748    /// Horizontal movement for this scroll frame, in vp.
749    pub offset_x: f32,
750    /// Vertical movement for this scroll frame, in vp.
751    pub offset_y: f32,
752    /// Whether this event carried offsets instead of visible indices.
753    pub has_offset: bool,
754}
755
756impl From<ArkEventData> for ScrollData {
757    fn from(data: ArkEventData) -> Self {
758        Self::from(&data)
759    }
760}
761
762impl From<&ArkEventData> for ScrollData {
763    fn from(data: &ArkEventData) -> Self {
764        match &data.payload {
765            ArkEventPayload::ScrollIndex(s) => ScrollData {
766                first_index: s.first,
767                last_index: s.last,
768                center_index: s.center,
769                ..ScrollData::default()
770            },
771            ArkEventPayload::ScrollOffset(offset) => ScrollData {
772                offset_x: offset.x,
773                offset_y: offset.y,
774                has_offset: true,
775                ..ScrollData::default()
776            },
777            _ => ScrollData::default(),
778        }
779    }
780}
781
782/// Data for a submit event (text input/area enter key). Carries the optional
783/// return code ArkUI passes to the submit callback.
784#[derive(Default, Clone, Copy, Debug)]
785pub struct SubmitData {
786    pub return_code: i32,
787}
788
789impl From<ArkEventData> for SubmitData {
790    fn from(data: ArkEventData) -> Self {
791        Self::from(&data)
792    }
793}
794
795impl From<&ArkEventData> for SubmitData {
796    fn from(data: &ArkEventData) -> Self {
797        let return_code = match &data.payload {
798            ArkEventPayload::Int(i) => *i,
799            _ => 0,
800        };
801        SubmitData { return_code }
802    }
803}
804
805/// Data for a swiper change event. Carries the newly selected index.
806#[derive(Default, Clone, Copy, Debug)]
807pub struct SwiperChangeData {
808    pub index: i32,
809}
810
811impl From<ArkEventData> for SwiperChangeData {
812    fn from(data: ArkEventData) -> Self {
813        Self::from(&data)
814    }
815}
816
817impl From<&ArkEventData> for SwiperChangeData {
818    fn from(data: &ArkEventData) -> Self {
819        let index = match &data.payload {
820            ArkEventPayload::Int(i) => *i,
821            _ => 0,
822        };
823        SwiperChangeData { index }
824    }
825}
826
827/// Data for a refresh event (refresh trigger). No payload.
828#[derive(Default, Clone, Copy, Debug)]
829pub struct RefreshData;
830
831impl From<ArkEventData> for RefreshData {
832    fn from(_data: ArkEventData) -> Self {
833        RefreshData
834    }
835}
836
837impl From<&ArkEventData> for RefreshData {
838    fn from(_data: &ArkEventData) -> Self {
839        RefreshData
840    }
841}
842
843/// Data for a scroll-container reach-end event. No payload.
844#[derive(Default, Clone, Copy, Debug)]
845pub struct ReachEndData;
846
847impl From<ArkEventData> for ReachEndData {
848    fn from(_data: ArkEventData) -> Self {
849        ReachEndData
850    }
851}
852
853impl From<&ArkEventData> for ReachEndData {
854    fn from(_data: &ArkEventData) -> Self {
855        ReachEndData
856    }
857}
858
859#[cfg(test)]
860mod tests {
861    use super::{
862        classify_event_name, ArkEventData, ArkEventKind, ArkEventPayload, KeyAction, KeyData,
863        KeyPayload, KeyboardKey, MouseButton, MouseData, MousePayload, PointerAction,
864    };
865
866    #[test]
867    fn canonical_event_names_share_one_semantic_identity() {
868        for name in ["onclick", "click"] {
869            assert_eq!(classify_event_name(name), Some(ArkEventKind::Click));
870        }
871        for name in ["onlongpress", "longpress"] {
872            assert_eq!(classify_event_name(name), Some(ArkEventKind::LongPress));
873        }
874        for name in ["onfocus", "focus"] {
875            assert_eq!(classify_event_name(name), Some(ArkEventKind::Focus));
876        }
877        for name in ["onblur", "blur"] {
878            assert_eq!(classify_event_name(name), Some(ArkEventKind::Blur));
879        }
880        for name in ["onreachend", "reachend"] {
881            assert_eq!(classify_event_name(name), Some(ArkEventKind::ReachEnd));
882        }
883        for name in ["onkey", "key"] {
884            assert_eq!(classify_event_name(name), Some(ArkEventKind::Key));
885        }
886        for name in ["onmouse", "mouse"] {
887            assert_eq!(classify_event_name(name), Some(ArkEventKind::Mouse));
888        }
889        assert!(ArkEventKind::Click.bubbles());
890        assert!(ArkEventKind::LongPress.bubbles());
891        assert!(!ArkEventKind::Change.bubbles());
892    }
893
894    #[test]
895    fn keyboard_activation_requires_a_press_of_enter_or_space() {
896        let enter = KeyData::from(ArkEventData::with_payload(ArkEventPayload::Key(
897            KeyPayload {
898                key: KeyboardKey::Enter,
899                action: KeyAction::Down,
900                ..Default::default()
901            },
902        )));
903        assert!(enter.activates());
904
905        let released_space = KeyData::from(ArkEventData::with_payload(ArkEventPayload::Key(
906            KeyPayload {
907                key: KeyboardKey::Space,
908                action: KeyAction::Up,
909                ..Default::default()
910            },
911        )));
912        assert!(!released_space.activates());
913        let repeated = KeyData {
914            key: KeyboardKey::Enter,
915            action: KeyAction::Repeat,
916            ..Default::default()
917        };
918        assert!(repeated.is_down());
919        assert!(!repeated.activates());
920        let shortcut = KeyData {
921            key: KeyboardKey::Enter,
922            action: KeyAction::Down,
923            modifiers: super::KeyModifiers {
924                ctrl: true,
925                ..Default::default()
926            },
927            ..Default::default()
928        };
929        assert!(!shortcut.activates());
930    }
931
932    #[test]
933    fn native_key_capture_preserves_unclaimed_editing_and_shortcuts() {
934        let capture = super::KeyboardCapture::from_names("enter space left right escape");
935        let mut key = KeyPayload {
936            key: KeyboardKey::ArrowRight,
937            action: KeyAction::Down,
938            ..Default::default()
939        };
940        assert!(capture.captures(&key));
941        key.modifiers.ctrl = true;
942        assert!(!capture.captures(&key));
943        key.key = KeyboardKey::Character('c');
944        assert!(!capture.captures(&key));
945        assert!(super::KeyboardCapture::from_names("all").captures(&key));
946    }
947
948    #[test]
949    fn roving_navigation_skips_disabled_items_and_keeps_calendar_columns() {
950        use super::{keyboard_navigation_target as next, KeyboardNavigation};
951        let enabled = [true, false, true, true];
952        assert_eq!(
953            next(
954                0,
955                KeyboardKey::ArrowRight,
956                KeyboardNavigation::Horizontal,
957                &enabled
958            ),
959            Some(2)
960        );
961        assert_eq!(
962            next(
963                0,
964                KeyboardKey::ArrowLeft,
965                KeyboardNavigation::Horizontal,
966                &enabled
967            ),
968            Some(3)
969        );
970        let grid = [true; 42];
971        assert_eq!(
972            next(
973                8,
974                KeyboardKey::ArrowDown,
975                KeyboardNavigation::Grid(7),
976                &grid
977            ),
978            Some(15)
979        );
980        assert_eq!(
981            next(8, KeyboardKey::ArrowUp, KeyboardNavigation::Grid(7), &grid),
982            Some(1)
983        );
984        assert_eq!(
985            next(1, KeyboardKey::ArrowUp, KeyboardNavigation::Grid(7), &grid),
986            None
987        );
988        assert_eq!(
989            next(0, KeyboardKey::End, KeyboardNavigation::Vertical, &enabled),
990            Some(3)
991        );
992        assert_eq!(
993            next(
994                0,
995                KeyboardKey::ArrowRight,
996                KeyboardNavigation::Horizontal,
997                &[false; 3]
998            ),
999            None
1000        );
1001    }
1002
1003    #[test]
1004    fn secondary_mouse_press_is_preserved() {
1005        let mouse = MouseData::from(ArkEventData::with_payload(ArkEventPayload::Mouse(
1006            MousePayload {
1007                button: MouseButton::Secondary,
1008                action: PointerAction::Down,
1009                window_x: 24.0,
1010                window_y: 48.0,
1011                ..Default::default()
1012            },
1013        )));
1014        assert!(mouse.secondary_down());
1015        assert_eq!((mouse.window_x, mouse.window_y), (24.0, 48.0));
1016    }
1017
1018    /// The classifier is generated from the listener declarations, so every
1019    /// declared listener must resolve and no name may be declared twice.
1020    #[test]
1021    fn every_declared_listener_classifies_in_both_spellings() {
1022        let mut seen = std::collections::HashSet::new();
1023        assert!(!crate::events::EVENT_KINDS.is_empty());
1024        for (canonical, kind) in crate::events::EVENT_KINDS {
1025            assert!(canonical.starts_with("on"), "{canonical}");
1026            assert!(seen.insert(*canonical), "duplicate listener {canonical}");
1027            assert_eq!(classify_event_name(canonical), Some(*kind), "{canonical}");
1028            let compact = &canonical["on".len()..];
1029            assert_eq!(classify_event_name(compact), Some(*kind), "{compact}");
1030        }
1031    }
1032
1033    #[test]
1034    fn historical_snake_case_aliases_are_rejected() {
1035        for name in [
1036            "on_press",
1037            "_press",
1038            "on_long_press",
1039            "_long_press",
1040            "on_focus",
1041            "_focus",
1042            "on_blur",
1043            "_blur",
1044            "on_reach_end",
1045            "_reach_end",
1046        ] {
1047            assert_eq!(classify_event_name(name), None, "{name}");
1048        }
1049    }
1050}