Skip to main content

azul_layout/managers/
sensors.rs

1//! Sensor manager — cross-platform state for the motion-sensor surface
2//! (`SUPER_PLAN_2` §1 feature 5 + research/03).
3//!
4//! Continuous + push-driven, like geolocation:
5//!
6//! - The **platform backend** (`dll/src/desktop/extra/sensors/<plat>.rs`)
7//!   subscribes to `CoreMotion` (`CMMotionManager`) / Android `SensorManager`
8//!   and calls [`push_sensor_reading`] on every sample (arbitrary thread).
9//! - The dll **layout pass** drains the channel via
10//!   [`drain_sensor_readings`] and folds each into the manager through
11//!   [`SensorManager::set_reading`].
12//! - **Callbacks** read `reading(kind)` synchronously (via
13//!   `CallbackInfo::get_sensor_reading`) to drive tilt / shake / compass UI.
14//!
15//! One reading slot per [`SensorKind`]. No platform deps
16//! (`SUPER_PLAN_2` §0.5); the channel mirrors `geolocation.rs` verbatim.
17
18use alloc::vec::Vec;
19
20use azul_core::dom::DomNodeId;
21use azul_core::events::{
22    EventData, EventProvider, EventSource as CoreEventSource, EventType, SyntheticEvent,
23};
24use azul_core::task::Instant;
25pub use azul_core::sensors::{SensorKind, SensorReading};
26
27/// Cross-platform sensor state. One per `App` — the OS exposes a single
28/// per-process sensor subscription, not per-window.
29#[derive(Copy, Debug, Clone, PartialEq, Default)]
30pub struct SensorManager {
31    /// Latest accelerometer reading (m/s²), or `None` until a sample arrives.
32    pub accelerometer: Option<SensorReading>,
33    /// Latest gyroscope reading (rad/s).
34    pub gyroscope: Option<SensorReading>,
35    /// Latest magnetometer reading (µT).
36    pub magnetometer: Option<SensorReading>,
37    /// `true` when a reading advanced since the last event-pass drain. Set by
38    /// [`set_reading`](Self::set_reading), read by the `EventProvider` impl,
39    /// cleared by [`clear_pending_event`](Self::clear_pending_event).
40    pub pending_event: bool,
41    /// `true` while any node in the current layout registers a
42    /// `SensorChanged` callback (Hover or Window filter). Recomputed on every
43    /// relayout by the DOM walk in `shell2::common::layout`; the capability
44    /// pump polls the platform sensor backend only while this is set
45    /// (MWA-A1 arming signal — no listeners, no polling, no timer).
46    pub has_listeners: bool,
47}
48
49impl SensorManager {
50    #[must_use] pub fn new() -> Self {
51        Self::default()
52    }
53
54    /// Latest reading for `kind`, or `None` if no backend has delivered one.
55    #[must_use] pub const fn reading(&self, kind: SensorKind) -> Option<SensorReading> {
56        match kind {
57            SensorKind::Accelerometer => self.accelerometer,
58            SensorKind::Gyroscope => self.gyroscope,
59            SensorKind::Magnetometer => self.magnetometer,
60        }
61    }
62
63    /// Apply a reading the backend delivered. Returns `true` if it advanced
64    /// (bit-pattern different from the previous, so missing-as-`NaN` axes
65    /// don't make every sample look "changed").
66    pub fn set_reading(&mut self, reading: SensorReading) -> bool {
67        let slot = match reading.kind {
68            SensorKind::Accelerometer => &mut self.accelerometer,
69            SensorKind::Gyroscope => &mut self.gyroscope,
70            SensorKind::Magnetometer => &mut self.magnetometer,
71        };
72        let changed = slot.as_mut().is_none_or(|prev| !reading_bitwise_eq(prev, &reading));
73        *slot = Some(reading);
74        if changed {
75            self.pending_event = true;
76        }
77        changed
78    }
79
80    /// Clear the pending-event flag. The dll calls this after the event pass
81    /// has collected the `SensorChanged` event (mirrors `clear_changeset`).
82    pub const fn clear_pending_event(&mut self) {
83        self.pending_event = false;
84    }
85
86    /// Relayout walk reports whether any node listens for `SensorChanged`.
87    pub const fn set_has_listeners(&mut self, has: bool) {
88        self.has_listeners = has;
89    }
90
91    /// `true` while the capability pump should poll the sensor backend.
92    #[must_use] pub const fn has_listeners(&self) -> bool {
93        self.has_listeners
94    }
95}
96
97impl EventProvider for SensorManager {
98    /// Yield a window-level `SensorChanged` event when a reading advanced
99    /// since the last drain (target = root; read the value via
100    /// `CallbackInfo::get_sensor_reading` inside the callback).
101    fn get_pending_events(&self, timestamp: Instant) -> Vec<SyntheticEvent> {
102        if self.pending_event {
103            alloc::vec![SyntheticEvent::new(
104                EventType::SensorChanged,
105                CoreEventSource::User,
106                DomNodeId::ROOT,
107                timestamp,
108                EventData::None,
109            )]
110        } else {
111            Vec::new()
112        }
113    }
114}
115
116fn reading_bitwise_eq(a: &SensorReading, b: &SensorReading) -> bool {
117    a.kind == b.kind
118        && a.x.to_bits() == b.x.to_bits()
119        && a.y.to_bits() == b.y.to_bits()
120        && a.z.to_bits() == b.z.to_bits()
121        && a.timestamp_ms == b.timestamp_ms
122}
123
124// ────────── Async reading channel (platform backend → manager) ─────────
125//
126// CoreMotion / Android `SensorManager` deliver on an arbitrary thread with
127// no handle to the live `SensorManager` (inside the window's
128// `LayoutWindow`). The backend parks each reading here; the layout pass
129// drains it and applies the latest per kind. Pure Rust — no platform
130// dependency (SUPER_PLAN_2 §0.5). Mirrors the geolocation fix channel.
131
132static PENDING_READINGS: std::sync::Mutex<Vec<SensorReading>> =
133    std::sync::Mutex::new(Vec::new());
134
135/// Park a sensor reading delivered by a platform backend (in the dll).
136/// Thread-safe; poison-recovering.
137pub fn push_sensor_reading(reading: SensorReading) {
138    let mut q = PENDING_READINGS.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
139    q.push(reading);
140}
141
142/// Drain every reading parked by [`push_sensor_reading`], in arrival order.
143/// Called once per layout pass; the caller applies them through
144/// [`SensorManager::set_reading`] (the last per kind wins).
145pub fn drain_sensor_readings() -> Vec<SensorReading> {
146    let mut q = PENDING_READINGS.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
147    core::mem::take(&mut *q)
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn listener_flag_gates_polling_decision() {
156        let mut mgr = SensorManager::new();
157        assert!(!mgr.has_listeners(), "no listeners until the relayout walk reports some");
158        mgr.set_has_listeners(true);
159        assert!(mgr.has_listeners());
160        mgr.set_has_listeners(false);
161        assert!(!mgr.has_listeners());
162    }
163
164    fn r(kind: SensorKind, x: f32, y: f32, z: f32) -> SensorReading {
165        SensorReading {
166            kind,
167            x,
168            y,
169            z,
170            timestamp_ms: 0,
171        }
172    }
173
174    #[test]
175    fn manager_defaults_to_no_readings() {
176        let mgr = SensorManager::new();
177        assert_eq!(mgr.reading(SensorKind::Accelerometer), None);
178        assert_eq!(mgr.reading(SensorKind::Gyroscope), None);
179        assert_eq!(mgr.reading(SensorKind::Magnetometer), None);
180    }
181
182    #[test]
183    fn set_reading_routes_by_kind_and_flags_change() {
184        let mut mgr = SensorManager::new();
185        assert!(mgr.set_reading(r(SensorKind::Accelerometer, 0.0, 0.0, 9.81)));
186        // Only the accelerometer slot is filled.
187        assert!(mgr.reading(SensorKind::Accelerometer).is_some());
188        assert_eq!(mgr.reading(SensorKind::Gyroscope), None);
189        // Same value again — no change.
190        assert!(!mgr.set_reading(r(SensorKind::Accelerometer, 0.0, 0.0, 9.81)));
191        // Different value — change.
192        assert!(mgr.set_reading(r(SensorKind::Accelerometer, 1.0, 0.0, 9.81)));
193        // A different kind fills its own slot.
194        assert!(mgr.set_reading(r(SensorKind::Gyroscope, 0.1, 0.0, 0.0)));
195        assert_eq!(
196            mgr.reading(SensorKind::Gyroscope).map(|r| r.x),
197            Some(0.1)
198        );
199    }
200
201    #[test]
202    fn magnitude_of_resting_accelerometer() {
203        let g = r(SensorKind::Accelerometer, 0.0, 0.0, 9.81);
204        assert!((g.magnitude() - 9.81).abs() < 1e-4);
205    }
206
207    #[test]
208    fn readings_round_trip_through_manager() {
209        drop(drain_sensor_readings());
210
211        push_sensor_reading(r(SensorKind::Accelerometer, 1.0, 2.0, 3.0));
212        push_sensor_reading(r(SensorKind::Accelerometer, 4.0, 5.0, 6.0)); // last wins per kind
213        push_sensor_reading(r(SensorKind::Magnetometer, 20.0, 0.0, 40.0));
214        let drained = drain_sensor_readings();
215        assert_eq!(drained.len(), 3, "all parked readings drain in order");
216
217        let mut mgr = SensorManager::new();
218        for reading in &drained {
219            mgr.set_reading(*reading);
220        }
221        assert_eq!(
222            mgr.reading(SensorKind::Accelerometer).map(|r| r.x),
223            Some(4.0),
224            "the last accelerometer reading wins"
225        );
226        assert_eq!(
227            mgr.reading(SensorKind::Magnetometer).map(|r| r.z),
228            Some(40.0)
229        );
230
231        assert!(drain_sensor_readings().is_empty());
232    }
233}