Skip to main content

shuyi_app/
pads.rs

1//! Controllers (ticket 0219, game ADR-0034): reads every connected pad's raw
2//! state each frame, with gilrs on native and the browser's Gamepad API
3//! (through `crates/cli/web/gamepad.js`) on web, and hands it to `shuyi-ui`'s [`Pads`],
4//! which holds all the rules (sticks as directions, the Switch-style swap,
5//! several pads as one). Like `keys.rs`, this only translates, and ignores
6//! the pads while the window has no focus (`pad_events`).
7
8#[cfg(not(target_arch = "wasm32"))]
9mod native;
10#[cfg(target_arch = "wasm32")]
11mod web;
12
13use shuyi_ui::RawInputEvent;
14use shuyi_ui::input::{PadId, PadKind, PadState, Pads, StickDef};
15
16/// Every connected controller, polled once per frame.
17pub struct PadInput {
18    /// gilrs; the browser needs nothing kept.
19    #[cfg(not(target_arch = "wasm32"))]
20    source: native::Source,
21    pads: Pads,
22    /// Whether the window had focus at the last poll.
23    focused: bool,
24}
25
26impl PadInput {
27    /// Starts watching for controllers; `stick` is the keymap's stick
28    /// thresholds. If the platform's controller support can't start, the
29    /// game runs without controllers.
30    pub fn new(stick: StickDef) -> Self {
31        Self {
32            #[cfg(not(target_arch = "wasm32"))]
33            source: native::Source::new(),
34            pads: Pads::new(stick),
35            focused: true,
36        }
37    }
38
39    /// This frame's controller button releases, then presses, while the
40    /// window has focus if `focused` (`pad_events`). Pads plugged in or
41    /// removed since the last frame are picked up here.
42    pub fn poll(&mut self, focused: bool) -> Vec<RawInputEvent> {
43        // Read whatever the focus, so gilrs's queue keeps draining.
44        #[cfg(not(target_arch = "wasm32"))]
45        let connected = self.source.read();
46        #[cfg(target_arch = "wasm32")]
47        let connected = web::read();
48        let was = core::mem::replace(&mut self.focused, focused);
49        pad_events(&mut self.pads, &connected, (was, focused))
50    }
51}
52
53/// The controller events of a frame `connected` describes, the window's
54/// focus at the last frame and at this one being `focus`. A window without
55/// focus hears no pad: every held button and stick direction goes up, and
56/// nothing goes down. On the frame focus comes back, its presses are
57/// dropped, so a button held across the change counts once it is let go
58/// and pressed again, as a key does.
59pub(crate) fn pad_events(
60    pads: &mut Pads,
61    connected: &[(PadId, PadKind, PadState)],
62    (was, focused): (bool, bool),
63) -> Vec<RawInputEvent> {
64    // A pad left out counts as removed: its buttons go up.
65    let heard = if focused { connected } else { &[] };
66    let mut events = RawInputEvent::from_pads(pads, heard);
67    if focused && !was {
68        events.retain(|event| !matches!(event, RawInputEvent::PadDown(..)));
69    }
70    events
71}
72
73#[cfg(test)]
74mod tests {
75    use shuyi_ui::input::Button;
76
77    use super::*;
78
79    fn pads() -> Pads {
80        Pads::new(StickDef {
81            press_percent: 50,
82            release_percent: 35,
83        })
84    }
85
86    /// One Xbox pad holding `buttons`, its left stick at `stick`.
87    fn pad(buttons: &[Button], stick: (f32, f32)) -> [(PadId, PadKind, PadState); 1] {
88        let state = PadState {
89            buttons: buttons.iter().copied().collect(),
90            left_stick: stick,
91            ..PadState::default()
92        };
93        [(0, PadKind::Xbox, state)]
94    }
95
96    const FOCUSED: (bool, bool) = (true, true);
97    const LOST: (bool, bool) = (true, false);
98    const AWAY: (bool, bool) = (false, false);
99    const BACK: (bool, bool) = (false, true);
100
101    #[test]
102    fn losing_focus_lets_go_of_buttons_and_stick_directions_and_presses_none() {
103        let mut p = pads();
104        let held = pad(&[Button::South], (1.0, 0.0));
105        let down = pad_events(&mut p, &held, FOCUSED);
106        assert_eq!(down.len(), 2, "{down:?}");
107        let mut up = pad_events(&mut p, &held, LOST);
108        up.sort_by_key(|e| format!("{e:?}"));
109        assert_eq!(
110            up,
111            [
112                RawInputEvent::PadUp(Button::LeftStickRight),
113                RawInputEvent::PadUp(Button::South),
114            ]
115        );
116    }
117
118    #[test]
119    fn a_press_while_unfocused_gives_nothing() {
120        let mut p = pads();
121        assert_eq!(pad_events(&mut p, &pad(&[], (0.0, 0.0)), FOCUSED), []);
122        let pressed = pad(&[Button::East], (0.0, 0.0));
123        assert_eq!(pad_events(&mut p, &pressed, LOST), []);
124        assert_eq!(pad_events(&mut p, &pressed, AWAY), []);
125    }
126
127    #[test]
128    fn a_button_held_across_the_change_counts_once_let_go_and_pressed_again() {
129        let mut p = pads();
130        let held = pad(&[Button::South], (0.0, 0.0));
131        let _ = pad_events(&mut p, &held, FOCUSED);
132        let _ = pad_events(&mut p, &held, LOST);
133        // Focus back, the button still held: no press.
134        assert_eq!(pad_events(&mut p, &held, BACK), []);
135        assert_eq!(pad_events(&mut p, &held, FOCUSED), []);
136        let free = pad(&[], (0.0, 0.0));
137        let up = pad_events(&mut p, &free, FOCUSED);
138        assert_eq!(up, [RawInputEvent::PadUp(Button::South)]);
139        let down = pad_events(&mut p, &held, FOCUSED);
140        assert_eq!(down, [RawInputEvent::PadDown(Button::South, PadKind::Xbox)]);
141    }
142}