Skip to main content

azul_css/props/style/
transform.rs

1//! CSS properties for 2D and 3D transformations.
2
3use crate::corety::AzString;
4use alloc::{
5    string::{String, ToString},
6    vec::Vec,
7};
8use core::fmt;
9use std::num::ParseFloatError;
10
11#[cfg(feature = "parser")]
12use crate::props::basic::{
13    error::WrongComponentCountError,
14    length::parse_float_value,
15    parse::{parse_parentheses, ParenthesisParseError, ParenthesisParseErrorOwned},
16};
17use 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)]
41pub struct StylePerspectiveOrigin {
42    pub x: PixelValue,
43    pub y: PixelValue,
44}
45
46impl StylePerspectiveOrigin {
47    #[must_use]
48    pub fn interpolate(&self, other: &Self, t: f32) -> Self {
49        Self {
50            x: self.x.interpolate(&other.x, t),
51            y: self.y.interpolate(&other.y, t),
52        }
53    }
54}
55
56impl PrintAsCssValue for StylePerspectiveOrigin {
57    fn print_as_css_value(&self) -> String {
58        format!("{} {}", self.x, self.y)
59    }
60}
61
62/// Represents a `transform-origin` attribute
63#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
64#[repr(C)]
65pub struct StyleTransformOrigin {
66    pub x: PixelValue,
67    pub y: PixelValue,
68}
69
70impl Default for StyleTransformOrigin {
71    fn default() -> Self {
72        Self {
73            x: PixelValue::const_percent(50),
74            y: PixelValue::const_percent(50),
75        }
76    }
77}
78
79impl StyleTransformOrigin {
80    #[must_use]
81    pub fn interpolate(&self, other: &Self, t: f32) -> Self {
82        Self {
83            x: self.x.interpolate(&other.x, t),
84            y: self.y.interpolate(&other.y, t),
85        }
86    }
87}
88
89impl PrintAsCssValue for StyleTransformOrigin {
90    fn print_as_css_value(&self) -> String {
91        format!("{} {}", self.x, self.y)
92    }
93}
94
95// Formatting to Rust code
96impl crate::codegen::format::FormatAsRustCode for StylePerspectiveOrigin {
97    fn format_as_rust_code(&self, _tabs: usize) -> String {
98        format!(
99            "StylePerspectiveOrigin {{ x: {}, y: {} }}",
100            crate::codegen::format::format_pixel_value(&self.x),
101            crate::codegen::format::format_pixel_value(&self.y)
102        )
103    }
104}
105
106// Formatting to Rust code for StyleTransformOrigin
107impl crate::codegen::format::FormatAsRustCode for StyleTransformOrigin {
108    fn format_as_rust_code(&self, _tabs: usize) -> String {
109        format!(
110            "StyleTransformOrigin {{ x: {}, y: {} }}",
111            crate::codegen::format::format_pixel_value(&self.x),
112            crate::codegen::format::format_pixel_value(&self.y)
113        )
114    }
115}
116
117/// Represents a `backface-visibility` attribute
118#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
119#[repr(C)]
120pub enum StyleBackfaceVisibility {
121    #[default]
122    Visible,
123    Hidden,
124}
125
126impl PrintAsCssValue for StyleBackfaceVisibility {
127    fn print_as_css_value(&self) -> String {
128        String::from(match self {
129            Self::Hidden => "hidden",
130            Self::Visible => "visible",
131        })
132    }
133}
134
135/// Whether dragging this element moves the WINDOW — azul's `app-region`.
136///
137/// Modelled on Electron's `-webkit-app-region`, and accepted under both
138/// `-azul-app-region` and `-webkit-app-region` so existing CSS ports over
139/// unchanged.
140///
141/// ```css
142/// .titlebar        { -azul-app-region: drag; }
143/// .titlebar button { -azul-app-region: no-drag; }
144/// ```
145///
146/// This is what lets an application turn window decorations off and draw its
147/// own title bar without losing what a native one does: drag to move,
148/// double-click to maximize.
149///
150/// It does NOT cascade. A drag region names the exact element that is
151/// draggable, and inheriting it would make every button, label and icon inside
152/// a title bar drag the window instead of doing its own job — which is the bug
153/// `no-drag` exists to undo in Electron. Here the default simply never
154/// propagates, so children opt IN rather than out.
155#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
156#[repr(C)]
157pub enum StyleAppRegion {
158    /// Normal content: the element handles its own input.
159    #[default]
160    NoDrag,
161    /// Dragging this element moves the window; double-clicking it toggles
162    /// maximize/restore.
163    Drag,
164}
165
166impl PrintAsCssValue for StyleAppRegion {
167    fn print_as_css_value(&self) -> String {
168        String::from(match self {
169            Self::Drag => "drag",
170            Self::NoDrag => "no-drag",
171        })
172    }
173}
174
175/// Represents one component of a `transform` attribute
176#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
177#[repr(C, u8)]
178pub enum StyleTransform {
179    Matrix(StyleTransformMatrix2D),
180    Matrix3D(StyleTransformMatrix3D),
181    Translate(StyleTransformTranslate2D),
182    Translate3D(StyleTransformTranslate3D),
183    TranslateX(PixelValue),
184    TranslateY(PixelValue),
185    TranslateZ(PixelValue),
186    Rotate(AngleValue),
187    Rotate3D(StyleTransformRotate3D),
188    RotateX(AngleValue),
189    RotateY(AngleValue),
190    RotateZ(AngleValue),
191    Scale(StyleTransformScale2D),
192    Scale3D(StyleTransformScale3D),
193    ScaleX(PercentageValue),
194    ScaleY(PercentageValue),
195    ScaleZ(PercentageValue),
196    Skew(StyleTransformSkew2D),
197    SkewX(AngleValue),
198    SkewY(AngleValue),
199    Perspective(PixelValue),
200}
201
202impl_option!(
203    StyleTransform,
204    OptionStyleTransform,
205    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd]
206);
207
208impl_vec!(
209    StyleTransform,
210    StyleTransformVec,
211    StyleTransformVecDestructor,
212    StyleTransformVecDestructorType,
213    StyleTransformVecSlice,
214    OptionStyleTransform
215);
216impl_vec_debug!(StyleTransform, StyleTransformVec);
217impl_vec_partialord!(StyleTransform, StyleTransformVec);
218impl_vec_ord!(StyleTransform, StyleTransformVec);
219impl_vec_clone!(
220    StyleTransform,
221    StyleTransformVec,
222    StyleTransformVecDestructor
223);
224impl_vec_partialeq!(StyleTransform, StyleTransformVec);
225impl_vec_eq!(StyleTransform, StyleTransformVec);
226impl_vec_hash!(StyleTransform, StyleTransformVec);
227
228impl PrintAsCssValue for StyleTransformVec {
229    fn print_as_css_value(&self) -> String {
230        self.as_ref()
231            .iter()
232            .map(PrintAsCssValue::print_as_css_value)
233            .collect::<Vec<_>>()
234            .join(" ")
235    }
236}
237
238// Formatting to Rust code for StyleTransformVec
239impl crate::codegen::format::FormatAsRustCode for StyleTransformVec {
240    fn format_as_rust_code(&self, _tabs: usize) -> String {
241        format!(
242            "StyleTransformVec::from_const_slice(STYLE_TRANSFORM_{}_ITEMS)",
243            self.get_hash()
244        )
245    }
246}
247
248impl PrintAsCssValue for StyleTransform {
249    fn print_as_css_value(&self) -> String {
250        match self {
251            Self::Matrix(m) => format!(
252                "matrix({}, {}, {}, {}, {}, {})",
253                m.a, m.b, m.c, m.d, m.tx, m.ty
254            ),
255            Self::Matrix3D(m) => format!(
256                "matrix3d({}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {})",
257                m.m11,
258                m.m12,
259                m.m13,
260                m.m14,
261                m.m21,
262                m.m22,
263                m.m23,
264                m.m24,
265                m.m31,
266                m.m32,
267                m.m33,
268                m.m34,
269                m.m41,
270                m.m42,
271                m.m43,
272                m.m44
273            ),
274            Self::Translate(t) => format!("translate({}, {})", t.x, t.y),
275            Self::Translate3D(t) => format!("translate3d({}, {}, {})", t.x, t.y, t.z),
276            Self::TranslateX(x) => format!("translateX({x})"),
277            Self::TranslateY(y) => format!("translateY({y})"),
278            Self::TranslateZ(z) => format!("translateZ({z})"),
279            Self::Rotate(r) => format!("rotate({r})"),
280            Self::Rotate3D(r) => {
281                format!("rotate3d({}, {}, {}, {})", r.x, r.y, r.z, r.angle)
282            }
283            Self::RotateX(x) => format!("rotateX({x})"),
284            Self::RotateY(y) => format!("rotateY({y})"),
285            Self::RotateZ(z) => format!("rotateZ({z})"),
286            Self::Scale(s) => format!("scale({}, {})", s.x, s.y),
287            Self::Scale3D(s) => format!("scale3d({}, {}, {})", s.x, s.y, s.z),
288            Self::ScaleX(x) => format!("scaleX({x})"),
289            Self::ScaleY(y) => format!("scaleY({y})"),
290            Self::ScaleZ(z) => format!("scaleZ({z})"),
291            Self::Skew(sk) => format!("skew({}, {})", sk.x, sk.y),
292            Self::SkewX(x) => format!("skewX({x})"),
293            Self::SkewY(y) => format!("skewY({y})"),
294            Self::Perspective(dist) => format!("perspective({dist})"),
295        }
296    }
297}
298
299/// Represents a CSS `matrix(a, b, c, d, tx, ty)` 2D transform function.
300#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
301#[repr(C)]
302pub struct StyleTransformMatrix2D {
303    pub a: FloatValue,
304    pub b: FloatValue,
305    pub c: FloatValue,
306    pub d: FloatValue,
307    pub tx: FloatValue,
308    pub ty: FloatValue,
309}
310
311impl Default for StyleTransformMatrix2D {
312    fn default() -> Self {
313        Self {
314            a: FloatValue::const_new(1),
315            b: FloatValue::const_new(0),
316            c: FloatValue::const_new(0),
317            d: FloatValue::const_new(1),
318            tx: FloatValue::const_new(0),
319            ty: FloatValue::const_new(0),
320        }
321    }
322}
323
324/// Represents a CSS `matrix3d(...)` 3D transform function (4x4 matrix).
325#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
326#[repr(C)]
327pub struct StyleTransformMatrix3D {
328    pub m11: FloatValue,
329    pub m12: FloatValue,
330    pub m13: FloatValue,
331    pub m14: FloatValue,
332    pub m21: FloatValue,
333    pub m22: FloatValue,
334    pub m23: FloatValue,
335    pub m24: FloatValue,
336    pub m31: FloatValue,
337    pub m32: FloatValue,
338    pub m33: FloatValue,
339    pub m34: FloatValue,
340    pub m41: FloatValue,
341    pub m42: FloatValue,
342    pub m43: FloatValue,
343    pub m44: FloatValue,
344}
345
346impl Default for StyleTransformMatrix3D {
347    fn default() -> Self {
348        Self {
349            m11: FloatValue::const_new(1),
350            m12: FloatValue::const_new(0),
351            m13: FloatValue::const_new(0),
352            m14: FloatValue::const_new(0),
353            m21: FloatValue::const_new(0),
354            m22: FloatValue::const_new(1),
355            m23: FloatValue::const_new(0),
356            m24: FloatValue::const_new(0),
357            m31: FloatValue::const_new(0),
358            m32: FloatValue::const_new(0),
359            m33: FloatValue::const_new(1),
360            m34: FloatValue::const_new(0),
361            m41: FloatValue::const_new(0),
362            m42: FloatValue::const_new(0),
363            m43: FloatValue::const_new(0),
364            m44: FloatValue::const_new(1),
365        }
366    }
367}
368
369/// Represents a CSS `translate(x, y)` 2D translation.
370#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
371#[repr(C)]
372pub struct StyleTransformTranslate2D {
373    pub x: PixelValue,
374    pub y: PixelValue,
375}
376
377/// Represents a CSS `translate3d(x, y, z)` 3D translation.
378#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
379#[repr(C)]
380pub struct StyleTransformTranslate3D {
381    pub x: PixelValue,
382    pub y: PixelValue,
383    pub z: PixelValue,
384}
385
386/// Represents a CSS `rotate3d(x, y, z, angle)` 3D rotation.
387#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
388#[repr(C)]
389pub struct StyleTransformRotate3D {
390    pub x: FloatValue,
391    pub y: FloatValue,
392    pub z: FloatValue,
393    pub angle: AngleValue,
394}
395
396/// Represents a CSS `scale(x, y)` 2D scaling.
397#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
398#[repr(C)]
399pub struct StyleTransformScale2D {
400    pub x: FloatValue,
401    pub y: FloatValue,
402}
403
404/// Represents a CSS `scale3d(x, y, z)` 3D scaling.
405#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
406#[repr(C)]
407pub struct StyleTransformScale3D {
408    pub x: FloatValue,
409    pub y: FloatValue,
410    pub z: FloatValue,
411}
412
413/// Represents a CSS `skew(x, y)` 2D skew transformation.
414#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
415#[repr(C)]
416pub struct StyleTransformSkew2D {
417    pub x: AngleValue,
418    pub y: AngleValue,
419}
420
421// -- Errors --
422
423#[derive(Clone, PartialEq, Eq)]
424pub enum CssStyleTransformParseError<'a> {
425    InvalidTransform(&'a str),
426    InvalidParenthesis(ParenthesisParseError<'a>),
427    WrongNumberOfComponents {
428        expected: usize,
429        got: usize,
430        input: &'a str,
431    },
432    NumberParseError(ParseFloatError),
433    PixelValueParseError(CssPixelValueParseError<'a>),
434    AngleValueParseError(CssAngleValueParseError<'a>),
435    PercentageValueParseError(PercentageParseError),
436}
437
438impl_debug_as_display!(CssStyleTransformParseError<'a>);
439impl_display! { CssStyleTransformParseError<'a>, {
440    InvalidTransform(e) => format!("Invalid transform property: \"{}\"", e),
441    InvalidParenthesis(e) => format!("Invalid transform property - parenthesis error: {}", e),
442    WrongNumberOfComponents { expected, got, input } => format!("Invalid number of components: expected {}, got {}: \"{}\"", expected, got, input),
443    NumberParseError(e) => format!("Could not parse number: {}", e),
444    PixelValueParseError(e) => format!("Invalid pixel value: {}", e),
445    AngleValueParseError(e) => format!("Invalid angle value: {}", e),
446    PercentageValueParseError(e) => format!("Error parsing percentage: {}", e),
447}}
448
449impl_from! { ParenthesisParseError<'a>, CssStyleTransformParseError::InvalidParenthesis }
450impl_from! { CssPixelValueParseError<'a>, CssStyleTransformParseError::PixelValueParseError }
451impl_from! { CssAngleValueParseError<'a>, CssStyleTransformParseError::AngleValueParseError }
452// Written out (not impl_from!): ParseFloatError carries no lifetime, so the
453// macro's `<'a>` would be used only by the target type (single_use_lifetimes).
454impl From<ParseFloatError> for CssStyleTransformParseError<'_> {
455    fn from(e: ParseFloatError) -> Self {
456        Self::NumberParseError(e)
457    }
458}
459
460impl From<PercentageParseError> for CssStyleTransformParseError<'_> {
461    fn from(p: PercentageParseError) -> Self {
462        Self::PercentageValueParseError(p)
463    }
464}
465
466#[derive(Debug, Clone, PartialEq, Eq)]
467#[repr(C, u8)]
468pub enum CssStyleTransformParseErrorOwned {
469    InvalidTransform(AzString),
470    InvalidParenthesis(ParenthesisParseErrorOwned),
471    WrongNumberOfComponents(WrongComponentCountError),
472    NumberParseError(crate::props::basic::error::ParseFloatError),
473    PixelValueParseError(CssPixelValueParseErrorOwned),
474    AngleValueParseError(CssAngleValueParseErrorOwned),
475    PercentageValueParseError(PercentageParseError),
476}
477
478impl CssStyleTransformParseError<'_> {
479    #[must_use]
480    pub fn to_contained(&self) -> CssStyleTransformParseErrorOwned {
481        match self {
482            Self::InvalidTransform(s) => {
483                CssStyleTransformParseErrorOwned::InvalidTransform((*s).to_string().into())
484            }
485            Self::InvalidParenthesis(e) => {
486                CssStyleTransformParseErrorOwned::InvalidParenthesis(e.to_contained())
487            }
488            Self::WrongNumberOfComponents {
489                expected,
490                got,
491                input,
492            } => CssStyleTransformParseErrorOwned::WrongNumberOfComponents(
493                WrongComponentCountError {
494                    expected: *expected,
495                    got: *got,
496                    input: (*input).to_string().into(),
497                },
498            ),
499            Self::NumberParseError(e) => {
500                CssStyleTransformParseErrorOwned::NumberParseError(e.clone().into())
501            }
502            Self::PixelValueParseError(e) => {
503                CssStyleTransformParseErrorOwned::PixelValueParseError(e.to_contained())
504            }
505            Self::AngleValueParseError(e) => {
506                CssStyleTransformParseErrorOwned::AngleValueParseError(e.to_contained())
507            }
508            Self::PercentageValueParseError(e) => {
509                CssStyleTransformParseErrorOwned::PercentageValueParseError(e.clone())
510            }
511        }
512    }
513}
514
515impl CssStyleTransformParseErrorOwned {
516    #[must_use]
517    pub fn to_shared(&self) -> CssStyleTransformParseError<'_> {
518        match self {
519            Self::InvalidTransform(s) => CssStyleTransformParseError::InvalidTransform(s),
520            Self::InvalidParenthesis(e) => {
521                CssStyleTransformParseError::InvalidParenthesis(e.to_shared())
522            }
523            Self::WrongNumberOfComponents(e) => {
524                CssStyleTransformParseError::WrongNumberOfComponents {
525                    expected: e.expected,
526                    got: e.got,
527                    input: e.input.as_str(),
528                }
529            }
530            Self::NumberParseError(e) => CssStyleTransformParseError::NumberParseError(e.to_std()),
531            Self::PixelValueParseError(e) => {
532                CssStyleTransformParseError::PixelValueParseError(e.to_shared())
533            }
534            Self::AngleValueParseError(e) => {
535                CssStyleTransformParseError::AngleValueParseError(e.to_shared())
536            }
537            Self::PercentageValueParseError(e) => {
538                CssStyleTransformParseError::PercentageValueParseError(e.clone())
539            }
540        }
541    }
542}
543
544#[derive(Clone, PartialEq, Eq)]
545pub enum CssStyleTransformOriginParseError<'a> {
546    WrongNumberOfComponents {
547        expected: usize,
548        got: usize,
549        input: &'a str,
550    },
551    PixelValueParseError(CssPixelValueParseError<'a>),
552}
553
554impl_debug_as_display!(CssStyleTransformOriginParseError<'a>);
555impl_display! { CssStyleTransformOriginParseError<'a>, {
556    WrongNumberOfComponents { expected, got, input } => format!("Invalid number of components: expected {}, got {}: \"{}\"", expected, got, input),
557    PixelValueParseError(e) => format!("Invalid pixel value: {}", e),
558}}
559impl_from! { CssPixelValueParseError<'a>, CssStyleTransformOriginParseError::PixelValueParseError }
560
561#[derive(Debug, Clone, PartialEq, Eq)]
562#[repr(C, u8)]
563pub enum CssStyleTransformOriginParseErrorOwned {
564    WrongNumberOfComponents(WrongComponentCountError),
565    PixelValueParseError(CssPixelValueParseErrorOwned),
566}
567
568impl CssStyleTransformOriginParseError<'_> {
569    #[must_use]
570    pub fn to_contained(&self) -> CssStyleTransformOriginParseErrorOwned {
571        match self {
572            Self::WrongNumberOfComponents {
573                expected,
574                got,
575                input,
576            } => CssStyleTransformOriginParseErrorOwned::WrongNumberOfComponents(
577                WrongComponentCountError {
578                    expected: *expected,
579                    got: *got,
580                    input: (*input).to_string().into(),
581                },
582            ),
583            Self::PixelValueParseError(e) => {
584                CssStyleTransformOriginParseErrorOwned::PixelValueParseError(e.to_contained())
585            }
586        }
587    }
588}
589
590impl CssStyleTransformOriginParseErrorOwned {
591    #[must_use]
592    pub fn to_shared(&self) -> CssStyleTransformOriginParseError<'_> {
593        match self {
594            Self::WrongNumberOfComponents(e) => {
595                CssStyleTransformOriginParseError::WrongNumberOfComponents {
596                    expected: e.expected,
597                    got: e.got,
598                    input: e.input.as_str(),
599                }
600            }
601            Self::PixelValueParseError(e) => {
602                CssStyleTransformOriginParseError::PixelValueParseError(e.to_shared())
603            }
604        }
605    }
606}
607
608#[derive(Clone, PartialEq, Eq)]
609pub enum CssStylePerspectiveOriginParseError<'a> {
610    WrongNumberOfComponents {
611        expected: usize,
612        got: usize,
613        input: &'a str,
614    },
615    PixelValueParseError(CssPixelValueParseError<'a>),
616}
617
618impl_debug_as_display!(CssStylePerspectiveOriginParseError<'a>);
619impl_display! { CssStylePerspectiveOriginParseError<'a>, {
620    WrongNumberOfComponents { expected, got, input } => format!("Invalid number of components: expected {}, got {}: \"{}\"", expected, got, input),
621    PixelValueParseError(e) => format!("Invalid pixel value: {}", e),
622}}
623impl_from! { CssPixelValueParseError<'a>, CssStylePerspectiveOriginParseError::PixelValueParseError }
624
625#[derive(Debug, Clone, PartialEq, Eq)]
626#[repr(C, u8)]
627pub enum CssStylePerspectiveOriginParseErrorOwned {
628    WrongNumberOfComponents(WrongComponentCountError),
629    PixelValueParseError(CssPixelValueParseErrorOwned),
630}
631
632impl CssStylePerspectiveOriginParseError<'_> {
633    #[must_use]
634    pub fn to_contained(&self) -> CssStylePerspectiveOriginParseErrorOwned {
635        match self {
636            Self::WrongNumberOfComponents {
637                expected,
638                got,
639                input,
640            } => CssStylePerspectiveOriginParseErrorOwned::WrongNumberOfComponents(
641                WrongComponentCountError {
642                    expected: *expected,
643                    got: *got,
644                    input: (*input).to_string().into(),
645                },
646            ),
647            Self::PixelValueParseError(e) => {
648                CssStylePerspectiveOriginParseErrorOwned::PixelValueParseError(e.to_contained())
649            }
650        }
651    }
652}
653
654impl CssStylePerspectiveOriginParseErrorOwned {
655    #[must_use]
656    pub fn to_shared(&self) -> CssStylePerspectiveOriginParseError<'_> {
657        match self {
658            Self::WrongNumberOfComponents(e) => {
659                CssStylePerspectiveOriginParseError::WrongNumberOfComponents {
660                    expected: e.expected,
661                    got: e.got,
662                    input: e.input.as_str(),
663                }
664            }
665            Self::PixelValueParseError(e) => {
666                CssStylePerspectiveOriginParseError::PixelValueParseError(e.to_shared())
667            }
668        }
669    }
670}
671
672#[derive(Clone, PartialEq, Eq)]
673pub enum CssBackfaceVisibilityParseError<'a> {
674    InvalidValue(&'a str),
675}
676
677impl_debug_as_display!(CssBackfaceVisibilityParseError<'a>);
678impl_display! { CssBackfaceVisibilityParseError<'a>, {
679    InvalidValue(s) => format!("Invalid value for backface-visibility: \"{}\", expected \"visible\" or \"hidden\"", s),
680}}
681
682#[derive(Debug, Clone, PartialEq, Eq)]
683#[repr(C, u8)]
684pub enum CssBackfaceVisibilityParseErrorOwned {
685    InvalidValue(AzString),
686}
687
688#[derive(Clone, PartialEq, Eq)]
689#[repr(C, u8)]
690pub enum CssAppRegionParseError<'a> {
691    InvalidValue(&'a str),
692}
693
694impl_debug_as_display!(CssAppRegionParseError<'a>);
695impl_display! { CssAppRegionParseError<'a>, {
696    InvalidValue(s) => format!("Invalid value for app-region: \"{}\", expected \"drag\" or \"no-drag\"", s),
697}}
698
699#[derive(Debug, Clone, PartialEq, Eq)]
700#[repr(C, u8)]
701pub enum CssAppRegionParseErrorOwned {
702    InvalidValue(AzString),
703}
704
705impl CssAppRegionParseError<'_> {
706    #[must_use]
707    pub fn to_contained(&self) -> CssAppRegionParseErrorOwned {
708        match self {
709            Self::InvalidValue(s) => CssAppRegionParseErrorOwned::InvalidValue((*s).into()),
710        }
711    }
712}
713
714impl CssAppRegionParseErrorOwned {
715    #[must_use]
716    pub fn to_shared(&self) -> CssAppRegionParseError<'_> {
717        match self {
718            Self::InvalidValue(s) => CssAppRegionParseError::InvalidValue(s.as_str()),
719        }
720    }
721}
722
723impl CssBackfaceVisibilityParseError<'_> {
724    #[must_use]
725    pub fn to_contained(&self) -> CssBackfaceVisibilityParseErrorOwned {
726        match self {
727            Self::InvalidValue(s) => {
728                CssBackfaceVisibilityParseErrorOwned::InvalidValue((*s).to_string().into())
729            }
730        }
731    }
732}
733
734impl CssBackfaceVisibilityParseErrorOwned {
735    #[must_use]
736    pub fn to_shared(&self) -> CssBackfaceVisibilityParseError<'_> {
737        match self {
738            Self::InvalidValue(s) => CssBackfaceVisibilityParseError::InvalidValue(s),
739        }
740    }
741}
742
743// -- Parsers --
744
745#[cfg(feature = "parser")]
746/// # Errors
747///
748/// Returns an error if `input` is not a valid CSS `transform-vec` value.
749pub fn parse_style_transform_vec(
750    input: &str,
751) -> Result<StyleTransformVec, CssStyleTransformParseError<'_>> {
752    crate::props::basic::parse::split_string_respect_whitespace(input)
753        .iter()
754        .map(|i| parse_style_transform(i))
755        .collect::<Result<Vec<_>, _>>()
756        .map(Into::into)
757}
758
759#[cfg(feature = "parser")]
760#[allow(clippy::too_many_lines)]
761// large but cohesive: single-purpose CSS parser/formatter/dispatch table (one branch per property/variant)
762/// # Errors
763///
764/// Returns an error if `input` is not a valid CSS `transform` value.
765pub fn parse_style_transform(
766    input: &str,
767) -> Result<StyleTransform, CssStyleTransformParseError<'_>> {
768    fn get_numbers(
769        input: &str,
770        expected: usize,
771    ) -> Result<Vec<f32>, CssStyleTransformParseError<'_>> {
772        let numbers: Vec<_> = input
773            .split(',')
774            .map(|s| s.trim().parse::<f32>())
775            .collect::<Result<_, _>>()?;
776        if numbers.len() == expected {
777            Ok(numbers)
778        } else {
779            Err(CssStyleTransformParseError::WrongNumberOfComponents {
780                expected,
781                got: numbers.len(),
782                input,
783            })
784        }
785    }
786
787    let (transform_type, transform_values) = parse_parentheses(
788        input,
789        &[
790            "matrix",
791            "matrix3d",
792            "translate",
793            "translate3d",
794            "translateX",
795            "translateY",
796            "translateZ",
797            "rotate",
798            "rotate3d",
799            "rotateX",
800            "rotateY",
801            "rotateZ",
802            "scale",
803            "scale3d",
804            "scaleX",
805            "scaleY",
806            "scaleZ",
807            "skew",
808            "skewX",
809            "skewY",
810            "perspective",
811        ],
812    )?;
813
814    match transform_type {
815        "matrix" => {
816            let nums = get_numbers(transform_values, 6)?;
817            Ok(StyleTransform::Matrix(StyleTransformMatrix2D {
818                a: FloatValue::new(nums[0]),
819                b: FloatValue::new(nums[1]),
820                c: FloatValue::new(nums[2]),
821                d: FloatValue::new(nums[3]),
822                tx: FloatValue::new(nums[4]),
823                ty: FloatValue::new(nums[5]),
824            }))
825        }
826        "matrix3d" => {
827            let nums = get_numbers(transform_values, 16)?;
828            Ok(StyleTransform::Matrix3D(StyleTransformMatrix3D {
829                m11: FloatValue::new(nums[0]),
830                m12: FloatValue::new(nums[1]),
831                m13: FloatValue::new(nums[2]),
832                m14: FloatValue::new(nums[3]),
833                m21: FloatValue::new(nums[4]),
834                m22: FloatValue::new(nums[5]),
835                m23: FloatValue::new(nums[6]),
836                m24: FloatValue::new(nums[7]),
837                m31: FloatValue::new(nums[8]),
838                m32: FloatValue::new(nums[9]),
839                m33: FloatValue::new(nums[10]),
840                m34: FloatValue::new(nums[11]),
841                m41: FloatValue::new(nums[12]),
842                m42: FloatValue::new(nums[13]),
843                m43: FloatValue::new(nums[14]),
844                m44: FloatValue::new(nums[15]),
845            }))
846        }
847        "translate" => {
848            let components: Vec<_> = transform_values.split(',').collect();
849
850            // translate() takes exactly 1 or 2 parameters (x, or x and y)
851            if components.len() > 2 {
852                return Err(CssStyleTransformParseError::WrongNumberOfComponents {
853                    expected: 2,
854                    got: components.len(),
855                    input: transform_values,
856                });
857            }
858
859            let x = parse_pixel_value(
860                components
861                    .first()
862                    .ok_or(CssStyleTransformParseError::WrongNumberOfComponents {
863                        expected: 2,
864                        got: 0,
865                        input: transform_values,
866                    })?
867                    .trim(),
868            )?;
869            let y = match components.get(1) {
870                Some(c) => parse_pixel_value(c.trim())?,
871                None => PixelValue::px(0.0),
872            };
873            Ok(StyleTransform::Translate(StyleTransformTranslate2D {
874                x,
875                y,
876            }))
877        }
878        "translate3d" => {
879            let components: Vec<_> = transform_values.split(',').collect();
880            let x = parse_pixel_value(
881                components
882                    .first()
883                    .ok_or(CssStyleTransformParseError::WrongNumberOfComponents {
884                        expected: 3,
885                        got: 0,
886                        input: transform_values,
887                    })?
888                    .trim(),
889            )?;
890            let y = parse_pixel_value(
891                components
892                    .get(1)
893                    .ok_or(CssStyleTransformParseError::WrongNumberOfComponents {
894                        expected: 3,
895                        got: 1,
896                        input: transform_values,
897                    })?
898                    .trim(),
899            )?;
900            let z = parse_pixel_value(
901                components
902                    .get(2)
903                    .ok_or(CssStyleTransformParseError::WrongNumberOfComponents {
904                        expected: 3,
905                        got: 2,
906                        input: transform_values,
907                    })?
908                    .trim(),
909            )?;
910            Ok(StyleTransform::Translate3D(StyleTransformTranslate3D {
911                x,
912                y,
913                z,
914            }))
915        }
916        "translateX" => Ok(StyleTransform::TranslateX(parse_pixel_value(
917            transform_values,
918        )?)),
919        "translateY" => Ok(StyleTransform::TranslateY(parse_pixel_value(
920            transform_values,
921        )?)),
922        "translateZ" => Ok(StyleTransform::TranslateZ(parse_pixel_value(
923            transform_values,
924        )?)),
925        "rotate" => Ok(StyleTransform::Rotate(parse_angle_value(transform_values)?)),
926        "rotate3d" => {
927            let parts: Vec<_> = transform_values.splitn(4, ',').collect();
928            if parts.len() != 4 {
929                return Err(CssStyleTransformParseError::WrongNumberOfComponents {
930                    expected: 4,
931                    got: parts.len(),
932                    input: transform_values,
933                });
934            }
935            let x = parts[0].trim().parse::<f32>()?;
936            let y = parts[1].trim().parse::<f32>()?;
937            let z = parts[2].trim().parse::<f32>()?;
938            let angle = parse_angle_value(parts[3].trim())?;
939            Ok(StyleTransform::Rotate3D(StyleTransformRotate3D {
940                x: FloatValue::new(x),
941                y: FloatValue::new(y),
942                z: FloatValue::new(z),
943                angle,
944            }))
945        }
946        "rotateX" => Ok(StyleTransform::RotateX(parse_angle_value(
947            transform_values,
948        )?)),
949        "rotateY" => Ok(StyleTransform::RotateY(parse_angle_value(
950            transform_values,
951        )?)),
952        "rotateZ" => Ok(StyleTransform::RotateZ(parse_angle_value(
953            transform_values,
954        )?)),
955        "scale" => {
956            let parts: Vec<_> = transform_values.split(',').collect();
957            if parts.is_empty() || parts.len() > 2 {
958                return Err(CssStyleTransformParseError::WrongNumberOfComponents {
959                    expected: 2,
960                    got: parts.len(),
961                    input: transform_values,
962                });
963            }
964            let x = parts[0].trim().parse::<f32>()?;
965            let y = if parts.len() == 2 {
966                parts[1].trim().parse::<f32>()?
967            } else {
968                x
969            };
970            Ok(StyleTransform::Scale(StyleTransformScale2D {
971                x: FloatValue::new(x),
972                y: FloatValue::new(y),
973            }))
974        }
975        "scale3d" => {
976            let nums = get_numbers(transform_values, 3)?;
977            Ok(StyleTransform::Scale3D(StyleTransformScale3D {
978                x: FloatValue::new(nums[0]),
979                y: FloatValue::new(nums[1]),
980                z: FloatValue::new(nums[2]),
981            }))
982        }
983        "scaleX" => Ok(StyleTransform::ScaleX(PercentageValue::new(
984            transform_values.trim().parse::<f32>()? * 100.0,
985        ))),
986        "scaleY" => Ok(StyleTransform::ScaleY(PercentageValue::new(
987            transform_values.trim().parse::<f32>()? * 100.0,
988        ))),
989        "scaleZ" => Ok(StyleTransform::ScaleZ(PercentageValue::new(
990            transform_values.trim().parse::<f32>()? * 100.0,
991        ))),
992        "skew" => {
993            let components: Vec<_> = transform_values.split(',').collect();
994            if components.is_empty() || components.len() > 2 {
995                return Err(CssStyleTransformParseError::WrongNumberOfComponents {
996                    expected: 2,
997                    got: components.len(),
998                    input: transform_values,
999                });
1000            }
1001            let x = parse_angle_value(components[0].trim())?;
1002            let y = match components.get(1) {
1003                Some(c) => parse_angle_value(c.trim())?,
1004                None => AngleValue::deg(0.0),
1005            };
1006            Ok(StyleTransform::Skew(StyleTransformSkew2D { x, y }))
1007        }
1008        "skewX" => Ok(StyleTransform::SkewX(parse_angle_value(transform_values)?)),
1009        "skewY" => Ok(StyleTransform::SkewY(parse_angle_value(transform_values)?)),
1010        "perspective" => Ok(StyleTransform::Perspective(parse_pixel_value(
1011            transform_values,
1012        )?)),
1013        _ => unreachable!(),
1014    }
1015}
1016
1017#[cfg(feature = "parser")]
1018/// # Errors
1019///
1020/// Returns an error if `input` is not a valid CSS `transform-origin` value.
1021pub fn parse_style_transform_origin(
1022    input: &str,
1023) -> Result<StyleTransformOrigin, CssStyleTransformOriginParseError<'_>> {
1024    // Helper to parse position keywords or pixel values
1025    fn parse_position_component(
1026        s: &str,
1027        is_horizontal: bool,
1028    ) -> Result<PixelValue, CssPixelValueParseError<'_>> {
1029        match s.trim() {
1030            "left" if is_horizontal => Ok(PixelValue::percent(0.0)),
1031            "center" => Ok(PixelValue::percent(50.0)),
1032            "right" if is_horizontal => Ok(PixelValue::percent(100.0)),
1033            "top" if !is_horizontal => Ok(PixelValue::percent(0.0)),
1034            "bottom" if !is_horizontal => Ok(PixelValue::percent(100.0)),
1035            _ => parse_pixel_value(s),
1036        }
1037    }
1038
1039    let components: Vec<_> = input.split_whitespace().collect();
1040    if components.len() != 2 {
1041        return Err(CssStyleTransformOriginParseError::WrongNumberOfComponents {
1042            expected: 2,
1043            got: components.len(),
1044            input,
1045        });
1046    }
1047
1048    let x = parse_position_component(components[0], true)?;
1049    let y = parse_position_component(components[1], false)?;
1050    Ok(StyleTransformOrigin { x, y })
1051}
1052
1053#[cfg(feature = "parser")]
1054/// # Errors
1055///
1056/// Returns an error if `input` is not a valid CSS `perspective-origin` value.
1057pub fn parse_style_perspective_origin(
1058    input: &str,
1059) -> Result<StylePerspectiveOrigin, CssStylePerspectiveOriginParseError<'_>> {
1060    let components: Vec<_> = input.split_whitespace().collect();
1061    if components.len() != 2 {
1062        return Err(
1063            CssStylePerspectiveOriginParseError::WrongNumberOfComponents {
1064                expected: 2,
1065                got: components.len(),
1066                input,
1067            },
1068        );
1069    }
1070    let x = parse_pixel_value(components[0])?;
1071    let y = parse_pixel_value(components[1])?;
1072    Ok(StylePerspectiveOrigin { x, y })
1073}
1074
1075#[cfg(feature = "parser")]
1076/// # Errors
1077///
1078/// Returns an error if `input` is not `drag` or `no-drag`.
1079///
1080/// Electron writes `-webkit-app-region: drag`; both spellings map here, so CSS
1081/// written for Electron works unchanged.
1082pub fn parse_style_app_region(input: &str) -> Result<StyleAppRegion, CssAppRegionParseError<'_>> {
1083    match input.trim() {
1084        "drag" => Ok(StyleAppRegion::Drag),
1085        "no-drag" | "none" => Ok(StyleAppRegion::NoDrag),
1086        _ => Err(CssAppRegionParseError::InvalidValue(input)),
1087    }
1088}
1089
1090#[cfg(feature = "parser")]
1091/// # Errors
1092///
1093/// Returns an error if `input` is not a valid CSS `backface-visibility` value.
1094pub fn parse_style_backface_visibility(
1095    input: &str,
1096) -> Result<StyleBackfaceVisibility, CssBackfaceVisibilityParseError<'_>> {
1097    match input.trim() {
1098        "visible" => Ok(StyleBackfaceVisibility::Visible),
1099        "hidden" => Ok(StyleBackfaceVisibility::Hidden),
1100        _ => Err(CssBackfaceVisibilityParseError::InvalidValue(input)),
1101    }
1102}
1103
1104#[cfg(all(test, feature = "parser"))]
1105mod tests {
1106    // Tests assert that parsed values equal the exact source literals.
1107    #![allow(clippy::float_cmp)]
1108    use super::*;
1109
1110    #[test]
1111    fn test_parse_transform_vec() {
1112        let result =
1113            parse_style_transform_vec("translateX(10px) rotate(90deg) scale(0.5, 0.5)").unwrap();
1114        assert_eq!(result.len(), 3);
1115        assert!(matches!(
1116            result.as_slice()[0],
1117            StyleTransform::TranslateX(_)
1118        ));
1119        assert!(matches!(result.as_slice()[1], StyleTransform::Rotate(_)));
1120        assert!(matches!(result.as_slice()[2], StyleTransform::Scale(_)));
1121    }
1122
1123    #[test]
1124    fn test_parse_transform_functions() {
1125        // Translate
1126        assert_eq!(
1127            parse_style_transform("translateX(50%)").unwrap(),
1128            StyleTransform::TranslateX(PixelValue::percent(50.0))
1129        );
1130        let translate = parse_style_transform("translate(10px, -20px)").unwrap();
1131        if let StyleTransform::Translate(t) = translate {
1132            assert_eq!(t.x, PixelValue::px(10.0));
1133            assert_eq!(t.y, PixelValue::px(-20.0));
1134        } else {
1135            panic!("Expected Translate");
1136        }
1137
1138        // Scale
1139        assert_eq!(
1140            parse_style_transform("scaleY(1.2)").unwrap(),
1141            StyleTransform::ScaleY(PercentageValue::new(120.0))
1142        );
1143        let scale = parse_style_transform("scale(2, 0.5)").unwrap();
1144        if let StyleTransform::Scale(s) = scale {
1145            assert_eq!(s.x.get(), 2.0);
1146            assert_eq!(s.y.get(), 0.5);
1147        } else {
1148            panic!("Expected Scale");
1149        }
1150
1151        // Rotate
1152        assert_eq!(
1153            parse_style_transform("rotate(0.25turn)").unwrap(),
1154            StyleTransform::Rotate(AngleValue::turn(0.25))
1155        );
1156
1157        // Skew
1158        assert_eq!(
1159            parse_style_transform("skewX(-10deg)").unwrap(),
1160            StyleTransform::SkewX(AngleValue::deg(-10.0))
1161        );
1162        let skew = parse_style_transform("skew(20deg, 30deg)").unwrap();
1163        if let StyleTransform::Skew(s) = skew {
1164            assert_eq!(s.x, AngleValue::deg(20.0));
1165            assert_eq!(s.y, AngleValue::deg(30.0));
1166        } else {
1167            panic!("Expected Skew");
1168        }
1169    }
1170
1171    #[test]
1172    fn test_parse_transform_origin() {
1173        let result = parse_style_transform_origin("50% 50%").unwrap();
1174        assert_eq!(result.x, PixelValue::percent(50.0));
1175        assert_eq!(result.y, PixelValue::percent(50.0));
1176
1177        let result = parse_style_transform_origin("left top").unwrap();
1178        assert_eq!(result.x, PixelValue::percent(0.0));
1179        assert_eq!(result.y, PixelValue::percent(0.0));
1180
1181        let result = parse_style_transform_origin("20px bottom").unwrap();
1182        assert_eq!(result.x, PixelValue::px(20.0));
1183        assert_eq!(result.y, PixelValue::percent(100.0));
1184    }
1185
1186    #[test]
1187    fn test_parse_backface_visibility() {
1188        assert_eq!(
1189            parse_style_backface_visibility("visible").unwrap(),
1190            StyleBackfaceVisibility::Visible
1191        );
1192        assert_eq!(
1193            parse_style_backface_visibility("hidden").unwrap(),
1194            StyleBackfaceVisibility::Hidden
1195        );
1196        assert!(parse_style_backface_visibility("none").is_err());
1197    }
1198
1199    #[test]
1200    fn test_parse_transform_errors() {
1201        // Wrong function name
1202        assert!(parse_style_transform("translatex(10px)").is_err());
1203        // Wrong number of args
1204        assert!(parse_style_transform("translate(1, 2, 3)").is_err());
1205        // Single-arg forms (CSS spec compliant)
1206        let scale1 = parse_style_transform("scale(2)").unwrap();
1207        if let StyleTransform::Scale(s) = scale1 {
1208            assert_eq!(s.x.get(), 2.0);
1209            assert_eq!(s.y.get(), 2.0);
1210        } else {
1211            panic!("Expected Scale");
1212        }
1213        let translate1 = parse_style_transform("translate(10px)").unwrap();
1214        if let StyleTransform::Translate(t) = translate1 {
1215            assert_eq!(t.x, PixelValue::px(10.0));
1216            assert_eq!(t.y, PixelValue::px(0.0));
1217        } else {
1218            panic!("Expected Translate");
1219        }
1220        let skew1 = parse_style_transform("skew(20deg)").unwrap();
1221        if let StyleTransform::Skew(s) = skew1 {
1222            assert_eq!(s.x, AngleValue::deg(20.0));
1223            assert_eq!(s.y, AngleValue::deg(0.0));
1224        } else {
1225            panic!("Expected Skew");
1226        }
1227        // rotate3d with angle unit
1228        let rot3d = parse_style_transform("rotate3d(1, 0, 0, 45deg)").unwrap();
1229        if let StyleTransform::Rotate3D(r) = rot3d {
1230            assert_eq!(r.x.get(), 1.0);
1231            assert_eq!(r.angle, AngleValue::deg(45.0));
1232        } else {
1233            panic!("Expected Rotate3D");
1234        }
1235        // Invalid value
1236        assert!(parse_style_transform("rotate(10px)").is_err());
1237        assert!(parse_style_transform("translateX(auto)").is_err());
1238    }
1239}
1240
1241#[cfg(all(test, feature = "parser"))]
1242#[allow(clippy::too_many_lines, clippy::float_cmp)]
1243mod autotest_generated {
1244    // Tests compare parsed values against exact source literals, and deliberately
1245    // feed NaN/inf through the numeric encoders.
1246
1247    use super::*;
1248    use crate::props::basic::length::SizeMetric;
1249
1250    // ---------------------------------------------------------------------
1251    // helpers
1252    // ---------------------------------------------------------------------
1253
1254    /// Every `FloatValue` is stored as an `isize`, so `get()` can never be
1255    /// NaN/inf no matter what went in. Used as a blanket invariant below.
1256    fn assert_encodable(pv: PixelValue) {
1257        assert!(pv.number.get().is_finite());
1258    }
1259
1260    fn all_roundtrippable_transforms() -> Vec<StyleTransform> {
1261        vec![
1262            StyleTransform::Matrix(StyleTransformMatrix2D::default()),
1263            StyleTransform::Matrix3D(StyleTransformMatrix3D::default()),
1264            StyleTransform::Translate(StyleTransformTranslate2D {
1265                x: PixelValue::px(10.0),
1266                y: PixelValue::px(-20.0),
1267            }),
1268            StyleTransform::Translate3D(StyleTransformTranslate3D {
1269                x: PixelValue::px(1.0),
1270                y: PixelValue::em(2.0),
1271                z: PixelValue::pt(-3.5),
1272            }),
1273            StyleTransform::TranslateX(PixelValue::percent(50.0)),
1274            StyleTransform::TranslateY(PixelValue::px(0.0)),
1275            StyleTransform::TranslateZ(PixelValue::rem(1.25)),
1276            StyleTransform::Rotate(AngleValue::deg(90.0)),
1277            StyleTransform::Rotate3D(StyleTransformRotate3D {
1278                x: FloatValue::new(1.0),
1279                y: FloatValue::new(0.0),
1280                z: FloatValue::new(0.0),
1281                angle: AngleValue::turn(0.25),
1282            }),
1283            StyleTransform::RotateX(AngleValue::rad(1.5)),
1284            StyleTransform::RotateY(AngleValue::grad(100.0)),
1285            StyleTransform::RotateZ(AngleValue::deg(-45.0)),
1286            StyleTransform::Scale(StyleTransformScale2D {
1287                x: FloatValue::new(2.0),
1288                y: FloatValue::new(0.5),
1289            }),
1290            StyleTransform::Scale3D(StyleTransformScale3D {
1291                x: FloatValue::new(1.0),
1292                y: FloatValue::new(-1.0),
1293                z: FloatValue::new(0.25),
1294            }),
1295            StyleTransform::Skew(StyleTransformSkew2D {
1296                x: AngleValue::deg(20.0),
1297                y: AngleValue::deg(30.0),
1298            }),
1299            StyleTransform::SkewX(AngleValue::deg(-10.0)),
1300            StyleTransform::SkewY(AngleValue::deg(10.0)),
1301            StyleTransform::Perspective(PixelValue::px(500.0)),
1302        ]
1303    }
1304
1305    // =====================================================================
1306    // parse_style_transform  --  malformed / boundary / unicode
1307    // =====================================================================
1308
1309    #[test]
1310    fn transform_rejects_empty_and_whitespace_only_input() {
1311        for input in ["", "   ", "\t\n", "\r\n\t "] {
1312            let err = parse_style_transform(input).unwrap_err();
1313            assert!(
1314                matches!(
1315                    err,
1316                    CssStyleTransformParseError::InvalidParenthesis(
1317                        ParenthesisParseError::EmptyInput
1318                    )
1319                ),
1320                "expected EmptyInput for {input:?}, got {err}"
1321            );
1322        }
1323    }
1324
1325    #[test]
1326    fn transform_rejects_garbage_without_panicking() {
1327        // No opening brace at all.
1328        assert!(matches!(
1329            parse_style_transform("garbage").unwrap_err(),
1330            CssStyleTransformParseError::InvalidParenthesis(
1331                ParenthesisParseError::NoOpeningBraceFound
1332            )
1333        ));
1334        // Opening brace, no closing brace.
1335        assert!(matches!(
1336            parse_style_transform("rotate(90deg").unwrap_err(),
1337            CssStyleTransformParseError::InvalidParenthesis(
1338                ParenthesisParseError::NoClosingBraceFound
1339            )
1340        ));
1341        // Known-but-miscased function name is NOT accepted (CSS is case-insensitive
1342        // for function names; azul's stopword table is case-sensitive).
1343        assert!(matches!(
1344            parse_style_transform("translatex(10px)").unwrap_err(),
1345            CssStyleTransformParseError::InvalidParenthesis(
1346                ParenthesisParseError::StopWordNotFound("translatex")
1347            )
1348        ));
1349        assert!(matches!(
1350            parse_style_transform("ROTATE(90deg)").unwrap_err(),
1351            CssStyleTransformParseError::InvalidParenthesis(
1352                ParenthesisParseError::StopWordNotFound("ROTATE")
1353            )
1354        ));
1355        // Random byte soup, none of which forms a grammar.
1356        for input in [
1357            "((((",
1358            ")",
1359            "()",
1360            "(rotate)",
1361            "rotate",
1362            ";;;",
1363            "\0(\0)",
1364            "-1",
1365            "rotate(,,,,)",
1366            "matrix(,)",
1367        ] {
1368            assert!(
1369                parse_style_transform(input).is_err(),
1370                "expected Err for {input:?}"
1371            );
1372        }
1373    }
1374
1375    #[test]
1376    fn transform_never_hits_the_unreachable_arm_for_near_miss_stopwords() {
1377        // parse_style_transform ends in `_ => unreachable!()`; it is only sound as
1378        // long as parse_parentheses can never hand back a non-listed stopword.
1379        // Probe names that are prefixes/suffixes/case-variants of real ones.
1380        for name in [
1381            "translat",
1382            "translateXX",
1383            "xtranslateX",
1384            "rotate3",
1385            "rotate3D",
1386            "scale4d",
1387            "skewZ",
1388            "perspectives",
1389            "matrix2d",
1390            "MATRIX",
1391            "",
1392            " rotate",
1393        ] {
1394            let input = alloc::format!("{name}(1)");
1395            let res = parse_style_transform(&input);
1396            // Either a clean parse error or (for " rotate", which trims to "rotate")
1397            // a normal result - but never a panic.
1398            let _ = res;
1399        }
1400        // " rotate(1)" trims down to a valid rotate with a bare-number degree.
1401        assert_eq!(
1402            parse_style_transform("  rotate(1)  ").unwrap(),
1403            StyleTransform::Rotate(AngleValue::deg(1.0))
1404        );
1405    }
1406
1407    #[test]
1408    fn transform_handles_non_ascii_and_multibyte_input() {
1409        // parse_parentheses slices `input[..first_open_brace]`; a multibyte char
1410        // right before the '(' must not split a UTF-8 boundary.
1411        for input in [
1412            "\u{1F600}",
1413            "\u{1F600}(1)",
1414            "rotate(\u{1F600})",
1415            "rotate\u{0301}(1deg)",
1416            "\u{1F600}rotate(1deg)",
1417            "translateX(\u{1F600}px)",
1418            "translate(\u{1F600}, \u{1F600})",
1419            "matrix3d(\u{4F60}\u{597D})",
1420            "sk\u{0435}wX(10deg)", // cyrillic 'е' homoglyph
1421        ] {
1422            assert!(
1423                parse_style_transform(input).is_err(),
1424                "expected Err for {input:?}"
1425            );
1426        }
1427        assert!(matches!(
1428            parse_style_transform("rotate(\u{1F600})").unwrap_err(),
1429            CssStyleTransformParseError::AngleValueParseError(
1430                CssAngleValueParseError::InvalidAngle("\u{1F600}")
1431            )
1432        ));
1433    }
1434
1435    #[test]
1436    fn transform_boundary_numbers_saturate_instead_of_panicking() {
1437        // -0 collapses to +0 in the isize-backed encoding.
1438        assert_eq!(
1439            parse_style_transform("translateX(-0)").unwrap(),
1440            StyleTransform::TranslateX(PixelValue::px(0.0))
1441        );
1442        assert_eq!(
1443            parse_style_transform("translateX(0)").unwrap(),
1444            StyleTransform::TranslateX(PixelValue::px(0.0))
1445        );
1446
1447        // NaN parses as a float (Rust accepts "NaN"), and the f32 -> isize cast
1448        // maps NaN to 0. So `translateX(NaN)` silently becomes `0px`.
1449        let StyleTransform::TranslateX(nan_px) = parse_style_transform("translateX(NaN)").unwrap()
1450        else {
1451            panic!("expected TranslateX");
1452        };
1453        assert_eq!(nan_px.number.number(), 0);
1454        assert_encodable(nan_px);
1455
1456        // Infinities (literal, and via decimal overflow) saturate to isize::MAX/MIN.
1457        for input in [
1458            "translateX(inf)",
1459            "translateX(1e400)",
1460            "translateX(1e400px)",
1461        ] {
1462            let StyleTransform::TranslateX(px) = parse_style_transform(input).unwrap() else {
1463                panic!("expected TranslateX for {input}");
1464            };
1465            assert_eq!(px.number.number(), isize::MAX, "{input}");
1466            assert_encodable(px);
1467        }
1468        let StyleTransform::TranslateX(neg) = parse_style_transform("translateX(-inf)").unwrap()
1469        else {
1470            panic!("expected TranslateX");
1471        };
1472        assert_eq!(neg.number.number(), isize::MIN);
1473        assert_encodable(neg);
1474
1475        // i64::MAX / f64-scale magnitudes: fine, just saturated.
1476        for input in [
1477            "translateX(9223372036854775807px)",
1478            "translateX(-9223372036854775808px)",
1479            "translateX(1e-400px)",
1480            "translateX(0.0000000000001px)",
1481        ] {
1482            assert!(parse_style_transform(input).is_ok(), "{input}");
1483        }
1484
1485        // Angles take the same path: NaN -> 0deg, inf -> saturated.
1486        assert_eq!(
1487            parse_style_transform("rotate(NaN)").unwrap(),
1488            StyleTransform::Rotate(AngleValue::deg(0.0))
1489        );
1490        let StyleTransform::Rotate(a) = parse_style_transform("rotate(infdeg)").unwrap() else {
1491            panic!("expected Rotate");
1492        };
1493        assert_eq!(a.number.number(), isize::MAX);
1494
1495        // scaleX multiplies by 100 before encoding - inf * 100 must not trap.
1496        let StyleTransform::ScaleX(p) = parse_style_transform("scaleX(NaN)").unwrap() else {
1497            panic!("expected ScaleX");
1498        };
1499        assert_eq!(p, PercentageValue::new(0.0));
1500        assert!(parse_style_transform("scaleX(inf)").is_ok());
1501        assert!(parse_style_transform("scaleX(1e40)").is_ok());
1502    }
1503
1504    #[test]
1505    fn transform_component_counts_are_enforced() {
1506        // matrix wants exactly 6.
1507        assert!(matches!(
1508            parse_style_transform("matrix(1,2,3,4,5)").unwrap_err(),
1509            CssStyleTransformParseError::WrongNumberOfComponents {
1510                expected: 6,
1511                got: 5,
1512                ..
1513            }
1514        ));
1515        assert!(matches!(
1516            parse_style_transform("matrix(1,2,3,4,5,6,7)").unwrap_err(),
1517            CssStyleTransformParseError::WrongNumberOfComponents {
1518                expected: 6,
1519                got: 7,
1520                ..
1521            }
1522        ));
1523        // matrix3d wants exactly 16.
1524        assert!(matches!(
1525            parse_style_transform("matrix3d(1,0,0,0,0,1,0,0,0,0,1,0,0,0,0)").unwrap_err(),
1526            CssStyleTransformParseError::WrongNumberOfComponents {
1527                expected: 16,
1528                got: 15,
1529                ..
1530            }
1531        ));
1532        // translate takes at most 2.
1533        assert!(matches!(
1534            parse_style_transform("translate(1px, 2px, 3px)").unwrap_err(),
1535            CssStyleTransformParseError::WrongNumberOfComponents {
1536                expected: 2,
1537                got: 3,
1538                ..
1539            }
1540        ));
1541        // scale takes at most 2.
1542        assert!(matches!(
1543            parse_style_transform("scale(1, 2, 3)").unwrap_err(),
1544            CssStyleTransformParseError::WrongNumberOfComponents {
1545                expected: 2,
1546                got: 3,
1547                ..
1548            }
1549        ));
1550        // skew takes at most 2.
1551        assert!(matches!(
1552            parse_style_transform("skew(1deg, 2deg, 3deg)").unwrap_err(),
1553            CssStyleTransformParseError::WrongNumberOfComponents {
1554                expected: 2,
1555                got: 3,
1556                ..
1557            }
1558        ));
1559        // rotate3d wants exactly 4 (splitn(4) makes >4 fold into the angle, which
1560        // then fails to parse as an angle).
1561        assert!(matches!(
1562            parse_style_transform("rotate3d(1, 0, 0)").unwrap_err(),
1563            CssStyleTransformParseError::WrongNumberOfComponents {
1564                expected: 4,
1565                got: 3,
1566                ..
1567            }
1568        ));
1569        assert!(parse_style_transform("rotate3d(1, 0, 0, 45deg, 99)").is_err());
1570        // translate3d wants exactly 3 when short...
1571        assert!(matches!(
1572            parse_style_transform("translate3d(1px, 2px)").unwrap_err(),
1573            CssStyleTransformParseError::WrongNumberOfComponents {
1574                expected: 3,
1575                got: 2,
1576                ..
1577            }
1578        ));
1579        // scale3d wants exactly 3.
1580        assert!(matches!(
1581            parse_style_transform("scale3d(1, 2)").unwrap_err(),
1582            CssStyleTransformParseError::WrongNumberOfComponents {
1583                expected: 3,
1584                got: 2,
1585                ..
1586            }
1587        ));
1588    }
1589
1590    #[test]
1591    fn transform_translate3d_silently_ignores_extra_components() {
1592        // BUG (leniency): unlike matrix/scale3d (which go through get_numbers and
1593        // check the count), translate3d only indexes [0], [1], [2] and never
1594        // rejects a 4th+ component. Per CSS this must be a parse error.
1595        // Pinned as current behaviour so a future fix shows up as a diff here.
1596        let parsed = parse_style_transform("translate3d(1px, 2px, 3px, 4px, 5px)").unwrap();
1597        assert_eq!(
1598            parsed,
1599            StyleTransform::Translate3D(StyleTransformTranslate3D {
1600                x: PixelValue::px(1.0),
1601                y: PixelValue::px(2.0),
1602                z: PixelValue::px(3.0),
1603            })
1604        );
1605    }
1606
1607    #[test]
1608    fn transform_ignores_junk_after_the_closing_paren() {
1609        // BUG (leniency): parse_parentheses uses find('(') .. rfind(')'), so any
1610        // trailing junk that contains no ')' is silently dropped. Per CSS,
1611        // "rotate(90deg)garbage" is invalid. Pinned as current behaviour.
1612        assert_eq!(
1613            parse_style_transform("rotate(90deg)garbage").unwrap(),
1614            StyleTransform::Rotate(AngleValue::deg(90.0))
1615        );
1616        assert_eq!(
1617            parse_style_transform("rotate(90deg) ;drop table").unwrap(),
1618            StyleTransform::Rotate(AngleValue::deg(90.0))
1619        );
1620        // ...but junk containing a ')' gets swallowed INTO the argument, which then
1621        // fails - so the leniency is content-dependent, not a clean "trim" rule.
1622        assert!(parse_style_transform("rotate(90deg))").is_err());
1623        assert!(parse_style_transform("rotate(90deg) rotate(1deg)").is_err());
1624    }
1625
1626    #[test]
1627    fn transform_empty_argument_lists_are_rejected() {
1628        for input in [
1629            "matrix()",
1630            "matrix3d()",
1631            "translate()",
1632            "translate3d()",
1633            "translateX()",
1634            "translateY()",
1635            "translateZ()",
1636            "rotate()",
1637            "rotate3d()",
1638            "rotateX()",
1639            "rotateY()",
1640            "rotateZ()",
1641            "scale()",
1642            "scale3d()",
1643            "scaleX()",
1644            "scaleY()",
1645            "scaleZ()",
1646            "skew()",
1647            "skewX()",
1648            "skewY()",
1649            "perspective()",
1650        ] {
1651            assert!(
1652                parse_style_transform(input).is_err(),
1653                "expected Err for {input:?}"
1654            );
1655        }
1656        // Trailing-comma forms, too.
1657        for input in [
1658            "translate(10px,)",
1659            "translate3d(1px,2px,)",
1660            "scale(2,)",
1661            "skew(10deg,)",
1662            "matrix(1,2,3,4,5,)",
1663        ] {
1664            assert!(
1665                parse_style_transform(input).is_err(),
1666                "expected Err for {input:?}"
1667            );
1668        }
1669    }
1670
1671    #[test]
1672    fn transform_wrong_unit_kinds_are_rejected() {
1673        // A length where an angle is expected, and vice versa.
1674        assert!(parse_style_transform("rotate(10px)").is_err());
1675        assert!(parse_style_transform("rotateX(10px)").is_err());
1676        assert!(parse_style_transform("skewY(10px)").is_err());
1677        assert!(parse_style_transform("translateX(10deg)").is_err());
1678        assert!(parse_style_transform("perspective(10deg)").is_err());
1679        // Keywords are not lengths.
1680        assert!(parse_style_transform("translateX(auto)").is_err());
1681        assert!(parse_style_transform("translateX(none)").is_err());
1682        // scaleX takes a bare number, NOT a percentage (see the round-trip test).
1683        assert!(parse_style_transform("scaleX(120%)").is_err());
1684    }
1685
1686    #[test]
1687    fn transform_extremely_long_input_does_not_hang_or_panic() {
1688        // ~100k-digit number: must scan in linear time and saturate, not panic.
1689        let huge = alloc::format!("translateX({}px)", "9".repeat(100_000));
1690        let StyleTransform::TranslateX(px) = parse_style_transform(&huge).unwrap() else {
1691            panic!("expected TranslateX");
1692        };
1693        assert_eq!(px.number.number(), isize::MAX);
1694
1695        // Long garbage of the same size must simply be an error.
1696        let junk = alloc::format!("translateX({})", "a".repeat(100_000));
1697        assert!(parse_style_transform(&junk).is_err());
1698
1699        // Long stopword: no quadratic blowup in the stopword scan.
1700        let long_name = alloc::format!("{}(1px)", "x".repeat(100_000));
1701        assert!(parse_style_transform(&long_name).is_err());
1702    }
1703
1704    #[test]
1705    fn transform_deeply_nested_parens_do_not_stack_overflow() {
1706        // parse_parentheses is iterative; prove there is no recursion by feeding it
1707        // 10k levels of nesting.
1708        let open_only = "rotate(".repeat(10_000);
1709        assert!(matches!(
1710            parse_style_transform(&open_only).unwrap_err(),
1711            CssStyleTransformParseError::InvalidParenthesis(
1712                ParenthesisParseError::NoClosingBraceFound
1713            )
1714        ));
1715
1716        let braces = "(".repeat(10_000);
1717        assert!(matches!(
1718            parse_style_transform(&braces).unwrap_err(),
1719            CssStyleTransformParseError::InvalidParenthesis(
1720                ParenthesisParseError::StopWordNotFound("")
1721            )
1722        ));
1723
1724        let balanced = alloc::format!(
1725            "translateX({}1px{})",
1726            "translateX(".repeat(1_000),
1727            ")".repeat(1_000)
1728        );
1729        assert!(parse_style_transform(&balanced).is_err());
1730
1731        let closers = ")".repeat(10_000);
1732        assert!(parse_style_transform(&closers).is_err());
1733    }
1734
1735    #[test]
1736    fn transform_valid_minimal_positive_control() {
1737        assert_eq!(
1738            parse_style_transform("rotate(0deg)").unwrap(),
1739            StyleTransform::Rotate(AngleValue::deg(0.0))
1740        );
1741    }
1742
1743    // =====================================================================
1744    // parse_style_transform_vec
1745    // =====================================================================
1746
1747    #[test]
1748    fn transform_vec_accepts_empty_and_whitespace_only_input_as_an_empty_list() {
1749        // NOTE: "" is NOT an error - split_string_respect_whitespace yields zero
1750        // tokens and the collect() succeeds with an empty Vec. Callers relying on
1751        // `parse_style_transform_vec("").is_err()` to reject an empty declaration
1752        // will not get one. Pinned as current behaviour.
1753        for input in ["", "   ", "\t\n", "\r \n \t"] {
1754            let v = parse_style_transform_vec(input).unwrap();
1755            assert_eq!(v.len(), 0, "{input:?}");
1756        }
1757    }
1758
1759    #[test]
1760    fn transform_vec_propagates_the_first_error() {
1761        assert!(parse_style_transform_vec("translateX(10px) garbage").is_err());
1762        assert!(parse_style_transform_vec("garbage translateX(10px)").is_err());
1763        assert!(parse_style_transform_vec("translateX(10px) rotate(10px)").is_err());
1764        assert!(parse_style_transform_vec("\u{1F600}").is_err());
1765    }
1766
1767    #[test]
1768    fn transform_vec_keeps_whitespace_inside_parens_together() {
1769        // "scale(2, 0.5)" contains a space at depth 1 - it must stay one token.
1770        let v = parse_style_transform_vec("scale(2, 0.5) matrix(1, 0, 0, 1, 0, 0)").unwrap();
1771        assert_eq!(v.len(), 2);
1772        assert!(matches!(v.as_slice()[0], StyleTransform::Scale(_)));
1773        assert!(matches!(v.as_slice()[1], StyleTransform::Matrix(_)));
1774
1775        // Repeated / redundant whitespace collapses.
1776        let v = parse_style_transform_vec("  translateX(1px)\t\trotate(2deg)\n ").unwrap();
1777        assert_eq!(v.len(), 2);
1778    }
1779
1780    #[test]
1781    fn transform_vec_extremely_long_list_does_not_hang() {
1782        let long = "translateX(1px) ".repeat(20_000);
1783        let v = parse_style_transform_vec(&long).unwrap();
1784        assert_eq!(v.len(), 20_000);
1785        for t in v.as_slice() {
1786            assert_eq!(*t, StyleTransform::TranslateX(PixelValue::px(1.0)));
1787        }
1788    }
1789
1790    #[test]
1791    fn transform_vec_unbalanced_parens_do_not_underflow_the_depth_counter() {
1792        // split_string_respect_whitespace does `depth -= 1` on every ')' with no
1793        // floor; a run of closers drives it negative. Must not panic in debug.
1794        let closers = ")".repeat(10_000);
1795        assert!(parse_style_transform_vec(&closers).is_err());
1796        let mixed = alloc::format!("{} {}", ")".repeat(5_000), "(".repeat(5_000));
1797        assert!(parse_style_transform_vec(&mixed).is_err());
1798    }
1799
1800    // =====================================================================
1801    // parse_style_transform_origin
1802    // =====================================================================
1803
1804    #[test]
1805    fn transform_origin_requires_exactly_two_components() {
1806        for (input, got) in [("", 0), ("50%", 1), ("left", 1), ("50% 50% 50%", 3)] {
1807            let err = parse_style_transform_origin(input).unwrap_err();
1808            assert!(
1809                matches!(
1810                    err,
1811                    CssStyleTransformOriginParseError::WrongNumberOfComponents {
1812                        expected: 2,
1813                        got: g,
1814                        ..
1815                    } if g == got
1816                ),
1817                "{input:?} -> {err}"
1818            );
1819        }
1820        // Whitespace-only collapses to zero components.
1821        assert!(matches!(
1822            parse_style_transform_origin("   \t\n ").unwrap_err(),
1823            CssStyleTransformOriginParseError::WrongNumberOfComponents { got: 0, .. }
1824        ));
1825    }
1826
1827    #[test]
1828    fn transform_origin_keywords_are_position_sensitive() {
1829        assert_eq!(
1830            parse_style_transform_origin("left top").unwrap(),
1831            StyleTransformOrigin {
1832                x: PixelValue::percent(0.0),
1833                y: PixelValue::percent(0.0),
1834            }
1835        );
1836        assert_eq!(
1837            parse_style_transform_origin("right bottom").unwrap(),
1838            StyleTransformOrigin {
1839                x: PixelValue::percent(100.0),
1840                y: PixelValue::percent(100.0),
1841            }
1842        );
1843        assert_eq!(
1844            parse_style_transform_origin("center center").unwrap(),
1845            StyleTransformOrigin::default()
1846        );
1847        // BUG (spec deviation): CSS allows the keywords in either order
1848        // ("top left" == "left top"). Here the horizontal slot rejects
1849        // "top"/"bottom" and the vertical slot rejects "left"/"right", so the
1850        // swapped form is an error. Pinned as current behaviour.
1851        assert!(parse_style_transform_origin("top left").is_err());
1852        assert!(parse_style_transform_origin("bottom right").is_err());
1853        assert!(parse_style_transform_origin("left left").is_err());
1854        assert!(parse_style_transform_origin("top top").is_err());
1855    }
1856
1857    #[test]
1858    fn transform_origin_garbage_and_unicode_do_not_panic() {
1859        for input in [
1860            "\u{1F600} \u{1F600}",
1861            "left \u{1F600}",
1862            "NaN NaN",
1863            "auto auto",
1864            "-- --",
1865            "10 20",   // bare numbers -> px, actually valid
1866            "1px;2px", // no whitespace -> 1 component
1867        ] {
1868            let _ = parse_style_transform_origin(input);
1869        }
1870        // Bare numbers fall through to parse_pixel_value's px default.
1871        assert_eq!(
1872            parse_style_transform_origin("10 20").unwrap(),
1873            StyleTransformOrigin {
1874                x: PixelValue::px(10.0),
1875                y: PixelValue::px(20.0),
1876            }
1877        );
1878        assert!(matches!(
1879            parse_style_transform_origin("\u{1F600} \u{1F600}").unwrap_err(),
1880            CssStyleTransformOriginParseError::PixelValueParseError(_)
1881        ));
1882    }
1883
1884    #[test]
1885    fn transform_origin_boundary_numbers_saturate() {
1886        let o = parse_style_transform_origin("inf% -inf%").unwrap();
1887        assert_eq!(o.x.number.number(), isize::MAX);
1888        assert_eq!(o.y.number.number(), isize::MIN);
1889        assert_encodable(o.x);
1890        assert_encodable(o.y);
1891
1892        // NaN -> 0, keeping the metric.
1893        let o = parse_style_transform_origin("NaNpx NaN%").unwrap();
1894        assert_eq!(o.x, PixelValue::px(0.0));
1895        assert_eq!(o.y, PixelValue::percent(0.0));
1896
1897        let o = parse_style_transform_origin("-0px 1e400px").unwrap();
1898        assert_eq!(o.x.number.number(), 0);
1899        assert_eq!(o.y.number.number(), isize::MAX);
1900    }
1901
1902    #[test]
1903    fn transform_origin_extremely_long_input_does_not_hang() {
1904        let many = "50% ".repeat(20_000);
1905        assert!(matches!(
1906            parse_style_transform_origin(&many).unwrap_err(),
1907            CssStyleTransformOriginParseError::WrongNumberOfComponents { got: 20_000, .. }
1908        ));
1909        let huge = alloc::format!("{}px 0px", "9".repeat(100_000));
1910        assert!(parse_style_transform_origin(&huge).is_ok());
1911    }
1912
1913    #[test]
1914    fn transform_origin_round_trips_through_its_css_repr() {
1915        for origin in [
1916            StyleTransformOrigin::default(),
1917            StyleTransformOrigin {
1918                x: PixelValue::px(20.0),
1919                y: PixelValue::percent(100.0),
1920            },
1921            StyleTransformOrigin {
1922                x: PixelValue::em(-1.5),
1923                y: PixelValue::rem(2.25),
1924            },
1925            StyleTransformOrigin {
1926                x: PixelValue::px(0.0),
1927                y: PixelValue::px(0.0),
1928            },
1929        ] {
1930            let css = origin.print_as_css_value();
1931            let reparsed = parse_style_transform_origin(&css)
1932                .unwrap_or_else(|e| panic!("{css:?} did not re-parse: {e}"));
1933            assert_eq!(reparsed, origin, "round-trip failed for {css:?}");
1934        }
1935    }
1936
1937    // =====================================================================
1938    // parse_style_perspective_origin
1939    // =====================================================================
1940
1941    #[test]
1942    fn perspective_origin_requires_exactly_two_components() {
1943        for (input, got) in [("", 0), ("50%", 1), ("1px 2px 3px", 3)] {
1944            let err = parse_style_perspective_origin(input).unwrap_err();
1945            assert!(
1946                matches!(
1947                    err,
1948                    CssStylePerspectiveOriginParseError::WrongNumberOfComponents {
1949                        expected: 2,
1950                        got: g,
1951                        ..
1952                    } if g == got
1953                ),
1954                "{input:?} -> {err}"
1955            );
1956        }
1957    }
1958
1959    #[test]
1960    fn perspective_origin_does_not_accept_position_keywords() {
1961        // BUG (spec deviation): CSS `perspective-origin` accepts the same
1962        // left/center/right/top/bottom keywords as `transform-origin`, but this
1963        // parser only takes pixel values. Pinned as current behaviour.
1964        assert!(parse_style_perspective_origin("left top").is_err());
1965        assert!(parse_style_perspective_origin("center center").is_err());
1966        assert!(matches!(
1967            parse_style_perspective_origin("center center").unwrap_err(),
1968            CssStylePerspectiveOriginParseError::PixelValueParseError(_)
1969        ));
1970    }
1971
1972    #[test]
1973    fn perspective_origin_garbage_boundary_and_unicode() {
1974        for input in ["\u{1F600} \u{1F600}", "auto auto", "-- --", "px px"] {
1975            assert!(
1976                parse_style_perspective_origin(input).is_err(),
1977                "expected Err for {input:?}"
1978            );
1979        }
1980        let o = parse_style_perspective_origin("inf -inf").unwrap();
1981        assert_eq!(o.x.number.number(), isize::MAX);
1982        assert_eq!(o.y.number.number(), isize::MIN);
1983        assert_encodable(o.x);
1984        assert_encodable(o.y);
1985
1986        let o = parse_style_perspective_origin("NaN -0").unwrap();
1987        assert_eq!(o.x, PixelValue::px(0.0));
1988        assert_eq!(o.y, PixelValue::px(0.0));
1989
1990        let huge = alloc::format!("{}px 0px", "9".repeat(100_000));
1991        assert!(parse_style_perspective_origin(&huge).is_ok());
1992    }
1993
1994    #[test]
1995    fn perspective_origin_round_trips_through_its_css_repr() {
1996        for origin in [
1997            StylePerspectiveOrigin::default(),
1998            StylePerspectiveOrigin {
1999                x: PixelValue::px(100.0),
2000                y: PixelValue::percent(50.0),
2001            },
2002            StylePerspectiveOrigin {
2003                x: PixelValue::pt(-3.5),
2004                y: PixelValue::cm(1.0),
2005            },
2006        ] {
2007            let css = origin.print_as_css_value();
2008            let reparsed = parse_style_perspective_origin(&css)
2009                .unwrap_or_else(|e| panic!("{css:?} did not re-parse: {e}"));
2010            assert_eq!(reparsed, origin, "round-trip failed for {css:?}");
2011        }
2012    }
2013
2014    // =====================================================================
2015    // parse_style_backface_visibility
2016    // =====================================================================
2017
2018    #[test]
2019    fn backface_visibility_accepts_only_the_two_keywords() {
2020        assert_eq!(
2021            parse_style_backface_visibility("visible").unwrap(),
2022            StyleBackfaceVisibility::Visible
2023        );
2024        assert_eq!(
2025            parse_style_backface_visibility("hidden").unwrap(),
2026            StyleBackfaceVisibility::Hidden
2027        );
2028        // Surrounding whitespace is trimmed.
2029        assert_eq!(
2030            parse_style_backface_visibility("  \t visible \n ").unwrap(),
2031            StyleBackfaceVisibility::Visible
2032        );
2033        // Everything else is rejected - including case variants, substrings,
2034        // both keywords at once and zero-width joiners.
2035        for input in [
2036            "",
2037            "   ",
2038            "Visible",
2039            "HIDDEN",
2040            "visible hidden",
2041            "vis",
2042            "visiblee",
2043            "none",
2044            "0",
2045            "NaN",
2046            "\u{1F600}",
2047            "visible\u{200B}",
2048            "hidden;",
2049        ] {
2050            assert!(
2051                parse_style_backface_visibility(input).is_err(),
2052                "expected Err for {input:?}"
2053            );
2054        }
2055    }
2056
2057    #[test]
2058    fn backface_visibility_error_carries_the_untrimmed_input() {
2059        // The match is on `input.trim()` but the error is built from `input`,
2060        // so the original (untrimmed) slice is what shows up in the message.
2061        let err = parse_style_backface_visibility("  bogus  ").unwrap_err();
2062        assert_eq!(
2063            err,
2064            CssBackfaceVisibilityParseError::InvalidValue("  bogus  ")
2065        );
2066        assert!(alloc::format!("{err}").contains("  bogus  "));
2067    }
2068
2069    #[test]
2070    fn backface_visibility_extremely_long_input_does_not_hang() {
2071        let huge = "visible".repeat(100_000);
2072        assert!(parse_style_backface_visibility(&huge).is_err());
2073    }
2074
2075    #[test]
2076    fn backface_visibility_round_trips_through_its_css_repr() {
2077        for v in [
2078            StyleBackfaceVisibility::Visible,
2079            StyleBackfaceVisibility::Hidden,
2080        ] {
2081            let css = v.print_as_css_value();
2082            assert_eq!(parse_style_backface_visibility(&css).unwrap(), v);
2083        }
2084        assert_eq!(
2085            StyleBackfaceVisibility::default(),
2086            StyleBackfaceVisibility::Visible
2087        );
2088    }
2089
2090    // =====================================================================
2091    // StyleTransform / StyleTransformVec round-trips (encode == decode)
2092    // =====================================================================
2093
2094    #[test]
2095    fn transform_print_parse_round_trip() {
2096        for t in all_roundtrippable_transforms() {
2097            let css = t.print_as_css_value();
2098            let reparsed = parse_style_transform(&css)
2099                .unwrap_or_else(|e| panic!("{css:?} did not re-parse: {e}"));
2100            assert_eq!(reparsed, t, "round-trip failed for {css:?}");
2101        }
2102    }
2103
2104    #[test]
2105    fn transform_vec_print_parse_round_trip() {
2106        let v: StyleTransformVec = all_roundtrippable_transforms().into();
2107        let css = v.print_as_css_value();
2108        let reparsed = parse_style_transform_vec(&css)
2109            .unwrap_or_else(|e| panic!("{css:?} did not re-parse: {e}"));
2110        assert_eq!(reparsed.len(), v.len());
2111        assert_eq!(reparsed.as_slice(), v.as_slice());
2112    }
2113
2114    #[test]
2115    fn scale_axis_print_does_not_round_trip() {
2116        // BUG: `StyleTransform::ScaleX/Y/Z` hold a `PercentageValue`, whose Display
2117        // appends a '%' ("scaleX(120%)"), but the parser reads the argument with a
2118        // bare `parse::<f32>()` and multiplies by 100. So print -> parse fails for
2119        // every ScaleX/ScaleY/ScaleZ, and the printed CSS is invalid per spec
2120        // (`scaleX()` takes a <number>, not a <percentage>).
2121        // Pinned as current behaviour; the fix is to print the raw number.
2122        for t in [
2123            StyleTransform::ScaleX(PercentageValue::new(120.0)),
2124            StyleTransform::ScaleY(PercentageValue::new(120.0)),
2125            StyleTransform::ScaleZ(PercentageValue::new(120.0)),
2126        ] {
2127            let css = t.print_as_css_value();
2128            assert!(css.contains('%'), "{css:?}");
2129            assert!(
2130                parse_style_transform(&css).is_err(),
2131                "{css:?} unexpectedly re-parsed - the ScaleX round-trip bug may be fixed"
2132            );
2133        }
2134        // The parser's own accepted form (a bare number) does work.
2135        assert_eq!(
2136            parse_style_transform("scaleX(1.2)").unwrap(),
2137            StyleTransform::ScaleX(PercentageValue::new(120.0))
2138        );
2139    }
2140
2141    // =====================================================================
2142    // StyleTransformOrigin::interpolate / StylePerspectiveOrigin::interpolate
2143    // =====================================================================
2144
2145    #[test]
2146    fn transform_origin_interpolate_endpoints_are_exact() {
2147        let a = StyleTransformOrigin {
2148            x: PixelValue::px(10.0),
2149            y: PixelValue::percent(0.0),
2150        };
2151        let b = StyleTransformOrigin {
2152            x: PixelValue::px(30.0),
2153            y: PixelValue::percent(100.0),
2154        };
2155        assert_eq!(a.interpolate(&b, 0.0), a);
2156        assert_eq!(a.interpolate(&b, 1.0), b);
2157        assert_eq!(
2158            a.interpolate(&b, 0.5),
2159            StyleTransformOrigin {
2160                x: PixelValue::px(20.0),
2161                y: PixelValue::percent(50.0),
2162            }
2163        );
2164        // Interpolating a value with itself is the identity for every finite t.
2165        for t in [-1.0, 0.0, 0.25, 1.0, 2.0, 1e30] {
2166            assert_eq!(a.interpolate(&a, t), a, "t = {t}");
2167        }
2168    }
2169
2170    #[test]
2171    fn transform_origin_interpolate_extrapolates_outside_zero_one() {
2172        let a = StyleTransformOrigin {
2173            x: PixelValue::px(10.0),
2174            y: PixelValue::px(10.0),
2175        };
2176        let b = StyleTransformOrigin {
2177            x: PixelValue::px(30.0),
2178            y: PixelValue::px(30.0),
2179        };
2180        // t is NOT clamped.
2181        assert_eq!(a.interpolate(&b, -1.0).x, PixelValue::px(-10.0));
2182        assert_eq!(a.interpolate(&b, 2.0).x, PixelValue::px(50.0));
2183    }
2184
2185    #[test]
2186    fn transform_origin_interpolate_with_nan_or_infinite_t_stays_defined() {
2187        let a = StyleTransformOrigin {
2188            x: PixelValue::px(10.0),
2189            y: PixelValue::percent(10.0),
2190        };
2191        let b = StyleTransformOrigin {
2192            x: PixelValue::px(30.0),
2193            y: PixelValue::percent(30.0),
2194        };
2195
2196        // NaN t -> NaN value -> the f32->isize cast maps NaN to 0.
2197        let nan = a.interpolate(&b, f32::NAN);
2198        assert_eq!(nan.x.number.number(), 0);
2199        assert_eq!(nan.y.number.number(), 0);
2200        assert_eq!(nan.x.metric, SizeMetric::Px);
2201        assert_eq!(nan.y.metric, SizeMetric::Percent);
2202        assert_encodable(nan.x);
2203        assert_encodable(nan.y);
2204
2205        // +inf t on an increasing range saturates to isize::MAX, -inf to isize::MIN.
2206        let pos = a.interpolate(&b, f32::INFINITY);
2207        assert_eq!(pos.x.number.number(), isize::MAX);
2208        assert_encodable(pos.x);
2209        let neg = a.interpolate(&b, f32::NEG_INFINITY);
2210        assert_eq!(neg.x.number.number(), isize::MIN);
2211        assert_encodable(neg.x);
2212
2213        // inf * 0 (identical endpoints) is NaN, which collapses to 0.
2214        let degenerate = a.interpolate(&a, f32::INFINITY);
2215        assert_eq!(degenerate.x.number.number(), 0);
2216        assert_encodable(degenerate.x);
2217    }
2218
2219    #[test]
2220    fn transform_origin_interpolate_between_saturated_extremes_does_not_panic() {
2221        let a = StyleTransformOrigin {
2222            x: PixelValue::px(f32::MAX),
2223            y: PixelValue::px(f32::MIN),
2224        };
2225        let b = StyleTransformOrigin {
2226            x: PixelValue::px(f32::MIN),
2227            y: PixelValue::px(f32::MAX),
2228        };
2229        // Both endpoints are already clamped to isize::MAX / isize::MIN.
2230        assert_eq!(a.x.number.number(), isize::MAX);
2231        assert_eq!(a.y.number.number(), isize::MIN);
2232
2233        for t in [
2234            -1e30,
2235            -1.0,
2236            0.0,
2237            0.5,
2238            1.0,
2239            1e30,
2240            f32::NAN,
2241            f32::INFINITY,
2242            f32::NEG_INFINITY,
2243        ] {
2244            let out = a.interpolate(&b, t);
2245            assert_encodable(out.x);
2246            assert_encodable(out.y);
2247        }
2248    }
2249
2250    #[test]
2251    fn transform_origin_interpolate_across_metrics_falls_back_to_px() {
2252        let a = StyleTransformOrigin {
2253            x: PixelValue::px(0.0),
2254            y: PixelValue::px(0.0),
2255        };
2256        let b = StyleTransformOrigin {
2257            x: PixelValue::percent(100.0),
2258            y: PixelValue::em(2.0),
2259        };
2260        for t in [0.0, 0.5, 1.0, f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
2261            let out = a.interpolate(&b, t);
2262            assert_eq!(out.x.metric, SizeMetric::Px, "t = {t}");
2263            assert_eq!(out.y.metric, SizeMetric::Px, "t = {t}");
2264            assert_encodable(out.x);
2265            assert_encodable(out.y);
2266        }
2267    }
2268
2269    #[test]
2270    fn perspective_origin_interpolate_matches_transform_origin_semantics() {
2271        let a = StylePerspectiveOrigin {
2272            x: PixelValue::px(10.0),
2273            y: PixelValue::percent(0.0),
2274        };
2275        let b = StylePerspectiveOrigin {
2276            x: PixelValue::px(30.0),
2277            y: PixelValue::percent(100.0),
2278        };
2279        assert_eq!(a.interpolate(&b, 0.0), a);
2280        assert_eq!(a.interpolate(&b, 1.0), b);
2281        assert_eq!(
2282            a.interpolate(&b, 0.5),
2283            StylePerspectiveOrigin {
2284                x: PixelValue::px(20.0),
2285                y: PixelValue::percent(50.0),
2286            }
2287        );
2288        // Default is 0px 0px, and interpolating it with itself is stable.
2289        let d = StylePerspectiveOrigin::default();
2290        assert_eq!(d.interpolate(&d, 0.5), d);
2291
2292        // NaN / inf are defined, not panics.
2293        let nan = a.interpolate(&b, f32::NAN);
2294        assert_eq!(nan.x.number.number(), 0);
2295        assert_encodable(nan.x);
2296        let inf = a.interpolate(&b, f32::INFINITY);
2297        assert_eq!(inf.x.number.number(), isize::MAX);
2298        assert_encodable(inf.x);
2299
2300        // Saturated extremes.
2301        let lo = StylePerspectiveOrigin {
2302            x: PixelValue::px(f32::MIN),
2303            y: PixelValue::px(f32::MIN),
2304        };
2305        let hi = StylePerspectiveOrigin {
2306            x: PixelValue::px(f32::MAX),
2307            y: PixelValue::px(f32::MAX),
2308        };
2309        for t in [0.0, 0.5, 1.0, f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
2310            let out = lo.interpolate(&hi, t);
2311            assert_encodable(out.x);
2312            assert_encodable(out.y);
2313        }
2314    }
2315
2316    // =====================================================================
2317    // Error types: to_contained / to_shared round-trips + Display invariants
2318    // =====================================================================
2319
2320    fn transform_errors() -> Vec<CssStyleTransformParseError<'static>> {
2321        vec![
2322            CssStyleTransformParseError::InvalidTransform("rotate"),
2323            // Edge: empty payload.
2324            CssStyleTransformParseError::InvalidTransform(""),
2325            CssStyleTransformParseError::InvalidTransform("\u{1F600}"),
2326            CssStyleTransformParseError::InvalidParenthesis(ParenthesisParseError::EmptyInput),
2327            CssStyleTransformParseError::InvalidParenthesis(ParenthesisParseError::UnclosedBraces),
2328            CssStyleTransformParseError::InvalidParenthesis(
2329                ParenthesisParseError::NoOpeningBraceFound,
2330            ),
2331            CssStyleTransformParseError::InvalidParenthesis(
2332                ParenthesisParseError::NoClosingBraceFound,
2333            ),
2334            CssStyleTransformParseError::InvalidParenthesis(
2335                ParenthesisParseError::StopWordNotFound("nope"),
2336            ),
2337            CssStyleTransformParseError::WrongNumberOfComponents {
2338                expected: 6,
2339                got: 5,
2340                input: "1,2,3,4,5",
2341            },
2342            // Edge: extreme counts + empty input.
2343            CssStyleTransformParseError::WrongNumberOfComponents {
2344                expected: usize::MAX,
2345                got: usize::MAX,
2346                input: "",
2347            },
2348            CssStyleTransformParseError::WrongNumberOfComponents {
2349                expected: 0,
2350                got: 0,
2351                input: "",
2352            },
2353            CssStyleTransformParseError::NumberParseError("x".parse::<f32>().unwrap_err()),
2354            CssStyleTransformParseError::NumberParseError("".parse::<f32>().unwrap_err()),
2355            CssStyleTransformParseError::PixelValueParseError(CssPixelValueParseError::EmptyString),
2356            CssStyleTransformParseError::PixelValueParseError(
2357                CssPixelValueParseError::InvalidPixelValue("auto"),
2358            ),
2359            CssStyleTransformParseError::AngleValueParseError(CssAngleValueParseError::EmptyString),
2360            CssStyleTransformParseError::AngleValueParseError(
2361                CssAngleValueParseError::InvalidAngle(""),
2362            ),
2363            CssStyleTransformParseError::PercentageValueParseError(
2364                PercentageParseError::NoPercentSign,
2365            ),
2366            CssStyleTransformParseError::PercentageValueParseError(
2367                PercentageParseError::InvalidUnit(AzString::from("")),
2368            ),
2369        ]
2370    }
2371
2372    #[test]
2373    fn transform_parse_error_round_trips_through_owned() {
2374        for err in transform_errors() {
2375            let owned = err.to_contained();
2376            let shared = owned.to_shared();
2377            assert_eq!(shared, err, "round-trip failed for {err}");
2378            // Re-owning the shared copy must be stable.
2379            assert_eq!(shared.to_contained(), owned);
2380        }
2381    }
2382
2383    #[test]
2384    fn transform_parse_error_round_trips_errors_from_the_real_parsers() {
2385        // Errors that actually come out of the parsers (rather than hand-built).
2386        for input in [
2387            "",
2388            "garbage",
2389            "translatex(1px)",
2390            "matrix(1,2,3)",
2391            "rotate(10px)",
2392            "translateX(auto)",
2393            "scaleX(abc)",
2394            "translate3d(1px,2px)",
2395        ] {
2396            let err = parse_style_transform(input).unwrap_err();
2397            assert_eq!(err.to_contained().to_shared(), err, "for {input:?}");
2398            assert!(!alloc::format!("{err}").is_empty());
2399            // Debug is implemented as Display (impl_debug_as_display!).
2400            assert_eq!(alloc::format!("{err:?}"), alloc::format!("{err}"));
2401        }
2402    }
2403
2404    #[test]
2405    fn transform_origin_parse_error_round_trips_through_owned() {
2406        let errs = [
2407            CssStyleTransformOriginParseError::WrongNumberOfComponents {
2408                expected: 2,
2409                got: 0,
2410                input: "",
2411            },
2412            CssStyleTransformOriginParseError::WrongNumberOfComponents {
2413                expected: usize::MAX,
2414                got: usize::MAX,
2415                input: "\u{1F600}",
2416            },
2417            CssStyleTransformOriginParseError::PixelValueParseError(
2418                CssPixelValueParseError::EmptyString,
2419            ),
2420            CssStyleTransformOriginParseError::PixelValueParseError(
2421                CssPixelValueParseError::InvalidPixelValue(""),
2422            ),
2423        ];
2424        for err in errs {
2425            let owned = err.to_contained();
2426            assert_eq!(owned.to_shared(), err, "round-trip failed for {err}");
2427            assert!(!alloc::format!("{err}").is_empty());
2428        }
2429        // ...and one straight out of the parser.
2430        let err = parse_style_transform_origin("top left").unwrap_err();
2431        assert_eq!(err.to_contained().to_shared(), err);
2432    }
2433
2434    #[test]
2435    fn perspective_origin_parse_error_round_trips_through_owned() {
2436        let errs = [
2437            CssStylePerspectiveOriginParseError::WrongNumberOfComponents {
2438                expected: 2,
2439                got: 0,
2440                input: "",
2441            },
2442            CssStylePerspectiveOriginParseError::WrongNumberOfComponents {
2443                expected: 0,
2444                got: usize::MAX,
2445                input: "\u{1F600}\u{0301}",
2446            },
2447            CssStylePerspectiveOriginParseError::PixelValueParseError(
2448                CssPixelValueParseError::EmptyString,
2449            ),
2450        ];
2451        for err in errs {
2452            let owned = err.to_contained();
2453            assert_eq!(owned.to_shared(), err, "round-trip failed for {err}");
2454            assert!(!alloc::format!("{err}").is_empty());
2455        }
2456        let err = parse_style_perspective_origin("center center").unwrap_err();
2457        assert_eq!(err.to_contained().to_shared(), err);
2458    }
2459
2460    #[test]
2461    fn backface_visibility_parse_error_round_trips_through_owned() {
2462        for payload in ["", "none", "\u{1F600}", "  bogus  "] {
2463            let err = CssBackfaceVisibilityParseError::InvalidValue(payload);
2464            let owned = err.to_contained();
2465            assert_eq!(owned.to_shared(), err, "round-trip failed for {payload:?}");
2466            assert!(alloc::format!("{err}").contains(payload) || payload.is_empty());
2467        }
2468        let err = parse_style_backface_visibility("nope").unwrap_err();
2469        assert_eq!(err.to_contained().to_shared(), err);
2470    }
2471
2472    #[test]
2473    fn owned_errors_borrow_from_themselves_not_from_the_original_input() {
2474        // to_contained() must deep-copy the &str payload: the owned error has to
2475        // outlive the input it was parsed from.
2476        let owned = {
2477            let input = String::from("translatex(1px)");
2478            parse_style_transform(&input).unwrap_err().to_contained()
2479        };
2480        assert_eq!(
2481            owned,
2482            CssStyleTransformParseErrorOwned::InvalidParenthesis(
2483                ParenthesisParseErrorOwned::StopWordNotFound(AzString::from("translatex"))
2484            )
2485        );
2486        // And the re-shared borrow points at the owned buffer.
2487        assert!(matches!(
2488            owned.to_shared(),
2489            CssStyleTransformParseError::InvalidParenthesis(
2490                ParenthesisParseError::StopWordNotFound("translatex")
2491            )
2492        ));
2493    }
2494
2495    #[test]
2496    fn wrong_number_of_components_preserves_counts_across_the_owned_conversion() {
2497        let err = CssStyleTransformParseError::WrongNumberOfComponents {
2498            expected: usize::MAX,
2499            got: 0,
2500            input: "\u{1F600}",
2501        };
2502        let CssStyleTransformParseErrorOwned::WrongNumberOfComponents(WrongComponentCountError {
2503            expected,
2504            got,
2505            input,
2506        }) = err.to_contained()
2507        else {
2508            panic!("expected WrongNumberOfComponents");
2509        };
2510        assert_eq!(expected, usize::MAX);
2511        assert_eq!(got, 0);
2512        assert_eq!(input.as_str(), "\u{1F600}");
2513    }
2514
2515    // =====================================================================
2516    // parse_float_value (re-exported into this module's parse path)
2517    // =====================================================================
2518
2519    #[test]
2520    fn float_value_parse_helper_saturates_and_rejects_garbage() {
2521        assert_eq!(parse_float_value("1.5").unwrap(), FloatValue::new(1.5));
2522        assert_eq!(parse_float_value("  -0  ").unwrap(), FloatValue::new(0.0));
2523        assert_eq!(parse_float_value("inf").unwrap().number(), isize::MAX);
2524        assert_eq!(parse_float_value("-inf").unwrap().number(), isize::MIN);
2525        assert_eq!(parse_float_value("NaN").unwrap().number(), 0);
2526        assert!(parse_float_value("").is_err());
2527        assert!(parse_float_value("abc").is_err());
2528        assert!(parse_float_value("\u{1F600}").is_err());
2529    }
2530}
2531
2532#[cfg(all(test, feature = "parser"))]
2533mod app_region_tests {
2534    use super::*;
2535
2536    /// Both spellings must reach the same property, so CSS written for Electron
2537    /// works unchanged.
2538    #[test]
2539    fn both_spellings_map_to_the_same_property() {
2540        use crate::props::property::{get_css_key_map, CssPropertyType};
2541        let map = get_css_key_map();
2542        // Go through the public lookup rather than the private table: this is
2543        // the path a stylesheet actually takes.
2544        assert_eq!(
2545            CssPropertyType::from_str("-azul-app-region", &map),
2546            Some(CssPropertyType::AppRegion)
2547        );
2548        assert_eq!(
2549            CssPropertyType::from_str("-webkit-app-region", &map),
2550            Some(CssPropertyType::AppRegion),
2551            "Electron's spelling must be accepted verbatim"
2552        );
2553    }
2554
2555    #[test]
2556    fn drag_and_no_drag_parse_and_round_trip() {
2557        assert_eq!(parse_style_app_region("drag"), Ok(StyleAppRegion::Drag));
2558        assert_eq!(
2559            parse_style_app_region(" no-drag "),
2560            Ok(StyleAppRegion::NoDrag)
2561        );
2562        // `none` is what someone reaches for after writing `-webkit-app-region:
2563        // none` out of habit; accept it rather than silently dropping the rule.
2564        assert_eq!(parse_style_app_region("none"), Ok(StyleAppRegion::NoDrag));
2565        assert!(parse_style_app_region("sometimes").is_err());
2566
2567        assert_eq!(StyleAppRegion::Drag.print_as_css_value(), "drag");
2568        assert_eq!(StyleAppRegion::NoDrag.print_as_css_value(), "no-drag");
2569    }
2570
2571    /// The DEFAULT must be NoDrag. A default of Drag would make every element
2572    /// in the tree move the window.
2573    #[test]
2574    fn the_default_is_not_draggable() {
2575        assert_eq!(StyleAppRegion::default(), StyleAppRegion::NoDrag);
2576    }
2577}