1use 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#[derive(Debug, Clone, PartialEq, Default)]
33pub struct GamepadManager {
34 pads: Vec<GamepadState>,
37 pending_event: bool,
40 has_listeners: bool,
46}
47
48impl GamepadManager {
49 #[must_use] pub fn new() -> Self {
50 Self::default()
51 }
52
53 #[must_use] pub fn state(&self, id: GamepadId) -> Option<GamepadState> {
55 self.pads.iter().find(|p| p.id == id).copied()
56 }
57
58 #[must_use] pub fn primary(&self) -> Option<GamepadState> {
61 self.pads.iter().find(|p| p.connected).copied()
62 }
63
64 #[must_use] pub fn gamepads(&self) -> &[GamepadState] {
66 &self.pads
67 }
68
69 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 pub const fn clear_pending_event(&mut self) {
90 self.pending_event = false;
91 }
92
93 pub const fn set_has_listeners(&mut self, has: bool) {
95 self.has_listeners = has;
96 }
97
98 #[must_use] pub const fn has_listeners(&self) -> bool {
100 self.has_listeners
101 }
102}
103
104impl EventProvider for GamepadManager {
105 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
135static PENDING_STATES: std::sync::Mutex<Vec<GamepadState>> = std::sync::Mutex::new(Vec::new());
144
145pub 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
152pub 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 assert!(mgr.set_state(st(0, true, 0b1)));
177 assert!(mgr.state(GamepadId { id: 0 }).is_some());
178 assert!(!mgr.set_state(st(0, true, 0b1)));
180 assert!(mgr.set_state(st(0, true, 0b11)));
182 assert_eq!(mgr.gamepads().len(), 1);
183 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)); 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)); 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 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 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 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 #[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 #[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 #[test]
354 fn state_does_not_alias_neighbouring_or_truncated_ids() {
355 let mut mgr = GamepadManager::new();
356 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 #[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 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 #[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 #[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)); 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 #[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)); 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 #[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)); 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)); assert_eq!(mgr.primary(), None);
479 assert_eq!(mgr.gamepads().len(), 2);
480
481 mgr.set_state(connected(0)); 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 #[test]
494 fn gamepads_keeps_one_slot_per_id_in_arrival_order() {
495 let mut mgr = GamepadManager::new();
496 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 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 #[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 #[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 #[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 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 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 #[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 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 #[test]
627 fn set_state_notices_a_change_in_every_single_field() {
628 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 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 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 #[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 #[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 #[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 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 #[test]
740 fn clear_pending_event_is_idempotent_and_touches_nothing_else() {
741 let mut mgr = GamepadManager::new();
742 mgr.clear_pending_event(); 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 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 #[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 mgr.set_state(connected(0));
789 assert!(!mgr.has_listeners(), "a connected pad must not arm the pump on its own");
790 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 #[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, ©), "a bit-identical copy must compare equal");
818 assert!(state_bitwise_eq(©, &s), "…symmetrically");
819 }
820
821 #[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); 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 #[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 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 #[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 #[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 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 #[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}