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

            
18
use alloc::vec::Vec;
19

            
20
use azul_core::dom::DomNodeId;
21
use azul_core::events::{
22
    EventData, EventProvider, EventSource as CoreEventSource, EventType, SyntheticEvent,
23
};
24
use azul_core::task::Instant;
25
pub 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)]
30
pub 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

            
49
impl SensorManager {
50
5767
    #[must_use] pub fn new() -> Self {
51
5767
        Self::default()
52
5767
    }
53

            
54
    /// Latest reading for `kind`, or `None` if no backend has delivered one.
55
144
    #[must_use] pub const fn reading(&self, kind: SensorKind) -> Option<SensorReading> {
56
144
        match kind {
57
52
            SensorKind::Accelerometer => self.accelerometer,
58
47
            SensorKind::Gyroscope => self.gyroscope,
59
45
            SensorKind::Magnetometer => self.magnetometer,
60
        }
61
144
    }
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
348
    pub fn set_reading(&mut self, reading: SensorReading) -> bool {
67
348
        let slot = match reading.kind {
68
133
            SensorKind::Accelerometer => &mut self.accelerometer,
69
116
            SensorKind::Gyroscope => &mut self.gyroscope,
70
99
            SensorKind::Magnetometer => &mut self.magnetometer,
71
        };
72
348
        let changed = slot.as_mut().is_none_or(|prev| !reading_bitwise_eq(prev, &reading));
73
348
        *slot = Some(reading);
74
348
        if changed {
75
216
            self.pending_event = true;
76
216
        }
77
348
        changed
78
348
    }
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
80
    pub const fn clear_pending_event(&mut self) {
83
80
        self.pending_event = false;
84
80
    }
85

            
86
    /// Relayout walk reports whether any node listens for `SensorChanged`.
87
9
    pub const fn set_has_listeners(&mut self, has: bool) {
88
9
        self.has_listeners = has;
89
9
    }
90

            
91
    /// `true` while the capability pump should poll the sensor backend.
92
14
    #[must_use] pub const fn has_listeners(&self) -> bool {
93
14
        self.has_listeners
94
14
    }
95
}
96

            
97
impl 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
99
    fn get_pending_events(&self, timestamp: Instant) -> Vec<SyntheticEvent> {
102
99
        if self.pending_event {
103
13
            alloc::vec![SyntheticEvent::new(
104
13
                EventType::SensorChanged,
105
13
                CoreEventSource::User,
106
                DomNodeId::ROOT,
107
13
                timestamp,
108
13
                EventData::None,
109
            )]
110
        } else {
111
86
            Vec::new()
112
        }
113
99
    }
