Skip to main content

repose_ui/
gestures.rs

1use repose_core::Vec2;
2
3use crate::input::*;
4use std::rc::Rc;
5use web_time::{Duration, Instant};
6
7pub struct GestureDetector {
8    on_tap: Option<Rc<dyn Fn(Vec2)>>,
9    on_double_tap: Option<Rc<dyn Fn(Vec2)>>,
10    on_long_press: Option<Rc<dyn Fn(Vec2)>>,
11    on_drag: Option<Rc<dyn Fn(DragEvent)>>,
12    on_swipe: Option<Rc<dyn Fn(SwipeDirection)>>,
13
14    // Internal state
15    last_tap: Option<Instant>,
16    press_start: Option<(Instant, Vec2)>,
17    drag_start: Option<Vec2>,
18    last_position: Option<Vec2>,
19    last_move_time: Option<Instant>,
20}
21
22pub struct DragEvent {
23    pub start: Vec2,
24    pub current: Vec2,
25    pub delta: Vec2,
26    pub velocity: Vec2,
27}
28
29pub enum SwipeDirection {
30    Up,
31    Down,
32    Left,
33    Right,
34}
35
36impl Default for GestureDetector {
37    fn default() -> Self {
38        Self::new()
39    }
40}
41
42impl GestureDetector {
43    pub fn new() -> Self {
44        Self {
45            on_tap: None,
46            on_double_tap: None,
47            on_long_press: None,
48            on_drag: None,
49            on_swipe: None,
50            last_tap: None,
51            press_start: None,
52            drag_start: None,
53            last_position: None,
54            last_move_time: None,
55        }
56    }
57
58    pub fn handle_pointer(&mut self, event: &PointerEvent) {
59        match event.event {
60            PointerEventKind::Down(_) => {
61                self.press_start = Some((Instant::now(), event.position));
62                self.drag_start = Some(event.position);
63                self.last_position = Some(event.position);
64                self.last_move_time = Some(Instant::now());
65
66                // Check for double tap
67                if let Some(last) = self.last_tap
68                    && (Instant::now() - last) < Duration::from_millis(300)
69                {
70                    if let Some(cb) = &self.on_double_tap {
71                        cb(event.position);
72                    }
73                    self.last_tap = None;
74                }
75            }
76            PointerEventKind::Up(_) => {
77                if let Some((start_time, start_pos)) = self.press_start {
78                    let elapsed = Instant::now() - start_time;
79                    let distance = ((event.position.x - start_pos.x).powi(2)
80                        + (event.position.y - start_pos.y).powi(2))
81                    .sqrt();
82
83                    if elapsed < Duration::from_millis(200) && distance < 10.0 {
84                        // Tap
85                        if let Some(cb) = &self.on_tap {
86                            cb(event.position);
87                        }
88                        self.last_tap = Some(Instant::now());
89                    } else if distance > 50.0 {
90                        // Swipe detection
91                        let dx = event.position.x - start_pos.x;
92                        let dy = event.position.y - start_pos.y;
93
94                        if let Some(cb) = &self.on_swipe {
95                            let dir = if dx.abs() > dy.abs() {
96                                if dx > 0.0 {
97                                    SwipeDirection::Right
98                                } else {
99                                    SwipeDirection::Left
100                                }
101                            } else if dy > 0.0 {
102                                SwipeDirection::Down
103                            } else {
104                                SwipeDirection::Up
105                            };
106                            cb(dir);
107                        }
108                    }
109                }
110                self.press_start = None;
111                self.drag_start = None;
112                self.last_position = None;
113                self.last_move_time = None;
114            }
115            PointerEventKind::Move => {
116                if let Some(start) = self.drag_start
117                    && let Some(cb) = &self.on_drag
118                {
119                    let delta = if let Some(prev) = self.last_position {
120                        Vec2 {
121                            x: event.position.x - prev.x,
122                            y: event.position.y - prev.y,
123                        }
124                    } else {
125                        Vec2::default()
126                    };
127
128                    let velocity = if let (Some(prev_time), Some(now)) =
129                        (self.last_move_time, Some(Instant::now()))
130                    {
131                        let dt = (now - prev_time).as_secs_f32().max(1.0 / 240.0);
132                        Vec2 {
133                            x: delta.x / dt,
134                            y: delta.y / dt,
135                        }
136                    } else {
137                        Vec2::default()
138                    };
139
140                    cb(DragEvent {
141                        start,
142                        current: event.position,
143                        delta,
144                        velocity,
145                    });
146                }
147
148                self.last_position = Some(event.position);
149                self.last_move_time = Some(Instant::now());
150
151                // Long press detection
152                if let Some((start_time, pos)) = self.press_start
153                    && (Instant::now() - start_time) > Duration::from_millis(500)
154                {
155                    if let Some(cb) = &self.on_long_press {
156                        cb(pos);
157                    }
158                    self.press_start = None; // Fire once
159                }
160            }
161            _ => {}
162        }
163    }
164}