1
//! CSS properties for 2D and 3D transformations.
2

            
3
use alloc::{
4
    string::{String, ToString},
5
    vec::Vec,
6
};
7
use core::fmt;
8
use std::num::ParseFloatError;
9
use crate::corety::AzString;
10

            
11
#[cfg(feature = "parser")]
12
use crate::props::basic::{
13
    error::WrongComponentCountError,
14
    length::parse_float_value,
15
    parse::{parse_parentheses, ParenthesisParseError, ParenthesisParseErrorOwned},
16
};
17
use crate::{
18
    codegen::format::GetHash,
19
    props::{
20
        basic::{
21
            angle::{
22
                parse_angle_value, AngleValue, CssAngleValueParseError,
23
                CssAngleValueParseErrorOwned,
24
            },
25
            length::{PercentageParseError, PercentageValue},
26
            pixel::{
27
                parse_pixel_value, CssPixelValueParseError, CssPixelValueParseErrorOwned,
28
                PixelValue,
29
            },
30
            FloatValue,
31
        },
32
        formatter::PrintAsCssValue,
33
    },
34
};
35

            
36
// -- Data Structures --
37

            
38
/// Represents a `perspective-origin` attribute
39
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
40
#[repr(C)]
41
pub struct StylePerspectiveOrigin {
42
    pub x: PixelValue,
43
    pub y: PixelValue,
44
}
45

            
46
impl StylePerspectiveOrigin {
47
12
    #[must_use] pub fn interpolate(&self, other: &Self, t: f32) -> Self {
48
12
        Self {
49
12
            x: self.x.interpolate(&other.x, t),
50
12
            y: self.y.interpolate(&other.y, t),
51
12
        }
52
12
    }
53
}
54

            
55
impl PrintAsCssValue for StylePerspectiveOrigin {
56
3
    fn print_as_css_value(&self) -> String {
57
3
        format!("{} {}", self.x, self.y)
58
3
    }
59
}
60

            
61
/// Represents a `transform-origin` attribute
62
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
63
#[repr(C)]
64
pub struct StyleTransformOrigin {
65
    pub x: PixelValue,
66
    pub y: PixelValue,
67
}
68

            
69
impl Default for StyleTransformOrigin {
70
8306
    fn default() -> Self {
71
8306
        Self {
72
8306
            x: PixelValue::const_percent(50),
73
8306
            y: PixelValue::const_percent(50),
74
8306
        }
75
8306
    }
76
}
77

            
78
impl StyleTransformOrigin {
79
30
    #[must_use] pub fn interpolate(&self, other: &Self, t: f32) -> Self {
80
30
        Self {
81
30
            x: self.x.interpolate(&other.x, t),
82
30
            y: self.y.interpolate(&other.y, t),
83
30
        }
84
30
    }
