Skip to main content

cranpose_app_shell/
wheel.rs

1use cranpose_foundation::Modifiers;
2use cranpose_ui_graphics::Point;
3
4const NOTCH_LOGICAL_PX: f32 = 40.0;
5const ZOOM_PER_NOTCH: f32 = 1.2;
6
7/// A mouse-wheel or trackpad scroll sample ready for
8/// [`AppShell::wheel_scrolled`](crate::AppShell::wheel_scrolled).
9#[derive(Clone, Copy, Debug, PartialEq)]
10pub struct WheelScroll {
11    /// Scroll amount in logical pixels; positive moves content down and right
12    /// (see the module docs — this is winit's sign, not the DOM's).
13    pub delta: Point,
14    /// Keyboard modifiers held during the sample. `ctrl` makes it a zoom
15    /// gesture, `alt` turns a vertical wheel into a horizontal scroll.
16    pub modifiers: Modifiers,
17    /// Monotonic milliseconds, for the rotary event's velocity tracking. Only
18    /// differences between samples are meaningful.
19    pub uptime_millis: u64,
20}
21
22impl WheelScroll {
23    /// A sample with no modifiers held.
24    pub fn new(delta: Point, uptime_millis: u64) -> Self {
25        Self {
26            delta,
27            modifiers: Modifiers::NONE,
28            uptime_millis,
29        }
30    }
31
32    /// This sample with `modifiers` held.
33    pub fn with_modifiers(mut self, modifiers: Modifiers) -> Self {
34        self.modifiers = modifiers;
35        self
36    }
37
38    /// Whether this sample is the zoom gesture (ctrl+wheel, which is also how
39    /// trackpad pinches arrive in a browser) rather than a scroll.
40    pub fn is_zoom(&self) -> bool {
41        self.modifiers.ctrl
42    }
43
44    /// The multiplicative zoom step for a ctrl+wheel sample: one notch up
45    /// (positive delta) zooms in by `ZOOM_PER_NOTCH`.
46    pub fn zoom_factor(&self) -> f32 {
47        ZOOM_PER_NOTCH.powf(self.delta.y / NOTCH_LOGICAL_PX)
48    }
49
50    /// The delta the hovered scrollable should see: with alt held, a vertical
51    /// wheel drives the horizontal axis instead (the shift-less way to scroll
52    /// a row on a wheel that only has a vertical axis).
53    pub fn scroll_delta(&self) -> Point {
54        if !self.modifiers.alt {
55            return self.delta;
56        }
57        let x = if self.delta.x.abs() <= f32::EPSILON {
58            self.delta.y
59        } else {
60            self.delta.x
61        };
62        Point { x, y: 0.0 }
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    fn wheel(x: f32, y: f32) -> WheelScroll {
71        WheelScroll::new(Point { x, y }, 0)
72    }
73
74    #[test]
75    fn a_plain_sample_is_neither_a_zoom_nor_axis_swapped() {
76        let sample = wheel(3.0, -40.0);
77
78        assert!(!sample.is_zoom());
79        assert_eq!(sample.scroll_delta(), sample.delta);
80    }
81
82    #[test]
83    fn ctrl_makes_a_sample_a_zoom_that_grows_when_the_wheel_turns_up() {
84        let up = wheel(0.0, NOTCH_LOGICAL_PX).with_modifiers(Modifiers {
85            ctrl: true,
86            ..Modifiers::NONE
87        });
88        let down = wheel(0.0, -NOTCH_LOGICAL_PX).with_modifiers(Modifiers {
89            ctrl: true,
90            ..Modifiers::NONE
91        });
92
93        assert!(up.is_zoom());
94        assert!((up.zoom_factor() - ZOOM_PER_NOTCH).abs() < 1.0e-6);
95        assert!((up.zoom_factor() * down.zoom_factor() - 1.0).abs() < 1.0e-6);
96    }
97
98    #[test]
99    fn alt_moves_a_vertical_wheel_onto_the_horizontal_axis() {
100        let alt = Modifiers {
101            alt: true,
102            ..Modifiers::NONE
103        };
104
105        assert_eq!(
106            wheel(0.0, 48.0).with_modifiers(alt).scroll_delta(),
107            Point { x: 48.0, y: 0.0 }
108        );
109        assert_eq!(
110            wheel(12.0, 48.0).with_modifiers(alt).scroll_delta(),
111            Point { x: 12.0, y: 0.0 }
112        );
113    }
114}