1use frus_core::Point;
4
5use crate::widget::Widget;
6
7pub struct GestureDetector<Msg = crate::callback::Callback> {
35 inner: Box<dyn Widget<Msg>>,
36 tap: Option<Message<Msg>>,
37 double_tap: Option<Message<Msg>>,
38 long_press: Option<Message<Msg>>,
39 secondary_tap: Option<Message<Msg>>,
40 pan_axis: PanAxis,
41 pan_start: Option<Box<dyn Fn(Point) -> Msg>>,
42 pan_update: Option<Box<dyn Fn(Point, Point) -> Msg>>,
43 pan_end: Option<Box<dyn Fn(Point) -> Msg>>,
44}
45
46type Message<Msg> = Box<dyn Fn() -> Msg>;
48
49#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
54pub enum PanAxis {
55 #[default]
57 Free,
58 Horizontal,
61 Vertical,
63}
64
65#[derive(Clone, Copy, Debug, PartialEq)]
68pub enum PanEvent {
69 Start {
72 local: Point,
74 },
75 Update {
78 local: Point,
80 delta: Point,
82 },
83 End {
85 velocity: Point,
87 },
88}
89
90impl<Msg> GestureDetector<Msg> {
91 pub fn new(child: impl Widget<Msg> + 'static) -> Self {
93 Self {
94 inner: Box::new(child),
95 tap: None,
96 double_tap: None,
97 long_press: None,
98 secondary_tap: None,
99 pan_axis: PanAxis::Free,
100 pan_start: None,
101 pan_update: None,
102 pan_end: None,
103 }
104 }
105
106 fn restyle(&self, base: frus_layout::Style) -> frus_layout::Style {
108 base
109 }
110}
111
112fn keep<Msg: Clone + 'static>(message: impl Into<Msg>) -> Option<Message<Msg>> {
114 let message = message.into();
115 Some(Box::new(move || message.clone()))
116}
117
118impl<Msg: Clone + 'static> GestureDetector<Msg> {
119 pub fn on_tap(mut self, message: impl Into<Msg>) -> Self {
122 self.tap = keep(message);
123 self
124 }
125
126 pub fn on_double_tap(mut self, message: impl Into<Msg>) -> Self {
128 self.double_tap = keep(message);
129 self
130 }
131
132 pub fn on_long_press(mut self, message: impl Into<Msg>) -> Self {
135 self.long_press = keep(message);
136 self
137 }
138
139 pub fn on_secondary_tap(mut self, message: impl Into<Msg>) -> Self {
142 self.secondary_tap = keep(message);
143 self
144 }
145
146 pub fn on_pan_start(mut self, build: impl Fn(Point) -> Msg + 'static) -> Self {
149 self.pan_start = Some(Box::new(build));
150 self
151 }
152
153 pub fn on_pan_update(mut self, build: impl Fn(Point, Point) -> Msg + 'static) -> Self {
156 self.pan_update = Some(Box::new(build));
157 self
158 }
159
160 pub fn on_pan_end(mut self, build: impl Fn(Point) -> Msg + 'static) -> Self {
163 self.pan_end = Some(Box::new(build));
164 self
165 }
166
167 pub fn pan_axis(mut self, axis: PanAxis) -> Self {
171 self.pan_axis = axis;
172 self
173 }
174}
175
176crate::transparent::forward_transparent!(gestures GestureDetector {
177 fn on_click(&self) -> Option<Msg> {
179 self.inner
180 .on_click()
181 .or_else(|| self.tap.as_ref().map(|message| message()))
182 }
183
184 fn on_long_press(&self) -> Option<Msg> {
185 self.inner
186 .on_long_press()
187 .or_else(|| self.long_press.as_ref().map(|message| message()))
188 }
189
190 fn on_double_tap(&self) -> Option<Msg> {
191 self.inner
192 .on_double_tap()
193 .or_else(|| self.double_tap.as_ref().map(|message| message()))
194 }
195
196 fn on_secondary_tap(&self) -> Option<Msg> {
197 self.inner
198 .on_secondary_tap()
199 .or_else(|| self.secondary_tap.as_ref().map(|message| message()))
200 }
201
202 fn pan_axis(&self) -> Option<PanAxis> {
204 self.inner.pan_axis().or_else(|| {
205 (self.pan_start.is_some() || self.pan_update.is_some() || self.pan_end.is_some())
206 .then_some(self.pan_axis)
207 })
208 }
209
210 fn on_pan(&self, event: PanEvent) -> Option<Msg> {
211 if self.inner.pan_axis().is_some() {
212 return self.inner.on_pan(event);
213 }
214 match event {
215 PanEvent::Start { local } => self.pan_start.as_ref().map(|build| build(local)),
216 PanEvent::Update { local, delta } => {
217 self.pan_update.as_ref().map(|build| build(local, delta))
218 }
219 PanEvent::End { velocity } => self.pan_end.as_ref().map(|build| build(velocity)),
220 }
221 }
222
223 fn key(&self) -> Option<u64> {
226 self.inner.key()
227 }
228 fn positioned(&self) -> Option<crate::positioned::Positioning> {
229 self.inner.positioned()
230 }
231 fn theme_override(
232 &self,
233 inherited: &crate::theme::Theme,
234 ) -> Option<Box<crate::theme::Theme>> {
235 self.inner.theme_override(inherited)
236 }
237 fn media_override(&self, inherited: crate::MediaQuery) -> Option<crate::MediaQuery> {
238 self.inner.media_override(inherited)
239 }
240 fn scaffold_override(&self) -> Option<crate::ScaffoldInfo> {
241 self.inner.scaffold_override()
242 }
243 fn scroll_behavior_override(&self) -> Option<crate::ScrollBehavior> {
244 self.inner.scroll_behavior_override()
245 }
246 fn autofill_group(&self) -> bool {
247 self.inner.autofill_group()
248 }
249 fn area_toolbar(
250 &self,
251 context: crate::ToolbarContext,
252 ) -> Option<Option<&dyn crate::widget::Widget<Msg>>> {
253 self.inner.area_toolbar(context)
254 }
255});
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260 use crate::{build_ui, Container, Flex, Keyed, Runtime, Theme};
261 use frus_core::{Point, Size};
262
263 #[test]
265 fn a_wrapped_detector_still_answers() {
266 let detector = GestureDetector::<u8>::new(Container::new())
267 .on_tap(1)
268 .on_double_tap(2)
269 .on_long_press(3)
270 .on_secondary_tap(4);
271 let wrapped = Keyed::new(9u64, detector);
272 assert_eq!(wrapped.on_click(), Some(1));
273 assert_eq!(wrapped.on_double_tap(), Some(2));
274 assert_eq!(wrapped.on_long_press(), Some(3));
275 assert_eq!(wrapped.on_secondary_tap(), Some(4));
276 }
277
278 #[test]
281 fn a_detector_without_a_tap_is_still_a_target() {
282 let size = Size::new(100.0, 100.0);
283 let rt = Runtime::default();
284 let theme = Theme::default();
285 let block = || Container::<u8>::new().width(100.0).height(100.0);
286 let double = GestureDetector::new(block()).on_double_tap(2);
287 let ui = build_ui(&double, size, &rt, &theme);
288 assert!(ui.hit(Point::new(50.0, 50.0)).is_some());
289 let secondary = GestureDetector::new(block()).on_secondary_tap(4);
290 let ui = build_ui(&secondary, size, &rt, &theme);
291 assert!(ui.hit(Point::new(50.0, 50.0)).is_some());
292 let nothing = GestureDetector::new(block());
293 let ui = build_ui(¬hing, size, &rt, &theme);
294 assert!(ui.hit(Point::new(50.0, 50.0)).is_none());
295 }
296
297 #[test]
300 fn a_drag_is_answered_through_a_wrapper() {
301 let plain = GestureDetector::<u8>::new(Container::new()).on_tap(1);
302 assert_eq!(Widget::pan_axis(&plain), None, "a tap is not a drag");
303 let detector = GestureDetector::<f32>::new(Container::new())
304 .on_pan_start(|at| at.x)
305 .on_pan_update(|_, delta| delta.y)
306 .on_pan_end(|velocity| velocity.x + 1000.0)
307 .pan_axis(PanAxis::Vertical);
308 let wrapped = Keyed::new(3u64, detector);
309 assert_eq!(wrapped.pan_axis(), Some(PanAxis::Vertical));
310 let at = Point::new(7.0, 9.0);
311 assert_eq!(wrapped.on_pan(PanEvent::Start { local: at }), Some(7.0));
312 let moved = PanEvent::Update {
313 local: at,
314 delta: Point::new(0.0, -4.0),
315 };
316 assert_eq!(wrapped.on_pan(moved), Some(-4.0));
317 let end = PanEvent::End {
318 velocity: Point::new(5.0, 0.0),
319 };
320 assert_eq!(wrapped.on_pan(end), Some(1005.0));
321 let updates = GestureDetector::<f32>::new(Container::new()).on_pan_update(|_, d| d.x);
323 assert_eq!(Widget::pan_axis(&updates), Some(PanAxis::Free));
324 assert_eq!(
325 Widget::on_pan(&updates, PanEvent::Start { local: at }),
326 None
327 );
328 }
329
330 #[test]
333 fn a_dragging_detector_is_found_even_from_the_cache() {
334 let size = Size::new(200.0, 200.0);
335 let rt = Runtime::default();
336 let theme = Theme::default();
337 let inner = GestureDetector::new(Container::<u8>::new().width(50.0).height(50.0))
338 .on_pan_update(|_, _| 2);
339 let tree = GestureDetector::new(
340 Container::<u8>::new()
341 .repaint_boundary()
342 .width(200.0)
343 .height(200.0)
344 .child(inner),
345 )
346 .on_pan_update(|_, _| 1);
347 let first = build_ui(&tree, size, &rt, &theme);
348 let second = build_ui(&tree, size, &rt, &theme);
349 assert_eq!(rt.paint_cache.borrow().last_frame_stats().0, 1, "replayed");
350 for ui in [&first, &second] {
351 let (outer, _) = ui.pan_at(Point::new(150.0, 150.0)).expect("the outer one");
352 let (inner, rect) = ui.pan_at(Point::new(20.0, 20.0)).expect("the inner one");
353 assert_ne!(outer, inner, "the innermost wins");
354 assert_eq!(rect.width, 50.0);
355 }
356 let plain = GestureDetector::new(Container::<u8>::new().width(50.0).height(50.0)).on_tap(1);
357 assert!(build_ui(&plain, size, &rt, &theme)
358 .pan_at(Point::new(5.0, 5.0))
359 .is_none());
360 }
361
362 #[test]
367 fn a_detector_leaves_the_layout_alone() {
368 let red = frus_core::Color::rgb(1.0, 0.0, 0.0);
369 let pad = || Container::<u8>::new().height(40.0).color(red);
370 let size = Size::new(300.0, 200.0);
371 let rt = Runtime::default();
372 let theme = Theme::default();
373 let bare = Flex::<u8>::column().width(300.0).child(pad());
374 let detected = Flex::<u8>::column()
375 .width(300.0)
376 .child(GestureDetector::new(pad()).on_tap(1));
377 let reds = |ui: &crate::Ui<u8>| -> Vec<frus_core::Rect> {
378 ui.scene()
379 .primitives()
380 .iter()
381 .filter_map(|p| match p {
382 frus_core::Primitive::Rect { rect, color, .. } if *color == red => Some(*rect),
383 _ => None,
384 })
385 .collect()
386 };
387 let without = reds(&build_ui(&bare, size, &rt, &theme));
388 let with = reds(&build_ui(&detected, size, &rt, &theme));
389 assert_eq!(without.first().map(|r| r.width), Some(300.0), "{without:?}");
390 assert_eq!(with, without);
391 }
392
393 #[test]
396 fn the_child_answers_first() {
397 let child = Container::<u8>::new().on_click(7u8);
398 let detector = GestureDetector::new(child).on_tap(1).on_double_tap(2);
399 assert_eq!(detector.on_click(), Some(7), "the child's tap");
400 assert_eq!(
401 Widget::on_double_tap(&detector),
402 Some(2),
403 "the detector's double tap"
404 );
405 }
406}