85
}
86

            
87
impl PrintAsCssValue for StyleTransformOrigin {
88
4
    fn print_as_css_value(&self) -> String {
89
4
        format!("{} {}", self.x, self.y)
90
4
    }
91
}
92

            
93
// Formatting to Rust code
94
impl crate::codegen::format::FormatAsRustCode for StylePerspectiveOrigin {
95
    fn format_as_rust_code(&self, _tabs: usize) -> String {
96
        format!(
97
            "StylePerspectiveOrigin {{ x: {}, y: {} }}",
98
            crate::codegen::format::format_pixel_value(&self.x),
99
            crate::codegen::format::format_pixel_value(&self.y)
100
        )
101
    }
102
}
103

            
104
// Formatting to Rust code for StyleTransformOrigin
105
impl crate::codegen::format::FormatAsRustCode for StyleTransformOrigin {
106
    fn format_as_rust_code(&self, _tabs: usize) -> String {
107
        format!(
108
            "StyleTransformOrigin {{ x: {}, y: {} }}",
109
            crate::codegen::format::format_pixel_value(&self.x),
110
            crate::codegen::format::format_pixel_value(&self.y)
111
        )
112
    }
113
}
114

            
115
/// Represents a `backface-visibility` attribute
116
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
117
#[repr(C)]
118
pub enum StyleBackfaceVisibility {
119
    #[default]
120
    Visible,
121
    Hidden,
122
}
123

            
124
impl PrintAsCssValue for StyleBackfaceVisibility {
125
2
    fn print_as_css_value(&self) -> String {
126
2
        String::from(match self {
127
1
            Self::Hidden => "hidden",
128
1
            Self::Visible => "visible",
129
        })
130
2
    }
131
}
132

            
133
/// Represents one component of a `transform` attribute
134
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
135
#[repr(C, u8)]
136
pub enum StyleTransform {
137
    Matrix(StyleTransformMatrix2D),
138
    Matrix3D(StyleTransformMatrix3D),
139
    Translate(StyleTransformTranslate2D),
140
    Translate3D(StyleTransformTranslate3D),
141
    TranslateX(PixelValue),
142
    TranslateY(PixelValue),
143
    TranslateZ(PixelValue),
144
    Rotate(AngleValue),
145
    Rotate3D(StyleTransformRotate3D),
146
    RotateX(AngleValue),
147
    RotateY(AngleValue),
148
    RotateZ(AngleValue),
149
    Scale(StyleTransformScale2D),
150
    Scale3D(StyleTransformScale3D),
151
    ScaleX(PercentageValue),
152
    ScaleY(PercentageValue),
153
    ScaleZ(PercentageValue),
154
    Skew(StyleTransformSkew2D),
155
    SkewX(AngleValue),
156
    SkewY(AngleValue),
157
    Perspective(PixelValue),
158
}
159

            
160
impl_option!(
161
    StyleTransform,
162
    OptionStyleTransform,
163
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd]
164
);
165

            
166
impl_vec!(StyleTransform, StyleTransformVec, StyleTransformVecDestructor, StyleTransformVecDestructorType, StyleTransformVecSlice, OptionStyleTransform);
167
impl_vec_debug!(StyleTransform, StyleTransformVec);
168
impl_vec_partialord!(StyleTransform, StyleTransformVec);
169
impl_vec_ord!(StyleTransform, StyleTransformVec);
170
impl_vec_clone!(
171
    StyleTransform,
172
    StyleTransformVec,
173
    StyleTransformVecDestructor
174
);
175
impl_vec_partialeq!(StyleTransform, StyleTransformVec);
176
impl_vec_eq!(StyleTransform, StyleTransformVec);
177
impl_vec_hash!(StyleTransform, StyleTransformVec);
178

            
179
impl PrintAsCssValue for StyleTransformVec {
180
1
    fn print_as_css_value(&self) -> String {
181
1
        self.as_ref()
182
1
            .iter()
183
1
            .map(PrintAsCssValue::print_as_css_value)
184
1
            .collect::<Vec<_>>()
185
1
            .join(" ")
186
1
    }
187
}
188

            
189
// Formatting to Rust code for StyleTransformVec
190
impl crate::codegen::format::FormatAsRustCode for StyleTransformVec {
191
    fn format_as_rust_code(&self, _tabs: usize) -> String {
192
        format!(
193
            "StyleTransformVec::from_const_slice(STYLE_TRANSFORM_{}_ITEMS)",
194
            self.get_hash()
195
        )
196
    }
197
}
198

            
199
impl PrintAsCssValue for StyleTransform {
200
39
    fn print_as_css_value(&self) -> String {
201
39
        match self {
202
2
            Self::Matrix(m) => format!(
203
2
                "matrix({}, {}, {}, {}, {}, {})",
204
                m.a, m.b, m.c, m.d, m.tx, m.ty
205
            ),
206
2
            Self::Matrix3D(m) => format!(
207
2
                "matrix3d({}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {})",
208
                m.m11,
209
                m.m12,
210
                m.m13,
211
                m.m14,
212
                m.m21,
213
                m.m22,
214
                m.m23,
215
                m.m24,
216
                m.m31,
217
                m.m32,
218
                m.m33,
219
                m.m34,
220
                m.m41,
221
                m.m42,
222
                m.m43,
223
                m.m44
224
            ),
225
2
            Self::Translate(t) => format!("translate({}, {})", t.x, t.y),
226
2
            Self::Translate3D(t) => format!("translate3d({}, {}, {})", t.x, t.y, t.z),
227
2
            Self::TranslateX(x) => format!("translateX({x})"),
228
2
            Self::TranslateY(y) => format!("translateY({y})"),
229
2
            Self::TranslateZ(z) => format!("translateZ({z})"),
230
2
            Self::Rotate(r) => format!("rotate({r})"),
231
2
            Self::Rotate3D(r) => {
232
2
                format!("rotate3d({}, {}, {}, {})", r.x, r.y, r.z, r.angle)
233
            }
234
2
            Self::RotateX(x) => format!("rotateX({x})"),
235
2
            Self::RotateY(y) => format!("rotateY({y})"),
236
2
            Self::RotateZ(z) => format!("rotateZ({z})"),
237
2
            Self::Scale(s) => format!("scale({}, {})", s.x, s.y),
238
2
            Self::Scale3D(s) => format!("scale3d({}, {}, {})", s.x, s.y, s.z),
239
1
            Self::ScaleX(x) => format!("scaleX({x})"),
240
1
            Self::ScaleY(y) => format!("scaleY({y})"),
241
1
            Self::ScaleZ(z) => format!("scaleZ({z})"),
242
2
            Self::Skew(sk) => format!("skew({}, {})", sk.x, sk.y),
243
2
            Self::SkewX(x) => format!("skewX({x})"),
244
2
            Self::SkewY(y) => format!("skewY({y})"),
245
2
            Self::Perspective(dist) => format!("perspective({dist})"),
246
        }
247
39
    }
248
}
249

            
250
/// Represents a CSS `matrix(a, b, c, d, tx, ty)` 2D transform function.
251
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
252
#[repr(C)]
253
pub struct StyleTransformMatrix2D {
254
    pub a: FloatValue,
255
    pub b: FloatValue,
256
    pub c: FloatValue,
257
    pub d: FloatValue,
258
    pub tx: FloatValue,
259
    pub ty: FloatValue,
260
}
261

            
262
impl Default for StyleTransformMatrix2D {
263
2
    fn default() -> Self {
264
2
        Self {
265
2
            a: FloatValue::const_new(1),
266
2
            b: FloatValue::const_new(0),
267
2
            c: FloatValue::const_new(0),
268
2
            d: FloatValue::const_new(1),
269
2
            tx: FloatValue::const_new(0),
270
2
            ty: FloatValue::const_new(0),
271
2
        }
272
2
    }
273
}
274

            
275
/// Represents a CSS `matrix3d(...)` 3D transform function (4x4 matrix).
276
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
277
#[repr(C)]
278
pub struct StyleTransformMatrix3D {
279
    pub m11: FloatValue,
280
    pub m12: FloatValue,
281
    pub m13: FloatValue,
282
    pub m14: FloatValue,
283
    pub m21: FloatValue,
284
    pub m22: FloatValue,
285
    pub m23: FloatValue,
286
    pub m24: FloatValue,
287
    pub m31: FloatValue,
288
    pub m32: FloatValue,
289
    pub m33: FloatValue,
290
    pub m34: FloatValue,
291
    pub m41: FloatValue,
292
    pub m42: FloatValue,
293
    pub m43: FloatValue,
294
    pub m44: FloatValue,
295
}
296

            
297
impl Default for StyleTransformMatrix3D {
298
3
    fn default() -> Self {
299
3
        Self {
300
3
            m11: FloatValue::const_new(1),
301
3
            m12: FloatValue::const_new(0),
302
3
            m13: FloatValue::const_new(0),
303
3
            m14: FloatValue::const_new(0),
304
3
            m21: FloatValue::const_new(0),
305
3
            m22: FloatValue::const_new(1),
306
3
            m23: FloatValue::const_new(0),
307
3
            m24: FloatValue::const_new(0),
308
3
            m31: FloatValue::const_new(0),
309
3
            m32: FloatValue::const_new(0),
310
3
            m33: FloatValue::const_new(1),
311
3
            m34: FloatValue::const_new(0),
312
3
            m41: FloatValue::const_new(0),
313
3
            m42: FloatValue::const_new(0),
314
3
            m43: FloatValue::const_new(0),
315
3
            m44: FloatValue::const_new(1),
316
3
        }
317
3
    }
318
}
319

            
320
/// Represents a CSS `translate(x, y)` 2D translation.
321
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
322
#[repr(C)]
323
pub struct StyleTransformTranslate2D {
324
    pub x: PixelValue,
325
    pub y: PixelValue,
326
}
327

            
328
/// Represents a CSS `translate3d(x, y, z)` 3D translation.
329
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
330
#[repr(C)]
331
pub struct StyleTransformTranslate3D {
332
    pub x: PixelValue,
333
    pub y: PixelValue,
334
    pub z: PixelValue,
335
}
336

            
337
/// Represents a CSS `rotate3d(x, y, z, angle)` 3D rotation.
338
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
339
#[repr(C)]
340
pub struct StyleTransformRotate3D {
341
    pub x: FloatValue,
342
    pub y: FloatValue,
343
    pub z: FloatValue,
344
    pub angle: AngleValue,
345
}
346

            
347
/// Represents a CSS `scale(x, y)` 2D scaling.
348
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
349
#[repr(C)]
350
pub struct StyleTransformScale2D {
351
    pub x: FloatValue,
352
    pub y: FloatValue,
353
}
354

            
355
/// Represents a CSS `scale3d(x, y, z)` 3D scaling.
356
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
357
#[repr(C)]
358
pub struct StyleTransformScale3D {
359
    pub x: FloatValue,
360
    pub y: FloatValue,
361
    pub z: FloatValue,
362
}
363

            
364
/// Represents a CSS `skew(x, y)` 2D skew transformation.
365
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
366
#[repr(C)]
367
pub struct StyleTransformSkew2D {
368
    pub x: AngleValue,
369
    pub y: AngleValue,
370
}
371

            
372
// -- Errors --
373

            
374
#[derive(Clone, PartialEq, Eq)]
375
pub enum CssStyleTransformParseError<'a> {
376
    InvalidTransform(&'a str),
377
    InvalidParenthesis(ParenthesisParseError<'a>),
378
    WrongNumberOfComponents {
379
        expected: usize,
380
        got: usize,
381
        input: &'a str,
382
    },
383
    NumberParseError(ParseFloatError),
384
    PixelValueParseError(CssPixelValueParseError<'a>),
385
    AngleValueParseError(CssAngleValueParseError<'a>),
386
    PercentageValueParseError(PercentageParseError),
387
}
388

            
389
impl_debug_as_display!(CssStyleTransformParseError<'a>);
390
impl_display! { CssStyleTransformParseError<'a>, {
391
    InvalidTransform(e) => format!("Invalid transform property: \"{}\"", e),
392
    InvalidParenthesis(e) => format!("Invalid transform property - parenthesis error: {}", e),
393
    WrongNumberOfComponents { expected, got, input } => format!("Invalid number of components: expected {}, got {}: \"{}\"", expected, got, input),
394
    NumberParseError(e) => format!("Could not parse number: {}", e),
395
    PixelValueParseError(e) => format!("Invalid pixel value: {}", e),
396
    AngleValueParseError(e) => format!("Invalid angle value: {}", e),
397
    PercentageValueParseError(e) => format!("Error parsing percentage: {}", e),
398
}}
399

            
400
impl_from! { ParenthesisParseError<'a>, CssStyleTransformParseError::InvalidParenthesis }
401
impl_from! { CssPixelValueParseError<'a>, CssStyleTransformParseError::PixelValueParseError }
402
impl_from! { CssAngleValueParseError<'a>, CssStyleTransformParseError::AngleValueParseError }
403
// Written out (not impl_from!): ParseFloatError carries no lifetime, so the
404
// macro's `<'a>` would be used only by the target type (single_use_lifetimes).
405
impl From<ParseFloatError> for CssStyleTransformParseError<'_> {
406
16
    fn from(e: ParseFloatError) -> Self {
407
16
        Self::NumberParseError(e)
408
16
    }
409
}
410

            
411
impl From<PercentageParseError> for CssStyleTransformParseError<'_> {
412
    fn from(p: PercentageParseError) -> Self {
413
        Self::PercentageValueParseError(p)
414
    }
415
}
416

            
417
#[derive(Debug, Clone, PartialEq, Eq)]
418
#[repr(C, u8)]
419
pub enum CssStyleTransformParseErrorOwned {
420
    InvalidTransform(AzString),
421
    InvalidParenthesis(ParenthesisParseErrorOwned),
422
    WrongNumberOfComponents(WrongComponentCountError),
423
    NumberParseError(crate::props::basic::error::ParseFloatError),
424
    PixelValueParseError(CssPixelValueParseErrorOwned),
425
    AngleValueParseError(CssAngleValueParseErrorOwned),
426
    PercentageValueParseError(PercentageParseError),
427
}
428

            
429
impl CssStyleTransformParseError<'_> {
430
49
    #[must_use] pub fn to_contained(&self) -> CssStyleTransformParseErrorOwned {
431
49
        match self {
432
6
            Self::InvalidTransform(s) => {
433
6
                CssStyleTransformParseErrorOwned::InvalidTransform((*s).to_string().into())
434
            }
435
15
            Self::InvalidParenthesis(e) => {
436
15
                CssStyleTransformParseErrorOwned::InvalidParenthesis(e.to_contained())
437
            }
438
9
            Self::WrongNumberOfComponents {
439
9
                expected,
440
9
                got,
441
9
                input,
442
9
            } => CssStyleTransformParseErrorOwned::WrongNumberOfComponents(WrongComponentCountError {
443
9
                expected: *expected,
444
9
                got: *got,
445
9
                input: (*input).to_string().into(),
446
9
            }),
447
5
            Self::NumberParseError(e) => {
448
5
                CssStyleTransformParseErrorOwned::NumberParseError(e.clone().into())
449
            }
450
5
            Self::PixelValueParseError(e) => {
451
5
                CssStyleTransformParseErrorOwned::PixelValueParseError(e.to_contained())
452
            }
453
5
            Self::AngleValueParseError(e) => {
454
5
                CssStyleTransformParseErrorOwned::AngleValueParseError(e.to_contained())
455
            }
456
4
            Self::PercentageValueParseError(e) => {
457
4
                CssStyleTransformParseErrorOwned::PercentageValueParseError(e.clone())
458
            }
459
        }
460
49
    }
461
}
462

            
463
impl CssStyleTransformParseErrorOwned {
464
29
    #[must_use] pub fn to_shared(&self) -> CssStyleTransformParseError<'_> {
465
29
        match self {
466
3
            Self::InvalidTransform(s) => CssStyleTransformParseError::InvalidTransform(s),
467
10
            Self::InvalidParenthesis(e) => {
468
10
                CssStyleTransformParseError::InvalidParenthesis(e.to_shared())
469
            }
470
5
            Self::WrongNumberOfComponents(e) => CssStyleTransformParseError::WrongNumberOfComponents {
471
5
                expected: e.expected,
472
5
                got: e.got,
473
5
                input: e.input.as_str(),
474
5
            },
475
3
            Self::NumberParseError(e) => CssStyleTransformParseError::NumberParseError(e.to_std()),
476
3
            Self::PixelValueParseError(e) => {
477
3
                CssStyleTransformParseError::PixelValueParseError(e.to_shared())
478
            }
479
3
            Self::AngleValueParseError(e) => {
480
3
                CssStyleTransformParseError::AngleValueParseError(e.to_shared())
481
            }
482
2
            Self::PercentageValueParseError(e) => {
483
2
                CssStyleTransformParseError::PercentageValueParseError(e.clone())
484
            }
485
        }
486
29
    }
487
}
488

            
489
#[derive(Clone, PartialEq, Eq)]
490
pub enum CssStyleTransformOriginParseError<'a> {
491
    WrongNumberOfComponents {
492
        expected: usize,
493
        got: usize,
494
        input: &'a str,
495
    },
496
    PixelValueParseError(CssPixelValueParseError<'a>),
497
}
498

            
499
impl_debug_as_display!(CssStyleTransformOriginParseError<'a>);
500
impl_display! { CssStyleTransformOriginParseError<'a>, {
501
    WrongNumberOfComponents { expected, got, input } => format!("Invalid number of components: expected {}, got {}: \"{}\"", expected, got, input),
502
    PixelValueParseError(e) => format!("Invalid pixel value: {}", e),
503
}}
504
impl_from! { CssPixelValueParseError<'a>, CssStyleTransformOriginParseError::PixelValueParseError }
505

            
506
#[derive(Debug, Clone, PartialEq, Eq)]
507
#[repr(C, u8)]
508
pub enum CssStyleTransformOriginParseErrorOwned {
509
    WrongNumberOfComponents(WrongComponentCountError),
510
    PixelValueParseError(CssPixelValueParseErrorOwned),
511
}
512

            
513
impl CssStyleTransformOriginParseError<'_> {
514
5
    #[must_use] pub fn to_contained(&self) -> CssStyleTransformOriginParseErrorOwned {
515
5
        match self {
516
2
            Self::WrongNumberOfComponents {
517
2
                expected,
518
2
                got,
519
2
                input,
520
2
            } => CssStyleTransformOriginParseErrorOwned::WrongNumberOfComponents(WrongComponentCountError {
521
2
                expected: *expected,
522
2
                got: *got,
523
2
                input: (*input).to_string().into(),
524
2
            }),
525
3
            Self::PixelValueParseError(e) => {
526
3
                CssStyleTransformOriginParseErrorOwned::PixelValueParseError(e.to_contained())
527
            }
528
        }
529
5
    }
530
}
531

            
532
impl CssStyleTransformOriginParseErrorOwned {
533
5
    #[must_use] pub fn to_shared(&self) -> CssStyleTransformOriginParseError<'_> {
534
5
        match self {
535
2
            Self::WrongNumberOfComponents(e) => CssStyleTransformOriginParseError::WrongNumberOfComponents {
536
2
                expected: e.expected,
537
2
                got: e.got,
538
2
                input: e.input.as_str(),
539
2
            },
540
3
            Self::PixelValueParseError(e) => {
541
3
                CssStyleTransformOriginParseError::PixelValueParseError(e.to_shared())
542
            }
543
        }
544
5
    }
545
}
546

            
547
#[derive(Clone, PartialEq, Eq)]
548
pub enum CssStylePerspectiveOriginParseError<'a> {
549
    WrongNumberOfComponents {
550
        expected: usize,
551
        got: usize,
552
        input: &'a str,
553
    },
