Skip to main content

a7n_core/
input.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2
3//! Allocation-free cumulative pointer motion and desktop edge detection.
4
5use a7n_proto::seq::{delta, is_newer};
6use a7n_proto::v1::{Edge, Motion};
7
8use crate::layout::Corner;
9
10mod geometry;
11mod scroll;
12pub use geometry::{Bounds, landing_point};
13pub use scroll::ScrollScale;
14
15/// Maximum distance in pixels from an edge end for a diagonal crossing.
16pub const CORNER_BOX: i64 = 8;
17
18/// Receiver-side speed scaling, retaining subpixel motion between updates.
19#[derive(Debug)]
20pub struct MotionScale {
21    speed: f64,
22    remainder: (f64, f64),
23}
24
25impl MotionScale {
26    /// `speed` has already been validated at the configuration boundary.
27    #[must_use]
28    pub fn new(speed: f64) -> Self {
29        Self {
30            speed,
31            remainder: (0.0, 0.0),
32        }
33    }
34
35    pub fn apply(&mut self, dx: i32, dy: i32) -> (i32, i32) {
36        (
37            Self::axis(f64::from(dx), self.speed, &mut self.remainder.0),
38            Self::axis(f64::from(dy), self.speed, &mut self.remainder.1),
39        )
40    }
41
42    #[allow(clippy::cast_possible_truncation)] // Pixel deltas truncate toward zero and saturate at i32 bounds.
43    fn axis(delta: f64, speed: f64, remainder: &mut f64) -> i32 {
44        if !delta.is_finite() {
45            return 0;
46        }
47        let scaled = delta * speed + *remainder;
48        *remainder = scaled.fract();
49        scaled as i32
50    }
51}
52
53#[derive(Debug, Default)]
54pub struct MotionCounter {
55    seq: u32,
56    x: i32,
57    y: i32,
58}
59
60impl MotionCounter {
61    pub fn push(&mut self, dx: i32, dy: i32) -> Motion {
62        self.seq = self.seq.wrapping_add(1);
63        self.x = self.x.wrapping_add(dx);
64        self.y = self.y.wrapping_add(dy);
65        self.current()
66    }
67
68    /// The daemon supplies the route for this snapshot.
69    #[must_use]
70    pub fn current(&self) -> Motion {
71        Motion {
72            route: 0,
73            motion_seq: self.seq,
74            x: self.x,
75            y: self.y,
76        }
77    }
78
79    pub fn reset(&mut self) {
80        *self = Self::default();
81    }
82}
83
84#[derive(Debug, Default)]
85pub struct MotionTracker {
86    seq: u32,
87    x: i32,
88    y: i32,
89}
90
91impl MotionTracker {
92    pub fn apply(&mut self, seq: u32, x: i32, y: i32) -> Option<(i32, i32)> {
93        if !is_newer(seq, self.seq) {
94            return None;
95        }
96        Some(self.update(seq, x, y))
97    }
98
99    pub fn resync(&mut self, seq: u32, x: i32, y: i32) -> Option<(i32, i32)> {
100        if is_newer(seq, self.seq) || (seq == self.seq && (x, y) != (self.x, self.y)) {
101            return Some(self.update(seq, x, y));
102        }
103        None
104    }
105
106    pub fn reset(&mut self) {
107        *self = Self::default();
108    }
109
110    fn update(&mut self, seq: u32, x: i32, y: i32) -> (i32, i32) {
111        let movement = (delta(x, self.x), delta(y, self.y));
112        (self.seq, self.x, self.y) = (seq, x, y);
113        movement
114    }
115}
116
117/// Clamp a desktop point to its bounds, including negative origins.
118#[must_use]
119pub fn clamp_position(
120    (left, top, width, height): (i32, i32, u32, u32),
121    x: i32,
122    y: i32,
123) -> (i32, i32) {
124    (
125        x.clamp(left, left.saturating_add_unsigned(width.saturating_sub(1))),
126        y.clamp(top, top.saturating_add_unsigned(height.saturating_sub(1))),
127    )
128}
129
130/// Whether a desktop x coordinate falls within the notch's horizontal span.
131#[must_use]
132pub fn within_notch(notch: Bounds, x: i32) -> bool {
133    i64::from(x) >= i64::from(notch.x) && i64::from(x) < i64::from(notch.x) + i64::from(notch.width)
134}
135
136/// Detect outward motion at or beyond an edge; horizontal motion wins at corners.
137#[must_use]
138#[allow(clippy::cast_possible_truncation)] // Normalized fractions in 0..=1 fit f32.
139pub fn edge_push(
140    bounds @ (left, top, width, height): (i32, i32, u32, u32),
141    monitors: &[Bounds],
142    x: i32,
143    y: i32,
144    dx: i32,
145    dy: i32,
146) -> Option<(Edge, i32, f32, Bounds)> {
147    if width == 0 || height == 0 {
148        return None;
149    }
150    let monitor = geometry::nearest_monitor(bounds, monitors, x, y);
151    let (x, y) = clamp_position(monitor.rect(), x, y);
152    let (ml, mt, mw, mh) = monitor.rect();
153    let right = i64::from(ml) + i64::from(mw) - 1;
154    let bottom = i64::from(mt) + i64::from(mh) - 1;
155    let outside = |px, py| !monitors.iter().any(|m| m.contains(px, py));
156    let (edge, overshoot, position, start, length) =
157        if x == ml && dx < 0 && outside(i64::from(x) - 1, i64::from(y)) {
158            (Edge::Left, dx.saturating_neg(), y, top, height)
159        } else if i64::from(x) == right && dx > 0 && outside(right + 1, i64::from(y)) {
160            (Edge::Right, dx, y, top, height)
161        } else if y == mt && dy < 0 && outside(i64::from(x), i64::from(y) - 1) {
162            (Edge::Top, dy.saturating_neg(), x, left, width)
163        } else if i64::from(y) == bottom && dy > 0 && outside(i64::from(x), bottom + 1) {
164            (Edge::Bottom, dy, x, left, width)
165        } else {
166            return None;
167        };
168    let along =
169        (f64::from(position) - f64::from(start)) / f64::from(length.saturating_sub(1).max(1));
170    Some((edge, overshoot, along as f32, monitor))
171}
172
173/// Detect an edge push whose other axis points toward a nearby corner.
174#[must_use]
175pub fn corner_push(
176    bounds @ (left, top, width, height): (i32, i32, u32, u32),
177    monitors: &[Bounds],
178    x: i32,
179    y: i32,
180    dx: i32,
181    dy: i32,
182) -> Option<(Edge, Corner)> {
183    let (edge, _, _, monitor) = edge_push(bounds, monitors, x, y, dx, dy)?;
184    let (x, y) = clamp_position(monitor.rect(), x, y);
185    let on_bbox = match edge {
186        Edge::Left => monitor.x == left,
187        Edge::Right => {
188            i64::from(monitor.x) + i64::from(monitor.width) == i64::from(left) + i64::from(width)
189        }
190        Edge::Top => monitor.y == top,
191        Edge::Bottom => {
192            i64::from(monitor.y) + i64::from(monitor.height) == i64::from(top) + i64::from(height)
193        }
194        Edge::Unspecified => false,
195    };
196    if !on_bbox {
197        return None;
198    }
199    let right = i64::from(left) + i64::from(width) - 1;
200    let bottom = i64::from(top) + i64::from(height) - 1;
201    let (position, start, end, delta) = match edge {
202        Edge::Left | Edge::Right => (y, top, bottom, dy),
203        Edge::Top | Edge::Bottom => (x, left, right, dx),
204        Edge::Unspecified => return None,
205    };
206    let at_end = if delta < 0 && i64::from(position) - i64::from(start) <= CORNER_BOX {
207        false
208    } else if delta > 0 && end - i64::from(position) <= CORNER_BOX {
209        true
210    } else {
211        return None;
212    };
213    let corner = match (edge, at_end) {
214        (Edge::Left | Edge::Top, false) => Corner::TopLeft,
215        (Edge::Right, false) | (Edge::Top, true) => Corner::TopRight,
216        (Edge::Left, true) | (Edge::Bottom, false) => Corner::BottomLeft,
217        (Edge::Right | Edge::Bottom, true) => Corner::BottomRight,
218        (Edge::Unspecified, _) => return None,
219    };
220    Some((edge, corner))
221}
222
223#[cfg(test)]
224mod tests;