Skip to main content

freya_components/
gesture_area.rs

1use std::{
2    collections::VecDeque,
3    time::Instant,
4};
5
6use dioxus::prelude::*;
7use freya_elements::{
8    self as dioxus_elements,
9    events::{
10        touch::TouchPhase,
11        TouchEvent,
12    },
13};
14use futures_util::StreamExt;
15
16/// Distance between the first tap and the second tap in [`Gesture::DoubleTap`] gesture.
17const DOUBLE_TAP_DISTANCE: f64 = 100.0;
18
19/// Maximum time between the start of the first tap and the start of the second tap in a [`Gesture::DoubleTap`] gesture.
20const DOUBLE_TAP_TIMEOUT: u128 = 300; // 300ms
21
22/// Minimum time between the end of the first time to the start of the second tap in a [`Gesture::DoubleTap`] gesture.
23const DOUBLE_TAP_MIN: u128 = 40; // 40ms
24
25/// In-memory events queue maximum size.
26const MAX_EVENTS_QUEUE: usize = 20;
27
28/// Gesture emitted by the [`GestureArea`] component.
29#[derive(Debug, PartialEq, Eq)]
30pub enum Gesture {
31    TapUp,
32    TapDown,
33    DoubleTap,
34}
35
36/// Properties for the [`GestureArea`] component.
37#[derive(Props, Clone, PartialEq)]
38pub struct GestureAreaProps {
39    /// Inner children for the GestureArea.
40    pub children: Element,
41    /// Handler for the `ongesture` event.
42    pub ongesture: EventHandler<Gesture>,
43}
44
45type EventsQueue = VecDeque<(Instant, TouchEvent)>;
46
47/// Detect complex touch gestures such as [`Gesture::DoubleTap`].
48///
49/// # Example
50///
51/// ```rust,no_run
52/// # use freya::prelude::*;
53/// fn app() -> Element {
54///    let mut gesture = use_signal(|| "Tap here".to_string());
55///    rsx!(
56///        GestureArea {
57///            ongesture: move |g| gesture.set(format!("{g:?}")),
58///            label {
59///                "{gesture}"
60///            }
61///        }
62///    )
63/// }
64/// ```
65#[allow(non_snake_case)]
66pub fn GestureArea(props: GestureAreaProps) -> Element {
67    let event_emitter = use_coroutine(
68        move |mut rx: UnboundedReceiver<(Instant, TouchEvent)>| async move {
69            let mut touch_events = VecDeque::<(Instant, TouchEvent)>::new();
70
71            while let Some(new_event) = rx.next().await {
72                touch_events.push_back(new_event);
73
74                // Keep the touch events queue under a certain size
75                if touch_events.len() > MAX_EVENTS_QUEUE {
76                    touch_events.pop_front();
77                }
78
79                // Find the first event with the `target_phase` that happened before the `start_time`
80                let find_previous_event = |start_time: &Instant,
81                                           events: &EventsQueue,
82                                           target_phase: TouchPhase|
83                 -> Option<(Instant, TouchEvent)> {
84                    let mut start = false;
85                    for (time, event) in events.iter().rev() {
86                        if time == start_time {
87                            start = true;
88                            continue;
89                        }
90                        if event.phase == target_phase && start {
91                            return Some((*time, event.clone()));
92                        }
93                    }
94                    None
95                };
96
97                // Process the most recent event
98                let event = touch_events.iter().last();
99
100                if let Some((time, event)) = event {
101                    let phase = event.get_touch_phase();
102
103                    match phase {
104                        TouchPhase::Started => {
105                            // TapDown
106                            props.ongesture.call(Gesture::TapDown);
107
108                            let last_ended_event =
109                                find_previous_event(time, &touch_events, TouchPhase::Ended);
110                            let last_started_event =
111                                find_previous_event(time, &touch_events, TouchPhase::Started);
112
113                            // DoubleTap
114                            if let Some(((ended_time, ended_event), (started_time, _))) =
115                                last_ended_event.zip(last_started_event)
116                            {
117                                // Has the latest `touchend` event went too far?
118                                let is_ended_close = event
119                                    .get_screen_coordinates()
120                                    .distance_to(ended_event.get_screen_coordinates())
121                                    < DOUBLE_TAP_DISTANCE;
122                                // Is the latest `touchend` mature enough?
123                                let is_ended_mature =
124                                    ended_time.elapsed().as_millis() >= DOUBLE_TAP_MIN;
125
126                                // Hast the latest `touchstart` event expired?
127                                let is_started_recent =
128                                    started_time.elapsed().as_millis() <= DOUBLE_TAP_TIMEOUT;
129
130                                if is_ended_close && is_ended_mature && is_started_recent {
131                                    props.ongesture.call(Gesture::DoubleTap);
132                                }
133                            }
134                        }
135                        TouchPhase::Ended => {
136                            // TapUp
137                            props.ongesture.call(Gesture::TapUp);
138                        }
139                        _ => {}
140                    }
141                }
142            }
143        },
144    );
145
146    let ontouchcancel = move |e: TouchEvent| {
147        event_emitter.send((Instant::now(), e));
148    };
149
150    let ontouchend = move |e: TouchEvent| {
151        event_emitter.send((Instant::now(), e));
152    };
153
154    let ontouchmove = move |e: TouchEvent| {
155        event_emitter.send((Instant::now(), e));
156    };
157
158    let ontouchstart = move |e: TouchEvent| {
159        event_emitter.send((Instant::now(), e));
160    };
161
162    rsx!(
163        rect {
164            ontouchcancel: ontouchcancel,
165            ontouchend: ontouchend,
166            ontouchmove: ontouchmove,
167            ontouchstart: ontouchstart,
168            {props.children}
169        }
170    )
171}
172
173#[cfg(test)]
174mod test {
175    use std::time::Duration;
176
177    use freya::prelude::*;
178    use freya_testing::prelude::*;
179    use tokio::time::sleep;
180
181    use crate::gesture_area::DOUBLE_TAP_MIN;
182
183    /// This test simulates a `DoubleTap` gesture in this order:
184    /// 1. Touch start
185    /// 2. Touch end
186    /// 3. Wait 40ms
187    /// 4. Touch start
188    #[tokio::test]
189    pub async fn double_tap() {
190        fn dobule_tap_app() -> Element {
191            let mut value = use_signal(|| "EMPTY".to_string());
192
193            let ongesture = move |e: Gesture| {
194                value.set(format!("{e:?}"));
195            };
196
197            rsx!(
198                GestureArea {
199                    ongesture,
200                    rect {
201                        width: "100%",
202                        height: "100%",
203
204                    }
205                }
206                label {
207                    "{value}"
208                }
209            )
210        }
211
212        let mut utils = launch_test(dobule_tap_app);
213
214        // Initial state
215        utils.wait_for_update().await;
216
217        assert_eq!(utils.root().get(1).get(0).text(), Some("EMPTY"));
218
219        utils.push_event(TestEvent::Touch {
220            name: EventName::TouchStart,
221            location: (1.0, 1.0).into(),
222            phase: TouchPhase::Started,
223            finger_id: 0,
224            force: None,
225        });
226
227        utils.push_event(TestEvent::Touch {
228            name: EventName::TouchEnd,
229            location: (1.0, 1.0).into(),
230            phase: TouchPhase::Ended,
231            finger_id: 0,
232            force: None,
233        });
234
235        utils.wait_for_update().await;
236        utils.wait_for_update().await;
237
238        sleep(Duration::from_millis(DOUBLE_TAP_MIN as u64)).await;
239
240        utils.push_event(TestEvent::Touch {
241            name: EventName::TouchStart,
242            location: (1.0, 1.0).into(),
243            phase: TouchPhase::Started,
244            finger_id: 0,
245            force: None,
246        });
247
248        utils.wait_for_update().await;
249        utils.wait_for_update().await;
250
251        assert_eq!(utils.root().get(1).get(0).text(), Some("DoubleTap"));
252    }
253
254    /// Simulates `TapUp` and `TapDown` gestures.
255    #[tokio::test]
256    pub async fn tap_up_down() {
257        fn tap_up_down_app() -> Element {
258            let mut value = use_signal(|| "EMPTY".to_string());
259
260            let ongesture = move |e: Gesture| {
261                value.set(format!("{e:?}"));
262            };
263
264            rsx!(
265                GestureArea {
266                    ongesture,
267                    rect {
268                        width: "100%",
269                        height: "100%",
270
271                    }
272                }
273                label {
274                    "{value}"
275                }
276            )
277        }
278
279        let mut utils = launch_test(tap_up_down_app);
280
281        // Initial state
282        utils.wait_for_update().await;
283
284        assert_eq!(utils.root().get(1).get(0).text(), Some("EMPTY"));
285
286        utils.push_event(TestEvent::Touch {
287            name: EventName::TouchStart,
288            location: (1.0, 1.0).into(),
289            phase: TouchPhase::Started,
290            finger_id: 0,
291            force: None,
292        });
293
294        utils.wait_for_update().await;
295        utils.wait_for_update().await;
296
297        assert_eq!(utils.root().get(1).get(0).text(), Some("TapDown"));
298
299        utils.push_event(TestEvent::Touch {
300            name: EventName::TouchEnd,
301            location: (1.0, 1.0).into(),
302            phase: TouchPhase::Ended,
303            finger_id: 0,
304            force: None,
305        });
306
307        utils.wait_for_update().await;
308        utils.wait_for_update().await;
309
310        assert_eq!(utils.root().get(1).get(0).text(), Some("TapUp"));
311    }
312}