Skip to main content

frus_widgets/
gesturedetector.rs

1//! [`GestureDetector`]: taps, double taps, long presses, secondary clicks and drags on anything.
2
3use frus_core::Point;
4
5use crate::widget::Widget;
6
7/// **Answers gestures on its child** with the application's messages: a tap, a double tap,
8/// a long press, a secondary click (the right mouse button), and a drag.
9///
10/// It is a transparent wrapper: it **is** its child, laid out and painted exactly as the child
11/// would be without it, and it hears the gestures that land inside the child. A detector that
12/// wants a tap gives the child none of its own look — no ink, no hover — which is what
13/// [`InkWell`](crate::InkWell) and the buttons are for.
14///
15/// ```
16/// use frus_widgets::{GestureDetector, Text};
17///
18/// #[derive(Clone)]
19/// enum Msg { Open, Like, Menu }
20///
21/// let _photo: GestureDetector<Msg> = GestureDetector::new(Text::new("photo"))
22///     .on_tap(Msg::Open)
23///     .on_double_tap(Msg::Like)
24///     .on_secondary_tap(Msg::Menu);
25/// ```
26///
27/// **A tap waits when a double tap is possible.** With both set, a first tap is not a tap
28/// until 300 ms have gone by without a second one, as on every platform: the second tap of a
29/// double tap is not also two taps. With only a tap set, it answers at once.
30///
31/// **What the child answers itself, it keeps.** A detector around a button does not take the
32/// button's tap: the innermost answer wins, as in the reference, and the detector answers
33/// only the gestures the child has no answer to.
34pub 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
46/// A message the detector sends, made afresh each time it is asked for.
47type Message<Msg> = Box<dyn Fn() -> Msg>;
48
49/// Which way a [`GestureDetector`]'s drag may go (milestone 582).
50///
51/// The reference has three families of callbacks for this — pan, horizontal drag, vertical
52/// drag — and forbids mixing them; here it is one family and a direction.
53#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
54pub enum PanAxis {
55    /// Any direction: a pan.
56    #[default]
57    Free,
58    /// Left and right only: the vertical part of the movement is not reported, and a vertical
59    /// movement inside a scroll is the scroll's.
60    Horizontal,
61    /// Up and down only.
62    Vertical,
63}
64
65/// One moment of a drag on a [`GestureDetector`], as the shell reports it through
66/// [`Widget::on_pan`](crate::Widget::on_pan).
67#[derive(Clone, Copy, Debug, PartialEq)]
68pub enum PanEvent {
69    /// The drag began: the pointer went down at `local` (in the detector's own coordinates)
70    /// and has since moved far enough to be a drag rather than a tap.
71    Start {
72        /// Where the pointer went down.
73        local: Point,
74    },
75    /// The pointer moved by `delta` and is now at `local`. Along a [`PanAxis`] that is not
76    /// free, the other component of `delta` is zero.
77    Update {
78        /// Where the pointer is now, in the detector's own coordinates.
79        local: Point,
80        /// How far it moved since the last update, in logical pixels.
81        delta: Point,
82    },
83    /// The pointer lifted, moving at `velocity` (logical pixels per second).
84    End {
85        /// The release velocity, masked to the axis like the updates.
86        velocity: Point,
87    },
88}
89
90impl<Msg> GestureDetector<Msg> {
91    /// Listens for gestures on `child`. Nothing is answered until a gesture is given a message.
92    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    /// A detector is not a box: its child's own, unchanged.
107    fn restyle(&self, base: frus_layout::Style) -> frus_layout::Style {
108        base
109    }
110}
111
112/// The message `message` will send, each time it is asked for.
113fn 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    /// A **tap**: a press and a release in the same place, with a finger or the main mouse
120    /// button.
121    pub fn on_tap(mut self, message: impl Into<Msg>) -> Self {
122        self.tap = keep(message);
123        self
124    }
125
126    /// A **double tap**: two taps in quick succession, close together.
127    pub fn on_double_tap(mut self, message: impl Into<Msg>) -> Self {
128        self.double_tap = keep(message);
129        self
130    }
131
132    /// A **long press**: a press held still for half a second. The release that follows is
133    /// not also a tap.
134    pub fn on_long_press(mut self, message: impl Into<Msg>) -> Self {
135        self.long_press = keep(message);
136        self
137    }
138
139    /// A **secondary tap**: a click of the secondary mouse button, usually the right one,
140    /// which on a desktop opens a context menu.
141    pub fn on_secondary_tap(mut self, message: impl Into<Msg>) -> Self {
142        self.secondary_tap = keep(message);
143        self
144    }
145
146    /// A **drag** has begun: `build` is given where the pointer went down, in the detector's
147    /// own coordinates. A drag begins once the pointer has moved far enough to not be a tap.
148    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    /// The pointer moved during a drag: `build` is given where it is now, in the detector's
154    /// own coordinates, and how far it moved since the last update.
155    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    /// The drag ended: `build` is given the velocity the pointer lifted at, in logical pixels
161    /// per second.
162    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    /// Keeps the drag to one direction — the reference's horizontal and vertical drags. The
168    /// other component of each movement is not reported, and inside a scroll that runs the
169    /// other way the drag no longer competes with it. Free, a pan, by default.
170    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    /// The child's tap, or the detector's.
178    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    /// The child's drag, if it has one; the detector's otherwise.
203    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    /// Forwarded, as for every transparent wrapper: a detector is not an identity, a place, a
224    /// theme, a surface, a form, nor a selection area.
225    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    /// **Every gesture is the detector's to answer**, through any wrapper around it.
264    #[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    /// **A detector with no tap of its own is still hit** by a press, so that the shell can
279    /// ask it for the double or secondary tap; and one with nothing at all is not.
280    #[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(&nothing, size, &rt, &theme);
294        assert!(ui.hit(Point::new(50.0, 50.0)).is_none());
295    }
296
297    /// **A detector drags only once told what a drag sends**, in the way it was told, and
298    /// every moment of the drag reaches its builder — through any wrapper (milestone 582).
299    #[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        // Only an update told: the other moments send nothing.
322        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    /// **A detector that drags is found under the pointer**, the innermost first, and still
331    /// found when its frame is replayed from the paint cache.
332    #[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    /// **A detector changes nothing about the layout**: its child comes out exactly as it
363    /// would without it — a box with no width of its own stretched across a column, as a bare
364    /// one is. (A detector that was a box of its own gave such a child no width at all, and
365    /// the pad in the demo's About page drew nothing: milestone 582, seen on the phone.)
366    #[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    /// **What the child answers itself, it keeps**: a detector around something that takes a
394    /// tap answers only what the child does not.
395    #[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}