ui_events/scroll/
mod.rs

1// Copyright 2025 the UI Events Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4use dpi::PhysicalPosition;
5
6/// Scroll delta.
7///
8/// Deltas are in a Y-down coordinate system, and represent a ‘navigation’
9/// direction; a positive Y value means that the viewport should move downward
10/// relative to the content.
11///
12/// For mouse wheel events, only `LineDelta` and `PixelDelta` are typical.
13/// For scroll deltas generated by scrollbars or other elements, `PageDelta`
14/// may be used (for example, when clicking in the well of the scrollbar).
15#[derive(Clone, Copy, Debug, PartialEq)]
16pub enum ScrollDelta {
17    /// Page delta.
18    ///
19    /// Page deltas are almost always synthetic, typically generated by clicking
20    /// in the well of a scrollbar.
21    PageDelta(f32, f32),
22    /// Line delta.
23    LineDelta(f32, f32),
24    /// Pixel delta.
25    PixelDelta(PhysicalPosition<f64>),
26}
27
28impl ScrollDelta {
29    /// Convert this scroll delta into a pixel delta using caller-provided scaling.
30    ///
31    /// This is a policy hook: the caller chooses what a "line" or "page" means in pixels.
32    ///
33    /// ## Example (physical pixel policy)
34    ///
35    /// Convert line deltas into a pixel delta by choosing a line size in physical pixels:
36    ///
37    /// ```
38    /// use dpi::PhysicalPosition;
39    /// use ui_events::ScrollDelta;
40    ///
41    /// let delta = ScrollDelta::LineDelta(0.0, -3.0);
42    ///
43    /// // Policy: 1 line = 40 physical px vertically.
44    /// let line_px = PhysicalPosition { x: 0.0, y: 40.0 };
45    /// // Policy: 1 page = 800 physical px vertically (e.g. viewport height).
46    /// let page_px = PhysicalPosition { x: 0.0, y: 800.0 };
47    ///
48    /// let px = delta.to_pixel_delta(line_px, page_px);
49    /// assert_eq!(px, PhysicalPosition { x: 0.0, y: -120.0 });
50    /// ```
51    ///
52    /// ## Scale factor and logical units
53    ///
54    /// If your policy is expressed in logical/CSS pixels, apply your scale factor
55    /// (device pixel ratio) before calling `to_pixel_delta`:
56    ///
57    /// ```no_run
58    /// use dpi::PhysicalPosition;
59    /// use ui_events::ScrollDelta;
60    ///
61    /// let delta = ScrollDelta::LineDelta(0.0, 1.0);
62    /// let dpr = 2.0; // from your platform/window
63    ///
64    /// // Policy: 1 line = 16 CSS px vertically.
65    /// let line_px = PhysicalPosition { x: 0.0, y: 16.0 * dpr };
66    /// // Policy: 1 page = 800 physical px vertically.
67    /// let page_px = PhysicalPosition { x: 0.0, y: 800.0 };
68    ///
69    /// let _px = delta.to_pixel_delta(line_px, page_px);
70    /// ```
71    ///
72    /// - [`ScrollDelta::PixelDelta`] is returned unchanged.
73    /// - [`ScrollDelta::LineDelta`] is multiplied by `line_px` per axis.
74    /// - [`ScrollDelta::PageDelta`] is multiplied by `page_px` per axis.
75    #[inline]
76    pub fn to_pixel_delta(
77        self,
78        line_px: PhysicalPosition<f64>,
79        page_px: PhysicalPosition<f64>,
80    ) -> PhysicalPosition<f64> {
81        match self {
82            Self::PixelDelta(p) => p,
83            Self::LineDelta(x, y) => PhysicalPosition {
84                x: f64::from(x) * line_px.x,
85                y: f64::from(y) * line_px.y,
86            },
87            Self::PageDelta(x, y) => PhysicalPosition {
88                x: f64::from(x) * page_px.x,
89                y: f64::from(y) * page_px.y,
90            },
91        }
92    }
93
94    /// Convert this scroll delta into [`ScrollDelta::PixelDelta`] using caller-provided scaling.
95    #[inline]
96    pub fn into_pixel_delta(
97        self,
98        line_px: PhysicalPosition<f64>,
99        page_px: PhysicalPosition<f64>,
100    ) -> Self {
101        Self::PixelDelta(self.to_pixel_delta(line_px, page_px))
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn pixel_passthrough() {
111        let p = PhysicalPosition { x: 1.0, y: -2.0 };
112        assert_eq!(
113            ScrollDelta::PixelDelta(p).to_pixel_delta(
114                PhysicalPosition { x: 10.0, y: 10.0 },
115                PhysicalPosition { x: 100.0, y: 100.0 },
116            ),
117            p
118        );
119    }
120
121    #[test]
122    fn line_delta_scales_per_axis() {
123        let line_px = PhysicalPosition { x: 2.0, y: 10.0 };
124        let page_px = PhysicalPosition { x: 100.0, y: 100.0 };
125        assert_eq!(
126            ScrollDelta::LineDelta(3.0, -1.0).to_pixel_delta(line_px, page_px),
127            PhysicalPosition { x: 6.0, y: -10.0 }
128        );
129    }
130
131    #[test]
132    fn page_delta_scales_per_axis() {
133        let line_px = PhysicalPosition { x: 10.0, y: 10.0 };
134        let page_px = PhysicalPosition { x: 4.0, y: 20.0 };
135        assert_eq!(
136            ScrollDelta::PageDelta(0.5, -2.0).to_pixel_delta(line_px, page_px),
137            PhysicalPosition { x: 2.0, y: -40.0 }
138        );
139    }
140
141    #[test]
142    fn into_pixel_delta_wraps() {
143        let out = ScrollDelta::LineDelta(1.0, 1.0).into_pixel_delta(
144            PhysicalPosition { x: 5.0, y: 6.0 },
145            PhysicalPosition { x: 100.0, y: 100.0 },
146        );
147        assert_eq!(
148            out,
149            ScrollDelta::PixelDelta(PhysicalPosition { x: 5.0, y: 6.0 })
150        );
151    }
152}