Skip to main content

hidpp/feature/hires_wheel/
mod.rs

1//! Implements the `HiResWheel` feature (ID `0x2121`) that allows configuring
2//! and using high-resolution scrolling.
3
4use std::{hash::Hash, sync::Arc};
5
6use num_enum::{IntoPrimitive, TryFromPrimitive};
7
8use crate::{
9    channel::HidppChannel,
10    event::EventEmitter,
11    feature::{CreatableFeature, EmittingFeature, Feature, FeatureEndpoint, event_payload},
12    nibble::U4,
13    protocol::v20::Hidpp20Error,
14};
15
16/// Implements the `HiResWheel` / `0x2121` feature.
17///
18/// The analytics part of the feature is not implemented here as its data
19/// structure lacks any documentation.
20pub struct HiResWheelFeature {
21    /// The endpoint this feature talks to.
22    endpoint: FeatureEndpoint,
23
24    /// The emitter used to emit events.
25    emitter: Arc<EventEmitter<HiResWheelEvent>>,
26
27    /// The handle assigned to the message listener registered via
28    /// [`HidppChannel::add_msg_listener`].
29    /// This is used to remove the listener when the feature is dropped.
30    msg_listener_hdl: u32,
31}
32
33impl CreatableFeature for HiResWheelFeature {
34    const ID: u16 = 0x2121;
35    const STARTING_VERSION: u8 = 0;
36
37    fn new(chan: Arc<HidppChannel>, device_index: u8, feature_index: u8) -> Self {
38        let emitter = Arc::new(EventEmitter::new());
39
40        let hdl = chan.add_msg_listener({
41            let emitter = Arc::clone(&emitter);
42
43            move |raw, matched| {
44                let Some((func, payload)) =
45                    event_payload(raw, matched, device_index, feature_index)
46                else {
47                    return;
48                };
49
50                // HiResWheel dispatches on the sub-id: 0 = movement, 1 = ratchet switch.
51                let event = match func.to_lo() {
52                    0 => {
53                        let Ok(resolution) =
54                            WheelResolution::try_from((payload[0] & (1 << 4)) >> 4)
55                        else {
56                            return;
57                        };
58
59                        HiResWheelEvent::WheelMovement(WheelMovementData {
60                            resolution,
61                            periods: U4::from_lo(payload[0]),
62                            delta_vertical: i16::from_be_bytes(payload[1..=2].try_into().unwrap()),
63                        })
64                    }
65                    1 => {
66                        let Ok(state) = WheelRatchetState::try_from(payload[0] & 1) else {
67                            return;
68                        };
69
70                        HiResWheelEvent::RatchetSwitch(state)
71                    }
72                    _ => return,
73                };
74
75                emitter.emit(event);
76            }
77        });
78
79        Self {
80            endpoint: FeatureEndpoint::new(chan, device_index, feature_index),
81            emitter,
82            msg_listener_hdl: hdl,
83        }
84    }
85}
86
87impl Feature for HiResWheelFeature {}
88
89impl EmittingFeature<HiResWheelEvent> for HiResWheelFeature {
90    fn listen(&self) -> async_channel::Receiver<HiResWheelEvent> {
91        self.emitter.create_receiver()
92    }
93}
94
95impl Drop for HiResWheelFeature {
96    fn drop(&mut self) {
97        self.endpoint
98            .chan()
99            .remove_msg_listener(self.msg_listener_hdl);
100    }
101}
102
103impl HiResWheelFeature {
104    /// Retrieves the capabilities of the hi-res wheel and this feature.
105    pub async fn get_wheel_capabilities(&self) -> Result<WheelCapabilities, Hidpp20Error> {
106        let payload = self.endpoint.call(0, [0; 3]).await?.extend_payload();
107
108        Ok(WheelCapabilities {
109            multiplier: payload[0],
110            has_invert: payload[1] & (1 << 3) != 0,
111            has_switch: payload[1] & (1 << 2) != 0,
112            ratches_per_rotation: payload[2],
113            wheel_diameter: payload[3],
114        })
115    }
116
117    /// Retrieves the current mode of the hi-res wheel.
118    pub async fn get_wheel_mode(&self) -> Result<WheelMode, Hidpp20Error> {
119        let payload = self.endpoint.call(1, [0; 3]).await?.extend_payload();
120
121        Ok(WheelMode {
122            inverted: payload[0] & (1 << 2) != 0,
123            resolution: WheelResolution::try_from((payload[0] & (1 << 1)) >> 1)
124                .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
125            target: WheelEventTarget::try_from(payload[0] & 1)
126                .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
127        })
128    }
129
130    /// Sets the mode of the hi-res wheel.
131    ///
132    /// Setting the bit to control analytics collection is not supported in this
133    /// feature implementation as the analytics data structure is completely
134    /// undocumented.\
135    /// If this is implemented in the future, a new implementation will do so to
136    /// not break this one.
137    pub async fn set_wheel_mode(
138        &self,
139        target: WheelEventTarget,
140        resolution: WheelResolution,
141        inverted: bool,
142    ) -> Result<WheelMode, Hidpp20Error> {
143        let mut mode_byte = 0u8;
144        if inverted {
145            mode_byte |= 1 << 2;
146        }
147        mode_byte |= u8::from(resolution) << 1;
148        mode_byte |= u8::from(target);
149
150        let payload = self
151            .endpoint
152            .call(2, [mode_byte, 0x00, 0x00])
153            .await?
154            .extend_payload();
155
156        Ok(WheelMode {
157            inverted: payload[0] & (1 << 2) != 0,
158            resolution: WheelResolution::try_from((payload[0] & (1 << 1)) >> 1)
159                .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
160            target: WheelEventTarget::try_from(payload[0] & 1)
161                .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
162        })
163    }
164
165    /// Retrieves the current state of the ratchet switch.
166    pub async fn get_ratchet_switch_state(&self) -> Result<WheelRatchetState, Hidpp20Error> {
167        let payload = self.endpoint.call(3, [0; 3]).await?.extend_payload();
168
169        WheelRatchetState::try_from(payload[0] & 1).map_err(|_| Hidpp20Error::UnsupportedResponse)
170    }
171}
172
173/// Represents the capabilities of the hi-res wheel and this feature as reported
174/// by [`HiResWheelFeature::get_wheel_capabilities`].
175#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
176#[cfg_attr(feature = "serde", derive(serde::Serialize))]
177#[non_exhaustive]
178pub struct WheelCapabilities {
179    /// The report multiplier for the high-resolution mode. A single ratchet
180    /// distance will produce this amount of wheel movement reports in hi-res
181    /// mode.
182    pub multiplier: u8,
183
184    /// Whether the device supports inverting the scrolling direction when in
185    /// native HID reporting mode.
186    ///
187    /// Inverting is never supported in diverted HID++ mode.
188    pub has_invert: bool,
189
190    /// Whether the device has a switch to control the ratchet mode.
191    pub has_switch: bool,
192
193    /// The amount of ratches that would be generated by a whole rotation of the
194    /// scroll wheel.
195    pub ratches_per_rotation: u8,
196
197    /// The nominal wheel diameter in millimeters.
198    pub wheel_diameter: u8,
199}
200
201/// Represents the wheel mode as reported by
202/// [`HiResWheelFeature::get_wheel_mode`].
203#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
204#[cfg_attr(feature = "serde", derive(serde::Serialize))]
205#[non_exhaustive]
206pub struct WheelMode {
207    /// Whether the scrolling direction is inverted.
208    /// Only applies when in native HID mode.
209    pub inverted: bool,
210
211    /// The current scrolling resolution.
212    pub resolution: WheelResolution,
213
214    /// The target of wheel movement reports (native or diverted).
215    pub target: WheelEventTarget,
216}
217
218/// Represents the resolution of the hi-res wheel.
219#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
220#[cfg_attr(feature = "serde", derive(serde::Serialize))]
221#[non_exhaustive]
222#[repr(u8)]
223pub enum WheelResolution {
224    Low = 0,
225    High = 1,
226}
227
228/// Represents the target of wheel movement reports.
229#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
230#[cfg_attr(feature = "serde", derive(serde::Serialize))]
231#[non_exhaustive]
232#[repr(u8)]
233pub enum WheelEventTarget {
234    Native = 0,
235    Diverted = 1,
236}
237
238/// Represents the state of the wheel ratchet.
239#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
240#[cfg_attr(feature = "serde", derive(serde::Serialize))]
241#[non_exhaustive]
242#[repr(u8)]
243pub enum WheelRatchetState {
244    Freespin = 0,
245    Ratchet = 1,
246}
247
248/// Represents an event emitted by the [`HiResWheelFeature`] feature.
249#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
250#[cfg_attr(feature = "serde", derive(serde::Serialize))]
251#[non_exhaustive]
252pub enum HiResWheelEvent {
253    /// Is emitted whenever the scroll wheel is moved in diverted HID++ mode.
254    WheelMovement(WheelMovementData),
255
256    /// Is emitted whenever the wheel ratchet mode is changed.
257    ///
258    /// This event is always enabled.
259    RatchetSwitch(WheelRatchetState),
260}
261
262/// Represents the data of the [`HiResWheelEvent::WheelMovement`] event.
263#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
264#[cfg_attr(feature = "serde", derive(serde::Serialize))]
265#[non_exhaustive]
266pub struct WheelMovementData {
267    /// The current resolution of the wheel.
268    pub resolution: WheelResolution,
269
270    /// The amount of sampling periods for this event. Maxes at 15.
271    pub periods: U4,
272
273    /// The vertical movement delta. Moving away from the user produces positive
274    /// values.
275    pub delta_vertical: i16,
276}