1
//! POD types for the gamepad / game-controller surface
2
//! (SUPER_PLAN_2 §1 feature 6 + research/03 §"Feature 6").
3
//!
4
//! Cross-platform controller input: `gilrs` on the desktop
5
//! (Windows / Linux / macOS), iOS `GCController` + Android `InputDevice`
6
//! on mobile (research/03). Defined here in `azul-core` so the manager +
7
//! accessors cross the FFI without `azul-layout` as a dependency; the
8
//! stateful side lives in `azul_layout::managers::gamepad::GamepadManager`.
9
//!
10
//! Poll model, like the sensors: the backend keeps a [`GamepadState`]
11
//! snapshot per connected pad current, and a callback reads the latest each
12
//! frame (`CallbackInfo::get_gamepad_state`) to drive movement / menus.
13
//! Button + axis naming follows the SDL / gilrs "standard gamepad" mapping,
14
//! so the face buttons are Xbox-style: South = A, East = B, West = X,
15
//! North = Y.
16

            
17
/// A connected gamepad's id — stable for the lifetime of the connection,
18
/// assigned by the backend on connect. (gilrs `GamepadId` / the platform
19
/// device id, normalised to a `u32`.)
20
#[repr(C)]
21
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
22
pub struct GamepadId {
23
    pub id: u32,
24
}
25

            
26
/// A standard-layout gamepad button. Face buttons are Xbox-style by
27
/// position (South = A / Cross, East = B / Circle, West = X / Square,
28
/// North = Y / Triangle), so layouts stay consistent across vendors.
29
///
30
/// The discriminant order is also the bit position in
31
/// [`GamepadState::buttons`] — don't reorder without bumping the ABI.
32
#[repr(C)]
33
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
34
pub enum GamepadButton {
35
    /// Bottom face button (A / Cross).
36
    South,
37
    /// Right face button (B / Circle).
38
    East,
39
    /// Top face button (Y / Triangle).
40
    North,
41
    /// Left face button (X / Square).
42
    West,
43
    /// Left shoulder button (L1 / LB).
44
    LeftBumper,
45
    /// Right shoulder button (R1 / RB).
46
    RightBumper,
47
    /// Left trigger as a digital press (L2 / LT). Analog value: `LeftZ`.
48
    LeftTrigger,
49
    /// Right trigger as a digital press (R2 / RT). Analog value: `RightZ`.
50
    RightTrigger,
51
    /// Select / Back / Share.
52
    Select,
53
    /// Start / Options / Menu.
54
    Start,
55
    /// Vendor / guide button (Xbox / PS / Home).
56
    Mode,
57
    /// Left stick click (L3).
58
    LeftThumb,
59
    /// Right stick click (R3).
60
    RightThumb,
61
    /// D-pad up.
62
    DPadUp,
63
    /// D-pad down.
64
    DPadDown,
65
    /// D-pad left.
66
    DPadLeft,
67
    /// D-pad right.
68
    DPadRight,
69
}
70

            
71
/// A gamepad analog axis. Stick axes are in `[-1, 1]` (right / up positive);
72
/// trigger axes ([`GamepadAxis::LeftZ`] / [`GamepadAxis::RightZ`]) in
73
/// `[0, 1]`.
74
#[repr(C)]
75
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
76
pub enum GamepadAxis {
77
    /// Left stick horizontal (left −1 … right +1).
78
    LeftStickX,
79
    /// Left stick vertical (down −1 … up +1).
80
    LeftStickY,
81
    /// Right stick horizontal.
82
    RightStickX,
83
    /// Right stick vertical.
84
    RightStickY,
85
    /// Left trigger pressure (0 … 1).
86
    LeftZ,
87
    /// Right trigger pressure (0 … 1).
88
    RightZ,
89
}
90

            
91
/// Snapshot of one gamepad's state. Buttons are a bitset (bit `n` = the
92
/// [`GamepadButton`] with discriminant `n`); axes are explicit fields. All
93
/// POD / `Copy`, so it crosses the FFI by value.
94
#[repr(C)]
95
#[derive(Debug, Clone, Copy, PartialEq)]
96
pub struct GamepadState {
97
    /// Which pad this snapshot is for.
98
    pub id: GamepadId,
99
    /// `false` once the pad disconnects (the manager keeps the last slot so
100
    /// a callback can observe the disconnect).
101
    pub connected: bool,
102
    /// Pressed-button bitset — bit `n` set ⇔ the `GamepadButton` with
103
    /// discriminant `n` is held. Read via [`GamepadState::is_pressed`].
104
    pub buttons: u32,
105
    /// Left stick X in `[-1, 1]`.
106
    pub left_stick_x: f32,
107
    /// Left stick Y in `[-1, 1]`.
108
    pub left_stick_y: f32,
109
    /// Right stick X in `[-1, 1]`.
110
    pub right_stick_x: f32,
111
    /// Right stick Y in `[-1, 1]`.
112
    pub right_stick_y: f32,
113
    /// Left trigger pressure in `[0, 1]`.
114
    pub left_z: f32,
115
    /// Right trigger pressure in `[0, 1]`.
116
    pub right_z: f32,
117
}
118

            
119
impl GamepadButton {
120
    /// This button's bit in [`GamepadState::buttons`].
121
611
    #[must_use] pub const fn bit(self) -> u32 {
122
611
        1u32 << (self as u32)
123
611
    }