554
    PixelValueParseError(CssPixelValueParseError<'a>),
555
}
556

            
557
impl_debug_as_display!(CssStylePerspectiveOriginParseError<'a>);
558
impl_display! { CssStylePerspectiveOriginParseError<'a>, {
559
    WrongNumberOfComponents { expected, got, input } => format!("Invalid number of components: expected {}, got {}: \"{}\"", expected, got, input),
560
    PixelValueParseError(e) => format!("Invalid pixel value: {}", e),
561
}}
562
impl_from! { CssPixelValueParseError<'a>, CssStylePerspectiveOriginParseError::PixelValueParseError }
563

            
564
#[derive(Debug, Clone, PartialEq, Eq)]
565
#[repr(C, u8)]
566
pub enum CssStylePerspectiveOriginParseErrorOwned {
567
    WrongNumberOfComponents(WrongComponentCountError),
568
    PixelValueParseError(CssPixelValueParseErrorOwned),
569
}
570

            
571
impl CssStylePerspectiveOriginParseError<'_> {
572
4
    #[must_use] pub fn to_contained(&self) -> CssStylePerspectiveOriginParseErrorOwned {
573
4
        match self {
574
2
            Self::WrongNumberOfComponents {
575
2
                expected,
576
2
                got,
577
2
                input,
578
2
            } => CssStylePerspectiveOriginParseErrorOwned::WrongNumberOfComponents(WrongComponentCountError {
579
2
                expected: *expected,
580
2
                got: *got,
581
2
                input: (*input).to_string().into(),
582
2
            }),
583
2
            Self::PixelValueParseError(e) => {
584
2
                CssStylePerspectiveOriginParseErrorOwned::PixelValueParseError(e.to_contained())
585
            }
586
        }
587
4
    }
588
}
589

            
590
impl CssStylePerspectiveOriginParseErrorOwned {
591
4
    #[must_use] pub fn to_shared(&self) -> CssStylePerspectiveOriginParseError<'_> {
592
4
        match self {
593
2
            Self::WrongNumberOfComponents(e) => CssStylePerspectiveOriginParseError::WrongNumberOfComponents {
594
2
                expected: e.expected,
595
2
                got: e.got,
596
2
                input: e.input.as_str(),
597
2
            },
598
2
            Self::PixelValueParseError(e) => {
599
2
                CssStylePerspectiveOriginParseError::PixelValueParseError(e.to_shared())
600
            }
601
        }
602
4
    }
603
}
604

            
605
#[derive(Clone, PartialEq, Eq)]
606
pub enum CssBackfaceVisibilityParseError<'a> {
607
    InvalidValue(&'a str),
608
}
609

            
610
impl_debug_as_display!(CssBackfaceVisibilityParseError<'a>);
611
impl_display! { CssBackfaceVisibilityParseError<'a>, {
612
    InvalidValue(s) => format!("Invalid value for backface-visibility: \"{}\", expected \"visible\" or \"hidden\"", s),
613
}}
614

            
615
#[derive(Debug, Clone, PartialEq, Eq)]
616
#[repr(C, u8)]
617
pub enum CssBackfaceVisibilityParseErrorOwned {
618
    InvalidValue(AzString),
619
}
620

            
621
impl CssBackfaceVisibilityParseError<'_> {
622
5
    #[must_use] pub fn to_contained(&self) -> CssBackfaceVisibilityParseErrorOwned {
623
5
        match self {
624
5
            Self::InvalidValue(s) => {
625
5
                CssBackfaceVisibilityParseErrorOwned::InvalidValue((*s).to_string().into())
626
            }
627
        }
628
5
    }
629
}
630

            
631
impl CssBackfaceVisibilityParseErrorOwned {
632
5
    #[must_use] pub fn to_shared(&self) -> CssBackfaceVisibilityParseError<'_> {
633
5
        match self {
634
5
            Self::InvalidValue(s) => CssBackfaceVisibilityParseError::InvalidValue(s),
635
        }
636
5
    }
637
}
638

            
639
// -- Parsers --
640

            
641
#[cfg(feature = "parser")]
642
/// # Errors
643
///
644
/// Returns an error if `input` is not a valid CSS `transform-vec` value.
645
841
pub fn parse_style_transform_vec(
646
841
    input: &str,
647
841
) -> Result<StyleTransformVec, CssStyleTransformParseError<'_>> {
648
841
    crate::props::basic::parse::split_string_respect_whitespace(input)
649
841
        .iter()
650
20873
        .map(|i| parse_style_transform(i))
651
841
        .collect::<Result<Vec<_>, _>>()
652
841
        .map(Into::into)
653
841
}
654

            
655
#[cfg(feature = "parser")]
656
#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose CSS parser/formatter/dispatch table (one branch per property/variant)
657
/// # Errors
658
///
659
/// Returns an error if `input` is not a valid CSS `transform` value.
660
21033
pub fn parse_style_transform(
661
21033
    input: &str,
662
21033
) -> Result<StyleTransform, CssStyleTransformParseError<'_>> {
663
18
    fn get_numbers(
664
18
        input: &str,
665
18
        expected: usize,
666
18
    ) -> Result<Vec<f32>, CssStyleTransformParseError<'_>> {
667
18
        let numbers: Vec<_> = input
668
18
            .split(',')
669
99
            .map(|s| s.trim().parse::<f32>())
670
18
            .collect::<Result<_, _>>()?;
671
12
        if numbers.len() == expected {
672
7
            Ok(numbers)
673
        } else {
674
5
            Err(CssStyleTransformParseError::WrongNumberOfComponents {
675
5
                expected,
676
5
                got: numbers.len(),
677
5
                input,
678
5
            })
679
        }
680
18
    }
681

            
682
21033
    let (transform_type, transform_values) = parse_parentheses(
683
21033
        input,
684
21033
        &[
685
21033
            "matrix",
686
21033
            "matrix3d",
687
21033
            "translate",
688
21033
            "translate3d",
689
21033
            "translateX",
690
21033
            "translateY",
691
21033
            "translateZ",
692
21033
            "rotate",
693
21033
            "rotate3d",
694
21033
            "rotateX",
695
21033
            "rotateY",
696
21033
            "rotateZ",
697
21033
            "scale",
698
21033
            "scale3d",
699
21033
            "scaleX",
700
21033
            "scaleY",
701
21033
            "scaleZ",
702
21033
            "skew",
703
21033
            "skewX",
704
21033
            "skewY",
705
21033
            "perspective",
706
21033
        ],
707
47
    )?;
708

            
709
20986
    match transform_type {
710
20986
        "matrix" => {
711
9
            let nums = get_numbers(transform_values, 6)?;
712
3
            Ok(StyleTransform::Matrix(StyleTransformMatrix2D {
713
3
                a: FloatValue::new(nums[0]),
714
3
                b: FloatValue::new(nums[1]),
715
3
                c: FloatValue::new(nums[2]),
716
3
                d: FloatValue::new(nums[3]),
717
3
                tx: FloatValue::new(nums[4]),
718
3
                ty: FloatValue::new(nums[5]),
719
3
            }))
720
        }
721
20977
        "matrix3d" => {
722
5
            let nums = get_numbers(transform_values, 16)?;
723
2
            Ok(StyleTransform::Matrix3D(StyleTransformMatrix3D {
724
2
                m11: FloatValue::new(nums[0]),
725
2
                m12: FloatValue::new(nums[1]),
726
2
                m13: FloatValue::new(nums[2]),
727
2
                m14: FloatValue::new(nums[3]),
728
2
                m21: FloatValue::new(nums[4]),
729
2
                m22: FloatValue::new(nums[5]),
730
2
                m23: FloatValue::new(nums[6]),
731
2
                m24: FloatValue::new(nums[7]),
732
2
                m31: FloatValue::new(nums[8]),
733
2
                m32: FloatValue::new(nums[9]),
734
2
                m33: FloatValue::new(nums[10]),
735
2
                m34: FloatValue::new(nums[11]),
736
2
                m41: FloatValue::new(nums[12]),
737
2
                m42: FloatValue::new(nums[13]),
738
2
                m43: FloatValue::new(nums[14]),
739
2
                m44: FloatValue::new(nums[15]),
740
2
            }))
741
        }
742
20972
        "translate" => {
743
40
            let components: Vec<_> = transform_values.split(',').collect();
744

            
745
            // translate() takes exactly 1 or 2 parameters (x, or x and y)
746
40
            if components.len() > 2 {
747
2
                return Err(CssStyleTransformParseError::WrongNumberOfComponents {
748
2
                    expected: 2,
749
2
                    got: components.len(),
750
2
                    input: transform_values,
751
2
                });
752
38
            }
753

            
754
38
            let x = parse_pixel_value(
755
38
                components.first()
756
38
                    .ok_or(CssStyleTransformParseError::WrongNumberOfComponents {
757
38
                        expected: 2,
758
38
                        got: 0,
759
38
                        input: transform_values,
760
38
                    })?
761
38
                    .trim(),
762
2
            )?;
763
36
            let y = match components.get(1) {
764
35
                Some(c) => parse_pixel_value(c.trim())?,
765
1
                None => PixelValue::px(0.0),
766
            };
767
35
            Ok(StyleTransform::Translate(StyleTransformTranslate2D {
768
35
                x,
769
35
                y,
770
35
            }))
771
        }
772
20932
        "translate3d" => {
773
7
            let components: Vec<_> = transform_values.split(',').collect();
774
7
            let x = parse_pixel_value(
775
7
                components.first()
776
7
                    .ok_or(CssStyleTransformParseError::WrongNumberOfComponents {
777
7
                        expected: 3,
778
7
                        got: 0,
779
7
                        input: transform_values,
780
7
                    })?
781
7
                    .trim(),
782
1
            )?;
783
6
            let y = parse_pixel_value(
784
6
                components
785
6
                    .get(1)
786
6
                    .ok_or(CssStyleTransformParseError::WrongNumberOfComponents {
787
6
                        expected: 3,
788
6
                        got: 1,
789
6
                        input: transform_values,
790
6
                    })?
791
6
                    .trim(),
792
            )?;
793
4
            let z = parse_pixel_value(
794
6
                components
795
6
                    .get(2)
796
6
                    .ok_or(CssStyleTransformParseError::WrongNumberOfComponents {
797
6
                        expected: 3,
798
6
                        got: 2,
799
6
                        input: transform_values,
800
6
                    })?
801
4
                    .trim(),
802
1
            )?;
803
3
            Ok(StyleTransform::Translate3D(StyleTransformTranslate3D {
804
3
                x,
805
3
                y,
806
3
                z,
807
3
            }))
808
        }
809
20925
        "translateX" => Ok(StyleTransform::TranslateX(parse_pixel_value(
810
20738
            transform_values,
811
9
        )?)),
812
187
        "translateY" => Ok(StyleTransform::TranslateY(parse_pixel_value(
813
3
            transform_values,
814
1
        )?)),
815
184
        "translateZ" => Ok(StyleTransform::TranslateZ(parse_pixel_value(
816
3
            transform_values,
817
1
        )?)),
818
181
        "rotate" => Ok(StyleTransform::Rotate(parse_angle_value(transform_values)?)),
819
68
        "rotate3d" => {
820
6
            let parts: Vec<_> = transform_values.splitn(4, ',').collect();
821
6
            if parts.len() != 4 {
822
2
                return Err(CssStyleTransformParseError::WrongNumberOfComponents {
823
2
                    expected: 4,
824
2
                    got: parts.len(),
825
2
                    input: transform_values,
826
2
                });
827
4
            }
828
4
            let x = parts[0].trim().parse::<f32>()?;
829
4
            let y = parts[1].trim().parse::<f32>()?;
830
4
            let z = parts[2].trim().parse::<f32>()?;
831
4
            let angle = parse_angle_value(parts[3].trim())?;
832
3
            Ok(StyleTransform::Rotate3D(StyleTransformRotate3D {
833
3
                x: FloatValue::new(x),
834
3
                y: FloatValue::new(y),
835
3
                z: FloatValue::new(z),
836
3
                angle,
837
3
            }))
838
        }
839
62
        "rotateX" => Ok(StyleTransform::RotateX(parse_angle_value(
840
4
            transform_values,
841
2
        )?)),
842
58
        "rotateY" => Ok(StyleTransform::RotateY(parse_angle_value(
843
3
            transform_values,
844
1
        )?)),
845
55
        "rotateZ" => Ok(StyleTransform::RotateZ(parse_angle_value(
846
3
            transform_values,
847
1
        )?)),
848
52
        "scale" => {
849
16
            let parts: Vec<_> = transform_values.split(',').collect();
850
16
            if parts.is_empty() || parts.len() > 2 {
851
1
                return Err(CssStyleTransformParseError::WrongNumberOfComponents {
852
1
                    expected: 2,
853
1
                    got: parts.len(),
854
1
                    input: transform_values,
855
1
                });
856
15
            }
857
15
            let x = parts[0].trim().parse::<f32>()?;
858
14
            let y = if parts.len() == 2 {
859
6
                parts[1].trim().parse::<f32>()?
860
            } else {
861
8
                x
862
            };
863
13
            Ok(StyleTransform::Scale(StyleTransformScale2D {
864
13
                x: FloatValue::new(x),
865
13
                y: FloatValue::new(y),
866
13
            }))
867
        }
868
36
        "scale3d" => {
869
4
            let nums = get_numbers(transform_values, 3)?;
870
2
            Ok(StyleTransform::Scale3D(StyleTransformScale3D {
871
2
                x: FloatValue::new(nums[0]),
872
2
                y: FloatValue::new(nums[1]),
873
2
                z: FloatValue::new(nums[2]),
874
2
            }))
875
        }
876
32
        "scaleX" => Ok(StyleTransform::ScaleX(PercentageValue::new(
877
8
            transform_values.trim().parse::<f32>()? * 100.0,
878
        ))),
879
24
        "scaleY" => Ok(StyleTransform::ScaleY(PercentageValue::new(
880
3
            transform_values.trim().parse::<f32>()? * 100.0,
881
        ))),
882
21
        "scaleZ" => Ok(StyleTransform::ScaleZ(PercentageValue::new(
883
2
            transform_values.trim().parse::<f32>()? * 100.0,
884
        ))),
885
19
        "skew" => {
886
7
            let components: Vec<_> = transform_values.split(',').collect();
887
7
            if components.is_empty() || components.len() > 2 {
888
1
                return Err(CssStyleTransformParseError::WrongNumberOfComponents {
889
1
                    expected: 2,
890
1
                    got: components.len(),
891
1
                    input: transform_values,
892
1
                });
893
6
            }
894
6
            let x = parse_angle_value(components[0].trim())?;
895
5
            let y = match components.get(1) {
896
4
                Some(c) => parse_angle_value(c.trim())?,
897
1
                None => AngleValue::deg(0.0),
898
            };
899
4
            Ok(StyleTransform::Skew(StyleTransformSkew2D { x, y }))
900
        }
901
12
        "skewX" => Ok(StyleTransform::SkewX(parse_angle_value(transform_values)?)),
902
8
        "skewY" => Ok(StyleTransform::SkewY(parse_angle_value(transform_values)?)),
903
4
        "perspective" => Ok(StyleTransform::Perspective(parse_pixel_value(
904
4
            transform_values,
905
2
        )?)),
906
        _ => unreachable!(),
907
    }
