1use core::fmt;
15use thiserror::Error;
16
17#[derive(Debug, Clone, Error)]
19#[error("value {value} is out of range {range}")]
20pub struct OutOfRange {
21 pub value: f64,
23 pub range: &'static str,
25}
26
27macro_rules! ranged_unit {
28 (
29 $(#[$meta:meta])*
30 name = $name:ident,
31 repr = $repr:ty,
32 min = $min:expr,
33 max = $max:expr,
34 range_doc = $range_doc:literal $(,)?
35 ) => {
36 $(#[$meta])*
37 #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
38 pub struct $name($repr);
39
40 impl $name {
41 pub const MIN: Self = Self($min);
43 pub const MAX: Self = Self($max);
45
46 pub fn new(value: $repr) -> Result<Self, OutOfRange> {
53 if !value.is_finite() || value < $min || value > $max {
54 return Err(OutOfRange {
55 value: f64::from(value),
56 range: $range_doc,
57 });
58 }
59 Ok(Self(value))
60 }
61
62 #[must_use]
66 pub const fn new_unchecked(value: $repr) -> Self {
67 Self(value)
68 }
69
70 #[must_use]
72 pub const fn get(self) -> $repr {
73 self.0
74 }
75 }
76
77 impl fmt::Display for $name {
78 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79 fmt::Display::fmt(&self.0, f)
80 }
81 }
82 };
83}
84
85ranged_unit! {
86 name = Brightness,
88 repr = f32,
89 min = 0.0,
90 max = 1.0,
91 range_doc = "0.0..=1.0",
92}
93
94ranged_unit! {
95 name = Volume,
97 repr = f32,
98 min = 0.0,
99 max = 1.0,
100 range_doc = "0.0..=1.0",
101}
102
103ranged_unit! {
104 name = Pan,
106 repr = f32,
107 min = -1.0,
108 max = 1.0,
109 range_doc = "-1.0..=1.0",
110}
111
112ranged_unit! {
113 name = PlaybackRate,
116 repr = f32,
117 min = 0.25,
118 max = 4.0,
119 range_doc = "0.25..=4.0",
120}
121
122ranged_unit! {
123 name = Pitch,
125 repr = f32,
126 min = 0.5,
127 max = 2.0,
128 range_doc = "0.5..=2.0",
129}
130
131ranged_unit! {
132 name = Zoom,
135 repr = f32,
136 min = 1.0,
137 max = 100.0,
138 range_doc = "1.0..=100.0",
139}
140
141ranged_unit! {
142 name = Latitude,
144 repr = f64,
145 min = -90.0,
146 max = 90.0,
147 range_doc = "-90.0..=90.0",
148}
149
150ranged_unit! {
151 name = Longitude,
153 repr = f64,
154 min = -180.0,
155 max = 180.0,
156 range_doc = "-180.0..=180.0",
157}
158
159ranged_unit! {
160 name = RefreshRate,
163 repr = f32,
164 min = 1.0,
165 max = 480.0,
166 range_doc = "1.0..=480.0",
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 #[test]
174 fn brightness_accepts_in_range() {
175 assert!(Brightness::new(0.0).is_ok());
176 assert!(Brightness::new(1.0).is_ok());
177 assert!(Brightness::new(0.5).is_ok());
178 }
179
180 #[test]
181 fn brightness_rejects_out_of_range() {
182 assert!(Brightness::new(-0.1).is_err());
183 assert!(Brightness::new(1.1).is_err());
184 assert!(Brightness::new(f32::NAN).is_err());
185 assert!(Brightness::new(f32::INFINITY).is_err());
186 }
187
188 #[test]
189 fn pan_accepts_negative_range() {
190 assert!(Pan::new(-1.0).is_ok());
191 assert!(Pan::new(0.0).is_ok());
192 assert!(Pan::new(1.0).is_ok());
193 assert!(Pan::new(-1.001).is_err());
194 }
195
196 #[test]
197 fn min_max_consts_are_consistent() {
198 assert!((Brightness::MIN.get() - 0.0).abs() < f32::EPSILON);
199 assert!((Brightness::MAX.get() - 1.0).abs() < f32::EPSILON);
200 assert!((Pan::MIN.get() - -1.0).abs() < f32::EPSILON);
201 assert!((Pan::MAX.get() - 1.0).abs() < f32::EPSILON);
202 }
203}