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
57#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
58#[serde(transparent)]
59pub struct FrameInstant(Duration);
60
61impl 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)]
82pub struct DragThreshold(LayoutPx);
83
84impl 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)]
109pub struct DoubleClickWindow(Duration);
110
111impl 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)]
132pub struct ClickCount(u8);
133
134impl 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)]
160pub struct PointerSample {
161 pub position: LayoutPos,
162}
163
164impl PointerSample {
165 #[must_use]
166 pub const fn new(position: LayoutPos) -> Self {
167 Self { position }
168 }
169}
170
171#[cfg(test)]
172mod 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}