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

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

            
6
use crate::{
7
    corety::AzString,
8
    props::{
9
        basic::{
10
            angle::{
11
                parse_angle_value, AngleValue, CssAngleValueParseError,
12
                CssAngleValueParseErrorOwned,
13
            },
14
            geometry::{LayoutPoint, LayoutRect},
15
        },
16
        formatter::PrintAsCssValue,
17
    },
18
};
19

            
20
/// Corner or side of a rectangle, used to specify CSS gradient directions
21
/// (e.g. `to top right`).
22
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
23
#[repr(C)]
24
pub enum DirectionCorner {
25
    Right,
26
    Left,
27
    Top,
28
    Bottom,
29
    TopRight,
30
    TopLeft,
31
    BottomRight,
32
    BottomLeft,
33
}
34

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

            
54
impl PrintAsCssValue for DirectionCorner {
55
8
    fn print_as_css_value(&self) -> String {
56
8
        format!("{self}")
57
8
    }
58
}
59

            
60
impl DirectionCorner {
61
    #[must_use]
62
5972
    pub const fn opposite(&self) -> Self {
63
        use self::DirectionCorner::{
64
            Bottom, BottomLeft, BottomRight, Left, Right, Top, TopLeft, TopRight,
65
        };
66
5972
        match *self {
67
4429
            Right => Left,
68
24
            Left => Right,
69
23
            Top => Bottom,
70
1404
            Bottom => Top,
71
22
            TopRight => BottomLeft,
72
21
            BottomLeft => TopRight,
73
23
            TopLeft => BottomRight,
74
26
            BottomRight => TopLeft,
75
        }
76
5972
    }
77

            
78
    #[must_use]
79
208
    pub const fn combine(&self, other: &Self) -> Option<Self> {
80
        use self::DirectionCorner::{
81
            Bottom, BottomLeft, BottomRight, Left, Right, Top, TopLeft, TopRight,
82
        };
83
208
        match (*self, *other) {
84
10
            (Right, Top) | (Top, Right) => Some(TopRight),
85
11
            (Left, Top) | (Top, Left) => Some(TopLeft),
86
9
            (Right, Bottom) | (Bottom, Right) => Some(BottomRight),
87
9
            (Left, Bottom) | (Bottom, Left) => Some(BottomLeft),
88
169
            _ => None,
89
        }
90
208
    }
91

            
92
    #[must_use]
93
490
    pub const fn to_point(&self, rect: &LayoutRect) -> LayoutPoint {
94
        use self::DirectionCorner::{
95
            Bottom, BottomLeft, BottomRight, Left, Right, Top, TopLeft, TopRight,
96
        };
97
490
        match *self {
98
66
            Right => LayoutPoint {
99
66
                x: rect.size.width,
100
66
                y: rect.size.height / 2,
101
66
            },
102
62
            Left => LayoutPoint {
103
62
                x: 0,
104
62
                y: rect.size.height / 2,
105
62
            },
106
78
            Top => LayoutPoint {
107
78
                x: rect.size.width / 2,
108
78
                y: 0,
109
78
            },
110
76
            Bottom => LayoutPoint {
111
76
                x: rect.size.width / 2,
112
76
                y: rect.size.height,
113
76
            },
114
49
            TopRight => LayoutPoint {
115
49
                x: rect.size.width,
116
49
                y: 0,
117
49
            },
118
54
            TopLeft => LayoutPoint { x: 0, y: 0 },
119
55
            BottomRight => LayoutPoint {
120
55
                x: rect.size.width,
121
55
                y: rect.size.height,
122
55
            },
123
50
            BottomLeft => LayoutPoint {
124
50
                x: 0,
125
50
                y: rect.size.height,
126
50
            },
127
        }
128
490
    }
129
}
130

            
131
/// A pair of corners representing the start and end of a CSS gradient direction.
132
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
133
#[repr(C)]
134
pub struct DirectionCorners {
135
    /// The corner or side from which the gradient starts.
136
    pub dir_from: DirectionCorner,
137
    /// The corner or side at which the gradient ends.
138
    pub dir_to: DirectionCorner,
139
}
140

            
141
/// CSS direction (necessary for gradients). Can either be a fixed angle or
142
/// a direction ("to right" / "to left", etc.).
143
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
144
#[repr(C, u8)]
145
pub enum Direction {
146
    Angle(AngleValue),
147
    FromTo(DirectionCorners),
148
}
149

            
150
impl Default for Direction {
151
6256
    fn default() -> Self {
152
6256
        Self::FromTo(DirectionCorners {
153
6256
            dir_from: DirectionCorner::Top,
154
6256
            dir_to: DirectionCorner::Bottom,
155
6256
        })
156
6256
    }
157
}
158

            
159
impl PrintAsCssValue for Direction {
160
37
    fn print_as_css_value(&self) -> String {
161
37
        match self {
162
20
            Self::Angle(a) => format!("{a}"),
163
17
            Self::FromTo(d) => format!("to {}", d.dir_to), // simplified "from X to Y"
164
        }
165
37
    }
166
}
167

            
168
impl Direction {
169
    #[must_use]
170
280
    pub fn to_points(&self, rect: &LayoutRect) -> (LayoutPoint, LayoutPoint) {
171
280
        match self {
172
172
            Self::Angle(angle_value) => {
173
                // Convert the angle to start/end points on the rectangle.
174
                // Normalize to [0, 360) so negative angles and angles >= 360 fall
175
                // into the same quadrant branches below (rem_euclid is always >= 0).
176
172
                let deg = (-angle_value.to_degrees()).rem_euclid(360.0);
177
172
                let width_half = crate::cast::isize_to_f32(rect.size.width) / 2.0;
178
172
                let height_half = crate::cast::isize_to_f32(rect.size.height) / 2.0;
179
172
                let hypotenuse_len = libm::hypotf(width_half, height_half);
180
172
                let angle_to_corner = libm::atanf(height_half / width_half).to_degrees();
181
172
                let corner_angle = if deg < 90.0 {
182
51
                    90.0 - angle_to_corner
183
121
                } else if deg < 180.0 {
184
47
                    90.0 + angle_to_corner
185
74
                } else if deg < 270.0 {
186
34
                    270.0 - angle_to_corner
187
                } else {
188
40
                    270.0 + angle_to_corner
189
                };
190
172
                let angle_diff = corner_angle - deg;
191
172
                let line_length = libm::fabsf(hypotenuse_len * libm::cosf(angle_diff.to_radians()));
192
172
                let dx = libm::sinf(deg.to_radians()) * line_length;
193
172
                let dy = libm::cosf(deg.to_radians()) * line_length;
194
172
                (
195
172
                    LayoutPoint::new(
196
172
                        crate::cast::f32_to_isize(libm::roundf(width_half - dx)),
197
172
                        crate::cast::f32_to_isize(libm::roundf(height_half + dy)),
198
172
                    ),
199
172
                    LayoutPoint::new(
200
172
                        crate::cast::f32_to_isize(libm::roundf(width_half + dx)),
201
172
                        crate::cast::f32_to_isize(libm::roundf(height_half - dy)),
202
172
                    ),
203
172
                )
204
            }
205
108
            Self::FromTo(ft) => (ft.dir_from.to_point(rect), ft.dir_to.to_point(rect)),
206
        }
207
280
    }
208
}
209

            
210
// -- Parser
211

            
212
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
213
pub enum CssDirectionCornerParseError<'a> {
214
    InvalidDirection(&'a str),
215
}
216

            
217
impl_display! { CssDirectionCornerParseError<'a>, {
218
    InvalidDirection(val) => format!("Invalid direction: \"{}\"", val),
219
}}
220

            
221
#[derive(Debug, Clone, PartialEq, Eq)]
222
#[repr(C, u8)]
223
pub enum CssDirectionCornerParseErrorOwned {
224
    InvalidDirection(AzString),
225
}
226

            
227
impl CssDirectionCornerParseError<'_> {
228
    #[must_use]
