Skip to main content

azul_css/props/basic/
direction.rs

1//! CSS property types for direction (for gradients).
2
3use 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/// Corner or side of a rectangle, used to specify CSS gradient directions
18/// (e.g. `to top right`).
19#[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/// A pair of corners representing the start and end of a CSS gradient direction.
120#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
121#[repr(C)]
122pub struct DirectionCorners {
123    /// The corner or side from which the gradient starts.
124    pub dir_from: DirectionCorner,
125    /// The corner or side at which the gradient ends.
126    pub dir_to: DirectionCorner,
127}
128
129/// CSS direction (necessary for gradients). Can either be a fixed angle or
130/// a direction ("to right" / "to left", etc.).
131#[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), // simplified "from X to Y"
152        }
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                // Convert the angle to start/end points on the rectangle.
161                // Normalize to [0, 360) so negative angles and angles >= 360 fall
162                // into the same quadrant branches below (rem_euclid is always >= 0).
163                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// -- Parser
198
199#[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")]
323/// # Errors
324///
325/// Returns an error if `input` is not a valid CSS `direction` value.
326pub 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    // Angles/coordinates are compared against exact literals that the code under
417    // test can reproduce bit-for-bit (or with an explicit ±1 pixel tolerance).
418    #![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    // ---- helpers -----------------------------------------------------------
426
427    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    /// Canonical direction: `to <corner>`, i.e. starting at the opposite corner.
461    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-evaluability of the `const fn`s -----------------------------
476
477    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        // to_point() is rect-local: the origin (7, 9) is not added.
489        assert_eq!(CONST_POINT, LayoutPoint::new(200, 50));
490    }
491
492    // ---- DirectionCorner: Display / PrintAsCssValue (serializer) ------------
493
494    #[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            // PrintAsCssValue must agree with Display.
510            assert_eq!(corner.print_as_css_value(), printed);
511            // Well-formed: non-empty, ASCII, lowercase, no stray whitespace.
512            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        // no_panic_default: Default::default() serializes.
528        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        // edge_values: NaN / infinite angles must still serialize without panic.
538        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            // The fixed-point encoding must never leak NaN/inf into the output.
552            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    // ---- DirectionCorner::opposite (getter) --------------------------------
558
559    #[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            // No fixed points: a corner is never its own opposite.
588            assert_ne!(c.opposite(), c, "{c} is its own opposite");
589            // Involution.
590            assert_eq!(c.opposite().opposite(), c, "opposite² != id for {c}");
591            // A side maps to a side, a diagonal maps to a diagonal.
592            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    // ---- DirectionCorner::combine (other) ----------------------------------
603
604    #[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                // Commutative.
612                assert_eq!(r, b.combine(&a), "combine({a}, {b}) is not commutative");
613
614                match r {
615                    Some(c) => {
616                        some_count += 1;
617                        // Only perpendicular side pairs may combine, and the
618                        // result is always a diagonal.
619                        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                        // The corner name must mention both inputs.
624                        let name = format!("{c}");
625                        assert!(name.contains(&format!("{a}")) && name.contains(&format!("{b}")));
626                        // Combining the opposites yields the opposite corner.
627                        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        // Exactly the 4 corners × 2 orderings.
642        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        // A diagonal never combines with anything.
657        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    // ---- DirectionCorner::to_point (numeric) -------------------------------
665
666    #[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        // to_point() works in rect-local space: a far-away (even negative)
705        // origin must not shift the result.
706        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        // p(c) + p(opposite(c)) == (width, height) for even extents.
722        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        // Rust integer division truncates toward zero: 3/2 == 1, -3/2 == -1.
736        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        // Halving isize::MIN / isize::MAX is the only arithmetic here; neither
755        // can overflow (the divisor is a constant 2), so no debug-panic.
756        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        // Mixed extremes: every corner stays inside the rect's own bounds.
781        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    // ---- Direction::to_points (numeric) ------------------------------------
790
791    #[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        // CSS: 0deg == "to top", so the gradient starts at the bottom edge.
808        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        // CSS: 180deg == "to bottom".
817        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        // Both endpoints sit on the horizontal midline, at the two extremes.
828        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        // start = center - d, end = center + d (mod rounding), for *every* angle
839        // and metric — so the endpoints must always straddle the center.
840        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            // The half-length is the corner projected onto the gradient line, so
869            // it can never exceed the center-to-corner distance (~111.8 here).
870            // (The endpoints themselves may fall outside a non-square box.)
871            // |start - end| == 2 * L <= 2 * hypot(100, 50) == 223.6 (+ rounding).
872            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        // width_half == height_half == 0 => atan(0/0) == NaN propagates through
883        // the whole computation; the f32 -> isize cast saturates NaN to 0, so
884        // the result must be (0,0)/(0,0) rather than a panic or garbage.
885        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        // Zero width => height/width == +-inf => atan(inf) == 90deg. Must not panic.
901        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        // FloatValue::new(NaN) -> f32_to_isize(NaN) == 0, so a NaN angle is
917        // silently the same value as 0deg. Assert the collapse (no NaN escapes).
918        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(&deg), "{v} normalized to {deg}");
937
938            let r = rect(200, 100);
939            let first = Direction::Angle(a).to_points(&r);
940            // Deterministic (no NaN-dependent branch flapping).
941            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                // The f32 round-trip saturates instead of panicking; only assert
962                // that the computation terminates and is deterministic.
963                assert_eq!(d.to_points(&r), d.to_points(&r), "{w}x{h} @ {a}");
964            }
965            // The FromTo path on extreme rects is exact.
966            let d = canonical(DirectionCorner::BottomRight);
967            assert_eq!(d.to_points(&r).1, LayoutPoint::new(w, h));
968        }
969    }
970
971    // ---- parse_direction_corner (parser, private) ---------------------------
972
973    #[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        // parse_direction_corner does NOT trim and is case-sensitive; every one
986        // of these must be a clean Err carrying the input verbatim.
987        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",   // combining acute on 'i'
1011            "\u{0440}ight",    // Cyrillic 'р'
1012            "\u{200b}right",   // zero-width space (not Rust whitespace)
1013            "right\u{200b}",
1014            "\u{feff}right",   // BOM
1015            "\u{202e}right",   // RTL override
1016            "right",       // fullwidth
1017            "\u{a0}right",     // NBSP (Rust whitespace, but not trimmed here)
1018        ] {
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        // Deeply "nested" input: no recursion in the matcher, so no stack blowup.
1058        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    // ---- parse_direction (parser) -------------------------------------------
1076
1077    #[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        // A bare number is degrees.
1099        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            // Invariant: a parsed FromTo always starts at the opposite corner.
1138            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        // Missing "to" keyword.
1162        assert!(matches!(
1163            parse_direction("right"),
1164            Err(CssDirectionParseError::InvalidArguments(_))
1165        ));
1166        // "to" with no corner.
1167        assert!(matches!(
1168            parse_direction("to"),
1169            Err(CssDirectionParseError::InvalidArguments(_))
1170        ));
1171        // Too many components (checked before the corners are parsed).
1172        assert!(matches!(
1173            parse_direction("to top right bottom"),
1174            Err(CssDirectionParseError::InvalidArguments(_))
1175        ));
1176        // Unknown corner.
1177        assert!(matches!(
1178            parse_direction("to center"),
1179            Err(CssDirectionParseError::CornerError(
1180                CssDirectionCornerParseError::InvalidDirection("center")
1181            ))
1182        ));
1183        // Non-combinable corner pairs.
1184        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        // NOTE: CSS keywords are case-insensitive; this parser is not. Asserting
1206        // the current (stricter) behavior so a future relaxation is a visible change.
1207        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        // Leniency: once the first token parses as an angle, the rest of the
1233        // input is dropped on the floor instead of being rejected.
1234        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}",   // degree sign, not a CSS unit
1249            "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            // Must never panic; whatever comes back must be a well-formed value.
1263            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        // U+00A0 is Unicode White_Space (so `split_whitespace` splits on it) but
1279        // is NOT CSS whitespace. Accept either outcome; just pin down that the
1280        // result can't be some third thing.
1281        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", // ~f32::MAX
1299            "1e400",       // parses to f32 inf
1300            "-1e400deg",
1301            "1e-400deg",   // underflows to 0
1302            "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(&deg),
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                    // And it must survive geometry without producing garbage.
1322                    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        // "NaN" is a valid f32 literal, so the angle path accepts it; the
1336        // fixed-point encoding then clamps it to 0. No NaN may escape.
1337        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        // 1M bytes of garbage: rejected without hanging.
1345        let garbage = "x".repeat(1_000_000);
1346        assert!(parse_direction(&garbage).is_err());
1347
1348        // 1M bytes of whitespace: no token at all.
1349        let blank = " ".repeat(1_000_000);
1350        assert!(matches!(
1351            parse_direction(&blank),
1352            Err(CssDirectionParseError::Error(_))
1353        ));
1354
1355        // A huge component list must be rejected (len > 2), not truncated.
1356        let many = format!("to {}", "top ".repeat(50_000));
1357        assert!(matches!(
1358            parse_direction(&many),
1359            Err(CssDirectionParseError::InvalidArguments(_))
1360        ));
1361
1362        // A 100k-digit number: overflows f32 to inf, which the fixed-point
1363        // encoding saturates -- must still be finite downstream.
1364        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        // Deeply nested brackets: no recursion, so no stack overflow.
1370        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        // Values are exact multiples of the 1/1000 fixed-point step, so the
1393        // encode -> print -> parse cycle must be lossless.
1394        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        // print_as_css_value() only encodes dir_to, so a FromTo whose dir_from is
1427        // not the opposite of dir_to cannot survive a round-trip. Pin the loss.
1428        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    // ---- error conversions (getters) ---------------------------------------
1439
1440    #[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            // The payload is preserved byte-for-byte.
1448            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            // to_contained() must be idempotent through the shared form.
1476            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        // Unicode / empty payloads must not panic the formatter.
1510        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}