makepad_widgets/
touch_gesture.rs

1use crate::{
2    Area,
3    Cx,
4    Event,
5    Hit,
6    MouseCursor,
7    NextFrame,
8};
9
10#[derive(Clone, Copy, Debug)]
11struct ScrollSample{
12    abs: f64,
13    time: f64,
14}
15
16#[derive(Default, Clone, Debug)]
17pub enum ScrollMode {
18    #[default]
19    DragAndDrop,
20    Swipe,
21}
22
23#[derive(Default, Clone, Debug)]
24enum ScrollState {
25    #[default]
26    Stopped,
27    Drag{samples:Vec<ScrollSample>},
28    Flick {delta: f64, next_frame: NextFrame},
29    Pulldown {next_frame: NextFrame},
30}
31
32#[derive(Default, PartialEq)]
33pub enum TouchMotionChange {
34    #[default]
35    None,
36    ScrollStateChanged,
37    ScrolledAtChanged,
38}
39
40#[derive(Default, Clone)]
41pub struct TouchGesture {
42    flick_scroll_minimum: f64,
43    flick_scroll_maximum: f64,
44    flick_scroll_scaling: f64,
45    flick_scroll_decay: f64,
46
47    scroll_mode: ScrollMode,
48    scroll_state: ScrollState,
49
50    min_scrolled_at: f64,
51    max_scrolled_at: f64,
52    pulldown_maximum: f64,
53
54    pub scrolled_at: f64,
55}
56
57impl TouchGesture {
58    pub fn new() -> Self {
59        Self {
60            flick_scroll_minimum: 0.2,
61            flick_scroll_maximum: 80.0,
62            flick_scroll_scaling: 0.005,
63            flick_scroll_decay: 0.98,
64
65            scroll_state: ScrollState::Stopped,
66            scroll_mode: ScrollMode::DragAndDrop,
67
68            scrolled_at: 0.0,
69            min_scrolled_at: f64::MIN,
70            max_scrolled_at: f64::MAX,
71            pulldown_maximum: 60.0,
72        }
73    }
74
75    pub fn reset_scrolled_at(&mut self) {
76        self.scrolled_at = 0.0;
77    }
78
79    pub fn set_mode(&mut self, scroll_mode: ScrollMode) {
80        self.scroll_mode = scroll_mode;
81    }
82
83    pub fn set_range(&mut self, min_offset: f64, max_offset: f64) {
84        self.min_scrolled_at = min_offset;
85        self.max_scrolled_at = max_offset;
86        self.scrolled_at = self.scrolled_at.clamp(
87            self.min_scrolled_at - self.pulldown_maximum,
88            self.max_scrolled_at + self.pulldown_maximum
89        );
90    }
91
92    pub fn stop(&mut self) {
93        self.scrolled_at = 0.0;
94        self.scroll_state = ScrollState::Stopped;
95    }
96
97    pub fn is_stopped(&self) -> bool {
98        match self.scroll_state {
99            ScrollState::Stopped => true,
100            _ => false
101        }
102    }
103
104    pub fn is_dragging(&self) -> bool {
105        match self.scroll_state {
106            ScrollState::Drag {..} => true,
107            _ => false
108        }
109    }
110
111    pub fn handle_event(&mut self, cx: &mut Cx, event: &Event, area: Area) -> TouchMotionChange {
112        let needs_pulldown_when_flicking = self.needs_pulldown_when_flicking();
113        let needs_pulldown = self.needs_pulldown();
114
115        match &mut self.scroll_state {
116            ScrollState::Flick {delta, next_frame} => {
117                if let Some(_) = next_frame.is_event(event) {
118                    *delta = *delta * self.flick_scroll_decay;
119                    if needs_pulldown_when_flicking {
120                        self.scroll_state = ScrollState::Pulldown {next_frame: cx.new_next_frame()};
121                        return TouchMotionChange::ScrollStateChanged
122                    } else if delta.abs() > self.flick_scroll_minimum {
123                        *next_frame = cx.new_next_frame();
124                        let delta = *delta;
125
126                        let new_offset = self.scrolled_at - delta;
127                        self.scrolled_at = new_offset.clamp(
128                            self.min_scrolled_at - self.pulldown_maximum,
129                            self.max_scrolled_at + self.pulldown_maximum
130                        );
131
132                        return TouchMotionChange::ScrolledAtChanged
133                    } else {
134                        if needs_pulldown {
135                            self.scroll_state = ScrollState::Pulldown {next_frame: cx.new_next_frame()};
136                        } else {
137                            self.scroll_state = ScrollState::Stopped;
138                        }
139
140                        return TouchMotionChange::ScrollStateChanged
141                    }
142                }
143            }
144            ScrollState::Pulldown {next_frame} => {
145                if let Some(_) = next_frame.is_event(event) {
146                    if self.scrolled_at < self.min_scrolled_at {
147                        self.scrolled_at += (self.min_scrolled_at - self.scrolled_at) * 0.1;
148                        if self.min_scrolled_at - self.scrolled_at < 1.0 {
149                            self.scrolled_at = self.min_scrolled_at + 0.5;
150                        }
151                        else {
152                            *next_frame = cx.new_next_frame();
153                        }
154
155                        return TouchMotionChange::ScrolledAtChanged
156                    }
157                    else if self.scrolled_at > self.max_scrolled_at {
158                        self.scrolled_at -= (self.scrolled_at - self.max_scrolled_at) * 0.1;
159                        if self.scrolled_at - self.max_scrolled_at < 1.0 {
160                            self.scrolled_at = self.max_scrolled_at - 0.5;
161
162                            return TouchMotionChange::ScrolledAtChanged
163                        }
164                        else {
165                            *next_frame = cx.new_next_frame();
166                        }
167
168                        return TouchMotionChange::ScrolledAtChanged
169                    }
170                    else {
171                        self.scroll_state = ScrollState::Stopped;
172                        return TouchMotionChange::ScrollStateChanged
173                    }
174                }
175            }
176            _=>()
177        }
178
179        match event.hits_with_capture_overload(cx, area, true) {
180            Hit::FingerDown(e) => {
181                self.scroll_state = ScrollState::Drag {
182                    samples: vec![ScrollSample{abs: e.abs.y, time: e.time}]
183                };
184
185                return TouchMotionChange::ScrollStateChanged
186            }
187            Hit::FingerMove(e) => {
188                cx.set_cursor(MouseCursor::Default);
189                match &mut self.scroll_state {
190                    ScrollState::Drag {samples}=>{
191                        let new_abs = e.abs.y;
192                        let old_sample = *samples.last().unwrap();
193                        samples.push(ScrollSample{abs: new_abs, time: e.time});
194                        if samples.len() > 4 {
195                            samples.remove(0);
196                        }
197                        let new_offset = self.scrolled_at + old_sample.abs - new_abs;
198                        self.scrolled_at = new_offset.clamp(
199                            self.min_scrolled_at - self.pulldown_maximum,
200                            self.max_scrolled_at + self.pulldown_maximum
201                        );
202
203                        return TouchMotionChange::ScrolledAtChanged
204                    }
205                    _=>()
206                }
207            }
208            Hit::FingerUp(_e) => {
209                match &mut self.scroll_state {
210                    ScrollState::Drag {samples} => {
211                        match self.scroll_mode {
212                            ScrollMode::Swipe => {
213                                let mut last = None;
214                                let mut scaled_delta = 0.0;
215                                let mut total_delta = 0.0;
216                                for sample in samples.iter().rev() {
217                                    if last.is_none() {
218                                        last = Some(sample);
219                                    }
220                                    else {
221                                        total_delta += last.unwrap().abs - sample.abs;
222                                        scaled_delta += (last.unwrap().abs - sample.abs)/ (last.unwrap().time - sample.time)
223                                    }
224                                }
225                                scaled_delta *= self.flick_scroll_scaling;
226
227                                if self.needs_pulldown() {
228                                    self.scroll_state = ScrollState::Pulldown {next_frame: cx.new_next_frame()};
229                                }
230                                else if total_delta.abs() > 10.0 && scaled_delta.abs() > self.flick_scroll_minimum {
231                                    self.scroll_state = ScrollState::Flick {
232                                        delta: scaled_delta.min(self.flick_scroll_maximum).max(-self.flick_scroll_maximum),
233                                        next_frame: cx.new_next_frame()
234                                    };
235                                } else {
236                                    self.scroll_state = ScrollState::Stopped;
237                                }
238
239                                return TouchMotionChange::ScrollStateChanged
240                            }
241                            ScrollMode::DragAndDrop => {
242                                self.scroll_state = ScrollState::Stopped;
243                                return TouchMotionChange::ScrollStateChanged
244                            }
245                        }
246                    }
247                    _=>()
248                }
249            }
250            _ => ()
251        }
252
253        TouchMotionChange::None
254    }
255
256    fn needs_pulldown(&self) -> bool {
257        self.scrolled_at < self.min_scrolled_at || self.scrolled_at > self.max_scrolled_at
258    }
259
260    fn needs_pulldown_when_flicking(&self) -> bool {
261        self.scrolled_at - 0.5 < self.min_scrolled_at - self.pulldown_maximum ||
262            self.scrolled_at + 0.5 > self.max_scrolled_at + self.pulldown_maximum
263    }
264}
265
266impl TouchMotionChange {
267    pub fn has_changed(&self) -> bool {
268        match self {
269            TouchMotionChange::None => false,
270            _ => true
271        }
272    }
273}