114
}
115

            
116
15793
fn reading_bitwise_eq(a: &SensorReading, b: &SensorReading) -> bool {
117
15793
    a.kind == b.kind
118
5423
        && a.x.to_bits() == b.x.to_bits()
119
651
        && a.y.to_bits() == b.y.to_bits()
120
647
        && a.z.to_bits() == b.z.to_bits()
121
643
        && a.timestamp_ms == b.timestamp_ms
122
15793
}
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

            
132
static 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.
137
12
pub fn push_sensor_reading(reading: SensorReading) {
138
12
    let mut q = PENDING_READINGS.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
139
12
    q.push(reading);
140
12
}
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).
145
30
pub fn drain_sensor_readings() -> Vec<SensorReading> {
146
30
    let mut q = PENDING_READINGS.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
147
30
    core::mem::take(&mut *q)
148
30
}
149

            
150
#[cfg(test)]
151
mod tests {
152
    use super::*;
153

            
154
    #[test]
155
1
    fn listener_flag_gates_polling_decision() {
156
1
        let mut mgr = SensorManager::new();
157
1
        assert!(!mgr.has_listeners(), "no listeners until the relayout walk reports some");
158
1
        mgr.set_has_listeners(true);
159
1
        assert!(mgr.has_listeners());
160
1
        mgr.set_has_listeners(false);
161
1
        assert!(!mgr.has_listeners());
162
1
    }
163

            
164
8
    fn r(kind: SensorKind, x: f32, y: f32, z: f32) -> SensorReading {
165
8
        SensorReading {
166
8
            kind,
167
8
            x,
168
8
            y,
169
8
            z,
170
8
            timestamp_ms: 0,
171
8
        }
172
8
    }
173

            
174
    #[test]
175
1
    fn manager_defaults_to_no_readings() {
176
1
        let mgr = SensorManager::new();
177
1
        assert_eq!(mgr.reading(SensorKind::Accelerometer), None);
178
1
        assert_eq!(mgr.reading(SensorKind::Gyroscope), None);
179
1
        assert_eq!(mgr.reading(SensorKind::Magnetometer), None);
180
1
    }
181

            
182
    #[test]
183
1
    fn set_reading_routes_by_kind_and_flags_change() {
184
1
        let mut mgr = SensorManager::new();
185
1
        assert!(mgr.set_reading(r(SensorKind::Accelerometer, 0.0, 0.0, 9.81)));
186
        // Only the accelerometer slot is filled.
187
1
        assert!(mgr.reading(SensorKind::Accelerometer).is_some());
188
1
        assert_eq!(mgr.reading(SensorKind::Gyroscope), None);
189
        // Same value again — no change.
190
1
        assert!(!mgr.set_reading(r(SensorKind::Accelerometer, 0.0, 0.0, 9.81)));
191
        // Different value — change.
192
1
        assert!(mgr.set_reading(r(SensorKind::Accelerometer, 1.0, 0.0, 9.81)));
193
        // A different kind fills its own slot.
194
1
        assert!(mgr.set_reading(r(SensorKind::Gyroscope, 0.1, 0.0, 0.0)));
195
1
        assert_eq!(
196
1
            mgr.reading(SensorKind::Gyroscope).map(|r| r.x),
197
            Some(0.1)
198
        );
199
1
    }
200

            
201
    #[test]
202
1
    fn magnitude_of_resting_accelerometer() {
203
1
        let g = r(SensorKind::Accelerometer, 0.0, 0.0, 9.81);
204
1
        assert!((g.magnitude() - 9.81).abs() < 1e-4);
205
1
    }
206

            
207
    #[test]
208
1
    fn readings_round_trip_through_manager() {
209
1
        drop(drain_sensor_readings());
210

            
211
1
        push_sensor_reading(r(SensorKind::Accelerometer, 1.0, 2.0, 3.0));
212
1
        push_sensor_reading(r(SensorKind::Accelerometer, 4.0, 5.0, 6.0)); // last wins per kind
213
1
        push_sensor_reading(r(SensorKind::Magnetometer, 20.0, 0.0, 40.0));
214
1
        let drained = drain_sensor_readings();
215
1
        assert_eq!(drained.len(), 3, "all parked readings drain in order");
216

            
217
1
        let mut mgr = SensorManager::new();
218
4
        for reading in &drained {
219
3
            mgr.set_reading(*reading);
220
3
        }
221
1
        assert_eq!(
222
1
            mgr.reading(SensorKind::Accelerometer).map(|r| r.x),
223
            Some(4.0),
224
            "the last accelerometer reading wins"
225
        );
226
1
        assert_eq!(
227
1
            mgr.reading(SensorKind::Magnetometer).map(|r| r.z),
228
            Some(40.0)
229
        );
230

            
231
1
        assert!(drain_sensor_readings().is_empty());
232
1
    }
233
}
234

            
235
#[cfg(test)]
236
#[allow(clippy::float_cmp, clippy::eq_op)] // bit-exactness IS the invariant under test
237
mod autotest_generated {
238
    // Imported explicitly (not just via the `super::*` glob) so the trait
239
    // method `get_pending_events` and `Vec` resolve regardless of how the
240
    // parent's own imports are re-exported.
241
    use alloc::vec::Vec;
242

            
243
    use azul_core::{
244
        dom::DomNodeId,
245
        events::{EventProvider, EventType},
246
        task::{Instant, SystemTick},
247
    };
248

            
249
    use super::*;
250

            
251
    // ─────────────────────────── helpers ────────────────────────────
252
    //
253
    // NOTE — the process-global `PENDING_READINGS` channel
254
    // (`push_sensor_reading` / `drain_sensor_readings`) is deliberately NOT
255
    // exercised here. `tests::readings_round_trip_through_manager` above
256
    // asserts an *exact* drain count (`len() == 3`) on that same global, and
257
    // libtest runs the two modules' tests concurrently in one binary — any
258
    // push or drain from here could be observed by (or steal readings from)
259
    // that test and make it flake. The sibling `geolocation.rs` autotest
260
    // module leaves its identical channel alone for the same reason.
261

            
262
    const KINDS: [SensorKind; 3] = [
263
        SensorKind::Accelerometer,
264
        SensorKind::Gyroscope,
265
        SensorKind::Magnetometer,
266
    ];
267

            
268
    const fn r(kind: SensorKind, x: f32, y: f32, z: f32, timestamp_ms: u64) -> SensorReading {
269
        SensorReading {
270
            kind,
271
            x,
272
            y,
273
            z,
274
            timestamp_ms,
275
        }
276
    }
277

            
278
    /// A representative "real sample" — every field distinct and non-zero, so
279
    /// knocking out a single field flips exactly one comparison.
280
    const fn base() -> SensorReading {
281
        r(SensorKind::Accelerometer, 1.5, -2.25, 9.81, 1_234)
282
    }
283

            
284
    /// The shape `set_reading` documents as its reason for comparing bit
285
    /// patterns: a backend that reports "axis missing" as `NaN`.
286
    const fn nan_sample(kind: SensorKind) -> SensorReading {
287
        r(kind, f32::NAN, f32::NAN, f32::NAN, 7)
288
    }
289

            
290
    fn tick() -> Instant {
291
        Instant::Tick(SystemTick::new(0))
292
    }
293

            
294
    /// The smallest possible perturbation of a finite float: flip the lowest
295
    /// mantissa bit. The result is a *different* bit pattern by construction,
296
    /// so "did this sample advance?" must answer yes for it.
297
    fn ulp_flip(v: f32) -> f32 {
298
        f32::from_bits(v.to_bits() ^ 1)
299
    }
300

            
301
    /// Every field of a `SensorReading`, compared bit-for-bit — the property
302
    /// the manager promises when it stores a sample.
303
    fn bits_identical(a: &SensorReading, b: &SensorReading) -> bool {
304
        a.kind == b.kind
305
            && a.x.to_bits() == b.x.to_bits()
306
            && a.y.to_bits() == b.y.to_bits()
307
            && a.z.to_bits() == b.z.to_bits()
308
            && a.timestamp_ms == b.timestamp_ms
309
    }
310

            
311
    /// Float values that break naive `==` comparisons: both zeroes, both
312
    /// infinities, two distinct NaN bit patterns, the subnormal floor and the
313
    /// finite extremes.
314
    fn hostile_floats() -> [f32; 12] {
315
        [
316
            0.0,
317
            -0.0,
318
            1.0,
319
            -1.0,
320
            f32::MIN_POSITIVE,
321
            f32::from_bits(1), // smallest subnormal
322
            f32::MAX,
323
            f32::MIN,
324
            f32::INFINITY,
325
            f32::NEG_INFINITY,
326
            f32::NAN,
327
            f32::from_bits(0xffc0_0000), // negative NaN
328
        ]
329
    }
330

            
331
    // ───────────────────── SensorManager::new (constructor) ─────────────────
332

            
333
    /// no_panic + invariants_hold: a fresh manager has no readings, no pending
334
    /// event and no listeners — the "cold start, don't poll the backend" state.
335
    #[test]
336
    fn new_starts_cold_and_matches_default() {
337
        let mgr = SensorManager::new();
338
        for kind in KINDS {
339
            assert_eq!(mgr.reading(kind), None, "{kind:?} slot must start empty");
340
        }
341
        assert!(!mgr.pending_event, "no event may be pending before any sample");
342
        assert!(!mgr.has_listeners(), "polling must not be armed at construction");
343
        assert_eq!(mgr, SensorManager::default(), "new() must equal Default");
344
        assert_eq!(mgr, SensorManager::new(), "new() must be deterministic");
345
    }
346

            
347
    // ───────────────────────── reading (accessor) ───────────────────────────
348

            
349
    /// invariant: the three slots are independent — writing one kind must not
350
    /// make any other kind report a reading.
351
    #[test]
352
    fn reading_slots_are_independent_per_kind() {
353
        for written in KINDS {
354
            let mut mgr = SensorManager::new();
355
            assert!(mgr.set_reading(r(written, 1.0, 2.0, 3.0, 0)));
356
            for probed in KINDS {
357
                if probed == written {
358
                    assert_eq!(
359
                        mgr.reading(probed).map(|s| s.kind),
360
                        Some(written),
361
                        "{written:?} must land in its own slot, tagged with its kind"
362
                    );
363
                } else {
364
                    assert_eq!(
365
                        mgr.reading(probed),
366
                        None,
367
                        "writing {written:?} must not leak into the {probed:?} slot"
368
                    );
369
                }
370
            }
371
        }
372
    }
373

            
374
    /// invariant: `reading` hands back a *copy* (`SensorReading: Copy`), so a
375
    /// caller mutating it cannot corrupt the manager's stored sample.
376
    #[test]
377
    fn reading_returns_a_copy_not_an_alias() {
378
        let mut mgr = SensorManager::new();
379
        mgr.set_reading(base());
380
        let mut got = mgr.reading(SensorKind::Accelerometer).unwrap();
381
        got.x = -999.0;
382
        got.timestamp_ms = u64::MAX;
383
        assert_eq!(
384
            mgr.reading(SensorKind::Accelerometer),
385
            Some(base()),
386
            "mutating the returned copy must not write through to the manager"
387
        );
388
    }
389

            
390
    // ──────────────────────────── set_reading ───────────────────────────────
391

            
392
    /// The headline case from the doc comment: an all-`NaN` sample repeated
393
    /// verbatim must NOT look like it advanced. Under `PartialEq` every NaN
394
    /// compares unequal, so a naive `prev != new` would report a change on
395
    /// *every* sample and wake the event pass forever.
396
    #[test]
397
    fn repeated_nan_sample_does_not_advance() {
398
        for kind in KINDS {
399
            let mut mgr = SensorManager::new();
400
            let sample = nan_sample(kind);
401

            
402
            assert!(
403
                mgr.set_reading(sample),
404
                "{kind:?}: the first sample always advances (slot was empty)"
405
            );
406
            assert!(
407
                !mgr.set_reading(sample),
408
                "{kind:?}: an identical NaN sample must not be reported as changed"
409
            );
410
            assert!(
411
                !mgr.set_reading(sample),
412
                "{kind:?}: still no change on the third identical NaN sample"
413
            );
414

            
415
            // And the derived `PartialEq` really would have disagreed:
416
            let stored = mgr.reading(kind).unwrap();
417
            assert!(
418
                stored != sample,
419
                "{kind:?}: sanity — NaN fields make PartialEq report inequality, \
420
                 which is exactly why set_reading must compare bit patterns"
421
            );
422
        }
423
    }
424

            
425
    /// no_panic + invariant: the first sample into an empty slot always counts
426
    /// as an advance, even when every axis is NaN or infinite.
427
    #[test]
428
    fn first_sample_always_advances() {
429
        for kind in KINDS {
430
            for v in hostile_floats() {
431
                let mut mgr = SensorManager::new();
432
                assert!(
433
                    mgr.set_reading(r(kind, v, v, v, 0)),
434
                    "{kind:?}: first sample ({v:?}) must advance"
435
                );
436
                assert!(mgr.pending_event, "{kind:?}: an advance must arm the event");
437
            }
438
        }
439
    }
440

            
441
    /// round-trip: whatever is stored comes back bit-for-bit — NaN payloads,
442
    /// signed zeroes, infinities, subnormals, `u64::MAX` timestamps included.
443
    /// A resend of that exact sample is then correctly reported as *no* change.
444
    #[test]
445
    fn extremes_round_trip_bit_exactly_and_resend_is_idempotent() {
446
        for kind in KINDS {
447
            for x in hostile_floats() {
448
                for ts in [0_u64, 1, u64::MAX] {
449
                    let sample = r(kind, x, -x, x, ts);
450
                    let mut mgr = SensorManager::new();
451

            
452
                    assert!(mgr.set_reading(sample));
453
                    let stored = mgr.reading(kind).expect("just written");
454
                    assert!(
455
                        bits_identical(&stored, &sample),
456
                        "{kind:?}: stored sample must be bit-identical to the input \
457
                         (x = {x:?}, ts = {ts})"
458
                    );
459
                    assert!(
460
                        !mgr.set_reading(sample),
461
                        "{kind:?}: resending the identical sample must not advance \
462
                         (x = {x:?}, ts = {ts})"
463
                    );
464
                }
465
            }
466
        }
467
    }
468

            
469
    /// Every mutable field must be watched, down to the last mantissa bit.
470
    /// Perturbing x, y, z (by one ULP) or the timestamp alone has to register
471
    /// as an advance — a comparison that skipped one field would silently
472
    /// swallow that sensor axis.
473
    #[test]
474
    fn a_change_in_any_single_field_advances() {
475
        let mut with_x = base();
476
        with_x.x = ulp_flip(base().x);
477
        let mut with_y = base();
478
        with_y.y = ulp_flip(base().y);
479
        let mut with_z = base();
480
        with_z.z = ulp_flip(base().z);
481
        let mut with_ts = base();
482
        with_ts.timestamp_ms = base().timestamp_ms + 1;
483

            
484
        for (field, mutated) in [
485
            ("x", with_x),
486
            ("y", with_y),
487
            ("z", with_z),
488
            ("timestamp_ms", with_ts),
489
        ] {
490
            let mut mgr = SensorManager::new();
491
            assert!(mgr.set_reading(base()));
492
            assert!(
493
                mgr.set_reading(mutated),
494
                "a change in `{field}` alone must be reported as an advance"
495
            );
496
            assert!(
497
                !mgr.set_reading(mutated),
498
                "`{field}`: the mutated sample is now the previous one"
499
            );
500
            assert!(
501
                mgr.set_reading(base()),
502
                "`{field}`: reverting back to the original is also an advance"
503
            );
504
        }
505
    }
506

            
507
    /// Bit-pattern comparison, not numeric: `-0.0 == 0.0` is *true* for floats,
508
    /// but the two are different samples and must be reported as an advance.
509
    #[test]
510
    fn signed_zero_flip_advances_even_though_it_compares_equal() {
511
        assert_eq!(0.0_f32, -0.0_f32, "sanity: the two zeroes compare equal");
512

            
513
        let mut mgr = SensorManager::new();
514
        let pos = r(SensorKind::Gyroscope, 0.0, 0.0, 0.0, 0);
515
        let neg = r(SensorKind::Gyroscope, -0.0, 0.0, 0.0, 0);
516

            
517
        assert!(mgr.set_reading(pos));
518
        assert!(
519
            mgr.set_reading(neg),
520
            "+0.0 → -0.0 differs in bits and must count as an advance"
521
        );
522
        assert!(!mgr.set_reading(neg), "…but the same -0.0 twice must not");
523
        assert!(
524
            mgr.reading(SensorKind::Gyroscope)
525
                .is_some_and(|s| s.x.is_sign_negative()),
526
            "the -0.0 must actually be the stored value"
527
        );
528
    }
529

            
530
    /// Distinct NaN payloads are distinct samples; the same payload is not.
531
    #[test]
532
    fn distinct_nan_payloads_advance_but_identical_ones_do_not() {
533
        let quiet = f32::NAN;
534
        let other = f32::from_bits(quiet.to_bits() | 1); // different payload
535
        let negative = f32::from_bits(0xffc0_0000); // sign bit set
536
        assert!(quiet.is_nan() && other.is_nan() && negative.is_nan());
537

            
538
        let mut mgr = SensorManager::new();
539
        assert!(mgr.set_reading(r(SensorKind::Magnetometer, quiet, 0.0, 0.0, 0)));
540
        assert!(
541
            mgr.set_reading(r(SensorKind::Magnetometer, other, 0.0, 0.0, 0)),
542
            "a different NaN payload is a different bit pattern → advance"
543
        );
544
        assert!(
545
            mgr.set_reading(r(SensorKind::Magnetometer, negative, 0.0, 0.0, 0)),
546
            "a sign-flipped NaN is a different bit pattern → advance"
547
        );
548
        assert!(
549
            !mgr.set_reading(r(SensorKind::Magnetometer, negative, 0.0, 0.0, 0)),
550
            "the very same NaN bit pattern → no advance"
551
        );
552
    }
553

            
554
    /// invariant: the sample's own `kind` picks the slot, whatever was written
555
    /// before — a gyroscope sample can never overwrite the accelerometer.
556
    #[test]
557
    fn set_reading_routes_strictly_by_kind() {
558
        let mut mgr = SensorManager::new();
559
        mgr.set_reading(r(SensorKind::Accelerometer, 1.0, 1.0, 1.0, 1));
560
        mgr.set_reading(r(SensorKind::Gyroscope, 2.0, 2.0, 2.0, 2));
561
        mgr.set_reading(r(SensorKind::Magnetometer, 3.0, 3.0, 3.0, 3));
562

            
563
        assert_eq!(mgr.reading(SensorKind::Accelerometer).map(|s| s.x), Some(1.0));
564
        assert_eq!(mgr.reading(SensorKind::Gyroscope).map(|s| s.x), Some(2.0));
565
        assert_eq!(mgr.reading(SensorKind::Magnetometer).map(|s| s.x), Some(3.0));
566
        for kind in KINDS {
567
            assert_eq!(
568
                mgr.reading(kind).map(|s| s.kind),
569
                Some(kind),
570
                "the {kind:?} slot must hold a {kind:?}-tagged sample"
571
            );
572
        }
573
    }
574

            
575
    /// The `pending_event` flag is *sticky*: it stays armed until the event
576
    /// pass clears it. A redundant sample arriving in between returns `false`
577
    /// but must not silently disarm the pending event (an implementation that
578
    /// wrote `self.pending_event = changed` would drop the notification).
579
    #[test]
580
    fn redundant_sample_does_not_disarm_a_pending_event() {
581
        let mut mgr = SensorManager::new();
582
        let sample = base();
583

            
584
        assert!(mgr.set_reading(sample));
585
        assert!(mgr.pending_event, "the advance armed the event");
586

            
587
        assert!(!mgr.set_reading(sample), "identical sample: no advance");
588
        assert!(
589
            mgr.pending_event,
590
            "a redundant sample must NOT clear an event the pass has not seen yet"
591
        );
592
        assert_eq!(
593
            mgr.get_pending_events(tick()).len(),
594
            1,
595
            "the event must still be deliverable"
596
        );
597
    }
598

            
599
    /// …and the converse: once cleared, a redundant sample must not re-arm it.
600
    #[test]
601
    fn redundant_sample_after_clear_does_not_re_arm() {
602
        let mut mgr = SensorManager::new();
603
        mgr.set_reading(base());
604
        mgr.clear_pending_event();
605

            
606
        assert!(!mgr.set_reading(base()), "identical sample: no advance");
607
        assert!(
608
            !mgr.pending_event,
609
            "a no-op sample must not raise a fresh event"
610
        );
611
        assert!(mgr.get_pending_events(tick()).is_empty());
612

            
613
        let mut moved = base();
614
        moved.x += 1.0;
615
        assert!(mgr.set_reading(moved), "a real change re-arms");
616
        assert!(mgr.pending_event);
617
    }
618

            
619
    // ───────────────────── clear_pending_event / listeners ──────────────────
620

            
621
    /// no_panic + invariant: clearing is idempotent, safe on a fresh manager,
622
    /// and touches neither the stored readings nor the listener flag.
623
    #[test]
624
    fn clear_pending_event_is_idempotent_and_narrow() {
625
        let mut mgr = SensorManager::new();
626
        mgr.clear_pending_event(); // on a cold manager: no-op, no panic
627
        assert!(!mgr.pending_event);
628

            
629
        mgr.set_has_listeners(true);
630
        mgr.set_reading(base());
631
        assert!(mgr.pending_event);
632

            
633
        mgr.clear_pending_event();
634
        mgr.clear_pending_event(); // twice — still just cleared
635
        assert!(!mgr.pending_event);
636
        assert_eq!(
637
            mgr.reading(SensorKind::Accelerometer),
638
            Some(base()),
639
            "clearing the flag must not discard the stored reading"
640
        );
641
        assert!(
642
            mgr.has_listeners(),
643
            "clearing the flag must not disarm polling"
644
        );
645
    }
646

            
647
    /// basic_true_false + edge_inputs: the listener flag round-trips, is
648
    /// idempotent, and is independent of the reading/event state.
649
    #[test]
650
    fn has_listeners_round_trips_and_stays_independent() {
651
        let mut mgr = SensorManager::new();
652
        assert!(!mgr.has_listeners(), "default is 'do not poll'");
653

            
654
        for arm in [true, true, false, false, true] {
655
            mgr.set_has_listeners(arm);
656
            assert_eq!(mgr.has_listeners(), arm, "set_has_listeners({arm}) must stick");
657
        }
658

            
659
        // Arming/disarming polling must not fabricate or destroy readings.
660
        assert_eq!(mgr.reading(SensorKind::Accelerometer), None);
661
        assert!(!mgr.pending_event);
662

            
663
        mgr.set_reading(base());
664
        mgr.set_has_listeners(false);
665
        assert!(
666
            mgr.pending_event,
667
            "dropping the listeners must not swallow an already-pending event"
668
        );
669
        assert_eq!(mgr.reading(SensorKind::Accelerometer), Some(base()));
670
    }
671

            
672
    /// Applying a reading must not arm polling by itself — only the relayout
673
    /// walk gets to decide that (the MWA-A1 arming signal).
674
    #[test]
675
    fn set_reading_never_arms_the_listener_flag() {
676
        let mut mgr = SensorManager::new();
677
        for kind in KINDS {
678
            mgr.set_reading(nan_sample(kind));
679
            assert!(
680
                !mgr.has_listeners(),
681
                "{kind:?}: only set_has_listeners may arm polling"
682
            );
683
        }
684
    }
685

            
686
    // ────────────────────── reading_bitwise_eq (private) ────────────────────
687

            
688
    /// The crux of the helper: it is *reflexive even for NaN*, where the
689
    /// derived `PartialEq` is not.
690
    #[test]
691
    fn bitwise_eq_is_reflexive_even_for_nan() {
692
        for kind in KINDS {
693
            let a = nan_sample(kind);
694
            let b = a; // Copy — same bit pattern
695
            assert!(
696
                reading_bitwise_eq(&a, &b),
697
                "{kind:?}: identical bit patterns must compare equal"
698
            );
699
            assert!(
700
                a != b,
701
                "{kind:?}: sanity — derived PartialEq disagrees, because NaN != NaN"
702
            );
703
        }
704
    }
705

            
706
    /// …and it is *not* reflexive over numeric equality: `-0.0` and `0.0`
707
    /// compare equal as floats but are different bit patterns.
708
    #[test]
709
    fn bitwise_eq_separates_signed_zeroes_that_partialeq_merges() {
710
        let pos = r(SensorKind::Accelerometer, 0.0, 0.0, 0.0, 0);
711
        let neg = r(SensorKind::Accelerometer, -0.0, -0.0, -0.0, 0);
712

            
713
        assert_eq!(pos, neg, "sanity: derived PartialEq calls the zeroes equal");
714
        assert!(
715
            !reading_bitwise_eq(&pos, &neg),
716
            "the bitwise comparison must tell +0.0 and -0.0 apart"
717
        );
718
    }
719

            
720
    /// Completeness: flipping *any single field* — including `kind`, the branch
721
    /// `set_reading` can never reach because the kind picks the slot — must make
722
    /// the helper report "not equal".
723
    #[test]
724
    fn bitwise_eq_watches_every_field() {
725
        let b = base();
726

            
727
        let mut other_kind = b;
728
        other_kind.kind = SensorKind::Gyroscope;
729
        let mut other_x = b;
730
        other_x.x = ulp_flip(b.x);
731
        let mut other_y = b;
732
        other_y.y = ulp_flip(b.y);
733
        let mut other_z = b;
734
        other_z.z = ulp_flip(b.z);
735
        let mut other_ts = b;
736
        other_ts.timestamp_ms = b.timestamp_ms + 1;
737

            
738
        for (field, mutated) in [
739
            ("kind", other_kind),
740
            ("x", other_x),
741
            ("y", other_y),
742
            ("z", other_z),
743
            ("timestamp_ms", other_ts),
744
        ] {
745
            assert!(
746
                !reading_bitwise_eq(&b, &mutated),
747
                "a difference in `{field}` must be detected"
748
            );
749
            assert!(
750
                !reading_bitwise_eq(&mutated, &b),
751
                "`{field}`: and symmetrically so"
752
            );
753
        }
754
    }
755

            
756
    /// no_panic + invariant: reflexive and symmetric across a hostile matrix of
757
    /// float values, kinds and boundary timestamps.
758
    #[test]
759
    fn bitwise_eq_is_reflexive_and_symmetric_over_hostile_inputs() {
760
        let mut samples = Vec::new();
761
        for kind in KINDS {
762
            for v in hostile_floats() {
763
                for ts in [0_u64, u64::MAX] {
764
                    samples.push(r(kind, v, -v, v, ts));
765
                }
766
            }
767
        }
768

            
769
        for a in &samples {
770
            assert!(
771
                reading_bitwise_eq(a, a),
772
                "must be reflexive for {a:?}"
773
            );
774
            for b in &samples {
775
                assert_eq!(
776
                    reading_bitwise_eq(a, b),
777
                    reading_bitwise_eq(b, a),
778
                    "must be symmetric for {a:?} vs {b:?}"
779
                );
780
                assert_eq!(
781
                    reading_bitwise_eq(a, b),
782
                    bits_identical(a, b),
783
                    "must agree with a field-by-field bit comparison for {a:?} vs {b:?}"
784
                );
785
            }
786
        }
787
    }
788

            
789
    // ───────────────────────── EventProvider surface ────────────────────────
790

            
791
    /// The event is emitted only while armed, exactly once, at the root — and
792
    /// `clear_pending_event` is what stops it repeating every pass.
793
    #[test]
794
    fn sensor_changed_event_is_emitted_only_while_pending() {
795
        let mut mgr = SensorManager::new();
796
        assert!(
797
            mgr.get_pending_events(tick()).is_empty(),
798
            "a cold manager emits nothing"
799
        );
800

            
801
        assert!(mgr.set_reading(base()));
802
        let events = mgr.get_pending_events(tick());
803
        assert_eq!(events.len(), 1, "one advance → exactly one event");
804
        assert_eq!(events[0].event_type, EventType::SensorChanged);
805
        assert_eq!(events[0].target, DomNodeId::ROOT, "window-level: target is root");
806

            
807
        assert_eq!(
808
            mgr.get_pending_events(tick()).len(),
809
            1,
810
            "collecting is non-destructive; only clear_pending_event disarms"
811
        );
812

            
813
        mgr.clear_pending_event();
814
        assert!(
815
            mgr.get_pending_events(tick()).is_empty(),
816
            "after the event pass drained it, nothing more is pending"
817
        );
818
    }
819

            
820
    /// Several kinds advancing in one pass still collapse into a single
821
    /// window-level event (the callback reads the values it wants by kind).
822
    #[test]
823
    fn many_advances_collapse_into_one_event() {
824
        let mut mgr = SensorManager::new();
825
        for kind in KINDS {
826
            for (ts, x) in [(0_u64, 1.0_f32), (1, 2.0), (2, 3.0), (3, 4.0)] {
827
                mgr.set_reading(r(kind, x, 0.0, 0.0, ts));
828
            }
829
        }
830
        assert_eq!(
831
            mgr.get_pending_events(tick()).len(),
832
            1,
833
            "12 samples across 3 kinds still yield one SensorChanged"
834
        );
835
    }
836
}