1#![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#[derive(Debug, Clone, PartialEq, Eq, Hash)]
17pub struct Touch {
18    pub id: u8,
23
24    pub location: TouchPoint,
26
27    pub phase: Phase,
29
30    pub tool: Tool,
32}
33
34impl Touch {
35    #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49pub enum Phase {
50    Started,
52    Moved,
54    Ended,
56    Cancelled,
58    Hovering(Option<u16>),
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
65pub enum Tool {
66    Finger,
68    Pointer {
70        button: PointerButton,
72    },
73    Stylus {
75        pressure: Option<u16>,
77        tilt: Option<UnitAngle>,
81        azimuth: Option<UnitAngle>,
85    },
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
90pub enum PointerButton {
91    None,
93    Primary,
95    Secondary,
97    Tertiary,
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
105pub struct UnitAngle(fixed::types::U1F15);
106
107impl UnitAngle {
108    #[must_use]
115    pub fn from_pi_radians(value: impl ToFixed) -> Self {
116        UnitAngle(value.wrapping_to_fixed())
117    }
118
119    #[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    #[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    #[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    #[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}