Skip to main content

sgl_input/
lib.rs

1//! Caller-polled controller input. Games own the window/event loop, focus,
2//! bindings, active-player selection and menu behavior.
3//!
4//! Construct and poll on the window's main thread. macOS uses Apple's Game
5//! Controller framework; other targets use Gilrs. See the `controllers` example.
6use std::collections::{BTreeMap, HashMap, HashSet};
7
8#[cfg(target_os = "macos")]
9#[allow(unsafe_code)]
10mod macos;
11#[cfg(target_os = "macos")]
12use macos::Backend;
13#[cfg(not(target_os = "macos"))]
14mod portable;
15#[cfg(not(target_os = "macos"))]
16use portable::Backend;
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
19pub struct GamepadId(pub(crate) usize);
20
21/// Face buttons are named by position: South is Xbox A / `PlayStation` Cross.
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
23pub enum Button {
24    South,
25    East,
26    West,
27    North,
28    DPadUp,
29    DPadDown,
30    DPadLeft,
31    DPadRight,
32    LeftTrigger,
33    RightTrigger,
34    LeftTrigger2,
35    RightTrigger2,
36    LeftThumb,
37    RightThumb,
38    Select,
39    Start,
40    Mode,
41}
42
43/// Stick axes range from -1 to 1. Positive Y always points up.
44#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
45pub enum Axis {
46    LeftStickX,
47    LeftStickY,
48    RightStickX,
49    RightStickY,
50}
51
52#[derive(Clone, Copy, Debug, PartialEq)]
53pub enum EventType {
54    Connected,
55    Disconnected,
56    ButtonPressed(Button),
57    ButtonReleased(Button),
58    AxisChanged(Axis, f32),
59}
60#[derive(Clone, Copy, Debug, PartialEq)]
61pub struct Event {
62    pub id: GamepadId,
63    pub event: EventType,
64}
65
66#[derive(Debug)]
67pub struct Gamepad {
68    name: String,
69    pressed: HashSet<Button>,
70    axes: HashMap<Axis, f32>,
71}
72impl Gamepad {
73    #[must_use]
74    pub fn name(&self) -> &str {
75        &self.name
76    }
77    #[must_use]
78    pub fn is_pressed(&self, button: Button) -> bool {
79        self.pressed.contains(&button)
80    }
81    #[must_use]
82    pub fn value(&self, axis: Axis) -> f32 {
83        self.axes.get(&axis).copied().unwrap_or(0.0)
84    }
85}
86
87pub(crate) enum Update {
88    Connected(GamepadId, String),
89    Input(Event),
90}
91
92#[derive(Default)]
93struct State {
94    pads: BTreeMap<GamepadId, Gamepad>,
95}
96impl State {
97    fn apply(&mut self, update: Update) -> Option<Event> {
98        let event = match update {
99            Update::Connected(id, name) => {
100                self.pads.insert(
101                    id,
102                    Gamepad {
103                        name,
104                        pressed: HashSet::new(),
105                        axes: HashMap::new(),
106                    },
107                );
108                return Some(Event {
109                    id,
110                    event: EventType::Connected,
111                });
112            }
113            Update::Input(event) => event,
114        };
115        if event.event == EventType::Disconnected {
116            self.pads.remove(&event.id)?;
117        } else {
118            // Callbacks already in flight for a removed controller are ignored.
119            let pad = self.pads.get_mut(&event.id)?;
120            match event.event {
121                EventType::ButtonPressed(button) => {
122                    // A press of a held button (the seeded state and the
123                    // handler both reporting one press on macOS) is dropped.
124                    if !pad.pressed.insert(button) {
125                        return None;
126                    }
127                }
128                EventType::ButtonReleased(button) => {
129                    // A release with no reported press (held when the pad
130                    // connected on Gilrs) never reaches the game unmatched.
131                    if !pad.pressed.remove(&button) {
132                        return None;
133                    }
134                }
135                EventType::AxisChanged(axis, value) => {
136                    pad.axes.insert(axis, value);
137                }
138                _ => {}
139            }
140        }
141        Some(event)
142    }
143}
144
145pub struct Gamepads {
146    backend: Backend,
147    state: State,
148}
149impl Gamepads {
150    pub fn new() -> Result<Self, String> {
151        Ok(Self {
152            backend: Backend::new()?,
153            state: State::default(),
154        })
155    }
156    /// Drain ordered events and update current state. Poll even while unfocused
157    /// so disconnects/releases are consumed; gate game actions on window focus.
158    /// Press/release pairs remain in the event list even within a single poll.
159    pub fn poll(&mut self) -> Vec<Event> {
160        self.backend
161            .poll()
162            .into_iter()
163            .filter_map(|event| self.state.apply(event))
164            .collect()
165    }
166    #[must_use]
167    pub fn gamepad(&self, id: GamepadId) -> Option<&Gamepad> {
168        self.state.pads.get(&id)
169    }
170    pub fn gamepads(&self) -> impl Iterator<Item = (GamepadId, &Gamepad)> {
171        self.state.pads.iter().map(|(&id, pad)| (id, pad))
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178    #[test]
179    fn quick_taps_survive_polling_and_disconnect_clears_held_state() {
180        let mut state = State::default();
181        let id = GamepadId(0);
182        state.apply(Update::Connected(id, "pad".into()));
183        let events: Vec<_> = [
184            EventType::ButtonPressed(Button::South),
185            EventType::ButtonReleased(Button::South),
186        ]
187        .into_iter()
188        .filter_map(|event| state.apply(Update::Input(Event { id, event })))
189        .collect();
190        assert_eq!(
191            events.len(),
192            2,
193            "a complete tap between frames must remain actionable"
194        );
195        assert!(!state.pads[&id].is_pressed(Button::South));
196        state.apply(Update::Input(Event {
197            id,
198            event: EventType::ButtonPressed(Button::DPadLeft),
199        }));
200        assert!(state.pads[&id].is_pressed(Button::DPadLeft));
201        state.apply(Update::Input(Event {
202            id,
203            event: EventType::Disconnected,
204        }));
205        assert!(state.pads.is_empty());
206        assert!(
207            state
208                .apply(Update::Input(Event {
209                    id,
210                    event: EventType::ButtonPressed(Button::South)
211                }))
212                .is_none()
213        );
214        let new_id = GamepadId(1);
215        state.apply(Update::Connected(new_id, "pad".into()));
216        assert!(!state.pads[&new_id].is_pressed(Button::DPadLeft));
217    }
218
219    /// #317: a release with no reported press (a button held when the pad
220    /// connected) yields no event and changes no state; a matched release
221    /// still reports.
222    #[test]
223    fn an_unmatched_release_is_dropped() {
224        let mut state = State::default();
225        let id = GamepadId(0);
226        state.apply(Update::Connected(id, "pad".into()));
227        state.apply(Update::Input(Event {
228            id,
229            event: EventType::ButtonPressed(Button::East),
230        }));
231        let release = |button| {
232            Update::Input(Event {
233                id,
234                event: EventType::ButtonReleased(button),
235            })
236        };
237        assert!(state.apply(release(Button::South)).is_none());
238        assert!(state.pads[&id].is_pressed(Button::East));
239        assert!(!state.pads[&id].is_pressed(Button::South));
240        assert!(state.apply(release(Button::East)).is_some());
241        assert!(!state.pads[&id].is_pressed(Button::East));
242    }
243
244    /// #531: a press reported twice without a release in between (macOS
245    /// seeding held state as its handler fires) reaches the game once, so
246    /// presses and releases stay paired (specs/input.md).
247    #[test]
248    fn a_repeated_press_is_reported_once() {
249        let mut state = State::default();
250        let id = GamepadId(0);
251        state.apply(Update::Connected(id, "pad".into()));
252        let events: Vec<_> = [
253            EventType::ButtonPressed(Button::South),
254            EventType::ButtonPressed(Button::South),
255            EventType::ButtonReleased(Button::South),
256        ]
257        .into_iter()
258        .filter_map(|event| state.apply(Update::Input(Event { id, event })))
259        .map(|event| event.event)
260        .collect();
261        assert_eq!(
262            events,
263            [
264                EventType::ButtonPressed(Button::South),
265                EventType::ButtonReleased(Button::South),
266            ]
267        );
268        assert!(!state.pads[&id].is_pressed(Button::South));
269    }
270}