229
15
    pub fn to_contained(&self) -> CssDirectionCornerParseErrorOwned {
230
15
        match self {
231
15
            CssDirectionCornerParseError::InvalidDirection(s) => {
232
15
                CssDirectionCornerParseErrorOwned::InvalidDirection((*s).to_string().into())
233
            }
234
        }
235
15
    }
236
}
237

            
238
impl CssDirectionCornerParseErrorOwned {
239
    #[must_use]
240
12
    pub fn to_shared(&self) -> CssDirectionCornerParseError<'_> {
241
12
        match self {
242
12
            Self::InvalidDirection(s) => CssDirectionCornerParseError::InvalidDirection(s.as_str()),
243
        }
244
12
    }
245
}
246

            
247
#[derive(Debug, Clone, PartialEq, Eq)]
248
pub enum CssDirectionParseError<'a> {
249
    Error(&'a str),
250
    InvalidArguments(&'a str),
251
    ParseFloat(ParseFloatError),
252
    CornerError(CssDirectionCornerParseError<'a>),
253
    AngleError(CssAngleValueParseError<'a>),
254
}
255

            
256
impl_display! {CssDirectionParseError<'a>, {
257
    Error(e) => e,
258
    InvalidArguments(val) => format!("Invalid arguments: \"{}\"", val),
259
    ParseFloat(e) => format!("Invalid value: {}", e),
260
    CornerError(e) => format!("Invalid corner value: {}", e),
261
    AngleError(e) => format!("Invalid angle value: {}", e),
262
}}
263

            
264
impl From<ParseFloatError> for CssDirectionParseError<'_> {
265
1
    fn from(e: ParseFloatError) -> Self {
266
1
        CssDirectionParseError::ParseFloat(e)
267
1
    }
268
}
269
impl_from! { CssDirectionCornerParseError<'a>, CssDirectionParseError::CornerError }
270
impl_from! { CssAngleValueParseError<'a>, CssDirectionParseError::AngleError }
271

            
272
#[derive(Debug, Clone, PartialEq, Eq)]
273
#[repr(C, u8)]
274
pub enum CssDirectionParseErrorOwned {
275
    Error(AzString),
276
    InvalidArguments(AzString),
277
    ParseFloat(crate::props::basic::error::ParseFloatError),
278
    CornerError(CssDirectionCornerParseErrorOwned),
279
    AngleError(CssAngleValueParseErrorOwned),
280
}
281

            
282
impl CssDirectionParseError<'_> {
283
    #[must_use]
284
22
    pub fn to_contained(&self) -> CssDirectionParseErrorOwned {
285
22
        match self {
286
5
            CssDirectionParseError::Error(s) => {
287
5
                CssDirectionParseErrorOwned::Error((*s).to_string().into())
288
            }
289
7
            CssDirectionParseError::InvalidArguments(s) => {
290
7
                CssDirectionParseErrorOwned::InvalidArguments((*s).to_string().into())
291
            }
292
4
            CssDirectionParseError::ParseFloat(e) => {
293
4
                CssDirectionParseErrorOwned::ParseFloat(e.clone().into())
294
            }
295
2
            CssDirectionParseError::CornerError(e) => {
296
2
                CssDirectionParseErrorOwned::CornerError(e.to_contained())
297
            }
298
4
            CssDirectionParseError::AngleError(e) => {
299
4
                CssDirectionParseErrorOwned::AngleError(e.to_contained())
300
            }
301
        }
302
22
    }
303
}
304

            
305
impl CssDirectionParseErrorOwned {
306
    #[must_use]
307
23
    pub fn to_shared(&self) -> CssDirectionParseError<'_> {
308
23
        match self {
309
6
            Self::Error(s) => CssDirectionParseError::Error(s.as_str()),
310
7
            Self::InvalidArguments(s) => CssDirectionParseError::InvalidArguments(s.as_str()),
311
4
            Self::ParseFloat(e) => CssDirectionParseError::ParseFloat(e.to_std()),
312
2
            Self::CornerError(e) => CssDirectionParseError::CornerError(e.to_shared()),
313
4
            Self::AngleError(e) => CssDirectionParseError::AngleError(e.to_shared()),
314
        }
315
23
    }
316
}
317

            
318
#[cfg(feature = "parser")]
319
5902
fn parse_direction_corner(
320
5902
    input: &str,
321
5902
) -> Result<DirectionCorner, CssDirectionCornerParseError<'_>> {
322
5902
    match input {
323
5902
        "right" => Ok(DirectionCorner::Right),
324
1487
        "left" => Ok(DirectionCorner::Left),
325
1471
        "top" => Ok(DirectionCorner::Top),
326
1453
        "bottom" => Ok(DirectionCorner::Bottom),
327
61
        _ => Err(CssDirectionCornerParseError::InvalidDirection(input)),
328
    }
329
5902
}
330

            
331
#[cfg(feature = "parser")]
332
/// # Errors
333
///
334
/// Returns an error if `input` is not a valid CSS `direction` value.
335
7678
pub fn parse_direction(input: &str) -> Result<Direction, CssDirectionParseError<'_>> {
336
7678
    let mut input_iter = input.split_whitespace();
