1
//! Gamepad manager — cross-platform state for the controller surface
2
//! (`SUPER_PLAN_2` §1 feature 6 + research/03).
3
//!
4
//! Poll + push-driven, like the sensors:
5
//!
6
//! - The **platform backend** (`dll/src/desktop/extra/gamepad/<plat>.rs`)
7
//!   polls `gilrs` / iOS `GCController` / Android `InputDevice` and calls
8
//!   [`push_gamepad_state`] whenever a pad's state changes.
9
//! - The dll **layout pass** drains the channel via
10
//!   [`drain_gamepad_states`] and folds each into the manager through
11
//!   [`GamepadManager::set_state`].
12
//! - **Callbacks** read [`GamepadManager::state`] / [`GamepadManager::primary`]
13
//!   synchronously (via `CallbackInfo::get_gamepad_state`) to drive
14
//!   movement / menu UI.
15
//!
16
//! Unlike the sensors' fixed three slots, the set of pads is dynamic: one
17
//! [`GamepadState`] slot per [`GamepadId`] seen this session, kept across
18
//! frames so a disconnect stays observable (`connected = false`). No
19
//! platform deps (`SUPER_PLAN_2` §0.5); the channel mirrors `sensors.rs`.
20

            
21
use alloc::vec::Vec;
22

            
23
use azul_core::dom::DomNodeId;
24
use azul_core::events::{
25
    EventData, EventProvider, EventSource as CoreEventSource, EventType, SyntheticEvent,
26
};
27
use azul_core::task::Instant;
28
pub use azul_core::gamepad::{GamepadAxis, GamepadButton, GamepadId, GamepadState};
29

            
30
/// Cross-platform gamepad state. One per `App` — the OS exposes a single
31
/// per-process controller subscription, not per-window.
32
#[derive(Debug, Clone, PartialEq, Default)]
33
pub struct GamepadManager {
34
    /// One slot per pad seen this session; `connected` flips to `false` on
35
    /// unplug (the slot is retained so a callback can observe it).
36
    pads: Vec<GamepadState>,
37
    /// `true` when a pad's state advanced since the last event-pass drain.
38
    /// Set by [`set_state`](Self::set_state); cleared by the dll after dispatch.
39
    pending_event: bool,
40
    /// `true` while any node in the current layout registers a
41
    /// `GamepadInput` callback (Hover or Window filter). Recomputed on every
42
    /// relayout by the DOM walk in `shell2::common::layout`; the capability
43
    /// pump polls gilrs/GCController only while this is set (MWA-A1 arming
44
    /// signal — no listeners, no polling, no ~16ms timer).
45
    has_listeners: bool,
46
}
47

            
48
impl GamepadManager {
49
5636
    #[must_use] pub fn new() -> Self {
50
5636
        Self::default()
51
5636
    }
52

            
53
    /// Latest state for `id`, or `None` if that pad was never seen.
54
148
    #[must_use] pub fn state(&self, id: GamepadId) -> Option<GamepadState> {
55
220
        self.pads.iter().find(|p| p.id == id).copied()
56
148
    }
57

            
58
    /// The first currently-connected pad — the common single-controller
59
    /// case, so a callback doesn't have to track ids.
60
28
    #[must_use] pub fn primary(&self) -> Option<GamepadState> {
61
28
        self.pads.iter().find(|p| p.connected).copied()
62
28
    }
63

            
64
    /// Every pad slot seen this session (connected or not).
65
31
    #[must_use] pub fn gamepads(&self) -> &[GamepadState] {
66
31
        &self.pads
67
31
    }
68

            
69
    /// Apply a state the backend delivered (upsert by id). Returns `true`
70
    /// if it advanced (bit-pattern different from the previous slot), so an
71
    /// idle controller doesn't make every frame look "changed".
72
1252
    pub fn set_state(&mut self, state: GamepadState) -> bool {
73
1285
        let changed = if let Some(slot) = self.pads.iter_mut().find(|p| p.id == state.id) {
74
1177
            let changed = !state_bitwise_eq(slot, &state);
75
1177
            *slot = state;
76
1177
            changed
77
        } else {
78
75
            self.pads.push(state);
79
75
            true
80
        };
81
1252
        if changed {
82
1235
            self.pending_event = true;
83
1235
        }
84
1252
        changed
85
1252
    }
86

            
87
    /// Clear the pending-event flag. The dll calls this after the event pass
88
    /// has collected the `GamepadInput` event.
89
81
    pub const fn clear_pending_event(&mut self) {
90
81
        self.pending_event = false;
91
81
    }
92

            
93
    /// Relayout walk reports whether any node listens for `GamepadInput`.
94
12
    pub const fn set_has_listeners(&mut self, has: bool) {
95
12
        self.has_listeners = has;
96
12
    }
97

            
98
    /// `true` while the capability pump should poll the controller backend.
99
21
    #[must_use] pub const fn has_listeners(&self) -> bool {
100
21
        self.has_listeners
101
21
    }
