embedded_touch/
lib.rs

1//! Common traits and types for touch screen drivers (and mice)
2
3#![no_std]
4
5use core::{
6    fmt::Debug,
7    ops::{Add, AddAssign, Sub, SubAssign},
8};
9
10use fixed::{traits::ToFixed, types::U17F15};
11use fixed_macro::types::{I17F15, U17F15};
12
13pub mod traits;
14
15/// Represents a single touch point on the screen
16#[derive(Debug, Clone, PartialEq, Eq, Hash)]
17pub struct Touch {
18    /// Unique ID for tracking this touch point across frames
19    ///
20    /// The ID should remain stable for a given finger/stylus while it remains in contact
21    /// but can be reused for new touches after sending [`Phase::Ended`] or [`Phase::Cancelled`]
22    pub id: u8,
23
24    /// Coordinates of the interaction in units of screen pixels
25    pub location: TouchPoint,
26
27    /// Current phase of this touch interaction
28    pub phase: Phase,
29
30    /// The tool used for this touch point
31    pub tool: Tool,
32}
33
34impl Touch {
35    /// Create a new touch point
36    #[must_use]
37    pub fn new(id: u8, location: TouchPoint, phase: Phase, tool: Tool) -> Self {
38        Self {
39            id,
40            location,
41            phase,
42            tool,
43        }
44    }
45}
46
47/// Phase of a touch interaction
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49pub enum Phase {
50    /// Touch just started
51    Started,
52    /// Touch moved from previous position
53    Moved,
54    /// Touch ended normally
55    Ended,
56    /// Touch was cancelled (e.g., palm rejection triggered)
57    Cancelled,
58    /// Touch is hovering above the screen without contact, with an optional
59    /// proximity (implementation-specific units)
60    Hovering(Option<u16>),
61}
62
63/// Tool/instrument used for touch interaction
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
65pub enum Tool {
66    /// Finger or unknown tool
67    Finger,
68    /// Virtual pointing device (e.g., mouse cursor)
69    Pointer {
70        /// The button pressed on the virtual pointer
71        button: PointerButton,
72    },
73    /// Passive or active stylus
74    Stylus {
75        /// Pressure, in grams
76        pressure: Option<u16>,
77        /// Tilt angle
78        ///
79        /// 0 degrees is a vector normal to the screen, and 90 degrees is parallel to the screen.
80        tilt: Option<UnitAngle>,
81        /// Azimuth angle
82        ///
83        /// 0 degrees points up to the top of the screen in its default orientation.
84        azimuth: Option<UnitAngle>,
85    },
86}
87
88/// The button state of a virtual pointer device
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
90pub enum PointerButton {
91    /// No button pressed, e.g., mouse hover state
92    None,
93    /// Primary mouse button, typically left
94    Primary,
95    /// Secondary mouse button, typically right
96    Secondary,
97    /// Tertiary mouse button, typically middle or wheel
98    Tertiary,
99}
100
101/// An angle in the range [0, 2π) radians
102///
103/// The angle is stored as a [`fixed::types::U1F15`]
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
105pub struct UnitAngle(fixed::types::U1F15);
106
107impl UnitAngle {
108    /// Create a new angle from an angle in π * radians.
109    ///
110    /// Angles outside the range [0, 2) are wrapped.
111    ///
112    /// This method does not result in loss of precision if the argument is
113    /// [`fixed::types::U1F15`]
114    #[must_use]
115    pub fn from_pi_radians(value: impl ToFixed) -> Self {
116        UnitAngle(value.wrapping_to_fixed())
117    }
118
119    /// Create a new angle from an angle in radians
120    ///
121    /// Angles outside the range [0, 2π) are wrapped.
122    #[must_use]
123    pub fn from_radians(value: impl ToFixed) -> Self {
124        let fixed_radians = value.to_fixed::<fixed::types::U17F15>();
125        let pi_radians = fixed_radians / U17F15!(3.14159265359);
126        UnitAngle(pi_radians.wrapping_to_fixed())
127    }
128
129    /// Create a new angle from an angle in degrees
130    ///
131    /// Angles outside the range [0, 360) are wrapped.
132    #[must_use]
133    pub fn from_degrees(value: impl ToFixed) -> Self {
134        let fixed_degrees = value.to_fixed::<fixed::types::I17F15>();
135        let radians = fixed_degrees / I17F15!(180);
136        UnitAngle(radians.wrapping_to_fixed())
137    }
138
139    /// Returns the angle in π radians, in the range [0, 2)
140    ///
141    /// This method does not result in loss of precision from the original value.
142    #[must_use]
143    #[inline]
144    pub fn as_pi_radians(&self) -> fixed::types::U1F15 {
145        self.0
146    }
147
148    #[must_use]
149    #[inline]
150    pub fn as_radians_f32(&self) -> f32 {
151        (self.0.to_fixed::<U17F15>() * U17F15!(3.14159265359)).to_num::<f32>()
152    }
153
154    #[must_use]
155    #[inline]
156    pub fn as_degrees_f32(&self) -> f32 {
157        (self.0.to_fixed::<U17F15>() * U17F15!(180.0)).to_num::<f32>()
158    }
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
162pub struct TouchPoint {
163    pub x: i32,
164    pub y: i32,
165}
166
167impl TouchPoint {
168    /// Create a new touch point
169    #[must_use]
170    pub fn new(x: impl Into<i32>, y: impl Into<i32>) -> Self {
171        Self {
172            x: x.into(),
173            y: y.into(),
174        }
175    }
176}
177
178impl Add for TouchPoint {
179    type Output = Self;
180
181    fn add(self, rhs: Self) -> Self::Output {
182        TouchPoint {
183            x: self.x + rhs.x,
184            y: self.y + rhs.y,
185        }
186    }
187}
188
189impl AddAssign for TouchPoint {
190    fn add_assign(&mut self, rhs: Self) {
191        self.x += rhs.x;
192        self.y += rhs.y;
193    }
194}
195
196impl Sub for TouchPoint {
197    type Output = Self;
198
199    fn sub(self, rhs: Self) -> Self::Output {
200        TouchPoint {
201            x: self.x - rhs.x,
202            y: self.y - rhs.y,
203        }
204    }
205}
206
207impl SubAssign for TouchPoint {
208    fn sub_assign(&mut self, rhs: Self) {
209        self.x -= rhs.x;
210        self.y -= rhs.y;
211    }
212}
213
214impl core::ops::Neg for TouchPoint {
215    type Output = Self;
216    fn neg(self) -> Self {
217        Self {
218            x: -self.x,
219            y: -self.y,
220        }
221    }
222}
223
224#[cfg(test)]
225mod tests {
226    use core::f32;
227
228    use super::*;
229
230    #[test]
231    #[expect(clippy::cast_precision_loss)]
232    fn angle_from_pi_radians() {
233        let angle = UnitAngle::from_pi_radians(0.0);
234        assert_eq!(angle.as_pi_radians(), fixed::types::U1F15::from_num(0.0));
235        assert!(angle.as_radians_f32().abs() < 0.00001);
236        assert!(angle.as_degrees_f32().abs() < 0.00001);
237
238        for i in -8..8 {
239            let offset = (i * 2) as f32;
240            let angle = UnitAngle::from_pi_radians(1.0 + offset);
241            assert_eq!(angle.as_pi_radians(), fixed::types::U1F15::from_num(1.0));
242            assert!((angle.as_radians_f32() - 1.0 * f32::consts::PI).abs() < 0.00001);
243            assert!((angle.as_degrees_f32() - 180.0).abs() < 0.00001);
244        }
245    }
246
247    #[test]
248    #[expect(clippy::cast_precision_loss)]
249    fn sweep_360_degrees() {
250        for i in -1080..1080 {
251            let angle = UnitAngle::from_degrees(i);
252
253            let unit_degrees = (i + 360 * 20) % 360;
254            let radians = unit_degrees as f32 * f32::consts::PI / 180.0;
255
256            assert!(
257                (angle.as_degrees_f32() - unit_degrees as f32).abs() < 0.01,
258                "Expected {} to be nearly {unit_degrees}",
259                angle.as_degrees_f32()
260            );
261            assert!(
262                (angle.as_radians_f32() - radians).abs() < 0.001,
263                "Expected {}  to be nearly {radians}",
264                angle.as_radians_f32()
265            );
266        }
267    }
268}