1use topcoat_core::fnv1a::Fnv1a;
5
6#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
12pub struct ObliqueAngle(f32);
13
14impl ObliqueAngle {
15 pub const DEFAULT: Self = Self(14.0);
18
19 #[must_use]
26 #[track_caller]
27 pub const fn new(degrees: f32) -> Self {
28 assert!(
29 degrees >= -90.0 && degrees <= 90.0,
30 "oblique angle out of range -90deg..=90deg"
31 );
32 Self(degrees)
33 }
34
35 pub(crate) const fn hash(self, h: Fnv1a<u64>) -> Fnv1a<u64> {
37 h.write(&self.0.to_bits().to_le_bytes())
38 }
39}
40
41impl Default for ObliqueAngle {
42 fn default() -> Self {
43 Self::DEFAULT
44 }
45}
46
47impl From<ObliqueAngle> for f32 {
48 fn from(value: ObliqueAngle) -> Self {
49 value.0
50 }
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub struct ObliqueAngleOutOfRangeError;
57
58impl std::fmt::Display for ObliqueAngleOutOfRangeError {
59 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60 f.write_str("oblique angle out of range -90deg..=90deg")
61 }
62}
63
64impl std::error::Error for ObliqueAngleOutOfRangeError {}
65
66impl TryFrom<f32> for ObliqueAngle {
67 type Error = ObliqueAngleOutOfRangeError;
68
69 fn try_from(value: f32) -> Result<Self, Self::Error> {
70 if !(-90.0..=90.0).contains(&value) {
71 return Err(ObliqueAngleOutOfRangeError);
72 }
73 Ok(Self(value))
74 }
75}
76
77impl std::fmt::Display for ObliqueAngle {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 write!(f, "{}deg", self.0)
80 }
81}
82
83#[derive(Debug, Clone, Copy, PartialEq)]
88pub struct ObliqueAngleRange {
89 start: ObliqueAngle,
90 end: ObliqueAngle,
91}
92
93impl ObliqueAngleRange {
94 #[must_use]
100 #[track_caller]
101 pub const fn new(start: ObliqueAngle, end: ObliqueAngle) -> Self {
102 assert!(end.0 >= start.0, "oblique angle range must not be empty");
103 Self { start, end }
104 }
105
106 #[must_use]
113 #[track_caller]
114 pub const fn from_degrees(start: f32, end: f32) -> Self {
115 Self::new(ObliqueAngle::new(start), ObliqueAngle::new(end))
116 }
117
118 #[must_use]
120 pub const fn start(&self) -> ObliqueAngle {
121 self.start
122 }
123
124 #[must_use]
126 pub const fn end(&self) -> ObliqueAngle {
127 self.end
128 }
129
130 pub(crate) const fn hash(self, h: Fnv1a<u64>) -> Fnv1a<u64> {
132 self.end.hash(self.start.hash(h))
133 }
134}
135
136impl std::fmt::Display for ObliqueAngleRange {
137 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138 if self.start == self.end {
139 self.start.fmt(f)
140 } else {
141 write!(f, "{} {}", self.start, self.end)
142 }
143 }
144}
145
146#[derive(Debug, Clone, Copy, PartialEq, Default)]
152pub enum FontStyle {
153 #[default]
155 Normal,
156 Italic,
158 Oblique(Option<ObliqueAngleRange>),
163}
164
165impl FontStyle {
166 #[must_use]
168 pub const fn oblique() -> Self {
169 Self::Oblique(None)
170 }
171
172 #[must_use]
179 #[track_caller]
180 pub const fn oblique_angle(degrees: f32) -> Self {
181 let angle = ObliqueAngle::new(degrees);
182 Self::Oblique(Some(ObliqueAngleRange::new(angle, angle)))
183 }
184
185 #[must_use]
193 #[track_caller]
194 pub const fn oblique_range(start: f32, end: f32) -> Self {
195 Self::Oblique(Some(ObliqueAngleRange::from_degrees(start, end)))
196 }
197
198 pub(crate) const fn hash(self, h: Fnv1a<u64>) -> Fnv1a<u64> {
200 match self {
201 Self::Normal => h.write(b"n"),
202 Self::Italic => h.write(b"i"),
203 Self::Oblique(None) => h.write(b"o"),
204 Self::Oblique(Some(range)) => range.hash(h.write(b"oa")),
205 }
206 }
207}
208
209impl std::fmt::Display for FontStyle {
210 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211 match self {
212 Self::Normal => f.write_str("normal"),
213 Self::Italic => f.write_str("italic"),
214 Self::Oblique(None) => f.write_str("oblique"),
215 Self::Oblique(Some(angle)) => write!(f, "oblique {angle}"),
216 }
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn normal_and_italic_display_as_keywords() {
226 assert_eq!(FontStyle::Normal.to_string(), "normal");
227 assert_eq!(FontStyle::Italic.to_string(), "italic");
228 }
229
230 #[test]
231 fn default_is_normal() {
232 assert_eq!(FontStyle::default(), FontStyle::Normal);
233 }
234
235 #[test]
236 fn bare_oblique_displays_without_an_angle() {
237 assert_eq!(FontStyle::oblique().to_string(), "oblique");
238 }
239
240 #[test]
241 fn oblique_with_an_angle_displays_the_angle() {
242 assert_eq!(FontStyle::oblique_angle(14.0).to_string(), "oblique 14deg");
243 }
244
245 #[test]
246 fn oblique_with_a_negative_angle_displays_the_sign() {
247 assert_eq!(
248 FontStyle::oblique_angle(-12.5).to_string(),
249 "oblique -12.5deg",
250 );
251 }
252
253 #[test]
254 fn oblique_with_a_range_displays_both_angles() {
255 assert_eq!(
256 FontStyle::oblique_range(20.0, 40.0).to_string(),
257 "oblique 20deg 40deg",
258 );
259 }
260
261 #[test]
262 fn oblique_range_collapses_when_start_equals_end() {
263 assert_eq!(
264 FontStyle::oblique_range(14.0, 14.0).to_string(),
265 "oblique 14deg",
266 );
267 }
268
269 #[test]
270 fn default_oblique_angle_is_14_degrees() {
271 assert_eq!(ObliqueAngle::DEFAULT.to_string(), "14deg");
272 assert_eq!(ObliqueAngle::default(), ObliqueAngle::DEFAULT);
273 }
274
275 #[test]
276 fn angle_converts_to_f32() {
277 assert!((f32::from(ObliqueAngle::new(30.0)) - 30.0).abs() < 0.001);
278 }
279
280 #[test]
281 fn try_from_accepts_the_bounds() {
282 assert_eq!(ObliqueAngle::try_from(-90.0), Ok(ObliqueAngle::new(-90.0)));
283 assert_eq!(ObliqueAngle::try_from(90.0), Ok(ObliqueAngle::new(90.0)));
284 }
285
286 #[test]
287 fn try_from_rejects_out_of_range() {
288 assert_eq!(
289 ObliqueAngle::try_from(90.1),
290 Err(ObliqueAngleOutOfRangeError),
291 );
292 assert_eq!(
293 ObliqueAngle::try_from(-90.1),
294 Err(ObliqueAngleOutOfRangeError),
295 );
296 }
297
298 #[test]
299 #[should_panic = "out of range"]
300 fn new_panics_above_the_maximum() {
301 let _ = ObliqueAngle::new(90.1);
302 }
303
304 #[test]
305 #[should_panic = "out of range"]
306 fn new_panics_below_the_minimum() {
307 let _ = ObliqueAngle::new(-90.1);
308 }
309
310 #[test]
311 #[should_panic = "empty"]
312 fn range_panics_when_end_precedes_start() {
313 let _ = ObliqueAngleRange::from_degrees(40.0, 20.0);
314 }
315
316 #[test]
317 fn range_exposes_its_bounds() {
318 let range = ObliqueAngleRange::from_degrees(20.0, 40.0);
319 assert_eq!(range.start(), ObliqueAngle::new(20.0));
320 assert_eq!(range.end(), ObliqueAngle::new(40.0));
321 }
322}