102
}
103

            
104
impl EventProvider for GamepadManager {
105
    /// Yield a window-level `GamepadInput` event when a pad's state advanced
106
    /// since the last drain (target = root; read it via
107
    /// `CallbackInfo::get_primary_gamepad` / `get_gamepad_state`).
108
93
    fn get_pending_events(&self, timestamp: Instant) -> Vec<SyntheticEvent> {
109
93
        if self.pending_event {
110
13
            alloc::vec![SyntheticEvent::new(
111
13
                EventType::GamepadInput,
112
13
                CoreEventSource::User,
113
                DomNodeId::ROOT,
114
13
                timestamp,
115
13
                EventData::None,
116
            )]
117
        } else {
118
80
            Vec::new()
119
        }
120
93
    }
121
}
122

            
123
1229
fn state_bitwise_eq(a: &GamepadState, b: &GamepadState) -> bool {
124
1229
    a.id == b.id
125
1227
        && a.connected == b.connected
126
1220
        && a.buttons == b.buttons
127
191
        && a.left_stick_x.to_bits() == b.left_stick_x.to_bits()
128
163
        && a.left_stick_y.to_bits() == b.left_stick_y.to_bits()
129
136
        && a.right_stick_x.to_bits() == b.right_stick_x.to_bits()
130
110
        && a.right_stick_y.to_bits() == b.right_stick_y.to_bits()
131
84
        && a.left_z.to_bits() == b.left_z.to_bits()
132
57
        && a.right_z.to_bits() == b.right_z.to_bits()
133
1229
}
134

            
135
// ────────── Async update channel (platform backend → manager) ──────────
136
//
137
// gilrs / GCController / InputDevice deliver on the backend's poll thread
138
// with no handle to the live `GamepadManager` (inside the window's
139
// `LayoutWindow`). The backend parks each changed state here; the layout
140
// pass drains it and applies the latest per id. Pure Rust — no platform
141
// dependency (SUPER_PLAN_2 §0.5). Mirrors the sensor reading channel.
142

            
143
static PENDING_STATES: std::sync::Mutex<Vec<GamepadState>> = std::sync::Mutex::new(Vec::new());
144

            
145
/// Park a gamepad state delivered by a platform backend (in the dll).
146
/// Thread-safe; poison-recovering.
147
12
pub fn push_gamepad_state(state: GamepadState) {
148
12
    let mut q = PENDING_STATES.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
149
12
    q.push(state);
150
12
}
151

            
152
/// Drain every state parked by [`push_gamepad_state`], in arrival order.
153
/// Called once per layout pass; the caller applies them through
154
/// [`GamepadManager::set_state`] (the last per id wins).
155
21
pub fn drain_gamepad_states() -> Vec<GamepadState> {
156
21
    let mut q = PENDING_STATES.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
157
21
    core::mem::take(&mut *q)
158
21
}
159

            
160
#[cfg(test)]
161
mod tests {
162
    use super::*;
163

            
164
10
    fn st(id: u32, connected: bool, buttons: u32) -> GamepadState {
165
10
        let mut s = GamepadState::empty(GamepadId { id });
166
10
        s.connected = connected;
167
10
        s.buttons = buttons;
168
10
        s
169
10
    }
170

            
171
    #[test]
172
1
    fn manager_upserts_by_id_and_flags_change() {
173
1
        let mut mgr = GamepadManager::new();
174
1
        assert_eq!(mgr.state(GamepadId { id: 0 }), None);
175
        // First state for an id is a change + adds a slot.
176
1
        assert!(mgr.set_state(st(0, true, 0b1)));
177
1
        assert!(mgr.state(GamepadId { id: 0 }).is_some());
178
        // Same state again — no change.
179
1
        assert!(!mgr.set_state(st(0, true, 0b1)));
180
        // Different buttons — change, same slot (not a new pad).
181
1
        assert!(mgr.set_state(st(0, true, 0b11)));
182
1
        assert_eq!(mgr.gamepads().len(), 1);
183
        // A second pad adds a slot.
184
1
        assert!(mgr.set_state(st(1, true, 0)));
185
1
        assert_eq!(mgr.gamepads().len(), 2);
186
1
    }
187

            
188
    #[test]
189
1
    fn primary_is_first_connected() {
190
1
        let mut mgr = GamepadManager::new();
191
1
        mgr.set_state(st(0, false, 0)); // disconnected
192
1
        mgr.set_state(st(1, true, 0));
193
1
        assert_eq!(mgr.primary().map(|p| p.id.id), Some(1));
194
1
    }
195

            
196
    #[test]
197
1
    fn is_pressed_decodes_the_bitset() {
198
1
        let s = st(0, true, GamepadButton::South.bit() | GamepadButton::Start.bit());
199
1
        assert!(s.is_pressed(GamepadButton::South));
200
1
        assert!(s.is_pressed(GamepadButton::Start));
201
1
        assert!(!s.is_pressed(GamepadButton::East));
202
1
    }
203

            
204
    #[test]
205
1
    fn listener_flag_gates_polling_decision() {
206
1
        let mut mgr = GamepadManager::new();
207
1
        assert!(!mgr.has_listeners(), "no listeners until the relayout walk reports some");
208
1
        mgr.set_has_listeners(true);
209
1
        assert!(mgr.has_listeners());
210
1
        mgr.set_has_listeners(false);
211
1
        assert!(!mgr.has_listeners());
212
1
    }
213

            
214
    #[test]
215
1
    fn states_round_trip_through_the_channel() {
216
1
        drop(drain_gamepad_states());
217
1
        push_gamepad_state(st(0, true, 0b1));
218
1
        push_gamepad_state(st(0, true, 0b10)); // last per id wins
219
1
        push_gamepad_state(st(1, true, 0));
220
1
        let drained = drain_gamepad_states();
221
1
        assert_eq!(drained.len(), 3);
222

            
223
1
        let mut mgr = GamepadManager::new();
224
4
        for s in &drained {
225
3
            mgr.set_state(*s);
226
3
        }
227
1
        assert_eq!(mgr.state(GamepadId { id: 0 }).map(|p| p.buttons), Some(0b10));
228
1
        assert_eq!(mgr.gamepads().len(), 2);
229
1
        assert!(drain_gamepad_states().is_empty());
230
1
    }
231
}
232

            
233
#[cfg(test)]
234
mod autotest_generated {
235
    use azul_core::task::SystemTick;
236

            
237
    use super::*;
238

            
239
    // NOTE on coverage: `push_gamepad_state` / `drain_gamepad_states` are
240
    // deliberately NOT tested here. They share one process-global
241
    // `PENDING_STATES` mutex, and `tests::states_round_trip_through_the_channel`
242
    // above already asserts an *exact* drained length on it. Since the test
243
    // harness runs both modules on parallel threads in the same binary, a push
244
    // or drain from here could interleave with that test's push/drain window and
245
    // make it fail spuriously. The round-trip is already covered there.
246

            
247
    /// Every axis field, paired with a setter, so a test can walk all six
248
    /// without depending on `GamepadState::axis` (a bug there must not mask a
249
    /// bug here).
250
    type AxisSetter = (&'static str, fn(&mut GamepadState, f32));
251
    const AXIS_SETTERS: [AxisSetter; 6] = [
252
        ("left_stick_x", |s, v| s.left_stick_x = v),
253
        ("left_stick_y", |s, v| s.left_stick_y = v),
254
        ("right_stick_x", |s, v| s.right_stick_x = v),
255
        ("right_stick_y", |s, v| s.right_stick_y = v),
256
        ("left_z", |s, v| s.left_z = v),
257
        ("right_z", |s, v| s.right_z = v),
258
    ];
259

            
260
    /// Floats a real backend can hand us that break naive `==` comparison.
261
    const NASTY_FLOATS: [f32; 10] = [
262
        f32::NAN,
263
        f32::INFINITY,
264
        f32::NEG_INFINITY,
265
        0.0,
266
        -0.0,
267
        -1.0,
268
        1.0,
269
        f32::MIN,
270
        f32::MAX,
271
        f32::MIN_POSITIVE,
272
    ];
273

            
274
    fn pad(id: u32) -> GamepadState {
275
        GamepadState::empty(GamepadId { id })
276
    }
277

            
278
    fn connected(id: u32) -> GamepadState {
279
        let mut s = pad(id);
280
        s.connected = true;
281
        s
282
    }
283

            
284
    fn ts(tick: u64) -> Instant {
285
        Instant::Tick(SystemTick::new(tick))
286
    }
287

            
288
    /// Field-by-field bit comparison, written independently of
289
    /// `state_bitwise_eq` so it can be used to check that function.
290
    fn same_bits(a: &GamepadState, b: &GamepadState) -> bool {
291
        a.id == b.id
292
            && a.connected == b.connected
293
            && a.buttons == b.buttons
294
            && [
295
                (a.left_stick_x, b.left_stick_x),
296
                (a.left_stick_y, b.left_stick_y),
297
                (a.right_stick_x, b.right_stick_x),
298
                (a.right_stick_y, b.right_stick_y),
299
                (a.left_z, b.left_z),
300
                (a.right_z, b.right_z),
301
            ]
302
            .iter()
303
            .all(|(x, y)| x.to_bits() == y.to_bits())
304
    }
305

            
306
    // ------------------------------------------------------------------
307
    // GamepadManager::new  (constructor)
308
    // ------------------------------------------------------------------
309

            
310
    /// no_panic + invariants_hold: a fresh manager is the documented zero —
311
    /// no slots, nothing pending, no listeners — and is indistinguishable from
312
    /// `Default` (the dll builds one per `App` either way).
313
    #[test]
314
    fn new_is_default_and_starts_completely_empty() {
315
        let mgr = GamepadManager::new();
316

            
317
        assert_eq!(mgr, GamepadManager::default());
318
        assert!(mgr.gamepads().is_empty());
319
        assert_eq!(mgr.gamepads().len(), 0);
320
        assert_eq!(mgr.primary(), None);
321
        assert!(!mgr.has_listeners(), "polling must be disarmed until a relayout arms it");
322
        assert!(!mgr.pending_event, "a manager nobody touched cannot have a pending event");
323
        assert!(
324
            mgr.get_pending_events(ts(0)).is_empty(),
325
            "a fresh manager must not synthesise a GamepadInput event"
326
        );
327
    }
328

            
329
    // ------------------------------------------------------------------
330
    // GamepadManager::state  (other)
331
    // ------------------------------------------------------------------
332

            
333
    /// no_panic_smoke + boundary ids: an id that was never seen is `None`, on
334
    /// an empty manager *and* on a populated one. `u32::MAX` / `0` are the
335
    /// interesting ones — the backend normalises platform device ids into a
336
    /// `u32`, so both ends of the range are reachable.
337
    #[test]
338
    fn state_returns_none_for_ids_never_seen() {
339
        let mut mgr = GamepadManager::new();
340
        for id in [0, 1, u32::MAX / 2, u32::MAX - 1, u32::MAX] {
341
            assert_eq!(mgr.state(GamepadId { id }), None, "id {id} on an empty manager");
342
        }
343

            
344
        mgr.set_state(connected(7));
345
        assert!(mgr.state(GamepadId { id: 7 }).is_some());
346
        for id in [0, 6, 8, u32::MAX] {
347
            assert_eq!(mgr.state(GamepadId { id }), None, "id {id} was never pushed");
348
        }
349
    }
350

            
351
    /// The lookup must key on the *whole* id, not a truncated / masked one:
352
    /// pads whose ids differ only in the low or high bits must not alias.
353
    #[test]
354
    fn state_does_not_alias_neighbouring_or_truncated_ids() {
355
        let mut mgr = GamepadManager::new();
356
        // (id, fingerprint) — ids that differ only in the low or the high half.
357
        let pads = [
358
            (0u32, 0x0000_0001u32),
359
            (1, 0x0000_0002),
360
            (0xFFFF, 0x0000_0004),
361
            (0x1_0000, 0x0000_0008),
362
            (u32::MAX - 1, 0x0000_0010),
363
            (u32::MAX, 0x0000_0020),
364
        ];
365
        for (id, fingerprint) in pads {
366
            let mut s = connected(id);
367
            s.buttons = fingerprint;
368
            mgr.set_state(s);
369
        }
370
        assert_eq!(
371
            mgr.gamepads().len(),
372
            pads.len(),
373
            "distinct ids must not collapse into one slot"
374
        );
375
        for (id, fingerprint) in pads {
376
            let got = mgr.state(GamepadId { id }).expect("pad was pushed");
377
            assert_eq!(got.id.id, id);
378
            assert_eq!(got.buttons, fingerprint, "id {id} returned another pad's snapshot");
379
        }
380
    }
381

            
382
    /// round-trip / encode == decode: whatever the backend delivered comes back
383
    /// out of `state()` bit-for-bit, including the floats `==` would mangle
384
    /// (NaN, ±inf, −0.0) and a fully-set button bitset.
385
    #[test]
386
    fn state_returns_the_pushed_snapshot_bit_exactly() {
387
        let mut mgr = GamepadManager::new();
388

            
389
        let mut s = connected(u32::MAX);
390
        s.buttons = u32::MAX;
391
        s.left_stick_x = f32::NAN;
392
        s.left_stick_y = -0.0;
393
        s.right_stick_x = f32::INFINITY;
394
        s.right_stick_y = f32::NEG_INFINITY;
395
        s.left_z = f32::MIN;
396
        s.right_z = f32::MAX;
397
        mgr.set_state(s);
398

            
399
        let got = mgr.state(GamepadId { id: u32::MAX }).expect("pad u32::MAX was pushed");
400
        assert!(
401
            same_bits(&got, &s),
402
            "state() did not return the pushed snapshot bit-exactly: {got:?} vs {s:?}"
403
        );
404
        // …and the NaN axis really is a NaN, i.e. nothing sanitised it on the way.
405
        assert!(got.left_stick_x.is_nan());
406
        assert_eq!(got.left_stick_y.to_bits(), (-0.0f32).to_bits(), "−0.0 collapsed to +0.0");
407
    }
408

            
409
    /// `state()` is a read-only view: calling it (even for a missing id) leaves
410
    /// the manager — slots, pending flag, listener flag — untouched.
411
    #[test]
412
    fn state_does_not_mutate_the_manager() {
413
        let mut mgr = GamepadManager::new();
414
        mgr.set_state(connected(0));
415
        mgr.clear_pending_event();
416
        let before = mgr.clone();
417

            
418
        for id in [0, 1, u32::MAX] {
419
            let _ = mgr.state(GamepadId { id });
420
        }
421
        assert_eq!(mgr, before);
422
        assert!(!mgr.pending_event, "a read must not raise the pending-event flag");
423
    }
424

            
425
    // ------------------------------------------------------------------
426
    // GamepadManager::primary  (getter)
427
    // ------------------------------------------------------------------
428

            
429
    /// edge_access: `None` on a default manager, and still `None` once pads
430
    /// exist but every one of them is disconnected (the slots are retained, so
431
    /// "has slots" must not be confused with "has a pad").
432
    #[test]
433
    fn primary_is_none_when_empty_or_when_nothing_is_connected() {
434
        let mut mgr = GamepadManager::new();
435
        assert_eq!(mgr.primary(), None);
436
        assert_eq!(GamepadManager::default().primary(), None);
437

            
438
        mgr.set_state(pad(0)); // connected = false
439
        mgr.set_state(pad(1));
440
        assert_eq!(mgr.gamepads().len(), 2, "disconnected pads still occupy slots");
441
        assert_eq!(mgr.primary(), None, "a disconnected slot is not a primary pad");
442
    }
443

            
444
    /// basic_access, pinned against the plausible misreading: "first" means
445
    /// *first connected slot in arrival order*, NOT lowest id. Pad 9 arrives
446
    /// before pad 1, so pad 9 is primary — a callback that assumed
447
    /// `min(id)` would drive the wrong controller.
448
    #[test]
449
    fn primary_is_the_first_connected_in_arrival_order_not_the_lowest_id() {
450
        let mut mgr = GamepadManager::new();
451
        mgr.set_state(pad(3)); // seen, but disconnected — must be skipped
452
        mgr.set_state(connected(9));
453
        mgr.set_state(connected(1));
454

            
455
        assert_eq!(mgr.primary().map(|p| p.id.id), Some(9));
456
        assert_eq!(mgr.gamepads().first().map(|p| p.id.id), Some(3), "arrival order kept");
457
    }
458

            
459
    /// A disconnect must hand primacy to the next connected pad, and the last
460
    /// disconnect must take it back to `None` — without ever dropping a slot.
461
    #[test]
462
    fn primary_follows_disconnects_while_slots_are_retained() {
463
        let mut mgr = GamepadManager::new();
464
        mgr.set_state(connected(0));
465
        mgr.set_state(connected(1));
466
        assert_eq!(mgr.primary().map(|p| p.id.id), Some(0));
467

            
468
        mgr.set_state(pad(0)); // unplug pad 0
469
        assert_eq!(mgr.primary().map(|p| p.id.id), Some(1), "primacy must fall through to pad 1");
470
        assert_eq!(mgr.gamepads().len(), 2, "the unplugged slot must be retained");
471
        assert_eq!(
472
            mgr.state(GamepadId { id: 0 }).map(|p| p.connected),
473
            Some(false),
474
            "the disconnect must stay observable"
475
        );
476

            
477
        mgr.set_state(pad(1)); // unplug the last one
478
        assert_eq!(mgr.primary(), None);
479
        assert_eq!(mgr.gamepads().len(), 2);
480

            
481
        mgr.set_state(connected(0)); // re-plug: the same slot comes back, no new one
482
        assert_eq!(mgr.primary().map(|p| p.id.id), Some(0));
483
        assert_eq!(mgr.gamepads().len(), 2, "a re-plug must reuse the id's slot");
484
    }
485

            
486
    // ------------------------------------------------------------------
487
    // GamepadManager::gamepads  (getter)
488
    // ------------------------------------------------------------------
489

            
490
    /// basic_access + invariant: one slot per *unique* id, in arrival order,
491
    /// no matter how many updates each pad delivers. A slot leak here would
492
    /// grow unboundedly at ~60 Hz for the lifetime of the process.
493
    #[test]
494
    fn gamepads_keeps_one_slot_per_id_in_arrival_order() {
495
        let mut mgr = GamepadManager::new();
496
        // (id, buttons) — 3 unique ids, each pushed more than once. The button
497
        // value differs on every push, so each one is a genuine change.
498
        let arrival = [(5u32, 1u32), (0, 2), (u32::MAX, 3), (5, 4), (0, 5), (5, 6)];
499

            
500
        for (id, buttons) in arrival {
501
            let mut s = connected(id);
502
            s.buttons = buttons;
503
            mgr.set_state(s);
504
        }
505

            
506
        let ids: Vec<u32> = mgr.gamepads().iter().map(|p| p.id.id).collect();
507
        assert_eq!(ids, alloc::vec![5, 0, u32::MAX], "arrival order / dedup by id broken");
508
        assert_eq!(mgr.gamepads().len(), 3);
509

            
510
        // 1000 further updates to a known id must not add a single slot.
511
        for i in 0..1000u32 {
512
            let mut s = connected(5);
513
            s.buttons = i;
514
            mgr.set_state(s);
515
        }
516
        assert_eq!(mgr.gamepads().len(), 3, "repeated updates leaked slots");
517
    }
518

            
519
    /// The slice `gamepads()` hands out must agree with `state()` /
520
    /// `primary()` — they are three views of the same `pads` vec, so a caller
521
    /// iterating the slice must never see something the id lookup denies.
522
    #[test]
523
    fn gamepads_slice_agrees_with_state_and_primary() {
524
        let mut mgr = GamepadManager::new();
525
        mgr.set_state(pad(2));
526
        mgr.set_state(connected(4));
527
        mgr.set_state(connected(8));
528

            
529
        for p in mgr.gamepads() {
530
            assert_eq!(mgr.state(p.id).as_ref(), Some(p), "slice and state() disagree for {p:?}");
531
        }
532
        assert_eq!(
533
            mgr.primary().as_ref(),
534
            mgr.gamepads().iter().find(|p| p.connected),
535
            "primary() must be the first connected element of the slice"
536
        );
537
    }
538

            
539
    // ------------------------------------------------------------------
540
    // GamepadManager::set_state  (other)
541
    // ------------------------------------------------------------------
542

            
543
    /// The core contract: `true` iff the bit pattern advanced. An idle
544
    /// controller re-reporting the same snapshot every frame must return
545
    /// `false`, or the dll would relayout at the poll rate forever.
546
    #[test]
547
    fn set_state_reports_change_only_when_the_bits_advance() {
548
        let mut mgr = GamepadManager::new();
549
        let mut s = connected(0);
550

            
551
        assert!(mgr.set_state(s), "a never-seen id is always a change");
552
        assert!(!mgr.set_state(s), "an idle controller must not look changed");
553
        assert!(!mgr.set_state(s), "…and must keep not looking changed");
554

            
555
        s.buttons = GamepadButton::South.bit();
556
        assert!(mgr.set_state(s), "a button press is a change");
557
        assert!(!mgr.set_state(s), "a held button is not a new change");
558

            
559
        s.connected = false;
560
        assert!(mgr.set_state(s), "a disconnect is a change");
561
        assert!(!mgr.set_state(s));
562

            
563
        s.left_stick_x = 0.5;
564
        assert!(mgr.set_state(s), "an axis move is a change");
565
        assert!(!mgr.set_state(s));
566

            
567
        assert_eq!(mgr.gamepads().len(), 1, "all of that was one pad");
568
    }
569

            
570
    /// Adversarial float #1 — NaN. A stick that reports NaN (a real gilrs /
571
    /// driver failure mode) would make a derived-`PartialEq` comparison say
572
    /// "changed" on *every* frame forever, because NaN != NaN. `set_state`
573
    /// compares `to_bits()`, so an unchanging NaN correctly reads as idle.
574
    /// This test pins that: the derived `==` disagrees, and `set_state` is right.
575
    #[test]
576
    fn set_state_treats_an_unchanging_nan_axis_as_idle() {
577
        let mut mgr = GamepadManager::new();
578
        let mut s = connected(0);
579
        s.left_stick_x = f32::NAN;
580

            
581
        // Sanity: the derived PartialEq really is non-reflexive here, so a
582
        // `!=`-based implementation would spin.
583
        let bit_identical_copy = s;
584
        assert_ne!(
585
            s, bit_identical_copy,
586
            "precondition: a NaN axis makes derived PartialEq non-reflexive"
587
        );
588

            
589
        assert!(mgr.set_state(s), "first sighting of the pad is a change");
590
        assert!(!mgr.set_state(s), "a stuck NaN axis must NOT look like a change every frame");
591
        assert!(!mgr.set_state(s));
592

            
593
        // A *different* NaN payload is a different bit pattern → a change.
594
        let mut other = s;
595
        other.left_stick_x = f32::from_bits(f32::NAN.to_bits() | 0x1);
596
        assert!(other.left_stick_x.is_nan());
597
        assert!(mgr.set_state(other), "a different NaN bit pattern is a bitwise change");
598
        assert!(!mgr.set_state(other));
599
    }
600

            
601
    /// Adversarial float #2 — signed zero. `-0.0 == 0.0` is *true* in IEEE, so
602
    /// a value-comparing implementation would miss a stick crossing centre from
603
    /// the negative side. `to_bits()` catches it: the flip is reported.
604
    #[test]
605
    fn set_state_reports_a_positive_to_negative_zero_flip() {
606
        let mut mgr = GamepadManager::new();
607
        let mut s = connected(0);
608
        s.left_stick_y = 0.0;
609
        assert!(mgr.set_state(s));
610

            
611
        s.left_stick_y = -0.0;
612
        // Precondition: the two zeroes compare *equal* under `==` (IEEE) yet
613
        // differ in their bits — which is exactly what set_state must key on.
614
        assert_ne!((0.0f32).to_bits(), (-0.0f32).to_bits());
615
        assert!(mgr.set_state(s), "a +0.0 → −0.0 sign flip is a bitwise change");
616
        assert!(!mgr.set_state(s));
617
        assert_eq!(
618
            mgr.state(GamepadId { id: 0 }).map(|p| p.left_stick_y.to_bits()),
619
            Some((-0.0f32).to_bits())
620
        );
621
    }
622

            
623
    /// Every field independently drives the change decision — a change in any
624
    /// one of `connected`, `buttons` or the six axes must be reported. A
625
    /// forgotten field in the comparison would silently swallow that input.
626
    #[test]
627
    fn set_state_notices_a_change_in_every_single_field() {
628
        // `connected`
629
        let mut mgr = GamepadManager::new();
630
        let base = connected(0);
631
        assert!(mgr.set_state(base));
632
        let mut flipped = base;
633
        flipped.connected = false;
634
        assert!(mgr.set_state(flipped), "a change in `connected` went unnoticed");
635

            
636
        // `buttons` — every defined bit, one at a time.
637
        for bit in 0..17u32 {
638
            let mut mgr = GamepadManager::new();
639
            assert!(mgr.set_state(base));
640
            let mut s = base;
641
            s.buttons = 1 << bit;
642
            assert!(mgr.set_state(s), "a change in button bit {bit} went unnoticed");
643
        }
644

            
645
        // the six axes.
646
        for (name, set) in AXIS_SETTERS {
647
            let mut mgr = GamepadManager::new();
648
            assert!(mgr.set_state(base));
649
            let mut s = base;
650
            set(&mut s, 1.0);
651
            assert!(mgr.set_state(s), "a change in axis `{name}` went unnoticed");
652
            assert!(!mgr.set_state(s), "…and re-reporting `{name}` is not a second change");
653
        }
654
    }
655

            
656
    /// no_panic_smoke over extremes: saturated ids / bitsets and every nasty
657
    /// float in every axis. Nothing panics, the slot count stays at one per id,
658
    /// and the stored snapshot is always exactly what was pushed.
659
    #[test]
660
    fn set_state_survives_extreme_ids_bitsets_and_floats() {
661
        let mut mgr = GamepadManager::new();
662

            
663
        for id in [0u32, u32::MAX] {
664
            for (_, set) in AXIS_SETTERS {
665
                for v in NASTY_FLOATS {
666
                    let mut s = connected(id);
667
                    s.buttons = u32::MAX;
668
                    set(&mut s, v);
669
                    mgr.set_state(s);
670
                    let got = mgr.state(GamepadId { id }).expect("just pushed");
671
                    assert!(same_bits(&got, &s), "pushing {v:?} into pad {id} did not round-trip");
672
                }
673
            }
674
        }
675
        assert_eq!(mgr.gamepads().len(), 2, "two ids must occupy exactly two slots");
676
    }
677

            
678
    /// Upserting one pad must not touch any other slot — the `find` walks by
679
    /// id, so an index/id mix-up would corrupt a neighbour.
680
    #[test]
681
    fn set_state_upsert_leaves_the_other_slots_untouched() {
682
        let mut mgr = GamepadManager::new();
683
        for (id, z) in [(0u32, 0.0f32), (1, 0.25), (2, 0.5), (3, 0.75)] {
684
            let mut s = connected(id);
685
            s.buttons = 1 << id;
686
            s.left_z = z;
687
            mgr.set_state(s);
688
        }
689
        let before: Vec<GamepadState> = mgr.gamepads().to_vec();
690

            
691
        let mut updated = connected(2);
692
        updated.buttons = u32::MAX;
693
        updated.left_z = -1.0;
694
        assert!(mgr.set_state(updated));
695

            
696
        assert_eq!(mgr.gamepads().len(), 4);
697
        for (i, p) in mgr.gamepads().iter().enumerate() {
698
            if i == 2 {
699
                assert!(same_bits(p, &updated), "the targeted slot was not updated");
700
            } else {
701
                assert!(same_bits(p, &before[i]), "slot {i} was corrupted by an upsert of pad 2");
702
            }
703
        }
704
    }
705

            
706
    /// The pending flag is set **only** by a real change, and is sticky until
707
    /// the dll drains it — several changes between two drains must coalesce
708
    /// into one flag (and one event), not queue up.
709
    #[test]
710
    fn set_state_raises_pending_only_on_a_real_change_and_coalesces() {
711
        let mut mgr = GamepadManager::new();
712
        assert!(!mgr.pending_event);
713

            
714
        let mut s = connected(0);
715
        assert!(mgr.set_state(s));
716
        assert!(mgr.pending_event, "a new pad must raise the pending flag");
717

            
718
        mgr.clear_pending_event();
719
        assert!(!mgr.set_state(s), "idle re-report");
720
        assert!(!mgr.pending_event, "an idle re-report must NOT raise the pending flag");
721

            
722
        // Three changes, one flag.
723
        s.buttons = 1;
724
        assert!(mgr.set_state(s));
725
        s.buttons = 2;
726
        assert!(mgr.set_state(s));
727
        s.left_z = 1.0;
728
        assert!(mgr.set_state(s));
729
        assert!(mgr.pending_event);
730
        assert_eq!(mgr.get_pending_events(ts(0)).len(), 1, "changes must coalesce into one event");
731
    }
732

            
733
    // ------------------------------------------------------------------
734
    // GamepadManager::clear_pending_event  (other)
735
    // ------------------------------------------------------------------
736

            
737
    /// no_panic_smoke: clearing is safe on an empty manager, is idempotent, and
738
    /// touches *only* the flag — not the pads, not the listener arming.
739
    #[test]
740
    fn clear_pending_event_is_idempotent_and_touches_nothing_else() {
741
        let mut mgr = GamepadManager::new();
742
        mgr.clear_pending_event(); // nothing pending, no pads at all
743
        mgr.clear_pending_event();
744
        assert!(!mgr.pending_event);
745

            
746
        mgr.set_has_listeners(true);
747
        mgr.set_state(connected(1));
748
        assert!(mgr.pending_event);
749

            
750
        mgr.clear_pending_event();
751
        assert!(!mgr.pending_event);
752
        mgr.clear_pending_event();
753
        assert!(!mgr.pending_event, "a second clear must not resurrect the flag");
754

            
755
        assert_eq!(mgr.gamepads().len(), 1, "clearing must not drop pad slots");
756
        assert!(mgr.has_listeners(), "clearing must not disarm the listener flag");
757
        assert!(mgr.get_pending_events(ts(1)).is_empty(), "no event after a clear");
758

            
759
        // …and a fresh change re-arms it, so the flag is not one-shot.
760
        let mut s = connected(1);
761
        s.buttons = 1;
762
        assert!(mgr.set_state(s));
763
        assert!(mgr.pending_event, "the flag must be re-raisable after a clear");
764
    }
765

            
766
    // ------------------------------------------------------------------
767
    // set_has_listeners / has_listeners  (other + predicate)
768
    // ------------------------------------------------------------------
769

            
770
    /// basic_true_false + edge_inputs: the arming flag round-trips, is
771
    /// idempotent in both directions, and is completely independent of the pad
772
    /// slots and the pending flag (the relayout walk owns it alone).
773
    #[test]
774
    fn has_listeners_roundtrips_and_is_independent_of_pad_state() {
775
        let mut mgr = GamepadManager::new();
776
        assert!(!mgr.has_listeners(), "default must be disarmed — no listeners, no polling");
777

            
778
        for _ in 0..3 {
779
            mgr.set_has_listeners(true);
780
            assert!(mgr.has_listeners());
781
        }
782
        for _ in 0..3 {
783
            mgr.set_has_listeners(false);
784
            assert!(!mgr.has_listeners());
785
        }
786

            
787
        // Pads arriving must not arm polling by themselves…
788
        mgr.set_state(connected(0));
789
        assert!(!mgr.has_listeners(), "a connected pad must not arm the pump on its own");
790
        // …and arming must not fabricate pads or events.
791
        let mut fresh = GamepadManager::new();
792
        fresh.set_has_listeners(true);
793
        assert!(fresh.gamepads().is_empty());
794
        assert!(!fresh.pending_event);
795
        assert!(fresh.get_pending_events(ts(0)).is_empty());
796
    }
797

            
798
    // ------------------------------------------------------------------
799
    // state_bitwise_eq  (private)
800
    // ------------------------------------------------------------------
801

            
802
    /// The whole reason this function exists: unlike the derived `PartialEq`,
803
    /// it is **reflexive over NaN**. A snapshot with a NaN in every axis must
804
    /// equal itself — otherwise an idle broken stick would look "changed"
805
    /// forever.
806
    #[test]
807
    fn state_bitwise_eq_is_reflexive_even_for_nan_axes() {
808
        let mut s = connected(u32::MAX);
809
        s.buttons = u32::MAX;
810
        for (_, set) in AXIS_SETTERS {
811
            set(&mut s, f32::NAN);
812
        }
813
        let copy = s;
814

            
815
        assert_ne!(s, copy, "precondition: derived PartialEq is non-reflexive over NaN");
816
        assert!(state_bitwise_eq(&s, &s), "bitwise eq must be reflexive");
817
        assert!(state_bitwise_eq(&s, &copy), "a bit-identical copy must compare equal");
818
        assert!(state_bitwise_eq(&copy, &s), "…symmetrically");
819
    }
820

            
821
    /// no_panic_smoke + exhaustive field coverage: flipping any ONE of the nine
822
    /// fields must make the comparison false, and the relation must stay
823
    /// symmetric. A field missing from the `&&` chain would show up here.
824
    #[test]
825
    fn state_bitwise_eq_detects_a_difference_in_every_field() {
826
        let base = {
827
            let mut s = connected(1);
828
            s.buttons = 0b1010;
829
            s.left_stick_x = 0.25;
830
            s.left_stick_y = -0.25;
831
            s.right_stick_x = 0.5;
832
            s.right_stick_y = -0.5;
833
            s.left_z = 0.75;
834
            s.right_z = 1.0;
835
            s
836
        };
837
        assert!(state_bitwise_eq(&base, &base));
838

            
839
        let mut mutations: Vec<(&str, GamepadState)> = Vec::new();
840

            
841
        let mut m = base;
842
        m.id = GamepadId { id: 2 };
843
        mutations.push(("id", m));
844

            
845
        let mut m = base;
846
        m.connected = false;
847
        mutations.push(("connected", m));
848

            
849
        let mut m = base;
850
        m.buttons = 0b1011;
851
        mutations.push(("buttons", m));
852

            
853
        for (name, set) in AXIS_SETTERS {
854
            let mut m = base;
855
            set(&mut m, -12.5); // a value no axis holds in `base`
856
            mutations.push((name, m));
857
        }
858

            
859
        assert_eq!(mutations.len(), 9, "all nine fields must be exercised");
860
        for (name, m) in mutations {
861
            assert!(!state_bitwise_eq(&base, &m), "a change in `{name}` was not detected");
862
            assert!(!state_bitwise_eq(&m, &base), "…and the relation must be symmetric (`{name}`)");
863
        }
864
    }
865

            
866
    /// The two IEEE traps in one place: `-0.0` vs `+0.0` (equal by `==`, must
867
    /// be *unequal* here) and NaN vs NaN with the same payload (unequal by
868
    /// `==`, must be *equal* here). Checked on every axis, so no arm of the
869
    /// comparison chain gets it right by accident.
870
    #[test]
871
    fn state_bitwise_eq_splits_signed_zero_and_joins_identical_nan() {
872
        for (name, set) in AXIS_SETTERS {
873
            let base = connected(0);
874

            
875
            let mut pos = base;
876
            set(&mut pos, 0.0);
877
            let mut neg = base;
878
            set(&mut neg, -0.0);
879
            assert_eq!(pos, neg, "precondition: ±0.0 compare equal via derived PartialEq");
880
            assert!(
881
                !state_bitwise_eq(&pos, &neg),
882
                "axis `{name}`: +0.0 and −0.0 must differ bitwise"
883
            );
884

            
885
            let mut nan_a = base;
886
            set(&mut nan_a, f32::NAN);
887
            let nan_b = nan_a;
888
            assert!(
889
                state_bitwise_eq(&nan_a, &nan_b),
890
                "axis `{name}`: identical NaN bit patterns must compare equal"
891
            );
892

            
893
            // Different NaN payloads are different bit patterns → not equal.
894
            let mut nan_c = base;
895
            set(&mut nan_c, f32::from_bits(f32::NAN.to_bits() | 0x7));
896
            assert!(
897
                !state_bitwise_eq(&nan_a, &nan_c),
898
                "axis `{name}`: distinct NaN payloads must not compare equal"
899
            );
900
        }
901
    }
902

            
903
    /// Infinities are ordinary bit patterns here — `inf == inf` must hold and
904
    /// `+inf != -inf`, with no arithmetic (which could produce a NaN) involved.
905
    #[test]
906
    fn state_bitwise_eq_handles_infinities_without_arithmetic() {
907
        for (name, set) in AXIS_SETTERS {
908
            let mut a = connected(0);
909
            set(&mut a, f32::INFINITY);
910
            let b = a;
911
            assert!(state_bitwise_eq(&a, &b), "axis `{name}`: +inf must equal +inf");
912

            
913
            let mut c = connected(0);
914
            set(&mut c, f32::NEG_INFINITY);
915
            assert!(!state_bitwise_eq(&a, &c), "axis `{name}`: +inf must not equal −inf");
916
        }
917
    }
918

            
919
    // ------------------------------------------------------------------
920
    // EventProvider::get_pending_events  (the manager's only output edge)
921
    // ------------------------------------------------------------------
922

            
923
    /// A pending change yields exactly one window-level `GamepadInput` event
924
    /// aimed at the root, carrying the timestamp it was given — and yields it
925
    /// *repeatedly* until the dll clears the flag (the event pass may run
926
    /// twice before the drain).
927
    #[test]
928
    fn pending_change_yields_one_root_gamepad_input_event() {
929
        let mut mgr = GamepadManager::new();
930
        assert!(mgr.get_pending_events(ts(0)).is_empty());
931

            
932
        mgr.set_state(connected(0));
933
        let evs = mgr.get_pending_events(ts(42));
934
        assert_eq!(evs.len(), 1);
935
        let ev = &evs[0];
936
        assert_eq!(ev.event_type, EventType::GamepadInput);
937
        assert_eq!(ev.source, CoreEventSource::User);
938
        assert_eq!(ev.target, DomNodeId::ROOT, "the gamepad event is window-level");
939
        assert_eq!(ev.timestamp, ts(42), "the caller's timestamp must be carried through");
940
        assert_eq!(ev.data, EventData::None, "the payload is read via CallbackInfo, not the event");
941

            
942
        // Still pending until it is explicitly cleared.
943
        assert_eq!(mgr.get_pending_events(ts(43)).len(), 1);
944
        mgr.clear_pending_event();
945
        assert!(mgr.get_pending_events(ts(44)).is_empty(), "cleared → no more events");
946
    }
947

            
948
    /// The listener flag arms the *pump*, not the event stream: it must not, by
949
    /// itself, make the manager emit (or suppress) an event. Only a real state
950
    /// change does.
951
    #[test]
952
    fn listener_flag_does_not_fabricate_or_suppress_events() {
953
        let mut mgr = GamepadManager::new();
954
        mgr.set_has_listeners(true);
955
        assert!(mgr.get_pending_events(ts(0)).is_empty(), "arming alone must not emit an event");
956

            
957
        mgr.set_state(connected(0));
958
        mgr.set_has_listeners(false);
959
        assert_eq!(
960
            mgr.get_pending_events(ts(0)).len(),
961
            1,
962
            "disarming must not swallow an already-pending event"
963
        );
964
    }
965
}