908
21033
}
909

            
910
#[cfg(feature = "parser")]
911
/// # Errors
912
///
913
/// Returns an error if `input` is not a valid CSS `transform-origin` value.
914
34
pub fn parse_style_transform_origin(
915
34
    input: &str,
916
34
) -> Result<StyleTransformOrigin, CssStyleTransformOriginParseError<'_>> {
917
    // Helper to parse position keywords or pixel values
918
46
    fn parse_position_component(
919
46
        s: &str,
920
46
        is_horizontal: bool,
921
46
    ) -> Result<PixelValue, CssPixelValueParseError<'_>> {
922
46
        match s.trim() {
923
46
            "left" if is_horizontal => Ok(PixelValue::percent(0.0)),
924
41
            "center" => Ok(PixelValue::percent(50.0)),
925
39
            "right" if is_horizontal => Ok(PixelValue::percent(100.0)),
926
38
            "top" if !is_horizontal => Ok(PixelValue::percent(0.0)),
927
33
            "bottom" if !is_horizontal => Ok(PixelValue::percent(100.0)),
928
35
            _ => parse_pixel_value(s),
929
        }
930
46
    }
931

            
932
34
    let components: Vec<_> = input.split_whitespace().collect();
933
34
    if components.len() != 2 {
934
7
        return Err(CssStyleTransformOriginParseError::WrongNumberOfComponents {
935
7
            expected: 2,
936
7
            got: components.len(),
937
7
            input,
938
7
        });
939
27
    }
940

            
941
27
    let x = parse_position_component(components[0], true)?;
942
19
    let y = parse_position_component(components[1], false)?;
943
17
    Ok(StyleTransformOrigin { x, y })
944
34
}
945

            
946
#[cfg(feature = "parser")]
947
/// # Errors
948
///
949
/// Returns an error if `input` is not a valid CSS `perspective-origin` value.
950
17
pub fn parse_style_perspective_origin(
951
17
    input: &str,
952
17
) -> Result<StylePerspectiveOrigin, CssStylePerspectiveOriginParseError<'_>> {
953
17
    let components: Vec<_> = input.split_whitespace().collect();
954
17
    if components.len() != 2 {
955
3
        return Err(
956
3
            CssStylePerspectiveOriginParseError::WrongNumberOfComponents {
957
3
                expected: 2,
958
3
                got: components.len(),
959
3
                input,
960
3
            },
961
3
        );
962
14
    }
963
14
    let x = parse_pixel_value(components[0])?;
964
6
    let y = parse_pixel_value(components[1])?;
965
6
    Ok(StylePerspectiveOrigin { x, y })
966
17
}
967

            
968
#[cfg(feature = "parser")]
969
/// # Errors
970
///
971
/// Returns an error if `input` is not a valid CSS `backface-visibility` value.
972
24
pub fn parse_style_backface_visibility(
973
24
    input: &str,
974
24
) -> Result<StyleBackfaceVisibility, CssBackfaceVisibilityParseError<'_>> {
975
24
    match input.trim() {
976
24
        "visible" => Ok(StyleBackfaceVisibility::Visible),
977
20
        "hidden" => Ok(StyleBackfaceVisibility::Hidden),
978
17
        _ => Err(CssBackfaceVisibilityParseError::InvalidValue(input)),
979
    }
