1use emath::{Rect, Vec2, vec2};
2
3use crate::{InputOptions, Modifiers, MouseWheelSource, MouseWheelUnit, TouchPhase};
4
5const SCROLL_ACTION_TIMEOUT: f64 = 0.150;
10
11const RESTING_FINGER_TIMEOUT: f64 = 1.0;
15
16#[derive(Clone, Debug, PartialEq, Eq)]
37pub enum Status {
38 Static,
40
41 Smoothing,
43
44 InTouch,
50}
51
52#[derive(Clone, Debug)]
54pub struct WheelState {
55 pub status: Status,
60
61 pub source: MouseWheelSource,
63
64 pub modifiers: Modifiers,
66
67 pub last_wheel_event: f64,
69
70 pub unprocessed_wheel_delta: Vec2,
72
73 pub smooth_wheel_delta: Vec2,
86}
87
88impl Default for WheelState {
89 fn default() -> Self {
90 Self {
91 status: Status::Static,
92 source: MouseWheelSource::Unknown,
93 modifiers: Default::default(),
94 last_wheel_event: f64::NEG_INFINITY,
95 unprocessed_wheel_delta: Vec2::ZERO,
96 smooth_wheel_delta: Vec2::ZERO,
97 }
98 }
99}
100
101impl WheelState {
102 #[expect(clippy::too_many_arguments)]
103 pub fn on_wheel_event(
104 &mut self,
105 viewport_rect: Rect,
106 options: &InputOptions,
107 time: f64,
108 unit: MouseWheelUnit,
109 delta: Vec2,
110 phase: TouchPhase,
111 source: MouseWheelSource,
112 latest_modifiers: Modifiers,
113 ) {
114 if self.is_scroll_action_over(time) {
115 self.end_scroll_action();
120 }
121
122 self.last_wheel_event = time;
123 self.source = source;
124 match phase {
125 crate::TouchPhase::Start => {
126 self.status = Status::InTouch;
127 self.modifiers = latest_modifiers;
128 }
129 crate::TouchPhase::Move => {
130 match self.status {
131 Status::Static | Status::Smoothing => {
132 self.modifiers = latest_modifiers;
133 self.status = Status::Smoothing;
134 }
135 Status::InTouch => {
136 self.modifiers |= latest_modifiers;
140 }
141 }
142
143 let mut delta = match unit {
144 MouseWheelUnit::Point => delta,
145 MouseWheelUnit::Line => options.line_scroll_speed * delta,
146 MouseWheelUnit::Page => viewport_rect.height() * delta,
147 };
148
149 let is_horizontal = self
150 .modifiers
151 .matches_any(options.horizontal_scroll_modifier);
152 let is_vertical = self.modifiers.matches_any(options.vertical_scroll_modifier);
153
154 if is_horizontal && !is_vertical {
155 delta = vec2(delta.x + delta.y, 0.0);
158 }
159 if !is_horizontal && is_vertical {
160 delta = vec2(0.0, delta.x + delta.y);
162 }
163
164 let is_smooth = self.status == Status::InTouch
170 || match unit {
171 MouseWheelUnit::Point => delta.length() < 8.0, MouseWheelUnit::Line | MouseWheelUnit::Page => false,
173 };
174
175 if is_smooth {
176 self.smooth_wheel_delta += delta;
177 } else {
178 self.unprocessed_wheel_delta += delta;
179 }
180 }
181 crate::TouchPhase::End | crate::TouchPhase::Cancel => {
182 self.status = Status::Static;
183 self.modifiers = Default::default();
184 self.unprocessed_wheel_delta = Default::default();
185 self.smooth_wheel_delta = Default::default();
186 }
187 }
188 }
189
190 pub fn after_events(&mut self, time: f64, dt: f32) {
191 let t = crate::emath::exponential_smooth_factor(0.90, 0.1, dt); if self.unprocessed_wheel_delta != Vec2::ZERO {
194 for d in 0..2 {
195 if self.unprocessed_wheel_delta[d].abs() < 1.0 {
196 self.smooth_wheel_delta[d] += self.unprocessed_wheel_delta[d];
197 self.unprocessed_wheel_delta[d] = 0.0;
198 } else {
199 let applied = t * self.unprocessed_wheel_delta[d];
200 self.smooth_wheel_delta[d] += applied;
201 self.unprocessed_wheel_delta[d] -= applied;
202 }
203 }
204 }
205
206 if self.smooth_wheel_delta == Vec2::ZERO && self.is_scroll_action_over(time) {
207 self.end_scroll_action();
208 }
209 }
210
211 fn is_scroll_action_over(&self, time: f64) -> bool {
217 let timeout = if self.status == Status::InTouch && self.source == MouseWheelSource::Trackpad
218 {
219 RESTING_FINGER_TIMEOUT
220 } else {
221 SCROLL_ACTION_TIMEOUT
222 };
223 self.status != Status::Static && timeout < time - self.last_wheel_event
224 }
225
226 fn end_scroll_action(&mut self) {
227 self.status = Status::Static;
228 self.modifiers = Default::default();
229 }
230
231 pub fn is_scrolling(&self) -> bool {
233 self.status != Status::Static
234 }
235
236 pub fn ui(&self, ui: &mut crate::Ui) {
237 let Self {
238 status,
239 source,
240 modifiers,
241 last_wheel_event,
242 unprocessed_wheel_delta,
243 smooth_wheel_delta,
244 } = self;
245
246 let time = ui.input(|i| i.time);
247
248 crate::Grid::new("ScrollState")
249 .num_columns(2)
250 .show(ui, |ui| {
251 ui.label("status");
252 ui.monospace(format!("{status:?}"));
253 ui.end_row();
254
255 ui.label("source");
256 ui.monospace(format!("{source:?}"));
257 ui.end_row();
258
259 ui.label("modifiers");
260 ui.monospace(format!("{modifiers:?}"));
261 ui.end_row();
262
263 ui.label("last_wheel_event");
264 ui.monospace(format!("{:.1}s ago", time - *last_wheel_event));
265 ui.end_row();
266
267 ui.label("unprocessed_wheel_delta");
268 ui.monospace(unprocessed_wheel_delta.to_string());
269 ui.end_row();
270
271 ui.label("smooth_wheel_delta");
272 ui.monospace(smooth_wheel_delta.to_string());
273 ui.end_row();
274 });
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use emath::{Vec2, vec2};
281
282 use crate::{
283 Context, Event, Modifiers, MouseWheelSource, MouseWheelUnit, RawInput, TouchPhase,
284 };
285
286 fn wheel(phase: TouchPhase, modifiers: Modifiers) -> Event {
287 wheel_from(phase, MouseWheelSource::Unknown, modifiers)
288 }
289
290 fn wheel_from(phase: TouchPhase, source: MouseWheelSource, modifiers: Modifiers) -> Event {
291 Event::MouseWheel {
292 unit: MouseWheelUnit::Point,
293 delta: vec2(0.0, -5.0),
294 phase,
295 source,
296 modifiers,
297 }
298 }
299
300 fn run_frame(ctx: &Context, time: f64, events: Vec<Event>) -> (Vec2, f32, bool) {
302 let input = RawInput {
303 time: Some(time),
304 events,
305 ..Default::default()
306 };
307 let mut result = None;
308 let output = ctx.run_ui(input, |ui| {
309 if result.is_none() {
311 result =
312 Some(ui.input(|i| (i.smooth_scroll_delta(), i.zoom_delta(), i.is_scrolling())));
313 }
314 });
315 output.drop_without_applying_deltas();
316 result.unwrap_or_default()
317 }
318
319 #[test]
323 fn modifiers_should_not_stick_when_touch_phase_end_never_arrives() {
324 let ctx = Context::default();
325
326 let (scroll, zoom, _) = run_frame(
327 &ctx,
328 0.0,
329 vec![
330 wheel(TouchPhase::Start, Modifiers::COMMAND),
331 wheel(TouchPhase::Move, Modifiers::COMMAND),
332 ],
333 );
334 assert_eq!(scroll, Vec2::ZERO, "ctrl+scroll should not scroll");
335 assert!(zoom != 1.0, "ctrl+scroll should zoom");
336
337 let (scroll, zoom, _) =
340 run_frame(&ctx, 1.0, vec![wheel(TouchPhase::Move, Modifiers::NONE)]);
341 assert_eq!(zoom, 1.0, "scrolling without ctrl should not zoom");
342 assert_eq!(
343 scroll,
344 vec2(0.0, -5.0),
345 "scrolling without ctrl should scroll"
346 );
347 }
348
349 #[test]
350 fn scroll_action_should_end_when_touch_phase_end_never_arrives() {
351 let ctx = Context::default();
352
353 let (_, _, is_scrolling) = run_frame(
354 &ctx,
355 0.0,
356 vec![
357 wheel(TouchPhase::Start, Modifiers::NONE),
358 wheel(TouchPhase::Move, Modifiers::NONE),
359 ],
360 );
361 assert!(is_scrolling, "we just scrolled");
362
363 let (_, _, is_scrolling) = run_frame(&ctx, 1.0, vec![]);
365 assert!(
366 !is_scrolling,
367 "no scroll events for a second: the scroll action is over"
368 );
369 }
370
371 #[test]
375 fn modifiers_should_stick_until_touch_phase_end() {
376 let ctx = Context::default();
377
378 let (scroll, _, _) = run_frame(
379 &ctx,
380 0.0,
381 vec![
382 wheel(TouchPhase::Start, Modifiers::SHIFT),
383 wheel(TouchPhase::Move, Modifiers::SHIFT),
384 ],
385 );
386 assert_eq!(
387 scroll,
388 vec2(-5.0, 0.0),
389 "shift+scroll should scroll horizontally"
390 );
391
392 let (scroll, _, is_scrolling) =
394 run_frame(&ctx, 0.05, vec![wheel(TouchPhase::Move, Modifiers::NONE)]);
395 assert_eq!(scroll, vec2(-5.0, 0.0), "should still scroll horizontally");
396 assert!(is_scrolling, "the scroll action is still going");
397
398 let (_, _, is_scrolling) =
399 run_frame(&ctx, 0.1, vec![wheel(TouchPhase::End, Modifiers::NONE)]);
400 assert!(!is_scrolling, "the scroll action ended");
401
402 let (scroll, _, _) = run_frame(
403 &ctx,
404 0.2,
405 vec![
406 wheel(TouchPhase::Start, Modifiers::NONE),
407 wheel(TouchPhase::Move, Modifiers::NONE),
408 ],
409 );
410 assert_eq!(
411 scroll,
412 vec2(0.0, -5.0),
413 "a new scroll action without shift is vertical"
414 );
415 }
416
417 #[test]
419 fn resting_fingers_keep_the_scroll_action_alive() {
420 let ctx = Context::default();
421 let finger = MouseWheelSource::Trackpad;
422
423 run_frame(
424 &ctx,
425 0.0,
426 vec![
427 wheel_from(TouchPhase::Start, finger, Modifiers::NONE),
428 wheel_from(TouchPhase::Move, finger, Modifiers::NONE),
429 ],
430 );
431
432 let (_, _, is_scrolling) = run_frame(&ctx, 0.5, vec![]);
433 assert!(is_scrolling, "fingers are resting on the trackpad");
434
435 let (_, _, is_scrolling) = run_frame(&ctx, 1.5, vec![]);
436 assert!(
437 !is_scrolling,
438 "but we give up eventually, in case the end got lost"
439 );
440
441 run_frame(&ctx, 2.0, vec![wheel(TouchPhase::Start, Modifiers::NONE)]);
443 let (_, _, is_scrolling) = run_frame(&ctx, 2.5, vec![]);
444 assert!(!is_scrolling);
445 }
446}