#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct Point {
pub(crate) x: f32,
pub(crate) y: f32,
}
impl Point {
pub(crate) fn new(x: f32, y: f32) -> Point {
Point { x, y }
}
#[expect(
clippy::cast_possible_truncation,
reason = "page coordinates beyond f32 have already lost meaning, and every \
geometric query in this crate is f32 — see the type's own docs"
)]
pub(crate) fn narrow(at: kurbo::Point) -> Point {
Point::new(at.x as f32, at.y as f32)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Button {
Left,
Right,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Key {
Unknown,
Backspace,
Tab,
Newline,
Return,
Escape,
Space,
PageUp,
PageDown,
End,
Home,
Left,
Up,
Right,
Down,
Insert,
Delete,
A,
Y,
Z,
Shift,
Control,
Other(u16),
}
impl Key {
#[must_use]
pub const fn from_virtual(code: u16) -> Key {
match code {
0x00 => Key::Unknown,
0x08 => Key::Backspace,
0x09 => Key::Tab,
0x0A => Key::Newline,
0x0D => Key::Return,
0x10 => Key::Shift,
0x11 => Key::Control,
0x1B => Key::Escape,
0x20 => Key::Space,
0x21 => Key::PageUp,
0x22 => Key::PageDown,
0x23 => Key::End,
0x24 => Key::Home,
0x25 => Key::Left,
0x26 => Key::Up,
0x27 => Key::Right,
0x28 => Key::Down,
0x2D => Key::Insert,
0x2E => Key::Delete,
0x41 => Key::A,
0x59 => Key::Y,
0x5A => Key::Z,
other => Key::Other(other),
}
}
#[must_use]
pub const fn virtual_code(self) -> u16 {
match self {
Key::Unknown => 0x00,
Key::Backspace => 0x08,
Key::Tab => 0x09,
Key::Newline => 0x0A,
Key::Return => 0x0D,
Key::Shift => 0x10,
Key::Control => 0x11,
Key::Escape => 0x1B,
Key::Space => 0x20,
Key::PageUp => 0x21,
Key::PageDown => 0x22,
Key::End => 0x23,
Key::Home => 0x24,
Key::Left => 0x25,
Key::Up => 0x26,
Key::Right => 0x27,
Key::Down => 0x28,
Key::Insert => 0x2D,
Key::Delete => 0x2E,
Key::A => 0x41,
Key::Y => 0x59,
Key::Z => 0x5A,
Key::Other(code) => code,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
pub struct Modifiers(u32);
impl Modifiers {
pub const NONE: Self = Self(0);
pub const SHIFT: Self = Self(1 << 0);
pub const CONTROL: Self = Self(1 << 1);
pub const ALT: Self = Self(1 << 2);
pub const META: Self = Self(1 << 3);
pub const KEYPAD: Self = Self(1 << 4);
pub const AUTO_REPEAT: Self = Self(1 << 5);
pub const LEFT_BUTTON: Self = Self(1 << 6);
pub const MIDDLE_BUTTON: Self = Self(1 << 7);
pub const RIGHT_BUTTON: Self = Self(1 << 8);
#[must_use]
pub const fn bits(self) -> u32 {
self.0
}
#[must_use]
pub const fn from_bits(bits: u32) -> Self {
Self(bits)
}
#[must_use]
pub const fn contains(self, other: Self) -> bool {
self.0 & other.0 == other.0
}
#[must_use]
pub const fn union(self, other: Self) -> Self {
Self(self.0 | other.0)
}
#[must_use]
pub const fn with(self, other: Self) -> Self {
self.union(other)
}
#[must_use]
pub const fn without(self, other: Self) -> Self {
Self(self.0 & !other.0)
}
#[must_use]
pub const fn is_empty(self) -> bool {
self.0 == 0
}
}
impl std::ops::BitOr for Modifiers {
type Output = Self;
fn bitor(self, rhs: Self) -> Self {
self.union(rhs)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Event {
MouseMove {
at: kurbo::Point,
modifiers: Modifiers,
},
MouseDown {
button: Button,
at: kurbo::Point,
modifiers: Modifiers,
},
MouseUp {
button: Button,
at: kurbo::Point,
modifiers: Modifiers,
},
DoubleClick {
at: kurbo::Point,
modifiers: Modifiers,
},
MouseWheel {
at: kurbo::Point,
delta: (i32, i32),
modifiers: Modifiers,
},
Focus {
at: kurbo::Point,
modifiers: Modifiers,
},
KeyDown {
key: Key,
modifiers: Modifiers,
},
Char {
ch: char,
modifiers: Modifiers,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[expect(
clippy::float_cmp,
reason = "bit-exactness is the assertion: a tolerance would pass under \
precisely the half-migration this test exists to forbid"
)]
#[test]
fn a_fractional_coordinate_is_narrowed_before_any_comparison() {
let at = kurbo::Point::new(10.1, 713.7);
let narrowed = Point::narrow(at);
assert_eq!(narrowed.x, 10.1_f32);
assert_eq!(narrowed.y, 713.7_f32);
assert!(f64::from(narrowed.x) != at.x, "10.1 is not exact in f32");
let edge = crate::page::to_rect(kurbo::Rect::new(10.1, 713.7, 20.0, 800.0));
assert_eq!(narrowed.x, edge.left);
assert_eq!(narrowed.y, edge.bottom);
assert!(
crate::hit::contains(edge, narrowed.x, narrowed.y),
"a click exactly on a fractional edge is inside it"
);
}
#[expect(
clippy::float_cmp,
reason = "exactness is the assertion — this is why no golden moved"
)]
#[test]
fn an_integer_coordinate_round_trips_exactly() {
for value in [0.0_f64, 1.0, 312.0, -450.0, 9999.0] {
let narrowed = Point::narrow(kurbo::Point::new(value, value));
assert_eq!(f64::from(narrowed.x), value);
assert_eq!(f64::from(narrowed.y), value);
}
}
#[test]
fn modifiers_contains_is_subset_not_equality() {
let both = Modifiers::SHIFT | Modifiers::CONTROL;
assert!(both.contains(Modifiers::SHIFT));
assert!(both.contains(Modifiers::CONTROL));
assert!(both.contains(Modifiers::NONE));
assert!(!both.contains(Modifiers::ALT));
assert!(!Modifiers::SHIFT.contains(both));
}
#[test]
fn modifiers_none_is_empty_and_everything_contains_it() {
assert!(Modifiers::NONE.is_empty());
assert!(!Modifiers::SHIFT.is_empty());
assert!(Modifiers::NONE.contains(Modifiers::NONE));
}
#[test]
fn modifiers_without_removes_only_named_bits() {
let all = Modifiers::SHIFT | Modifiers::CONTROL | Modifiers::ALT;
assert_eq!(
all.without(Modifiers::CONTROL),
Modifiers::SHIFT | Modifiers::ALT
);
}
#[test]
fn modifier_bits_are_the_documented_wire_values() {
assert_eq!(Modifiers::SHIFT.bits(), 1);
assert_eq!(Modifiers::CONTROL.bits(), 2);
assert_eq!((Modifiers::SHIFT | Modifiers::CONTROL).bits(), 3);
assert_eq!(Modifiers::ALT.bits(), 4);
assert_eq!(Modifiers::META.bits(), 8);
}
#[test]
fn unknown_modifier_bits_round_trip() {
let f = Modifiers::from_bits((1 << 30) | Modifiers::SHIFT.bits());
assert_eq!(f.bits(), (1 << 30) | 1);
assert!(f.contains(Modifiers::SHIFT));
assert!(!f.contains(Modifiers::CONTROL));
}
#[test]
fn modifier_set_algebra() {
let m = Modifiers::SHIFT | Modifiers::CONTROL | Modifiers::ALT;
assert!(m.contains(Modifiers::SHIFT | Modifiers::ALT));
assert!(m.contains(Modifiers::NONE));
assert!(!m.contains(Modifiers::SHIFT | Modifiers::META));
assert_eq!(
m.without(Modifiers::CONTROL),
Modifiers::SHIFT | Modifiers::ALT
);
assert_eq!(Modifiers::NONE.with(Modifiers::META), Modifiers::META);
assert!(Modifiers::NONE.is_empty());
assert!(!m.is_empty());
}
#[test]
fn key_codes_are_the_virtual_key_codes() {
assert_eq!(Key::Tab.virtual_code(), 0x09);
assert_eq!(Key::Return.virtual_code(), 0x0D);
assert_eq!(Key::Delete.virtual_code(), 0x2E);
assert_eq!(Key::A.virtual_code(), 0x41);
assert_eq!(Key::Z.virtual_code(), 0x5A);
}
#[test]
fn every_key_round_trips_through_its_virtual_code() {
let named = [
Key::Unknown,
Key::Backspace,
Key::Tab,
Key::Newline,
Key::Return,
Key::Escape,
Key::Space,
Key::PageUp,
Key::PageDown,
Key::End,
Key::Home,
Key::Left,
Key::Up,
Key::Right,
Key::Down,
Key::Insert,
Key::Delete,
Key::A,
Key::Y,
Key::Z,
Key::Shift,
Key::Control,
];
for key in named {
assert_eq!(Key::from_virtual(key.virtual_code()), key, "{key:?}");
}
let mut codes: Vec<u16> = named.iter().map(|k| k.virtual_code()).collect();
codes.sort_unstable();
let count = codes.len();
codes.dedup();
assert_eq!(codes.len(), count, "two variants share a virtual code");
}
#[test]
fn undecided_codes_arrive_as_other_unchanged() {
for code in [0x70_u16, 0x30, 0x43, 0x56, 0x58, 0xFFFF] {
assert_eq!(Key::from_virtual(code), Key::Other(code));
assert_eq!(Key::Other(code).virtual_code(), code);
}
}
}