use core::fmt;
bitflags::bitflags! {
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct Modifiers: u32 {
const SHIFT = 1 << 0;
const CTRL = 1 << 1;
const ALT = 1 << 2;
const SUPER = 1 << 3;
const CAPS_LOCK = 1 << 4;
const NUM_LOCK = 1 << 5;
const ALT_GR = 1 << 6;
}
}
impl Modifiers {
pub const CHORD: Self = Self::SHIFT
.union(Self::CTRL)
.union(Self::ALT)
.union(Self::SUPER);
#[inline]
#[must_use]
pub const fn chord(self) -> Self {
self.intersection(Self::CHORD)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct DeviceId(pub u32);
impl DeviceId {
pub const UNKNOWN: Self = Self(0);
}
impl fmt::Display for DeviceId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if *self == Self::UNKNOWN {
f.write_str("device ?")
} else {
write!(f, "device {}", self.0)
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ButtonState {
Pressed,
Released,
}
impl ButtonState {
#[inline]
#[must_use]
pub const fn is_pressed(self) -> bool {
matches!(self, Self::Pressed)
}
#[inline]
#[must_use]
pub const fn from_pressed(pressed: bool) -> Self {
if pressed {
Self::Pressed
} else {
Self::Released
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Scancode(pub u32);
impl fmt::Display for Scancode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "scancode {}", self.0)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Keysym(pub u32);
impl Keysym {
pub const NONE: Self = Self(0);
const UNICODE_BASE: u32 = 0x0100_0000;
#[must_use]
pub const fn to_char(self) -> Option<char> {
let codepoint = match self.0 {
0x20..=0x7E | 0xA0..=0xFF => self.0,
value if value >= Self::UNICODE_BASE => value - Self::UNICODE_BASE,
_ => return None,
};
char::from_u32(codepoint)
}
#[must_use]
pub const fn from_char(character: char) -> Self {
let codepoint = character as u32;
match codepoint {
0x20..=0x7E | 0xA0..=0xFF => Self(codepoint),
_ => Self(Self::UNICODE_BASE | codepoint),
}
}
#[inline]
#[must_use]
pub const fn is_none(self) -> bool {
self.0 == Self::NONE.0
}
}
impl fmt::Display for Keysym {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.to_char() {
Some(character) if !character.is_control() => write!(f, "'{character}'"),
_ => write!(f, "keysym {:#06x}", self.0),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum PointerButton {
Left,
Right,
Middle,
Back,
Forward,
Other(u16),
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ContactId(pub u32);
impl fmt::Display for ContactId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "contact {}", self.0)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum TouchPhase {
Began,
Moved,
Ended,
Cancelled,
}
impl TouchPhase {
#[inline]
#[must_use]
pub const fn ends_contact(self) -> bool {
matches!(self, Self::Ended | Self::Cancelled)
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Began => "Began",
Self::Moved => "Moved",
Self::Ended => "Ended",
Self::Cancelled => "Cancelled",
}
}
}
impl fmt::Display for TouchPhase {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ScrollDelta {
Lines {
x: f32,
y: f32,
},
Pixels {
x: f64,
y: f64,
},
}
impl ScrollDelta {
#[must_use]
pub fn is_zero(self) -> bool {
match self {
Self::Lines { x, y } => x == 0.0 && y == 0.0,
Self::Pixels { x, y } => x == 0.0 && y == 0.0,
}
}
}
macro_rules! key_codes {
($($variant:ident => $name:literal),+ $(,)?) => {
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum KeyCode {
$(
#[doc = concat!("The `", $name, "` key.")]
$variant,
)+
}
impl KeyCode {
pub const ALL: &'static [Self] = &[$(Self::$variant),+];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
$(Self::$variant => $name,)+
}
}
#[must_use]
pub fn from_name(name: &str) -> Option<Self> {
match name {
$($name => Some(Self::$variant),)+
_ => None,
}
}
}
};
}
key_codes! {
KeyA => "KeyA", KeyB => "KeyB", KeyC => "KeyC", KeyD => "KeyD", KeyE => "KeyE",
KeyF => "KeyF", KeyG => "KeyG", KeyH => "KeyH", KeyI => "KeyI", KeyJ => "KeyJ",
KeyK => "KeyK", KeyL => "KeyL", KeyM => "KeyM", KeyN => "KeyN", KeyO => "KeyO",
KeyP => "KeyP", KeyQ => "KeyQ", KeyR => "KeyR", KeyS => "KeyS", KeyT => "KeyT",
KeyU => "KeyU", KeyV => "KeyV", KeyW => "KeyW", KeyX => "KeyX", KeyY => "KeyY",
KeyZ => "KeyZ",
Digit0 => "Digit0", Digit1 => "Digit1", Digit2 => "Digit2", Digit3 => "Digit3",
Digit4 => "Digit4", Digit5 => "Digit5", Digit6 => "Digit6", Digit7 => "Digit7",
Digit8 => "Digit8", Digit9 => "Digit9",
Backquote => "Backquote", Minus => "Minus", Equal => "Equal",
BracketLeft => "BracketLeft", BracketRight => "BracketRight", Backslash => "Backslash",
Semicolon => "Semicolon", Quote => "Quote", Comma => "Comma", Period => "Period",
Slash => "Slash",
IntlBackslash => "IntlBackslash", IntlRo => "IntlRo", IntlYen => "IntlYen",
Escape => "Escape", Tab => "Tab", CapsLock => "CapsLock", Space => "Space",
Backspace => "Backspace", Enter => "Enter", Delete => "Delete", Insert => "Insert",
ShiftLeft => "ShiftLeft", ShiftRight => "ShiftRight",
ControlLeft => "ControlLeft", ControlRight => "ControlRight",
AltLeft => "AltLeft", AltRight => "AltRight",
SuperLeft => "SuperLeft", SuperRight => "SuperRight",
ContextMenu => "ContextMenu",
Home => "Home", End => "End", PageUp => "PageUp", PageDown => "PageDown",
ArrowUp => "ArrowUp", ArrowDown => "ArrowDown",
ArrowLeft => "ArrowLeft", ArrowRight => "ArrowRight",
F1 => "F1", F2 => "F2", F3 => "F3", F4 => "F4", F5 => "F5", F6 => "F6",
F7 => "F7", F8 => "F8", F9 => "F9", F10 => "F10", F11 => "F11", F12 => "F12",
PrintScreen => "PrintScreen", ScrollLock => "ScrollLock", Pause => "Pause",
NumLock => "NumLock",
Numpad0 => "Numpad0", Numpad1 => "Numpad1", Numpad2 => "Numpad2",
Numpad3 => "Numpad3", Numpad4 => "Numpad4", Numpad5 => "Numpad5",
Numpad6 => "Numpad6", Numpad7 => "Numpad7", Numpad8 => "Numpad8",
Numpad9 => "Numpad9",
NumpadDivide => "NumpadDivide", NumpadMultiply => "NumpadMultiply",
NumpadSubtract => "NumpadSubtract", NumpadAdd => "NumpadAdd",
NumpadEnter => "NumpadEnter", NumpadDecimal => "NumpadDecimal",
}
impl fmt::Display for KeyCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
macro_rules! cursor_icons {
($($(#[$meta:meta])* $variant:ident => $name:literal),+ $(,)?) => {
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum CursorIcon {
$($(#[$meta])* $variant,)+
}
impl CursorIcon {
pub const ALL: &'static [Self] = &[$(Self::$variant),+];
#[must_use]
pub const fn as_css_name(self) -> &'static str {
match self {
$(Self::$variant => $name,)+
}
}
#[must_use]
pub fn from_css_name(name: &str) -> Option<Self> {
Self::ALL
.iter()
.copied()
.find(|shape| shape.as_css_name().eq_ignore_ascii_case(name))
}
}
};
}
cursor_icons! {
#[default]
Default => "default",
Pointer => "pointer",
Text => "text",
Crosshair => "crosshair",
Wait => "wait",
Progress => "progress",
NotAllowed => "not-allowed",
Grab => "grab",
Grabbing => "grabbing",
Move => "move",
ResizeNorthSouth => "ns-resize",
ResizeEastWest => "ew-resize",
ResizeNorthEastSouthWest => "nesw-resize",
ResizeNorthWestSouthEast => "nwse-resize",
ResizeColumn => "col-resize",
ResizeRow => "row-resize",
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum TextPurpose {
#[default]
Normal,
Password,
Pin,
}
impl TextPurpose {
#[must_use]
pub const fn is_sensitive(self) -> bool {
match self {
Self::Normal => false,
Self::Password | Self::Pin => true,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cursor_keywords_are_unique_css_shaped_and_parse_back() {
let shapes = CursorIcon::ALL;
let mut names: Vec<&str> = shapes.iter().map(|shape| shape.as_css_name()).collect();
let count = names.len();
names.sort_unstable();
names.dedup();
assert_eq!(names.len(), count, "cursor keywords must be unique");
assert!(
names
.iter()
.all(|name| name.chars().all(|c| c.is_ascii_lowercase() || c == '-')),
"keywords are CSS identifiers"
);
assert_eq!(CursorIcon::default().as_css_name(), "default");
for &shape in shapes {
assert_eq!(CursorIcon::from_css_name(shape.as_css_name()), Some(shape));
}
assert_eq!(
CursorIcon::from_css_name("Not-Allowed"),
Some(CursorIcon::NotAllowed),
"keywords ignore ASCII case"
);
for not_a_shape in ["auto", "none", "", "pointer ", "hand"] {
assert_eq!(
CursorIcon::from_css_name(not_a_shape),
None,
"{not_a_shape:?}"
);
}
}
#[test]
fn key_names_round_trip_and_are_unique() {
let mut names: Vec<&'static str> = KeyCode::ALL.iter().map(|key| key.as_str()).collect();
let count = names.len();
assert!(count > 100, "the table covers a full keyboard, got {count}");
names.sort_unstable();
names.dedup();
assert_eq!(names.len(), count, "key names must be unique");
for key in KeyCode::ALL {
assert_eq!(KeyCode::from_name(key.as_str()), Some(*key));
assert_eq!(key.to_string(), key.as_str());
}
assert_eq!(KeyCode::from_name("KeyÆ"), None);
}
#[test]
fn wasd_is_a_physical_square_not_a_layout_one() {
for name in ["KeyW", "KeyA", "KeyS", "KeyD"] {
assert!(KeyCode::from_name(name).is_some(), "{name}");
}
}
#[test]
fn keysyms_map_both_ways_over_the_xkb_rules() {
assert_eq!(Keysym::from_char('a'), Keysym(0x61));
assert_eq!(Keysym(0x61).to_char(), Some('a'));
assert_eq!(Keysym::from_char('ä'), Keysym(0xE4));
assert_eq!(Keysym(0xE4).to_char(), Some('ä'));
let euro = Keysym::from_char('€');
assert_eq!(euro, Keysym(0x0100_20AC));
assert_eq!(euro.to_char(), Some('€'));
const XKB_KEY_F1: Keysym = Keysym(0xFFBE);
assert_eq!(XKB_KEY_F1.to_char(), None);
assert!(Keysym::NONE.is_none());
assert_eq!(Keysym::NONE.to_char(), None);
assert_eq!(Keysym::from_char('q').to_string(), "'q'");
assert_eq!(XKB_KEY_F1.to_string(), "keysym 0xffbe");
}
#[test]
fn every_printable_ascii_round_trips_through_a_keysym() {
for codepoint in 0x20u32..=0x7E {
let character = char::from_u32(codepoint).expect("ascii");
assert_eq!(Keysym::from_char(character).to_char(), Some(character));
}
}
#[test]
fn chord_modifiers_exclude_latched_state() {
let held = Modifiers::CTRL | Modifiers::SHIFT | Modifiers::CAPS_LOCK | Modifiers::NUM_LOCK;
assert_eq!(held.chord(), Modifiers::CTRL | Modifiers::SHIFT);
assert!(!Modifiers::CHORD.contains(Modifiers::ALT_GR));
assert_eq!(Modifiers::default(), Modifiers::empty());
}
#[test]
fn button_state_is_one_type_for_every_kind_of_button() {
assert!(ButtonState::from_pressed(true).is_pressed());
assert_eq!(ButtonState::from_pressed(false), ButtonState::Released);
}
#[test]
fn scroll_units_stay_distinct() {
let lines = ScrollDelta::Lines { x: 0.0, y: -1.0 };
let pixels = ScrollDelta::Pixels { x: 0.0, y: -13.0 };
assert_ne!(
format!("{lines:?}"),
format!("{pixels:?}"),
"a wheel detent and 13 px must not be the same value"
);
assert!(ScrollDelta::Lines { x: 0.0, y: 0.0 }.is_zero());
assert!(ScrollDelta::Pixels { x: 0.0, y: 0.0 }.is_zero());
assert!(!lines.is_zero());
assert!(!pixels.is_zero());
}
#[test]
fn device_ids_are_displayable_and_default_to_unknown() {
assert_eq!(DeviceId::default(), DeviceId::UNKNOWN);
assert_eq!(DeviceId::UNKNOWN.to_string(), "device ?");
assert_eq!(DeviceId(4).to_string(), "device 4");
assert_eq!(Scancode(30).to_string(), "scancode 30");
}
}