1
//! CSS property types for direction (for gradients).
2

            
3
use alloc::string::String;
4
use core::{fmt, num::ParseFloatError};
5
use crate::corety::AzString;
6

            
7
use 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)]
21
pub enum DirectionCorner {
22
    Right,
23
    Left,
24
    Top,
25
    Bottom,
26
    TopRight,
27
    TopLeft,
28
    BottomRight,
29
    BottomLeft,
30
}
31

            
32
impl fmt::Display for DirectionCorner {
33
73
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34
73
        write!(
35
73
            f,
36
73
            "{}",
37
73
            match self {
38
12
                Self::Right => "right",
39
9
                Self::Left => "left",
40
9
                Self::Top => "top",
41
15
                Self::Bottom => "bottom",
42
7
                Self::TopRight => "top right",
43
7
                Self::TopLeft => "top left",
44
7
                Self::BottomRight => "bottom right",
45
7
                Self::BottomLeft => "bottom left",
46
            }
47
        )
48
73
    }
49
}
50

            
51
impl PrintAsCssValue for DirectionCorner {
52
8
    fn print_as_css_value(&self) -> String {
53
8
        format!("{self}")
54
8
    }
55
}
56

            
57
impl DirectionCorner {
58
261
    #[must_use] pub const fn opposite(&self) -> Self {
59
        use self::DirectionCorner::{Right, Left, Top, Bottom, TopRight, BottomLeft, TopLeft, BottomRight};
60
261
        match *self {
61
84
            Right => Left,
62
24
            Left => Right,
63
23
            Top => Bottom,
64
38
            Bottom => Top,
65
22
            TopRight => BottomLeft,
66
21
            BottomLeft => TopRight,
67
23
            TopLeft => BottomRight,
68
26
            BottomRight => TopLeft,
69
        }
70
261
    }
71

            
72
208
    #[must_use] pub const fn combine(&self, other: &Self) -> Option<Self> {
73
        use self::DirectionCorner::{Right, Top, TopRight, Left, TopLeft, Bottom, BottomRight, BottomLeft};
74
208
        match (*self, *other) {
75
10
            (Right, Top) | (Top, Right) => Some(TopRight),
76
11
            (Left, Top) | (Top, Left) => Some(TopLeft),
77
9
            (Right, Bottom) | (Bottom, Right) => Some(BottomRight),
78
9
            (Left, Bottom) | (Bottom, Left) => Some(BottomLeft),
79
169
            _ => None,
80
        }
81
208
    }
82

            
83
484
    #[must_use] pub const fn to_point(&self, rect: &LayoutRect) -> LayoutPoint {
84
        use self::DirectionCorner::{Right, Left, Top, Bottom, TopRight, TopLeft, BottomRight, BottomLeft};
85
484
        match *self {
86
65
            Right => LayoutPoint {
87
65
                x: rect.size.width,
88
65
                y: rect.size.height / 2,
89
65
            },
90
61
            Left => LayoutPoint {
91
61
                x: 0,
92
61
                y: rect.size.height / 2,
93
61
            },
94
76
            Top => LayoutPoint {
95
76
                x: rect.size.width / 2,
96
76
                y: 0,
97
76
            },
98
74
            Bottom => LayoutPoint {
99
74
                x: rect.size.width / 2,
100
74
                y: rect.size.height,
101
74
            },
102
49
            TopRight => LayoutPoint {
103
49
                x: rect.size.width,
104
49
                y: 0,
105
49
            },
106
54
            TopLeft => LayoutPoint { x: 0, y: 0 },
107
55
            BottomRight => LayoutPoint {
108
55
                x: rect.size.width,
109
55
                y: rect.size.height,
110
55
            },
111
50
            BottomLeft => LayoutPoint {
112
50
                x: 0,
113
50
                y: rect.size.height,
114
50
            },
115
        }
