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
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct ScrollWheelMode {
34 pub resolution: ScrollResolution,
36 pub inverted: bool,
38 pub target: ScrollReportingTarget,
40}
41
42impl ScrollWheelMode {
43 fn native(resolution: ScrollResolution, inverted: bool) -> Self {
44 Self {
45 resolution,
46 inverted,
47 target: ScrollReportingTarget::Native,
48 }
49 }
50}
51
52pub async fn get_scroll_wheel_mode(
54 backend: &dyn HidBackend,
55 route: &DeviceRoute,
56) -> Result<ScrollWheelMode, WriteError> {
57 let index = route.device_index();
58 with_route(backend, route, move |channel| async move {
59 get_scroll_wheel_mode_on_channel(&channel, index).await
60 })
61 .await
62}
63
64pub async fn get_scroll_wheel_mode_on(
66 shared: &SharedChannel,
67) -> Result<ScrollWheelMode, WriteError> {
68 get_scroll_wheel_mode_on_channel(shared.channel(), shared.device_index()).await
69}
70
71async fn get_scroll_wheel_mode_on_channel(
72 channel: &Arc<HidppChannel>,
73 index: u8,
74) -> Result<ScrollWheelMode, WriteError> {
75 let mut device = open_device(channel, index).await?;
76 let feature = open_feature::<HiResWheelFeature>(&mut device).await?;
77 read_mode(&feature).await
78}
79
80pub async fn set_scroll_resolution(
83 backend: &dyn HidBackend,
84 route: &DeviceRoute,
85 resolution: ScrollResolution,
86) -> Result<ScrollWheelMode, WriteError> {
87 let index = route.device_index();
88 with_route(backend, route, move |channel| async move {
89 change_wheel_mode_on_channel(&channel, index, Some(resolution), None, false).await
90 })
91 .await
92}
93
94pub async fn set_scroll_resolution_on(
96 shared: &SharedChannel,
97 resolution: ScrollResolution,
98) -> Result<ScrollWheelMode, WriteError> {
99 change_wheel_mode_on_channel(
100 shared.channel(),
101 shared.device_index(),
102 Some(resolution),
103 None,
104 false,
105 )
106 .await
107}
108
109pub async fn set_scroll_wheel_mode(
114 backend: &dyn HidBackend,
115 route: &DeviceRoute,
116 resolution: ScrollResolution,
117 inverted: bool,
118) -> Result<ScrollWheelMode, WriteError> {
119 let index = route.device_index();
120 with_route(backend, route, move |channel| async move {
121 change_wheel_mode_on_channel(&channel, index, Some(resolution), Some(inverted), true).await
122 })
123 .await
124}
125
126pub async fn set_scroll_wheel_mode_on(
128 shared: &SharedChannel,
129 resolution: ScrollResolution,
130 inverted: bool,
131) -> Result<ScrollWheelMode, WriteError> {
132 change_wheel_mode_on_channel(
133 shared.channel(),
134 shared.device_index(),
135 Some(resolution),
136 Some(inverted),
137 true,
138 )
139 .await
140}
141
142pub async fn set_scroll_inversion(
148 backend: &dyn HidBackend,
149 route: &DeviceRoute,
150 inverted: bool,
151) -> Result<(), WriteError> {
152 let index = route.device_index();
153 with_route(backend, route, move |channel| async move {
154 change_wheel_mode_on_channel(&channel, index, None, Some(inverted), true)
155 .await
156 .map(|_| ())
157 })
158 .await
159}
160
161pub async fn set_scroll_inversion_on(
163 shared: &SharedChannel,
164 inverted: bool,
165) -> Result<(), WriteError> {
166 change_wheel_mode_on_channel(
167 shared.channel(),
168 shared.device_index(),
169 None,
170 Some(inverted),
171 true,
172 )
173 .await
174 .map(|_| ())
175}
176
177async fn change_wheel_mode_on_channel(
178 channel: &Arc<HidppChannel>,
179 index: u8,
180 resolution: Option<ScrollResolution>,
181 inverted: Option<bool>,
182 require_invert_support: bool,
183) -> Result<ScrollWheelMode, WriteError> {
184 let mut device = open_device(channel, index).await?;
185 let feature = open_feature::<HiResWheelFeature>(&mut device).await?;
186 if require_invert_support {
187 let capabilities = feature.get_wheel_capabilities().await.map_err(|error| {
188 classify_hidpp_error(error, HidppOperation::ReadWheelMode, HiResWheelFeature::ID)
189 })?;
190 if !capabilities.has_invert {
191 return Err(WriteError::FeatureUnsupported {
192 feature_hex: HiResWheelFeature::ID,
193 });
194 }
195 }
196
197 let current = read_mode(&feature).await?;
198 let desired = desired_mode(current, resolution, inverted);
199 if current == desired {
200 debug!(index, ?desired, "native wheel mode already set; skipping");
201 return Ok(current);
202 }
203
204 let written = feature
205 .set_wheel_mode(
206 WheelEventTarget::Native,
207 resolution_to_hidpp(desired.resolution),
208 desired.inverted,
209 )
210 .await
211 .map_err(|error| {
212 classify_hidpp_error(error, HidppOperation::WriteWheelMode, HiResWheelFeature::ID)
213 })?;
214 validate_applied(mode_from_hidpp(written)?, desired)?;
215
216 let read_back = read_mode(&feature).await?;
217 validate_applied(read_back, desired)?;
218 debug!(index, ?read_back, "native wheel mode written and verified");
219 Ok(read_back)
220}
221
222async fn open_device(channel: &Arc<HidppChannel>, index: u8) -> Result<Device, WriteError> {
223 Device::new(Arc::clone(channel), index)
224 .await
225 .map_err(|_| WriteError::DeviceUnreachable { index })
226}
227
228async fn read_mode(feature: &HiResWheelFeature) -> Result<ScrollWheelMode, WriteError> {
229 let mode = feature.get_wheel_mode().await.map_err(|error| {
230 classify_hidpp_error(error, HidppOperation::ReadWheelMode, HiResWheelFeature::ID)
231 })?;
232 mode_from_hidpp(mode)
233}
234
235fn desired_mode(
236 current: ScrollWheelMode,
237 resolution: Option<ScrollResolution>,
238 inverted: Option<bool>,
239) -> ScrollWheelMode {
240 ScrollWheelMode::native(
241 resolution.unwrap_or(current.resolution),
242 inverted.unwrap_or(current.inverted),
243 )
244}
245
246fn validate_applied(actual: ScrollWheelMode, desired: ScrollWheelMode) -> Result<(), WriteError> {
247 if actual == desired {
248 Ok(())
249 } else {
250 Err(WriteError::UnsupportedResponse {
251 operation: HidppOperation::WriteWheelMode,
252 feature_hex: HiResWheelFeature::ID,
253 })
254 }
255}
256
257fn mode_from_hidpp(mode: HidppWheelMode) -> Result<ScrollWheelMode, WriteError> {
258 Ok(ScrollWheelMode {
259 resolution: resolution_from_hidpp(mode.resolution)?,
260 inverted: mode.inverted,
261 target: target_from_hidpp(mode.target)?,
262 })
263}
264
265fn resolution_from_hidpp(resolution: HidppWheelResolution) -> Result<ScrollResolution, WriteError> {
266 Ok(match resolution {
267 HidppWheelResolution::Low => ScrollResolution::Low,
268 HidppWheelResolution::High => ScrollResolution::High,
269 _ => return Err(unsupported_read_response()),
270 })
271}
272
273fn target_from_hidpp(target: WheelEventTarget) -> Result<ScrollReportingTarget, WriteError> {
274 Ok(match target {
275 WheelEventTarget::Native => ScrollReportingTarget::Native,
276 WheelEventTarget::Diverted => ScrollReportingTarget::Diverted,
277 _ => return Err(unsupported_read_response()),
278 })
279}
280
281fn resolution_to_hidpp(resolution: ScrollResolution) -> HidppWheelResolution {
282 match resolution {
283 ScrollResolution::Low => HidppWheelResolution::Low,
284 ScrollResolution::High => HidppWheelResolution::High,
285 }
286}
287
288fn unsupported_read_response() -> WriteError {
289 WriteError::UnsupportedResponse {
290 operation: HidppOperation::ReadWheelMode,
291 feature_hex: HiResWheelFeature::ID,
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 #[test]
300 fn mode_value_conversions_preserve_known_wire_values() -> Result<(), WriteError> {
301 assert_eq!(
302 resolution_from_hidpp(HidppWheelResolution::Low)?,
303 ScrollResolution::Low
304 );
305 assert_eq!(
306 resolution_from_hidpp(HidppWheelResolution::High)?,
307 ScrollResolution::High
308 );
309 assert_eq!(
310 target_from_hidpp(WheelEventTarget::Native)?,
311 ScrollReportingTarget::Native
312 );
313 assert_eq!(
314 target_from_hidpp(WheelEventTarget::Diverted)?,
315 ScrollReportingTarget::Diverted
316 );
317 Ok(())
318 }
319
320 #[test]
321 fn resolution_only_preserves_inversion_and_targets_native() {
322 let current = ScrollWheelMode {
323 resolution: ScrollResolution::High,
324 inverted: true,
325 target: ScrollReportingTarget::Diverted,
326 };
327 assert_eq!(
328 desired_mode(current, Some(ScrollResolution::Low), None),
329 ScrollWheelMode::native(ScrollResolution::Low, true)
330 );
331 }
332
333 #[test]
334 fn inversion_only_preserves_resolution_and_targets_native() {
335 let current = ScrollWheelMode {
336 resolution: ScrollResolution::Low,
337 inverted: false,
338 target: ScrollReportingTarget::Diverted,
339 };
340 assert_eq!(
341 desired_mode(current, None, Some(true)),
342 ScrollWheelMode::native(ScrollResolution::Low, true)
343 );
344 }
345
346 #[test]
347 fn mismatched_set_or_read_back_is_rejected() {
348 let desired = ScrollWheelMode::native(ScrollResolution::Low, false);
349 let actual = ScrollWheelMode::native(ScrollResolution::High, false);
350 assert!(matches!(
351 validate_applied(actual, desired),
352 Err(WriteError::UnsupportedResponse {
353 operation: HidppOperation::WriteWheelMode,
354 feature_hex: 0x2121,
355 })
356 ));
357 }
358}