Skip to main content

azul_css/props/basic/
direction.rs

1//! CSS property types for direction (for gradients).
2
3use 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/// 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]
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/// A pair of corners representing the start and end of a CSS gradient direction.
129#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
130#[repr(C)]
131pub struct DirectionCorners {
132    /// The corner or side from which the gradient starts.
133    pub dir_from: DirectionCorner,
134    /// The corner or side at which the gradient ends.
135    pub dir_to: DirectionCorner,
136}
137
138/// CSS direction (necessary for gradients). Can either be a fixed angle or
139/// a direction ("to right" / "to left", etc.).
140#[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), // simplified "from X to Y"
161        }
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                // Convert the angle to start/end points on the rectangle.
171                // Normalize to [0, 360) so negative angles and angles >= 360 fall
172                // into the same quadrant branches below (rem_euclid is always >= 0).
173                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// -- Parser
208
209#[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")]
329/// # Errors
330///
331/// Returns an error if `input` is not a valid CSS `direction` value.
332pub 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    // Angles/coordinates are compared against exact literals that the code under
423    // test can reproduce bit-for-bit (or with an explicit ±1 pixel tolerance).
424    #![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    // ---- helpers -----------------------------------------------------------
432
433    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    /// Canonical direction: `to <corner>`, i.e. starting at the opposite corner.
467    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-evaluability of the `const fn`s -----------------------------
482
483    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        // to_point() is rect-local: the origin (7, 9) is not added.
495        assert_eq!(CONST_POINT, LayoutPoint::new(200, 50));
496    }
497
498    // ---- DirectionCorner: Display / PrintAsCssValue (serializer) ------------
499
500    #[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            // PrintAsCssValue must agree with Display.
516            assert_eq!(corner.print_as_css_value(), printed);
517            // Well-formed: non-empty, ASCII, lowercase, no stray whitespace.
518            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        // no_panic_default: Default::default() serializes.
534        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        // edge_values: NaN / infinite angles must still serialize without panic.
544        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            // The fixed-point encoding must never leak NaN/inf into the output.
558            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    // ---- DirectionCorner::opposite (getter) --------------------------------
564
565    #[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            // No fixed points: a corner is never its own opposite.
594            assert_ne!(c.opposite(), c, "{c} is its own opposite");
595            // Involution.
596            assert_eq!(c.opposite().opposite(), c, "opposite² != id for {c}");
597            // A side maps to a side, a diagonal maps to a diagonal.
598            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    // ---- DirectionCorner::combine (other) ----------------------------------
609
610    #[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                // Commutative.
618                assert_eq!(r, b.combine(&a), "combine({a}, {b}) is not commutative");
619
620                match r {
621                    Some(c) => {
622                        some_count += 1;
623                        // Only perpendicular side pairs may combine, and the
624                        // result is always a diagonal.
625                        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                        // The corner name must mention both inputs.
633                        let name = format!("{c}");
634                        assert!(name.contains(&format!("{a}")) && name.contains(&format!("{b}")));
635                        // Combining the opposites yields the opposite corner.
636                        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        // Exactly the 4 corners × 2 orderings.
651        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        // A diagonal never combines with anything.
663        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    // ---- DirectionCorner::to_point (numeric) -------------------------------
671
672    #[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        // to_point() works in rect-local space: a far-away (even negative)
714        // origin must not shift the result.
715        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        // p(c) + p(opposite(c)) == (width, height) for even extents.
731        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        // Rust integer division truncates toward zero: 3/2 == 1, -3/2 == -1.
745        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        // Halving isize::MIN / isize::MAX is the only arithmetic here; neither
764        // can overflow (the divisor is a constant 2), so no debug-panic.
765        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        // Mixed extremes: every corner stays inside the rect's own bounds.
790        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    // ---- Direction::to_points (numeric) ------------------------------------
799
800    #[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        // CSS: 0deg == "to top", so the gradient starts at the bottom edge.
817        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        // CSS: 180deg == "to bottom".
826        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        // Both endpoints sit on the horizontal midline, at the two extremes.
837        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        // start = center - d, end = center + d (mod rounding), for *every* angle
848        // and metric — so the endpoints must always straddle the center.
849        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            // The half-length is the corner projected onto the gradient line, so
878            // it can never exceed the center-to-corner distance (~111.8 here).
879            // (The endpoints themselves may fall outside a non-square box.)
880            // |start - end| == 2 * L <= 2 * hypot(100, 50) == 223.6 (+ rounding).
881            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        // width_half == height_half == 0 => atan(0/0) == NaN propagates through
892        // the whole computation; the f32 -> isize cast saturates NaN to 0, so
893        // the result must be (0,0)/(0,0) rather than a panic or garbage.
894        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        // Zero width => height/width == +-inf => atan(inf) == 90deg. Must not panic.
910        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        // FloatValue::new(NaN) -> f32_to_isize(NaN) == 0, so a NaN angle is
926        // silently the same value as 0deg. Assert the collapse (no NaN escapes).
927        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(&deg), "{v} normalized to {deg}");
946
947            let r = rect(200, 100);
948            let first = Direction::Angle(a).to_points(&r);
949            // Deterministic (no NaN-dependent branch flapping).
950            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                // The f32 round-trip saturates instead of panicking; only assert
971                // that the computation terminates and is deterministic.
972                assert_eq!(d.to_points(&r), d.to_points(&r), "{w}x{h} @ {a}");
973            }
974            // The FromTo path on extreme rects is exact.
975            let d = canonical(DirectionCorner::BottomRight);
976            assert_eq!(d.to_points(&r).1, LayoutPoint::new(w, h));
977        }
978    }
979
980    // ---- parse_direction_corner (parser, private) ---------------------------
981
982    #[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        // parse_direction_corner does NOT trim and is case-sensitive; every one
998        // of these must be a clean Err carrying the input verbatim.
999        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", // combining acute on 'i'
1042            "\u{0440}ight",  // Cyrillic 'р'
1043            "\u{200b}right", // zero-width space (not Rust whitespace)
1044            "right\u{200b}",
1045            "\u{feff}right", // BOM
1046            "\u{202e}right", // RTL override
1047            "right",    // fullwidth
1048            "\u{a0}right",   // NBSP (Rust whitespace, but not trimmed here)
1049        ] {
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        // Deeply "nested" input: no recursion in the matcher, so no stack blowup.
1089        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    // ---- parse_direction (parser) -------------------------------------------
1107
1108    #[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        // A bare number is degrees.
1132        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            // Invariant: a parsed FromTo always starts at the opposite corner.
1171            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        // Missing "to" keyword.
1195        assert!(matches!(
1196            parse_direction("right"),
1197            Err(CssDirectionParseError::InvalidArguments(_))
1198        ));
1199        // "to" with no corner.
1200        assert!(matches!(
1201            parse_direction("to"),
1202            Err(CssDirectionParseError::InvalidArguments(_))
1203        ));
1204        // Too many components (checked before the corners are parsed).
1205        assert!(matches!(
1206            parse_direction("to top right bottom"),
1207            Err(CssDirectionParseError::InvalidArguments(_))
1208        ));
1209        // Unknown corner.
1210        assert!(matches!(
1211            parse_direction("to center"),
1212            Err(CssDirectionParseError::CornerError(
1213                CssDirectionCornerParseError::InvalidDirection("center")
1214            ))
1215        ));
1216        // Non-combinable corner pairs.
1217        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        // NOTE: CSS keywords are case-insensitive; this parser is not. Asserting
1239        // the current (stricter) behavior so a future relaxation is a visible change.
1240        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        // Leniency: once the first token parses as an angle, the rest of the
1266        // input is dropped on the floor instead of being rejected.
1267        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}", // degree sign, not a CSS unit
1282            "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            // Must never panic; whatever comes back must be a well-formed value.
1296            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        // U+00A0 is Unicode White_Space (so `split_whitespace` splits on it) but
1312        // is NOT CSS whitespace. Accept either outcome; just pin down that the
1313        // result can't be some third thing.
1314        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", // ~f32::MAX
1332            "1e400",                                      // parses to f32 inf
1333            "-1e400deg",
1334            "1e-400deg", // underflows to 0
1335            "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(&deg),
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                    // And it must survive geometry without producing garbage.
1355                    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        // "NaN" is a valid f32 literal, so the angle path accepts it; the
1369        // fixed-point encoding then clamps it to 0. No NaN may escape.
1370        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        // 1M bytes of garbage: rejected without hanging.
1378        let garbage = "x".repeat(1_000_000);
1379        assert!(parse_direction(&garbage).is_err());
1380
1381        // 1M bytes of whitespace: no token at all.
1382        let blank = " ".repeat(1_000_000);
1383        assert!(matches!(
1384            parse_direction(&blank),
1385            Err(CssDirectionParseError::Error(_))
1386        ));
1387
1388        // A huge component list must be rejected (len > 2), not truncated.
1389        let many = format!("to {}", "top ".repeat(50_000));
1390        assert!(matches!(
1391            parse_direction(&many),
1392            Err(CssDirectionParseError::InvalidArguments(_))
1393        ));
1394
1395        // A 100k-digit number: overflows f32 to inf, which the fixed-point
1396        // encoding saturates -- must still be finite downstream.
1397        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        // Deeply nested brackets: no recursion, so no stack overflow.
1403        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        // Values are exact multiples of the 1/1000 fixed-point step, so the
1426        // encode -> print -> parse cycle must be lossless.
1427        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        // print_as_css_value() only encodes dir_to, so a FromTo whose dir_from is
1460        // not the opposite of dir_to cannot survive a round-trip. Pin the loss.
1461        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    // ---- error conversions (getters) ---------------------------------------
1475
1476    #[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            // The payload is preserved byte-for-byte.
1491            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            // to_contained() must be idempotent through the shared form.
1519            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        // Unicode / empty payloads must not panic the formatter.
1556        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}