#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::{sanitize_str, Vec2};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum EventStatus {
#[default]
Ignored,
Captured,
}
impl EventStatus {
pub const fn is_captured(self) -> bool {
matches!(self, Self::Captured)
}
pub const fn merge(self, other: Self) -> Self {
match (self, other) {
(Self::Captured, _) | (_, Self::Captured) => Self::Captured,
(Self::Ignored, Self::Ignored) => Self::Ignored,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum CursorInteraction {
#[default]
Idle,
Pointer,
Text,
Grab,
Grabbing,
ResizeHorizontal,
ResizeVertical,
ResizeDiagonalDown,
ResizeDiagonalUp,
NotAllowed,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct EventResponse {
pub status: EventStatus,
pub cursor: CursorInteraction,
}
impl EventResponse {
pub const fn ignored() -> Self {
Self {
status: EventStatus::Ignored,
cursor: CursorInteraction::Idle,
}
}
pub const fn captured(cursor: CursorInteraction) -> Self {
Self {
status: EventStatus::Captured,
cursor,
}
}
pub const fn merge(self, other: Self) -> Self {
Self {
status: self.status.merge(other.status),
cursor: match other.cursor {
CursorInteraction::Idle => self.cursor,
cursor => cursor,
},
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum MouseButton {
Left,
Right,
Middle,
Back,
Forward,
Other(u16),
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum MouseScrollDelta {
Lines { x: f32, y: f32 },
Pixels { x: f32, y: f32 },
}
impl MouseScrollDelta {
pub fn pixels(self, line_height_px: f32) -> Vec2 {
let line_height_px = finite_or(line_height_px, 16.0).max(1.0);
match self {
Self::Lines { x, y } => {
Vec2::new(finite_or(x, 0.0), finite_or(y, 0.0)) * line_height_px
}
Self::Pixels { x, y } => Vec2::new(finite_or(x, 0.0), finite_or(y, 0.0)),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum MouseEvent {
CursorMoved {
position: Vec2,
},
CursorEntered,
CursorLeft,
ButtonPressed {
button: MouseButton,
position: Vec2,
},
ButtonReleased {
button: MouseButton,
position: Vec2,
},
WheelScrolled {
delta: MouseScrollDelta,
position: Vec2,
},
}
impl MouseEvent {
pub fn position(self) -> Option<Vec2> {
match self {
Self::CursorMoved { position }
| Self::ButtonPressed { position, .. }
| Self::ButtonReleased { position, .. }
| Self::WheelScrolled { position, .. } => Some(sanitize_vec2(position)),
Self::CursorEntered | Self::CursorLeft => None,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct KeyModifiers {
pub ctrl: bool,
pub shift: bool,
pub alt: bool,
pub meta: bool,
}
impl KeyModifiers {
pub const fn empty() -> Self {
Self {
ctrl: false,
shift: false,
alt: false,
meta: false,
}
}
pub const fn ctrl_shift() -> Self {
Self {
ctrl: true,
shift: true,
alt: false,
meta: false,
}
}
pub fn primary() -> Self {
if cfg!(target_os = "macos") {
Self {
meta: true,
..Self::empty()
}
} else {
Self {
ctrl: true,
..Self::empty()
}
}
}
pub fn primary_active(self) -> bool {
if cfg!(target_os = "macos") {
self.meta
} else {
self.ctrl
}
}
pub fn jump_active(self) -> bool {
if cfg!(target_os = "macos") {
self.alt
} else {
self.ctrl
}
}
pub fn label(self) -> String {
let mut labels = Vec::with_capacity(4);
if self.ctrl {
labels.push("Ctrl");
}
if self.shift {
labels.push("Shift");
}
if self.alt {
labels.push("Alt");
}
if self.meta {
labels.push(if cfg!(target_os = "macos") {
"Cmd"
} else {
"Meta"
});
}
labels.join("+")
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum KeyboardKey {
Character(String),
Named(NamedKey),
}
impl KeyboardKey {
pub fn character(input: impl Into<String>) -> Self {
Self::Character(sanitize_str(&input.into(), 32))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum NamedKey {
Enter,
Escape,
Tab,
Backspace,
Delete,
ArrowLeft,
ArrowRight,
ArrowUp,
ArrowDown,
Home,
End,
PageUp,
PageDown,
Space,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum KeyboardEvent {
KeyPressed {
key: KeyboardKey,
modifiers: KeyModifiers,
repeat: bool,
},
KeyReleased {
key: KeyboardKey,
modifiers: KeyModifiers,
},
ModifiersChanged(KeyModifiers),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum TouchPhase {
Started,
Moved,
Ended,
Cancelled,
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct TouchEvent {
pub id: u64,
pub phase: TouchPhase,
pub position: Vec2,
}
impl TouchEvent {
pub fn new(id: u64, phase: TouchPhase, position: Vec2) -> Self {
Self {
id,
phase,
position: sanitize_vec2(position),
}
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum InputMethodEvent {
Enabled,
Disabled,
Preedit { text: String, cursor: Option<usize> },
Commit(String),
}
impl InputMethodEvent {
pub fn preedit(text: impl Into<String>, cursor: Option<usize>) -> Self {
let text = sanitize_str(&text.into(), 512);
Self::Preedit {
cursor: cursor.map(|cursor| cursor.min(text.chars().count())),
text,
}
}
pub fn commit(text: impl Into<String>) -> Self {
Self::Commit(sanitize_str(&text.into(), 4096))
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum WindowEvent {
Resized { width: f32, height: f32 },
ScaleFactorChanged(f32),
Focused(bool),
CloseRequested,
RedrawRequested,
}
impl WindowEvent {
pub fn sanitized(self) -> Self {
match self {
Self::Resized { width, height } => Self::Resized {
width: finite_or(width, 0.0).max(0.0),
height: finite_or(height, 0.0).max(0.0),
},
Self::ScaleFactorChanged(scale) => {
Self::ScaleFactorChanged(finite_or(scale, 1.0).max(0.01))
}
event => event,
}
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum UiEvent {
Mouse(MouseEvent),
Keyboard(KeyboardEvent),
Touch(TouchEvent),
Window(WindowEvent),
InputMethod(InputMethodEvent),
ClipboardPaste(String),
Waken,
}
impl UiEvent {
pub fn pointer_position(&self) -> Option<Vec2> {
match self {
Self::Mouse(event) => event.position(),
Self::Touch(event) => Some(event.position),
_ => None,
}
}
pub fn is_pointer_event(&self) -> bool {
matches!(self, Self::Mouse(_) | Self::Touch(_))
}
pub fn sanitized(self) -> Self {
match self {
Self::Mouse(event) => match event {
MouseEvent::CursorMoved { position } => Self::Mouse(MouseEvent::CursorMoved {
position: sanitize_vec2(position),
}),
MouseEvent::ButtonPressed { button, position } => {
Self::Mouse(MouseEvent::ButtonPressed {
button,
position: sanitize_vec2(position),
})
}
MouseEvent::ButtonReleased { button, position } => {
Self::Mouse(MouseEvent::ButtonReleased {
button,
position: sanitize_vec2(position),
})
}
MouseEvent::WheelScrolled { delta, position } => {
Self::Mouse(MouseEvent::WheelScrolled {
delta,
position: sanitize_vec2(position),
})
}
event => Self::Mouse(event),
},
Self::Touch(event) => {
Self::Touch(TouchEvent::new(event.id, event.phase, event.position))
}
Self::Window(event) => Self::Window(event.sanitized()),
Self::InputMethod(InputMethodEvent::Commit(text)) => {
Self::InputMethod(InputMethodEvent::commit(text))
}
Self::InputMethod(InputMethodEvent::Preedit { text, cursor }) => {
Self::InputMethod(InputMethodEvent::preedit(text, cursor))
}
Self::ClipboardPaste(text) => Self::ClipboardPaste(sanitize_str(&text, 16_384)),
event => event,
}
}
}
fn sanitize_vec2(value: Vec2) -> Vec2 {
Vec2::new(finite_or(value.x, 0.0), finite_or(value.y, 0.0))
}
fn finite_or(value: f32, fallback: f32) -> f32 {
if value.is_finite() {
value
} else {
fallback
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn event_status_merge_captures_once() {
assert_eq!(
EventStatus::Ignored.merge(EventStatus::Ignored),
EventStatus::Ignored
);
assert_eq!(
EventStatus::Ignored.merge(EventStatus::Captured),
EventStatus::Captured
);
assert_eq!(
EventStatus::Captured.merge(EventStatus::Ignored),
EventStatus::Captured
);
}
#[test]
fn response_merge_keeps_latest_non_idle_cursor() {
let response = EventResponse::captured(CursorInteraction::Text)
.merge(EventResponse::captured(CursorInteraction::Pointer));
assert_eq!(response.status, EventStatus::Captured);
assert_eq!(response.cursor, CursorInteraction::Pointer);
}
#[test]
fn modifiers_use_platform_primary_without_cfg_branches_in_callers() {
let primary = KeyModifiers::primary();
assert!(primary.primary_active());
assert!(!KeyModifiers::empty().primary_active());
}
#[test]
fn events_sanitize_non_finite_public_coordinates() {
let event = UiEvent::Mouse(MouseEvent::CursorMoved {
position: Vec2::new(f32::NAN, f32::INFINITY),
})
.sanitized();
assert_eq!(event.pointer_position(), Some(Vec2::ZERO));
assert_eq!(
WindowEvent::ScaleFactorChanged(f32::NAN).sanitized(),
WindowEvent::ScaleFactorChanged(1.0)
);
}
#[test]
fn scroll_delta_normalizes_lines_to_pixels() {
assert_eq!(
MouseScrollDelta::Lines { x: 1.0, y: -2.0 }.pixels(20.0),
Vec2::new(20.0, -40.0)
);
assert_eq!(
MouseScrollDelta::Pixels {
x: f32::NAN,
y: 4.0
}
.pixels(20.0),
Vec2::new(0.0, 4.0)
);
}
}