980
24
}
981

            
982
#[cfg(all(test, feature = "parser"))]
983
mod tests {
984
    // Tests assert that parsed values equal the exact source literals.
985
    #![allow(clippy::float_cmp)]
986
    use super::*;
987

            
988
    #[test]
989
1
    fn test_parse_transform_vec() {
990
1
        let result =
991
1
            parse_style_transform_vec("translateX(10px) rotate(90deg) scale(0.5, 0.5)").unwrap();
992
1
        assert_eq!(result.len(), 3);
993
1
        assert!(matches!(
994
1
            result.as_slice()[0],
995
            StyleTransform::TranslateX(_)
996
        ));
997
1
        assert!(matches!(result.as_slice()[1], StyleTransform::Rotate(_)));
998
1
        assert!(matches!(result.as_slice()[2], StyleTransform::Scale(_)));
999
1
    }
    #[test]
1
    fn test_parse_transform_functions() {
        // Translate
1
        assert_eq!(
1
            parse_style_transform("translateX(50%)").unwrap(),
1
            StyleTransform::TranslateX(PixelValue::percent(50.0))
        );
1
        let translate = parse_style_transform("translate(10px, -20px)").unwrap();
1
        if let StyleTransform::Translate(t) = translate {
1
            assert_eq!(t.x, PixelValue::px(10.0));
1
            assert_eq!(t.y, PixelValue::px(-20.0));
        } else {
            panic!("Expected Translate");
        }
        // Scale
1
        assert_eq!(
1
            parse_style_transform("scaleY(1.2)").unwrap(),
1
            StyleTransform::ScaleY(PercentageValue::new(120.0))
        );
1
        let scale = parse_style_transform("scale(2, 0.5)").unwrap();
1
        if let StyleTransform::Scale(s) = scale {
1
            assert_eq!(s.x.get(), 2.0);
1
            assert_eq!(s.y.get(), 0.5);
        } else {
            panic!("Expected Scale");
        }
        // Rotate
1
        assert_eq!(
1
            parse_style_transform("rotate(0.25turn)").unwrap(),
1
            StyleTransform::Rotate(AngleValue::turn(0.25))
        );
        // Skew
1
        assert_eq!(
1
            parse_style_transform("skewX(-10deg)").unwrap(),
1
            StyleTransform::SkewX(AngleValue::deg(-10.0))
        );
1
        let skew = parse_style_transform("skew(20deg, 30deg)").unwrap();
1
        if let StyleTransform::Skew(s) = skew {
1
            assert_eq!(s.x, AngleValue::deg(20.0));
1
            assert_eq!(s.y, AngleValue::deg(30.0));
        } else {
            panic!("Expected Skew");
        }
1
    }
    #[test]
1
    fn test_parse_transform_origin() {
1
        let result = parse_style_transform_origin("50% 50%").unwrap();
1
        assert_eq!(result.x, PixelValue::percent(50.0));
1
        assert_eq!(result.y, PixelValue::percent(50.0));
1
        let result = parse_style_transform_origin("left top").unwrap();
1
        assert_eq!(result.x, PixelValue::percent(0.0));
1
        assert_eq!(result.y, PixelValue::percent(0.0));
1
        let result = parse_style_transform_origin("20px bottom").unwrap();
1
        assert_eq!(result.x, PixelValue::px(20.0));
1
        assert_eq!(result.y, PixelValue::percent(100.0));
1
    }
    #[test]
1
    fn test_parse_backface_visibility() {
1
        assert_eq!(
1
            parse_style_backface_visibility("visible").unwrap(),
            StyleBackfaceVisibility::Visible
        );
1
        assert_eq!(
1
            parse_style_backface_visibility("hidden").unwrap(),
            StyleBackfaceVisibility::Hidden
        );
1
        assert!(parse_style_backface_visibility("none").is_err());
1
    }
    #[test]
1
    fn test_parse_transform_errors() {
        // Wrong function name
1
        assert!(parse_style_transform("translatex(10px)").is_err());
        // Wrong number of args
1
        assert!(parse_style_transform("translate(1, 2, 3)").is_err());
        // Single-arg forms (CSS spec compliant)
1
        let scale1 = parse_style_transform("scale(2)").unwrap();
1
        if let StyleTransform::Scale(s) = scale1 {
1
            assert_eq!(s.x.get(), 2.0);
1
            assert_eq!(s.y.get(), 2.0);
        } else {
            panic!("Expected Scale");
        }
1
        let translate1 = parse_style_transform("translate(10px)").unwrap();
1
        if let StyleTransform::Translate(t) = translate1 {
1
            assert_eq!(t.x, PixelValue::px(10.0));
1
            assert_eq!(t.y, PixelValue::px(0.0));
        } else {
            panic!("Expected Translate");
        }
1
        let skew1 = parse_style_transform("skew(20deg)").unwrap();
1
        if let StyleTransform::Skew(s) = skew1 {
1
            assert_eq!(s.x, AngleValue::deg(20.0));
1
            assert_eq!(s.y, AngleValue::deg(0.0));
        } else {
            panic!("Expected Skew");
        }
        // rotate3d with angle unit
1
        let rot3d = parse_style_transform("rotate3d(1, 0, 0, 45deg)").unwrap();
1
        if let StyleTransform::Rotate3D(r) = rot3d {
1
            assert_eq!(r.x.get(), 1.0);
1
            assert_eq!(r.angle, AngleValue::deg(45.0));
        } else {
            panic!("Expected Rotate3D");
        }
        // Invalid value
1
        assert!(parse_style_transform("rotate(10px)").is_err());
1
        assert!(parse_style_transform("translateX(auto)").is_err());
1
    }
}
#[cfg(all(test, feature = "parser"))]
#[allow(clippy::too_many_lines, clippy::float_cmp)]
mod autotest_generated {
    // Tests compare parsed values against exact source literals, and deliberately
    // feed NaN/inf through the numeric encoders.
    use super::*;
    use crate::props::basic::length::SizeMetric;
    // ---------------------------------------------------------------------
    // helpers
    // ---------------------------------------------------------------------
    /// Every `FloatValue` is stored as an `isize`, so `get()` can never be
    /// NaN/inf no matter what went in. Used as a blanket invariant below.
    fn assert_encodable(pv: PixelValue) {
        assert!(pv.number.get().is_finite());
    }
    fn all_roundtrippable_transforms() -> Vec<StyleTransform> {
        vec![
            StyleTransform::Matrix(StyleTransformMatrix2D::default()),
            StyleTransform::Matrix3D(StyleTransformMatrix3D::default()),
            StyleTransform::Translate(StyleTransformTranslate2D {
                x: PixelValue::px(10.0),
                y: PixelValue::px(-20.0),
            }),
            StyleTransform::Translate3D(StyleTransformTranslate3D {
                x: PixelValue::px(1.0),
                y: PixelValue::em(2.0),
                z: PixelValue::pt(-3.5),
            }),
            StyleTransform::TranslateX(PixelValue::percent(50.0)),
            StyleTransform::TranslateY(PixelValue::px(0.0)),
            StyleTransform::TranslateZ(PixelValue::rem(1.25)),
            StyleTransform::Rotate(AngleValue::deg(90.0)),
            StyleTransform::Rotate3D(StyleTransformRotate3D {
                x: FloatValue::new(1.0),
                y: FloatValue::new(0.0),
                z: FloatValue::new(0.0),
                angle: AngleValue::turn(0.25),
            }),
            StyleTransform::RotateX(AngleValue::rad(1.5)),
            StyleTransform::RotateY(AngleValue::grad(100.0)),
            StyleTransform::RotateZ(AngleValue::deg(-45.0)),
            StyleTransform::Scale(StyleTransformScale2D {
                x: FloatValue::new(2.0),
                y: FloatValue::new(0.5),
            }),
            StyleTransform::Scale3D(StyleTransformScale3D {
                x: FloatValue::new(1.0),
                y: FloatValue::new(-1.0),
                z: FloatValue::new(0.25),
            }),
            StyleTransform::Skew(StyleTransformSkew2D {
                x: AngleValue::deg(20.0),
                y: AngleValue::deg(30.0),
            }),
            StyleTransform::SkewX(AngleValue::deg(-10.0)),
            StyleTransform::SkewY(AngleValue::deg(10.0)),
            StyleTransform::Perspective(PixelValue::px(500.0)),
        ]
    }
    // =====================================================================
    // parse_style_transform  --  malformed / boundary / unicode
    // =====================================================================
    #[test]
    fn transform_rejects_empty_and_whitespace_only_input() {
        for input in ["", "   ", "\t\n", "\r\n\t "] {
            let err = parse_style_transform(input).unwrap_err();
            assert!(
                matches!(
                    err,
                    CssStyleTransformParseError::InvalidParenthesis(
                        ParenthesisParseError::EmptyInput
                    )
                ),
                "expected EmptyInput for {input:?}, got {err}"
            );
        }
    }
    #[test]
    fn transform_rejects_garbage_without_panicking() {
        // No opening brace at all.
        assert!(matches!(
            parse_style_transform("garbage").unwrap_err(),
            CssStyleTransformParseError::InvalidParenthesis(
                ParenthesisParseError::NoOpeningBraceFound
            )
        ));
        // Opening brace, no closing brace.
        assert!(matches!(
            parse_style_transform("rotate(90deg").unwrap_err(),
            CssStyleTransformParseError::InvalidParenthesis(
                ParenthesisParseError::NoClosingBraceFound
            )
        ));
        // Known-but-miscased function name is NOT accepted (CSS is case-insensitive
        // for function names; azul's stopword table is case-sensitive).
        assert!(matches!(
            parse_style_transform("translatex(10px)").unwrap_err(),
            CssStyleTransformParseError::InvalidParenthesis(
                ParenthesisParseError::StopWordNotFound("translatex")
            )
        ));
        assert!(matches!(
            parse_style_transform("ROTATE(90deg)").unwrap_err(),
            CssStyleTransformParseError::InvalidParenthesis(
                ParenthesisParseError::StopWordNotFound("ROTATE")
            )
        ));
        // Random byte soup, none of which forms a grammar.
        for input in [
            "((((",
            ")",
            "()",
            "(rotate)",
            "rotate",
            ";;;",
            "\0(\0)",
            "-1",
            "rotate(,,,,)",
            "matrix(,)",
        ] {
            assert!(
                parse_style_transform(input).is_err(),
                "expected Err for {input:?}"
            );
        }
    }
    #[test]
    fn transform_never_hits_the_unreachable_arm_for_near_miss_stopwords() {
        // parse_style_transform ends in `_ => unreachable!()`; it is only sound as
        // long as parse_parentheses can never hand back a non-listed stopword.
        // Probe names that are prefixes/suffixes/case-variants of real ones.
        for name in [
            "translat", "translateXX", "xtranslateX", "rotate3", "rotate3D", "scale4d", "skewZ",
            "perspectives", "matrix2d", "MATRIX", "", " rotate",
        ] {
            let input = alloc::format!("{name}(1)");
            let res = parse_style_transform(&input);
            // Either a clean parse error or (for " rotate", which trims to "rotate")
            // a normal result - but never a panic.
            let _ = res;
        }
        // " rotate(1)" trims down to a valid rotate with a bare-number degree.
        assert_eq!(
            parse_style_transform("  rotate(1)  ").unwrap(),
            StyleTransform::Rotate(AngleValue::deg(1.0))
        );
    }
    #[test]
    fn transform_handles_non_ascii_and_multibyte_input() {
        // parse_parentheses slices `input[..first_open_brace]`; a multibyte char
        // right before the '(' must not split a UTF-8 boundary.
        for input in [
            "\u{1F600}",
            "\u{1F600}(1)",
            "rotate(\u{1F600})",
            "rotate\u{0301}(1deg)",
            "\u{1F600}rotate(1deg)",
            "translateX(\u{1F600}px)",
            "translate(\u{1F600}, \u{1F600})",
            "matrix3d(\u{4F60}\u{597D})",
            "sk\u{0435}wX(10deg)", // cyrillic 'е' homoglyph
        ] {
            assert!(
                parse_style_transform(input).is_err(),
                "expected Err for {input:?}"
            );
        }
        assert!(matches!(
            parse_style_transform("rotate(\u{1F600})").unwrap_err(),
            CssStyleTransformParseError::AngleValueParseError(
                CssAngleValueParseError::InvalidAngle("\u{1F600}")
            )
        ));
    }
    #[test]
    fn transform_boundary_numbers_saturate_instead_of_panicking() {
        // -0 collapses to +0 in the isize-backed encoding.
        assert_eq!(
            parse_style_transform("translateX(-0)").unwrap(),
            StyleTransform::TranslateX(PixelValue::px(0.0))
        );
        assert_eq!(
            parse_style_transform("translateX(0)").unwrap(),
            StyleTransform::TranslateX(PixelValue::px(0.0))
        );
        // NaN parses as a float (Rust accepts "NaN"), and the f32 -> isize cast
        // maps NaN to 0. So `translateX(NaN)` silently becomes `0px`.
        let StyleTransform::TranslateX(nan_px) = parse_style_transform("translateX(NaN)").unwrap()
        else {
            panic!("expected TranslateX");
        };
        assert_eq!(nan_px.number.number(), 0);
        assert_encodable(nan_px);
        // Infinities (literal, and via decimal overflow) saturate to isize::MAX/MIN.
        for input in ["translateX(inf)", "translateX(1e400)", "translateX(1e400px)"] {
            let StyleTransform::TranslateX(px) = parse_style_transform(input).unwrap() else {
                panic!("expected TranslateX for {input}");
            };
            assert_eq!(px.number.number(), isize::MAX, "{input}");
            assert_encodable(px);
        }
        let StyleTransform::TranslateX(neg) = parse_style_transform("translateX(-inf)").unwrap()
        else {
            panic!("expected TranslateX");
        };
        assert_eq!(neg.number.number(), isize::MIN);
        assert_encodable(neg);
        // i64::MAX / f64-scale magnitudes: fine, just saturated.
        for input in [
            "translateX(9223372036854775807px)",
            "translateX(-9223372036854775808px)",
            "translateX(1e-400px)",
            "translateX(0.0000000000001px)",
        ] {
            assert!(parse_style_transform(input).is_ok(), "{input}");
        }
        // Angles take the same path: NaN -> 0deg, inf -> saturated.
        assert_eq!(
            parse_style_transform("rotate(NaN)").unwrap(),
            StyleTransform::Rotate(AngleValue::deg(0.0))
        );
        let StyleTransform::Rotate(a) = parse_style_transform("rotate(infdeg)").unwrap() else {
            panic!("expected Rotate");
        };
        assert_eq!(a.number.number(), isize::MAX);
        // scaleX multiplies by 100 before encoding - inf * 100 must not trap.
        let StyleTransform::ScaleX(p) = parse_style_transform("scaleX(NaN)").unwrap() else {
            panic!("expected ScaleX");
        };
        assert_eq!(p, PercentageValue::new(0.0));
        assert!(parse_style_transform("scaleX(inf)").is_ok());
        assert!(parse_style_transform("scaleX(1e40)").is_ok());
    }
    #[test]
    fn transform_component_counts_are_enforced() {
        // matrix wants exactly 6.
        assert!(matches!(
            parse_style_transform("matrix(1,2,3,4,5)").unwrap_err(),
            CssStyleTransformParseError::WrongNumberOfComponents {
                expected: 6,
                got: 5,
                ..
            }
        ));
        assert!(matches!(
            parse_style_transform("matrix(1,2,3,4,5,6,7)").unwrap_err(),
            CssStyleTransformParseError::WrongNumberOfComponents {
                expected: 6,
                got: 7,
                ..
            }
        ));
        // matrix3d wants exactly 16.
        assert!(matches!(
            parse_style_transform("matrix3d(1,0,0,0,0,1,0,0,0,0,1,0,0,0,0)").unwrap_err(),
            CssStyleTransformParseError::WrongNumberOfComponents {
                expected: 16,
                got: 15,
                ..
            }
        ));
        // translate takes at most 2.
        assert!(matches!(
            parse_style_transform("translate(1px, 2px, 3px)").unwrap_err(),
            CssStyleTransformParseError::WrongNumberOfComponents {
                expected: 2,
                got: 3,
                ..
            }
        ));
        // scale takes at most 2.
        assert!(matches!(
            parse_style_transform("scale(1, 2, 3)").unwrap_err(),
            CssStyleTransformParseError::WrongNumberOfComponents {
                expected: 2,
                got: 3,
                ..
            }
        ));
        // skew takes at most 2.
        assert!(matches!(
            parse_style_transform("skew(1deg, 2deg, 3deg)").unwrap_err(),
            CssStyleTransformParseError::WrongNumberOfComponents {
                expected: 2,
                got: 3,
                ..
            }
        ));
        // rotate3d wants exactly 4 (splitn(4) makes >4 fold into the angle, which
        // then fails to parse as an angle).
        assert!(matches!(
            parse_style_transform("rotate3d(1, 0, 0)").unwrap_err(),
            CssStyleTransformParseError::WrongNumberOfComponents {
                expected: 4,
                got: 3,
                ..
            }
        ));
        assert!(parse_style_transform("rotate3d(1, 0, 0, 45deg, 99)").is_err());
        // translate3d wants exactly 3 when short...
        assert!(matches!(
            parse_style_transform("translate3d(1px, 2px)").unwrap_err(),
            CssStyleTransformParseError::WrongNumberOfComponents {
                expected: 3,
                got: 2,
                ..
            }
        ));
        // scale3d wants exactly 3.
        assert!(matches!(
            parse_style_transform("scale3d(1, 2)").unwrap_err(),
            CssStyleTransformParseError::WrongNumberOfComponents {
                expected: 3,
                got: 2,
                ..
            }
        ));
    }
    #[test]
    fn transform_translate3d_silently_ignores_extra_components() {
        // BUG (leniency): unlike matrix/scale3d (which go through get_numbers and
        // check the count), translate3d only indexes [0], [1], [2] and never
        // rejects a 4th+ component. Per CSS this must be a parse error.
        // Pinned as current behaviour so a future fix shows up as a diff here.
        let parsed = parse_style_transform("translate3d(1px, 2px, 3px, 4px, 5px)").unwrap();
        assert_eq!(
            parsed,
            StyleTransform::Translate3D(StyleTransformTranslate3D {
                x: PixelValue::px(1.0),
                y: PixelValue::px(2.0),
                z: PixelValue::px(3.0),
            })
        );
    }
    #[test]
    fn transform_ignores_junk_after_the_closing_paren() {
        // BUG (leniency): parse_parentheses uses find('(') .. rfind(')'), so any
        // trailing junk that contains no ')' is silently dropped. Per CSS,
        // "rotate(90deg)garbage" is invalid. Pinned as current behaviour.
        assert_eq!(
            parse_style_transform("rotate(90deg)garbage").unwrap(),
            StyleTransform::Rotate(AngleValue::deg(90.0))
        );
        assert_eq!(
            parse_style_transform("rotate(90deg) ;drop table").unwrap(),
            StyleTransform::Rotate(AngleValue::deg(90.0))
        );
        // ...but junk containing a ')' gets swallowed INTO the argument, which then
        // fails - so the leniency is content-dependent, not a clean "trim" rule.
        assert!(parse_style_transform("rotate(90deg))").is_err());
        assert!(parse_style_transform("rotate(90deg) rotate(1deg)").is_err());
    }
    #[test]
    fn transform_empty_argument_lists_are_rejected() {
        for input in [
            "matrix()",
            "matrix3d()",
            "translate()",
            "translate3d()",
            "translateX()",
            "translateY()",
            "translateZ()",
            "rotate()",
            "rotate3d()",
            "rotateX()",
            "rotateY()",
            "rotateZ()",
            "scale()",
            "scale3d()",
            "scaleX()",
            "scaleY()",
            "scaleZ()",
            "skew()",
            "skewX()",
            "skewY()",
            "perspective()",
        ] {
            assert!(
                parse_style_transform(input).is_err(),
                "expected Err for {input:?}"
            );
        }
        // Trailing-comma forms, too.
        for input in [
            "translate(10px,)",
            "translate3d(1px,2px,)",
            "scale(2,)",
            "skew(10deg,)",
            "matrix(1,2,3,4,5,)",
        ] {
            assert!(
                parse_style_transform(input).is_err(),
                "expected Err for {input:?}"
            );
        }
    }
    #[test]
    fn transform_wrong_unit_kinds_are_rejected() {
        // A length where an angle is expected, and vice versa.
        assert!(parse_style_transform("rotate(10px)").is_err());
        assert!(parse_style_transform("rotateX(10px)").is_err());
        assert!(parse_style_transform("skewY(10px)").is_err());
        assert!(parse_style_transform("translateX(10deg)").is_err());
        assert!(parse_style_transform("perspective(10deg)").is_err());
        // Keywords are not lengths.
        assert!(parse_style_transform("translateX(auto)").is_err());
        assert!(parse_style_transform("translateX(none)").is_err());
        // scaleX takes a bare number, NOT a percentage (see the round-trip test).
        assert!(parse_style_transform("scaleX(120%)").is_err());
    }
    #[test]
    fn transform_extremely_long_input_does_not_hang_or_panic() {
        // ~100k-digit number: must scan in linear time and saturate, not panic.
        let huge = alloc::format!("translateX({}px)", "9".repeat(100_000));
        let StyleTransform::TranslateX(px) = parse_style_transform(&huge).unwrap() else {
            panic!("expected TranslateX");
        };
        assert_eq!(px.number.number(), isize::MAX);
        // Long garbage of the same size must simply be an error.
        let junk = alloc::format!("translateX({})", "a".repeat(100_000));
        assert!(parse_style_transform(&junk).is_err());
        // Long stopword: no quadratic blowup in the stopword scan.
        let long_name = alloc::format!("{}(1px)", "x".repeat(100_000));
        assert!(parse_style_transform(&long_name).is_err());
    }
    #[test]
    fn transform_deeply_nested_parens_do_not_stack_overflow() {
        // parse_parentheses is iterative; prove there is no recursion by feeding it
        // 10k levels of nesting.
        let open_only = "rotate(".repeat(10_000);
        assert!(matches!(
            parse_style_transform(&open_only).unwrap_err(),
            CssStyleTransformParseError::InvalidParenthesis(
                ParenthesisParseError::NoClosingBraceFound
            )
        ));
        let braces = "(".repeat(10_000);
        assert!(matches!(
            parse_style_transform(&braces).unwrap_err(),
            CssStyleTransformParseError::InvalidParenthesis(
                ParenthesisParseError::StopWordNotFound("")
            )
        ));
        let balanced = alloc::format!(
            "translateX({}1px{})",
            "translateX(".repeat(1_000),
            ")".repeat(1_000)
        );
        assert!(parse_style_transform(&balanced).is_err());
        let closers = ")".repeat(10_000);
        assert!(parse_style_transform(&closers).is_err());
    }
    #[test]
    fn transform_valid_minimal_positive_control() {
        assert_eq!(
            parse_style_transform("rotate(0deg)").unwrap(),
            StyleTransform::Rotate(AngleValue::deg(0.0))
        );
    }
    // =====================================================================
    // parse_style_transform_vec
    // =====================================================================
    #[test]
    fn transform_vec_accepts_empty_and_whitespace_only_input_as_an_empty_list() {
        // NOTE: "" is NOT an error - split_string_respect_whitespace yields zero
        // tokens and the collect() succeeds with an empty Vec. Callers relying on
        // `parse_style_transform_vec("").is_err()` to reject an empty declaration
        // will not get one. Pinned as current behaviour.
        for input in ["", "   ", "\t\n", "\r \n \t"] {
            let v = parse_style_transform_vec(input).unwrap();
            assert_eq!(v.len(), 0, "{input:?}");
        }
    }
    #[test]
    fn transform_vec_propagates_the_first_error() {
        assert!(parse_style_transform_vec("translateX(10px) garbage").is_err());
        assert!(parse_style_transform_vec("garbage translateX(10px)").is_err());
        assert!(parse_style_transform_vec("translateX(10px) rotate(10px)").is_err());
        assert!(parse_style_transform_vec("\u{1F600}").is_err());
    }
    #[test]
    fn transform_vec_keeps_whitespace_inside_parens_together() {
        // "scale(2, 0.5)" contains a space at depth 1 - it must stay one token.
        let v = parse_style_transform_vec("scale(2, 0.5) matrix(1, 0, 0, 1, 0, 0)").unwrap();
        assert_eq!(v.len(), 2);
        assert!(matches!(v.as_slice()[0], StyleTransform::Scale(_)));
        assert!(matches!(v.as_slice()[1], StyleTransform::Matrix(_)));
        // Repeated / redundant whitespace collapses.
        let v = parse_style_transform_vec("  translateX(1px)\t\trotate(2deg)\n ").unwrap();
        assert_eq!(v.len(), 2);
    }
    #[test]
    fn transform_vec_extremely_long_list_does_not_hang() {
        let long = "translateX(1px) ".repeat(20_000);
        let v = parse_style_transform_vec(&long).unwrap();
        assert_eq!(v.len(), 20_000);
        for t in v.as_slice() {
            assert_eq!(*t, StyleTransform::TranslateX(PixelValue::px(1.0)));
        }
    }
    #[test]
    fn transform_vec_unbalanced_parens_do_not_underflow_the_depth_counter() {
        // split_string_respect_whitespace does `depth -= 1` on every ')' with no
        // floor; a run of closers drives it negative. Must not panic in debug.
        let closers = ")".repeat(10_000);
        assert!(parse_style_transform_vec(&closers).is_err());
        let mixed = alloc::format!("{} {}", ")".repeat(5_000), "(".repeat(5_000));
        assert!(parse_style_transform_vec(&mixed).is_err());
    }
    // =====================================================================
    // parse_style_transform_origin
    // =====================================================================
    #[test]
    fn transform_origin_requires_exactly_two_components() {
        for (input, got) in [("", 0), ("50%", 1), ("left", 1), ("50% 50% 50%", 3)] {
            let err = parse_style_transform_origin(input).unwrap_err();
            assert!(
                matches!(
                    err,
                    CssStyleTransformOriginParseError::WrongNumberOfComponents {
                        expected: 2,
                        got: g,
                        ..
                    } if g == got
                ),
                "{input:?} -> {err}"
            );
        }
        // Whitespace-only collapses to zero components.
        assert!(matches!(
            parse_style_transform_origin("   \t\n ").unwrap_err(),
            CssStyleTransformOriginParseError::WrongNumberOfComponents { got: 0, .. }
        ));
    }
    #[test]
    fn transform_origin_keywords_are_position_sensitive() {
        assert_eq!(
            parse_style_transform_origin("left top").unwrap(),
            StyleTransformOrigin {
                x: PixelValue::percent(0.0),
                y: PixelValue::percent(0.0),
            }
        );
        assert_eq!(
            parse_style_transform_origin("right bottom").unwrap(),
            StyleTransformOrigin {
                x: PixelValue::percent(100.0),
                y: PixelValue::percent(100.0),
            }
        );
        assert_eq!(
            parse_style_transform_origin("center center").unwrap(),
            StyleTransformOrigin::default()
        );
        // BUG (spec deviation): CSS allows the keywords in either order
        // ("top left" == "left top"). Here the horizontal slot rejects
        // "top"/"bottom" and the vertical slot rejects "left"/"right", so the
        // swapped form is an error. Pinned as current behaviour.
        assert!(parse_style_transform_origin("top left").is_err());
        assert!(parse_style_transform_origin("bottom right").is_err());
        assert!(parse_style_transform_origin("left left").is_err());
        assert!(parse_style_transform_origin("top top").is_err());
    }
    #[test]
    fn transform_origin_garbage_and_unicode_do_not_panic() {
        for input in [
            "\u{1F600} \u{1F600}",
            "left \u{1F600}",
            "NaN NaN",
            "auto auto",
            "-- --",
            "10 20",   // bare numbers -> px, actually valid
            "1px;2px", // no whitespace -> 1 component
        ] {
            let _ = parse_style_transform_origin(input);
        }
        // Bare numbers fall through to parse_pixel_value's px default.
        assert_eq!(
            parse_style_transform_origin("10 20").unwrap(),
            StyleTransformOrigin {
                x: PixelValue::px(10.0),
                y: PixelValue::px(20.0),
            }
        );
        assert!(matches!(
            parse_style_transform_origin("\u{1F600} \u{1F600}").unwrap_err(),
            CssStyleTransformOriginParseError::PixelValueParseError(_)
        ));
    }
    #[test]
    fn transform_origin_boundary_numbers_saturate() {
        let o = parse_style_transform_origin("inf% -inf%").unwrap();
        assert_eq!(o.x.number.number(), isize::MAX);
        assert_eq!(o.y.number.number(), isize::MIN);
        assert_encodable(o.x);
        assert_encodable(o.y);
        // NaN -> 0, keeping the metric.
        let o = parse_style_transform_origin("NaNpx NaN%").unwrap();
        assert_eq!(o.x, PixelValue::px(0.0));
        assert_eq!(o.y, PixelValue::percent(0.0));
        let o = parse_style_transform_origin("-0px 1e400px").unwrap();
        assert_eq!(o.x.number.number(), 0);
        assert_eq!(o.y.number.number(), isize::MAX);
    }
    #[test]
    fn transform_origin_extremely_long_input_does_not_hang() {
        let many = "50% ".repeat(20_000);
        assert!(matches!(
            parse_style_transform_origin(&many).unwrap_err(),
            CssStyleTransformOriginParseError::WrongNumberOfComponents { got: 20_000, .. }
        ));
        let huge = alloc::format!("{}px 0px", "9".repeat(100_000));
        assert!(parse_style_transform_origin(&huge).is_ok());
    }
    #[test]
    fn transform_origin_round_trips_through_its_css_repr() {
        for origin in [
            StyleTransformOrigin::default(),
            StyleTransformOrigin {
                x: PixelValue::px(20.0),
                y: PixelValue::percent(100.0),
            },
            StyleTransformOrigin {
                x: PixelValue::em(-1.5),
                y: PixelValue::rem(2.25),
            },
            StyleTransformOrigin {
                x: PixelValue::px(0.0),
                y: PixelValue::px(0.0),
            },
        ] {
            let css = origin.print_as_css_value();
            let reparsed = parse_style_transform_origin(&css)
                .unwrap_or_else(|e| panic!("{css:?} did not re-parse: {e}"));
            assert_eq!(reparsed, origin, "round-trip failed for {css:?}");
        }
    }
    // =====================================================================
    // parse_style_perspective_origin
    // =====================================================================
    #[test]
    fn perspective_origin_requires_exactly_two_components() {
        for (input, got) in [("", 0), ("50%", 1), ("1px 2px 3px", 3)] {
            let err = parse_style_perspective_origin(input).unwrap_err();
            assert!(
                matches!(
                    err,
                    CssStylePerspectiveOriginParseError::WrongNumberOfComponents {
                        expected: 2,
                        got: g,
                        ..
                    } if g == got
                ),
                "{input:?} -> {err}"
            );
        }
    }
    #[test]
    fn perspective_origin_does_not_accept_position_keywords() {
        // BUG (spec deviation): CSS `perspective-origin` accepts the same
        // left/center/right/top/bottom keywords as `transform-origin`, but this
        // parser only takes pixel values. Pinned as current behaviour.
        assert!(parse_style_perspective_origin("left top").is_err());
        assert!(parse_style_perspective_origin("center center").is_err());
        assert!(matches!(
            parse_style_perspective_origin("center center").unwrap_err(),
            CssStylePerspectiveOriginParseError::PixelValueParseError(_)
        ));
    }
    #[test]
    fn perspective_origin_garbage_boundary_and_unicode() {
        for input in ["\u{1F600} \u{1F600}", "auto auto", "-- --", "px px"] {
            assert!(
                parse_style_perspective_origin(input).is_err(),
                "expected Err for {input:?}"
            );
        }
        let o = parse_style_perspective_origin("inf -inf").unwrap();
        assert_eq!(o.x.number.number(), isize::MAX);
        assert_eq!(o.y.number.number(), isize::MIN);
        assert_encodable(o.x);
        assert_encodable(o.y);
        let o = parse_style_perspective_origin("NaN -0").unwrap();
        assert_eq!(o.x, PixelValue::px(0.0));
        assert_eq!(o.y, PixelValue::px(0.0));
        let huge = alloc::format!("{}px 0px", "9".repeat(100_000));
        assert!(parse_style_perspective_origin(&huge).is_ok());
    }
    #[test]
    fn perspective_origin_round_trips_through_its_css_repr() {
        for origin in [
            StylePerspectiveOrigin::default(),
            StylePerspectiveOrigin {
                x: PixelValue::px(100.0),
                y: PixelValue::percent(50.0),
            },
            StylePerspectiveOrigin {
                x: PixelValue::pt(-3.5),
                y: PixelValue::cm(1.0),
            },
        ] {
            let css = origin.print_as_css_value();
            let reparsed = parse_style_perspective_origin(&css)
                .unwrap_or_else(|e| panic!("{css:?} did not re-parse: {e}"));
            assert_eq!(reparsed, origin, "round-trip failed for {css:?}");
        }
    }
    // =====================================================================
    // parse_style_backface_visibility
    // =====================================================================
    #[test]
    fn backface_visibility_accepts_only_the_two_keywords() {
        assert_eq!(
            parse_style_backface_visibility("visible").unwrap(),
            StyleBackfaceVisibility::Visible
        );
        assert_eq!(
            parse_style_backface_visibility("hidden").unwrap(),
            StyleBackfaceVisibility::Hidden
        );
        // Surrounding whitespace is trimmed.
        assert_eq!(
            parse_style_backface_visibility("  \t visible \n ").unwrap(),
            StyleBackfaceVisibility::Visible
        );
        // Everything else is rejected - including case variants, substrings,
        // both keywords at once and zero-width joiners.
        for input in [
            "",
            "   ",
            "Visible",
            "HIDDEN",
            "visible hidden",
            "vis",
            "visiblee",
            "none",
            "0",
            "NaN",
            "\u{1F600}",
            "visible\u{200B}",
            "hidden;",
        ] {
            assert!(
                parse_style_backface_visibility(input).is_err(),
                "expected Err for {input:?}"
            );
        }
    }
    #[test]
    fn backface_visibility_error_carries_the_untrimmed_input() {
        // The match is on `input.trim()` but the error is built from `input`,
        // so the original (untrimmed) slice is what shows up in the message.
        let err = parse_style_backface_visibility("  bogus  ").unwrap_err();
        assert_eq!(err, CssBackfaceVisibilityParseError::InvalidValue("  bogus  "));
        assert!(alloc::format!("{err}").contains("  bogus  "));
    }
    #[test]
    fn backface_visibility_extremely_long_input_does_not_hang() {
        let huge = "visible".repeat(100_000);
        assert!(parse_style_backface_visibility(&huge).is_err());
    }
    #[test]
    fn backface_visibility_round_trips_through_its_css_repr() {
        for v in [
            StyleBackfaceVisibility::Visible,
            StyleBackfaceVisibility::Hidden,
        ] {
            let css = v.print_as_css_value();
            assert_eq!(parse_style_backface_visibility(&css).unwrap(), v);
        }
        assert_eq!(
            StyleBackfaceVisibility::default(),
            StyleBackfaceVisibility::Visible
        );
    }
    // =====================================================================
    // StyleTransform / StyleTransformVec round-trips (encode == decode)
    // =====================================================================
    #[test]
    fn transform_print_parse_round_trip() {
        for t in all_roundtrippable_transforms() {
            let css = t.print_as_css_value();
            let reparsed = parse_style_transform(&css)
                .unwrap_or_else(|e| panic!("{css:?} did not re-parse: {e}"));
            assert_eq!(reparsed, t, "round-trip failed for {css:?}");
        }
    }
    #[test]
    fn transform_vec_print_parse_round_trip() {
        let v: StyleTransformVec = all_roundtrippable_transforms().into();
        let css = v.print_as_css_value();
        let reparsed = parse_style_transform_vec(&css)
            .unwrap_or_else(|e| panic!("{css:?} did not re-parse: {e}"));
        assert_eq!(reparsed.len(), v.len());
        assert_eq!(reparsed.as_slice(), v.as_slice());
    }
    #[test]
    fn scale_axis_print_does_not_round_trip() {
        // BUG: `StyleTransform::ScaleX/Y/Z` hold a `PercentageValue`, whose Display
        // appends a '%' ("scaleX(120%)"), but the parser reads the argument with a
        // bare `parse::<f32>()` and multiplies by 100. So print -> parse fails for
        // every ScaleX/ScaleY/ScaleZ, and the printed CSS is invalid per spec
        // (`scaleX()` takes a <number>, not a <percentage>).
        // Pinned as current behaviour; the fix is to print the raw number.
        for t in [
            StyleTransform::ScaleX(PercentageValue::new(120.0)),
            StyleTransform::ScaleY(PercentageValue::new(120.0)),
            StyleTransform::ScaleZ(PercentageValue::new(120.0)),
        ] {
            let css = t.print_as_css_value();
            assert!(css.contains('%'), "{css:?}");
            assert!(
                parse_style_transform(&css).is_err(),
                "{css:?} unexpectedly re-parsed - the ScaleX round-trip bug may be fixed"
            );
        }
        // The parser's own accepted form (a bare number) does work.
        assert_eq!(
            parse_style_transform("scaleX(1.2)").unwrap(),
            StyleTransform::ScaleX(PercentageValue::new(120.0))
        );
    }
    // =====================================================================
    // StyleTransformOrigin::interpolate / StylePerspectiveOrigin::interpolate
    // =====================================================================
    #[test]
    fn transform_origin_interpolate_endpoints_are_exact() {
        let a = StyleTransformOrigin {
            x: PixelValue::px(10.0),
            y: PixelValue::percent(0.0),
        };
        let b = StyleTransformOrigin {
            x: PixelValue::px(30.0),
            y: PixelValue::percent(100.0),
        };
        assert_eq!(a.interpolate(&b, 0.0), a);
        assert_eq!(a.interpolate(&b, 1.0), b);
        assert_eq!(
            a.interpolate(&b, 0.5),
            StyleTransformOrigin {
                x: PixelValue::px(20.0),
                y: PixelValue::percent(50.0),
            }
        );
        // Interpolating a value with itself is the identity for every finite t.
        for t in [-1.0, 0.0, 0.25, 1.0, 2.0, 1e30] {
            assert_eq!(a.interpolate(&a, t), a, "t = {t}");
        }
    }
    #[test]
    fn transform_origin_interpolate_extrapolates_outside_zero_one() {
        let a = StyleTransformOrigin {
            x: PixelValue::px(10.0),
            y: PixelValue::px(10.0),
        };
        let b = StyleTransformOrigin {
            x: PixelValue::px(30.0),
            y: PixelValue::px(30.0),
        };
        // t is NOT clamped.
        assert_eq!(a.interpolate(&b, -1.0).x, PixelValue::px(-10.0));
        assert_eq!(a.interpolate(&b, 2.0).x, PixelValue::px(50.0));
    }
    #[test]
    fn transform_origin_interpolate_with_nan_or_infinite_t_stays_defined() {
        let a = StyleTransformOrigin {
            x: PixelValue::px(10.0),
            y: PixelValue::percent(10.0),
        };
        let b = StyleTransformOrigin {
            x: PixelValue::px(30.0),
            y: PixelValue::percent(30.0),
        };
        // NaN t -> NaN value -> the f32->isize cast maps NaN to 0.
        let nan = a.interpolate(&b, f32::NAN);
        assert_eq!(nan.x.number.number(), 0);
        assert_eq!(nan.y.number.number(), 0);
        assert_eq!(nan.x.metric, SizeMetric::Px);
        assert_eq!(nan.y.metric, SizeMetric::Percent);
        assert_encodable(nan.x);
        assert_encodable(nan.y);
        // +inf t on an increasing range saturates to isize::MAX, -inf to isize::MIN.
        let pos = a.interpolate(&b, f32::INFINITY);
        assert_eq!(pos.x.number.number(), isize::MAX);
        assert_encodable(pos.x);
        let neg = a.interpolate(&b, f32::NEG_INFINITY);
        assert_eq!(neg.x.number.number(), isize::MIN);
        assert_encodable(neg.x);
        // inf * 0 (identical endpoints) is NaN, which collapses to 0.
        let degenerate = a.interpolate(&a, f32::INFINITY);
        assert_eq!(degenerate.x.number.number(), 0);
        assert_encodable(degenerate.x);
    }
    #[test]
    fn transform_origin_interpolate_between_saturated_extremes_does_not_panic() {
        let a = StyleTransformOrigin {
            x: PixelValue::px(f32::MAX),
            y: PixelValue::px(f32::MIN),
        };
        let b = StyleTransformOrigin {
            x: PixelValue::px(f32::MIN),
            y: PixelValue::px(f32::MAX),
        };
        // Both endpoints are already clamped to isize::MAX / isize::MIN.
        assert_eq!(a.x.number.number(), isize::MAX);
        assert_eq!(a.y.number.number(), isize::MIN);
        for t in [
            -1e30,
            -1.0,
            0.0,
            0.5,
            1.0,
            1e30,
            f32::NAN,
            f32::INFINITY,
            f32::NEG_INFINITY,
        ] {
            let out = a.interpolate(&b, t);
            assert_encodable(out.x);
            assert_encodable(out.y);
        }
    }
    #[test]
    fn transform_origin_interpolate_across_metrics_falls_back_to_px() {
        let a = StyleTransformOrigin {
            x: PixelValue::px(0.0),
            y: PixelValue::px(0.0),
        };
        let b = StyleTransformOrigin {
            x: PixelValue::percent(100.0),
            y: PixelValue::em(2.0),
        };
        for t in [0.0, 0.5, 1.0, f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
            let out = a.interpolate(&b, t);
            assert_eq!(out.x.metric, SizeMetric::Px, "t = {t}");
            assert_eq!(out.y.metric, SizeMetric::Px, "t = {t}");
            assert_encodable(out.x);
            assert_encodable(out.y);
        }
    }
    #[test]
    fn perspective_origin_interpolate_matches_transform_origin_semantics() {
        let a = StylePerspectiveOrigin {
            x: PixelValue::px(10.0),
            y: PixelValue::percent(0.0),
        };
        let b = StylePerspectiveOrigin {
            x: PixelValue::px(30.0),
            y: PixelValue::percent(100.0),
        };
        assert_eq!(a.interpolate(&b, 0.0), a);
        assert_eq!(a.interpolate(&b, 1.0), b);
        assert_eq!(
            a.interpolate(&b, 0.5),
            StylePerspectiveOrigin {
                x: PixelValue::px(20.0),
                y: PixelValue::percent(50.0),
            }
        );
        // Default is 0px 0px, and interpolating it with itself is stable.
        let d = StylePerspectiveOrigin::default();
        assert_eq!(d.interpolate(&d, 0.5), d);
        // NaN / inf are defined, not panics.
        let nan = a.interpolate(&b, f32::NAN);
        assert_eq!(nan.x.number.number(), 0);
        assert_encodable(nan.x);
        let inf = a.interpolate(&b, f32::INFINITY);
        assert_eq!(inf.x.number.number(), isize::MAX);
        assert_encodable(inf.x);
        // Saturated extremes.
        let lo = StylePerspectiveOrigin {
            x: PixelValue::px(f32::MIN),
            y: PixelValue::px(f32::MIN),
        };
        let hi = StylePerspectiveOrigin {
            x: PixelValue::px(f32::MAX),
            y: PixelValue::px(f32::MAX),
        };
        for t in [0.0, 0.5, 1.0, f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
            let out = lo.interpolate(&hi, t);
            assert_encodable(out.x);
            assert_encodable(out.y);
        }
    }
    // =====================================================================
    // Error types: to_contained / to_shared round-trips + Display invariants
    // =====================================================================
    fn transform_errors() -> Vec<CssStyleTransformParseError<'static>> {
        vec![
            CssStyleTransformParseError::InvalidTransform("rotate"),
            // Edge: empty payload.
            CssStyleTransformParseError::InvalidTransform(""),
            CssStyleTransformParseError::InvalidTransform("\u{1F600}"),
            CssStyleTransformParseError::InvalidParenthesis(ParenthesisParseError::EmptyInput),
            CssStyleTransformParseError::InvalidParenthesis(
                ParenthesisParseError::UnclosedBraces,
            ),
            CssStyleTransformParseError::InvalidParenthesis(
                ParenthesisParseError::NoOpeningBraceFound,
            ),
            CssStyleTransformParseError::InvalidParenthesis(
                ParenthesisParseError::NoClosingBraceFound,
            ),
            CssStyleTransformParseError::InvalidParenthesis(
                ParenthesisParseError::StopWordNotFound("nope"),
            ),
            CssStyleTransformParseError::WrongNumberOfComponents {
                expected: 6,
                got: 5,
                input: "1,2,3,4,5",
            },
            // Edge: extreme counts + empty input.
            CssStyleTransformParseError::WrongNumberOfComponents {
                expected: usize::MAX,
                got: usize::MAX,
                input: "",
            },
            CssStyleTransformParseError::WrongNumberOfComponents {
                expected: 0,
                got: 0,
                input: "",
            },
            CssStyleTransformParseError::NumberParseError("x".parse::<f32>().unwrap_err()),
            CssStyleTransformParseError::NumberParseError("".parse::<f32>().unwrap_err()),
            CssStyleTransformParseError::PixelValueParseError(
                CssPixelValueParseError::EmptyString,
            ),
            CssStyleTransformParseError::PixelValueParseError(
                CssPixelValueParseError::InvalidPixelValue("auto"),
            ),
            CssStyleTransformParseError::AngleValueParseError(
                CssAngleValueParseError::EmptyString,
            ),
            CssStyleTransformParseError::AngleValueParseError(CssAngleValueParseError::InvalidAngle(
                "",
            )),
            CssStyleTransformParseError::PercentageValueParseError(
                PercentageParseError::NoPercentSign,
            ),
            CssStyleTransformParseError::PercentageValueParseError(
                PercentageParseError::InvalidUnit(AzString::from("")),
            ),
        ]
    }
    #[test]
    fn transform_parse_error_round_trips_through_owned() {
        for err in transform_errors() {
            let owned = err.to_contained();
            let shared = owned.to_shared();
            assert_eq!(shared, err, "round-trip failed for {err}");
            // Re-owning the shared copy must be stable.
            assert_eq!(shared.to_contained(), owned);
        }
    }
    #[test]
    fn transform_parse_error_round_trips_errors_from_the_real_parsers() {
        // Errors that actually come out of the parsers (rather than hand-built).
        for input in [
            "",
            "garbage",
            "translatex(1px)",
            "matrix(1,2,3)",
            "rotate(10px)",
            "translateX(auto)",
            "scaleX(abc)",
            "translate3d(1px,2px)",
        ] {
            let err = parse_style_transform(input).unwrap_err();
            assert_eq!(err.to_contained().to_shared(), err, "for {input:?}");
            assert!(!alloc::format!("{err}").is_empty());
            // Debug is implemented as Display (impl_debug_as_display!).
            assert_eq!(alloc::format!("{err:?}"), alloc::format!("{err}"));
        }
    }
    #[test]
    fn transform_origin_parse_error_round_trips_through_owned() {
        let errs = [
            CssStyleTransformOriginParseError::WrongNumberOfComponents {
                expected: 2,
                got: 0,
                input: "",
            },
            CssStyleTransformOriginParseError::WrongNumberOfComponents {
                expected: usize::MAX,
                got: usize::MAX,
                input: "\u{1F600}",
            },
            CssStyleTransformOriginParseError::PixelValueParseError(
                CssPixelValueParseError::EmptyString,
            ),
            CssStyleTransformOriginParseError::PixelValueParseError(
                CssPixelValueParseError::InvalidPixelValue(""),
            ),
        ];
        for err in errs {
            let owned = err.to_contained();
            assert_eq!(owned.to_shared(), err, "round-trip failed for {err}");
            assert!(!alloc::format!("{err}").is_empty());
        }
        // ...and one straight out of the parser.
        let err = parse_style_transform_origin("top left").unwrap_err();
        assert_eq!(err.to_contained().to_shared(), err);
    }
    #[test]
    fn perspective_origin_parse_error_round_trips_through_owned() {
        let errs = [
            CssStylePerspectiveOriginParseError::WrongNumberOfComponents {
                expected: 2,
                got: 0,
                input: "",
            },
            CssStylePerspectiveOriginParseError::WrongNumberOfComponents {
                expected: 0,
                got: usize::MAX,
                input: "\u{1F600}\u{0301}",
            },
            CssStylePerspectiveOriginParseError::PixelValueParseError(
                CssPixelValueParseError::EmptyString,
            ),
        ];
        for err in errs {
            let owned = err.to_contained();
            assert_eq!(owned.to_shared(), err, "round-trip failed for {err}");
            assert!(!alloc::format!("{err}").is_empty());
        }
        let err = parse_style_perspective_origin("center center").unwrap_err();
        assert_eq!(err.to_contained().to_shared(), err);
    }
    #[test]
    fn backface_visibility_parse_error_round_trips_through_owned() {
        for payload in ["", "none", "\u{1F600}", "  bogus  "] {
            let err = CssBackfaceVisibilityParseError::InvalidValue(payload);
            let owned = err.to_contained();
            assert_eq!(owned.to_shared(), err, "round-trip failed for {payload:?}");
            assert!(alloc::format!("{err}").contains(payload) || payload.is_empty());
        }
        let err = parse_style_backface_visibility("nope").unwrap_err();
        assert_eq!(err.to_contained().to_shared(), err);
    }
    #[test]
    fn owned_errors_borrow_from_themselves_not_from_the_original_input() {
        // to_contained() must deep-copy the &str payload: the owned error has to
        // outlive the input it was parsed from.
        let owned = {
            let input = String::from("translatex(1px)");
            parse_style_transform(&input).unwrap_err().to_contained()
        };
        assert_eq!(
            owned,
            CssStyleTransformParseErrorOwned::InvalidParenthesis(
                ParenthesisParseErrorOwned::StopWordNotFound(AzString::from("translatex"))
            )
        );
        // And the re-shared borrow points at the owned buffer.
        assert!(matches!(
            owned.to_shared(),
            CssStyleTransformParseError::InvalidParenthesis(
                ParenthesisParseError::StopWordNotFound("translatex")
            )
        ));
    }
    #[test]
    fn wrong_number_of_components_preserves_counts_across_the_owned_conversion() {
        let err = CssStyleTransformParseError::WrongNumberOfComponents {
            expected: usize::MAX,
            got: 0,
            input: "\u{1F600}",
        };
        let CssStyleTransformParseErrorOwned::WrongNumberOfComponents(WrongComponentCountError {
            expected,
            got,
            input,
        }) = err.to_contained()
        else {
            panic!("expected WrongNumberOfComponents");
        };
        assert_eq!(expected, usize::MAX);
        assert_eq!(got, 0);
        assert_eq!(input.as_str(), "\u{1F600}");
    }
    // =====================================================================
    // parse_float_value (re-exported into this module's parse path)
    // =====================================================================
    #[test]
    fn float_value_parse_helper_saturates_and_rejects_garbage() {
        assert_eq!(parse_float_value("1.5").unwrap(), FloatValue::new(1.5));
        assert_eq!(parse_float_value("  -0  ").unwrap(), FloatValue::new(0.0));
        assert_eq!(parse_float_value("inf").unwrap().number(), isize::MAX);
        assert_eq!(parse_float_value("-inf").unwrap().number(), isize::MIN);
        assert_eq!(parse_float_value("NaN").unwrap().number(), 0);
        assert!(parse_float_value("").is_err());
        assert!(parse_float_value("abc").is_err());
        assert!(parse_float_value("\u{1F600}").is_err());
    }
}