#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ArkEventKind {
AccessibilityAction,
Click,
LongPress,
Change,
Submit,
Scroll,
ReachEnd,
SwiperChange,
Refresh,
AreaChange,
Focus,
Blur,
FocusIn,
FocusOut,
Hover,
HoverMove,
DragStart,
DragMove,
DragEnd,
DragLeave,
DragEnter,
Touch,
Key,
KeyPreIme,
Mouse,
Axis,
}
impl ArkEventKind {
pub const fn bubbles(self) -> bool {
matches!(
self,
Self::Click
| Self::LongPress
| Self::Touch
| Self::Key
| Self::KeyPreIme
| Self::Mouse
| Self::FocusIn
| Self::FocusOut
)
}
}
pub fn classify_event_name(name: &str) -> Option<ArkEventKind> {
crate::events::EVENT_KINDS
.iter()
.find_map(|(canonical, kind)| {
(*canonical == name || canonical.get("on".len()..) == Some(name)).then_some(*kind)
})
}
#[derive(Default, Clone, Debug)]
pub enum ArkEventPayload {
#[default]
None,
Bool(bool),
Float(f32),
Int(i32),
AccessibilityAction(u32),
String(String),
ScrollIndex(ScrollIndexPayload),
ScrollOffset(ScrollOffsetPayload),
Layout(LayoutPayload),
Pointer(PointerPayload),
Key(KeyPayload),
Mouse(MousePayload),
Axis(AxisPayload),
}
#[derive(Default, Clone, Copy, Debug)]
pub struct ScrollIndexPayload {
pub first: i32,
pub last: i32,
pub center: i32,
}
#[derive(Default, Clone, Copy, Debug, PartialEq)]
pub struct ScrollOffsetPayload {
pub x: f32,
pub y: f32,
}
#[derive(Default, Clone, Copy, Debug, PartialEq)]
pub struct LayoutPayload {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
}
impl LayoutPayload {
pub fn is_measured(self) -> bool {
self.width > 0.0 && self.height > 0.0
}
}
#[derive(Default, Clone, Copy, Debug, PartialEq)]
pub struct PointerPayload {
pub source: PointerSource,
pub action: PointerAction,
pub timestamp_nanos: u64,
pub pointer_id: i32,
pub buttons: u64,
pub pressure: f32,
pub x: f32,
pub y: f32,
pub window_x: f32,
pub window_y: f32,
pub target_x: f32,
pub target_y: f32,
pub target_width: f32,
pub target_height: f32,
}
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
pub enum PointerSource {
#[default]
Unknown,
Touch,
Mouse,
}
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
pub enum PointerAction {
#[default]
Unknown,
Cancel,
Down,
Move,
Up,
}
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
pub enum MouseButton {
#[default]
None,
Primary,
Secondary,
Middle,
Back,
Forward,
Side,
Extra,
Task,
Other(i32),
}
#[derive(Default, Clone, Copy, Debug, PartialEq)]
pub struct MousePayload {
pub button: MouseButton,
pub action: PointerAction,
pub x: f32,
pub y: f32,
pub window_x: f32,
pub window_y: f32,
pub target_x: f32,
pub target_y: f32,
pub target_width: f32,
pub target_height: f32,
pub modifiers: KeyModifiers,
}
impl From<MousePayload> for PointerPayload {
fn from(mouse: MousePayload) -> Self {
Self {
source: PointerSource::Mouse,
action: mouse.action,
x: mouse.x,
y: mouse.y,
window_x: mouse.window_x,
window_y: mouse.window_y,
target_x: mouse.target_x,
target_y: mouse.target_y,
target_width: mouse.target_width,
target_height: mouse.target_height,
..Default::default()
}
}
}
#[derive(Default, Clone, Copy, Debug, PartialEq)]
pub struct AxisPayload {
pub horizontal: f64,
pub vertical: f64,
pub x: f32,
pub y: f32,
pub modifiers: KeyModifiers,
}
#[derive(Default, Clone, Copy, Debug)]
pub struct AxisData {
pub horizontal: f64,
pub vertical: f64,
pub x: f32,
pub y: f32,
pub modifiers: KeyModifiers,
}
impl From<ArkEventData> for AxisData {
fn from(data: ArkEventData) -> Self {
Self::from(&data)
}
}
impl From<&ArkEventData> for AxisData {
fn from(data: &ArkEventData) -> Self {
match data.payload {
ArkEventPayload::Axis(axis) => Self {
horizontal: axis.horizontal,
vertical: axis.vertical,
x: axis.x,
y: axis.y,
modifiers: axis.modifiers,
},
_ => Self::default(),
}
}
}
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
pub enum KeyboardKey {
#[default]
Unknown,
Enter,
Space,
Tab,
Escape,
ArrowUp,
ArrowDown,
ArrowLeft,
ArrowRight,
Home,
End,
PageUp,
PageDown,
Delete,
Backspace,
Menu,
F10,
Function(u8),
Character(char),
Other(i32),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyboardNavigation {
Horizontal,
Vertical,
Grid(usize),
}
pub fn keyboard_navigation_target(
current: usize,
key: KeyboardKey,
navigation: KeyboardNavigation,
enabled: &[bool],
) -> Option<usize> {
let count = enabled.len();
if count == 0 {
return None;
}
if key == KeyboardKey::Home {
return enabled.iter().position(|enabled| *enabled);
}
if key == KeyboardKey::End {
return enabled.iter().rposition(|enabled| *enabled);
}
let step = match (navigation, key) {
(KeyboardNavigation::Horizontal | KeyboardNavigation::Grid(_), KeyboardKey::ArrowLeft)
| (KeyboardNavigation::Vertical, KeyboardKey::ArrowUp) => -1,
(KeyboardNavigation::Horizontal | KeyboardNavigation::Grid(_), KeyboardKey::ArrowRight)
| (KeyboardNavigation::Vertical, KeyboardKey::ArrowDown) => 1,
(KeyboardNavigation::Grid(columns), KeyboardKey::ArrowUp) => -(columns.max(1) as isize),
(KeyboardNavigation::Grid(columns), KeyboardKey::ArrowDown) => columns.max(1) as isize,
_ => return None,
};
let mut next = current.min(count - 1) as isize;
for _ in 0..count {
next += step;
match navigation {
KeyboardNavigation::Grid(_) if next < 0 || next >= count as isize => return None,
KeyboardNavigation::Grid(_) => {}
_ => next = next.rem_euclid(count as isize),
}
if enabled[next as usize] {
return Some(next as usize);
}
}
None
}
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
pub enum KeyAction {
#[default]
Unknown,
Down,
Up,
Repeat,
Click,
}
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
pub struct KeyModifiers {
pub ctrl: bool,
pub shift: bool,
pub alt: bool,
pub function: bool,
}
#[derive(Default, Clone, Debug, PartialEq, Eq)]
pub struct KeyPayload {
pub key: KeyboardKey,
pub action: KeyAction,
pub raw_code: i32,
pub text: String,
pub modifiers: KeyModifiers,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct KeyboardCapture(u32);
impl KeyboardCapture {
pub fn from_names(names: &str) -> Self {
let mut mask = 0;
for name in names.split([' ', ',', '|']).filter(|name| !name.is_empty()) {
mask |= match name {
"all" => u32::MAX,
"enter" => 1,
"space" => 2,
"tab" => 4,
"escape" => 8,
"up" => 16,
"down" => 32,
"left" => 64,
"right" => 128,
"home" => 256,
"end" => 512,
"page_up" => 1024,
"page_down" => 2048,
_ => 0,
};
}
Self(mask)
}
pub fn captures(self, key: &KeyPayload) -> bool {
if self.0 == u32::MAX {
return true;
}
if key.modifiers.ctrl || key.modifiers.alt {
return false;
}
let bit = match key.key {
KeyboardKey::Enter => 1,
KeyboardKey::Space => 2,
KeyboardKey::Tab => 4,
KeyboardKey::Escape => 8,
KeyboardKey::ArrowUp => 16,
KeyboardKey::ArrowDown => 32,
KeyboardKey::ArrowLeft => 64,
KeyboardKey::ArrowRight => 128,
KeyboardKey::Home => 256,
KeyboardKey::End => 512,
KeyboardKey::PageUp => 1024,
KeyboardKey::PageDown => 2048,
_ => 0,
};
self.0 & bit != 0
}
}
impl PointerPayload {
pub fn has_target_bounds(self) -> bool {
self.target_x.is_finite()
&& self.target_y.is_finite()
&& self.target_width.is_finite()
&& self.target_height.is_finite()
&& self.target_width > 0.0
&& self.target_height > 0.0
}
pub fn has_window_position(self) -> bool {
self.window_x.is_finite()
&& self.window_y.is_finite()
&& (self.window_x != 0.0 || self.window_y != 0.0)
}
}
#[derive(Default)]
pub struct ArkEventData {
pub payload: ArkEventPayload,
}
impl ArkEventData {
pub fn new() -> Self {
Self::default()
}
pub fn with_payload(payload: ArkEventPayload) -> Self {
Self { payload }
}
}
#[derive(Default, Clone, Copy, Debug)]
pub struct ClickData {
pub pointer: Option<PointerPayload>,
}
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
pub struct AccessibilityActionData {
pub action: u32,
}
impl From<ArkEventData> for AccessibilityActionData {
fn from(data: ArkEventData) -> Self {
Self::from(&data)
}
}
impl From<&ArkEventData> for AccessibilityActionData {
fn from(data: &ArkEventData) -> Self {
match &data.payload {
ArkEventPayload::AccessibilityAction(action) => Self { action: *action },
_ => Self::default(),
}
}
}
impl From<ArkEventData> for ClickData {
fn from(data: ArkEventData) -> Self {
Self::from(&data)
}
}
impl From<&ArkEventData> for ClickData {
fn from(data: &ArkEventData) -> Self {
let pointer = match &data.payload {
ArkEventPayload::Pointer(pointer) => Some(*pointer),
_ => None,
};
ClickData { pointer }
}
}
#[derive(Default, Clone, Copy, Debug)]
pub struct HoverData {
pub is_hovering: bool,
}
#[derive(Default, Clone, Copy, Debug)]
pub struct FocusData {
pub focused: bool,
}
impl From<ArkEventData> for FocusData {
fn from(data: ArkEventData) -> Self {
Self::from(&data)
}
}
impl From<&ArkEventData> for FocusData {
fn from(data: &ArkEventData) -> Self {
let focused = match &data.payload {
ArkEventPayload::Bool(focused) => *focused,
_ => false,
};
Self { focused }
}
}
impl From<ArkEventData> for HoverData {
fn from(data: ArkEventData) -> Self {
Self::from(&data)
}
}
impl From<&ArkEventData> for HoverData {
fn from(data: &ArkEventData) -> Self {
let is_hovering = match &data.payload {
ArkEventPayload::Bool(b) => *b,
_ => false,
};
HoverData { is_hovering }
}
}
#[derive(Default, Clone, Copy, Debug)]
pub struct PointerData {
pub pointer: Option<PointerPayload>,
}
impl From<ArkEventData> for PointerData {
fn from(data: ArkEventData) -> Self {
Self::from(&data)
}
}
impl From<&ArkEventData> for PointerData {
fn from(data: &ArkEventData) -> Self {
let pointer = match &data.payload {
ArkEventPayload::Pointer(pointer) => Some(*pointer),
_ => None,
};
PointerData { pointer }
}
}
pub type MouseData = MousePayload;
impl MouseData {
pub fn secondary_down(self) -> bool {
self.button == MouseButton::Secondary && self.action == PointerAction::Down
}
}
impl From<ArkEventData> for MouseData {
fn from(data: ArkEventData) -> Self {
Self::from(&data)
}
}
impl From<&ArkEventData> for MouseData {
fn from(data: &ArkEventData) -> Self {
match data.payload {
ArkEventPayload::Mouse(payload) => payload,
_ => Self::default(),
}
}
}
#[derive(Default, Clone, Debug, PartialEq, Eq)]
pub struct KeyData {
pub key: KeyboardKey,
pub action: KeyAction,
pub raw_code: i32,
pub text: String,
pub modifiers: KeyModifiers,
}
impl KeyData {
pub fn is_down(&self) -> bool {
matches!(self.action, KeyAction::Down | KeyAction::Repeat)
}
pub fn activates(&self) -> bool {
self.action == KeyAction::Down
&& !self.modifiers.ctrl
&& !self.modifiers.alt
&& matches!(self.key, KeyboardKey::Enter | KeyboardKey::Space)
}
}
impl From<ArkEventData> for KeyData {
fn from(data: ArkEventData) -> Self {
Self::from(&data)
}
}
impl From<&ArkEventData> for KeyData {
fn from(data: &ArkEventData) -> Self {
match &data.payload {
ArkEventPayload::Key(payload) => Self {
key: payload.key,
action: payload.action,
raw_code: payload.raw_code,
text: payload.text.clone(),
modifiers: payload.modifiers,
},
_ => Self::default(),
}
}
}
#[derive(Default, Clone, Debug)]
pub struct ChangeData {
pub bool_value: bool,
pub float_value: f32,
pub string_value: String,
}
impl From<ArkEventData> for ChangeData {
fn from(data: ArkEventData) -> Self {
Self::from(&data)
}
}
impl From<&ArkEventData> for ChangeData {
fn from(data: &ArkEventData) -> Self {
let mut out = ChangeData::default();
match &data.payload {
ArkEventPayload::Bool(b) => out.bool_value = *b,
ArkEventPayload::Float(f) => out.float_value = *f,
ArkEventPayload::Int(i) => out.float_value = *i as f32,
ArkEventPayload::String(s) => out.string_value = s.clone(),
ArkEventPayload::ScrollIndex(_)
| ArkEventPayload::ScrollOffset(_)
| ArkEventPayload::Layout(_)
| ArkEventPayload::Pointer(_)
| ArkEventPayload::AccessibilityAction(_)
| ArkEventPayload::Key(_)
| ArkEventPayload::Mouse(_)
| ArkEventPayload::Axis(_)
| ArkEventPayload::None => {}
}
out
}
}
#[derive(Default, Clone, Copy, Debug)]
pub struct AreaData {
pub frame: LayoutPayload,
}
impl From<ArkEventData> for AreaData {
fn from(data: ArkEventData) -> Self {
Self::from(&data)
}
}
impl From<&ArkEventData> for AreaData {
fn from(data: &ArkEventData) -> Self {
match &data.payload {
ArkEventPayload::Layout(frame) => AreaData { frame: *frame },
_ => AreaData::default(),
}
}
}
#[derive(Default, Clone, Copy, Debug)]
pub struct ScrollData {
pub first_index: i32,
pub last_index: i32,
pub center_index: i32,
pub offset_x: f32,
pub offset_y: f32,
pub has_offset: bool,
}
impl From<ArkEventData> for ScrollData {
fn from(data: ArkEventData) -> Self {
Self::from(&data)
}
}
impl From<&ArkEventData> for ScrollData {
fn from(data: &ArkEventData) -> Self {
match &data.payload {
ArkEventPayload::ScrollIndex(s) => ScrollData {
first_index: s.first,
last_index: s.last,
center_index: s.center,
..ScrollData::default()
},
ArkEventPayload::ScrollOffset(offset) => ScrollData {
offset_x: offset.x,
offset_y: offset.y,
has_offset: true,
..ScrollData::default()
},
_ => ScrollData::default(),
}
}
}
#[derive(Default, Clone, Copy, Debug)]
pub struct SubmitData {
pub return_code: i32,
}
impl From<ArkEventData> for SubmitData {
fn from(data: ArkEventData) -> Self {
Self::from(&data)
}
}
impl From<&ArkEventData> for SubmitData {
fn from(data: &ArkEventData) -> Self {
let return_code = match &data.payload {
ArkEventPayload::Int(i) => *i,
_ => 0,
};
SubmitData { return_code }
}
}
#[derive(Default, Clone, Copy, Debug)]
pub struct SwiperChangeData {
pub index: i32,
}
impl From<ArkEventData> for SwiperChangeData {
fn from(data: ArkEventData) -> Self {
Self::from(&data)
}
}
impl From<&ArkEventData> for SwiperChangeData {
fn from(data: &ArkEventData) -> Self {
let index = match &data.payload {
ArkEventPayload::Int(i) => *i,
_ => 0,
};
SwiperChangeData { index }
}
}
#[derive(Default, Clone, Copy, Debug)]
pub struct RefreshData;
impl From<ArkEventData> for RefreshData {
fn from(_data: ArkEventData) -> Self {
RefreshData
}
}
impl From<&ArkEventData> for RefreshData {
fn from(_data: &ArkEventData) -> Self {
RefreshData
}
}
#[derive(Default, Clone, Copy, Debug)]
pub struct ReachEndData;
impl From<ArkEventData> for ReachEndData {
fn from(_data: ArkEventData) -> Self {
ReachEndData
}
}
impl From<&ArkEventData> for ReachEndData {
fn from(_data: &ArkEventData) -> Self {
ReachEndData
}
}
#[cfg(test)]
mod tests {
use super::{
classify_event_name, ArkEventData, ArkEventKind, ArkEventPayload, KeyAction, KeyData,
KeyPayload, KeyboardKey, MouseButton, MouseData, MousePayload, PointerAction,
};
#[test]
fn canonical_event_names_share_one_semantic_identity() {
for name in ["onclick", "click"] {
assert_eq!(classify_event_name(name), Some(ArkEventKind::Click));
}
for name in ["onlongpress", "longpress"] {
assert_eq!(classify_event_name(name), Some(ArkEventKind::LongPress));
}
for name in ["onfocus", "focus"] {
assert_eq!(classify_event_name(name), Some(ArkEventKind::Focus));
}
for name in ["onblur", "blur"] {
assert_eq!(classify_event_name(name), Some(ArkEventKind::Blur));
}
for name in ["onreachend", "reachend"] {
assert_eq!(classify_event_name(name), Some(ArkEventKind::ReachEnd));
}
for name in ["onkey", "key"] {
assert_eq!(classify_event_name(name), Some(ArkEventKind::Key));
}
for name in ["onmouse", "mouse"] {
assert_eq!(classify_event_name(name), Some(ArkEventKind::Mouse));
}
assert!(ArkEventKind::Click.bubbles());
assert!(ArkEventKind::LongPress.bubbles());
assert!(!ArkEventKind::Change.bubbles());
}
#[test]
fn keyboard_activation_requires_a_press_of_enter_or_space() {
let enter = KeyData::from(ArkEventData::with_payload(ArkEventPayload::Key(
KeyPayload {
key: KeyboardKey::Enter,
action: KeyAction::Down,
..Default::default()
},
)));
assert!(enter.activates());
let released_space = KeyData::from(ArkEventData::with_payload(ArkEventPayload::Key(
KeyPayload {
key: KeyboardKey::Space,
action: KeyAction::Up,
..Default::default()
},
)));
assert!(!released_space.activates());
let repeated = KeyData {
key: KeyboardKey::Enter,
action: KeyAction::Repeat,
..Default::default()
};
assert!(repeated.is_down());
assert!(!repeated.activates());
let shortcut = KeyData {
key: KeyboardKey::Enter,
action: KeyAction::Down,
modifiers: super::KeyModifiers {
ctrl: true,
..Default::default()
},
..Default::default()
};
assert!(!shortcut.activates());
}
#[test]
fn native_key_capture_preserves_unclaimed_editing_and_shortcuts() {
let capture = super::KeyboardCapture::from_names("enter space left right escape");
let mut key = KeyPayload {
key: KeyboardKey::ArrowRight,
action: KeyAction::Down,
..Default::default()
};
assert!(capture.captures(&key));
key.modifiers.ctrl = true;
assert!(!capture.captures(&key));
key.key = KeyboardKey::Character('c');
assert!(!capture.captures(&key));
assert!(super::KeyboardCapture::from_names("all").captures(&key));
}
#[test]
fn roving_navigation_skips_disabled_items_and_keeps_calendar_columns() {
use super::{keyboard_navigation_target as next, KeyboardNavigation};
let enabled = [true, false, true, true];
assert_eq!(
next(
0,
KeyboardKey::ArrowRight,
KeyboardNavigation::Horizontal,
&enabled
),
Some(2)
);
assert_eq!(
next(
0,
KeyboardKey::ArrowLeft,
KeyboardNavigation::Horizontal,
&enabled
),
Some(3)
);
let grid = [true; 42];
assert_eq!(
next(
8,
KeyboardKey::ArrowDown,
KeyboardNavigation::Grid(7),
&grid
),
Some(15)
);
assert_eq!(
next(8, KeyboardKey::ArrowUp, KeyboardNavigation::Grid(7), &grid),
Some(1)
);
assert_eq!(
next(1, KeyboardKey::ArrowUp, KeyboardNavigation::Grid(7), &grid),
None
);
assert_eq!(
next(0, KeyboardKey::End, KeyboardNavigation::Vertical, &enabled),
Some(3)
);
assert_eq!(
next(
0,
KeyboardKey::ArrowRight,
KeyboardNavigation::Horizontal,
&[false; 3]
),
None
);
}
#[test]
fn secondary_mouse_press_is_preserved() {
let mouse = MouseData::from(ArkEventData::with_payload(ArkEventPayload::Mouse(
MousePayload {
button: MouseButton::Secondary,
action: PointerAction::Down,
window_x: 24.0,
window_y: 48.0,
..Default::default()
},
)));
assert!(mouse.secondary_down());
assert_eq!((mouse.window_x, mouse.window_y), (24.0, 48.0));
}
#[test]
fn every_declared_listener_classifies_in_both_spellings() {
let mut seen = std::collections::HashSet::new();
assert!(!crate::events::EVENT_KINDS.is_empty());
for (canonical, kind) in crate::events::EVENT_KINDS {
assert!(canonical.starts_with("on"), "{canonical}");
assert!(seen.insert(*canonical), "duplicate listener {canonical}");
assert_eq!(classify_event_name(canonical), Some(*kind), "{canonical}");
let compact = &canonical["on".len()..];
assert_eq!(classify_event_name(compact), Some(*kind), "{compact}");
}
}
#[test]
fn historical_snake_case_aliases_are_rejected() {
for name in [
"on_press",
"_press",
"on_long_press",
"_long_press",
"on_focus",
"_focus",
"on_blur",
"_blur",
"on_reach_end",
"_reach_end",
] {
assert_eq!(classify_event_name(name), None, "{name}");
}
}
}