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;