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}
41
42impl GamepadManager {
43    pub fn new() -> Self {
44        Self::default()
45    }
46
47    /// Latest state for `id`, or `None` if that pad was never seen.
48    pub fn state(&self, id: GamepadId) -> Option<GamepadState> {
49        self.pads.iter().find(|p| p.id == id).copied()
50    }
51
52    /// The first currently-connected pad — the common single-controller
53    /// case, so a callback doesn't have to track ids.
54    pub fn primary(&self) -> Option<GamepadState> {
55        self.pads.iter().find(|p| p.connected).copied()
56    }
57
58    /// Every pad slot seen this session (connected or not).
59    pub fn gamepads(&self) -> &[GamepadState] {
60        &self.pads
61    }
62
63    /// Apply a state the backend delivered (upsert by id). Returns `true`
64    /// if it advanced (bit-pattern different from the previous slot), so an
65    /// idle controller doesn't make every frame look "changed".
66    pub fn set_state(&mut self, state: GamepadState) -> bool {
67        let changed = if let Some(slot) = self.pads.iter_mut().find(|p| p.id == state.id) {
68            let changed = !state_bitwise_eq(slot, &state);
69            *slot = state;
70            changed
71        } else {
72            self.pads.push(state);
73            true
74        };
75        if changed {
76            self.pending_event = true;
77        }
78        changed
79    }
80
81    /// Clear the pending-event flag. The dll calls this after the event pass
82    /// has collected the `GamepadInput` event.
83    pub fn clear_pending_event(&mut self) {
84        self.pending_event = false;
85    }
86}
87
88impl EventProvider for GamepadManager {
89    /// Yield a window-level `GamepadInput` event when a pad's state advanced
90    /// since the last drain (target = root; read it via
91    /// `CallbackInfo::get_primary_gamepad` / `get_gamepad_state`).
92    fn get_pending_events(&self, timestamp: Instant) -> Vec<SyntheticEvent> {
93        if self.pending_event {
94            alloc::vec![SyntheticEvent::new(
95                EventType::GamepadInput,
96                CoreEventSource::User,
97                DomNodeId::ROOT,
98                timestamp,
99                EventData::None,
100            )]
101        } else {
102            Vec::new()
103        }
104    }
105}
106
107fn state_bitwise_eq(a: &GamepadState, b: &GamepadState) -> bool {
108    a.id == b.id
109        && a.connected == b.connected
110        && a.buttons == b.buttons
111        && a.left_stick_x.to_bits() == b.left_stick_x.to_bits()
112        && a.left_stick_y.to_bits() == b.left_stick_y.to_bits()
113        && a.right_stick_x.to_bits() == b.right_stick_x.to_bits()
114        && a.right_stick_y.to_bits() == b.right_stick_y.to_bits()
115        && a.left_z.to_bits() == b.left_z.to_bits()
116        && a.right_z.to_bits() == b.right_z.to_bits()
117}
118
119// ────────── Async update channel (platform backend → manager) ──────────
120//
121// gilrs / GCController / InputDevice deliver on the backend's poll thread
122// with no handle to the live `GamepadManager` (inside the window's
123// `LayoutWindow`). The backend parks each changed state here; the layout
124// pass drains it and applies the latest per id. Pure Rust — no platform
125// dependency (SUPER_PLAN_2 §0.5). Mirrors the sensor reading channel.
126
127static PENDING_STATES: std::sync::Mutex<Vec<GamepadState>> = std::sync::Mutex::new(Vec::new());
128
129/// Park a gamepad state delivered by a platform backend (in the dll).
130/// Thread-safe; poison-recovering.
131pub fn push_gamepad_state(state: GamepadState) {
132    let mut q = PENDING_STATES.lock().unwrap_or_else(|e| e.into_inner());
133    q.push(state);
134}
135
136/// Drain every state parked by [`push_gamepad_state`], in arrival order.
137/// Called once per layout pass; the caller applies them through
138/// [`GamepadManager::set_state`] (the last per id wins).
139pub fn drain_gamepad_states() -> Vec<GamepadState> {
140    let mut q = PENDING_STATES.lock().unwrap_or_else(|e| e.into_inner());
141    core::mem::take(&mut *q)
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    fn st(id: u32, connected: bool, buttons: u32) -> GamepadState {
149        let mut s = GamepadState::empty(GamepadId { id });
150        s.connected = connected;
151        s.buttons = buttons;
152        s
153    }
154
155    #[test]
156    fn manager_upserts_by_id_and_flags_change() {
157        let mut mgr = GamepadManager::new();
158        assert_eq!(mgr.state(GamepadId { id: 0 }), None);
159        // First state for an id is a change + adds a slot.
160        assert!(mgr.set_state(st(0, true, 0b1)));
161        assert!(mgr.state(GamepadId { id: 0 }).is_some());
162        // Same state again — no change.
163        assert!(!mgr.set_state(st(0, true, 0b1)));
164        // Different buttons — change, same slot (not a new pad).
165        assert!(mgr.set_state(st(0, true, 0b11)));
166        assert_eq!(mgr.gamepads().len(), 1);
167        // A second pad adds a slot.
168        assert!(mgr.set_state(st(1, true, 0)));
169        assert_eq!(mgr.gamepads().len(), 2);
170    }
171
172    #[test]
173    fn primary_is_first_connected() {
174        let mut mgr = GamepadManager::new();
175        mgr.set_state(st(0, false, 0)); // disconnected
176        mgr.set_state(st(1, true, 0));
177        assert_eq!(mgr.primary().map(|p| p.id.id), Some(1));
178    }
179
180    #[test]
181    fn is_pressed_decodes_the_bitset() {
182        let s = st(0, true, GamepadButton::South.bit() | GamepadButton::Start.bit());
183        assert!(s.is_pressed(GamepadButton::South));
184        assert!(s.is_pressed(GamepadButton::Start));
185        assert!(!s.is_pressed(GamepadButton::East));
186    }
187
188    #[test]
189    fn states_round_trip_through_the_channel() {
190        let _ = drain_gamepad_states();
191        push_gamepad_state(st(0, true, 0b1));
192        push_gamepad_state(st(0, true, 0b10)); // last per id wins
193        push_gamepad_state(st(1, true, 0));
194        let drained = drain_gamepad_states();
195        assert_eq!(drained.len(), 3);
196
197        let mut mgr = GamepadManager::new();
198        for s in &drained {
199            mgr.set_state(*s);
200        }
201        assert_eq!(mgr.state(GamepadId { id: 0 }).map(|p| p.buttons), Some(0b10));
202        assert_eq!(mgr.gamepads().len(), 2);
203        assert!(drain_gamepad_states().is_empty());
204    }
205}