Skip to main content

azul_core/
hid.rs

1//! Generic HID — the escape hatch for devices azul does not model.
2//!
3//! `GamepadState` assumes an Xbox-shaped controller: two sticks, two triggers,
4//! a d-pad, a fixed button set. Plenty of real input hardware is not that
5//! shape — flight sticks with dozens of axes, racing wheels with pedal
6//! clusters and force feedback, 6-DOF SpaceMice, foot pedals, Stream Decks,
7//! MIDI controllers, barcode wedges. SDL keeps *joystick* events (arbitrary
8//! axes, hats, balls) separate from *gamepad* events for exactly this reason.
9//!
10//! Rather than grow a taxonomy that can never be complete, this exposes the
11//! raw report and lets the app decode it. That is the same trade the web made
12//! with WebHID, and it is the right one here: azul cannot know what a
13//! particular device's bytes mean, but the app that chose to support that
14//! device does.
15//!
16//! This is deliberately NOT a filter family of its own — a HID report is
17//! window-scoped like raw pointer motion, because it has no position and no
18//! node it belongs to.
19
20use alloc::vec::Vec;
21
22use azul_css::{
23    impl_option, impl_option_inner, impl_vec, impl_vec_clone, impl_vec_debug, impl_vec_partialeq,
24    AzString,
25};
26
27/// Identity of one HID device.
28///
29/// The four numbers are the HID descriptor's own vocabulary, not azul's:
30/// `usage_page` + `usage` say what KIND of device it claims to be (page 0x01
31/// usage 0x04 is a joystick, 0x05 a gamepad, 0x08 a multi-axis controller),
32/// while vendor and product identify the model. An app matches on whichever
33/// pair it needs — usage for "any joystick", vid/pid for "this exact wheel".
34#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
35#[repr(C)]
36pub struct HidDevice {
37    /// USB vendor id.
38    pub vendor_id: u16,
39    /// USB product id.
40    pub product_id: u16,
41    /// HID usage page of the device's top-level collection.
42    pub usage_page: u16,
43    /// HID usage within that page.
44    pub usage: u16,
45    /// Human-readable product string, empty when the device reports none.
46    pub name: AzString,
47    /// The device's SERIAL NUMBER as it reports it (USB `iSerial`; a
48    /// DualSense reports its Bluetooth address), empty when it reports none
49    /// or the platform does not expose it (Windows, see the backend).
50    pub serial: AzString,
51    /// PER-INSTANCE identity (8f-i-a-i, user ruling: two identical pads must
52    /// stay distinct for multiplayer). Stable for the device's connected
53    /// lifetime and never `0` for a real device. Derived from vendor,
54    /// product and serial when a serial is reported - so it survives a
55    /// reconnect - and from the platform's own handle for the device
56    /// otherwise, which is unique among the devices present but may change
57    /// when the device is re-plugged. Reports carry it, so an app keyed on
58    /// this field reads each pad's stream apart from its twin's.
59    pub instance: u64,
60}
61
62impl HidDevice {
63    /// The reconnect-stable instance id of a device with a serial number:
64    /// an FNV-1a hash over vendor, product and serial. `None` when the
65    /// device reports no serial - the caller falls back to its platform
66    /// handle through [`Self::handle_instance`].
67    #[must_use]
68    pub fn serial_instance(vendor_id: u16, product_id: u16, serial: &str) -> Option<u64> {
69        if serial.is_empty() {
70            return None;
71        }
72        let mut h: u64 = 0xcbf2_9ce4_8422_2325;
73        for b in vendor_id
74            .to_le_bytes()
75            .iter()
76            .chain(product_id.to_le_bytes().iter())
77            .chain(serial.as_bytes())
78        {
79            h ^= u64::from(*b);
80            h = h.wrapping_mul(0x0100_0000_01b3);
81        }
82        Some(h | 1)
83    }
84
85    /// An instance id from a platform handle (a device path, a kernel
86    /// object address, a hidraw number) for a device with no serial. Never
87    /// `0`, so "no identity" stays distinguishable from a real one.
88    #[must_use]
89    pub fn handle_instance(handle: &[u8]) -> u64 {
90        let mut h: u64 = 0x84222325_cbf29ce4;
91        for b in handle {
92            h ^= u64::from(*b);
93            h = h.wrapping_mul(0x0100_0000_01b3);
94        }
95        h | 1
96    }
97
98    /// The instance id for this device's identity: the serial-derived one
99    /// when a serial is present, else the handle-derived one.
100    #[must_use]
101    pub fn instance_for(vendor_id: u16, product_id: u16, serial: &str, handle: &[u8]) -> u64 {
102        Self::serial_instance(vendor_id, product_id, serial)
103            .unwrap_or_else(|| Self::handle_instance(handle))
104    }
105}
106
107/// One input report from a HID device.
108///
109/// Bytes exactly as the device sent them. Decoding is the app's job: the
110/// report descriptor that explains the layout is device-specific, and a
111/// framework guessing at it would be wrong more often than useful.
112#[derive(Debug, Clone, PartialEq, Eq)]
113#[repr(C)]
114pub struct HidReport {
115    /// Which device sent it.
116    pub device: HidDevice,
117    /// Report id, or `0` for a device whose descriptor uses no ids.
118    pub report_id: u8,
119    /// The report payload.
120    pub bytes: azul_css::U8Vec,
121}
122
123// FFI collection types. `CallbackInfo::get_hid_devices`/`get_hid_reports` hand
124// these to bindings, and a borrowed `&[T]` is not C-compatible - a slice is a
125// fat pointer whose layout C has no name for. Same treatment `TouchPointVec`
126// and `MonitorVec` already get.
127impl_option!(
128    HidDevice,
129    OptionHidDevice,
130    copy = false,
131    [Debug, Clone, PartialEq, Eq]
132);
133impl_vec!(
134    HidDevice,
135    HidDeviceVec,
136    HidDeviceVecDestructor,
137    HidDeviceVecDestructorType,
138    HidDeviceVecSlice,
139    OptionHidDevice
140);
141impl_vec_debug!(HidDevice, HidDeviceVec);
142impl_vec_clone!(HidDevice, HidDeviceVec, HidDeviceVecDestructor);
143impl_vec_partialeq!(HidDevice, HidDeviceVec);
144
145impl_option!(
146    HidReport,
147    OptionHidReport,
148    copy = false,
149    [Debug, Clone, PartialEq, Eq]
150);
151impl_vec!(
152    HidReport,
153    HidReportVec,
154    HidReportVecDestructor,
155    HidReportVecDestructorType,
156    HidReportVecSlice,
157    OptionHidReport
158);
159impl_vec_debug!(HidReport, HidReportVec);
160impl_vec_clone!(HidReport, HidReportVec, HidReportVecDestructor);
161impl_vec_partialeq!(HidReport, HidReportVec);
162
163/// Collects HID reports from the platform backends.
164///
165/// Poll-and-drain like the sensor and gamepad managers, for the same reason:
166/// a device can report faster than the frame rate, and a callback per report
167/// would swamp an app that only wants this frame's state.
168#[derive(Debug, Clone, Default, PartialEq, Eq)]
169pub struct HidManager {
170    devices: Vec<HidDevice>,
171    pending: Vec<HidReport>,
172}
173
174impl HidManager {
175    #[must_use]
176    pub fn new() -> Self {
177        Self::default()
178    }
179
180    /// Replace the enumerated device list. Called by the backend at startup
181    /// and on hotplug.
182    pub fn set_devices(&mut self, devices: Vec<HidDevice>) {
183        self.devices = devices;
184    }
185
186    /// The devices the platform found.
187    #[must_use]
188    pub fn devices(&self) -> &[HidDevice] {
189        &self.devices
190    }
191
192    /// Queue an input report.
193    pub fn push_report(&mut self, report: HidReport) {
194        self.pending.push(report);
195    }
196
197    /// Read the queued reports without consuming them — what a callback does
198    /// while the event is being dispatched.
199    #[must_use]
200    pub fn reports(&self) -> &[HidReport] {
201        &self.pending
202    }
203
204    /// Drain the queue.
205    pub fn take_reports(&mut self) -> Vec<HidReport> {
206        core::mem::take(&mut self.pending)
207    }
208}
209
210#[cfg(test)]
211mod instance_tests {
212    use super::*;
213
214    /// Two identical pads (same vendor and product) with different serials
215    /// get different, reconnect-stable ids; the same pad gets the same id
216    /// every time.
217    #[test]
218    fn identical_pads_with_different_serials_stay_distinct() {
219        let a = HidDevice::serial_instance(0x054c, 0x0ce6, "AA:BB:CC:DD:EE:01").unwrap();
220        let b = HidDevice::serial_instance(0x054c, 0x0ce6, "AA:BB:CC:DD:EE:02").unwrap();
221        assert_ne!(a, b);
222        assert_eq!(
223            a,
224            HidDevice::serial_instance(0x054c, 0x0ce6, "AA:BB:CC:DD:EE:01").unwrap(),
225            "stable across reconnects"
226        );
227        assert_ne!(a, 0);
228    }
229
230    /// No serial: the platform handle decides, and it is never zero.
231    #[test]
232    fn a_device_without_a_serial_falls_back_to_its_handle() {
233        assert_eq!(HidDevice::serial_instance(1, 2, ""), None);
234        let x = HidDevice::instance_for(1, 2, "", b"/dev/hidraw3");
235        let y = HidDevice::instance_for(1, 2, "", b"/dev/hidraw4");
236        assert_ne!(x, y);
237        assert_ne!(x, 0);
238        assert_eq!(x, HidDevice::handle_instance(b"/dev/hidraw3"));
239        // A serial outranks the handle.
240        assert_eq!(
241            HidDevice::instance_for(1, 2, "S1", b"/dev/hidraw3"),
242            HidDevice::serial_instance(1, 2, "S1").unwrap()
243        );
244    }
245}