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