124
}
125

            
126
impl GamepadState {
127
    /// An empty (disconnected) state for `id` — all buttons up, axes zero.
128
1351
    #[must_use] pub const fn empty(id: GamepadId) -> Self {
129
1351
        Self {
130
1351
            id,
131
1351
            connected: false,
132
1351
            buttons: 0,
133
1351
            left_stick_x: 0.0,
134
1351
            left_stick_y: 0.0,
135
1351
            right_stick_x: 0.0,
136
1351
            right_stick_y: 0.0,
137
1351
            left_z: 0.0,
138
1351
            right_z: 0.0,
139
1351
        }
140
1351
    }
141

            
142
    /// Whether `button` is currently held.
143
513
    #[must_use] pub const fn is_pressed(&self, button: GamepadButton) -> bool {
144
513
        self.buttons & button.bit() != 0
145
513
    }
146

            
147
    /// The current value of `axis` (sticks `[-1, 1]`, triggers `[0, 1]`).
148
186
    #[must_use] pub const fn axis(&self, axis: GamepadAxis) -> f32 {
149
186
        match axis {
150
31
            GamepadAxis::LeftStickX => self.left_stick_x,
151
31
            GamepadAxis::LeftStickY => self.left_stick_y,
152
31
            GamepadAxis::RightStickX => self.right_stick_x,
153
31
            GamepadAxis::RightStickY => self.right_stick_y,
154
31
            GamepadAxis::LeftZ => self.left_z,
155
31
            GamepadAxis::RightZ => self.right_z,
156
        }
157
186
    }
