1use serde::{Deserialize, Serialize};
7
8use super::WriteError;
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12pub struct DpiCapabilities {
13 values: Vec<u16>,
14}
15
16impl DpiCapabilities {
17 pub fn new(mut values: Vec<u16>) -> Result<Self, WriteError> {
20 values.sort_unstable();
21 values.dedup();
22 if values.is_empty() {
23 return Err(WriteError::EmptyDpiList);
24 }
25 Ok(Self { values })
26 }
27
28 #[must_use]
30 pub fn values(&self) -> &[u16] {
31 &self.values
32 }
33
34 #[must_use]
36 pub fn min(&self) -> u16 {
37 self.values[0]
38 }
39
40 #[must_use]
42 pub fn max(&self) -> u16 {
43 self.values[self.values.len() - 1]
44 }
45
46 #[must_use]
48 pub fn contains(&self, dpi: u16) -> bool {
49 self.values.binary_search(&dpi).is_ok()
50 }
51
52 #[must_use]
54 pub fn nearest(&self, dpi: u32) -> u16 {
55 let mut nearest = self.values[0];
56 let mut best_delta = u32::from(nearest).abs_diff(dpi);
57 for &candidate in &self.values[1..] {
58 let delta = u32::from(candidate).abs_diff(dpi);
59 if delta < best_delta {
60 nearest = candidate;
61 best_delta = delta;
62 }
63 }
64 nearest
65 }
66
67 #[must_use]
71 pub fn snap(&self, dpi: u32) -> u32 {
72 u32::from(self.nearest(dpi))
73 }
74
75 #[must_use]
78 pub fn step_hint(&self) -> u16 {
79 self.values
80 .array_windows::<2>()
81 .filter_map(|&[low, high]| high.checked_sub(low))
82 .filter(|step| *step > 0)
83 .min()
84 .unwrap_or(1)
85 }
86
87 #[must_use]
89 pub fn adjacent_test_target(&self, current: u16) -> Option<u16> {
90 if self.values.len() < 2 {
91 return None;
92 }
93 match self.values.binary_search(¤t) {
94 Ok(index) if index + 1 < self.values.len() => Some(self.values[index + 1]),
95 Ok(index) if index > 0 => Some(self.values[index - 1]),
96 Ok(_) => None,
97 Err(index) if index < self.values.len() => Some(self.values[index]),
98 Err(_) => self.values.last().copied(),
99 }
100 .filter(|target| *target != current)
101 }
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
110pub struct DpiInfo {
111 pub current: u16,
113 pub capabilities: DpiCapabilities,
115}
116
117#[cfg(test)]
118mod tests {
119 use std::assert_matches;
120
121 use super::{DpiCapabilities, WriteError};
122
123 #[test]
124 fn capabilities_sort_and_deduplicate_values() -> Result<(), WriteError> {
125 let caps = DpiCapabilities::new(vec![1600, 400, 800, 800])?;
126
127 assert_eq!(caps.values(), [400, 800, 1600]);
128 assert_eq!(caps.min(), 400);
129 assert_eq!(caps.max(), 1600);
130 Ok(())
131 }
132
133 #[test]
134 fn capabilities_reject_empty_list() {
135 assert_matches!(
136 DpiCapabilities::new(Vec::new()),
137 Err(WriteError::EmptyDpiList)
138 );
139 }
140
141 #[test]
142 fn nearest_returns_closest_supported_value() -> Result<(), WriteError> {
143 let caps = DpiCapabilities::new(vec![400, 800, 1600])?;
144
145 assert_eq!(caps.nearest(390), 400);
146 assert_eq!(caps.nearest(1000), 800);
147 assert_eq!(caps.nearest(2000), 1600);
148 Ok(())
149 }
150
151 #[test]
152 fn step_hint_returns_smallest_positive_gap() -> Result<(), WriteError> {
153 let caps = DpiCapabilities::new(vec![400, 800, 1200, 2000])?;
154
155 assert_eq!(caps.step_hint(), 400);
156 Ok(())
157 }
158
159 #[test]
160 fn adjacent_test_target_prefers_next_then_previous_value() -> Result<(), WriteError> {
161 let caps = DpiCapabilities::new(vec![400, 800, 1600])?;
162
163 assert_eq!(caps.adjacent_test_target(400), Some(800));
164 assert_eq!(caps.adjacent_test_target(800), Some(1600));
165 assert_eq!(caps.adjacent_test_target(1600), Some(800));
166 Ok(())
167 }
168
169 #[test]
170 fn adjacent_test_target_handles_current_outside_list() -> Result<(), WriteError> {
171 let caps = DpiCapabilities::new(vec![400, 800, 1600])?;
172
173 assert_eq!(caps.adjacent_test_target(1000), Some(1600));
174 assert_eq!(caps.adjacent_test_target(2000), Some(1600));
175 Ok(())
176 }
177}