Skip to main content

azul_core/
sensors.rs

1//! POD types for the motion-sensor surface
2//! (SUPER_PLAN_2 §1 feature 5 + research/03 §"Feature 5").
3//!
4//! The three raw sensors apps want — accelerometer, gyroscope,
5//! magnetometer — each delivered as an `(x, y, z)` triple in the sensor's
6//! natural unit. Defined here in `azul-core` so the manager + accessors
7//! cross the FFI without `azul-layout` being a dependency. The stateful
8//! side lives in `azul_layout::managers::sensors::SensorManager`.
9//!
10//! Coordinate frame (research/03 §coordinate-frame): right-handed,
11//! +X right, +Y up, +Z out of the screen toward the user, in the device's
12//! default-portrait frame (iOS keeps the device frame regardless of UI
13//! orientation; Android auto-rotates only fused sensors). v1 reports the
14//! raw device frame.
15
16/// Which motion sensor a [`SensorReading`] came from.
17#[repr(C)]
18#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
19pub enum SensorKind {
20    /// Linear acceleration including gravity, in **m/s²**
21    /// (iOS `CMAccelerometerData` ×9.80665, Android `TYPE_ACCELEROMETER`).
22    Accelerometer,
23    /// Angular velocity, in **rad/s** (iOS `CMGyroData`, Android
24    /// `TYPE_GYROSCOPE`).
25    Gyroscope,
26    /// Geomagnetic field, in **µT** (iOS `magneticField`, Android
27    /// `TYPE_MAGNETIC_FIELD`).
28    Magnetometer,
29    // APPENDED at the end for ABI stability. The three above are the hard
30    // ones — they need real per-OS backends and have them. Most of what
31    // follows is DERIVED by the OS from those three, which is exactly why it
32    // is worth exposing rather than making every app redo the fusion badly.
33    /// Device orientation as a unit quaternion, x/y/z carrying the vector
34    /// part (Android `TYPE_ROTATION_VECTOR`, iOS `CMAttitude.quaternion`).
35    ///
36    /// The OS fuses accelerometer, gyroscope and magnetometer to produce it,
37    /// with drift correction an app cannot reproduce from the raw three.
38    RotationVector,
39    /// Gravity alone, in **m/s²** — the accelerometer with device motion
40    /// removed (Android `TYPE_GRAVITY`, iOS `CMDeviceMotion.gravity`).
41    Gravity,
42    /// Device motion alone, in **m/s²** — the accelerometer with gravity
43    /// removed (Android `TYPE_LINEAR_ACCELERATION`,
44    /// iOS `CMDeviceMotion.userAcceleration`).
45    ///
46    /// `Gravity` and this always sum to `Accelerometer`; they are separate
47    /// kinds because the split is what the OS's fusion buys you.
48    LinearAcceleration,
49    /// Illuminance in **lux**, in `x`. `y`/`z` unused.
50    ///
51    /// The signal behind "adapt to a dark room" — a UI dimming itself,
52    /// a camera view raising exposure.
53    AmbientLight,
54    /// Proximity in **cm**, in `x`. `y`/`z` unused. The RAW distance, where
55    /// the platform reports one (Android, Linux iio).
56    ///
57    /// Many phone sensors are binary and report only their maximum range or
58    /// `0.0`; the TYPED answer - [`Proximity::Near`], [`Proximity::Far`] or a
59    /// [`Proximity::Distance`] - is `CallbackInfo::get_proximity`, which is
60    /// also the only form the boolean sensors (iOS, Windows `IsDetected`)
61    /// can fill (8e-i-a-i, 8e-i-a-ii).
62    Proximity,
63    /// Atmospheric pressure in **hPa**, in `x`. `y`/`z` unused. Used for
64    /// relative altitude, which GPS gives poorly.
65    Barometer,
66    /// Cumulative step count since boot, in `x`. `y`/`z` unused.
67    ///
68    /// Monotonic and NOT resettable — an app takes differences against its
69    /// own baseline rather than expecting it to start at zero.
70    StepCounter,
71    /// Foldable hinge angle in **degrees**, in `x`: `0.0` fully closed,
72    /// `180.0` flat. `y`/`z` unused.
73    ///
74    /// A LAYOUT input more than a sensor. Android exposes it as
75    /// `TYPE_HINGE_ANGLE` and the web as `DevicePosture`, and it is the only
76    /// way to tell a book-posture fold from a laptop-posture one — which
77    /// decides whether a two-pane layout should split across the crease.
78    HingeAngle,
79}
80
81impl SensorKind {
82    /// How many kinds exist — the length of a slot array indexed by
83    /// [`Self::slot`].
84    pub const COUNT: usize = 11;
85
86    /// Dense index for this kind, for a fixed-size slot array.
87    ///
88    /// An array rather than one named field per kind: the set grew from 3 to
89    /// 11 and would have needed a new field, two new match arms and a new
90    /// accessor each time. Indexing keeps adding a kind to one line.
91    #[must_use]
92    pub const fn slot(self) -> usize {
93        match self {
94            Self::Accelerometer => 0,
95            Self::Gyroscope => 1,
96            Self::Magnetometer => 2,
97            Self::RotationVector => 3,
98            Self::Gravity => 4,
99            Self::LinearAcceleration => 5,
100            Self::AmbientLight => 6,
101            Self::Proximity => 7,
102            Self::Barometer => 8,
103            Self::StepCounter => 9,
104            Self::HingeAngle => 10,
105        }
106    }
107}
108
109
110/// One `(x, y, z)` sample from a motion sensor. Units depend on
111/// [`SensorReading::kind`] (see [`SensorKind`]). All POD / `Copy`.
112#[repr(C)]
113#[derive(Debug, Clone, Copy, PartialEq)]
114pub struct SensorReading {
115    /// Which sensor produced this reading.
116    pub kind: SensorKind,
117    /// X axis (device frame: right), in the kind's unit.
118    pub x: f32,
119    /// Y axis (device frame: up), in the kind's unit.
120    pub y: f32,
121    /// Z axis (device frame: out of screen toward user), in the kind's unit.
122    pub z: f32,
123    /// Monotonic timestamp in milliseconds since program start.
124    pub timestamp_ms: u64,
125}
126
127impl SensorReading {
128    /// The magnitude of the `(x, y, z)` vector — e.g. total acceleration
129    /// (≈9.81 at rest for the accelerometer) or field strength.
130    #[allow(clippy::suboptimal_flops)] // mul_add not guaranteed faster/available without target +fma; keep explicit a*b+c
131    #[must_use]
132    pub fn magnitude(&self) -> f32 {
133        (self.x * self.x + self.y * self.y + self.z * self.z).sqrt()
134    }
135}
136
137// FFI Option wrapper for `CallbackInfo::get_sensor_reading(kind) ->
138// Option<SensorReading>` (mirrors `OptionLocationFix`).
139impl_option!(
140    SensorReading,
141    OptionSensorReading,
142    [Debug, Clone, Copy, PartialEq]
143);
144
145/// The length unit of a [`ProximityDistance`] - the sensor's NATIVE unit,
146/// kept rather than converted so no precision is invented: Windows reports
147/// millimetres, Android centimetres, Linux iio metres.
148#[repr(C)]
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
150pub enum DistanceUnit {
151    Millimeters,
152    Centimeters,
153    Meters,
154}
155
156/// A measured distance to the nearest object, from a RANGING proximity
157/// sensor (8e-i-a-i, 8e-i-a-ii).
158#[repr(C)]
159#[derive(Debug, Clone, Copy, PartialEq)]
160pub struct ProximityDistance {
161    pub value: f32,
162    pub unit: DistanceUnit,
163}
164
165impl ProximityDistance {
166    #[must_use]
167    pub fn in_millimeters(&self) -> f32 {
168        match self.unit {
169            DistanceUnit::Millimeters => self.value,
170            DistanceUnit::Centimeters => self.value * 10.0,
171            DistanceUnit::Meters => self.value * 1000.0,
172        }
173    }
174
175    #[must_use]
176    pub fn in_centimeters(&self) -> f32 {
177        self.in_millimeters() / 10.0
178    }
179
180    #[must_use]
181    pub fn in_meters(&self) -> f32 {
182        self.in_millimeters() / 1000.0
183    }
184}
185
186/// What the proximity sensor says (8e-i-a-i, 8e-i-a-ii; USER RULING
187/// 2026-09-03): a proper model instead of a distance with a made-up value
188/// for "far".
189///
190/// Most phone sensors are BINARY - iOS exposes only
191/// `UIDevice.proximityState`, Windows only `IsDetected` unless the sensor
192/// also ranges - and answer [`Self::Near`] / [`Self::Far`]. A ranging sensor
193/// answers [`Self::Distance`] in its native unit, and whether that is "near"
194/// is the app's call: a distance is a measurement, not a verdict.
195#[repr(C, u8)]
196#[derive(Debug, Clone, Copy, PartialEq)]
197pub enum Proximity {
198    /// Something is close - a phone at the ear, a hand over the sensor.
199    Near,
200    /// Nothing within the sensor's range.
201    Far,
202    /// A measured distance (ranging sensors only).
203    Distance(ProximityDistance),
204}
205
206impl Proximity {
207    /// `Some(true)` near, `Some(false)` far, `None` for a distance - which
208    /// only the app can turn into a verdict, against its own threshold.
209    #[must_use]
210    pub const fn is_near(&self) -> Option<bool> {
211        match self {
212            Self::Near => Some(true),
213            Self::Far => Some(false),
214            Self::Distance(_) => None,
215        }
216    }
217}
218
219// FFI Option wrapper for `CallbackInfo::get_proximity() -> Option<Proximity>`.
220impl_option!(Proximity, OptionProximity, [Debug, Clone, Copy, PartialEq]);
221
222#[cfg(test)]
223#[path = "sensors_test.rs"]
224mod sensors_test;