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