Skip to main content

a7n_core/input/
scroll.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2
3use a7n_proto::v1::{ScrollEvent, ScrollUnit};
4
5use super::MotionScale;
6
7/// Receiver-side scroll scaling with independent pixel and line remainders.
8#[derive(Debug)]
9pub struct ScrollScale {
10    pixels: MotionScale,
11    lines: MotionScale,
12}
13
14impl ScrollScale {
15    /// Speed is validated at the configuration boundary; reversal applies to both axes.
16    #[must_use]
17    pub fn new(speed: f64, reverse: bool) -> Self {
18        let speed = if reverse { -speed } else { speed };
19        Self {
20            pixels: MotionScale::new(speed),
21            lines: MotionScale::new(speed),
22        }
23    }
24
25    #[allow(clippy::cast_precision_loss)] // ScrollEvent uses f32; whole deltas saturate at i32 bounds.
26    pub fn apply(&mut self, event: &mut ScrollEvent) {
27        let scale = match event.unit() {
28            ScrollUnit::Pixel => &mut self.pixels,
29            ScrollUnit::Line => &mut self.lines,
30            ScrollUnit::Unspecified => return,
31        };
32        event.dx =
33            MotionScale::axis(f64::from(event.dx), scale.speed, &mut scale.remainder.0) as f32;
34        event.dy =
35            MotionScale::axis(f64::from(event.dy), scale.speed, &mut scale.remainder.1) as f32;
36    }
37}
38
39#[cfg(test)]
40mod tests {
41    use super::*;
42
43    #[test]
44    fn apply_fractional_scroll_retains_remainder_per_unit_and_reverses_axes() {
45        // Arrange
46        let mut scale = ScrollScale::new(0.25, true);
47        let mut output = Vec::new();
48
49        // Act
50        for _ in 0..4 {
51            for unit in [ScrollUnit::Line, ScrollUnit::Pixel] {
52                let mut event = ScrollEvent {
53                    dx: 1.0,
54                    dy: -1.0,
55                    unit: unit.into(),
56                };
57                scale.apply(&mut event);
58                output.push((event.dx, event.dy));
59            }
60        }
61
62        // Assert
63        assert_eq!(
64            output,
65            [
66                (0.0, 0.0),
67                (0.0, 0.0),
68                (0.0, 0.0),
69                (0.0, 0.0),
70                (0.0, 0.0),
71                (0.0, 0.0),
72                (-1.0, 1.0),
73                (-1.0, 1.0)
74            ]
75        );
76    }
77
78    #[test]
79    fn apply_non_finite_scroll_preserves_pending_fraction() {
80        // Arrange
81        let mut scale = ScrollScale::new(0.5, false);
82        let mut event = ScrollEvent {
83            dx: 1.0,
84            dy: 1.0,
85            unit: ScrollUnit::Line.into(),
86        };
87        scale.apply(&mut event);
88
89        // Act
90        let mut invalid = ScrollEvent {
91            dx: f32::NAN,
92            dy: f32::INFINITY,
93            unit: ScrollUnit::Line.into(),
94        };
95        scale.apply(&mut invalid);
96        event.dx = 1.0;
97        event.dy = 1.0;
98        scale.apply(&mut event);
99
100        // Assert
101        assert_eq!((invalid.dx, invalid.dy), (0.0, 0.0));
102        assert_eq!((event.dx, event.dy), (1.0, 1.0));
103    }
104}