1use std::time::Duration as StdDuration;
4
5use crate::{DesktopError, Point};
6
7const MIN_RESOLVED: StdDuration = StdDuration::from_millis(1);
11
12#[derive(Copy, Clone, Debug, PartialEq)]
15pub enum Duration {
16 Fixed(StdDuration),
18
19 Distance {
25 speed_px_per_sec: f32,
26 min: Option<StdDuration>,
27 max: Option<StdDuration>,
28 },
29}
30
31impl Duration {
32 #[inline]
34 pub const fn fixed(d: StdDuration) -> Self {
35 Self::Fixed(d)
36 }
37
38 #[inline]
40 pub const fn distance(speed_px_per_sec: f32) -> Self {
41 Self::Distance {
42 speed_px_per_sec,
43 min: None,
44 max: None,
45 }
46 }
47
48 #[inline]
50 pub const fn distance_clamped(
51 speed_px_per_sec: f32,
52 min: StdDuration,
53 max: StdDuration,
54 ) -> Self {
55 Self::Distance {
56 speed_px_per_sec,
57 min: Some(min),
58 max: Some(max),
59 }
60 }
61
62 pub fn resolve(&self, from: Point, to: Point) -> Result<StdDuration, DesktopError> {
68 match *self {
69 Duration::Fixed(d) => {
70 if d.is_zero() {
71 Err(DesktopError::InvalidDuration(
72 "fixed duration must be non-zero".into(),
73 ))
74 } else {
75 Ok(d)
76 }
77 }
78 Duration::Distance {
79 speed_px_per_sec,
80 min,
81 max,
82 } => {
83 if !speed_px_per_sec.is_finite() || speed_px_per_sec <= 0.0 {
84 return Err(DesktopError::InvalidDuration(format!(
85 "distance duration requires positive finite speed, got {speed_px_per_sec}"
86 )));
87 }
88 if let (Some(lo), Some(hi)) = (min, max) {
89 if lo > hi {
90 return Err(DesktopError::InvalidDuration(format!(
91 "distance duration min ({lo:?}) is greater than max ({hi:?})"
92 )));
93 }
94 }
95
96 let dist = Point::distance(from, to);
97 let seconds = (dist / speed_px_per_sec).max(0.0);
98 let mut d = StdDuration::try_from_secs_f32(seconds).unwrap_or(StdDuration::ZERO);
99
100 if let Some(lo) = min {
101 if d < lo {
102 d = lo;
103 }
104 }
105 if let Some(hi) = max {
106 if d > hi {
107 d = hi;
108 }
109 }
110
111 if d.is_zero() {
114 d = MIN_RESOLVED;
115 }
116 Ok(d)
117 }
118 }
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn fixed_returns_value_verbatim() {
128 let d = Duration::fixed(StdDuration::from_millis(500));
129 assert_eq!(
130 d.resolve(Point::ZERO, Point::new(999, 0)).unwrap(),
131 StdDuration::from_millis(500)
132 );
133 }
134
135 #[test]
136 fn fixed_zero_is_rejected() {
137 let d = Duration::fixed(StdDuration::ZERO);
138 let err = d.resolve(Point::ZERO, Point::ZERO).unwrap_err();
139 assert!(matches!(err, DesktopError::InvalidDuration(_)));
140 }
141
142 #[test]
143 fn distance_scales_with_distance() {
144 let d = Duration::distance(1000.0);
146 let out = d.resolve(Point::ZERO, Point::new(300, 400)).unwrap();
147 let expected = StdDuration::from_millis(500);
149 assert!(
150 (out.as_secs_f64() - expected.as_secs_f64()).abs() < 0.01,
151 "got {out:?}"
152 );
153 }
154
155 #[test]
156 fn distance_zero_falls_back_to_min_resolved() {
157 let d = Duration::distance(1000.0);
158 let out = d.resolve(Point::ZERO, Point::ZERO).unwrap();
159 assert_eq!(out, MIN_RESOLVED);
160 }
161
162 #[test]
163 fn distance_min_clamps_short_moves() {
164 let d = Duration::distance_clamped(
165 1000.0,
166 StdDuration::from_millis(300),
167 StdDuration::from_secs(2),
168 );
169 let out = d.resolve(Point::ZERO, Point::new(10, 0)).unwrap();
171 assert_eq!(out, StdDuration::from_millis(300));
172 }
173
174 #[test]
175 fn distance_max_clamps_long_moves() {
176 let d = Duration::distance_clamped(
177 1.0, StdDuration::from_millis(100),
179 StdDuration::from_millis(500),
180 );
181 let out = d.resolve(Point::ZERO, Point::new(5000, 0)).unwrap();
182 assert_eq!(out, StdDuration::from_millis(500));
183 }
184
185 #[test]
186 fn distance_bad_speed_rejected() {
187 for &s in &[0.0f32, -1.0, f32::NAN, f32::INFINITY] {
188 let d = Duration::distance(s);
189 let err = d.resolve(Point::ZERO, Point::new(10, 0)).unwrap_err();
190 assert!(matches!(err, DesktopError::InvalidDuration(_)), "s = {s}");
191 }
192 }
193
194 #[test]
195 fn distance_min_greater_than_max_rejected() {
196 let d = Duration::Distance {
197 speed_px_per_sec: 100.0,
198 min: Some(StdDuration::from_secs(2)),
199 max: Some(StdDuration::from_secs(1)),
200 };
201 assert!(matches!(
202 d.resolve(Point::ZERO, Point::new(10, 0)).unwrap_err(),
203 DesktopError::InvalidDuration(_)
204 ));
205 }
206}