use std::sync::Arc;
use crate::{
channel::HidppChannel,
feature::{CreatableFeature, Feature, FeatureEndpoint},
protocol::v20::Hidpp20Error,
};
pub const THUMBWHEEL_GESTURE_ID: u8 = 46;
const MAX_DESCRIPTOR_FIELDS: u16 = 1024;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ThumbwheelGesture {
pub diversion_index: Option<u16>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum DescriptorScan {
Thumbwheel(ThumbwheelGesture),
End,
Continue { next_diversion_index: u16 },
}
fn scan_descriptor_page(payload: &[u8], mut diversion_index: u16) -> DescriptorScan {
for field in payload.chunks_exact(2).take(8) {
let high = field[0];
let low = field[1];
if high == 0x01 {
return DescriptorScan::End;
}
if high & 0x80 == 0 {
continue;
}
let divertable = high & 0x02 != 0;
if low == THUMBWHEEL_GESTURE_ID {
return DescriptorScan::Thumbwheel(ThumbwheelGesture {
diversion_index: divertable.then_some(diversion_index),
});
}
if divertable {
diversion_index = diversion_index.saturating_add(1);
}
}
DescriptorScan::Continue {
next_diversion_index: diversion_index,
}
}
fn diversion_address(index: u16) -> Result<(u8, u8), Hidpp20Error> {
let offset = u8::try_from(index >> 3).map_err(|_| Hidpp20Error::UnsupportedResponse)?;
let mask = 1u8 << u32::from(index & 7);
Ok((offset, mask))
}
fn diversion_write_payload(index: u16, diverted: bool) -> Result<[u8; 16], Hidpp20Error> {
let (offset, mask) = diversion_address(index)?;
let mut payload = [0u8; 16];
payload[..4].copy_from_slice(&[offset, 0x01, mask, if diverted { mask } else { 0 }]);
Ok(payload)
}
#[derive(Clone)]
pub struct Gestures2Feature {
endpoint: FeatureEndpoint,
}
impl CreatableFeature for Gestures2Feature {
const ID: u16 = 0x6501;
const STARTING_VERSION: u8 = 0;
fn new(chan: Arc<HidppChannel>, device_index: u8, feature_index: u8) -> Self {
Self {
endpoint: FeatureEndpoint::new(chan, device_index, feature_index),
}
}
}
impl Feature for Gestures2Feature {}
impl Gestures2Feature {
pub async fn thumbwheel(&self) -> Result<Option<ThumbwheelGesture>, Hidpp20Error> {
let mut index = 0u16;
let mut diversion_index = 0u16;
while index < MAX_DESCRIPTOR_FIELDS {
let [hi, lo] = index.to_be_bytes();
let payload = self.endpoint.call(0, [hi, lo, 0]).await?.extend_payload();
match scan_descriptor_page(&payload, diversion_index) {
DescriptorScan::Thumbwheel(thumbwheel) => return Ok(Some(thumbwheel)),
DescriptorScan::End => return Ok(None),
DescriptorScan::Continue {
next_diversion_index,
} => {
diversion_index = next_diversion_index;
index = index.saturating_add(8);
}
}
}
Err(Hidpp20Error::UnsupportedResponse)
}
pub async fn has_thumbwheel(&self) -> Result<bool, Hidpp20Error> {
Ok(self.thumbwheel().await?.is_some())
}
pub async fn thumbwheel_diverted(&self) -> Result<Option<bool>, Hidpp20Error> {
let Some(index) = self.thumbwheel().await?.and_then(|g| g.diversion_index) else {
return Ok(None);
};
let (offset, mask) = diversion_address(index)?;
let payload = self
.endpoint
.call(3, [offset, 0x01, mask])
.await?
.extend_payload();
Ok(Some(payload[0] & mask != 0))
}
pub async fn set_thumbwheel_diverted(&self, diverted: bool) -> Result<bool, Hidpp20Error> {
let Some(index) = self.thumbwheel().await?.and_then(|g| g.diversion_index) else {
return Ok(false);
};
self.endpoint
.call_long(4, diversion_write_payload(index, diverted)?)
.await?;
Ok(true)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn descriptor_page_detects_thumbwheel_and_end_marker() {
let mut payload = [0u8; 16];
payload[0] = 0x83; payload[1] = THUMBWHEEL_GESTURE_ID;
assert_eq!(
scan_descriptor_page(&payload, 0),
DescriptorScan::Thumbwheel(ThumbwheelGesture {
diversion_index: Some(0)
})
);
let mut end = [0u8; 16];
end[0] = 0x01;
assert_eq!(scan_descriptor_page(&end, 0), DescriptorScan::End);
}
#[test]
fn descriptor_page_ignores_other_gestures() {
let mut payload = [0u8; 16];
payload[0] = 0x83;
payload[1] = 45; assert_eq!(
scan_descriptor_page(&payload, 0),
DescriptorScan::Continue {
next_diversion_index: 1
}
);
}
#[test]
fn descriptor_page_counts_divertable_gestures_before_thumbwheel() {
let mut payload = [0u8; 16];
payload[0] = 0x82; payload[1] = 40;
payload[2] = 0x80; payload[3] = 41;
payload[4] = 0x82; payload[5] = THUMBWHEEL_GESTURE_ID;
assert_eq!(
scan_descriptor_page(&payload, 3),
DescriptorScan::Thumbwheel(ThumbwheelGesture {
diversion_index: Some(4)
})
);
}
#[test]
fn diversion_write_payload_uses_offset_mask_and_value() {
let enabled = diversion_write_payload(9, true).unwrap();
assert_eq!(&enabled[..4], &[1, 1, 2, 2]);
let disabled = diversion_write_payload(9, false).unwrap();
assert_eq!(&disabled[..4], &[1, 1, 2, 0]);
}
}