116
484
    }
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)]
122
pub 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)]
133
pub enum Direction {
134
    Angle(AngleValue),
135
    FromTo(DirectionCorners),
136
}
137

            
138
impl Default for Direction {
139
524
    fn default() -> Self {
140
524
        Self::FromTo(DirectionCorners {
141
524
            dir_from: DirectionCorner::Top,
142
524
            dir_to: DirectionCorner::Bottom,
143
524
        })
144
524
    }
145
}
146

            
147
impl PrintAsCssValue for Direction {
148
37
    fn print_as_css_value(&self) -> String {
149
37
        match self {
150
20
            Self::Angle(a) => format!("{a}"),
151
17
            Self::FromTo(d) => format!("to {}", d.dir_to), // simplified "from X to Y"
152
        }
153
37
    }
154
}
155

            
156
impl Direction {
157
277
    #[must_use] pub fn to_points(&self, rect: &LayoutRect) -> (LayoutPoint, LayoutPoint) {
158
277
        match self {
159
172
            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
172
                let deg = (-angle_value.to_degrees()).rem_euclid(360.0);
164
172
                let width_half = crate::cast::isize_to_f32(rect.size.width) / 2.0;
165
172
                let height_half = crate::cast::isize_to_f32(rect.size.height) / 2.0;
166
172
                let hypotenuse_len = libm::hypotf(width_half, height_half);
167
172
                let angle_to_corner = libm::atanf(height_half / width_half).to_degrees();
168
172
                let corner_angle = if deg < 90.0 {
169
51
                    90.0 - angle_to_corner
170
121
                } else if deg < 180.0 {
171
47
                    90.0 + angle_to_corner
172
74
                } else if deg < 270.0 {
173
34
                    270.0 - angle_to_corner
174
                } else {
175
40
                    270.0 + angle_to_corner
176
                };
177
172
                let angle_diff = corner_angle - deg;
178
172
                let line_length = libm::fabsf(hypotenuse_len * libm::cosf(angle_diff.to_radians()));
179
172
                let dx = libm::sinf(deg.to_radians()) * line_length;
180
172
                let dy = libm::cosf(deg.to_radians()) * line_length;
181
172
                (
182
172
                    LayoutPoint::new(
183
172
                        crate::cast::f32_to_isize(libm::roundf(width_half - dx)),
184
172
                        crate::cast::f32_to_isize(libm::roundf(height_half + dy)),
185
172
                    ),
186
172
                    LayoutPoint::new(
187
172
                        crate::cast::f32_to_isize(libm::roundf(width_half + dx)),
188
172
                        crate::cast::f32_to_isize(libm::roundf(height_half - dy)),
189
172
                    ),
190
172
                )
191
            }
192
105
            Self::FromTo(ft) => (ft.dir_from.to_point(rect), ft.dir_to.to_point(rect)),
193
        }
194
277
    }
195
}
196

            
197
// -- Parser
198

            
199
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
200
pub enum CssDirectionCornerParseError<'a> {
201
    InvalidDirection(&'a str),
202
}
203

            
204
impl_display! { CssDirectionCornerParseError<'a>, {
205
    InvalidDirection(val) => format!("Invalid direction: \"{}\"", val),
206
}}
207

            
208
#[derive(Debug, Clone, PartialEq, Eq)]
209
#[repr(C, u8)]
210
pub enum CssDirectionCornerParseErrorOwned {
211
    InvalidDirection(AzString),
212
}
213

            
214
impl CssDirectionCornerParseError<'_> {
215
15
    #[must_use] pub fn to_contained(&self) -> CssDirectionCornerParseErrorOwned {
216
15
        match self {
217
15
            CssDirectionCornerParseError::InvalidDirection(s) => {
218
15
                CssDirectionCornerParseErrorOwned::InvalidDirection((*s).to_string().into())
219
            }
220
        }
221
15
    }
222
}
223

            
224
impl CssDirectionCornerParseErrorOwned {
225
12
    #[must_use] pub fn to_shared(&self) -> CssDirectionCornerParseError<'_> {
226
12
        match self {
227
12
            Self::InvalidDirection(s) => {
228
12
                CssDirectionCornerParseError::InvalidDirection(s.as_str())
229
            }
230
        }
231
12
    }
