freya_components/
gesture_area.rs1use 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
16const DOUBLE_TAP_DISTANCE: f64 = 100.0;
18
19const DOUBLE_TAP_TIMEOUT: u128 = 300; const DOUBLE_TAP_MIN: u128 = 40; const MAX_EVENTS_QUEUE: usize = 20;
27
28#[derive(Debug, PartialEq, Eq)]
30pub enum Gesture {
31 TapUp,
32 TapDown,
33 DoubleTap,
34}
35
36#[derive(Props, Clone, PartialEq)]
38pub struct GestureAreaProps {
39 pub children: Element,
41 pub ongesture: EventHandler<Gesture>,
43}
44
45type EventsQueue = VecDeque<(Instant, TouchEvent)>;
46
47#[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 if touch_events.len() > MAX_EVENTS_QUEUE {
76 touch_events.pop_front();
77 }
78
79 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 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 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 if let Some(((ended_time, ended_event), (started_time, _))) =
115 last_ended_event.zip(last_started_event)
116 {
117 let is_ended_close = event
119 .get_screen_coordinates()
120 .distance_to(ended_event.get_screen_coordinates())
121 < DOUBLE_TAP_DISTANCE;
122 let is_ended_mature =
124 ended_time.elapsed().as_millis() >= DOUBLE_TAP_MIN;
125
126 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 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 #[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 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 #[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 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}