Skip to main content

uzor_core/input/
touch.rs

1//! Multi-touch gesture state and recognition
2//!
3//! This module provides touch tracking and gesture recognition for platforms
4//! that support multi-touch input (tablets, touchscreens, trackpads).
5
6use std::collections::HashMap;
7
8/// Unique identifier for a touch point
9pub type TouchId = u64;
10
11/// Single touch point state
12#[derive(Clone, Copy, Debug)]
13pub struct Touch {
14    /// Unique identifier for this touch
15    pub id: TouchId,
16
17    /// Current position in screen coordinates
18    pub pos: (f64, f64),
19
20    /// Previous position (for calculating delta)
21    pub prev_pos: (f64, f64),
22
23    /// Force/pressure (0.0 to 1.0, optional depending on hardware)
24    pub force: Option<f64>,
25
26    /// Time when this touch started (in seconds)
27    pub start_time: f64,
28}
29
30impl Touch {
31    /// Create a new touch point
32    pub fn new(id: TouchId, x: f64, y: f64, time: f64) -> Self {
33        Self {
34            id,
35            pos: (x, y),
36            prev_pos: (x, y),
37            force: None,
38            start_time: time,
39        }
40    }
41
42    /// Update position
43    pub fn update_pos(&mut self, x: f64, y: f64) {
44        self.prev_pos = self.pos;
45        self.pos = (x, y);
46    }
47
48    /// Get movement delta since last frame
49    pub fn delta(&self) -> (f64, f64) {
50        (self.pos.0 - self.prev_pos.0, self.pos.1 - self.prev_pos.1)
51    }
52
53    /// Get distance from another touch point
54    pub fn distance_to(&self, other: &Touch) -> f64 {
55        let dx = self.pos.0 - other.pos.0;
56        let dy = self.pos.1 - other.pos.1;
57        (dx * dx + dy * dy).sqrt()
58    }
59
60    /// Get angle to another touch point (in radians)
61    pub fn angle_to(&self, other: &Touch) -> f64 {
62        let dx = other.pos.0 - self.pos.0;
63        let dy = other.pos.1 - self.pos.1;
64        dy.atan2(dx)
65    }
66}
67
68/// Multi-touch state tracker
69#[derive(Clone, Debug, Default)]
70pub struct TouchState {
71    /// Active touch points by ID
72    touches: HashMap<TouchId, Touch>,
73
74    /// Previous touch count (for detecting touch count changes)
75    prev_touch_count: usize,
76
77    /// Pinch gesture: distance delta between two touches
78    pub pinch_delta: Option<f64>,
79
80    /// Previous distance for pinch calculation
81    prev_pinch_distance: Option<f64>,
82
83    /// Rotation gesture: angle delta between two touches (radians)
84    pub rotation_delta: Option<f64>,
85
86    /// Previous angle for rotation calculation
87    prev_rotation_angle: Option<f64>,
88
89    /// Two-finger pan: average movement of two touches
90    pub pan_delta: Option<(f64, f64)>,
91
92    /// Primary touch ID (first touch that went down)
93    primary_id: Option<TouchId>,
94}
95
96impl TouchState {
97    /// Create new empty touch state
98    pub fn new() -> Self {
99        Self::default()
100    }
101
102    /// Add or update a touch point
103    pub fn update_touch(&mut self, id: TouchId, x: f64, y: f64, time: f64, force: Option<f64>) {
104        if let Some(touch) = self.touches.get_mut(&id) {
105            touch.update_pos(x, y);
106            touch.force = force;
107        } else {
108            let mut touch = Touch::new(id, x, y, time);
109            touch.force = force;
110            self.touches.insert(id, touch);
111
112            // Set as primary if this is the first touch
113            if self.primary_id.is_none() {
114                self.primary_id = Some(id);
115            }
116        }
117
118        self.update_gestures();
119    }
120
121    /// Start a new touch point (convenience wrapper)
122    pub fn touch_start(&mut self, id: TouchId, x: f64, y: f64, time: f64, force: Option<f64>) {
123        self.update_touch(id, x, y, time, force);
124    }
125
126    /// Move an existing touch point (convenience wrapper)
127    pub fn touch_move(&mut self, id: TouchId, x: f64, y: f64, time: f64, force: Option<f64>) {
128        self.update_touch(id, x, y, time, force);
129    }
130
131    /// End a touch point (convenience wrapper)
132    pub fn touch_end(&mut self, id: TouchId) {
133        self.remove_touch(id);
134    }
135
136    /// Cancel a touch point (convenience wrapper for touch_end)
137    pub fn touch_cancel(&mut self, id: TouchId) {
138        self.remove_touch(id);
139    }
140
141    /// Remove a touch point
142    pub fn remove_touch(&mut self, id: TouchId) {
143        self.touches.remove(&id);
144
145        // Clear primary if it was removed
146        if self.primary_id == Some(id) {
147            self.primary_id = self.touches.keys().next().copied();
148        }
149
150        self.update_gestures();
151    }
152
153    /// Clear all touches
154    pub fn clear(&mut self) {
155        self.touches.clear();
156        self.primary_id = None;
157        self.clear_deltas();
158    }
159
160    /// Clear frame-specific gesture deltas
161    pub fn clear_deltas(&mut self) {
162        self.pinch_delta = None;
163        self.rotation_delta = None;
164        self.pan_delta = None;
165        self.prev_touch_count = self.touches.len();
166    }
167
168    /// Update gesture calculations based on current touches
169    fn update_gestures(&mut self) {
170        let touch_count = self.touches.len();
171
172        // Only process gestures when we have exactly 2 touches
173        if touch_count == 2 {
174            let touches: Vec<&Touch> = self.touches.values().collect();
175            let t1 = touches[0];
176            let t2 = touches[1];
177
178            // Calculate pinch (distance change)
179            let current_distance = t1.distance_to(t2);
180            if let Some(prev_distance) = self.prev_pinch_distance {
181                self.pinch_delta = Some(current_distance - prev_distance);
182            }
183            self.prev_pinch_distance = Some(current_distance);
184
185            // Calculate rotation (angle change)
186            let current_angle = t1.angle_to(t2);
187            if let Some(prev_angle) = self.prev_rotation_angle {
188                let mut angle_delta = current_angle - prev_angle;
189
190                // Normalize angle delta to [-π, π]
191                while angle_delta > std::f64::consts::PI {
192                    angle_delta -= 2.0 * std::f64::consts::PI;
193                }
194                while angle_delta < -std::f64::consts::PI {
195                    angle_delta += 2.0 * std::f64::consts::PI;
196                }
197
198                self.rotation_delta = Some(angle_delta);
199            }
200            self.prev_rotation_angle = Some(current_angle);
201
202            // Calculate pan (average movement)
203            let delta1 = t1.delta();
204            let delta2 = t2.delta();
205            let avg_delta = (
206                (delta1.0 + delta2.0) / 2.0,
207                (delta1.1 + delta2.1) / 2.0,
208            );
209
210            // Only set pan if there's significant movement
211            if avg_delta.0.abs() > 0.1 || avg_delta.1.abs() > 0.1 {
212                self.pan_delta = Some(avg_delta);
213            }
214        } else {
215            // Not exactly 2 touches - clear multi-touch gestures
216            if self.prev_touch_count == 2 {
217                // Just transitioned away from 2 touches
218                self.prev_pinch_distance = None;
219                self.prev_rotation_angle = None;
220            }
221        }
222    }
223
224    /// Get the primary touch (first touch that went down)
225    pub fn primary_touch(&self) -> Option<&Touch> {
226        self.primary_id.and_then(|id| self.touches.get(&id))
227    }
228
229    /// Get touch by ID
230    pub fn get_touch(&self, id: TouchId) -> Option<&Touch> {
231        self.touches.get(&id)
232    }
233
234    /// Get all active touches
235    pub fn touches(&self) -> impl Iterator<Item = &Touch> {
236        self.touches.values()
237    }
238
239    /// Get number of active touches
240    pub fn touch_count(&self) -> usize {
241        self.touches.len()
242    }
243
244    /// Check if any touches are active
245    pub fn has_touches(&self) -> bool {
246        !self.touches.is_empty()
247    }
248
249    /// Get centroid (average position) of all touches
250    pub fn centroid(&self) -> Option<(f64, f64)> {
251        if self.touches.is_empty() {
252            return None;
253        }
254
255        let count = self.touches.len() as f64;
256        let sum = self.touches.values().fold((0.0, 0.0), |acc, touch| {
257            (acc.0 + touch.pos.0, acc.1 + touch.pos.1)
258        });
259
260        Some((sum.0 / count, sum.1 / count))
261    }
262
263    /// Check if this is a two-finger gesture
264    pub fn is_two_finger_gesture(&self) -> bool {
265        self.touches.len() == 2
266    }
267
268    /// Check if currently pinching
269    pub fn is_pinching(&self) -> bool {
270        self.pinch_delta.is_some() && self.pinch_delta.unwrap().abs() > 0.1
271    }
272
273    /// Check if currently rotating
274    pub fn is_rotating(&self) -> bool {
275        self.rotation_delta.is_some() && self.rotation_delta.unwrap().abs() > 0.01
276    }
277
278    /// Check if currently panning with two fingers
279    pub fn is_two_finger_panning(&self) -> bool {
280        self.pan_delta.is_some()
281    }
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287
288    #[test]
289    fn test_touch_creation() {
290        let touch = Touch::new(1, 100.0, 200.0, 0.0);
291        assert_eq!(touch.id, 1);
292        assert_eq!(touch.pos, (100.0, 200.0));
293        assert_eq!(touch.prev_pos, (100.0, 200.0));
294        assert_eq!(touch.delta(), (0.0, 0.0));
295    }
296
297    #[test]
298    fn test_touch_update() {
299        let mut touch = Touch::new(1, 100.0, 200.0, 0.0);
300        touch.update_pos(150.0, 250.0);
301
302        assert_eq!(touch.pos, (150.0, 250.0));
303        assert_eq!(touch.prev_pos, (100.0, 200.0));
304        assert_eq!(touch.delta(), (50.0, 50.0));
305    }
306
307    #[test]
308    fn test_touch_distance() {
309        let t1 = Touch::new(1, 0.0, 0.0, 0.0);
310        let t2 = Touch::new(2, 3.0, 4.0, 0.0);
311
312        assert_eq!(t1.distance_to(&t2), 5.0); // 3-4-5 triangle
313    }
314
315    #[test]
316    fn test_touch_angle() {
317        let t1 = Touch::new(1, 0.0, 0.0, 0.0);
318        let t2 = Touch::new(2, 1.0, 0.0, 0.0);
319
320        assert_eq!(t1.angle_to(&t2), 0.0); // Pointing right
321
322        let t3 = Touch::new(3, 0.0, 1.0, 0.0);
323        assert!((t1.angle_to(&t3) - std::f64::consts::PI / 2.0).abs() < 0.001); // Pointing up
324    }
325
326    #[test]
327    fn test_touch_state_add_remove() {
328        let mut state = TouchState::new();
329        assert_eq!(state.touch_count(), 0);
330        assert!(!state.has_touches());
331
332        state.update_touch(1, 100.0, 200.0, 0.0, None);
333        assert_eq!(state.touch_count(), 1);
334        assert!(state.has_touches());
335        assert_eq!(state.primary_touch().unwrap().id, 1);
336
337        state.update_touch(2, 300.0, 400.0, 0.0, None);
338        assert_eq!(state.touch_count(), 2);
339
340        state.remove_touch(1);
341        assert_eq!(state.touch_count(), 1);
342        assert_eq!(state.primary_touch().unwrap().id, 2); // Primary switches to remaining touch
343
344        state.clear();
345        assert_eq!(state.touch_count(), 0);
346        assert!(!state.has_touches());
347    }
348
349    #[test]
350    fn test_centroid() {
351        let mut state = TouchState::new();
352
353        state.update_touch(1, 0.0, 0.0, 0.0, None);
354        assert_eq!(state.centroid(), Some((0.0, 0.0)));
355
356        state.update_touch(2, 100.0, 100.0, 0.0, None);
357        assert_eq!(state.centroid(), Some((50.0, 50.0)));
358
359        state.update_touch(3, 200.0, 200.0, 0.0, None);
360        assert_eq!(state.centroid(), Some((100.0, 100.0)));
361    }
362
363    #[test]
364    fn test_pinch_gesture() {
365        let mut state = TouchState::new();
366
367        // Start with two touches 100 units apart
368        state.update_touch(1, 0.0, 0.0, 0.0, None);
369        state.update_touch(2, 100.0, 0.0, 0.0, None);
370        assert!(state.is_two_finger_gesture());
371
372        // Initial pinch_delta is None (no previous distance)
373        assert!(state.pinch_delta.is_none());
374
375        state.clear_deltas();
376
377        // Move touches closer (pinch in)
378        state.update_touch(1, 10.0, 0.0, 0.1, None);
379        state.update_touch(2, 90.0, 0.0, 0.1, None);
380
381        // Should detect pinch
382        assert!(state.pinch_delta.is_some());
383        let delta = state.pinch_delta.unwrap();
384        assert!(delta < 0.0); // Distance decreased
385    }
386
387    #[test]
388    fn test_rotation_gesture() {
389        let mut state = TouchState::new();
390
391        // Start horizontal
392        state.update_touch(1, 0.0, 0.0, 0.0, None);
393        state.update_touch(2, 100.0, 0.0, 0.0, None);
394
395        state.clear_deltas();
396
397        // Rotate to vertical (90 degrees)
398        state.update_touch(1, 0.0, 0.0, 0.1, None);
399        state.update_touch(2, 0.0, 100.0, 0.1, None);
400
401        assert!(state.rotation_delta.is_some());
402        let rotation = state.rotation_delta.unwrap();
403
404        // Should be approximately π/2 radians (90 degrees)
405        assert!((rotation - std::f64::consts::PI / 2.0).abs() < 0.1);
406    }
407
408    #[test]
409    fn test_pan_gesture() {
410        let mut state = TouchState::new();
411
412        state.update_touch(1, 0.0, 0.0, 0.0, None);
413        state.update_touch(2, 100.0, 0.0, 0.0, None);
414
415        state.clear_deltas();
416
417        // Move both touches right by 10
418        state.update_touch(1, 10.0, 0.0, 0.1, None);
419        state.update_touch(2, 110.0, 0.0, 0.1, None);
420
421        assert!(state.pan_delta.is_some());
422        let (dx, dy) = state.pan_delta.unwrap();
423        assert!((dx - 10.0).abs() < 0.1);
424        assert!(dy.abs() < 0.1);
425    }
426
427    #[test]
428    fn test_clear_deltas() {
429        let mut state = TouchState::new();
430
431        state.update_touch(1, 0.0, 0.0, 0.0, None);
432        state.update_touch(2, 100.0, 0.0, 0.0, None);
433
434        // Generate some deltas
435        state.update_touch(1, 10.0, 0.0, 0.1, None);
436        state.update_touch(2, 90.0, 0.0, 0.1, None);
437
438        // Clear deltas
439        state.clear_deltas();
440
441        assert!(state.pinch_delta.is_none());
442        assert!(state.rotation_delta.is_none());
443        assert!(state.pan_delta.is_none());
444    }
445}