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