232
}
233

            
234
#[derive(Debug, Clone, PartialEq, Eq)]
235
pub enum CssDirectionParseError<'a> {
236
    Error(&'a str),
237
    InvalidArguments(&'a str),
238
    ParseFloat(ParseFloatError),
239
    CornerError(CssDirectionCornerParseError<'a>),
240
    AngleError(CssAngleValueParseError<'a>),
241
}
242

            
243
impl_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

            
251
impl From<ParseFloatError> for CssDirectionParseError<'_> {
252
1
    fn from(e: ParseFloatError) -> Self {
253
1
        CssDirectionParseError::ParseFloat(e)
254
1
    }
255
}
256
impl_from! { CssDirectionCornerParseError<'a>, CssDirectionParseError::CornerError }
257
impl_from! { CssAngleValueParseError<'a>, CssDirectionParseError::AngleError }
258

            
259
#[derive(Debug, Clone, PartialEq, Eq)]
260
#[repr(C, u8)]
261
pub enum CssDirectionParseErrorOwned {
262
    Error(AzString),
263
    InvalidArguments(AzString),
264
    ParseFloat(crate::props::basic::error::ParseFloatError),
265
    CornerError(CssDirectionCornerParseErrorOwned),
266
    AngleError(CssAngleValueParseErrorOwned),
267
}
268

            
269
impl CssDirectionParseError<'_> {
270
22
    #[must_use] pub fn to_contained(&self) -> CssDirectionParseErrorOwned {
271
22
        match self {
272
5
            CssDirectionParseError::Error(s) => CssDirectionParseErrorOwned::Error((*s).to_string().into()),
273
7
            CssDirectionParseError::InvalidArguments(s) => {
274
7
                CssDirectionParseErrorOwned::InvalidArguments((*s).to_string().into())
275
            }
276
4
            CssDirectionParseError::ParseFloat(e) => {
277
4
                CssDirectionParseErrorOwned::ParseFloat(e.clone().into())
278
            }
279
2
            CssDirectionParseError::CornerError(e) => {
280
2
                CssDirectionParseErrorOwned::CornerError(e.to_contained())
281
            }
282
4
            CssDirectionParseError::AngleError(e) => {
283
4
                CssDirectionParseErrorOwned::AngleError(e.to_contained())
284
            }
285
        }
286
22
    }
287
}
288

            
289
impl CssDirectionParseErrorOwned {
290
23
    #[must_use] pub fn to_shared(&self) -> CssDirectionParseError<'_> {
291
23
        match self {
292
6
            Self::Error(s) => CssDirectionParseError::Error(s.as_str()),
293
7
            Self::InvalidArguments(s) => {
294
7
                CssDirectionParseError::InvalidArguments(s.as_str())
295
            }
296
4
            Self::ParseFloat(e) => {
297
4
                CssDirectionParseError::ParseFloat(e.to_std())
298
            }
299
2
            Self::CornerError(e) => {
300
2
                CssDirectionParseError::CornerError(e.to_shared())
301
            }
302
4
            Self::AngleError(e) => {
303
4
                CssDirectionParseError::AngleError(e.to_shared())
304
            }
305
        }
306
23
    }
307
}
308

            
309
#[cfg(feature = "parser")]
310
191
fn parse_direction_corner(
311
191
    input: &str,
312
191
) -> Result<DirectionCorner, CssDirectionCornerParseError<'_>> {
313
191
    match input {
314
191
        "right" => Ok(DirectionCorner::Right),
315
121
        "left" => Ok(DirectionCorner::Left),
316
105
        "top" => Ok(DirectionCorner::Top),
317
87
        "bottom" => Ok(DirectionCorner::Bottom),
318
61
        _ => Err(CssDirectionCornerParseError::InvalidDirection(input)),
319
    }
320
191
}
321

            
322
#[cfg(feature = "parser")]
323
/// # Errors
324
///
325
/// Returns an error if `input` is not a valid CSS `direction` value.
326
471
pub fn parse_direction(input: &str) -> Result<Direction, CssDirectionParseError<'_>> {
327
471
    let mut input_iter = input.split_whitespace();
328
471
    let first_input = input_iter
329
471
        .next()
