Another project
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(ui): input snapshot, key/pointer events

Lewis: May this revision serve well! <lu5a@proton.me>

+380
+103
crates/bone-ui/src/input/key.rs
··· 1 + use serde::{Deserialize, Serialize}; 2 + 3 + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] 4 + #[serde(transparent)] 5 + pub struct ModifierMask(u8); 6 + 7 + impl ModifierMask { 8 + pub const NONE: Self = Self(0); 9 + pub const CTRL: Self = Self(1 << 0); 10 + pub const SHIFT: Self = Self(1 << 1); 11 + pub const ALT: Self = Self(1 << 2); 12 + pub const META: Self = Self(1 << 3); 13 + 14 + #[must_use] 15 + pub const fn contains(self, other: Self) -> bool { 16 + (self.0 & other.0) == other.0 17 + } 18 + 19 + #[must_use] 20 + pub const fn union(self, other: Self) -> Self { 21 + Self(self.0 | other.0) 22 + } 23 + } 24 + 25 + impl core::ops::BitOr for ModifierMask { 26 + type Output = Self; 27 + fn bitor(self, rhs: Self) -> Self { 28 + self.union(rhs) 29 + } 30 + } 31 + 32 + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] 33 + pub enum NamedKey { 34 + Tab, 35 + Enter, 36 + Escape, 37 + Backspace, 38 + Delete, 39 + Space, 40 + ArrowUp, 41 + ArrowDown, 42 + ArrowLeft, 43 + ArrowRight, 44 + Home, 45 + End, 46 + PageUp, 47 + PageDown, 48 + } 49 + 50 + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] 51 + #[serde(transparent)] 52 + pub struct KeyChar(char); 53 + 54 + impl KeyChar { 55 + #[must_use] 56 + pub fn from_char(c: char) -> Self { 57 + Self(c.to_lowercase().next().unwrap_or(c)) 58 + } 59 + 60 + #[must_use] 61 + pub const fn get(self) -> char { 62 + self.0 63 + } 64 + } 65 + 66 + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] 67 + pub enum KeyCode { 68 + Named(NamedKey), 69 + Char(KeyChar), 70 + } 71 + 72 + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] 73 + pub struct KeyEvent { 74 + pub code: KeyCode, 75 + pub modifiers: ModifierMask, 76 + } 77 + 78 + impl KeyEvent { 79 + #[must_use] 80 + pub const fn new(code: KeyCode, modifiers: ModifierMask) -> Self { 81 + Self { code, modifiers } 82 + } 83 + } 84 + 85 + #[cfg(test)] 86 + mod tests { 87 + use super::{KeyChar, ModifierMask}; 88 + 89 + #[test] 90 + fn modifier_contains_self() { 91 + let m = ModifierMask::CTRL | ModifierMask::SHIFT; 92 + assert!(m.contains(ModifierMask::CTRL)); 93 + assert!(m.contains(ModifierMask::SHIFT)); 94 + assert!(!m.contains(ModifierMask::ALT)); 95 + } 96 + 97 + #[test] 98 + fn key_char_normalizes_to_lower() { 99 + assert_eq!(KeyChar::from_char('A').get(), 'a'); 100 + assert_eq!(KeyChar::from_char('z').get(), 'z'); 101 + assert_eq!(KeyChar::from_char('É').get(), 'é'); 102 + } 103 + }
+50
crates/bone-ui/src/input/mod.rs
··· 1 + mod key; 2 + mod pointer; 3 + 4 + pub use key::{KeyChar, KeyCode, KeyEvent, ModifierMask, NamedKey}; 5 + pub use pointer::{ 6 + ClickCount, DoubleClickWindow, DragThreshold, FrameInstant, PointerButton, PointerButtonMask, 7 + PointerSample, 8 + }; 9 + 10 + use serde::{Deserialize, Serialize}; 11 + 12 + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 13 + pub struct InputSnapshot { 14 + pub frame: FrameInstant, 15 + pub pointer: Option<PointerSample>, 16 + pub buttons_pressed: PointerButtonMask, 17 + pub buttons_released: PointerButtonMask, 18 + pub keys_pressed: Vec<KeyEvent>, 19 + pub double_click_window: DoubleClickWindow, 20 + pub drag_threshold: DragThreshold, 21 + } 22 + 23 + impl InputSnapshot { 24 + #[must_use] 25 + pub fn idle(frame: FrameInstant) -> Self { 26 + Self { 27 + frame, 28 + pointer: None, 29 + buttons_pressed: PointerButtonMask::EMPTY, 30 + buttons_released: PointerButtonMask::EMPTY, 31 + keys_pressed: Vec::new(), 32 + double_click_window: DoubleClickWindow::DEFAULT, 33 + drag_threshold: DragThreshold::DEFAULT, 34 + } 35 + } 36 + } 37 + 38 + #[cfg(test)] 39 + mod tests { 40 + use super::{FrameInstant, InputSnapshot}; 41 + 42 + #[test] 43 + fn idle_snapshot_has_no_input() { 44 + let snap = InputSnapshot::idle(FrameInstant::ZERO); 45 + assert!(snap.pointer.is_none()); 46 + assert!(snap.buttons_pressed.is_empty()); 47 + assert!(snap.buttons_released.is_empty()); 48 + assert!(snap.keys_pressed.is_empty()); 49 + } 50 + }
+227
crates/bone-ui/src/input/pointer.rs
··· 1 + use core::time::Duration; 2 + 3 + use serde::{Deserialize, Serialize}; 4 + 5 + use crate::layout::{LayoutOffset, LayoutPos, LayoutPx}; 6 + 7 + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] 8 + pub enum PointerButton { 9 + Primary, 10 + Secondary, 11 + Middle, 12 + } 13 + 14 + impl PointerButton { 15 + pub const ALL: [Self; 3] = [Self::Primary, Self::Secondary, Self::Middle]; 16 + 17 + #[must_use] 18 + pub const fn bit(self) -> u8 { 19 + match self { 20 + Self::Primary => 1 << 0, 21 + Self::Secondary => 1 << 1, 22 + Self::Middle => 1 << 2, 23 + } 24 + } 25 + } 26 + 27 + #[derive( 28 + Copy, Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, 29 + )] 30 + #[serde(transparent)] 31 + pub struct PointerButtonMask(u8); 32 + 33 + impl PointerButtonMask { 34 + pub const EMPTY: Self = Self(0); 35 + 36 + #[must_use] 37 + pub const fn just(button: PointerButton) -> Self { 38 + Self(button.bit()) 39 + } 40 + 41 + #[must_use] 42 + pub const fn contains(self, button: PointerButton) -> bool { 43 + (self.0 & button.bit()) != 0 44 + } 45 + 46 + #[must_use] 47 + pub const fn is_empty(self) -> bool { 48 + self.0 == 0 49 + } 50 + 51 + #[must_use] 52 + pub const fn with(self, button: PointerButton) -> Self { 53 + Self(self.0 | button.bit()) 54 + } 55 + } 56 + 57 + #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] 58 + #[serde(transparent)] 59 + pub struct FrameInstant(Duration); 60 + 61 + impl FrameInstant { 62 + pub const ZERO: Self = Self(Duration::ZERO); 63 + 64 + #[must_use] 65 + pub const fn from_duration(d: Duration) -> Self { 66 + Self(d) 67 + } 68 + 69 + #[must_use] 70 + pub const fn duration(self) -> Duration { 71 + self.0 72 + } 73 + 74 + #[must_use] 75 + pub fn since(self, earlier: Self) -> Duration { 76 + self.0.saturating_sub(earlier.0) 77 + } 78 + } 79 + 80 + #[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Serialize, Deserialize)] 81 + #[serde(transparent)] 82 + pub struct DragThreshold(LayoutPx); 83 + 84 + impl DragThreshold { 85 + pub const DEFAULT: Self = Self(LayoutPx::new(4.0)); 86 + 87 + #[must_use] 88 + pub const fn new(px: LayoutPx) -> Self { 89 + assert!(px.value() >= 0.0, "DragThreshold must be non-negative"); 90 + Self(px) 91 + } 92 + 93 + #[must_use] 94 + pub const fn px(self) -> LayoutPx { 95 + self.0 96 + } 97 + 98 + #[must_use] 99 + pub fn exceeded_by(self, offset: LayoutOffset) -> bool { 100 + let dx = offset.dx.value(); 101 + let dy = offset.dy.value(); 102 + let t = self.0.value(); 103 + dx * dx + dy * dy > t * t 104 + } 105 + } 106 + 107 + #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] 108 + #[serde(transparent)] 109 + pub struct DoubleClickWindow(Duration); 110 + 111 + impl DoubleClickWindow { 112 + pub const DEFAULT: Self = Self(Duration::from_millis(400)); 113 + 114 + #[must_use] 115 + pub const fn new(window: Duration) -> Self { 116 + Self(window) 117 + } 118 + 119 + #[must_use] 120 + pub const fn duration(self) -> Duration { 121 + self.0 122 + } 123 + 124 + #[must_use] 125 + pub fn contains(self, gap: Duration) -> bool { 126 + gap <= self.0 127 + } 128 + } 129 + 130 + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] 131 + #[serde(transparent)] 132 + pub struct ClickCount(u8); 133 + 134 + impl ClickCount { 135 + pub const SINGLE: Self = Self(1); 136 + pub const DOUBLE: Self = Self(2); 137 + 138 + #[must_use] 139 + pub const fn new(count: u8) -> Self { 140 + Self(count) 141 + } 142 + 143 + #[must_use] 144 + pub const fn get(self) -> u8 { 145 + self.0 146 + } 147 + 148 + #[must_use] 149 + pub const fn next(self) -> Self { 150 + Self(self.0.saturating_add(1)) 151 + } 152 + 153 + #[must_use] 154 + pub const fn is_double(self) -> bool { 155 + self.0 >= 2 156 + } 157 + } 158 + 159 + #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] 160 + pub struct PointerSample { 161 + pub position: LayoutPos, 162 + } 163 + 164 + impl PointerSample { 165 + #[must_use] 166 + pub const fn new(position: LayoutPos) -> Self { 167 + Self { position } 168 + } 169 + } 170 + 171 + #[cfg(test)] 172 + mod tests { 173 + use core::time::Duration; 174 + 175 + use super::{ 176 + ClickCount, DoubleClickWindow, DragThreshold, FrameInstant, PointerButton, 177 + PointerButtonMask, 178 + }; 179 + use crate::layout::{LayoutOffset, LayoutPx}; 180 + 181 + #[test] 182 + fn button_mask_with_just() { 183 + let mask = PointerButtonMask::just(PointerButton::Primary).with(PointerButton::Middle); 184 + assert!(mask.contains(PointerButton::Primary)); 185 + assert!(mask.contains(PointerButton::Middle)); 186 + assert!(!mask.contains(PointerButton::Secondary)); 187 + } 188 + 189 + #[test] 190 + fn drag_threshold_uses_squared_distance() { 191 + let t = DragThreshold::new(LayoutPx::new(5.0)); 192 + let inside = LayoutOffset::new(LayoutPx::new(3.0), LayoutPx::new(3.0)); 193 + let outside = LayoutOffset::new(LayoutPx::new(4.0), LayoutPx::new(4.0)); 194 + assert!(!t.exceeded_by(inside)); 195 + assert!(t.exceeded_by(outside)); 196 + } 197 + 198 + #[test] 199 + fn double_click_window_contains_short_gap() { 200 + let w = DoubleClickWindow::DEFAULT; 201 + assert!(w.contains(Duration::from_millis(200))); 202 + assert!(!w.contains(Duration::from_millis(800))); 203 + } 204 + 205 + #[test] 206 + fn click_count_progresses() { 207 + let one = ClickCount::SINGLE; 208 + let two = one.next(); 209 + assert!(!one.is_double()); 210 + assert!(two.is_double()); 211 + } 212 + 213 + #[test] 214 + fn click_count_triple_remains_double_flag() { 215 + let three = ClickCount::SINGLE.next().next(); 216 + assert_eq!(three.get(), 3); 217 + assert!(three.is_double()); 218 + } 219 + 220 + #[test] 221 + fn frame_instant_since_clamps() { 222 + let later = FrameInstant::from_duration(Duration::from_millis(100)); 223 + let earlier = FrameInstant::from_duration(Duration::from_millis(40)); 224 + assert_eq!(later.since(earlier), Duration::from_millis(60)); 225 + assert_eq!(earlier.since(later), Duration::ZERO); 226 + } 227 + }