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}