1use alloc::string::String;
4use core::{fmt, num::ParseFloatError};
5use crate::corety::AzString;
6
7use crate::props::{
8 basic::{
9 angle::{
10 parse_angle_value, AngleValue, CssAngleValueParseError, CssAngleValueParseErrorOwned,
11 },
12 geometry::{LayoutPoint, LayoutRect},
13 },
14 formatter::PrintAsCssValue,
15};
16
17#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
20#[repr(C)]
21pub enum DirectionCorner {
22 Right,
23 Left,
24 Top,
25 Bottom,
26 TopRight,
27 TopLeft,
28 BottomRight,
29 BottomLeft,
30}
31
32impl fmt::Display for DirectionCorner {
33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34 write!(
35 f,
36 "{}",
37 match self {
38 Self::Right => "right",
39 Self::Left => "left",
40 Self::Top => "top",
41 Self::Bottom => "bottom",
42 Self::TopRight => "top right",
43 Self::TopLeft => "top left",
44 Self::BottomRight => "bottom right",
45 Self::BottomLeft => "bottom left",
46 }
47 )
48 }
49}
50
51impl PrintAsCssValue for DirectionCorner {
52 fn print_as_css_value(&self) -> String {
53 format!("{self}")
54 }
55}
56
57impl DirectionCorner {
58 #[must_use] pub const fn opposite(&self) -> Self {
59 use self::DirectionCorner::{Right, Left, Top, Bottom, TopRight, BottomLeft, TopLeft, BottomRight};
60 match *self {
61 Right => Left,
62 Left => Right,
63 Top => Bottom,
64 Bottom => Top,
65 TopRight => BottomLeft,
66 BottomLeft => TopRight,
67 TopLeft => BottomRight,
68 BottomRight => TopLeft,
69 }
70 }
71
72 #[must_use] pub const fn combine(&self, other: &Self) -> Option<Self> {
73 use self::DirectionCorner::{Right, Top, TopRight, Left, TopLeft, Bottom, BottomRight, BottomLeft};
74 match (*self, *other) {
75 (Right, Top) | (Top, Right) => Some(TopRight),
76 (Left, Top) | (Top, Left) => Some(TopLeft),
77 (Right, Bottom) | (Bottom, Right) => Some(BottomRight),
78 (Left, Bottom) | (Bottom, Left) => Some(BottomLeft),
79 _ => None,
80 }
81 }
82
83 #[must_use] pub const fn to_point(&self, rect: &LayoutRect) -> LayoutPoint {
84 use self::DirectionCorner::{Right, Left, Top, Bottom, TopRight, TopLeft, BottomRight, BottomLeft};
85 match *self {
86 Right => LayoutPoint {
87 x: rect.size.width,
88 y: rect.size.height / 2,
89 },
90 Left => LayoutPoint {
91 x: 0,
92 y: rect.size.height / 2,
93 },
94 Top => LayoutPoint {
95 x: rect.size.width / 2,
96 y: 0,
97 },
98 Bottom => LayoutPoint {
99 x: rect.size.width / 2,
100 y: rect.size.height,
101 },
102 TopRight => LayoutPoint {
103 x: rect.size.width,
104 y: 0,
105 },
106 TopLeft => LayoutPoint { x: 0, y: 0 },
107 BottomRight => LayoutPoint {
108 x: rect.size.width,
109 y: rect.size.height,
110 },
111 BottomLeft => LayoutPoint {
112 x: 0,
113 y: rect.size.height,
114 },
115 }
116 }
117}
118
119#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
121#[repr(C)]
122pub struct DirectionCorners {
123 pub dir_from: DirectionCorner,
125 pub dir_to: DirectionCorner,
127}
128
129#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
132#[repr(C, u8)]
133pub enum Direction {
134 Angle(AngleValue),
135 FromTo(DirectionCorners),
136}
137
138impl Default for Direction {
139 fn default() -> Self {
140 Self::FromTo(DirectionCorners {
141 dir_from: DirectionCorner::Top,
142 dir_to: DirectionCorner::Bottom,
143 })
144 }
145}
146
147impl PrintAsCssValue for Direction {
148 fn print_as_css_value(&self) -> String {
149 match self {
150 Self::Angle(a) => format!("{a}"),
151 Self::FromTo(d) => format!("to {}", d.dir_to), }
153 }
154}
155
156impl Direction {
157 #[must_use] pub fn to_points(&self, rect: &LayoutRect) -> (LayoutPoint, LayoutPoint) {
158 match self {
159 Self::Angle(angle_value) => {
160 let deg = (-angle_value.to_degrees()).rem_euclid(360.0);
164 let width_half = crate::cast::isize_to_f32(rect.size.width) / 2.0;
165 let height_half = crate::cast::isize_to_f32(rect.size.height) / 2.0;
166 let hypotenuse_len = libm::hypotf(width_half, height_half);
167 let angle_to_corner = libm::atanf(height_half / width_half).to_degrees();
168 let corner_angle = if deg < 90.0 {
169 90.0 - angle_to_corner
170 } else if deg < 180.0 {
171 90.0 + angle_to_corner
172 } else if deg < 270.0 {
173 270.0 - angle_to_corner
174 } else {
175 270.0 + angle_to_corner
176 };
177 let angle_diff = corner_angle - deg;
178 let line_length = libm::fabsf(hypotenuse_len * libm::cosf(angle_diff.to_radians()));
179 let dx = libm::sinf(deg.to_radians()) * line_length;
180 let dy = libm::cosf(deg.to_radians()) * line_length;
181 (
182 LayoutPoint::new(
183 crate::cast::f32_to_isize(libm::roundf(width_half - dx)),
184 crate::cast::f32_to_isize(libm::roundf(height_half + dy)),
185 ),
186 LayoutPoint::new(
187 crate::cast::f32_to_isize(libm::roundf(width_half + dx)),
188 crate::cast::f32_to_isize(libm::roundf(height_half - dy)),
189 ),
190 )
191 }
192 Self::FromTo(ft) => (ft.dir_from.to_point(rect), ft.dir_to.to_point(rect)),
193 }
194 }
195}
196
197#[derive(Debug, Copy, Clone, PartialEq, Eq)]
200pub enum CssDirectionCornerParseError<'a> {
201 InvalidDirection(&'a str),
202}
203
204impl_display! { CssDirectionCornerParseError<'a>, {
205 InvalidDirection(val) => format!("Invalid direction: \"{}\"", val),
206}}
207
208#[derive(Debug, Clone, PartialEq, Eq)]
209#[repr(C, u8)]
210pub enum CssDirectionCornerParseErrorOwned {
211 InvalidDirection(AzString),
212}
213
214impl CssDirectionCornerParseError<'_> {
215 #[must_use] pub fn to_contained(&self) -> CssDirectionCornerParseErrorOwned {
216 match self {
217 CssDirectionCornerParseError::InvalidDirection(s) => {
218 CssDirectionCornerParseErrorOwned::InvalidDirection((*s).to_string().into())
219 }
220 }
221 }
222}
223
224impl CssDirectionCornerParseErrorOwned {
225 #[must_use] pub fn to_shared(&self) -> CssDirectionCornerParseError<'_> {
226 match self {
227 Self::InvalidDirection(s) => {
228 CssDirectionCornerParseError::InvalidDirection(s.as_str())
229 }
230 }
231 }
232}
233
234#[derive(Debug, Clone, PartialEq, Eq)]
235pub enum CssDirectionParseError<'a> {
236 Error(&'a str),
237 InvalidArguments(&'a str),
238 ParseFloat(ParseFloatError),
239 CornerError(CssDirectionCornerParseError<'a>),
240 AngleError(CssAngleValueParseError<'a>),
241}
242
243impl_display! {CssDirectionParseError<'a>, {
244 Error(e) => e,
245 InvalidArguments(val) => format!("Invalid arguments: \"{}\"", val),
246 ParseFloat(e) => format!("Invalid value: {}", e),
247 CornerError(e) => format!("Invalid corner value: {}", e),
248 AngleError(e) => format!("Invalid angle value: {}", e),
249}}
250
251impl From<ParseFloatError> for CssDirectionParseError<'_> {
252 fn from(e: ParseFloatError) -> Self {
253 CssDirectionParseError::ParseFloat(e)
254 }
255}
256impl_from! { CssDirectionCornerParseError<'a>, CssDirectionParseError::CornerError }
257impl_from! { CssAngleValueParseError<'a>, CssDirectionParseError::AngleError }
258
259#[derive(Debug, Clone, PartialEq, Eq)]
260#[repr(C, u8)]
261pub enum CssDirectionParseErrorOwned {
262 Error(AzString),
263 InvalidArguments(AzString),
264 ParseFloat(crate::props::basic::error::ParseFloatError),
265 CornerError(CssDirectionCornerParseErrorOwned),
266 AngleError(CssAngleValueParseErrorOwned),
267}
268
269impl CssDirectionParseError<'_> {
270 #[must_use] pub fn to_contained(&self) -> CssDirectionParseErrorOwned {
271 match self {
272 CssDirectionParseError::Error(s) => CssDirectionParseErrorOwned::Error((*s).to_string().into()),
273 CssDirectionParseError::InvalidArguments(s) => {
274 CssDirectionParseErrorOwned::InvalidArguments((*s).to_string().into())
275 }
276 CssDirectionParseError::ParseFloat(e) => {
277 CssDirectionParseErrorOwned::ParseFloat(e.clone().into())
278 }
279 CssDirectionParseError::CornerError(e) => {
280 CssDirectionParseErrorOwned::CornerError(e.to_contained())
281 }
282 CssDirectionParseError::AngleError(e) => {
283 CssDirectionParseErrorOwned::AngleError(e.to_contained())
284 }
285 }
286 }
287}
288
289impl CssDirectionParseErrorOwned {
290 #[must_use] pub fn to_shared(&self) -> CssDirectionParseError<'_> {
291 match self {
292 Self::Error(s) => CssDirectionParseError::Error(s.as_str()),
293 Self::InvalidArguments(s) => {
294 CssDirectionParseError::InvalidArguments(s.as_str())
295 }
296 Self::ParseFloat(e) => {
297 CssDirectionParseError::ParseFloat(e.to_std())
298 }
299 Self::CornerError(e) => {
300 CssDirectionParseError::CornerError(e.to_shared())
301 }
302 Self::AngleError(e) => {
303 CssDirectionParseError::AngleError(e.to_shared())
304 }
305 }
306 }
307}
308
309#[cfg(feature = "parser")]
310fn parse_direction_corner(
311 input: &str,
312) -> Result<DirectionCorner, CssDirectionCornerParseError<'_>> {
313 match input {
314 "right" => Ok(DirectionCorner::Right),
315 "left" => Ok(DirectionCorner::Left),
316 "top" => Ok(DirectionCorner::Top),
317 "bottom" => Ok(DirectionCorner::Bottom),
318 _ => Err(CssDirectionCornerParseError::InvalidDirection(input)),
319 }
320}
321
322#[cfg(feature = "parser")]
323pub fn parse_direction(input: &str) -> Result<Direction, CssDirectionParseError<'_>> {
327 let mut input_iter = input.split_whitespace();
328 let first_input = input_iter
329 .next()
330 .ok_or(CssDirectionParseError::Error(input))?;
331
332 if let Ok(angle) = parse_angle_value(first_input) {
333 return Ok(Direction::Angle(angle));
334 }
335
336 if first_input != "to" {
337 return Err(CssDirectionParseError::InvalidArguments(input));
338 }
339
340 let components = input_iter.collect::<Vec<_>>();
341 if components.is_empty() || components.len() > 2 {
342 return Err(CssDirectionParseError::InvalidArguments(input));
343 }
344
345 let first_corner = parse_direction_corner(components[0])?;
346 let end = if components.len() == 2 {
347 let second_corner = parse_direction_corner(components[1])?;
348 first_corner
349 .combine(&second_corner)
350 .ok_or(CssDirectionParseError::InvalidArguments(input))?
351 } else {
352 first_corner
353 };
354
355 Ok(Direction::FromTo(DirectionCorners {
356 dir_from: end.opposite(),
357 dir_to: end,
358 }))
359}
360
361#[cfg(all(test, feature = "parser"))]
362mod tests {
363 use super::*;
364 use crate::props::basic::angle::AngleValue;
365
366 #[test]
367 fn test_parse_direction_angle() {
368 assert_eq!(
369 parse_direction("45deg").unwrap(),
370 Direction::Angle(AngleValue::deg(45.0))
371 );
372 assert_eq!(
373 parse_direction(" -0.25turn ").unwrap(),
374 Direction::Angle(AngleValue::turn(-0.25))
375 );
376 }
377
378 #[test]
379 fn test_parse_direction_corners() {
380 assert_eq!(
381 parse_direction("to right").unwrap(),
382 Direction::FromTo(DirectionCorners {
383 dir_from: DirectionCorner::Left,
384 dir_to: DirectionCorner::Right,
385 })
386 );
387 assert_eq!(
388 parse_direction("to top left").unwrap(),
389 Direction::FromTo(DirectionCorners {
390 dir_from: DirectionCorner::BottomRight,
391 dir_to: DirectionCorner::TopLeft,
392 })
393 );
394 assert_eq!(
395 parse_direction("to left top").unwrap(),
396 Direction::FromTo(DirectionCorners {
397 dir_from: DirectionCorner::BottomRight,
398 dir_to: DirectionCorner::TopLeft,
399 })
400 );
401 }
402
403 #[test]
404 fn test_parse_direction_errors() {
405 assert!(parse_direction("").is_err());
406 assert!(parse_direction("to").is_err());
407 assert!(parse_direction("right").is_err());
408 assert!(parse_direction("to center").is_err());
409 assert!(parse_direction("to top right bottom").is_err());
410 assert!(parse_direction("to top top").is_err());
411 }
412}
413
414#[cfg(test)]
415mod autotest_generated {
416 #![allow(clippy::float_cmp, clippy::too_many_lines)]
419
420 use alloc::collections::BTreeSet;
421
422 use super::*;
423 use crate::props::basic::geometry::LayoutSize;
424
425 const ALL_CORNERS: [DirectionCorner; 8] = [
428 DirectionCorner::Right,
429 DirectionCorner::Left,
430 DirectionCorner::Top,
431 DirectionCorner::Bottom,
432 DirectionCorner::TopRight,
433 DirectionCorner::TopLeft,
434 DirectionCorner::BottomRight,
435 DirectionCorner::BottomLeft,
436 ];
437
438 const SIDES: [DirectionCorner; 4] = [
439 DirectionCorner::Right,
440 DirectionCorner::Left,
441 DirectionCorner::Top,
442 DirectionCorner::Bottom,
443 ];
444
445 const DIAGONALS: [DirectionCorner; 4] = [
446 DirectionCorner::TopRight,
447 DirectionCorner::TopLeft,
448 DirectionCorner::BottomRight,
449 DirectionCorner::BottomLeft,
450 ];
451
452 fn rect(w: isize, h: isize) -> LayoutRect {
453 LayoutRect::new(LayoutPoint::zero(), LayoutSize::new(w, h))
454 }
455
456 fn rect_at(x: isize, y: isize, w: isize, h: isize) -> LayoutRect {
457 LayoutRect::new(LayoutPoint::new(x, y), LayoutSize::new(w, h))
458 }
459
460 fn canonical(dir_to: DirectionCorner) -> Direction {
462 Direction::FromTo(DirectionCorners {
463 dir_from: dir_to.opposite(),
464 dir_to,
465 })
466 }
467
468 fn assert_near(actual: LayoutPoint, expected: LayoutPoint, tol: isize) {
469 assert!(
470 (actual.x - expected.x).abs() <= tol && (actual.y - expected.y).abs() <= tol,
471 "expected {expected:?} (±{tol}), got {actual:?}"
472 );
473 }
474
475 const CONST_RECT: LayoutRect =
478 LayoutRect::new(LayoutPoint::new(7, 9), LayoutSize::new(200, 100));
479 const CONST_OPPOSITE: DirectionCorner = DirectionCorner::TopRight.opposite();
480 const CONST_COMBINED: Option<DirectionCorner> =
481 DirectionCorner::Right.combine(&DirectionCorner::Top);
482 const CONST_POINT: LayoutPoint = DirectionCorner::Right.to_point(&CONST_RECT);
483
484 #[test]
485 fn const_fns_evaluate_at_compile_time() {
486 assert_eq!(CONST_OPPOSITE, DirectionCorner::BottomLeft);
487 assert_eq!(CONST_COMBINED, Some(DirectionCorner::TopRight));
488 assert_eq!(CONST_POINT, LayoutPoint::new(200, 50));
490 }
491
492 #[test]
495 fn corner_display_exact_values_and_wellformed() {
496 let expected = [
497 (DirectionCorner::Right, "right"),
498 (DirectionCorner::Left, "left"),
499 (DirectionCorner::Top, "top"),
500 (DirectionCorner::Bottom, "bottom"),
501 (DirectionCorner::TopRight, "top right"),
502 (DirectionCorner::TopLeft, "top left"),
503 (DirectionCorner::BottomRight, "bottom right"),
504 (DirectionCorner::BottomLeft, "bottom left"),
505 ];
506 for (corner, want) in expected {
507 let printed = format!("{corner}");
508 assert_eq!(printed, want);
509 assert_eq!(corner.print_as_css_value(), printed);
511 assert!(!printed.is_empty());
513 assert!(printed.is_ascii());
514 assert_eq!(printed, printed.to_lowercase());
515 assert_eq!(printed, printed.trim());
516 }
517 }
518
519 #[test]
520 fn corner_display_is_injective() {
521 let printed: BTreeSet<String> = ALL_CORNERS.iter().map(|c| format!("{c}")).collect();
522 assert_eq!(printed.len(), ALL_CORNERS.len());
523 }
524
525 #[test]
526 fn direction_default_and_display_do_not_panic() {
527 assert_eq!(
529 Direction::default(),
530 Direction::FromTo(DirectionCorners {
531 dir_from: DirectionCorner::Top,
532 dir_to: DirectionCorner::Bottom,
533 })
534 );
535 assert_eq!(Direction::default().print_as_css_value(), "to bottom");
536
537 for v in [
539 0.0_f32,
540 -0.0,
541 f32::NAN,
542 f32::INFINITY,
543 f32::NEG_INFINITY,
544 f32::MAX,
545 f32::MIN,
546 f32::MIN_POSITIVE,
547 ] {
548 let s = Direction::Angle(AngleValue::deg(v)).print_as_css_value();
549 assert!(!s.is_empty(), "empty serialization for {v}");
550 assert!(s.ends_with("deg"), "unexpected serialization: {s}");
551 assert!(!s.contains("NaN"), "NaN leaked into CSS output: {s}");
553 assert!(!s.contains("inf"), "inf leaked into CSS output: {s}");
554 }
555 }
556
557 #[test]
560 fn opposite_known_values() {
561 assert_eq!(DirectionCorner::Right.opposite(), DirectionCorner::Left);
562 assert_eq!(DirectionCorner::Left.opposite(), DirectionCorner::Right);
563 assert_eq!(DirectionCorner::Top.opposite(), DirectionCorner::Bottom);
564 assert_eq!(DirectionCorner::Bottom.opposite(), DirectionCorner::Top);
565 assert_eq!(
566 DirectionCorner::TopRight.opposite(),
567 DirectionCorner::BottomLeft
568 );
569 assert_eq!(
570 DirectionCorner::BottomLeft.opposite(),
571 DirectionCorner::TopRight
572 );
573 assert_eq!(
574 DirectionCorner::TopLeft.opposite(),
575 DirectionCorner::BottomRight
576 );
577 assert_eq!(
578 DirectionCorner::BottomRight.opposite(),
579 DirectionCorner::TopLeft
580 );
581 }
582
583 #[test]
584 fn opposite_is_an_involution_and_a_bijection() {
585 let mut images = BTreeSet::new();
586 for c in ALL_CORNERS {
587 assert_ne!(c.opposite(), c, "{c} is its own opposite");
589 assert_eq!(c.opposite().opposite(), c, "opposite² != id for {c}");
591 assert_eq!(
593 SIDES.contains(&c),
594 SIDES.contains(&c.opposite()),
595 "{c} changed class under opposite()"
596 );
597 images.insert(c.opposite());
598 }
599 assert_eq!(images.len(), 8, "opposite() is not a bijection");
600 }
601
602 #[test]
605 fn combine_exhaustive_over_all_64_pairs() {
606 let mut some_count = 0_usize;
607 for a in ALL_CORNERS {
608 for b in ALL_CORNERS {
609 let r = a.combine(&b);
610
611 assert_eq!(r, b.combine(&a), "combine({a}, {b}) is not commutative");
613
614 match r {
615 Some(c) => {
616 some_count += 1;
617 assert!(SIDES.contains(&a) && SIDES.contains(&b));
620 assert!(DIAGONALS.contains(&c), "combine({a}, {b}) = {c}, not a corner");
621 assert_ne!(a, b);
622 assert_ne!(a.opposite(), b, "opposite sides must not combine");
623 let name = format!("{c}");
625 assert!(name.contains(&format!("{a}")) && name.contains(&format!("{b}")));
626 assert_eq!(a.opposite().combine(&b.opposite()), Some(c.opposite()));
628 }
629 None => {
630 assert!(
631 !SIDES.contains(&a)
632 || !SIDES.contains(&b)
633 || a == b
634 || a.opposite() == b,
635 "combine({a}, {b}) unexpectedly returned None"
636 );
637 }
638 }
639 }
640 }
641 assert_eq!(some_count, 8);
643 }
644
645 #[test]
646 fn combine_degenerate_pairs_are_none() {
647 for c in ALL_CORNERS {
648 assert_eq!(c.combine(&c), None, "{c} combined with itself");
649 assert_eq!(c.combine(&c.opposite()), None, "{c} combined with opposite");
650 }
651 assert_eq!(
652 DirectionCorner::Top.combine(&DirectionCorner::Bottom),
653 None
654 );
655 assert_eq!(DirectionCorner::Left.combine(&DirectionCorner::Right), None);
656 for d in DIAGONALS {
658 for c in ALL_CORNERS {
659 assert_eq!(d.combine(&c), None, "{d} combined with {c}");
660 }
661 }
662 }
663
664 #[test]
667 fn to_point_zero_rect_is_origin() {
668 let r = rect(0, 0);
669 for c in ALL_CORNERS {
670 assert_eq!(c.to_point(&r), LayoutPoint::zero(), "corner {c}");
671 }
672 }
673
674 #[test]
675 fn to_point_known_values() {
676 let r = rect(200, 100);
677 assert_eq!(
678 DirectionCorner::Right.to_point(&r),
679 LayoutPoint::new(200, 50)
680 );
681 assert_eq!(DirectionCorner::Left.to_point(&r), LayoutPoint::new(0, 50));
682 assert_eq!(DirectionCorner::Top.to_point(&r), LayoutPoint::new(100, 0));
683 assert_eq!(
684 DirectionCorner::Bottom.to_point(&r),
685 LayoutPoint::new(100, 100)
686 );
687 assert_eq!(
688 DirectionCorner::TopRight.to_point(&r),
689 LayoutPoint::new(200, 0)
690 );
691 assert_eq!(DirectionCorner::TopLeft.to_point(&r), LayoutPoint::new(0, 0));
692 assert_eq!(
693 DirectionCorner::BottomRight.to_point(&r),
694 LayoutPoint::new(200, 100)
695 );
696 assert_eq!(
697 DirectionCorner::BottomLeft.to_point(&r),
698 LayoutPoint::new(0, 100)
699 );
700 }
701
702 #[test]
703 fn to_point_ignores_rect_origin() {
704 let local = rect(200, 100);
707 for offset in [(0, 0), (1000, -500), (isize::MIN, isize::MAX)] {
708 let moved = rect_at(offset.0, offset.1, 200, 100);
709 for c in ALL_CORNERS {
710 assert_eq!(
711 c.to_point(&moved),
712 c.to_point(&local),
713 "corner {c} shifted by origin {offset:?}"
714 );
715 }
716 }
717 }
718
719 #[test]
720 fn to_point_opposite_corners_sum_to_the_full_extent() {
721 for (w, h) in [(200_isize, 100_isize), (2, 2), (0, 0), (-40, -60)] {
723 let r = rect(w, h);
724 for c in ALL_CORNERS {
725 let p = c.to_point(&r);
726 let q = c.opposite().to_point(&r);
727 assert_eq!(p.x + q.x, w, "x-sum for {c} in {w}x{h}");
728 assert_eq!(p.y + q.y, h, "y-sum for {c} in {w}x{h}");
729 }
730 }
731 }
732
733 #[test]
734 fn to_point_odd_and_negative_extents_truncate_toward_zero() {
735 let r = rect(3, 3);
737 assert_eq!(DirectionCorner::Top.to_point(&r), LayoutPoint::new(1, 0));
738 assert_eq!(DirectionCorner::Right.to_point(&r), LayoutPoint::new(3, 1));
739
740 let neg = rect(-3, -3);
741 assert_eq!(DirectionCorner::Top.to_point(&neg), LayoutPoint::new(-1, 0));
742 assert_eq!(
743 DirectionCorner::Right.to_point(&neg),
744 LayoutPoint::new(-3, -1)
745 );
746 assert_eq!(
747 DirectionCorner::BottomLeft.to_point(&neg),
748 LayoutPoint::new(0, -3)
749 );
750 }
751
752 #[test]
753 fn to_point_isize_extremes_do_not_overflow() {
754 let max = rect(isize::MAX, isize::MAX);
757 assert_eq!(
758 DirectionCorner::Right.to_point(&max),
759 LayoutPoint::new(isize::MAX, isize::MAX / 2)
760 );
761 assert_eq!(
762 DirectionCorner::Bottom.to_point(&max),
763 LayoutPoint::new(isize::MAX / 2, isize::MAX)
764 );
765 assert_eq!(
766 DirectionCorner::BottomRight.to_point(&max),
767 LayoutPoint::new(isize::MAX, isize::MAX)
768 );
769
770 let min = rect(isize::MIN, isize::MIN);
771 assert_eq!(
772 DirectionCorner::Right.to_point(&min),
773 LayoutPoint::new(isize::MIN, isize::MIN / 2)
774 );
775 assert_eq!(
776 DirectionCorner::Top.to_point(&min),
777 LayoutPoint::new(isize::MIN / 2, 0)
778 );
779
780 let mixed = rect(isize::MAX, isize::MIN);
782 for c in ALL_CORNERS {
783 let p = c.to_point(&mixed);
784 assert!(p.x == 0 || p.x == isize::MAX || p.x == isize::MAX / 2);
785 assert!(p.y == 0 || p.y == isize::MIN || p.y == isize::MIN / 2);
786 }
787 }
788
789 #[test]
792 fn to_points_fromto_delegates_to_to_point() {
793 let r = rect(200, 100);
794 for from in ALL_CORNERS {
795 for to in ALL_CORNERS {
796 let d = Direction::FromTo(DirectionCorners {
797 dir_from: from,
798 dir_to: to,
799 });
800 assert_eq!(d.to_points(&r), (from.to_point(&r), to.to_point(&r)));
801 }
802 }
803 }
804
805 #[test]
806 fn to_points_angle_zero_deg_runs_bottom_to_top() {
807 let r = rect(100, 100);
809 let (start, end) = Direction::Angle(AngleValue::deg(0.0)).to_points(&r);
810 assert_near(start, LayoutPoint::new(50, 100), 1);
811 assert_near(end, LayoutPoint::new(50, 0), 1);
812 }
813
814 #[test]
815 fn to_points_angle_180_deg_runs_top_to_bottom() {
816 let r = rect(100, 100);
818 let (start, end) = Direction::Angle(AngleValue::deg(180.0)).to_points(&r);
819 assert_near(start, LayoutPoint::new(50, 0), 1);
820 assert_near(end, LayoutPoint::new(50, 100), 1);
821 }
822
823 #[test]
824 fn to_points_angle_90_deg_is_horizontal_across_the_full_width() {
825 let r = rect(100, 100);
826 let (start, end) = Direction::Angle(AngleValue::deg(90.0)).to_points(&r);
827 assert!((start.y - 50).abs() <= 1, "start.y = {}", start.y);
829 assert!((end.y - 50).abs() <= 1, "end.y = {}", end.y);
830 let mut xs = [start.x, end.x];
831 xs.sort_unstable();
832 assert!(xs[0].abs() <= 1 && (xs[1] - 100).abs() <= 1, "xs = {xs:?}");
833 assert_ne!(start, end);
834 }
835
836 #[test]
837 fn to_points_angle_is_symmetric_about_the_rect_center() {
838 let r = rect(200, 100);
841 let mut angles = Vec::new();
842 let mut deg = -720.0_f32;
843 while deg <= 720.0 {
844 angles.push(AngleValue::deg(deg));
845 deg += 15.0;
846 }
847 angles.extend([
848 AngleValue::rad(1.5),
849 AngleValue::rad(-3.0),
850 AngleValue::grad(100.0),
851 AngleValue::grad(-400.0),
852 AngleValue::turn(0.25),
853 AngleValue::turn(-2.5),
854 AngleValue::percent(50.0),
855 AngleValue::percent(-125.0),
856 ]);
857
858 for a in angles {
859 let (start, end) = Direction::Angle(a).to_points(&r);
860 assert!(
861 (start.x + end.x - 200).abs() <= 1,
862 "x not centered for {a}: {start:?} / {end:?}"
863 );
864 assert!(
865 (start.y + end.y - 100).abs() <= 1,
866 "y not centered for {a}: {start:?} / {end:?}"
867 );
868 let len_sq = (start.x - end.x).pow(2) + (start.y - end.y).pow(2);
873 assert!(
874 len_sq <= 4 * (100 * 100 + 50 * 50) + 1000,
875 "gradient line longer than the rect diagonal for {a}: {start:?} / {end:?}"
876 );
877 }
878 }
879
880 #[test]
881 fn to_points_zero_sized_rect_yields_origin_not_nan() {
882 let r = rect(0, 0);
886 for a in [
887 AngleValue::deg(0.0),
888 AngleValue::deg(45.0),
889 AngleValue::deg(-137.5),
890 AngleValue::turn(0.75),
891 ] {
892 let (start, end) = Direction::Angle(a).to_points(&r);
893 assert_eq!(start, LayoutPoint::zero(), "start for {a}");
894 assert_eq!(end, LayoutPoint::zero(), "end for {a}");
895 }
896 }
897
898 #[test]
899 fn to_points_degenerate_axis_rects_do_not_panic() {
900 let thin = rect(0, 100);
902 let (s, e) = Direction::Angle(AngleValue::deg(0.0)).to_points(&thin);
903 assert_eq!(s.x, 0);
904 assert_eq!(e.x, 0);
905 assert!((s.y + e.y - 100).abs() <= 1);
906
907 let flat = rect(100, 0);
908 let (s, e) = Direction::Angle(AngleValue::deg(90.0)).to_points(&flat);
909 assert_eq!(s.y, 0);
910 assert_eq!(e.y, 0);
911 assert!((s.x + e.x - 100).abs() <= 1);
912 }
913
914 #[test]
915 fn to_points_nan_angle_collapses_to_zero_degrees() {
916 let nan = AngleValue::deg(f32::NAN);
919 assert!(nan.to_degrees().is_finite());
920 assert_eq!(nan.to_degrees(), 0.0);
921 assert_eq!(nan, AngleValue::deg(0.0));
922
923 let r = rect(100, 100);
924 assert_eq!(
925 Direction::Angle(nan).to_points(&r),
926 Direction::Angle(AngleValue::deg(0.0)).to_points(&r)
927 );
928 }
929
930 #[test]
931 fn to_points_infinite_angle_saturates_and_stays_finite() {
932 for v in [f32::INFINITY, f32::NEG_INFINITY, f32::MAX, f32::MIN] {
933 let a = AngleValue::deg(v);
934 let deg = a.to_degrees();
935 assert!(deg.is_finite(), "non-finite degrees for {v}");
936 assert!((0.0..=360.0).contains(°), "{v} normalized to {deg}");
937
938 let r = rect(200, 100);
939 let first = Direction::Angle(a).to_points(&r);
940 assert_eq!(first, Direction::Angle(a).to_points(&r));
942 }
943 }
944
945 #[test]
946 fn to_points_isize_extreme_rects_do_not_panic() {
947 for (w, h) in [
948 (isize::MAX, isize::MAX),
949 (isize::MIN, isize::MIN),
950 (isize::MAX, isize::MIN),
951 (isize::MIN, 1),
952 (-1, isize::MAX),
953 ] {
954 let r = rect(w, h);
955 for a in [
956 AngleValue::deg(45.0),
957 AngleValue::deg(0.0),
958 AngleValue::deg(270.0),
959 ] {
960 let d = Direction::Angle(a);
961 assert_eq!(d.to_points(&r), d.to_points(&r), "{w}x{h} @ {a}");
964 }
965 let d = canonical(DirectionCorner::BottomRight);
967 assert_eq!(d.to_points(&r).1, LayoutPoint::new(w, h));
968 }
969 }
970
971 #[cfg(feature = "parser")]
974 #[test]
975 fn parse_corner_valid_minimal() {
976 assert_eq!(parse_direction_corner("right"), Ok(DirectionCorner::Right));
977 assert_eq!(parse_direction_corner("left"), Ok(DirectionCorner::Left));
978 assert_eq!(parse_direction_corner("top"), Ok(DirectionCorner::Top));
979 assert_eq!(parse_direction_corner("bottom"), Ok(DirectionCorner::Bottom));
980 }
981
982 #[cfg(feature = "parser")]
983 #[test]
984 fn parse_corner_rejects_untrimmed_cased_and_diagonal_input() {
985 for bad in [
988 "", " ", " ", "\t", "\n", "\r\n", " right", "right ", "right\n", "Right", "RIGHT",
989 "rIgHt", "top right", "top-right", "topright", "right;", "right)", "center", "start",
990 "end", "to", "to right",
991 ] {
992 assert_eq!(
993 parse_direction_corner(bad),
994 Err(CssDirectionCornerParseError::InvalidDirection(bad)),
995 "input {bad:?} was not rejected verbatim"
996 );
997 }
998 }
999
1000 #[cfg(feature = "parser")]
1001 #[test]
1002 fn parse_corner_garbage_and_unicode_do_not_panic() {
1003 for bad in [
1004 "!@#$%^&*()",
1005 "\u{0}",
1006 "\u{0}right",
1007 "right\u{0}",
1008 "\u{1F600}",
1009 "to \u{1F600}",
1010 "ri\u{0301}ght", "\u{0440}ight", "\u{200b}right", "right\u{200b}",
1014 "\u{feff}right", "\u{202e}right", "right", "\u{a0}right", ] {
1019 assert_eq!(
1020 parse_direction_corner(bad),
1021 Err(CssDirectionCornerParseError::InvalidDirection(bad)),
1022 "unicode input {bad:?} was not rejected"
1023 );
1024 }
1025 }
1026
1027 #[cfg(feature = "parser")]
1028 #[test]
1029 fn parse_corner_boundary_numbers_are_rejected() {
1030 for bad in [
1031 "0",
1032 "-0",
1033 "9223372036854775807",
1034 "-9223372036854775808",
1035 "1e400",
1036 "1e-400",
1037 "NaN",
1038 "inf",
1039 "-inf",
1040 ] {
1041 assert!(
1042 parse_direction_corner(bad).is_err(),
1043 "numeric input {bad:?} parsed as a corner"
1044 );
1045 }
1046 }
1047
1048 #[cfg(feature = "parser")]
1049 #[test]
1050 fn parse_corner_extremely_long_input_is_rejected_quickly() {
1051 let long = "a".repeat(1_000_000);
1052 assert!(parse_direction_corner(&long).is_err());
1053
1054 let repeated = "right".repeat(200_000);
1055 assert!(parse_direction_corner(&repeated).is_err());
1056
1057 let nested = "(".repeat(100_000);
1059 assert!(parse_direction_corner(&nested).is_err());
1060 }
1061
1062 #[cfg(feature = "parser")]
1063 #[test]
1064 fn parse_corner_error_round_trips_through_owned() {
1065 let long = "top".repeat(10_000);
1066 for bad in ["", "bogus", "\u{1F600}", long.as_str()] {
1067 let Err(err) = parse_direction_corner(bad) else {
1068 panic!("{bad:?} unexpectedly parsed");
1069 };
1070 let owned = err.to_contained();
1071 assert_eq!(owned.to_shared(), err);
1072 }
1073 }
1074
1075 #[cfg(feature = "parser")]
1078 #[test]
1079 fn parse_direction_empty_and_whitespace_only() {
1080 for empty in ["", " ", " ", "\t", "\n", "\r\n", "\t \n \r", "\u{a0}", "\u{3000}"] {
1081 assert!(
1082 matches!(
1083 parse_direction(empty),
1084 Err(CssDirectionParseError::Error(_))
1085 ),
1086 "whitespace input {empty:?} did not yield Error"
1087 );
1088 }
1089 }
1090
1091 #[cfg(feature = "parser")]
1092 #[test]
1093 fn parse_direction_valid_minimal_angles() {
1094 assert_eq!(
1095 parse_direction("45deg").unwrap(),
1096 Direction::Angle(AngleValue::deg(45.0))
1097 );
1098 assert_eq!(
1100 parse_direction("0").unwrap(),
1101 Direction::Angle(AngleValue::deg(0.0))
1102 );
1103 assert_eq!(
1104 parse_direction("1.5rad").unwrap(),
1105 Direction::Angle(AngleValue::rad(1.5))
1106 );
1107 assert_eq!(
1108 parse_direction("100grad").unwrap(),
1109 Direction::Angle(AngleValue::grad(100.0))
1110 );
1111 assert_eq!(
1112 parse_direction("50%").unwrap(),
1113 Direction::Angle(AngleValue::percent(50.0))
1114 );
1115 }
1116
1117 #[cfg(feature = "parser")]
1118 #[test]
1119 fn parse_direction_all_corner_spellings() {
1120 let cases = [
1121 ("to right", DirectionCorner::Right),
1122 ("to left", DirectionCorner::Left),
1123 ("to top", DirectionCorner::Top),
1124 ("to bottom", DirectionCorner::Bottom),
1125 ("to top right", DirectionCorner::TopRight),
1126 ("to right top", DirectionCorner::TopRight),
1127 ("to top left", DirectionCorner::TopLeft),
1128 ("to left top", DirectionCorner::TopLeft),
1129 ("to bottom right", DirectionCorner::BottomRight),
1130 ("to right bottom", DirectionCorner::BottomRight),
1131 ("to bottom left", DirectionCorner::BottomLeft),
1132 ("to left bottom", DirectionCorner::BottomLeft),
1133 ];
1134 for (input, dir_to) in cases {
1135 let parsed = parse_direction(input).unwrap();
1136 assert_eq!(parsed, canonical(dir_to), "input {input:?}");
1137 let Direction::FromTo(ft) = parsed else {
1139 panic!("{input:?} did not parse to FromTo");
1140 };
1141 assert_eq!(ft.dir_from, ft.dir_to.opposite());
1142 }
1143 }
1144
1145 #[cfg(feature = "parser")]
1146 #[test]
1147 fn parse_direction_surrounding_whitespace_is_ignored() {
1148 let expect = canonical(DirectionCorner::Right);
1149 for input in ["to right", " to right ", "\tto\nright\r", "to right"] {
1150 assert_eq!(parse_direction(input).unwrap(), expect, "input {input:?}");
1151 }
1152 }
1153
1154 #[cfg(feature = "parser")]
1155 #[test]
1156 fn parse_direction_error_classification() {
1157 assert!(matches!(
1158 parse_direction(""),
1159 Err(CssDirectionParseError::Error(_))
1160 ));
1161 assert!(matches!(
1163 parse_direction("right"),
1164 Err(CssDirectionParseError::InvalidArguments(_))
1165 ));
1166 assert!(matches!(
1168 parse_direction("to"),
1169 Err(CssDirectionParseError::InvalidArguments(_))
1170 ));
1171 assert!(matches!(
1173 parse_direction("to top right bottom"),
1174 Err(CssDirectionParseError::InvalidArguments(_))
1175 ));
1176 assert!(matches!(
1178 parse_direction("to center"),
1179 Err(CssDirectionParseError::CornerError(
1180 CssDirectionCornerParseError::InvalidDirection("center")
1181 ))
1182 ));
1183 for bad in [
1185 "to top top",
1186 "to top bottom",
1187 "to bottom top",
1188 "to left right",
1189 "to right left",
1190 "to left left",
1191 ] {
1192 assert!(
1193 matches!(
1194 parse_direction(bad),
1195 Err(CssDirectionParseError::InvalidArguments(_))
1196 ),
1197 "input {bad:?} was accepted or misclassified"
1198 );
1199 }
1200 }
1201
1202 #[cfg(feature = "parser")]
1203 #[test]
1204 fn parse_direction_is_case_sensitive() {
1205 for bad in ["TO RIGHT", "To Right", "to RIGHT", "TO right", "45DEG"] {
1208 assert!(parse_direction(bad).is_err(), "{bad:?} was accepted");
1209 }
1210 }
1211
1212 #[cfg(feature = "parser")]
1213 #[test]
1214 fn parse_direction_leading_trailing_junk_is_rejected() {
1215 for bad in [
1216 "45deg;",
1217 "45deg;garbage",
1218 "to right;",
1219 "to;right",
1220 "to right)",
1221 "(45deg)",
1222 "to right,",
1223 "-->45deg",
1224 ] {
1225 assert!(parse_direction(bad).is_err(), "{bad:?} was accepted");
1226 }
1227 }
1228
1229 #[cfg(feature = "parser")]
1230 #[test]
1231 fn parse_direction_ignores_tokens_after_a_leading_angle() {
1232 let expect = Direction::Angle(AngleValue::deg(45.0));
1235 assert_eq!(parse_direction("45deg garbage").unwrap(), expect);
1236 assert_eq!(parse_direction("45deg to right").unwrap(), expect);
1237 assert_eq!(parse_direction("45deg 90deg").unwrap(), expect);
1238 }
1239
1240 #[cfg(feature = "parser")]
1241 #[test]
1242 fn parse_direction_garbage_and_unicode_do_not_panic() {
1243 for bad in [
1244 "!@#$%^&*()",
1245 "\u{0}",
1246 "\u{1F600}",
1247 "to \u{1F600}",
1248 "45\u{00b0}", "to right\u{200b}",
1250 "\u{feff}to right",
1251 "to right",
1252 "to\u{200b}right",
1253 "\u{202e}to right",
1254 "deg",
1255 "%",
1256 "-",
1257 "+",
1258 ".",
1259 "e",
1260 "todeg",
1261 ] {
1262 match parse_direction(bad) {
1264 Ok(Direction::Angle(a)) => {
1265 assert!(a.to_degrees().is_finite(), "{bad:?} -> non-finite angle");
1266 }
1267 Ok(Direction::FromTo(ft)) => {
1268 assert_eq!(ft.dir_from, ft.dir_to.opposite(), "{bad:?}");
1269 }
1270 Err(_) => {}
1271 }
1272 }
1273 }
1274
1275 #[cfg(feature = "parser")]
1276 #[test]
1277 fn parse_direction_unicode_whitespace_separator_is_wellformed() {
1278 if let Ok(d) = parse_direction("to\u{a0}right") {
1282 assert_eq!(d, canonical(DirectionCorner::Right));
1283 }
1284 }
1285
1286 #[cfg(feature = "parser")]
1287 #[test]
1288 fn parse_direction_boundary_numbers_never_yield_nan_or_inf() {
1289 for input in [
1290 "0",
1291 "-0",
1292 "0deg",
1293 "-0deg",
1294 "360deg",
1295 "-360deg",
1296 "9223372036854775807",
1297 "-9223372036854775808deg",
1298 "340282350000000000000000000000000000000deg", "1e400", "-1e400deg",
1301 "1e-400deg", "NaN",
1303 "nan deg",
1304 "inf",
1305 "-infinity",
1306 "0.0000001turn",
1307 "-99999999rad",
1308 ] {
1309 match parse_direction(input) {
1310 Ok(Direction::Angle(a)) => {
1311 let deg = a.to_degrees();
1312 assert!(deg.is_finite(), "{input:?} produced non-finite {deg}");
1313 assert!(
1314 (0.0..=360.0).contains(°),
1315 "{input:?} normalized outside [0,360]: {deg}"
1316 );
1317 assert!(
1318 a.to_degrees_raw().is_finite(),
1319 "{input:?} produced non-finite raw degrees"
1320 );
1321 let (s, e) = Direction::Angle(a).to_points(&rect(200, 100));
1323 assert!((s.x + e.x - 200).abs() <= 1, "{input:?}: {s:?}/{e:?}");
1324 assert!((s.y + e.y - 100).abs() <= 1, "{input:?}: {s:?}/{e:?}");
1325 }
1326 Ok(Direction::FromTo(_)) => panic!("{input:?} parsed as a corner direction"),
1327 Err(_) => {}
1328 }
1329 }
1330 }
1331
1332 #[cfg(feature = "parser")]
1333 #[test]
1334 fn parse_direction_nan_string_silently_becomes_zero_degrees() {
1335 let parsed = parse_direction("NaN").unwrap();
1338 assert_eq!(parsed, Direction::Angle(AngleValue::deg(0.0)));
1339 }
1340
1341 #[cfg(feature = "parser")]
1342 #[test]
1343 fn parse_direction_extremely_long_input_terminates() {
1344 let garbage = "x".repeat(1_000_000);
1346 assert!(parse_direction(&garbage).is_err());
1347
1348 let blank = " ".repeat(1_000_000);
1350 assert!(matches!(
1351 parse_direction(&blank),
1352 Err(CssDirectionParseError::Error(_))
1353 ));
1354
1355 let many = format!("to {}", "top ".repeat(50_000));
1357 assert!(matches!(
1358 parse_direction(&many),
1359 Err(CssDirectionParseError::InvalidArguments(_))
1360 ));
1361
1362 let huge_number = format!("{}deg", "1".repeat(100_000));
1365 if let Ok(Direction::Angle(a)) = parse_direction(&huge_number) {
1366 assert!(a.to_degrees().is_finite());
1367 }
1368
1369 let nested = format!("to {}", "(".repeat(100_000));
1371 assert!(parse_direction(&nested).is_err());
1372 }
1373
1374 #[cfg(feature = "parser")]
1375 #[test]
1376 fn parse_direction_round_trips_all_eight_canonical_corners() {
1377 for dir_to in ALL_CORNERS {
1378 let d = canonical(dir_to);
1379 let printed = d.print_as_css_value();
1380 assert_eq!(printed, format!("to {dir_to}"));
1381 assert_eq!(
1382 parse_direction(&printed).unwrap(),
1383 d,
1384 "round-trip failed for {printed:?}"
1385 );
1386 }
1387 }
1388
1389 #[cfg(feature = "parser")]
1390 #[test]
1391 fn parse_direction_round_trips_angles_of_every_metric() {
1392 for a in [
1395 AngleValue::deg(45.0),
1396 AngleValue::deg(-90.0),
1397 AngleValue::deg(0.0),
1398 AngleValue::deg(359.999),
1399 AngleValue::rad(1.5),
1400 AngleValue::rad(-3.125),
1401 AngleValue::grad(100.0),
1402 AngleValue::turn(-0.25),
1403 AngleValue::turn(2.0),
1404 AngleValue::percent(50.0),
1405 ] {
1406 let d = Direction::Angle(a);
1407 let printed = d.print_as_css_value();
1408 assert_eq!(
1409 parse_direction(&printed).unwrap(),
1410 d,
1411 "round-trip failed for {printed:?}"
1412 );
1413 }
1414 }
1415
1416 #[cfg(feature = "parser")]
1417 #[test]
1418 fn parse_direction_round_trips_the_default() {
1419 let d = Direction::default();
1420 assert_eq!(parse_direction(&d.print_as_css_value()).unwrap(), d);
1421 }
1422
1423 #[cfg(feature = "parser")]
1424 #[test]
1425 fn print_as_css_value_is_lossy_for_non_canonical_fromto() {
1426 let weird = Direction::FromTo(DirectionCorners {
1429 dir_from: DirectionCorner::Top,
1430 dir_to: DirectionCorner::Right,
1431 });
1432 assert_eq!(weird.print_as_css_value(), "to right");
1433 let reparsed = parse_direction("to right").unwrap();
1434 assert_ne!(reparsed, weird, "dir_from unexpectedly survived the round-trip");
1435 assert_eq!(reparsed, canonical(DirectionCorner::Right));
1436 }
1437
1438 #[test]
1441 fn corner_error_to_contained_and_to_shared_round_trip() {
1442 let long = "x".repeat(100_000);
1443 for s in ["", " ", "bogus", "\u{1F600}\u{0301}", "\u{0}", long.as_str()] {
1444 let err = CssDirectionCornerParseError::InvalidDirection(s);
1445 let owned = err.to_contained();
1446 assert_eq!(owned.to_shared(), err, "round-trip failed for {s:?}");
1447 let CssDirectionCornerParseErrorOwned::InvalidDirection(payload) = &owned;
1449 assert_eq!(payload.as_str(), s);
1450 }
1451 }
1452
1453 #[test]
1454 fn direction_error_to_contained_and_to_shared_round_trip_all_variants() {
1455 let empty_float_err = "".parse::<f32>().unwrap_err();
1456 let invalid_float_err = "x".parse::<f32>().unwrap_err();
1457
1458 let errors = [
1459 CssDirectionParseError::Error("boom"),
1460 CssDirectionParseError::Error(""),
1461 CssDirectionParseError::InvalidArguments("to nowhere"),
1462 CssDirectionParseError::InvalidArguments("\u{1F600}"),
1463 CssDirectionParseError::ParseFloat(empty_float_err),
1464 CssDirectionParseError::ParseFloat(invalid_float_err),
1465 CssDirectionParseError::CornerError(CssDirectionCornerParseError::InvalidDirection(
1466 "nope",
1467 )),
1468 CssDirectionParseError::AngleError(CssAngleValueParseError::EmptyString),
1469 CssDirectionParseError::AngleError(CssAngleValueParseError::InvalidAngle("zzz")),
1470 ];
1471
1472 for err in errors {
1473 let owned = err.to_contained();
1474 assert_eq!(owned.to_shared(), err, "round-trip failed for {err:?}");
1475 assert_eq!(owned.to_shared().to_contained(), owned);
1477 }
1478 }
1479
1480 #[test]
1481 fn direction_error_from_impls_pick_the_right_variant() {
1482 let float_err: CssDirectionParseError<'_> = "x".parse::<f32>().unwrap_err().into();
1483 assert!(matches!(float_err, CssDirectionParseError::ParseFloat(_)));
1484
1485 let corner_err: CssDirectionParseError<'_> =
1486 CssDirectionCornerParseError::InvalidDirection("q").into();
1487 assert!(matches!(corner_err, CssDirectionParseError::CornerError(_)));
1488
1489 let angle_err: CssDirectionParseError<'_> = CssAngleValueParseError::EmptyString.into();
1490 assert!(matches!(angle_err, CssDirectionParseError::AngleError(_)));
1491 }
1492
1493 #[test]
1494 fn error_display_is_non_empty_and_keeps_the_offending_input() {
1495 let corner = CssDirectionCornerParseError::InvalidDirection("bogus");
1496 let printed = format!("{corner}");
1497 assert!(printed.contains("bogus"), "display lost the input: {printed}");
1498
1499 for err in [
1500 CssDirectionParseError::Error("boom"),
1501 CssDirectionParseError::InvalidArguments("to nowhere"),
1502 CssDirectionParseError::ParseFloat("x".parse::<f32>().unwrap_err()),
1503 CssDirectionParseError::CornerError(corner),
1504 CssDirectionParseError::AngleError(CssAngleValueParseError::EmptyString),
1505 ] {
1506 assert!(!format!("{err}").is_empty(), "empty display for {err:?}");
1507 }
1508
1509 for s in ["", "\u{1F600}", "\u{0}"] {
1511 let e = CssDirectionCornerParseError::InvalidDirection(s);
1512 let _ = format!("{e}");
1513 let _ = format!("{:?}", e.to_contained());
1514 }
1515 }
1516}