1use std::num::TryFromIntError;
7
8use az::SaturatingAs;
9use nutype::nutype;
10use serde::{Deserialize, Serialize};
11
12use super::WriteError;
13
14#[nutype(
16 const_fn,
17 derive(
18 Debug,
19 Clone,
20 Copy,
21 PartialEq,
22 Eq,
23 PartialOrd,
24 Ord,
25 From,
26 Into,
27 Display,
28 Serialize,
29 Deserialize
30 )
31)]
32pub struct Dpi(u16);
33
34impl Dpi {
35 #[must_use]
37 pub fn from_rounded(value: f32) -> Self {
38 Self::new(value.max(0.).round().saturating_as::<u16>())
39 }
40}
41
42impl TryFrom<u32> for Dpi {
43 type Error = TryFromIntError;
44
45 fn try_from(value: u32) -> Result<Self, Self::Error> {
46 u16::try_from(value).map(Self::new)
47 }
48}
49
50impl From<Dpi> for u32 {
51 fn from(dpi: Dpi) -> Self {
52 u32::from(dpi.into_inner())
53 }
54}
55
56impl From<Dpi> for f32 {
57 fn from(dpi: Dpi) -> Self {
58 f32::from(dpi.into_inner())
59 }
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64pub struct DpiCapabilities {
65 values: Vec<Dpi>,
66}
67
68impl DpiCapabilities {
69 pub fn new(values: Vec<u16>) -> Result<Self, WriteError> {
72 let mut values: Vec<Dpi> = values.into_iter().map(Dpi::from).collect();
73 values.sort_unstable();
74 values.dedup();
75 if values.is_empty() {
76 return Err(WriteError::EmptyDpiList);
77 }
78 Ok(Self { values })
79 }
80
81 #[must_use]
83 pub fn values(&self) -> &[Dpi] {
84 &self.values
85 }
86
87 #[must_use]
89 pub fn min(&self) -> Dpi {
90 self.values[0]
91 }
92
93 #[must_use]
95 pub fn max(&self) -> Dpi {
96 self.values[self.values.len() - 1]
97 }
98
99 #[must_use]
101 pub fn contains(&self, dpi: Dpi) -> bool {
102 self.values.binary_search(&dpi).is_ok()
103 }
104
105 #[must_use]
107 pub fn nearest(&self, dpi: Dpi) -> Dpi {
108 let mut nearest = self.values[0];
109 let raw_dpi = dpi.into_inner();
110 let mut best_delta = nearest.into_inner().abs_diff(raw_dpi);
111 for &candidate in &self.values[1..] {
112 let delta = candidate.into_inner().abs_diff(raw_dpi);
113 if delta < best_delta {
114 nearest = candidate;
115 best_delta = delta;
116 }
117 }
118 nearest
119 }
120
121 #[must_use]
124 pub fn step_hint(&self) -> Dpi {
125 self.values
126 .array_windows::<2>()
127 .filter_map(|&[low, high]| {
128 high.into_inner()
129 .checked_sub(low.into_inner())
130 .map(Dpi::new)
131 })
132 .filter(|step| step.into_inner() > 0)
133 .min()
134 .unwrap_or(Dpi::new(1))
135 }
136
137 #[must_use]
139 pub fn adjacent_test_target(&self, current: Dpi) -> Option<Dpi> {
140 if self.values.len() < 2 {
141 return None;
142 }
143 match self.values.binary_search(¤t) {
144 Ok(index) if index + 1 < self.values.len() => Some(self.values[index + 1]),
145 Ok(index) if index > 0 => Some(self.values[index - 1]),
146 Ok(_) => None,
147 Err(index) if index < self.values.len() => Some(self.values[index]),
148 Err(_) => self.values.last().copied(),
149 }
150 .filter(|target| *target != current)
151 }
152}
153
154#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
160pub struct DpiInfo {
161 pub current: Dpi,
163 pub capabilities: DpiCapabilities,
165}
166
167#[cfg(test)]
168mod tests {
169 use std::assert_matches;
170
171 use super::{Dpi, DpiCapabilities, WriteError};
172
173 #[test]
174 fn floating_control_values_round_and_stay_in_the_dpi_domain() {
175 assert_eq!(Dpi::from_rounded(1599.6), Dpi::new(1600));
176 assert_eq!(Dpi::from_rounded(-1.0), Dpi::new(0));
177 assert_eq!(Dpi::from_rounded(70_000.0), Dpi::new(u16::MAX));
178 }
179
180 #[test]
181 fn capabilities_sort_and_deduplicate_values() -> Result<(), WriteError> {
182 let caps = DpiCapabilities::new(vec![1600, 400, 800, 800])?;
183
184 assert_eq!(
185 caps.values(),
186 [Dpi::new(400), Dpi::new(800), Dpi::new(1600)]
187 );
188 assert_eq!(caps.min(), Dpi::new(400));
189 assert_eq!(caps.max(), Dpi::new(1600));
190 Ok(())
191 }
192
193 #[test]
194 fn capabilities_reject_empty_list() {
195 assert_matches!(
196 DpiCapabilities::new(Vec::new()),
197 Err(WriteError::EmptyDpiList)
198 );
199 }
200
201 #[test]
202 fn nearest_returns_closest_supported_value() -> Result<(), WriteError> {
203 let caps = DpiCapabilities::new(vec![400, 800, 1600])?;
204
205 assert_eq!(caps.nearest(Dpi::new(390)), Dpi::new(400));
206 assert_eq!(caps.nearest(Dpi::new(1000)), Dpi::new(800));
207 assert_eq!(caps.nearest(Dpi::new(2000)), Dpi::new(1600));
208 Ok(())
209 }
210
211 #[test]
212 fn step_hint_returns_smallest_positive_gap() -> Result<(), WriteError> {
213 let caps = DpiCapabilities::new(vec![400, 800, 1200, 2000])?;
214
215 assert_eq!(caps.step_hint(), Dpi::new(400));
216 Ok(())
217 }
218
219 #[test]
220 fn adjacent_test_target_prefers_next_then_previous_value() -> Result<(), WriteError> {
221 let caps = DpiCapabilities::new(vec![400, 800, 1600])?;
222
223 assert_eq!(
224 caps.adjacent_test_target(Dpi::new(400)),
225 Some(Dpi::new(800))
226 );
227 assert_eq!(
228 caps.adjacent_test_target(Dpi::new(800)),
229 Some(Dpi::new(1600))
230 );
231 assert_eq!(
232 caps.adjacent_test_target(Dpi::new(1600)),
233 Some(Dpi::new(800))
234 );
235 Ok(())
236 }
237
238 #[test]
239 fn adjacent_test_target_handles_current_outside_list() -> Result<(), WriteError> {
240 let caps = DpiCapabilities::new(vec![400, 800, 1600])?;
241
242 assert_eq!(
243 caps.adjacent_test_target(Dpi::new(1000)),
244 Some(Dpi::new(1600))
245 );
246 assert_eq!(
247 caps.adjacent_test_target(Dpi::new(2000)),
248 Some(Dpi::new(1600))
249 );
250 Ok(())
251 }
252}