Skip to main content

azul_layout/managers/
gamepad.rs

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
21use alloc::vec::Vec;
22
23use azul_core::dom::DomNodeId;
24use azul_core::events::{
25    EventData, EventProvider, EventSource as CoreEventSource, EventType, SyntheticEvent,
26};
27use azul_core::task::Instant;
28pub 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)]
33pub 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
48impl GamepadManager {
49    #[must_use] pub fn new() -> Self {
50        Self::default()
51    }
52
53    /// Latest state for `id`, or `None` if that pad was never seen.
54    #[must_use] pub fn state(&self, id: GamepadId) -> Option<GamepadState> {
55        self.pads.iter().find(|p| p.id == id).copied()
56    }
57
58    /// The first currently-connected pad — the common single-controller
59    /// case, so a callback doesn't have to track ids.
60    #[must_use] pub fn primary(&self) -> Option<GamepadState> {
61        self.pads.iter().find(|p| p.connected).copied()
62    }
63
64    /// Every pad slot seen this session (connected or not).
65    #[must_use] pub fn gamepads(&self) -> &[GamepadState] {
66        &self.pads
67    }
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    pub fn set_state(&mut self, state: GamepadState) -> bool {
73        let changed = if let Some(slot) = self.pads.iter_mut().find(|p| p.id == state.id) {
74            let changed = !state_bitwise_eq(slot, &state);
75            *slot = state;
76            changed
77        } else {
78            self.pads.push(state);
79            true
80        };
81        if changed {
82            self.pending_event = true;
83        }
84        changed
85    }
86
87    /// Clear the pending-event flag. The dll calls this after the event pass
88    /// has collected the `GamepadInput` event.
89    pub const fn clear_pending_event(&mut self) {
90        self.pending_event = false;
91    }
92
93    /// Relayout walk reports whether any node listens for `GamepadInput`.
94    pub const fn set_has_listeners(&mut self, has: bool) {
95        self.has_listeners = has;
96    }
97
98    /// `true` while the capability pump should poll the controller backend.
99    #[must_use] pub const fn has_listeners(&self) -> bool {
100        self.has_listeners
101    }
102}
103
104impl 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    fn get_pending_events(&self, timestamp: Instant) -> Vec<SyntheticEvent> {
109        if self.pending_event {
110            alloc::vec![SyntheticEvent::new(
111                EventType::GamepadInput,
112                CoreEventSource::User,
113                DomNodeId::ROOT,
114                timestamp,
115                EventData::None,
116            )]
117        } else {
118            Vec::new()
119        }
120    }
121}
122
123fn state_bitwise_eq(a: &GamepadState, b: &GamepadState) -> bool {
124    a.id == b.id
125        && a.connected == b.connected
126        && a.buttons == b.buttons
127        && a.left_stick_x.to_bits() == b.left_stick_x.to_bits()
128        && a.left_stick_y.to_bits() == b.left_stick_y.to_bits()
129        && a.right_stick_x.to_bits() == b.right_stick_x.to_bits()
130        && a.right_stick_y.to_bits() == b.right_stick_y.to_bits()
131        && a.left_z.to_bits() == b.left_z.to_bits()
132        && a.right_z.to_bits() == b.right_z.to_bits()
133}
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
143static 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.
147pub fn push_gamepad_state(state: GamepadState) {
148    let mut q = PENDING_STATES.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
149    q.push(state);
150}
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).
155pub fn drain_gamepad_states() -> Vec<GamepadState> {
156    let mut q = PENDING_STATES.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
157    core::mem::take(&mut *q)
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    fn st(id: u32, connected: bool, buttons: u32) -> GamepadState {
165        let mut s = GamepadState::empty(GamepadId { id });
166        s.connected = connected;
167        s.buttons = buttons;
168        s
169    }
170
171    #[test]
172    fn manager_upserts_by_id_and_flags_change() {
173        let mut mgr = GamepadManager::new();
174        assert_eq!(mgr.state(GamepadId { id: 0 }), None);
175        // First state for an id is a change + adds a slot.
176        assert!(mgr.set_state(st(0, true, 0b1)));
177        assert!(mgr.state(GamepadId { id: 0 }).is_some());
178        // Same state again — no change.
179        assert!(!mgr.set_state(st(0, true, 0b1)));
180        // Different buttons — change, same slot (not a new pad).
181        assert!(mgr.set_state(st(0, true, 0b11)));
182        assert_eq!(mgr.gamepads().len(), 1);
183        // A second pad adds a slot.
184        assert!(mgr.set_state(st(1, true, 0)));
185        assert_eq!(mgr.gamepads().len(), 2);
186    }
187
188    #[test]
189    fn primary_is_first_connected() {
190        let mut mgr = GamepadManager::new();
191        mgr.set_state(st(0, false, 0)); // disconnected
192        mgr.set_state(st(1, true, 0));
193        assert_eq!(mgr.primary().map(|p| p.id.id), Some(1));
194    }
195
196    #[test]
197    fn is_pressed_decodes_the_bitset() {
198        let s = st(0, true, GamepadButton::South.bit() | GamepadButton::Start.bit());
199        assert!(s.is_pressed(GamepadButton::South));
200        assert!(s.is_pressed(GamepadButton::Start));
201        assert!(!s.is_pressed(GamepadButton::East));
202    }
203
204    #[test]
205    fn listener_flag_gates_polling_decision() {
206        let mut mgr = GamepadManager::new();
207        assert!(!mgr.has_listeners(), "no listeners until the relayout walk reports some");
208        mgr.set_has_listeners(true);
209        assert!(mgr.has_listeners());
210        mgr.set_has_listeners(false);
211        assert!(!mgr.has_listeners());
212    }
213
214    #[test]
215    fn states_round_trip_through_the_channel() {
216        drop(drain_gamepad_states());
217        push_gamepad_state(st(0, true, 0b1));
218        push_gamepad_state(st(0, true, 0b10)); // last per id wins
219        push_gamepad_state(st(1, true, 0));
220        let drained = drain_gamepad_states();
221        assert_eq!(drained.len(), 3);
222
223        let mut mgr = GamepadManager::new();
224        for s in &drained {
225            mgr.set_state(*s);
226        }
227        assert_eq!(mgr.state(GamepadId { id: 0 }).map(|p| p.buttons), Some(0b10));
228        assert_eq!(mgr.gamepads().len(), 2);
229        assert!(drain_gamepad_states().is_empty());
230    }
231}
232
233#[cfg(test)]
234mod 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}