337
7678
    let first_input = input_iter
338
7678
        .next()
339
7678
        .ok_or(CssDirectionParseError::Error(input))?;
340

            
341
7652
    if let Ok(angle) = parse_angle_value(first_input) {
342
1658
        return Ok(Direction::Angle(angle));
343
5994
    }
344

            
345
5994
    if first_input != "to" {
346
165
        return Err(CssDirectionParseError::InvalidArguments(input));
347
5829
    }
348

            
349
5829
    let components = input_iter.collect::<Vec<_>>();
350
5829
    if components.is_empty() || components.len() > 2 {
351
5
        return Err(CssDirectionParseError::InvalidArguments(input));
352
5824
    }
353

            
354
5824
    let first_corner = parse_direction_corner(components[0])?;
355
5815
    let end = if components.len() == 2 {
356
22
        let second_corner = parse_direction_corner(components[1])?;
357
22
        first_corner
358
22
            .combine(&second_corner)
359
22
            .ok_or(CssDirectionParseError::InvalidArguments(input))?
360
    } else {
361
5793
        first_corner
362
    };
363

            
364
5808
    Ok(Direction::FromTo(DirectionCorners {
365
5808
        dir_from: end.opposite(),
366
5808
        dir_to: end,
367
5808
    }))
368
7678
}
369

            
370
#[cfg(all(test, feature = "parser"))]
371
mod tests {
372
    use super::*;
373
    use crate::props::basic::angle::AngleValue;
374

            
375
    #[test]
376
1
    fn test_parse_direction_angle() {
377
1
        assert_eq!(
378
1
            parse_direction("45deg").unwrap(),
379
1
            Direction::Angle(AngleValue::deg(45.0))
380
        );
381
1
        assert_eq!(
382
1
            parse_direction("  -0.25turn  ").unwrap(),
383
1
            Direction::Angle(AngleValue::turn(-0.25))
384
        );
385
1
    }
386

            
387
    #[test]
388
1
    fn test_parse_direction_corners() {
389
1
        assert_eq!(
390
1
            parse_direction("to right").unwrap(),
391
            Direction::FromTo(DirectionCorners {
392
                dir_from: DirectionCorner::Left,
393
                dir_to: DirectionCorner::Right,
394
            })
395
        );
396
1
        assert_eq!(
397
1
            parse_direction("to top left").unwrap(),
398
            Direction::FromTo(DirectionCorners {
399
                dir_from: DirectionCorner::BottomRight,
400
                dir_to: DirectionCorner::TopLeft,
401
            })
402
        );
403
1
        assert_eq!(
404
1
            parse_direction("to left top").unwrap(),
405
            Direction::FromTo(DirectionCorners {
406
                dir_from: DirectionCorner::BottomRight,
407
                dir_to: DirectionCorner::TopLeft,
408
            })
409
        );
410
1
    }
411

            
412
    #[test]
413
1
    fn test_parse_direction_errors() {
414
1
        assert!(parse_direction("").is_err());
415
1
        assert!(parse_direction("to").is_err());
416
1
        assert!(parse_direction("right").is_err());
417
1
        assert!(parse_direction("to center").is_err());
418
1
        assert!(parse_direction("to top right bottom").is_err());
419
1
        assert!(parse_direction("to top top").is_err());
420
1
    }
421
}
422

            
423
#[cfg(test)]
424
mod autotest_generated {
425
    // Angles/coordinates are compared against exact literals that the code under
426
    // test can reproduce bit-for-bit (or with an explicit ±1 pixel tolerance).
427
    #![allow(clippy::float_cmp, clippy::too_many_lines)]
428

            
429
    use alloc::collections::BTreeSet;
430

            
431
    use super::*;
432
    use crate::props::basic::geometry::LayoutSize;
433

            
434
    // ---- helpers -----------------------------------------------------------
435

            
436
    const ALL_CORNERS: [DirectionCorner; 8] = [
437
        DirectionCorner::Right,
438
        DirectionCorner::Left,
439
        DirectionCorner::Top,
440
        DirectionCorner::Bottom,
441
        DirectionCorner::TopRight,
442
        DirectionCorner::TopLeft,
443
        DirectionCorner::BottomRight,
444
        DirectionCorner::BottomLeft,
445
    ];
446

            
447
    const SIDES: [DirectionCorner; 4] = [
448
        DirectionCorner::Right,
449
        DirectionCorner::Left,
450
        DirectionCorner::Top,
451
        DirectionCorner::Bottom,
452
    ];
453

            
454
    const DIAGONALS: [DirectionCorner; 4] = [
455
        DirectionCorner::TopRight,
456
        DirectionCorner::TopLeft,
457
        DirectionCorner::BottomRight,
458
        DirectionCorner::BottomLeft,
459
    ];
460

            
461
    fn rect(w: isize, h: isize) -> LayoutRect {
462
        LayoutRect::new(LayoutPoint::zero(), LayoutSize::new(w, h))
463
    }
464

            
465
    fn rect_at(x: isize, y: isize, w: isize, h: isize) -> LayoutRect {
466
        LayoutRect::new(LayoutPoint::new(x, y), LayoutSize::new(w, h))
467
    }
468

            
469
    /// Canonical direction: `to <corner>`, i.e. starting at the opposite corner.
470
    fn canonical(dir_to: DirectionCorner) -> Direction {
471
        Direction::FromTo(DirectionCorners {
472
            dir_from: dir_to.opposite(),
473
            dir_to,
474
        })
475
    }
476

            
477
    fn assert_near(actual: LayoutPoint, expected: LayoutPoint, tol: isize) {
478
        assert!(
479
            (actual.x - expected.x).abs() <= tol && (actual.y - expected.y).abs() <= tol,
480
            "expected {expected:?} (±{tol}), got {actual:?}"
481
        );
482
    }
483

            
484
    // ---- const-evaluability of the `const fn`s -----------------------------
485

            
486
    const CONST_RECT: LayoutRect =
487
        LayoutRect::new(LayoutPoint::new(7, 9), LayoutSize::new(200, 100));
488
    const CONST_OPPOSITE: DirectionCorner = DirectionCorner::TopRight.opposite();
489
    const CONST_COMBINED: Option<DirectionCorner> =
490
        DirectionCorner::Right.combine(&DirectionCorner::Top);
491
    const CONST_POINT: LayoutPoint = DirectionCorner::Right.to_point(&CONST_RECT);
492

            
493
    #[test]
494
    fn const_fns_evaluate_at_compile_time() {
495
        assert_eq!(CONST_OPPOSITE, DirectionCorner::BottomLeft);
496
        assert_eq!(CONST_COMBINED, Some(DirectionCorner::TopRight));
497
        // to_point() is rect-local: the origin (7, 9) is not added.
498
        assert_eq!(CONST_POINT, LayoutPoint::new(200, 50));
499
    }
500

            
501
    // ---- DirectionCorner: Display / PrintAsCssValue (serializer) ------------
502

            
503
    #[test]
504
    fn corner_display_exact_values_and_wellformed() {
505
        let expected = [
506
            (DirectionCorner::Right, "right"),
507
            (DirectionCorner::Left, "left"),
508
            (DirectionCorner::Top, "top"),
509
            (DirectionCorner::Bottom, "bottom"),
510
            (DirectionCorner::TopRight, "top right"),
511
            (DirectionCorner::TopLeft, "top left"),
512
            (DirectionCorner::BottomRight, "bottom right"),
513
            (DirectionCorner::BottomLeft, "bottom left"),
514
        ];
515
        for (corner, want) in expected {
516
            let printed = format!("{corner}");
517
            assert_eq!(printed, want);
518
            // PrintAsCssValue must agree with Display.
519
            assert_eq!(corner.print_as_css_value(), printed);
520
            // Well-formed: non-empty, ASCII, lowercase, no stray whitespace.
521
            assert!(!printed.is_empty());
522
            assert!(printed.is_ascii());
523
            assert_eq!(printed, printed.to_lowercase());
524
            assert_eq!(printed, printed.trim());
525
        }
526
    }
527

            
528
    #[test]
529
    fn corner_display_is_injective() {
530
        let printed: BTreeSet<String> = ALL_CORNERS.iter().map(|c| format!("{c}")).collect();
531
        assert_eq!(printed.len(), ALL_CORNERS.len());
532
    }
533

            
534
    #[test]
535
    fn direction_default_and_display_do_not_panic() {
536
        // no_panic_default: Default::default() serializes.
537
        assert_eq!(
538
            Direction::default(),
539
            Direction::FromTo(DirectionCorners {
540
                dir_from: DirectionCorner::Top,
541
                dir_to: DirectionCorner::Bottom,
542
            })
543
        );
544
        assert_eq!(Direction::default().print_as_css_value(), "to bottom");
545

            
546
        // edge_values: NaN / infinite angles must still serialize without panic.
547
        for v in [
548
            0.0_f32,
549
            -0.0,
550
            f32::NAN,
551
            f32::INFINITY,
552
            f32::NEG_INFINITY,
553
            f32::MAX,
554
            f32::MIN,
555
            f32::MIN_POSITIVE,
556
        ] {
557
            let s = Direction::Angle(AngleValue::deg(v)).print_as_css_value();
558
            assert!(!s.is_empty(), "empty serialization for {v}");
559
            assert!(s.ends_with("deg"), "unexpected serialization: {s}");
560
            // The fixed-point encoding must never leak NaN/inf into the output.
561
            assert!(!s.contains("NaN"), "NaN leaked into CSS output: {s}");
562
            assert!(!s.contains("inf"), "inf leaked into CSS output: {s}");
563
        }
564
    }
565

            
566
    // ---- DirectionCorner::opposite (getter) --------------------------------
567

            
568
    #[test]
569
    fn opposite_known_values() {
570
        assert_eq!(DirectionCorner::Right.opposite(), DirectionCorner::Left);
571
        assert_eq!(DirectionCorner::Left.opposite(), DirectionCorner::Right);
572
        assert_eq!(DirectionCorner::Top.opposite(), DirectionCorner::Bottom);
573
        assert_eq!(DirectionCorner::Bottom.opposite(), DirectionCorner::Top);
574
        assert_eq!(
575
            DirectionCorner::TopRight.opposite(),
576
            DirectionCorner::BottomLeft
577
        );
578
        assert_eq!(
579
            DirectionCorner::BottomLeft.opposite(),
580
            DirectionCorner::TopRight
581
        );
582
        assert_eq!(
583
            DirectionCorner::TopLeft.opposite(),
584
            DirectionCorner::BottomRight
585
        );
586
        assert_eq!(
587
            DirectionCorner::BottomRight.opposite(),
588
            DirectionCorner::TopLeft
589
        );
590
    }
591

            
592
    #[test]
593
    fn opposite_is_an_involution_and_a_bijection() {
594
        let mut images = BTreeSet::new();
595
        for c in ALL_CORNERS {
596
            // No fixed points: a corner is never its own opposite.
597
            assert_ne!(c.opposite(), c, "{c} is its own opposite");
598
            // Involution.
599
            assert_eq!(c.opposite().opposite(), c, "opposite² != id for {c}");
600
            // A side maps to a side, a diagonal maps to a diagonal.
601
            assert_eq!(
602
                SIDES.contains(&c),
603
                SIDES.contains(&c.opposite()),
604
                "{c} changed class under opposite()"
605
            );
606
            images.insert(c.opposite());
607
        }
608
        assert_eq!(images.len(), 8, "opposite() is not a bijection");
609
    }
610

            
611
    // ---- DirectionCorner::combine (other) ----------------------------------
612

            
613
    #[test]
614
    fn combine_exhaustive_over_all_64_pairs() {
615
        let mut some_count = 0_usize;
616
        for a in ALL_CORNERS {
617
            for b in ALL_CORNERS {
618
                let r = a.combine(&b);
619

            
620
                // Commutative.
621
                assert_eq!(r, b.combine(&a), "combine({a}, {b}) is not commutative");
622

            
623
                match r {
624
                    Some(c) => {
625
                        some_count += 1;
626
                        // Only perpendicular side pairs may combine, and the
627
                        // result is always a diagonal.
628
                        assert!(SIDES.contains(&a) && SIDES.contains(&b));
629
                        assert!(
630
                            DIAGONALS.contains(&c),
631
                            "combine({a}, {b}) = {c}, not a corner"
632
                        );
633
                        assert_ne!(a, b);
634
                        assert_ne!(a.opposite(), b, "opposite sides must not combine");
635
                        // The corner name must mention both inputs.
636
                        let name = format!("{c}");
637
                        assert!(name.contains(&format!("{a}")) && name.contains(&format!("{b}")));
638
                        // Combining the opposites yields the opposite corner.
639
                        assert_eq!(a.opposite().combine(&b.opposite()), Some(c.opposite()));
640
                    }
641
                    None => {
642
                        assert!(
643
                            !SIDES.contains(&a)
644
                                || !SIDES.contains(&b)
645
                                || a == b
646
                                || a.opposite() == b,
647
                            "combine({a}, {b}) unexpectedly returned None"
648
                        );
649
                    }
650
                }
651
            }
652
        }
653
        // Exactly the 4 corners × 2 orderings.
654
        assert_eq!(some_count, 8);
655
    }
656

            
657
    #[test]
658
    fn combine_degenerate_pairs_are_none() {
659
        for c in ALL_CORNERS {
660
            assert_eq!(c.combine(&c), None, "{c} combined with itself");
661
            assert_eq!(c.combine(&c.opposite()), None, "{c} combined with opposite");
662
        }
663
        assert_eq!(DirectionCorner::Top.combine(&DirectionCorner::Bottom), None);
664
        assert_eq!(DirectionCorner::Left.combine(&DirectionCorner::Right), None);
665
        // A diagonal never combines with anything.
666
        for d in DIAGONALS {
667
            for c in ALL_CORNERS {
668
                assert_eq!(d.combine(&c), None, "{d} combined with {c}");
669
            }
670
        }
671
    }
672

            
673
    // ---- DirectionCorner::to_point (numeric) -------------------------------
674

            
675
    #[test]
676
    fn to_point_zero_rect_is_origin() {
677
        let r = rect(0, 0);
678
        for c in ALL_CORNERS {
679
            assert_eq!(c.to_point(&r), LayoutPoint::zero(), "corner {c}");
680
        }
681
    }
682

            
683
    #[test]
684
    fn to_point_known_values() {
685
        let r = rect(200, 100);
686
        assert_eq!(
687
            DirectionCorner::Right.to_point(&r),
688
            LayoutPoint::new(200, 50)
689
        );
690
        assert_eq!(DirectionCorner::Left.to_point(&r), LayoutPoint::new(0, 50));
691
        assert_eq!(DirectionCorner::Top.to_point(&r), LayoutPoint::new(100, 0));
692
        assert_eq!(
693
            DirectionCorner::Bottom.to_point(&r),
694
            LayoutPoint::new(100, 100)
695
        );
696
        assert_eq!(
697
            DirectionCorner::TopRight.to_point(&r),
698
            LayoutPoint::new(200, 0)
699
        );
700
        assert_eq!(
701
            DirectionCorner::TopLeft.to_point(&r),
702
            LayoutPoint::new(0, 0)
703
        );
704
        assert_eq!(
705
            DirectionCorner::BottomRight.to_point(&r),
706
            LayoutPoint::new(200, 100)
707
        );
708
        assert_eq!(
709
            DirectionCorner::BottomLeft.to_point(&r),
710
            LayoutPoint::new(0, 100)
711
        );
712
    }
713

            
714
    #[test]
715
    fn to_point_ignores_rect_origin() {
716
        // to_point() works in rect-local space: a far-away (even negative)
717
        // origin must not shift the result.
718
        let local = rect(200, 100);
719
        for offset in [(0, 0), (1000, -500), (isize::MIN, isize::MAX)] {
720
            let moved = rect_at(offset.0, offset.1, 200, 100);
721
            for c in ALL_CORNERS {
722
                assert_eq!(
723
                    c.to_point(&moved),
724
                    c.to_point(&local),
725
                    "corner {c} shifted by origin {offset:?}"
726
                );
727
            }
728
        }
729
    }
730

            
731
    #[test]
732
    fn to_point_opposite_corners_sum_to_the_full_extent() {
733
        // p(c) + p(opposite(c)) == (width, height) for even extents.
734
        for (w, h) in [(200_isize, 100_isize), (2, 2), (0, 0), (-40, -60)] {
735
            let r = rect(w, h);
736
            for c in ALL_CORNERS {
737
                let p = c.to_point(&r);
738
                let q = c.opposite().to_point(&r);
739
                assert_eq!(p.x + q.x, w, "x-sum for {c} in {w}x{h}");
740
                assert_eq!(p.y + q.y, h, "y-sum for {c} in {w}x{h}");
741
            }
742
        }
743
    }
744

            
745
    #[test]
746
    fn to_point_odd_and_negative_extents_truncate_toward_zero() {
747
        // Rust integer division truncates toward zero: 3/2 == 1, -3/2 == -1.
748
        let r = rect(3, 3);
749
        assert_eq!(DirectionCorner::Top.to_point(&r), LayoutPoint::new(1, 0));
750
        assert_eq!(DirectionCorner::Right.to_point(&r), LayoutPoint::new(3, 1));
751

            
752
        let neg = rect(-3, -3);
753
        assert_eq!(DirectionCorner::Top.to_point(&neg), LayoutPoint::new(-1, 0));
754
        assert_eq!(
755
            DirectionCorner::Right.to_point(&neg),
756
            LayoutPoint::new(-3, -1)
757
        );
758
        assert_eq!(
759
            DirectionCorner::BottomLeft.to_point(&neg),
760
            LayoutPoint::new(0, -3)
761
        );
762
    }
763

            
764
    #[test]
765
    fn to_point_isize_extremes_do_not_overflow() {
766
        // Halving isize::MIN / isize::MAX is the only arithmetic here; neither
767
        // can overflow (the divisor is a constant 2), so no debug-panic.
768
        let max = rect(isize::MAX, isize::MAX);
769
        assert_eq!(
770
            DirectionCorner::Right.to_point(&max),
771
            LayoutPoint::new(isize::MAX, isize::MAX / 2)
772
        );
773
        assert_eq!(
774
            DirectionCorner::Bottom.to_point(&max),
775
            LayoutPoint::new(isize::MAX / 2, isize::MAX)
776
        );
777
        assert_eq!(
778
            DirectionCorner::BottomRight.to_point(&max),
779
            LayoutPoint::new(isize::MAX, isize::MAX)
780
        );
781

            
782
        let min = rect(isize::MIN, isize::MIN);
783
        assert_eq!(
784
            DirectionCorner::Right.to_point(&min),
785
            LayoutPoint::new(isize::MIN, isize::MIN / 2)
786
        );
787
        assert_eq!(
788
            DirectionCorner::Top.to_point(&min),
789
            LayoutPoint::new(isize::MIN / 2, 0)
790
        );
791

            
792
        // Mixed extremes: every corner stays inside the rect's own bounds.
793
        let mixed = rect(isize::MAX, isize::MIN);
794
        for c in ALL_CORNERS {
795
            let p = c.to_point(&mixed);
796
            assert!(p.x == 0 || p.x == isize::MAX || p.x == isize::MAX / 2);
797
            assert!(p.y == 0 || p.y == isize::MIN || p.y == isize::MIN / 2);
798
        }
799
    }
800

            
801
    // ---- Direction::to_points (numeric) ------------------------------------
802

            
803
    #[test]
804
    fn to_points_fromto_delegates_to_to_point() {
805
        let r = rect(200, 100);
806
        for from in ALL_CORNERS {
807
            for to in ALL_CORNERS {
808
                let d = Direction::FromTo(DirectionCorners {
809
                    dir_from: from,
810
                    dir_to: to,
811
                });
812
                assert_eq!(d.to_points(&r), (from.to_point(&r), to.to_point(&r)));
813
            }
814
        }
815
    }
816

            
817
    #[test]
818
    fn to_points_angle_zero_deg_runs_bottom_to_top() {
819
        // CSS: 0deg == "to top", so the gradient starts at the bottom edge.
820
        let r = rect(100, 100);
821
        let (start, end) = Direction::Angle(AngleValue::deg(0.0)).to_points(&r);
822
        assert_near(start, LayoutPoint::new(50, 100), 1);
823
        assert_near(end, LayoutPoint::new(50, 0), 1);
824
    }
825

            
826
    #[test]
827
    fn to_points_angle_180_deg_runs_top_to_bottom() {
828
        // CSS: 180deg == "to bottom".
829
        let r = rect(100, 100);
830
        let (start, end) = Direction::Angle(AngleValue::deg(180.0)).to_points(&r);
831
        assert_near(start, LayoutPoint::new(50, 0), 1);
832
        assert_near(end, LayoutPoint::new(50, 100), 1);
833
    }
834

            
835
    #[test]
836
    fn to_points_angle_90_deg_is_horizontal_across_the_full_width() {
837
        let r = rect(100, 100);
838
        let (start, end) = Direction::Angle(AngleValue::deg(90.0)).to_points(&r);
839
        // Both endpoints sit on the horizontal midline, at the two extremes.
840
        assert!((start.y - 50).abs() <= 1, "start.y = {}", start.y);
841
        assert!((end.y - 50).abs() <= 1, "end.y = {}", end.y);
842
        let mut xs = [start.x, end.x];
843
        xs.sort_unstable();
844
        assert!(xs[0].abs() <= 1 && (xs[1] - 100).abs() <= 1, "xs = {xs:?}");
845
        assert_ne!(start, end);
846
    }
847

            
848
    #[test]
849
    fn to_points_angle_is_symmetric_about_the_rect_center() {
850
        // start = center - d, end = center + d (mod rounding), for *every* angle
851
        // and metric — so the endpoints must always straddle the center.
852
        let r = rect(200, 100);
853
        let mut angles = Vec::new();
854
        let mut deg = -720.0_f32;
855
        while deg <= 720.0 {
856
            angles.push(AngleValue::deg(deg));
857
            deg += 15.0;
858
        }
859
        angles.extend([
860
            AngleValue::rad(1.5),
861
            AngleValue::rad(-3.0),
862
            AngleValue::grad(100.0),
863
            AngleValue::grad(-400.0),
864
            AngleValue::turn(0.25),
865
            AngleValue::turn(-2.5),
866
            AngleValue::percent(50.0),
867
            AngleValue::percent(-125.0),
868
        ]);
869

            
870
        for a in angles {
871
            let (start, end) = Direction::Angle(a).to_points(&r);
872
            assert!(
873
                (start.x + end.x - 200).abs() <= 1,
874
                "x not centered for {a}: {start:?} / {end:?}"
875
            );
876
            assert!(
877
                (start.y + end.y - 100).abs() <= 1,
878
                "y not centered for {a}: {start:?} / {end:?}"
879
            );
880
            // The half-length is the corner projected onto the gradient line, so
881
            // it can never exceed the center-to-corner distance (~111.8 here).
882
            // (The endpoints themselves may fall outside a non-square box.)
883
            // |start - end| == 2 * L <= 2 * hypot(100, 50) == 223.6 (+ rounding).
884
            let len_sq = (start.x - end.x).pow(2) + (start.y - end.y).pow(2);
885
            assert!(
886
                len_sq <= 4 * (100 * 100 + 50 * 50) + 1000,
887
                "gradient line longer than the rect diagonal for {a}: {start:?} / {end:?}"
888
            );
889
        }
890
    }
891

            
892
    #[test]
893
    fn to_points_zero_sized_rect_yields_origin_not_nan() {
894
        // width_half == height_half == 0 => atan(0/0) == NaN propagates through
895
        // the whole computation; the f32 -> isize cast saturates NaN to 0, so
896
        // the result must be (0,0)/(0,0) rather than a panic or garbage.
897
        let r = rect(0, 0);
898
        for a in [
899
            AngleValue::deg(0.0),
900
            AngleValue::deg(45.0),
901
            AngleValue::deg(-137.5),
902
            AngleValue::turn(0.75),
903
        ] {
904
            let (start, end) = Direction::Angle(a).to_points(&r);
905
            assert_eq!(start, LayoutPoint::zero(), "start for {a}");
906
            assert_eq!(end, LayoutPoint::zero(), "end for {a}");
907
        }
908
    }
909

            
910
    #[test]
911
    fn to_points_degenerate_axis_rects_do_not_panic() {
912
        // Zero width => height/width == +-inf => atan(inf) == 90deg. Must not panic.
913
        let thin = rect(0, 100);
914
        let (s, e) = Direction::Angle(AngleValue::deg(0.0)).to_points(&thin);
915
        assert_eq!(s.x, 0);
916
        assert_eq!(e.x, 0);
917
        assert!((s.y + e.y - 100).abs() <= 1);
918

            
919
        let flat = rect(100, 0);
920
        let (s, e) = Direction::Angle(AngleValue::deg(90.0)).to_points(&flat);
921
        assert_eq!(s.y, 0);
922
        assert_eq!(e.y, 0);
923
        assert!((s.x + e.x - 100).abs() <= 1);
924
    }
925

            
926
    #[test]
927
    fn to_points_nan_angle_collapses_to_zero_degrees() {
928
        // FloatValue::new(NaN) -> f32_to_isize(NaN) == 0, so a NaN angle is
929
        // silently the same value as 0deg. Assert the collapse (no NaN escapes).
930
        let nan = AngleValue::deg(f32::NAN);
931
        assert!(nan.to_degrees().is_finite());
932
        assert_eq!(nan.to_degrees(), 0.0);
933
        assert_eq!(nan, AngleValue::deg(0.0));
934

            
935
        let r = rect(100, 100);
936
        assert_eq!(
937
            Direction::Angle(nan).to_points(&r),
938
            Direction::Angle(AngleValue::deg(0.0)).to_points(&r)
939
        );
940
    }
941

            
942
    #[test]
943
    fn to_points_infinite_angle_saturates_and_stays_finite() {
944
        for v in [f32::INFINITY, f32::NEG_INFINITY, f32::MAX, f32::MIN] {
945
            let a = AngleValue::deg(v);
946
            let deg = a.to_degrees();
947
            assert!(deg.is_finite(), "non-finite degrees for {v}");
948
            assert!((0.0..=360.0).contains(&deg), "{v} normalized to {deg}");
949

            
950
            let r = rect(200, 100);
951
            let first = Direction::Angle(a).to_points(&r);
952
            // Deterministic (no NaN-dependent branch flapping).
953
            assert_eq!(first, Direction::Angle(a).to_points(&r));
954
        }
955
    }
956

            
957
    #[test]
958
    fn to_points_isize_extreme_rects_do_not_panic() {
959
        for (w, h) in [
960
            (isize::MAX, isize::MAX),
961
            (isize::MIN, isize::MIN),
962
            (isize::MAX, isize::MIN),
963
            (isize::MIN, 1),
964
            (-1, isize::MAX),
965
        ] {
966
            let r = rect(w, h);
967
            for a in [
968
                AngleValue::deg(45.0),
969
                AngleValue::deg(0.0),
970
                AngleValue::deg(270.0),
971
            ] {
972
                let d = Direction::Angle(a);
973
                // The f32 round-trip saturates instead of panicking; only assert
974
                // that the computation terminates and is deterministic.
975
                assert_eq!(d.to_points(&r), d.to_points(&r), "{w}x{h} @ {a}");
976
            }
977
            // The FromTo path on extreme rects is exact.
978
            let d = canonical(DirectionCorner::BottomRight);
979
            assert_eq!(d.to_points(&r).1, LayoutPoint::new(w, h));
980
        }
981
    }
982

            
983
    // ---- parse_direction_corner (parser, private) ---------------------------
984

            
985
    #[cfg(feature = "parser")]
986
    #[test]
987
    fn parse_corner_valid_minimal() {
988
        assert_eq!(parse_direction_corner("right"), Ok(DirectionCorner::Right));
989
        assert_eq!(parse_direction_corner("left"), Ok(DirectionCorner::Left));
990
        assert_eq!(parse_direction_corner("top"), Ok(DirectionCorner::Top));
991
        assert_eq!(
992
            parse_direction_corner("bottom"),
993
            Ok(DirectionCorner::Bottom)
994
        );
995
    }
996

            
997
    #[cfg(feature = "parser")]
998
    #[test]
999
    fn parse_corner_rejects_untrimmed_cased_and_diagonal_input() {
        // parse_direction_corner does NOT trim and is case-sensitive; every one
        // of these must be a clean Err carrying the input verbatim.
        for bad in [
            "",
            " ",
            "   ",
            "\t",
            "\n",
            "\r\n",
            " right",
            "right ",
            "right\n",
            "Right",
            "RIGHT",
            "rIgHt",
            "top right",
            "top-right",
            "topright",
            "right;",
            "right)",
            "center",
            "start",
            "end",
            "to",
            "to right",
        ] {
            assert_eq!(
                parse_direction_corner(bad),
                Err(CssDirectionCornerParseError::InvalidDirection(bad)),
                "input {bad:?} was not rejected verbatim"
            );
        }
    }
    #[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());
        }
    }
}