maia_wasm/pointer.rs
1//! Pointer device handling.
2//!
3//! This module implements handling of pointer devices, including mouse dragging
4//! and two-finger touchscreen gestures.
5
6use web_sys::PointerEvent;
7
8const NUM_POINTERS: usize = 2;
9
10/// Pointer tracker.
11///
12/// A pointer tracker receives [`PointerEvent`]'s from the web browser, maintains
13/// state information about the pointers that are active, and generates
14/// [`PointerGesture`]'s.
15pub struct PointerTracker {
16 slots: [Option<Pointer>; NUM_POINTERS],
17 new_series_id: u8,
18}
19
20struct Pointer {
21 event: PointerEvent,
22 series_id: u8,
23}
24
25/// Pointer gesture.
26///
27/// Pointer gestures are higher level descriptions of the actions performed by
28/// pointer devices. These are easier to interpret than the information in a
29/// [`PointerEvent`], so the applicaton can implement actions according to
30/// pointer gestures.
31pub enum PointerGesture {
32 /// A drag gesture.
33 ///
34 /// Drag gestures are performed by clicking and dragging with a mouse or by
35 /// dragging one finger on a touchscreen.
36 Drag {
37 /// Displacement in the X dimension (in pixels).
38 dx: i32,
39 /// Displacement in the Y dimension (in pixels).
40 dy: i32,
41 /// X position (in pixels) of the previous pointer location
42 x0: i32,
43 /// Y position (in pixels) of the previous pointer location
44 y0: i32,
45 /// ID for the series of gestures.
46 ///
47 /// Gestures that are generated as part of the same action have the same
48 /// series ID.
49 series_id: u8,
50 },
51 /// A pinch gesture.
52 ///
53 /// Pinch gestures are performed by touching two fingers on a touchscreen
54 /// and moving them closer together or futher apart.
55 Pinch {
56 /// Center of the pinch gesture.
57 ///
58 /// The pinch gesture center is the mean point between the locations of
59 /// the two fingers involved in the pinch.
60 center: (i32, i32),
61 /// Dilation factor.
62 ///
63 /// The dilation factor indicates the zoom factor that the relative
64 /// movement of the fingers has caused.
65 dilation: (f32, f32),
66 /// ID for the series of gestures.
67 ///
68 /// Gestures that are generated as part of the same action have the same
69 /// series ID.
70 series_id: u8,
71 },
72}
73
74impl PointerTracker {
75 /// Creates a new pointer tracker.
76 pub fn new() -> PointerTracker {
77 PointerTracker {
78 slots: Default::default(),
79 new_series_id: 0,
80 }
81 }
82
83 /// Handler for the pointer down event.
84 ///
85 /// This function should be used as the handler for pointer down events.
86 pub fn on_pointer_down(&mut self, event: PointerEvent) {
87 self.record_event(event);
88 }
89
90 #[allow(clippy::needless_return)]
91 fn record_event(&mut self, event: PointerEvent) {
92 let pointer_id = event.pointer_id();
93 // Search previous event with same pointer ID.
94 if let Some(slot) = self.slots.iter_mut().find_map(|x| {
95 x.as_mut().and_then(|x| {
96 if x.event.pointer_id() == pointer_id {
97 Some(x)
98 } else {
99 None
100 }
101 })
102 }) {
103 // Replace event with the new one.
104 slot.event = event;
105 return;
106 }
107 // Search for an empty slot.
108 if let Some(slot) = self.slots.iter_mut().find(|x| x.is_none()) {
109 // create new series of pointer
110 slot.replace(Pointer {
111 event,
112 series_id: self.new_series_id,
113 });
114 self.new_series_id = self.new_series_id.wrapping_add(1);
115 return;
116 }
117 // We found no empty slots, so we cannot handle this pointer (this
118 // typically should not happen).
119 }
120
121 /// Handler for the pointer up event.
122 ///
123 /// This function should be used as the handler for pointer up events.
124 #[allow(clippy::needless_return)]
125 pub fn on_pointer_up(&mut self, event: PointerEvent) {
126 let pointer_id = event.pointer_id();
127 // Search previous event with the same pointer (this typically should be
128 // found).
129 if let Some(slot) = self.slots.iter_mut().find(|x| {
130 x.as_ref()
131 .is_some_and(|x| x.event.pointer_id() == pointer_id)
132 }) {
133 // Remove event.
134 slot.take();
135 return;
136 }
137 // The pointer event was not found, so there is nothing to remove (this
138 // typically should not happen).
139 }
140
141 fn get_pointer(&self, pointer_id: i32) -> Option<&Pointer> {
142 self.slots.iter().find_map(|x| {
143 x.as_ref().and_then(|x| {
144 if x.event.pointer_id() == pointer_id {
145 Some(x)
146 } else {
147 None
148 }
149 })
150 })
151 }
152
153 /// Handler for the pointer move event.
154 ///
155 /// This functions should be used as the hanlder for pointer move events.
156 ///
157 /// If the event produces a corresponding pointer gesture, it is returned.
158 pub fn on_pointer_move(&mut self, event: PointerEvent) -> Option<PointerGesture> {
159 let ret = match self.num_active_pointers() {
160 1 => self
161 .get_pointer(event.pointer_id())
162 .map(|old_event| self.drag(&event, old_event)),
163 2 => self.pinch(&event),
164 _ => None,
165 };
166 if ret.is_some() {
167 self.record_event(event);
168 }
169 ret
170 }
171
172 /// Checks if there are any active pointers.
173 ///
174 /// This function returns `true` if there are any active pointers
175 /// currently. An active pointer is one for which a pointer down event has
176 /// been received, and the corresponding pointer up event has not been
177 /// received yet.
178 pub fn has_active_pointers(&self) -> bool {
179 self.slots.iter().any(|x| x.is_some())
180 }
181
182 fn num_active_pointers(&self) -> usize {
183 self.slots.iter().filter(|x| x.is_some()).count()
184 }
185
186 fn drag(&self, new: &PointerEvent, old: &Pointer) -> PointerGesture {
187 let x0 = old.event.client_x();
188 let y0 = old.event.client_y();
189 PointerGesture::Drag {
190 dx: new.client_x() - x0,
191 dy: new.client_y() - y0,
192 x0,
193 y0,
194 series_id: old.series_id,
195 }
196 }
197
198 fn pinch(&self, event: &PointerEvent) -> Option<PointerGesture> {
199 let pointer_id = event.pointer_id();
200 // This event might not be present in the slots. In that case the pinch
201 // is invalid.
202 let same = self.get_pointer(pointer_id)?;
203 // There must be another event in the slots, since this is only called
204 // when there are 2 events in the slots.
205 let other = self
206 .slots
207 .iter()
208 .find_map(|x| {
209 x.as_ref().and_then(|x| {
210 if x.event.pointer_id() != pointer_id {
211 Some(x)
212 } else {
213 None
214 }
215 })
216 })
217 .unwrap();
218 let same_x = same.event.client_x() as f32;
219 let same_y = same.event.client_y() as f32;
220 let other_x = other.event.client_x() as f32;
221 let other_y = other.event.client_y() as f32;
222 let new_x = event.client_x() as f32;
223 let new_y = event.client_y() as f32;
224 let min_dilation = 0.5;
225 let max_dilation = 2.0;
226 let min_distance = 10.0;
227 let dist_x = (same_x - other_x).abs();
228 let dist_y = (same_y - other_y).abs();
229 let dilation_x = if dist_x >= min_distance {
230 ((new_x - other_x) / (same_x - other_x))
231 .abs()
232 .clamp(min_dilation, max_dilation)
233 } else {
234 1.0
235 };
236 let dilation_y = if dist_y >= min_distance {
237 ((new_y - other_y) / (same_y - other_y))
238 .abs()
239 .clamp(min_dilation, max_dilation)
240 } else {
241 1.0
242 };
243 Some(PointerGesture::Pinch {
244 center: (other.event.client_x(), other.event.client_y()),
245 dilation: (dilation_x, dilation_y),
246 // to assign a consistent series ID regardless of which pointer
247 // generated the event, we take the minimum
248 series_id: same.series_id.min(other.series_id),
249 })
250 }
251}
252
253impl Default for PointerTracker {
254 fn default() -> PointerTracker {
255 PointerTracker::new()
256 }
257}