1use std::sync::Arc;
4
5use hidpp::{
6 channel::HidppChannel,
7 device::Device,
8 feature::CreatableFeature,
9 feature::hires_wheel::{
10 HiResWheelFeature, WheelEventTarget, WheelMode as HidppWheelMode,
11 WheelResolution as HidppWheelResolution,
12 },
13};
14pub use openlogi_core::config::ScrollResolution;
15use tracing::debug;
16
17use super::{HidppOperation, WriteError, classify_hidpp_error, open_feature, with_route};
18use crate::SharedChannel;
19use crate::backend::HidBackend;
20use crate::channel::route::DeviceRoute;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum ScrollReportingTarget {
25 Native,
27 Diverted,
29}
30
31impl From<ScrollReportingTarget> for WheelEventTarget {
32 fn from(target: ScrollReportingTarget) -> Self {
33 match target {
34 ScrollReportingTarget::Native => Self::Native,
35 ScrollReportingTarget::Diverted => Self::Diverted,
36 }
37 }
38}
39
40impl TryFrom<WheelEventTarget> for ScrollReportingTarget {
41 type Error = WriteError;
42
43 fn try_from(target: WheelEventTarget) -> Result<Self, Self::Error> {
44 match target {
45 WheelEventTarget::Native => Ok(Self::Native),
46 WheelEventTarget::Diverted => Ok(Self::Diverted),
47 _ => Err(unsupported_read_response()),
48 }
49 }
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub struct ScrollWheelMode {
55 pub resolution: ScrollResolution,
57 pub inverted: bool,
59 pub target: ScrollReportingTarget,
61}
62
63impl TryFrom<HidppWheelMode> for ScrollWheelMode {
64 type Error = WriteError;
65
66 fn try_from(mode: HidppWheelMode) -> Result<Self, Self::Error> {
67 Ok(Self {
68 resolution: resolution_from_hidpp(mode.resolution)?,
69 inverted: mode.inverted,
70 target: mode.target.try_into()?,
71 })
72 }
73}
74
75#[cfg(test)]
76impl ScrollWheelMode {
77 fn native(resolution: ScrollResolution, inverted: bool) -> Self {
78 Self {
79 resolution,
80 inverted,
81 target: ScrollReportingTarget::Native,
82 }
83 }
84}
85
86pub async fn get_scroll_wheel_mode(
88 backend: &dyn HidBackend,
89 route: &DeviceRoute,
90) -> Result<ScrollWheelMode, WriteError> {
91 let index = route.device_index();
92 with_route(backend, route, move |channel| async move {
93 get_scroll_wheel_mode_on_channel(&channel, index).await
94 })
95 .await
96}
97
98pub async fn get_scroll_wheel_mode_on(
100 shared: &SharedChannel,
101) -> Result<ScrollWheelMode, WriteError> {
102 get_scroll_wheel_mode_on_channel(shared.channel(), shared.device_index()).await
103}
104
105async fn get_scroll_wheel_mode_on_channel(
106 channel: &Arc<HidppChannel>,
107 index: u8,
108) -> Result<ScrollWheelMode, WriteError> {
109 let mut device = open_device(channel, index).await?;
110 let feature = open_feature::<HiResWheelFeature>(&mut device).await?;
111 read_mode(&feature).await
112}
113
114pub async fn set_scroll_resolution(
117 backend: &dyn HidBackend,
118 route: &DeviceRoute,
119 resolution: ScrollResolution,
120) -> Result<ScrollWheelMode, WriteError> {
121 let index = route.device_index();
122 with_route(backend, route, move |channel| async move {
123 change_wheel_mode_on_channel(&channel, index, Some(resolution), None, false).await
124 })
125 .await
126}
127
128pub async fn set_scroll_resolution_on(
130 shared: &SharedChannel,
131 resolution: ScrollResolution,
132) -> Result<ScrollWheelMode, WriteError> {
133 change_wheel_mode_on_channel(
134 shared.channel(),
135 shared.device_index(),
136 Some(resolution),
137 None,
138 false,
139 )
140 .await
141}
142
143pub async fn set_scroll_wheel_mode(
148 backend: &dyn HidBackend,
149 route: &DeviceRoute,
150 resolution: ScrollResolution,
151 inverted: bool,
152) -> Result<ScrollWheelMode, WriteError> {
153 let index = route.device_index();
154 with_route(backend, route, move |channel| async move {
155 change_wheel_mode_on_channel(&channel, index, Some(resolution), Some(inverted), true).await
156 })
157 .await
158}
159
160pub async fn set_scroll_wheel_mode_on(
162 shared: &SharedChannel,
163 resolution: ScrollResolution,
164 inverted: bool,
165) -> Result<ScrollWheelMode, WriteError> {
166 change_wheel_mode_on_channel(
167 shared.channel(),
168 shared.device_index(),
169 Some(resolution),
170 Some(inverted),
171 true,
172 )
173 .await
174}
175
176pub async fn set_scroll_inversion(
184 backend: &dyn HidBackend,
185 route: &DeviceRoute,
186 inverted: bool,
187) -> Result<(), WriteError> {
188 let index = route.device_index();
189 with_route(backend, route, move |channel| async move {
190 change_wheel_mode_on_channel(&channel, index, None, Some(inverted), true)
191 .await
192 .map(|_| ())
193 })
194 .await
195}
196
197pub async fn set_scroll_inversion_on(
200 shared: &SharedChannel,
201 inverted: bool,
202) -> Result<(), WriteError> {
203 change_wheel_mode_on_channel(
204 shared.channel(),
205 shared.device_index(),
206 None,
207 Some(inverted),
208 true,
209 )
210 .await
211 .map(|_| ())
212}
213
214async fn change_wheel_mode_on_channel(
215 channel: &Arc<HidppChannel>,
216 index: u8,
217 resolution: Option<ScrollResolution>,
218 inverted: Option<bool>,
219 require_invert_support: bool,
220) -> Result<ScrollWheelMode, WriteError> {
221 let mut device = open_device(channel, index).await?;
222 let feature = open_feature::<HiResWheelFeature>(&mut device).await?;
223 if require_invert_support {
224 let capabilities = feature.get_wheel_capabilities().await.map_err(|error| {
225 classify_hidpp_error(error, HidppOperation::ReadWheelMode, HiResWheelFeature::ID)
226 })?;
227 if !capabilities.has_invert {
228 return Err(WriteError::FeatureUnsupported {
229 feature_hex: HiResWheelFeature::ID,
230 });
231 }
232 }
233
234 let current = read_mode(&feature).await?;
235 let desired = desired_mode(current, resolution, inverted);
236 if current == desired {
237 debug!(index, ?desired, "wheel mode already set; skipping");
238 return Ok(current);
239 }
240
241 let written = feature
242 .set_wheel_mode(
243 desired.target.into(),
244 resolution_to_hidpp(desired.resolution),
245 desired.inverted,
246 )
247 .await
248 .map_err(|error| {
249 classify_hidpp_error(error, HidppOperation::WriteWheelMode, HiResWheelFeature::ID)
250 })?;
251 validate_applied(written.try_into()?, desired)?;
252
253 let read_back = read_mode(&feature).await?;
254 validate_applied(read_back, desired)?;
255 debug!(index, ?read_back, "wheel mode written and verified");
256 Ok(read_back)
257}
258
259async fn open_device(channel: &Arc<HidppChannel>, index: u8) -> Result<Device, WriteError> {
260 Device::new(Arc::clone(channel), index)
261 .await
262 .map_err(|_| WriteError::DeviceUnreachable { index })
263}
264
265async fn read_mode(feature: &HiResWheelFeature) -> Result<ScrollWheelMode, WriteError> {
266 let mode = feature.get_wheel_mode().await.map_err(|error| {
267 classify_hidpp_error(error, HidppOperation::ReadWheelMode, HiResWheelFeature::ID)
268 })?;
269 mode.try_into()
270}
271
272fn desired_mode(
273 current: ScrollWheelMode,
274 resolution: Option<ScrollResolution>,
275 inverted: Option<bool>,
276) -> ScrollWheelMode {
277 ScrollWheelMode {
278 resolution: resolution.unwrap_or(current.resolution),
279 inverted: inverted.unwrap_or(current.inverted),
280 target: if resolution.is_some() || inverted == Some(true) {
285 ScrollReportingTarget::Native
286 } else {
287 current.target
288 },
289 }
290}
291
292fn validate_applied(actual: ScrollWheelMode, desired: ScrollWheelMode) -> Result<(), WriteError> {
293 if actual == desired {
294 Ok(())
295 } else {
296 Err(WriteError::UnsupportedResponse {
297 operation: HidppOperation::WriteWheelMode,
298 feature_hex: HiResWheelFeature::ID,
299 })
300 }
301}
302
303fn resolution_from_hidpp(resolution: HidppWheelResolution) -> Result<ScrollResolution, WriteError> {
304 Ok(match resolution {
305 HidppWheelResolution::Low => ScrollResolution::Low,
306 HidppWheelResolution::High => ScrollResolution::High,
307 _ => return Err(unsupported_read_response()),
308 })
309}
310
311fn resolution_to_hidpp(resolution: ScrollResolution) -> HidppWheelResolution {
312 match resolution {
313 ScrollResolution::Low => HidppWheelResolution::Low,
314 ScrollResolution::High => HidppWheelResolution::High,
315 }
316}
317
318fn unsupported_read_response() -> WriteError {
319 WriteError::UnsupportedResponse {
320 operation: HidppOperation::ReadWheelMode,
321 feature_hex: HiResWheelFeature::ID,
322 }
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328
329 #[test]
330 fn mode_value_conversions_preserve_known_wire_values() -> Result<(), WriteError> {
331 assert_eq!(
332 resolution_from_hidpp(HidppWheelResolution::Low)?,
333 ScrollResolution::Low
334 );
335 assert_eq!(
336 resolution_from_hidpp(HidppWheelResolution::High)?,
337 ScrollResolution::High
338 );
339 assert_eq!(
340 ScrollReportingTarget::try_from(WheelEventTarget::Native)?,
341 ScrollReportingTarget::Native
342 );
343 assert_eq!(
344 ScrollReportingTarget::try_from(WheelEventTarget::Diverted)?,
345 ScrollReportingTarget::Diverted
346 );
347 assert_eq!(
348 WheelEventTarget::from(ScrollReportingTarget::Native),
349 WheelEventTarget::Native
350 );
351 assert_eq!(
352 WheelEventTarget::from(ScrollReportingTarget::Diverted),
353 WheelEventTarget::Diverted
354 );
355 Ok(())
356 }
357
358 #[test]
359 fn resolution_only_preserves_inversion_and_targets_native() {
360 let current = ScrollWheelMode {
361 resolution: ScrollResolution::High,
362 inverted: true,
363 target: ScrollReportingTarget::Diverted,
364 };
365 assert_eq!(
366 desired_mode(current, Some(ScrollResolution::Low), None),
367 ScrollWheelMode::native(ScrollResolution::Low, true)
368 );
369 }
370
371 #[test]
372 fn inversion_only_preserves_resolution_and_targets_native() {
373 let current = ScrollWheelMode {
374 resolution: ScrollResolution::Low,
375 inverted: false,
376 target: ScrollReportingTarget::Diverted,
377 };
378 assert_eq!(
379 desired_mode(current, None, Some(true)),
380 ScrollWheelMode::native(ScrollResolution::Low, true)
381 );
382 }
383
384 #[test]
385 fn default_non_inverted_setting_preserves_diverted_reporting() {
386 let current = ScrollWheelMode {
387 resolution: ScrollResolution::High,
388 inverted: false,
389 target: ScrollReportingTarget::Diverted,
390 };
391 assert_eq!(desired_mode(current, None, Some(false)), current);
392 }
393
394 #[test]
395 fn mismatched_set_or_read_back_is_rejected() {
396 let desired = ScrollWheelMode::native(ScrollResolution::Low, false);
397 let actual = ScrollWheelMode::native(ScrollResolution::High, false);
398 assert!(matches!(
399 validate_applied(actual, desired),
400 Err(WriteError::UnsupportedResponse {
401 operation: HidppOperation::WriteWheelMode,
402 feature_hex: 0x2121,
403 })
404 ));
405 }
406}