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}