330
471
        .ok_or(CssDirectionParseError::Error(input))?;
331

            
332
445
    if let Ok(angle) = parse_angle_value(first_input) {
333
162
        return Ok(Direction::Angle(angle));
334
283
    }
335

            
336
283
    if first_input != "to" {
337
165
        return Err(CssDirectionParseError::InvalidArguments(input));
338
118
    }
339

            
340
118
    let components = input_iter.collect::<Vec<_>>();
341
118
    if components.is_empty() || components.len() > 2 {
342
5
        return Err(CssDirectionParseError::InvalidArguments(input));
343
113
    }
344

            
345
113
    let first_corner = parse_direction_corner(components[0])?;
346
104
    let end = if components.len() == 2 {
347
22
        let second_corner = parse_direction_corner(components[1])?;
348
22
        first_corner
349
22
            .combine(&second_corner)
350
22
            .ok_or(CssDirectionParseError::InvalidArguments(input))?
351
    } else {
352
82
        first_corner
353
    };
354

            
355
97
    Ok(Direction::FromTo(DirectionCorners {
356
97
        dir_from: end.opposite(),
357
97
        dir_to: end,
358
97
    }))
359
471
}
360

            
361
#[cfg(all(test, feature = "parser"))]
362
mod tests {
363
    use super::*;
364
    use crate::props::basic::angle::AngleValue;
365

            
366
    #[test]
367
1
    fn test_parse_direction_angle() {
368
1
        assert_eq!(
369
1
            parse_direction("45deg").unwrap(),
370
1
            Direction::Angle(AngleValue::deg(45.0))
371
        );
372
1
        assert_eq!(
373
1
            parse_direction("  -0.25turn  ").unwrap(),
374
1
            Direction::Angle(AngleValue::turn(-0.25))
375
        );
376
1
    }
377

            
378
    #[test]
379
1
    fn test_parse_direction_corners() {
380
1
        assert_eq!(
381
1
            parse_direction("to right").unwrap(),
382
            Direction::FromTo(DirectionCorners {
383
                dir_from: DirectionCorner::Left,
384
                dir_to: DirectionCorner::Right,
385
            })
386
        );
387
1
        assert_eq!(
388
1
            parse_direction("to top left").unwrap(),
389
            Direction::FromTo(DirectionCorners {
390
                dir_from: DirectionCorner::BottomRight,
391
                dir_to: DirectionCorner::TopLeft,
392
            })
393
        );
394
1
        assert_eq!(
395
1
            parse_direction("to left top").unwrap(),
396
            Direction::FromTo(DirectionCorners {
397
                dir_from: DirectionCorner::BottomRight,
398
                dir_to: DirectionCorner::TopLeft,
399
            })
400
        );
401
1
    }
402

            
403
    #[test]
404
1
    fn test_parse_direction_errors() {
405
1
        assert!(parse_direction("").is_err());
406
1
        assert!(parse_direction("to").is_err());
407
1
        assert!(parse_direction("right").is_err());
408
1
        assert!(parse_direction("to center").is_err());
409
1
        assert!(parse_direction("to top right bottom").is_err());
410
1
        assert!(parse_direction("to top top").is_err());
411
1
    }
412
}
413

            
414
#[cfg(test)]
415
mod 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

            
    #[cfg(feature = "parser")]
    #[test]
    fn parse_corner_garbage_and_unicode_do_not_panic() {
        for bad in [
            "!@#$%^&*()",
            "\u{0}",
            "\u{0}right",
            "right\u{0}",
            "\u{1F600}",
            "to \u{1F600}",
            "ri\u{0301}ght",   // combining acute on 'i'
            "\u{0440}ight",    // Cyrillic 'р'
            "\u{200b}right",   // zero-width space (not Rust whitespace)
            "right\u{200b}",
            "\u{feff}right",   // BOM
            "\u{202e}right",   // RTL override
            "right",       // fullwidth
            "\u{a0}right",     // NBSP (Rust whitespace, but not trimmed here)
        ] {
            assert_eq!(
                parse_direction_corner(bad),
                Err(CssDirectionCornerParseError::InvalidDirection(bad)),
                "unicode input {bad:?} was not rejected"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_corner_boundary_numbers_are_rejected() {
        for bad in [
            "0",
            "-0",
            "9223372036854775807",
            "-9223372036854775808",
            "1e400",
            "1e-400",
            "NaN",
            "inf",
            "-inf",
        ] {
            assert!(
                parse_direction_corner(bad).is_err(),
                "numeric input {bad:?} parsed as a corner"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_corner_extremely_long_input_is_rejected_quickly() {
        let long = "a".repeat(1_000_000);
        assert!(parse_direction_corner(&long).is_err());
        let repeated = "right".repeat(200_000);
        assert!(parse_direction_corner(&repeated).is_err());
        // Deeply "nested" input: no recursion in the matcher, so no stack blowup.
        let nested = "(".repeat(100_000);
        assert!(parse_direction_corner(&nested).is_err());
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_corner_error_round_trips_through_owned() {
        let long = "top".repeat(10_000);
        for bad in ["", "bogus", "\u{1F600}", long.as_str()] {
            let Err(err) = parse_direction_corner(bad) else {
                panic!("{bad:?} unexpectedly parsed");
            };
            let owned = err.to_contained();
            assert_eq!(owned.to_shared(), err);
        }
    }
    // ---- parse_direction (parser) -------------------------------------------
    #[cfg(feature = "parser")]
    #[test]
    fn parse_direction_empty_and_whitespace_only() {
        for empty in ["", " ", "   ", "\t", "\n", "\r\n", "\t \n \r", "\u{a0}", "\u{3000}"] {
            assert!(
                matches!(
                    parse_direction(empty),
                    Err(CssDirectionParseError::Error(_))
                ),
                "whitespace input {empty:?} did not yield Error"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_direction_valid_minimal_angles() {
        assert_eq!(
            parse_direction("45deg").unwrap(),
            Direction::Angle(AngleValue::deg(45.0))
        );
        // A bare number is degrees.
        assert_eq!(
            parse_direction("0").unwrap(),
            Direction::Angle(AngleValue::deg(0.0))
        );
        assert_eq!(
            parse_direction("1.5rad").unwrap(),
            Direction::Angle(AngleValue::rad(1.5))
        );
        assert_eq!(
            parse_direction("100grad").unwrap(),
            Direction::Angle(AngleValue::grad(100.0))
        );
        assert_eq!(
            parse_direction("50%").unwrap(),
            Direction::Angle(AngleValue::percent(50.0))
        );
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_direction_all_corner_spellings() {
        let cases = [
            ("to right", DirectionCorner::Right),
            ("to left", DirectionCorner::Left),
            ("to top", DirectionCorner::Top),
            ("to bottom", DirectionCorner::Bottom),
            ("to top right", DirectionCorner::TopRight),
            ("to right top", DirectionCorner::TopRight),
            ("to top left", DirectionCorner::TopLeft),
            ("to left top", DirectionCorner::TopLeft),
            ("to bottom right", DirectionCorner::BottomRight),
            ("to right bottom", DirectionCorner::BottomRight),
            ("to bottom left", DirectionCorner::BottomLeft),
            ("to left bottom", DirectionCorner::BottomLeft),
        ];
        for (input, dir_to) in cases {
            let parsed = parse_direction(input).unwrap();
            assert_eq!(parsed, canonical(dir_to), "input {input:?}");
            // Invariant: a parsed FromTo always starts at the opposite corner.
            let Direction::FromTo(ft) = parsed else {
                panic!("{input:?} did not parse to FromTo");
            };
            assert_eq!(ft.dir_from, ft.dir_to.opposite());
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_direction_surrounding_whitespace_is_ignored() {
        let expect = canonical(DirectionCorner::Right);
        for input in ["to right", "  to right  ", "\tto\nright\r", "to    right"] {
            assert_eq!(parse_direction(input).unwrap(), expect, "input {input:?}");
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_direction_error_classification() {
        assert!(matches!(
            parse_direction(""),
            Err(CssDirectionParseError::Error(_))
        ));
        // Missing "to" keyword.
        assert!(matches!(
            parse_direction("right"),
            Err(CssDirectionParseError::InvalidArguments(_))
        ));
        // "to" with no corner.
        assert!(matches!(
            parse_direction("to"),
            Err(CssDirectionParseError::InvalidArguments(_))
        ));
        // Too many components (checked before the corners are parsed).
        assert!(matches!(
            parse_direction("to top right bottom"),
            Err(CssDirectionParseError::InvalidArguments(_))
        ));
        // Unknown corner.
        assert!(matches!(
            parse_direction("to center"),
            Err(CssDirectionParseError::CornerError(
                CssDirectionCornerParseError::InvalidDirection("center")
            ))
        ));
        // Non-combinable corner pairs.
        for bad in [
            "to top top",
            "to top bottom",
            "to bottom top",
            "to left right",
            "to right left",
            "to left left",
        ] {
            assert!(
                matches!(
                    parse_direction(bad),
                    Err(CssDirectionParseError::InvalidArguments(_))
                ),
                "input {bad:?} was accepted or misclassified"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_direction_is_case_sensitive() {
        // NOTE: CSS keywords are case-insensitive; this parser is not. Asserting
        // the current (stricter) behavior so a future relaxation is a visible change.
        for bad in ["TO RIGHT", "To Right", "to RIGHT", "TO right", "45DEG"] {
            assert!(parse_direction(bad).is_err(), "{bad:?} was accepted");
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_direction_leading_trailing_junk_is_rejected() {
        for bad in [
            "45deg;",
            "45deg;garbage",
            "to right;",
            "to;right",
            "to right)",
            "(45deg)",
            "to right,",
            "-->45deg",
        ] {
            assert!(parse_direction(bad).is_err(), "{bad:?} was accepted");
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_direction_ignores_tokens_after_a_leading_angle() {
        // Leniency: once the first token parses as an angle, the rest of the
        // input is dropped on the floor instead of being rejected.
        let expect = Direction::Angle(AngleValue::deg(45.0));
        assert_eq!(parse_direction("45deg garbage").unwrap(), expect);
        assert_eq!(parse_direction("45deg to right").unwrap(), expect);
        assert_eq!(parse_direction("45deg 90deg").unwrap(), expect);
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_direction_garbage_and_unicode_do_not_panic() {
        for bad in [
            "!@#$%^&*()",
            "\u{0}",
            "\u{1F600}",
            "to \u{1F600}",
            "45\u{00b0}",   // degree sign, not a CSS unit
            "to right\u{200b}",
            "\u{feff}to right",
            "to right",
            "to\u{200b}right",
            "\u{202e}to right",
            "deg",
            "%",
            "-",
            "+",
            ".",
            "e",
            "todeg",
        ] {
            // Must never panic; whatever comes back must be a well-formed value.
            match parse_direction(bad) {
                Ok(Direction::Angle(a)) => {
                    assert!(a.to_degrees().is_finite(), "{bad:?} -> non-finite angle");
                }
                Ok(Direction::FromTo(ft)) => {
                    assert_eq!(ft.dir_from, ft.dir_to.opposite(), "{bad:?}");
                }
                Err(_) => {}
            }
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_direction_unicode_whitespace_separator_is_wellformed() {
        // U+00A0 is Unicode White_Space (so `split_whitespace` splits on it) but
        // is NOT CSS whitespace. Accept either outcome; just pin down that the
        // result can't be some third thing.
        if let Ok(d) = parse_direction("to\u{a0}right") {
            assert_eq!(d, canonical(DirectionCorner::Right));
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_direction_boundary_numbers_never_yield_nan_or_inf() {
        for input in [
            "0",
            "-0",
            "0deg",
            "-0deg",
            "360deg",
            "-360deg",
            "9223372036854775807",
            "-9223372036854775808deg",
            "340282350000000000000000000000000000000deg", // ~f32::MAX
            "1e400",       // parses to f32 inf
            "-1e400deg",
            "1e-400deg",   // underflows to 0
            "NaN",
            "nan deg",
            "inf",
            "-infinity",
            "0.0000001turn",
            "-99999999rad",
        ] {
            match parse_direction(input) {
                Ok(Direction::Angle(a)) => {
                    let deg = a.to_degrees();
                    assert!(deg.is_finite(), "{input:?} produced non-finite {deg}");
                    assert!(
                        (0.0..=360.0).contains(&deg),
                        "{input:?} normalized outside [0,360]: {deg}"
                    );
                    assert!(
                        a.to_degrees_raw().is_finite(),
                        "{input:?} produced non-finite raw degrees"
                    );
                    // And it must survive geometry without producing garbage.
                    let (s, e) = Direction::Angle(a).to_points(&rect(200, 100));
                    assert!((s.x + e.x - 200).abs() <= 1, "{input:?}: {s:?}/{e:?}");
                    assert!((s.y + e.y - 100).abs() <= 1, "{input:?}: {s:?}/{e:?}");
                }
                Ok(Direction::FromTo(_)) => panic!("{input:?} parsed as a corner direction"),
                Err(_) => {}
            }
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_direction_nan_string_silently_becomes_zero_degrees() {
        // "NaN" is a valid f32 literal, so the angle path accepts it; the
        // fixed-point encoding then clamps it to 0. No NaN may escape.
        let parsed = parse_direction("NaN").unwrap();
        assert_eq!(parsed, Direction::Angle(AngleValue::deg(0.0)));
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_direction_extremely_long_input_terminates() {
        // 1M bytes of garbage: rejected without hanging.
        let garbage = "x".repeat(1_000_000);
        assert!(parse_direction(&garbage).is_err());
        // 1M bytes of whitespace: no token at all.
        let blank = " ".repeat(1_000_000);
        assert!(matches!(
            parse_direction(&blank),
            Err(CssDirectionParseError::Error(_))
        ));
        // A huge component list must be rejected (len > 2), not truncated.
        let many = format!("to {}", "top ".repeat(50_000));
        assert!(matches!(
            parse_direction(&many),
            Err(CssDirectionParseError::InvalidArguments(_))
        ));
        // A 100k-digit number: overflows f32 to inf, which the fixed-point
        // encoding saturates -- must still be finite downstream.
        let huge_number = format!("{}deg", "1".repeat(100_000));
        if let Ok(Direction::Angle(a)) = parse_direction(&huge_number) {
            assert!(a.to_degrees().is_finite());
        }
        // Deeply nested brackets: no recursion, so no stack overflow.
        let nested = format!("to {}", "(".repeat(100_000));
        assert!(parse_direction(&nested).is_err());
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_direction_round_trips_all_eight_canonical_corners() {
        for dir_to in ALL_CORNERS {
            let d = canonical(dir_to);
            let printed = d.print_as_css_value();
            assert_eq!(printed, format!("to {dir_to}"));
            assert_eq!(
                parse_direction(&printed).unwrap(),
                d,
                "round-trip failed for {printed:?}"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_direction_round_trips_angles_of_every_metric() {
        // Values are exact multiples of the 1/1000 fixed-point step, so the
        // encode -> print -> parse cycle must be lossless.
        for a in [
            AngleValue::deg(45.0),
            AngleValue::deg(-90.0),
            AngleValue::deg(0.0),
            AngleValue::deg(359.999),
            AngleValue::rad(1.5),
            AngleValue::rad(-3.125),
            AngleValue::grad(100.0),
            AngleValue::turn(-0.25),
            AngleValue::turn(2.0),
            AngleValue::percent(50.0),
        ] {
            let d = Direction::Angle(a);
            let printed = d.print_as_css_value();
            assert_eq!(
                parse_direction(&printed).unwrap(),
                d,
                "round-trip failed for {printed:?}"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_direction_round_trips_the_default() {
        let d = Direction::default();
        assert_eq!(parse_direction(&d.print_as_css_value()).unwrap(), d);
    }
    #[cfg(feature = "parser")]
    #[test]
    fn print_as_css_value_is_lossy_for_non_canonical_fromto() {
        // print_as_css_value() only encodes dir_to, so a FromTo whose dir_from is
        // not the opposite of dir_to cannot survive a round-trip. Pin the loss.
        let weird = Direction::FromTo(DirectionCorners {
            dir_from: DirectionCorner::Top,
            dir_to: DirectionCorner::Right,
        });
        assert_eq!(weird.print_as_css_value(), "to right");
        let reparsed = parse_direction("to right").unwrap();
        assert_ne!(reparsed, weird, "dir_from unexpectedly survived the round-trip");
        assert_eq!(reparsed, canonical(DirectionCorner::Right));
    }
    // ---- error conversions (getters) ---------------------------------------
    #[test]
    fn corner_error_to_contained_and_to_shared_round_trip() {
        let long = "x".repeat(100_000);
        for s in ["", " ", "bogus", "\u{1F600}\u{0301}", "\u{0}", long.as_str()] {
            let err = CssDirectionCornerParseError::InvalidDirection(s);
            let owned = err.to_contained();
            assert_eq!(owned.to_shared(), err, "round-trip failed for {s:?}");
            // The payload is preserved byte-for-byte.
            let CssDirectionCornerParseErrorOwned::InvalidDirection(payload) = &owned;
            assert_eq!(payload.as_str(), s);
        }
    }
    #[test]
    fn direction_error_to_contained_and_to_shared_round_trip_all_variants() {
        let empty_float_err = "".parse::<f32>().unwrap_err();
        let invalid_float_err = "x".parse::<f32>().unwrap_err();
        let errors = [
            CssDirectionParseError::Error("boom"),
            CssDirectionParseError::Error(""),
            CssDirectionParseError::InvalidArguments("to nowhere"),
            CssDirectionParseError::InvalidArguments("\u{1F600}"),
            CssDirectionParseError::ParseFloat(empty_float_err),
            CssDirectionParseError::ParseFloat(invalid_float_err),
            CssDirectionParseError::CornerError(CssDirectionCornerParseError::InvalidDirection(
                "nope",
            )),
            CssDirectionParseError::AngleError(CssAngleValueParseError::EmptyString),
            CssDirectionParseError::AngleError(CssAngleValueParseError::InvalidAngle("zzz")),
        ];
        for err in errors {
            let owned = err.to_contained();
            assert_eq!(owned.to_shared(), err, "round-trip failed for {err:?}");
            // to_contained() must be idempotent through the shared form.
            assert_eq!(owned.to_shared().to_contained(), owned);
        }
    }
    #[test]
    fn direction_error_from_impls_pick_the_right_variant() {
        let float_err: CssDirectionParseError<'_> = "x".parse::<f32>().unwrap_err().into();
        assert!(matches!(float_err, CssDirectionParseError::ParseFloat(_)));
        let corner_err: CssDirectionParseError<'_> =
            CssDirectionCornerParseError::InvalidDirection("q").into();
        assert!(matches!(corner_err, CssDirectionParseError::CornerError(_)));
        let angle_err: CssDirectionParseError<'_> = CssAngleValueParseError::EmptyString.into();
        assert!(matches!(angle_err, CssDirectionParseError::AngleError(_)));
    }
    #[test]
    fn error_display_is_non_empty_and_keeps_the_offending_input() {
        let corner = CssDirectionCornerParseError::InvalidDirection("bogus");
        let printed = format!("{corner}");
        assert!(printed.contains("bogus"), "display lost the input: {printed}");
        for err in [
            CssDirectionParseError::Error("boom"),
            CssDirectionParseError::InvalidArguments("to nowhere"),
            CssDirectionParseError::ParseFloat("x".parse::<f32>().unwrap_err()),
            CssDirectionParseError::CornerError(corner),
            CssDirectionParseError::AngleError(CssAngleValueParseError::EmptyString),
        ] {
            assert!(!format!("{err}").is_empty(), "empty display for {err:?}");
        }
        // Unicode / empty payloads must not panic the formatter.
        for s in ["", "\u{1F600}", "\u{0}"] {
            let e = CssDirectionCornerParseError::InvalidDirection(s);
            let _ = format!("{e}");
            let _ = format!("{:?}", e.to_contained());
        }
    }
}