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}