158
}
159

            
160
// FFI Option wrapper for `CallbackInfo::get_gamepad_state(id) ->
161
// Option<GamepadState>` (mirrors `OptionSensorReading`).
162
impl_option!(
163
    GamepadState,
164
    OptionGamepadState,
165
    [Debug, Clone, Copy, PartialEq]
166
);
167

            
168
#[cfg(test)]
169
mod autotest_generated {
170
    use super::*;
171

            
172
    /// Every `GamepadButton`, in discriminant order. The order here is also
173
    /// the asserted bit order — see `bit_matches_documented_abi`.
174
    const ALL_BUTTONS: [GamepadButton; 17] = [
175
        GamepadButton::South,
176
        GamepadButton::East,
177
        GamepadButton::North,
178
        GamepadButton::West,
179
        GamepadButton::LeftBumper,
180
        GamepadButton::RightBumper,
181
        GamepadButton::LeftTrigger,
182
        GamepadButton::RightTrigger,
183
        GamepadButton::Select,
184
        GamepadButton::Start,
185
        GamepadButton::Mode,
186
        GamepadButton::LeftThumb,
187
        GamepadButton::RightThumb,
188
        GamepadButton::DPadUp,
189
        GamepadButton::DPadDown,
190
        GamepadButton::DPadLeft,
191
        GamepadButton::DPadRight,
192
    ];
193

            
194
    const ALL_AXES: [GamepadAxis; 6] = [
195
        GamepadAxis::LeftStickX,
196
        GamepadAxis::LeftStickY,
197
        GamepadAxis::RightStickX,
198
        GamepadAxis::RightStickY,
199
        GamepadAxis::LeftZ,
200
        GamepadAxis::RightZ,
201
    ];
202

            
203
    /// Bitset of every defined button — bits 0..=16.
204
    const ALL_BUTTONS_MASK: u32 = 0x0001_FFFF;
205

            
206
    /// Writes `v` into the field that `GamepadState::axis` reads for `axis`.
207
    /// Deliberately mirrors `axis()`; a mis-mapping here would still be caught
208
    /// by `axis_reads_each_field_uniquely`, which pokes the fields directly.
209
    fn set_axis(s: &mut GamepadState, axis: GamepadAxis, v: f32) {
210
        match axis {
211
            GamepadAxis::LeftStickX => s.left_stick_x = v,
212
            GamepadAxis::LeftStickY => s.left_stick_y = v,
213
            GamepadAxis::RightStickX => s.right_stick_x = v,
214
            GamepadAxis::RightStickY => s.right_stick_y = v,
215
            GamepadAxis::LeftZ => s.left_z = v,
216
            GamepadAxis::RightZ => s.right_z = v,
217
        }
218
    }
219

            
220
    // ------------------------------------------------------------------
221
    // GamepadButton::bit  (other)
222
    // ------------------------------------------------------------------
223

            
224
    /// no_panic_smoke + shift-overflow guard: `bit()` is `1u32 << (self as
225
    /// u32)`, which is UB / a panic in debug the moment a discriminant reaches
226
    /// 32. Pin the discriminants to a contiguous 0..17 so adding an 18th..32nd
227
    /// button stays safe and a 33rd fails HERE rather than at a user's shift.
228
    #[test]
229
    fn bit_discriminants_are_contiguous_and_shift_safe() {
230
        for (i, b) in ALL_BUTTONS.iter().enumerate() {
231
            let d = *b as u32;
232
            assert_eq!(
233
                d, i as u32,
234
                "{b:?} has discriminant {d}, expected {i} — the bitset in \
235
                 GamepadState::buttons assumes contiguous discriminants"
236
            );
237
            assert!(
238
                d < 32,
239
                "{b:?} discriminant {d} would overflow `1u32 << d` in bit()"
240
            );
241
        }
242
    }
243

            
244
    /// The ABI the doc comment promises ("the discriminant order is also the
245
    /// bit position … don't reorder without bumping the ABI"). Hard-coded so a
246
    /// reorder is a loud test failure, not a silent remap of every FFI client's
247
    /// button bits.
248
    #[test]
249
    fn bit_matches_documented_abi() {
250
        assert_eq!(GamepadButton::South.bit(), 1 << 0);
251
        assert_eq!(GamepadButton::East.bit(), 1 << 1);
252
        assert_eq!(GamepadButton::North.bit(), 1 << 2);
253
        assert_eq!(GamepadButton::West.bit(), 1 << 3);
254
        assert_eq!(GamepadButton::LeftBumper.bit(), 1 << 4);
255
        assert_eq!(GamepadButton::RightBumper.bit(), 1 << 5);
256
        assert_eq!(GamepadButton::LeftTrigger.bit(), 1 << 6);
257
        assert_eq!(GamepadButton::RightTrigger.bit(), 1 << 7);
258
        assert_eq!(GamepadButton::Select.bit(), 1 << 8);
259
        assert_eq!(GamepadButton::Start.bit(), 1 << 9);
260
        assert_eq!(GamepadButton::Mode.bit(), 1 << 10);
261
        assert_eq!(GamepadButton::LeftThumb.bit(), 1 << 11);
262
        assert_eq!(GamepadButton::RightThumb.bit(), 1 << 12);
263
        assert_eq!(GamepadButton::DPadUp.bit(), 1 << 13);
264
        assert_eq!(GamepadButton::DPadDown.bit(), 1 << 14);
265
        assert_eq!(GamepadButton::DPadLeft.bit(), 1 << 15);
266
        assert_eq!(GamepadButton::DPadRight.bit(), 1 << 16);
267
    }
268

            
269
    /// invariant: each bit is a distinct, non-zero power of two. Two buttons
270
    /// sharing a bit would make `is_pressed` report a phantom press.
271
    #[test]
272
    fn bit_is_a_distinct_power_of_two() {
273
        let mut seen = 0u32;
274
        for b in ALL_BUTTONS {
275
            let bit = b.bit();
276
            assert_ne!(bit, 0, "{b:?} maps to bit 0");
277
            assert_eq!(bit.count_ones(), 1, "{b:?} bit {bit:#x} is not a single bit");
278
            assert_eq!(seen & bit, 0, "{b:?} bit {bit:#x} collides with an earlier button");
279
            seen |= bit;
280
        }
281
        assert_eq!(seen, ALL_BUTTONS_MASK);
282
        assert_eq!(seen.count_ones(), ALL_BUTTONS.len() as u32);
283
    }
284

            
285
    /// `bit()` is `const fn` — usable in a `const` item / array length. A
286
    /// non-const-evaluable body (or an overflowing shift, which is a hard
287
    /// compile error in const context) fails to build.
288
    #[test]
289
    fn bit_is_const_evaluable() {
290
        const SOUTH: u32 = GamepadButton::South.bit();
291
        const DPAD_RIGHT: u32 = GamepadButton::DPadRight.bit();
292
        assert_eq!(SOUTH, 1);
293
        assert_eq!(DPAD_RIGHT, 65_536);
294
    }
295

            
296
    // ------------------------------------------------------------------
297
    // GamepadState::empty  (constructor)
298
    // ------------------------------------------------------------------
299

            
300
    /// no_panic + invariants_hold: extreme ids (0, 1, MAX/2, MAX) round-trip
301
    /// into the state unchanged and every other field is the documented zero.
302
    #[test]
303
    fn empty_preserves_id_and_zeroes_everything_else() {
304
        for raw in [0, 1, u32::MAX / 2, u32::MAX - 1, u32::MAX] {
305
            let s = GamepadState::empty(GamepadId { id: raw });
306
            assert_eq!(s.id, GamepadId { id: raw });
307
            assert_eq!(s.id.id, raw);
308
            assert!(!s.connected, "empty() must start disconnected");
309
            assert_eq!(s.buttons, 0);
310
        }
311
    }
312

            
313
    /// default_is_neutral: an empty state is the neutral element — no button
314
    /// reads as pressed and every axis is exactly *positive* zero. The
315
    /// `to_bits()` check is the point: a `-0.0` would still compare `== 0.0`
316
    /// yet flips the sign of anything a caller multiplies by it.
317
    #[test]
318
    fn empty_is_neutral_for_every_button_and_axis() {
319
        let s = GamepadState::empty(GamepadId { id: 42 });
320
        for b in ALL_BUTTONS {
321
            assert!(!s.is_pressed(b), "{b:?} reads as pressed in an empty state");
322
        }
323
        for a in ALL_AXES {
324
            assert_eq!(
325
                s.axis(a).to_bits(),
326
                0.0f32.to_bits(),
327
                "axis {a:?} of an empty state is not +0.0 (got {})",
328
                s.axis(a)
329
            );
330
        }
331
    }
332

            
333
    /// invariant: `empty()` is a pure function of `id` — same id gives an
334
    /// equal state, a different id gives an unequal one (so a stale slot for
335
    /// pad 0 can't be mistaken for pad 1's).
336
    #[test]
337
    fn empty_is_deterministic_and_id_discriminating() {
338
        let a = GamepadState::empty(GamepadId { id: 7 });
339
        let b = GamepadState::empty(GamepadId { id: 7 });
340
        let c = GamepadState::empty(GamepadId { id: 8 });
341
        assert_eq!(a, b);
342
        assert_ne!(a, c);
343
    }
344

            
345
    /// `empty()` is `const fn`, so a backend can build a static slot table.
346
    #[test]
347
    fn empty_is_const_evaluable() {
348
        const S: GamepadState = GamepadState::empty(GamepadId { id: u32::MAX });
349
        assert_eq!(S.id.id, u32::MAX);
350
        assert_eq!(S.buttons, 0);
351
        const _: () = assert!(!S.connected);
352
    }
353

            
354
    // ------------------------------------------------------------------
355
    // GamepadState::is_pressed  (predicate)
356
    // ------------------------------------------------------------------
357

            
358
    /// basic_true_false + isolation: with exactly one bit set, that button —
359
    /// and *only* that button — reads as pressed. Catches an off-by-one shift
360
    /// or a bit collision that a single known-true case would miss.
361
    #[test]
362
    fn is_pressed_isolates_each_single_bit() {
363
        for pressed in ALL_BUTTONS {
364
            let mut s = GamepadState::empty(GamepadId { id: 0 });
365
            s.buttons = pressed.bit();
366
            for other in ALL_BUTTONS {
367
                assert_eq!(
368
                    s.is_pressed(other),
369
                    other == pressed,
370
                    "buttons={:#x}: is_pressed({other:?}) disagrees with the only \
371
                     pressed button {pressed:?}",
372
                    s.buttons
373
                );
374
            }
375
        }
376
    }
377

            
378
    /// edge_inputs: the two saturating bitsets. `0` = nothing pressed,
379
    /// `u32::MAX` = everything pressed. Both are deterministic, neither panics.
380
    #[test]
381
    fn is_pressed_handles_empty_and_full_bitsets() {
382
        let mut s = GamepadState::empty(GamepadId { id: 0 });
383

            
384
        s.buttons = 0;
385
        for b in ALL_BUTTONS {
386
            assert!(!s.is_pressed(b), "{b:?} pressed with buttons == 0");
387
        }
388

            
389
        s.buttons = u32::MAX;
390
        for b in ALL_BUTTONS {
391
            assert!(s.is_pressed(b), "{b:?} not pressed with buttons == u32::MAX");
392
        }
393
    }
394

            
395
    /// Adversarial: a backend (or a hostile FFI caller) writes junk into the
396
    /// 15 *reserved* high bits, 17..=31. No defined button may light up — the
397
    /// mask is per-button, so garbage outside the defined range must be inert.
398
    #[test]
399
    fn is_pressed_ignores_reserved_high_bits() {
400
        let mut s = GamepadState::empty(GamepadId { id: 0 });
401
        for junk in [
402
            !ALL_BUTTONS_MASK,        // every reserved bit
403
            1 << 17,                  // the first reserved bit
404
            1 << 31,                  // the sign bit
405
            0xDEAD_0000 & !ALL_BUTTONS_MASK,
406
        ] {
407
            assert_eq!(junk & ALL_BUTTONS_MASK, 0, "test vector {junk:#x} is not reserved-only");
408
            s.buttons = junk;
409
            for b in ALL_BUTTONS {
410
                assert!(
411
                    !s.is_pressed(b),
412
                    "reserved-bit junk {junk:#x} made {b:?} read as pressed"
413
                );
414
            }
415
        }
416
    }
417

            
418
    /// round-trip: encode a button set into the bitset, decode it back through
419
    /// `is_pressed` — the decoded set must equal the encoded one, and
420
    /// re-encoding must reproduce the exact same bits (encode == decode).
421
    #[test]
422
    fn is_pressed_bitset_roundtrips() {
423
        let subsets: [&[GamepadButton]; 5] = [
424
            &[],
425
            &[GamepadButton::South],
426
            &[GamepadButton::DPadRight],
427
            &[
428
                GamepadButton::South,
429
                GamepadButton::DPadRight,
430
                GamepadButton::Mode,
431
            ],
432
            &ALL_BUTTONS,
433
        ];
434

            
435
        for subset in subsets {
436
            let encoded = subset.iter().fold(0u32, |acc, b| acc | b.bit());
437

            
438
            let mut s = GamepadState::empty(GamepadId { id: 3 });
439
            s.buttons = encoded;
440

            
441
            // Decode through the predicate, then re-encode from what we decoded.
442
            let mut re_encoded = 0u32;
443
            for (i, b) in ALL_BUTTONS.iter().enumerate() {
444
                let decoded = s.is_pressed(*b);
445
                assert_eq!(
446
                    decoded,
447
                    subset.contains(b),
448
                    "decode of {encoded:#x}: is_pressed({b:?}) disagrees with the \
449
                     encoded subset (bit {i})"
450
                );
451
                if decoded {
452
                    re_encoded |= b.bit();
453
                }
454
            }
455
            assert_eq!(re_encoded, encoded, "re-encode of {encoded:#x} is not bit-stable");
456
        }
457
    }
458

            
459
    /// `is_pressed` is `const fn` and takes `&self` — usable on a const state.
460
    #[test]
461
    fn is_pressed_is_const_evaluable() {
462
        const S: GamepadState = GamepadState {
463
            id: GamepadId { id: 0 },
464
            connected: true,
465
            buttons: 0b101, // South (bit 0) | North (bit 2)
466
            left_stick_x: 0.0,
467
            left_stick_y: 0.0,
468
            right_stick_x: 0.0,
469
            right_stick_y: 0.0,
470
            left_z: 0.0,
471
            right_z: 0.0,
472
        };
473
        const SOUTH: bool = S.is_pressed(GamepadButton::South);
474
        const EAST: bool = S.is_pressed(GamepadButton::East);
475
        const NORTH: bool = S.is_pressed(GamepadButton::North);
476
        const _: () = assert!(SOUTH && !EAST && NORTH);
477
    }
478

            
479
    /// invariant: `is_pressed` is a read-only view — polling every button
480
    /// leaves the snapshot byte-identical.
481
    #[test]
482
    fn is_pressed_does_not_mutate_the_snapshot() {
483
        let mut s = GamepadState::empty(GamepadId { id: 9 });
484
        s.buttons = 0x1_0F0F & ALL_BUTTONS_MASK;
485
        let before = s;
486
        for b in ALL_BUTTONS {
487
            let _ = s.is_pressed(b);
488
        }
489
        assert_eq!(s, before);
490
    }
491

            
492
    // ------------------------------------------------------------------
493
    // GamepadState::axis  (other)
494
    // ------------------------------------------------------------------
495

            
496
    /// The copy-paste trap: `axis()` is a six-arm match over six near-identical
497
    /// fields. Give every field a unique sentinel and poke the fields directly
498
    /// (never through a helper that could share the same bug), so any two arms
499
    /// reading the same field — or reading each other's — fails here.
500
    #[test]
501
    fn axis_reads_each_field_uniquely() {
502
        let s = GamepadState {
503
            id: GamepadId { id: 1 },
504
            connected: true,
505
            buttons: 0,
506
            left_stick_x: 1.0,
507
            left_stick_y: 2.0,
508
            right_stick_x: 3.0,
509
            right_stick_y: 4.0,
510
            left_z: 5.0,
511
            right_z: 6.0,
512
        };
513
        assert_eq!(s.axis(GamepadAxis::LeftStickX).to_bits(), 1.0f32.to_bits());
514
        assert_eq!(s.axis(GamepadAxis::LeftStickY).to_bits(), 2.0f32.to_bits());
515
        assert_eq!(s.axis(GamepadAxis::RightStickX).to_bits(), 3.0f32.to_bits());
516
        assert_eq!(s.axis(GamepadAxis::RightStickY).to_bits(), 4.0f32.to_bits());
517
        assert_eq!(s.axis(GamepadAxis::LeftZ).to_bits(), 5.0f32.to_bits());
518
        assert_eq!(s.axis(GamepadAxis::RightZ).to_bits(), 6.0f32.to_bits());
519

            
520
        // …and every axis is pairwise distinct, so no two arms alias one field.
521
        for (i, a) in ALL_AXES.iter().enumerate() {
522
            for b in ALL_AXES.iter().skip(i + 1) {
523
                assert_ne!(
524
                    s.axis(*a).to_bits(),
525
                    s.axis(*b).to_bits(),
526
                    "axes {a:?} and {b:?} read the same field"
527
                );
528
            }
529
        }
530
    }
531

            
532
    /// no_panic_smoke over the nasty floats: NaN (incl. a signalling payload),
533
    /// ±inf, ±0.0, subnormals, MIN/MAX. `axis()` is a getter, so it must hand
534
    /// each one back *bit-for-bit* — no clamping, no NaN canonicalisation,
535
    /// no sign-of-zero loss (a `-0.0` silently flipping to `+0.0` would change
536
    /// the direction of a caller's `v.signum()` deadzone check).
537
    #[test]
538
    fn axis_returns_extreme_floats_bit_exact() {
539
        let nasty: [f32; 12] = [
540
            f32::NAN,
541
            -f32::NAN,
542
            f32::from_bits(0x7F80_0001), // signalling NaN payload
543
            f32::INFINITY,
544
            f32::NEG_INFINITY,
545
            0.0,
546
            -0.0,
547
            f32::MIN_POSITIVE,
548
            f32::from_bits(1), // smallest subnormal
549
            -1.0,
550
            f32::MIN,
551
            f32::MAX,
552
        ];
553

            
554
        for a in ALL_AXES {
555
            for v in nasty {
556
                let mut s = GamepadState::empty(GamepadId { id: 0 });
557
                set_axis(&mut s, a, v);
558
                assert_eq!(
559
                    s.axis(a).to_bits(),
560
                    v.to_bits(),
561
                    "axis {a:?} did not return {v:?} bit-exactly (bits {:#x} vs {:#x})",
562
                    s.axis(a).to_bits(),
563
                    v.to_bits()
564
                );
565
            }
566
            // Writing one axis must not disturb the other five.
567
            let mut s = GamepadState::empty(GamepadId { id: 0 });
568
            set_axis(&mut s, a, f32::MAX);
569
            for other in ALL_AXES.iter().filter(|o| **o != a) {
570
                assert_eq!(s.axis(*other).to_bits(), 0.0f32.to_bits());
571
            }
572
        }
573
    }
574

            
575
    /// Boundary + out-of-range: the doc gives sticks `[-1, 1]` and triggers
576
    /// `[0, 1]`, but `axis()` is a plain accessor — range enforcement is the
577
    /// *backend's* contract, not this getter's. Pin the pass-through so nobody
578
    /// "helpfully" adds a silent clamp that would hide a mis-scaling backend.
579
    #[test]
580
    fn axis_does_not_clamp_out_of_range_values() {
581
        for a in ALL_AXES {
582
            for v in [-1.0f32, 0.0, 1.0, 1.000_001, -1.000_001, 1e9, -1e9] {
583
                let mut s = GamepadState::empty(GamepadId { id: 0 });
584
                set_axis(&mut s, a, v);
585
                assert_eq!(
586
                    s.axis(a).to_bits(),
587
                    v.to_bits(),
588
                    "axis {a:?} clamped or rounded {v}"
589
                );
590
            }
591
        }
592
    }
593

            
594
    /// `axis()` is `const fn`.
595
    #[test]
596
    fn axis_is_const_evaluable() {
597
        const S: GamepadState = GamepadState {
598
            id: GamepadId { id: 0 },
599
            connected: true,
600
            buttons: 0,
601
            left_stick_x: 0.0,
602
            left_stick_y: 0.0,
603
            right_stick_x: 0.0,
604
            right_stick_y: 0.0,
605
            left_z: 1.0,
606
            right_z: 0.0,
607
        };
608
        const LEFT_Z: f32 = S.axis(GamepadAxis::LeftZ);
609
        const RIGHT_Z: f32 = S.axis(GamepadAxis::RightZ);
610
        assert_eq!(LEFT_Z.to_bits(), 1.0f32.to_bits());
611
        assert_eq!(RIGHT_Z.to_bits(), 0.0f32.to_bits());
612
    }
613

            
614
    // ------------------------------------------------------------------
615
    // GamepadState / OptionGamepadState — derived-impl invariants
616
    // ------------------------------------------------------------------
617

            
618
    /// FFI trap, asserted rather than fixed: `GamepadState` derives
619
    /// `PartialEq` over `f32`, so IEEE-754 applies — a snapshot with a NaN axis
620
    /// is *not equal to itself*. Callers that dedupe frames with `==` (e.g.
621
    /// "skip the callback if the state didn't change") will therefore always
622
    /// see a change once a backend reports a NaN axis. Reflexivity holds for
623
    /// every non-NaN state.
624
    #[test]
625
    fn state_equality_is_ieee_not_reflexive_over_nan() {
626
        let mut nan_state = GamepadState::empty(GamepadId { id: 0 });
627
        nan_state.left_stick_x = f32::NAN;
628
        let a = nan_state;
629
        let b = nan_state; // a bit-identical copy
630
        assert_ne!(a, b, "NaN axis: derived PartialEq is expected to be non-reflexive");
631

            
632
        let mut sane = GamepadState::empty(GamepadId { id: 0 });
633
        sane.left_stick_x = 0.5;
634
        let c = sane;
635
        let d = sane;
636
        assert_eq!(c, d);
637
    }
638

            
639
    /// round-trip: `GamepadState` -> `Option` -> `OptionGamepadState` -> back.
640
    /// This is the wrapper `CallbackInfo::get_gamepad_state` returns across the
641
    /// FFI, so encode == decode must hold in both directions, and the default
642
    /// must be the "no such pad" case.
643
    #[test]
644
    fn option_gamepad_state_roundtrips() {
645
        assert!(OptionGamepadState::default().is_none());
646
        assert!(!OptionGamepadState::default().is_some());
647
        assert_eq!(Option::<GamepadState>::from(OptionGamepadState::default()), None);
648

            
649
        let mut s = GamepadState::empty(GamepadId { id: u32::MAX });
650
        s.connected = true;
651
        s.buttons = ALL_BUTTONS_MASK;
652
        s.right_stick_y = -1.0;
653
        s.left_z = 1.0;
654

            
655
        let wrapped: OptionGamepadState = Some(s).into();
656
        assert!(wrapped.is_some());
657
        assert!(!wrapped.is_none());
658
        assert_eq!(wrapped.as_option(), Some(&s));
659
        assert_eq!(Option::<GamepadState>::from(wrapped), Some(s));
660

            
661
        let none: OptionGamepadState = Option::<GamepadState>::None.into();
662
        assert!(none.is_none());
663

            
664
        // `replace` returns the PREVIOUS value (mem::replace semantics).
665
        let mut slot = OptionGamepadState::None;
666
        let prev = slot.replace(s);
667
        assert!(prev.is_none());
668
        assert_eq!(slot.as_option(), Some(&s));
669

            
670
        let other = GamepadState::empty(GamepadId { id: 1 });
671
        let prev = slot.replace(other);
672
        assert_eq!(Option::<GamepadState>::from(prev), Some(s));
673
        assert_eq!(slot.as_option(), Some(&other));
674
    }
675
}