kozan_core/input/wheel.rs
1//! Wheel (scroll) event types.
2//!
3//! Like Chrome's `WebMouseWheelEvent` — a dedicated struct for scroll input.
4//! Distinguishes between pixel-precise scrolling (trackpad) and line-based
5//! scrolling (mouse wheel) via [`WheelDelta`].
6//!
7//! Chrome equivalent: `third_party/blink/public/common/input/web_mouse_wheel_event.h`
8
9use std::time::Instant;
10
11use super::modifiers::Modifiers;
12
13/// Mouse wheel or trackpad scroll event.
14///
15/// Chrome equivalent: `WebMouseWheelEvent`.
16///
17/// The cursor position is included because scroll events should be
18/// dispatched to the element under the cursor (like Chrome's scroll targeting).
19#[derive(Debug, Clone, Copy)]
20pub struct WheelEvent {
21 /// Cursor X position in physical pixels, relative to view origin.
22 pub x: f64,
23 /// Cursor Y position in physical pixels, relative to view origin.
24 pub y: f64,
25 /// Scroll amount and type.
26 pub delta: WheelDelta,
27 /// Modifier keys and mouse button state at the time of this event.
28 pub modifiers: Modifiers,
29 /// When this event was received from the OS.
30 pub timestamp: Instant,
31}
32
33/// Scroll delta — distinguishes scroll source.
34///
35/// Chrome equivalent: `WebMouseWheelEvent::delta_x/y` +
36/// `has_precise_scrolling_deltas` flag.
37///
38/// Mouse wheels produce line deltas (discrete ticks).
39/// Trackpads produce pixel deltas (smooth, precise).
40/// The distinction matters for scroll behavior — line deltas get multiplied
41/// by a line height, pixel deltas are used directly.
42#[derive(Debug, Clone, Copy, PartialEq)]
43pub enum WheelDelta {
44 /// Mouse wheel: discrete line ticks. Positive = scroll up/left.
45 /// Chrome: `delta_x/y` when `has_precise_scrolling_deltas` is false.
46 Lines(f32, f32),
47
48 /// Trackpad: precise pixel offset. Positive = scroll up/left.
49 /// Chrome: `delta_x/y` when `has_precise_scrolling_deltas` is true.
50 Pixels(f64, f64),
51}
52
53/// Chrome: `kPixelsPerLineStep = 40`. Blink default for mouse wheel.
54const PIXELS_PER_LINE: f64 = 40.0;
55
56impl WheelDelta {
57 /// Raw horizontal component (lines or pixels, unscaled).
58 #[must_use]
59 pub fn dx(&self) -> f64 {
60 match self {
61 WheelDelta::Lines(x, _) => *x as f64,
62 WheelDelta::Pixels(x, _) => *x,
63 }
64 }
65
66 /// Raw vertical component (lines or pixels, unscaled).
67 #[must_use]
68 pub fn dy(&self) -> f64 {
69 match self {
70 WheelDelta::Lines(_, y) => *y as f64,
71 WheelDelta::Pixels(_, y) => *y,
72 }
73 }
74
75 /// Horizontal delta in CSS pixels. Lines are scaled by `PIXELS_PER_LINE`.
76 #[must_use]
77 pub fn px_dx(&self) -> f32 {
78 match self {
79 WheelDelta::Lines(x, _) => (*x as f64 * PIXELS_PER_LINE) as f32,
80 WheelDelta::Pixels(x, _) => *x as f32,
81 }
82 }
83
84 /// Vertical delta in CSS pixels. Lines are scaled by `PIXELS_PER_LINE`.
85 #[must_use]
86 pub fn px_dy(&self) -> f32 {
87 match self {
88 WheelDelta::Lines(_, y) => (*y as f64 * PIXELS_PER_LINE) as f32,
89 WheelDelta::Pixels(_, y) => *y as f32,
90 }
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn wheel_delta_lines() {
100 let delta = WheelDelta::Lines(0.0, -3.0);
101 assert_eq!(delta.dx(), 0.0);
102 assert_eq!(delta.dy(), -3.0);
103 }
104
105 #[test]
106 fn wheel_delta_pixels() {
107 let delta = WheelDelta::Pixels(10.5, -20.0);
108 assert_eq!(delta.dx(), 10.5);
109 assert_eq!(delta.dy(), -20.0);
110 }
111
112 #[test]
113 fn wheel_event_carries_position_and_modifiers() {
114 let evt = WheelEvent {
115 x: 300.0,
116 y: 400.0,
117 delta: WheelDelta::Lines(0.0, -1.0),
118 modifiers: Modifiers::EMPTY.with_shift(),
119 timestamp: Instant::now(),
120 };
121 assert!(evt.modifiers.shift());
122 assert_eq!(evt.x, 300.0);
123 }
124}