Another project
1use core::time::Duration;
2
3use serde::{Deserialize, Serialize};
4
5use crate::layout::{LayoutOffset, LayoutPos, LayoutPx};
6
7#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
8pub enum PointerButton {
9 Primary,
10 Secondary,
11 Middle,
12}
13
14impl 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)]
31pub struct PointerButtonMask(u8);
32
33impl 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 #[must_use]
57 pub const fn without(self, button: PointerButton) -> Self {
58 Self(self.0 & !button.bit())
59 }
60}
61
62#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
63#[serde(transparent)]
64pub struct FrameInstant(Duration);
65
66impl FrameInstant {
67 pub const ZERO: Self = Self(Duration::ZERO);
68
69 #[must_use]
70 pub const fn from_duration(d: Duration) -> Self {
71 Self(d)
72 }
73
74 #[must_use]
75 pub const fn duration(self) -> Duration {
76 self.0
77 }
78
79 #[must_use]
80 pub fn since(self, earlier: Self) -> Duration {
81 self.0.saturating_sub(earlier.0)
82 }
83}
84
85#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Serialize, Deserialize)]
86#[serde(transparent)]
87pub struct DragThreshold(LayoutPx);
88
89impl DragThreshold {
90 pub const DEFAULT: Self = Self(LayoutPx::new(4.0));
91
92 #[must_use]
93 pub const fn new(px: LayoutPx) -> Self {
94 assert!(px.value() >= 0.0, "DragThreshold must be non-negative");
95 Self(px)
96 }
97
98 #[must_use]
99 pub const fn px(self) -> LayoutPx {
100 self.0
101 }
102
103 #[must_use]
104 pub fn exceeded_by(self, offset: LayoutOffset) -> bool {
105 let dx = offset.dx.value();
106 let dy = offset.dy.value();
107 let t = self.0.value();
108 dx * dx + dy * dy > t * t
109 }
110}
111
112#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
113#[serde(transparent)]
114pub struct DoubleClickWindow(Duration);
115
116impl DoubleClickWindow {
117 pub const DEFAULT: Self = Self(Duration::from_millis(400));
118
119 #[must_use]
120 pub const fn new(window: Duration) -> Self {
121 Self(window)
122 }
123
124 #[must_use]
125 pub const fn duration(self) -> Duration {
126 self.0
127 }
128
129 #[must_use]
130 pub fn contains(self, gap: Duration) -> bool {
131 gap <= self.0
132 }
133}
134
135#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
136#[serde(transparent)]
137pub struct ClickCount(u8);
138
139impl ClickCount {
140 pub const SINGLE: Self = Self(1);
141 pub const DOUBLE: Self = Self(2);
142
143 #[must_use]
144 pub const fn new(count: u8) -> Self {
145 Self(count)
146 }
147
148 #[must_use]
149 pub const fn get(self) -> u8 {
150 self.0
151 }
152
153 #[must_use]
154 pub const fn next(self) -> Self {
155 Self(self.0.saturating_add(1))
156 }
157
158 #[must_use]
159 pub const fn is_double(self) -> bool {
160 self.0 >= 2
161 }
162}
163
164#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
165pub struct PointerSample {
166 pub position: LayoutPos,
167}
168
169impl PointerSample {
170 #[must_use]
171 pub const fn new(position: LayoutPos) -> Self {
172 Self { position }
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use core::time::Duration;
179
180 use super::{
181 ClickCount, DoubleClickWindow, DragThreshold, FrameInstant, PointerButton,
182 PointerButtonMask,
183 };
184 use crate::layout::{LayoutOffset, LayoutPx};
185
186 #[test]
187 fn button_mask_with_just() {
188 let mask = PointerButtonMask::just(PointerButton::Primary).with(PointerButton::Middle);
189 assert!(mask.contains(PointerButton::Primary));
190 assert!(mask.contains(PointerButton::Middle));
191 assert!(!mask.contains(PointerButton::Secondary));
192 }
193
194 #[test]
195 fn drag_threshold_uses_squared_distance() {
196 let t = DragThreshold::new(LayoutPx::new(5.0));
197 let inside = LayoutOffset::new(LayoutPx::new(3.0), LayoutPx::new(3.0));
198 let outside = LayoutOffset::new(LayoutPx::new(4.0), LayoutPx::new(4.0));
199 assert!(!t.exceeded_by(inside));
200 assert!(t.exceeded_by(outside));
201 }
202
203 #[test]
204 fn double_click_window_contains_short_gap() {
205 let w = DoubleClickWindow::DEFAULT;
206 assert!(w.contains(Duration::from_millis(200)));
207 assert!(!w.contains(Duration::from_millis(800)));
208 }
209
210 #[test]
211 fn click_count_progresses() {
212 let one = ClickCount::SINGLE;
213 let two = one.next();
214 assert!(!one.is_double());
215 assert!(two.is_double());
216 }
217
218 #[test]
219 fn click_count_triple_remains_double_flag() {
220 let three = ClickCount::SINGLE.next().next();
221 assert_eq!(three.get(), 3);
222 assert!(three.is_double());
223 }
224
225 #[test]
226 fn frame_instant_since_clamps() {
227 let later = FrameInstant::from_duration(Duration::from_millis(100));
228 let earlier = FrameInstant::from_duration(Duration::from_millis(40));
229 assert_eq!(later.since(earlier), Duration::from_millis(60));
230 assert_eq!(earlier.since(later), Duration::ZERO);
231 }
232}