Skip to main content

crcbl_input/evdev/
map.rs

1//! What an evdev device's codes mean on the seam: which devices are pads, which
2//! key is which button, and how an axis's range becomes −1…1 or 0…1.
3//!
4//! Pure, and compiled into every target's tests: nothing here touches a device.
5//! The table itself is in the parent module's docs.
6
7use super::ffi::{
8    ABS_BRAKE, ABS_CNT, ABS_GAS, ABS_HAT0X, ABS_HAT0Y, ABS_HAT2X, ABS_HAT2Y, ABS_RX, ABS_RY,
9    ABS_RZ, ABS_X, ABS_Y, ABS_Z, AbsBits, AbsInfo, BTN_DPAD_DOWN, BTN_DPAD_LEFT, BTN_DPAD_RIGHT,
10    BTN_DPAD_UP, BTN_EAST, BTN_GAMEPAD, BTN_MODE, BTN_NORTH, BTN_SELECT, BTN_SOUTH, BTN_START,
11    BTN_THUMBL, BTN_THUMBR, BTN_TL, BTN_TL2, BTN_TR, BTN_TR2, BTN_WEST, InputId, KeyBits,
12};
13use crate::usb::{self, VENDOR_NINTENDO, VENDOR_SONY};
14use crate::{GamepadSnapshot, PadAxis, PadButton, PadKind};
15
16/// A stick axis from its `input_absinfo` range to −1…1, before any Y flip.
17///
18/// The centre is the range's midpoint rounded up, and **one scale serves both
19/// halves** — the distance from the centre to `maximum` — with the value
20/// clamped to −1…1. That is `xinput::stick_axis`'s rule
21/// generalised: on xpad's −32768…32767 the centre is 0, 32767 reads 1 and the
22/// spare −32768 clamps to −1, and a push of *n* codes reads the same distance
23/// either way. A range with no extent (`maximum <= minimum`) reads 0.
24///
25/// `flat` is not applied: the seam's axes are raw (`gamepad.rs`), so the dead
26/// zone is the binding's.
27#[must_use]
28pub fn stick_axis(value: i32, minimum: i32, maximum: i32) -> f32 {
29    let (value, minimum, maximum) = (i64::from(value), i64::from(minimum), i64::from(maximum));
30    let centre = minimum + (maximum - minimum + 1) / 2;
31    let scale = maximum - centre;
32    if scale <= 0 {
33        return 0.0;
34    }
35    ((value - centre) as f64 / scale as f64).clamp(-1.0, 1.0) as f32
36}
37
38/// A trigger from its `input_absinfo` range to 0…1, `minimum` at rest,
39/// clamped. A range with no extent reads 0.
40#[must_use]
41pub fn trigger_axis(value: i32, minimum: i32, maximum: i32) -> f32 {
42    let (value, minimum, maximum) = (i64::from(value), i64::from(minimum), i64::from(maximum));
43    let range = maximum - minimum;
44    if range <= 0 {
45        return 0.0;
46    }
47    ((value - minimum) as f64 / range as f64).clamp(0.0, 1.0) as f32
48}
49
50/// What the kernel says about a device, read once when it is opened and again
51/// to resynchronise after `SYN_DROPPED`.
52#[derive(Clone, Debug, PartialEq)]
53pub(crate) struct Probe {
54    /// `EVIOCGID`.
55    pub(crate) id: InputId,
56    /// `EVIOCGBIT(EV_KEY)`: the keys it has.
57    pub(crate) keys: KeyBits,
58    /// `EVIOCGBIT(EV_ABS)`: the axes it has.
59    pub(crate) abs: AbsBits,
60    /// `EVIOCGKEY`: the keys held now.
61    pub(crate) held: KeyBits,
62    /// `EVIOCGABS` for every axis in `abs`, with its value now; the default
63    /// for an axis it does not have.
64    pub(crate) axes: [AbsInfo; ABS_CNT],
65}
66
67impl Probe {
68    /// Every axis's value now, indexed by code.
69    pub(crate) fn values(&self) -> [i32; ABS_CNT] {
70        self.axes.map(|info| info.value)
71    }
72}
73
74/// **Whether a device is a gamepad**: it has `BTN_GAMEPAD` (which is
75/// `BTN_SOUTH`) among its keys and `ABS_X` and `ABS_Y` among its axes — the
76/// kernel's gamepad spec (`Documentation/input/gamepad.rst`) requires both of
77/// a pad. A keyboard has neither; a touchpad and a flight stick have the axes
78/// but not the button; the Deck's motion-sensor node has axes and no keys.
79pub(crate) fn is_gamepad(keys: &KeyBits, abs: &AbsBits) -> bool {
80    keys.has(BTN_GAMEPAD) && abs.has(ABS_X) && abs.has(ABS_Y)
81}
82
83/// A device's family, from its vendor and product ids — see `usb::kind_of`.
84pub(crate) fn kind_of(id: InputId) -> PadKind {
85    usb::kind_of(id.vendor, id.product)
86}
87
88/// The buttons every pad maps the same way.
89const COMMON_BUTTONS: [(u16, PadButton); 9] = [
90    (BTN_SOUTH, PadButton::South),
91    (BTN_EAST, PadButton::East),
92    (BTN_TL, PadButton::LeftShoulder),
93    (BTN_TR, PadButton::RightShoulder),
94    (BTN_THUMBL, PadButton::LeftStick),
95    (BTN_THUMBR, PadButton::RightStick),
96    (BTN_START, PadButton::Start),
97    (BTN_SELECT, PadButton::Select),
98    (BTN_MODE, PadButton::Guide),
99];
100
101/// The top and left face buttons for a driver that follows the gamepad spec's
102/// positions: `hid-playstation`, `hid-sony`, `hid-nintendo`.
103const POSITIONAL_FACE: [(u16, PadButton); 2] =
104    [(BTN_NORTH, PadButton::North), (BTN_WEST, PadButton::West)];
105
106/// The top and left face buttons for a driver that names them by the Xbox
107/// letter instead: `xpad`, Steam Input's virtual pad and `hid-steam` report X
108/// as `BTN_X` and Y as `BTN_Y`, and the header aliases those to `BTN_NORTH`
109/// and `BTN_WEST` — so on these pads the code called north is the left button.
110const LETTERED_FACE: [(u16, PadButton); 2] =
111    [(BTN_NORTH, PadButton::West), (BTN_WEST, PadButton::North)];
112
113/// The d-pad as keys, on a device that has them.
114const DPAD_KEYS: [(u16, PadButton); 4] = [
115    (BTN_DPAD_UP, PadButton::DpadUp),
116    (BTN_DPAD_DOWN, PadButton::DpadDown),
117    (BTN_DPAD_LEFT, PadButton::DpadLeft),
118    (BTN_DPAD_RIGHT, PadButton::DpadRight),
119];
120
121/// Each stick axis's code.
122const STICKS: [(u16, PadAxis); 4] = [
123    (ABS_X, PadAxis::LeftX),
124    (ABS_Y, PadAxis::LeftY),
125    (ABS_RX, PadAxis::RightX),
126    (ABS_RY, PadAxis::RightY),
127];
128
129/// Each trigger: the analog axes that may carry it, first match wins, and the
130/// key that stands in for it on a pad with a digital trigger (a Switch Pro's
131/// ZL and ZR). `ABS_Z`/`ABS_RZ` is xpad's and the PlayStation drivers',
132/// `ABS_BRAKE`/`ABS_GAS` Bluetooth Xbox pads', and `ABS_HAT2Y`/`ABS_HAT2X`
133/// `hid-steam`'s.
134const TRIGGERS: [([u16; 3], u16, PadAxis); 2] = [
135    ([ABS_Z, ABS_BRAKE, ABS_HAT2Y], BTN_TL2, PadAxis::LeftTrigger),
136    ([ABS_RZ, ABS_GAS, ABS_HAT2X], BTN_TR2, PadAxis::RightTrigger),
137];
138
139/// How far a hat axis must be from centre, after [`stick_axis`], to press
140/// that d-pad direction: halfway, so a −1…1 hat presses at ±1 and a wider
141/// range is not pressed by noise.
142const HAT_PRESS: f32 = 0.5;
143
144/// What one axis code drives.
145#[derive(Clone, Copy, Debug, PartialEq, Eq)]
146enum Role {
147    /// A stick or trigger axis.
148    Axis(PadAxis),
149    /// The d-pad's left and right, from a hat.
150    HatX,
151    /// The d-pad's up and down, from a hat; −1 is up.
152    HatY,
153}
154
155/// How one device's codes land on a [`GamepadSnapshot`], worked out once from
156/// its [`Probe`].
157#[derive(Clone, Debug, PartialEq)]
158pub(crate) struct Layout {
159    /// The device's family.
160    pub(crate) kind: PadKind,
161    /// The keys it has that are buttons.
162    buttons: Vec<(u16, PadButton)>,
163    /// The axes it has that the seam reads, with their ranges.
164    axes: Vec<(u16, Role, AbsInfo)>,
165    /// Keys that stand in for a trigger it has no axis for.
166    digital_triggers: Vec<(u16, PadAxis)>,
167}
168
169impl Layout {
170    /// The layout of the device `probe` describes.
171    ///
172    /// The d-pad comes from the `BTN_DPAD_*` keys on a device that has any,
173    /// and from `ABS_HAT0X`/`ABS_HAT0Y` otherwise. Not both: `hid-steam`
174    /// reports the Deck's d-pad as keys and its left trackpad on the hat
175    /// axes, and a touch on the pad is not a d-pad press.
176    pub(crate) fn new(probe: &Probe) -> Self {
177        let kind = kind_of(probe.id);
178        let has_key = |code: u16| probe.keys.has(code);
179        let has_abs = |code: u16| probe.abs.has(code);
180        let face = match probe.id.vendor {
181            VENDOR_SONY | VENDOR_NINTENDO => POSITIONAL_FACE,
182            _ => LETTERED_FACE,
183        };
184        let dpad_keys = DPAD_KEYS.iter().any(|&(code, _)| has_key(code));
185        let dpad: &[(u16, PadButton)] = if dpad_keys { &DPAD_KEYS } else { &[] };
186        let buttons = COMMON_BUTTONS
187            .iter()
188            .chain(&face)
189            .chain(dpad)
190            .copied()
191            .filter(|&(code, _)| has_key(code))
192            .collect();
193
194        let info = |code: u16| probe.axes[usize::from(code)];
195        let mut axes: Vec<_> = STICKS
196            .iter()
197            .filter(|&&(code, _)| has_abs(code))
198            .map(|&(code, axis)| (code, Role::Axis(axis), info(code)))
199            .collect();
200        let mut digital_triggers = Vec::new();
201        for (candidates, key, axis) in TRIGGERS {
202            match candidates.into_iter().find(|&code| has_abs(code)) {
203                Some(code) => axes.push((code, Role::Axis(axis), info(code))),
204                None if has_key(key) => digital_triggers.push((key, axis)),
205                None => {}
206            }
207        }
208        if !dpad_keys {
209            for (code, role) in [(ABS_HAT0X, Role::HatX), (ABS_HAT0Y, Role::HatY)] {
210                if has_abs(code) {
211                    axes.push((code, role, info(code)));
212                }
213            }
214        }
215        Self {
216            kind,
217            buttons,
218            axes,
219            digital_triggers,
220        }
221    }
222
223    /// The snapshot a device in this layout is showing, from the keys it
224    /// holds and every axis's value. **evdev's +Y is down**, so both stick Y
225    /// axes are negated onto the seam's +Y up.
226    pub(crate) fn snapshot(&self, held: &KeyBits, values: &[i32; ABS_CNT]) -> GamepadSnapshot {
227        let mut snapshot = GamepadSnapshot::neutral(self.kind);
228        snapshot.buttons = self
229            .buttons
230            .iter()
231            .filter(|&&(code, _)| held.has(code))
232            .map(|&(_, button)| button)
233            .collect();
234        for &(code, axis) in &self.digital_triggers {
235            if held.has(code) {
236                snapshot.axes[axis as usize] = 1.0;
237            }
238        }
239        for &(code, role, info) in &self.axes {
240            let value = values[usize::from(code)];
241            let stick = stick_axis(value, info.minimum, info.maximum);
242            match role {
243                Role::Axis(axis @ (PadAxis::LeftTrigger | PadAxis::RightTrigger)) => {
244                    snapshot.axes[axis as usize] = trigger_axis(value, info.minimum, info.maximum);
245                }
246                Role::Axis(axis @ (PadAxis::LeftY | PadAxis::RightY)) => {
247                    snapshot.axes[axis as usize] = -stick;
248                }
249                Role::Axis(axis) => snapshot.axes[axis as usize] = stick,
250                Role::HatX if stick <= -HAT_PRESS => snapshot.buttons.insert(PadButton::DpadLeft),
251                Role::HatX if stick >= HAT_PRESS => snapshot.buttons.insert(PadButton::DpadRight),
252                Role::HatY if stick <= -HAT_PRESS => snapshot.buttons.insert(PadButton::DpadUp),
253                Role::HatY if stick >= HAT_PRESS => snapshot.buttons.insert(PadButton::DpadDown),
254                Role::HatX | Role::HatY => {}
255            }
256        }
257        snapshot
258    }
259}
260
261#[cfg(test)]
262pub(crate) mod tests {
263    use super::*;
264    use crate::PadButtons;
265    use crate::usb::{PRODUCT_STEAM_DECK, VENDOR_MICROSOFT, VENDOR_VALVE};
266
267    pub(crate) const XBOX: InputId = InputId {
268        bustype: 3,
269        vendor: VENDOR_MICROSOFT,
270        product: 0x028e,
271        version: 0,
272    };
273    const SONY: InputId = InputId {
274        bustype: 3,
275        vendor: VENDOR_SONY,
276        product: 0x0ce6,
277        version: 0,
278    };
279
280    /// xpad's stick range.
281    pub(crate) const XPAD_STICK: (i32, i32) = (-32768, 32767);
282    /// xpad's trigger range.
283    pub(crate) const XPAD_TRIGGER: (i32, i32) = (0, 255);
284
285    /// A device probe: `id`, the `keys` it has, and the `axes` it has with
286    /// their `(minimum, maximum)`, every axis at rest — a trigger at its
287    /// minimum, anything else at the centre of its range — and nothing held.
288    pub(crate) fn probe(id: InputId, keys: &[u16], axes: &[(u16, (i32, i32))]) -> Probe {
289        let is_trigger = |code| TRIGGERS.iter().any(|(axes, _, _)| axes.contains(&code));
290        let mut probe = Probe {
291            id,
292            keys: KeyBits::EMPTY,
293            abs: AbsBits::EMPTY,
294            held: KeyBits::EMPTY,
295            axes: [AbsInfo::default(); ABS_CNT],
296        };
297        for &code in keys {
298            probe.keys.set(code, true);
299        }
300        for &(code, (minimum, maximum)) in axes {
301            probe.abs.set(code, true);
302            probe.axes[usize::from(code)] = AbsInfo {
303                value: if is_trigger(code) {
304                    minimum
305                } else {
306                    minimum + (maximum - minimum + 1) / 2
307                },
308                minimum,
309                maximum,
310                flat: 128,
311                ..AbsInfo::default()
312            };
313        }
314        probe
315    }
316
317    /// Every key an Xbox 360 pad has under xpad.
318    pub(crate) const XPAD_KEYS: [u16; 11] = [
319        BTN_SOUTH, BTN_EAST, BTN_NORTH, BTN_WEST, BTN_TL, BTN_TR, BTN_SELECT, BTN_START, BTN_MODE,
320        BTN_THUMBL, BTN_THUMBR,
321    ];
322
323    /// An Xbox 360 pad under xpad: sticks, `ABS_Z`/`ABS_RZ` triggers, a hat.
324    pub(crate) fn xpad() -> Probe {
325        probe(
326            XBOX,
327            &XPAD_KEYS,
328            &[
329                (ABS_X, XPAD_STICK),
330                (ABS_Y, XPAD_STICK),
331                (ABS_RX, XPAD_STICK),
332                (ABS_RY, XPAD_STICK),
333                (ABS_Z, XPAD_TRIGGER),
334                (ABS_RZ, XPAD_TRIGGER),
335                (ABS_HAT0X, (-1, 1)),
336                (ABS_HAT0Y, (-1, 1)),
337            ],
338        )
339    }
340
341    /// `probe`'s snapshot with `keys` held and `values` on the axes that
342    /// name one, every other axis where the probe left it.
343    fn snapshot(probe: &Probe, keys: &[u16], values: &[(u16, i32)]) -> GamepadSnapshot {
344        let mut held = KeyBits::EMPTY;
345        for &code in keys {
346            held.set(code, true);
347        }
348        let mut current = probe.values();
349        for &(code, value) in values {
350            current[usize::from(code)] = value;
351        }
352        Layout::new(probe).snapshot(&held, &current)
353    }
354
355    fn buttons(list: &[PadButton]) -> PadButtons {
356        list.iter().copied().collect()
357    }
358
359    /// Hand-worked values at both ends and the centre of the ranges real
360    /// drivers declare.
361    #[test]
362    fn stick_values_normalise_against_known_ranges() {
363        // xpad: −32768…32767, centre 0, one scale of 32767.
364        assert_eq!(
365            stick_axis(-32768, -32768, 32767),
366            -1.0,
367            "the spare code clamps"
368        );
369        assert_eq!(stick_axis(-32767, -32768, 32767), -1.0);
370        assert_eq!(stick_axis(0, -32768, 32767), 0.0);
371        assert_eq!(stick_axis(32767, -32768, 32767), 1.0);
372        assert_eq!(
373            stick_axis(-16384, -32768, 32767),
374            -stick_axis(16384, -32768, 32767),
375            "one scale both ways"
376        );
377        // hid-steam's Deck sticks: symmetric −32767…32767.
378        assert_eq!(stick_axis(-32767, -32767, 32767), -1.0);
379        assert_eq!(stick_axis(0, -32767, 32767), 0.0);
380        // A byte-wide HID stick: 0…255, centre 128, scale 127.
381        assert_eq!(stick_axis(128, 0, 255), 0.0);
382        assert_eq!(stick_axis(255, 0, 255), 1.0);
383        assert_eq!(stick_axis(1, 0, 255), -1.0);
384        assert_eq!(stick_axis(0, 0, 255), -1.0, "the spare code clamps");
385        assert!((stick_axis(192, 0, 255) - 64.0 / 127.0).abs() < f32::EPSILON);
386        // A hat, and values a driver reports past its own range.
387        assert_eq!(stick_axis(-1, -1, 1), -1.0);
388        assert_eq!(stick_axis(40000, -32768, 32767), 1.0);
389        // No extent: nothing to scale by.
390        assert_eq!(stick_axis(5, 5, 5), 0.0);
391        assert_eq!(stick_axis(5, 10, 0), 0.0);
392        assert_eq!(stick_axis(i32::MAX, i32::MIN, i32::MAX), 1.0, "no overflow");
393    }
394
395    #[test]
396    fn trigger_values_normalise_against_known_ranges() {
397        assert_eq!(trigger_axis(0, 0, 255), 0.0);
398        assert_eq!(trigger_axis(255, 0, 255), 1.0);
399        assert!((trigger_axis(128, 0, 255) - 128.0 / 255.0).abs() < f32::EPSILON);
400        assert_eq!(trigger_axis(1023, 0, 1023), 1.0, "Xbox One's ten bits");
401        assert_eq!(trigger_axis(32767, 0, 32767), 1.0, "hid-steam's");
402        assert_eq!(trigger_axis(-5, 0, 255), 0.0, "clamped");
403        assert_eq!(trigger_axis(9, 9, 9), 0.0, "no extent");
404    }
405
406    /// **evdev's +Y is down**: a stick pushed to its maximum Y is pulled
407    /// toward the player, and reads −1 on the seam; X is not flipped; and
408    /// `flat` does not zero a small push, since the seam's axes are raw.
409    #[test]
410    fn stick_y_is_flipped_to_up_and_flat_is_not_applied() {
411        let pad = xpad();
412        let pushed_up = snapshot(&pad, &[], &[(ABS_Y, -32768), (ABS_RY, -32768)]);
413        assert_eq!(pushed_up.axis(PadAxis::LeftY), 1.0, "evdev −Y is up");
414        assert_eq!(pushed_up.axis(PadAxis::RightY), 1.0);
415        let pulled_down = snapshot(&pad, &[], &[(ABS_Y, 32767), (ABS_RY, 32767)]);
416        assert_eq!(pulled_down.axis(PadAxis::LeftY), -1.0);
417        assert_eq!(pulled_down.axis(PadAxis::RightY), -1.0);
418        let right = snapshot(&pad, &[], &[(ABS_X, 32767), (ABS_RX, -32768)]);
419        assert_eq!(right.axis(PadAxis::LeftX), 1.0, "+X right, unflipped");
420        assert_eq!(right.axis(PadAxis::RightX), -1.0);
421
422        let inside_flat = snapshot(&pad, &[], &[(ABS_X, 64)]);
423        assert_eq!(pad.axes[usize::from(ABS_X)].flat, 128);
424        assert!((inside_flat.axis(PadAxis::LeftX) - 64.0 / 32767.0).abs() < f32::EPSILON);
425
426        let at_rest = snapshot(&pad, &[], &[]);
427        assert_eq!(at_rest, GamepadSnapshot::neutral(PadKind::Xbox));
428    }
429
430    /// Every button code lands on its position, on a lettered (Xbox) driver
431    /// and on a positional (PlayStation) one.
432    #[test]
433    fn buttons_map_by_position_for_each_driver_family() {
434        let lettered = xpad();
435        for (code, button) in [
436            (BTN_SOUTH, PadButton::South),
437            (BTN_EAST, PadButton::East),
438            (BTN_NORTH, PadButton::West),
439            (BTN_WEST, PadButton::North),
440            (BTN_TL, PadButton::LeftShoulder),
441            (BTN_TR, PadButton::RightShoulder),
442            (BTN_THUMBL, PadButton::LeftStick),
443            (BTN_THUMBR, PadButton::RightStick),
444            (BTN_START, PadButton::Start),
445            (BTN_SELECT, PadButton::Select),
446            (BTN_MODE, PadButton::Guide),
447        ] {
448            assert_eq!(
449                snapshot(&lettered, &[code], &[]).buttons,
450                buttons(&[button]),
451                "xpad {code:#x}"
452            );
453        }
454
455        let positional = probe(
456            SONY,
457            &XPAD_KEYS,
458            &[(ABS_X, XPAD_TRIGGER), (ABS_Y, XPAD_TRIGGER)],
459        );
460        assert_eq!(
461            snapshot(&positional, &[BTN_NORTH], &[]).buttons,
462            buttons(&[PadButton::North]),
463            "hid-playstation's triangle"
464        );
465        assert_eq!(
466            snapshot(&positional, &[BTN_WEST], &[]).buttons,
467            buttons(&[PadButton::West]),
468            "hid-playstation's square"
469        );
470        assert_eq!(snapshot(&positional, &[], &[]).kind, PadKind::PlayStation);
471    }
472
473    /// The hat presses each d-pad direction at its ends, diagonals press two,
474    /// and −1 on the Y hat is up.
475    #[test]
476    fn the_hat_maps_onto_the_dpad() {
477        let pad = xpad();
478        for ((x, y), expected) in [
479            ((0, -1), &[PadButton::DpadUp][..]),
480            ((0, 1), &[PadButton::DpadDown]),
481            ((-1, 0), &[PadButton::DpadLeft]),
482            ((1, 0), &[PadButton::DpadRight]),
483            ((1, -1), &[PadButton::DpadRight, PadButton::DpadUp]),
484            ((0, 0), &[]),
485        ] {
486            assert_eq!(
487                snapshot(&pad, &[], &[(ABS_HAT0X, x), (ABS_HAT0Y, y)]).buttons,
488                buttons(expected),
489                "hat at ({x}, {y})"
490            );
491        }
492    }
493
494    /// A device with `BTN_DPAD_*` keys has its d-pad read from them, and its
495    /// hat axes — `hid-steam`'s left trackpad on the Deck — press nothing.
496    #[test]
497    fn dpad_keys_win_over_the_hat() {
498        let deck = probe(
499            InputId {
500                vendor: VENDOR_VALVE,
501                product: PRODUCT_STEAM_DECK,
502                ..XBOX
503            },
504            &[
505                BTN_SOUTH,
506                BTN_DPAD_UP,
507                BTN_DPAD_DOWN,
508                BTN_DPAD_LEFT,
509                BTN_DPAD_RIGHT,
510            ],
511            &[
512                (ABS_X, (-32767, 32767)),
513                (ABS_Y, (-32767, 32767)),
514                (ABS_HAT0X, (-32767, 32767)),
515                (ABS_HAT0Y, (-32767, 32767)),
516            ],
517        );
518        for (code, button) in DPAD_KEYS {
519            assert_eq!(snapshot(&deck, &[code], &[]).buttons, buttons(&[button]));
520        }
521        let touched = snapshot(&deck, &[], &[(ABS_HAT0X, 32767), (ABS_HAT0Y, -32767)]);
522        assert!(touched.buttons.is_empty(), "{touched:?}");
523        assert_eq!(touched.kind, PadKind::SteamDeck);
524    }
525
526    /// Each trigger reads its first axis the device has, in the order
527    /// `TRIGGERS` lists, and a digital trigger key stands in for a missing
528    /// axis.
529    #[test]
530    fn triggers_come_from_the_first_axis_present() {
531        let sticks = [(ABS_X, XPAD_STICK), (ABS_Y, XPAD_STICK)];
532        let both = probe(
533            XBOX,
534            &[BTN_SOUTH],
535            &[
536                sticks[0],
537                sticks[1],
538                (ABS_Z, XPAD_TRIGGER),
539                (ABS_RZ, XPAD_TRIGGER),
540                (ABS_BRAKE, (0, 1023)),
541                (ABS_GAS, (0, 1023)),
542            ],
543        );
544        let pulled = snapshot(&both, &[], &[(ABS_Z, 255), (ABS_BRAKE, 0), (ABS_GAS, 1023)]);
545        assert_eq!(pulled.axis(PadAxis::LeftTrigger), 1.0, "ABS_Z first");
546        assert_eq!(pulled.axis(PadAxis::RightTrigger), 0.0, "ABS_GAS ignored");
547
548        let bluetooth = probe(
549            XBOX,
550            &[BTN_SOUTH],
551            &[
552                sticks[0],
553                sticks[1],
554                (ABS_BRAKE, (0, 1023)),
555                (ABS_GAS, (0, 1023)),
556            ],
557        );
558        let pulled = snapshot(&bluetooth, &[], &[(ABS_BRAKE, 1023), (ABS_GAS, 0)]);
559        assert_eq!(pulled.axis(PadAxis::LeftTrigger), 1.0, "brake is left");
560        assert_eq!(pulled.axis(PadAxis::RightTrigger), 0.0);
561
562        let steam = probe(
563            XBOX,
564            &[BTN_SOUTH],
565            &[
566                sticks[0],
567                sticks[1],
568                (ABS_HAT2Y, (0, 32767)),
569                (ABS_HAT2X, (0, 32767)),
570            ],
571        );
572        let pulled = snapshot(&steam, &[], &[(ABS_HAT2X, 32767), (ABS_HAT2Y, 0)]);
573        assert_eq!(pulled.axis(PadAxis::RightTrigger), 1.0, "HAT2X is right");
574        assert_eq!(pulled.axis(PadAxis::LeftTrigger), 0.0);
575
576        let digital = probe(
577            InputId {
578                vendor: VENDOR_NINTENDO,
579                ..XBOX
580            },
581            &[BTN_SOUTH, BTN_TL2, BTN_TR2],
582            &sticks,
583        );
584        let pulled = snapshot(&digital, &[BTN_TR2], &[]);
585        assert_eq!(pulled.axis(PadAxis::RightTrigger), 1.0, "ZR held");
586        assert_eq!(pulled.axis(PadAxis::LeftTrigger), 0.0);
587        assert!(pulled.buttons.is_empty(), "a trigger, not a button");
588    }
589
590    /// **A keyboard is not a pad**, nor is anything else with only half of
591    /// what the spec requires.
592    #[test]
593    fn capability_bits_classify_pads() {
594        let class = |probe: &Probe| is_gamepad(&probe.keys, &probe.abs);
595        assert!(class(&xpad()), "an Xbox pad");
596        const KEY_ESC: u16 = 1;
597        const KEY_A: u16 = 30;
598        const KEY_SPACE: u16 = 57;
599        const BTN_LEFT: u16 = 0x110;
600        const BTN_TRIGGER: u16 = 0x120;
601        const BTN_TOUCH: u16 = 0x14a;
602        let axes = [(ABS_X, XPAD_STICK), (ABS_Y, XPAD_STICK)];
603        for (what, keys, axes) in [
604            ("a keyboard", &[KEY_ESC, KEY_A, KEY_SPACE][..], &[][..]),
605            ("a touchpad", &[BTN_LEFT, BTN_TOUCH], &axes[..]),
606            ("a flight stick", &[BTN_TRIGGER], &axes),
607            (
608                "motion sensors",
609                &[],
610                &[
611                    (ABS_X, XPAD_STICK),
612                    (ABS_Y, XPAD_STICK),
613                    (ABS_Z, XPAD_STICK),
614                ],
615            ),
616            ("buttons with no stick", &[BTN_SOUTH, BTN_EAST], &[]),
617            ("a pad missing ABS_Y", &[BTN_SOUTH], &[(ABS_X, XPAD_STICK)]),
618        ] {
619            assert!(!class(&probe(XBOX, keys, axes)), "{what} is not a pad");
620        }
621    }
622
623    #[test]
624    fn vendor_and_product_name_the_family() {
625        let id = |vendor, product| InputId {
626            vendor,
627            product,
628            ..XBOX
629        };
630        assert_eq!(kind_of(id(VENDOR_MICROSOFT, 0x028e)), PadKind::Xbox);
631        assert_eq!(
632            kind_of(id(VENDOR_VALVE, PRODUCT_STEAM_DECK)),
633            PadKind::SteamDeck
634        );
635        assert_eq!(
636            kind_of(id(VENDOR_VALVE, 0x11ff)),
637            PadKind::Xbox,
638            "Steam's virtual pad"
639        );
640        assert_eq!(kind_of(id(VENDOR_SONY, 0x0ce6)), PadKind::PlayStation);
641        assert_eq!(kind_of(id(VENDOR_NINTENDO, 0x2009)), PadKind::Switch);
642        assert_eq!(kind_of(id(0x1234, 0x5678)), PadKind::Generic);
643    }
644}