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