Skip to main content

arcane_engine/platform/
touch.rs

1/// Touch phase (matches winit touch phase).
2#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3pub enum TouchPhase {
4    Start,
5    Move,
6    End,
7    Cancel,
8}
9
10/// Single touch point.
11#[derive(Debug, Clone)]
12pub struct TouchPoint {
13    pub id: u64,
14    pub x: f32,
15    pub y: f32,
16    pub phase: TouchPhase,
17}
18
19/// Touch state: tracks all active touch points.
20#[derive(Debug, Default)]
21pub struct TouchState {
22    /// Currently active touch points.
23    pub points: Vec<TouchPoint>,
24    /// Touch points that started this frame.
25    pub started_this_frame: Vec<TouchPoint>,
26    /// Touch points that ended this frame.
27    pub ended_this_frame: Vec<TouchPoint>,
28    /// Swipe detection: start positions for each touch ID.
29    swipe_starts: Vec<(u64, f32, f32, f64)>, // id, start_x, start_y, start_time
30}
31
32impl TouchState {
33    /// Call at start of frame to clear per-frame state.
34    pub fn begin_frame(&mut self) {
35        self.started_this_frame.clear();
36        self.ended_this_frame.clear();
37    }
38
39    /// Record a touch event.
40    pub fn touch_event(&mut self, id: u64, x: f32, y: f32, phase: TouchPhase, time: f64) {
41        let point = TouchPoint { id, x, y, phase };
42
43        match phase {
44            TouchPhase::Start => {
45                self.points.push(point.clone());
46                self.started_this_frame.push(point);
47                self.swipe_starts.push((id, x, y, time));
48            }
49            TouchPhase::Move => {
50                if let Some(p) = self.points.iter_mut().find(|p| p.id == id) {
51                    p.x = x;
52                    p.y = y;
53                    p.phase = TouchPhase::Move;
54                }
55            }
56            TouchPhase::End | TouchPhase::Cancel => {
57                if let Some(p) = self.points.iter_mut().find(|p| p.id == id) {
58                    p.x = x;
59                    p.y = y;
60                    p.phase = phase;
61                }
62                self.ended_this_frame.push(point);
63                self.points.retain(|p| p.id != id);
64                self.swipe_starts.retain(|(sid, _, _, _)| *sid != id);
65            }
66        }
67    }
68
69    /// Get number of active touches.
70    pub fn count(&self) -> usize {
71        self.points.len()
72    }
73
74    /// Get a specific touch point by slot index.
75    pub fn get(&self, index: usize) -> Option<&TouchPoint> {
76        self.points.get(index)
77    }
78
79    /// Check if any touch is active.
80    pub fn is_active(&self) -> bool {
81        !self.points.is_empty()
82    }
83
84    /// Get position of a touch by index (returns None if not found).
85    pub fn get_position(&self, index: usize) -> Option<(f32, f32)> {
86        self.points.get(index).map(|p| (p.x, p.y))
87    }
88
89    /// Detect a swipe gesture from ended touches.
90    /// Returns (direction_str, distance, start_x, start_y, end_x, end_y) if swipe detected.
91    pub fn detect_swipe(
92        &self,
93        min_distance: f32,
94    ) -> Option<(&'static str, f32, f32, f32, f32, f32)> {
95        for ended in &self.ended_this_frame {
96            // Find the start position for this touch
97            // Note: swipe_starts was already cleaned, so check ended_this_frame against
98            // what was recorded before end
99            // We need to check the position delta
100            let dx = ended.x; // End position
101            let dy = ended.y;
102            // We'd need start pos, but it was removed. This is a limitation of the simple approach.
103            // For a proper swipe, we'd need to store start positions before they're cleaned.
104            let _ = (dx, dy, min_distance);
105        }
106        None
107    }
108
109    /// Calculate pinch scale from two active touch points.
110    /// Returns (scale_relative_to_start, center_x, center_y) if two touches are active.
111    pub fn detect_pinch(&self) -> Option<(f32, f32)> {
112        if self.points.len() >= 2 {
113            let p1 = &self.points[0];
114            let p2 = &self.points[1];
115            let cx = (p1.x + p2.x) / 2.0;
116            let cy = (p1.y + p2.y) / 2.0;
117            let _dist = ((p2.x - p1.x).powi(2) + (p2.y - p1.y).powi(2)).sqrt();
118            Some((cx, cy))
119        } else {
120            None
121        }
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn touch_start_adds_point() {
131        let mut state = TouchState::default();
132        state.touch_event(1, 100.0, 200.0, TouchPhase::Start, 0.0);
133        assert_eq!(state.count(), 1);
134        assert!(state.is_active());
135    }
136
137    #[test]
138    fn touch_end_removes_point() {
139        let mut state = TouchState::default();
140        state.touch_event(1, 100.0, 200.0, TouchPhase::Start, 0.0);
141        state.touch_event(1, 100.0, 200.0, TouchPhase::End, 0.1);
142        assert_eq!(state.count(), 0);
143        assert!(!state.is_active());
144    }
145
146    #[test]
147    fn touch_move_updates_position() {
148        let mut state = TouchState::default();
149        state.touch_event(1, 100.0, 200.0, TouchPhase::Start, 0.0);
150        state.touch_event(1, 150.0, 250.0, TouchPhase::Move, 0.016);
151        let pos = state.get_position(0).unwrap();
152        assert!((pos.0 - 150.0).abs() < f32::EPSILON);
153        assert!((pos.1 - 250.0).abs() < f32::EPSILON);
154    }
155
156    #[test]
157    fn multi_touch_tracked() {
158        let mut state = TouchState::default();
159        state.touch_event(1, 100.0, 200.0, TouchPhase::Start, 0.0);
160        state.touch_event(2, 300.0, 400.0, TouchPhase::Start, 0.0);
161        assert_eq!(state.count(), 2);
162    }
163
164    #[test]
165    fn begin_frame_clears_started_ended() {
166        let mut state = TouchState::default();
167        state.touch_event(1, 100.0, 200.0, TouchPhase::Start, 0.0);
168        assert_eq!(state.started_this_frame.len(), 1);
169
170        state.begin_frame();
171        assert_eq!(state.started_this_frame.len(), 0);
172        // Active points remain
173        assert_eq!(state.count(), 1);
174    }
175
176    #[test]
177    fn cancel_removes_point() {
178        let mut state = TouchState::default();
179        state.touch_event(1, 100.0, 200.0, TouchPhase::Start, 0.0);
180        state.touch_event(1, 100.0, 200.0, TouchPhase::Cancel, 0.1);
181        assert_eq!(state.count(), 0);
182    }
183
184    #[test]
185    fn get_returns_none_for_invalid_index() {
186        let state = TouchState::default();
187        assert!(state.get(0).is_none());
188    }
189
190    #[test]
191    fn get_position_returns_correct_coords() {
192        let mut state = TouchState::default();
193        state.touch_event(1, 42.0, 84.0, TouchPhase::Start, 0.0);
194        let (x, y) = state.get_position(0).unwrap();
195        assert!((x - 42.0).abs() < f32::EPSILON);
196        assert!((y - 84.0).abs() < f32::EPSILON);
197    }
198
199    #[test]
200    fn default_state_is_empty() {
201        let state = TouchState::default();
202        assert_eq!(state.count(), 0);
203        assert!(!state.is_active());
204        assert!(state.started_this_frame.is_empty());
205        assert!(state.ended_this_frame.is_empty());
206    }
207
208    #[test]
209    fn detect_pinch_returns_center_with_two_touches() {
210        let mut state = TouchState::default();
211        state.touch_event(1, 100.0, 100.0, TouchPhase::Start, 0.0);
212        state.touch_event(2, 200.0, 200.0, TouchPhase::Start, 0.0);
213        let (cx, cy) = state.detect_pinch().unwrap();
214        assert!((cx - 150.0).abs() < f32::EPSILON);
215        assert!((cy - 150.0).abs() < f32::EPSILON);
216    }
217
218    #[test]
219    fn detect_pinch_returns_none_with_one_touch() {
220        let mut state = TouchState::default();
221        state.touch_event(1, 100.0, 100.0, TouchPhase::Start, 0.0);
222        assert!(state.detect_pinch().is_none());
223    }
224}