Skip to main content

crcbl_input/
device.rs

1//! Which kind of device spoke last — what a UI's mixed-input rule reads to
2//! decide between showing hover and showing focus, and what
3//! [`ActionMap::hint`] reads to show `Space` or a pad button.
4//!
5//! **Only activity counts.** A key or button press, pointer movement, a wheel
6//! turn, an on-screen control pressed or deflected, and a pad button pressed or
7//! a pad stick or trigger pushed out past
8//! [`PAD_ACTIVITY_THRESHOLD`](crate::PAD_ACTIVITY_THRESHOLD) each set it; a
9//! release does not, since letting go of a key after reaching for the mouse is not the
10//! keyboard speaking, and neither does a zero delta. It is tracked from every
11//! event the map receives, whether or not any binding reads that input.
12
13use super::{ActionMap, Binding, PadKind};
14
15/// A kind of input device.
16#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
17pub enum Device {
18    /// A key.
19    Keyboard,
20    /// A mouse, or a platform's emulated pointer — which is how a phone's
21    /// primary contact arrives.
22    Pointer,
23    /// An on-screen control, reported through [`ActionMap::virtual_button`] or
24    /// [`ActionMap::virtual_stick`].
25    Touch,
26    /// A gamepad. **Every backend reports through
27    /// [`ActionMap::gamepad_event`]**, so which one spoke — XInput, evdev,
28    /// Steam Input — is not something this can tell apart, by design.
29    Gamepad,
30}
31
32impl ActionMap {
33    /// The kind of device that last spoke, or `None` before any did.
34    #[must_use]
35    pub fn last_device(&self) -> Option<Device> {
36        self.devices.first().copied()
37    }
38
39    /// Whether [`ActionMap::last_device`] changed since the last
40    /// [`ActionMap::begin_tick`] — the edge a game swapping its control
41    /// prompts reads, as it reads [`ActionMap::just_pressed`] for a button.
42    ///
43    /// Set only when a different device takes the front: the keyboard
44    /// speaking again while it is already the last device changes nothing,
45    /// and neither does a device moving up behind it. What counts as speaking
46    /// is `device.rs`'s rule, so a pad stick drifting inside
47    /// [`PAD_ACTIVITY_THRESHOLD`](crate::PAD_ACTIVITY_THRESHOLD) never raises
48    /// it.
49    #[must_use]
50    pub const fn last_device_changed(&self) -> bool {
51        self.device_changed
52    }
53
54    /// The family of the pad that last spoke, by the same rule, or `None`
55    /// before any pad did — what [`ActionMap::hint`] names a pad button after.
56    ///
57    /// Kept apart from [`ActionMap::last_device`] because it outlives it: a
58    /// player who put the pad down for the keyboard and picks it up again is
59    /// still holding the pad they were.
60    #[must_use]
61    pub fn last_pad_kind(&self) -> Option<PadKind> {
62        self.last_pad_kind
63    }
64
65    /// Records that `device` spoke: it moves to the front of the recency
66    /// list, and the rest keep their order.
67    pub(crate) fn spoke(&mut self, device: Device) {
68        if self.devices.first() != Some(&device) {
69            self.device_changed = true;
70        }
71        if let Some(index) = self.devices.iter().position(|&seen| seen == device) {
72            self.devices.remove(index);
73        }
74        self.devices.insert(0, device);
75    }
76}
77
78impl Binding {
79    /// The kind of device this binding listens to — which list of a binding
80    /// asset it is written in (`binding_asset.rs`), and what
81    /// [`ActionMap::hint`] matches against [`ActionMap::last_device`].
82    ///
83    /// A binding that reads two devices belongs to the one its value comes
84    /// from: a [`Binding::ScrollChord`] is the wheel with a key held, so it is
85    /// [`Device::Pointer`]'s. [`Binding::PointerPosition`] is the pointer's
86    /// too, though a phone's primary contact drives it — the platform hands
87    /// that contact over as a pointer.
88    #[must_use]
89    pub const fn device(&self) -> Device {
90        match self {
91            Self::Key(_) | Self::Chord { .. } | Self::KeyAxis { .. } | Self::Wasd { .. } => {
92                Device::Keyboard
93            }
94            Self::MouseButton(_)
95            | Self::ButtonChord { .. }
96            | Self::MouseMotion
97            | Self::MouseScroll
98            | Self::ScrollChord { .. }
99            | Self::PointerPosition { .. } => Device::Pointer,
100            Self::Virtual(_) => Device::Touch,
101            Self::PadButton(_)
102            | Self::PadChord { .. }
103            | Self::PadDpad
104            | Self::PadStick { .. }
105            | Self::PadTrigger { .. } => Device::Gamepad,
106        }
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use crate::{PadButton, PointerAxis};
114    use crcbl_core::input::{KeyCode, PointerButton};
115
116    /// A binding that reads two devices belongs to the one its value comes
117    /// from, as [`Binding::device`] documents.
118    #[test]
119    fn a_binding_belongs_to_the_device_its_value_comes_from() {
120        for (binding, device) in [
121            (
122                Binding::Chord {
123                    modifier: crate::Modifier::Shift,
124                    key: KeyCode::KeyR,
125                },
126                Device::Keyboard,
127            ),
128            (
129                Binding::ScrollChord {
130                    held: KeyCode::ControlLeft,
131                },
132                Device::Pointer,
133            ),
134            (
135                Binding::ButtonChord {
136                    modifier: crate::Modifier::Alt,
137                    button: PointerButton::Right,
138                },
139                Device::Pointer,
140            ),
141            (
142                Binding::PointerPosition {
143                    axis: PointerAxis::Y,
144                },
145                Device::Pointer,
146            ),
147            (Binding::Virtual("stick".to_owned()), Device::Touch),
148            (
149                Binding::PadChord {
150                    modifier: PadButton::LeftShoulder,
151                    button: PadButton::South,
152                },
153                Device::Gamepad,
154            ),
155        ] {
156            assert_eq!(binding.device(), device, "{binding}");
157        }
158    }
159
160    /// **The change edge rises when a different device takes over on a real
161    /// press, and not on a pad stick drifting inside the activity threshold**,
162    /// nor on the same device speaking again; [`ActionMap::begin_tick`] clears
163    /// it.
164    #[test]
165    fn the_last_device_changes_on_a_real_press_and_not_on_drift() {
166        use crate::{GamepadEvent, GamepadId, GamepadSnapshot, PAD_ACTIVITY_THRESHOLD, PadAxis};
167
168        let pad = |map: &mut ActionMap, edit: &dyn Fn(&mut GamepadSnapshot)| {
169            let mut snapshot = GamepadSnapshot::neutral(PadKind::Xbox);
170            edit(&mut snapshot);
171            map.gamepad_event(&GamepadEvent::State {
172                id: GamepadId(4),
173                snapshot,
174            });
175        };
176        let mut map = ActionMap::new();
177        assert!(!map.last_device_changed());
178
179        map.key_event(KeyCode::KeyQ, true);
180        assert!(map.last_device_changed(), "the first device to speak");
181        map.begin_tick(0.0);
182        assert!(!map.last_device_changed(), "begin_tick clears the edge");
183
184        let drift = PAD_ACTIVITY_THRESHOLD * 0.9;
185        pad(&mut map, &|pad| pad.axes[PadAxis::LeftX as usize] = drift);
186        pad(&mut map, &|pad| pad.axes[PadAxis::LeftY as usize] = -drift);
187        assert_eq!(
188            map.last_device(),
189            Some(Device::Keyboard),
190            "drift is not a push"
191        );
192        assert!(!map.last_device_changed(), "and raises nothing");
193
194        map.key_event(KeyCode::KeyE, true);
195        assert!(
196            !map.last_device_changed(),
197            "the keyboard speaking again changes nothing"
198        );
199
200        pad(&mut map, &|pad| pad.buttons.insert(PadButton::South));
201        assert_eq!(map.last_device(), Some(Device::Gamepad));
202        assert!(map.last_device_changed(), "a pad press takes over");
203    }
204
205    /// **Each device takes over when it speaks**, whether or not anything is
206    /// bound to what it said — and a release or a still pointer does not.
207    #[test]
208    fn the_last_device_follows_activity_and_not_releases() {
209        let mut map = ActionMap::new();
210        assert_eq!(map.last_device(), None);
211
212        map.key_event(KeyCode::KeyQ, true);
213        assert_eq!(map.last_device(), Some(Device::Keyboard), "nothing binds Q");
214
215        map.mouse_motion(0.0, 0.0);
216        map.mouse_scroll(0.0, 0.0);
217        assert_eq!(
218            map.last_device(),
219            Some(Device::Keyboard),
220            "a zero delta is not movement"
221        );
222
223        map.mouse_motion(3.0, 0.0);
224        assert_eq!(map.last_device(), Some(Device::Pointer));
225
226        map.key_event(KeyCode::KeyQ, false);
227        assert_eq!(
228            map.last_device(),
229            Some(Device::Pointer),
230            "letting go of a key is not the keyboard speaking",
231        );
232
233        map.virtual_stick("stick", 0.0, 0.0);
234        assert_eq!(
235            map.last_device(),
236            Some(Device::Pointer),
237            "a centred stick said nothing"
238        );
239        map.virtual_stick("stick", 0.5, 0.0);
240        assert_eq!(map.last_device(), Some(Device::Touch));
241
242        map.pointer_position(0.2, 0.3);
243        assert_eq!(map.last_device(), Some(Device::Pointer));
244        map.key_event(KeyCode::Enter, true);
245        map.pointer_position(0.2, 0.3);
246        assert_eq!(
247            map.last_device(),
248            Some(Device::Keyboard),
249            "a pointer re-reporting where it already is has not moved",
250        );
251
252        map.virtual_button("btn", true);
253        assert_eq!(map.last_device(), Some(Device::Touch));
254        map.mouse_button(PointerButton::Left, true);
255        assert_eq!(map.last_device(), Some(Device::Pointer));
256        map.mouse_scroll(0.0, 1.0);
257        map.key_event(KeyCode::Enter, true);
258        assert_eq!(map.last_device(), Some(Device::Keyboard));
259        map.mouse_scroll(0.0, 1.0);
260        assert_eq!(map.last_device(), Some(Device::Pointer));
261    }
262}