Skip to main content

azul_css/props/style/
background.rs

1//! CSS properties for backgrounds, including colors, images, and gradients.
2
3use alloc::{
4    string::{String, ToString},
5    vec::Vec,
6};
7use core::fmt;
8
9#[cfg(feature = "parser")]
10use crate::props::basic::{
11    error::{InvalidValueErr, InvalidValueErrOwned},
12    parse::{
13        parse_parentheses, parse_image, split_string_respect_comma,
14        CssImageParseError, CssImageParseErrorOwned,
15        ParenthesisParseError, ParenthesisParseErrorOwned,
16    },
17    color::parse_color_or_system,
18};
19use crate::{
20    corety::AzString,
21    codegen::format::GetHash,
22    props::{
23        basic::{
24            angle::{
25                parse_angle_value, AngleValue, CssAngleValueParseError,
26                CssAngleValueParseErrorOwned, OptionAngleValue,
27            },
28            color::{ColorU, ColorOrSystem, SystemColorRef, CssColorParseError, CssColorParseErrorOwned},
29            direction::{
30                parse_direction, CssDirectionParseError, CssDirectionParseErrorOwned, Direction,
31            },
32            length::{
33                parse_percentage_value, OptionPercentageValue, PercentageParseError,
34                PercentageParseErrorOwned, PercentageValue,
35            },
36            pixel::{
37                parse_pixel_value, CssPixelValueParseError, CssPixelValueParseErrorOwned,
38                PixelValue,
39            },
40        },
41        formatter::PrintAsCssValue,
42    },
43};
44
45// --- TYPE DEFINITIONS ---
46
47/// Whether a `gradient` should be repeated or clamped to the edges.
48#[derive(Debug, Copy, Clone, PartialEq, Ord, PartialOrd, Eq, Hash)]
49#[repr(C)]
50#[derive(Default)]
51pub enum ExtendMode {
52    #[default]
53    Clamp,
54    Repeat,
55}
56
57// -- Main Background Content Type --
58
59/// A single CSS background layer: a solid color, image URL, or gradient.
60#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
61#[repr(C, u8)]
62pub enum StyleBackgroundContent {
63    LinearGradient(LinearGradient),
64    RadialGradient(RadialGradient),
65    ConicGradient(ConicGradient),
66    Image(AzString),
67    Color(ColorU),
68    /// A theme-aware system color (e.g. `background: system:accent`), kept unresolved
69    /// and resolved at render time. Mirrors the `ColorOrSystem::System` value that
70    /// gradient color stops already accept.
71    SystemColor(SystemColorRef),
72}
73
74impl_option!(
75    StyleBackgroundContent,
76    OptionStyleBackgroundContent,
77    copy = false,
78    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
79);
80
81impl_vec!(StyleBackgroundContent, StyleBackgroundContentVec, StyleBackgroundContentVecDestructor, StyleBackgroundContentVecDestructorType, StyleBackgroundContentVecSlice, OptionStyleBackgroundContent);
82impl_vec_debug!(StyleBackgroundContent, StyleBackgroundContentVec);
83impl_vec_partialord!(StyleBackgroundContent, StyleBackgroundContentVec);
84impl_vec_ord!(StyleBackgroundContent, StyleBackgroundContentVec);
85impl_vec_clone!(
86    StyleBackgroundContent,
87    StyleBackgroundContentVec,
88    StyleBackgroundContentVecDestructor
89);
90impl_vec_partialeq!(StyleBackgroundContent, StyleBackgroundContentVec);
91impl_vec_eq!(StyleBackgroundContent, StyleBackgroundContentVec);
92impl_vec_hash!(StyleBackgroundContent, StyleBackgroundContentVec);
93
94impl Default for StyleBackgroundContent {
95    fn default() -> Self {
96        Self::Color(ColorU::TRANSPARENT)
97    }
98}
99
100impl PrintAsCssValue for StyleBackgroundContent {
101    fn print_as_css_value(&self) -> String {
102        match self {
103            Self::LinearGradient(lg) => {
104                let prefix = if lg.extend_mode == ExtendMode::Repeat {
105                    "repeating-linear-gradient"
106                } else {
107                    "linear-gradient"
108                };
109                format!("{}({})", prefix, lg.print_as_css_value())
110            }
111            Self::RadialGradient(rg) => {
112                let prefix = if rg.extend_mode == ExtendMode::Repeat {
113                    "repeating-radial-gradient"
114                } else {
115                    "radial-gradient"
116                };
117                format!("{}({})", prefix, rg.print_as_css_value())
118            }
119            Self::ConicGradient(cg) => {
120                let prefix = if cg.extend_mode == ExtendMode::Repeat {
121                    "repeating-conic-gradient"
122                } else {
123                    "conic-gradient"
124                };
125                format!("{}({})", prefix, cg.print_as_css_value())
126            }
127            Self::Image(id) => format!("url(\"{}\")", id.as_str()),
128            Self::Color(c) => c.to_hash(),
129            Self::SystemColor(s) => s.as_css_str().to_string(),
130        }
131    }
132}
133
134// Formatting to Rust code for background-related vecs
135
136impl crate::codegen::format::FormatAsRustCode for StyleBackgroundContent {
137    fn format_as_rust_code(&self, _tabs: usize) -> String {
138        // Delegate to the CSS value representation for single backgrounds
139        format!("StyleBackgroundContent::from_css(\"{}\")", self.print_as_css_value())
140    }
141}
142
143impl crate::codegen::format::FormatAsRustCode for StyleBackgroundSizeVec {
144    fn format_as_rust_code(&self, _tabs: usize) -> String {
145        format!(
146            "StyleBackgroundSizeVec::from_const_slice(STYLE_BACKGROUND_SIZE_{}_ITEMS)",
147            self.get_hash()
148        )
149    }
150}
151
152impl crate::codegen::format::FormatAsRustCode for StyleBackgroundRepeatVec {
153    fn format_as_rust_code(&self, _tabs: usize) -> String {
154        format!(
155            "StyleBackgroundRepeatVec::from_const_slice(STYLE_BACKGROUND_REPEAT_{}_ITEMS)",
156            self.get_hash()
157        )
158    }
159}
160
161impl crate::codegen::format::FormatAsRustCode for StyleBackgroundContentVec {
162    fn format_as_rust_code(&self, _tabs: usize) -> String {
163        format!(
164            "StyleBackgroundContentVec::from_const_slice(STYLE_BACKGROUND_CONTENT_{}_ITEMS)",
165            self.get_hash()
166        )
167    }
168}
169
170impl PrintAsCssValue for StyleBackgroundContentVec {
171    fn print_as_css_value(&self) -> String {
172        self.as_ref()
173            .iter()
174            .map(PrintAsCssValue::print_as_css_value)
175            .collect::<Vec<_>>()
176            .join(", ")
177    }
178}
179
180// -- Gradient Types --
181
182/// A CSS `linear-gradient()` or `repeating-linear-gradient()` value.
183#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
184#[repr(C)]
185pub struct LinearGradient {
186    pub direction: Direction,
187    pub extend_mode: ExtendMode,
188    pub stops: NormalizedLinearColorStopVec,
189}
190impl Default for LinearGradient {
191    fn default() -> Self {
192        Self {
193            direction: Direction::default(),
194            extend_mode: ExtendMode::default(),
195            stops: Vec::new().into(),
196        }
197    }
198}
199impl PrintAsCssValue for LinearGradient {
200    fn print_as_css_value(&self) -> String {
201        let dir_str = self.direction.print_as_css_value();
202        let stops_str = self
203            .stops
204            .iter()
205            .map(PrintAsCssValue::print_as_css_value)
206            .collect::<Vec<_>>()
207            .join(", ");
208        if stops_str.is_empty() {
209            dir_str
210        } else {
211            format!("{dir_str}, {stops_str}")
212        }
213    }
214}
215
216/// A CSS `radial-gradient()` or `repeating-radial-gradient()` value.
217#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
218#[repr(C)]
219pub struct RadialGradient {
220    pub shape: Shape,
221    pub size: RadialGradientSize,
222    pub position: StyleBackgroundPosition,
223    pub extend_mode: ExtendMode,
224    pub stops: NormalizedLinearColorStopVec,
225}
226impl Default for RadialGradient {
227    fn default() -> Self {
228        Self {
229            shape: Shape::default(),
230            size: RadialGradientSize::default(),
231            position: StyleBackgroundPosition::default(),
232            extend_mode: ExtendMode::default(),
233            stops: Vec::new().into(),
234        }
235    }
236}
237impl PrintAsCssValue for RadialGradient {
238    fn print_as_css_value(&self) -> String {
239        let stops_str = self
240            .stops
241            .iter()
242            .map(PrintAsCssValue::print_as_css_value)
243            .collect::<Vec<_>>()
244            .join(", ");
245        format!(
246            "{} {} at {}, {}",
247            self.shape,
248            self.size,
249            self.position.print_as_css_value(),
250            stops_str
251        )
252    }
253}
254
255/// A CSS `conic-gradient()` or `repeating-conic-gradient()` value.
256#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
257#[repr(C)]
258pub struct ConicGradient {
259    pub extend_mode: ExtendMode,
260    pub center: StyleBackgroundPosition,
261    pub angle: AngleValue,
262    pub stops: NormalizedRadialColorStopVec,
263}
264impl Default for ConicGradient {
265    fn default() -> Self {
266        Self {
267            extend_mode: ExtendMode::default(),
268            // CSS default for conic-gradient is `at center` (50% 50%), NOT the generic
269            // Left/Top of StyleBackgroundPosition::default() — a corner-anchored cone maps
270            // the whole element into one angular slice and renders a flat color.
271            center: StyleBackgroundPosition {
272                horizontal: BackgroundPositionHorizontal::Center,
273                vertical: BackgroundPositionVertical::Center,
274            },
275            angle: AngleValue::default(),
276            stops: Vec::new().into(),
277        }
278    }
279}
280impl PrintAsCssValue for ConicGradient {
281    fn print_as_css_value(&self) -> String {
282        let stops_str = self
283            .stops
284            .iter()
285            .map(PrintAsCssValue::print_as_css_value)
286            .collect::<Vec<_>>()
287            .join(", ");
288        format!(
289            "from {} at {}, {}",
290            self.angle,
291            self.center.print_as_css_value(),
292            stops_str
293        )
294    }
295}
296
297// -- Gradient Sub-types --
298
299/// The shape of a radial gradient: `circle` or `ellipse`.
300#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
301#[repr(C)]
302#[derive(Default)]
303pub enum Shape {
304    #[default]
305    Ellipse,
306    Circle,
307}
308impl fmt::Display for Shape {
309    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
310        write!(
311            f,
312            "{}",
313            match self {
314                Self::Ellipse => "ellipse",
315                Self::Circle => "circle",
316            }
317        )
318    }
319}
320
321/// The sizing keyword for a radial gradient (e.g. `closest-side`, `farthest-corner`).
322#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
323#[repr(C)]
324#[derive(Default)]
325pub enum RadialGradientSize {
326    ClosestSide,
327    ClosestCorner,
328    FarthestSide,
329    #[default]
330    FarthestCorner,
331}
332impl fmt::Display for RadialGradientSize {
333    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
334        write!(
335            f,
336            "{}",
337            match self {
338                Self::ClosestSide => "closest-side",
339                Self::ClosestCorner => "closest-corner",
340                Self::FarthestSide => "farthest-side",
341                Self::FarthestCorner => "farthest-corner",
342            }
343        )
344    }
345}
346
347#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
348#[repr(C)]
349pub struct NormalizedLinearColorStop {
350    pub offset: PercentageValue,
351    /// Color for this gradient stop. Can be a concrete color or a system color reference.
352    pub color: ColorOrSystem,
353}
354
355impl NormalizedLinearColorStop {
356    /// Create a new normalized linear color stop with a concrete color.
357    #[must_use] pub const fn new(offset: PercentageValue, color: ColorU) -> Self {
358        Self { offset, color: ColorOrSystem::color(color) }
359    }
360
361    /// Resolve the color against system colors.
362    #[must_use] pub fn resolve(&self, system_colors: &crate::system::SystemColors, fallback: ColorU) -> ColorU {
363        self.color.resolve(system_colors, fallback)
364    }
365}
366
367impl_option!(
368    NormalizedLinearColorStop,
369    OptionNormalizedLinearColorStop,
370    copy = false,
371    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
372);
373impl_vec!(NormalizedLinearColorStop, NormalizedLinearColorStopVec, NormalizedLinearColorStopVecDestructor, NormalizedLinearColorStopVecDestructorType, NormalizedLinearColorStopVecSlice, OptionNormalizedLinearColorStop);
374impl_vec_debug!(NormalizedLinearColorStop, NormalizedLinearColorStopVec);
375impl_vec_partialord!(NormalizedLinearColorStop, NormalizedLinearColorStopVec);
376impl_vec_ord!(NormalizedLinearColorStop, NormalizedLinearColorStopVec);
377impl_vec_clone!(
378    NormalizedLinearColorStop,
379    NormalizedLinearColorStopVec,
380    NormalizedLinearColorStopVecDestructor
381);
382impl_vec_partialeq!(NormalizedLinearColorStop, NormalizedLinearColorStopVec);
383impl_vec_eq!(NormalizedLinearColorStop, NormalizedLinearColorStopVec);
384impl_vec_hash!(NormalizedLinearColorStop, NormalizedLinearColorStopVec);
385impl PrintAsCssValue for NormalizedLinearColorStop {
386    fn print_as_css_value(&self) -> String {
387        match &self.color {
388            ColorOrSystem::Color(c) => format!("{} {}", c.to_hash(), self.offset),
389            ColorOrSystem::System(s) => format!("{} {}", s.as_css_str(), self.offset),
390        }
391    }
392}
393
394#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
395#[repr(C)]
396pub struct NormalizedRadialColorStop {
397    pub angle: AngleValue,
398    /// Color for this gradient stop. Can be a concrete color or a system color reference.
399    pub color: ColorOrSystem,
400}
401
402impl NormalizedRadialColorStop {
403    /// Create a new normalized radial color stop with a concrete color.
404    #[must_use] pub const fn new(angle: AngleValue, color: ColorU) -> Self {
405        Self { angle, color: ColorOrSystem::color(color) }
406    }
407
408    /// Resolve the color against system colors.
409    #[must_use] pub fn resolve(&self, system_colors: &crate::system::SystemColors, fallback: ColorU) -> ColorU {
410        self.color.resolve(system_colors, fallback)
411    }
412}
413
414impl_option!(
415    NormalizedRadialColorStop,
416    OptionNormalizedRadialColorStop,
417    copy = false,
418    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
419);
420impl_vec!(NormalizedRadialColorStop, NormalizedRadialColorStopVec, NormalizedRadialColorStopVecDestructor, NormalizedRadialColorStopVecDestructorType, NormalizedRadialColorStopVecSlice, OptionNormalizedRadialColorStop);
421impl_vec_debug!(NormalizedRadialColorStop, NormalizedRadialColorStopVec);
422impl_vec_partialord!(NormalizedRadialColorStop, NormalizedRadialColorStopVec);
423impl_vec_ord!(NormalizedRadialColorStop, NormalizedRadialColorStopVec);
424impl_vec_clone!(
425    NormalizedRadialColorStop,
426    NormalizedRadialColorStopVec,
427    NormalizedRadialColorStopVecDestructor
428);
429impl_vec_partialeq!(NormalizedRadialColorStop, NormalizedRadialColorStopVec);
430impl_vec_eq!(NormalizedRadialColorStop, NormalizedRadialColorStopVec);
431impl_vec_hash!(NormalizedRadialColorStop, NormalizedRadialColorStopVec);
432impl PrintAsCssValue for NormalizedRadialColorStop {
433    fn print_as_css_value(&self) -> String {
434        match &self.color {
435            ColorOrSystem::Color(c) => format!("{} {}", c.to_hash(), self.angle),
436            ColorOrSystem::System(s) => format!("{} {}", s.as_css_str(), self.angle),
437        }
438    }
439}
440
441/// Transient struct for parsing linear color stops before normalization.
442///
443/// Per W3C CSS Images Level 3, a color stop can have 0, 1, or 2 positions:
444/// - `red` (no position)
445/// - `red 50%` (one position)
446/// - `red 10% 30%` (two positions - creates two stops at same color)
447/// 
448/// Supports system colors like `system:accent` for theme-aware gradients.
449#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
450pub struct LinearColorStop {
451    pub color: ColorOrSystem,
452    /// First position (optional)
453    pub offset1: OptionPercentageValue,
454    /// Second position (optional, only valid if offset1 is Some)
455    /// When present, creates two color stops at the same color.
456    pub offset2: OptionPercentageValue,
457}
458
459/// Transient struct for parsing radial/conic color stops before normalization.
460///
461/// Per W3C CSS Images Level 3, a color stop can have 0, 1, or 2 positions:
462/// - `red` (no position)
463/// - `red 90deg` (one position)
464/// - `red 45deg 90deg` (two positions - creates two stops at same color)
465/// 
466/// Supports system colors like `system:accent` for theme-aware gradients.
467#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
468pub struct RadialColorStop {
469    pub color: ColorOrSystem,
470    /// First position (optional)
471    pub offset1: OptionAngleValue,
472    /// Second position (optional, only valid if offset1 is Some)
473    /// When present, creates two color stops at the same color.
474    pub offset2: OptionAngleValue,
475}
476
477// -- Other Background Properties --
478
479/// The `background-position` property (horizontal + vertical components).
480#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
481#[repr(C)]
482pub struct StyleBackgroundPosition {
483    pub horizontal: BackgroundPositionHorizontal,
484    pub vertical: BackgroundPositionVertical,
485}
486
487impl_option!(
488    StyleBackgroundPosition,
489    OptionStyleBackgroundPosition,
490    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
491);
492impl_vec!(StyleBackgroundPosition, StyleBackgroundPositionVec, StyleBackgroundPositionVecDestructor, StyleBackgroundPositionVecDestructorType, StyleBackgroundPositionVecSlice, OptionStyleBackgroundPosition);
493impl_vec_debug!(StyleBackgroundPosition, StyleBackgroundPositionVec);
494impl_vec_partialord!(StyleBackgroundPosition, StyleBackgroundPositionVec);
495impl_vec_ord!(StyleBackgroundPosition, StyleBackgroundPositionVec);
496impl_vec_clone!(
497    StyleBackgroundPosition,
498    StyleBackgroundPositionVec,
499    StyleBackgroundPositionVecDestructor
500);
501impl_vec_partialeq!(StyleBackgroundPosition, StyleBackgroundPositionVec);
502impl_vec_eq!(StyleBackgroundPosition, StyleBackgroundPositionVec);
503impl_vec_hash!(StyleBackgroundPosition, StyleBackgroundPositionVec);
504impl Default for StyleBackgroundPosition {
505    fn default() -> Self {
506        Self {
507            horizontal: BackgroundPositionHorizontal::Left,
508            vertical: BackgroundPositionVertical::Top,
509        }
510    }
511}
512
513impl StyleBackgroundPosition {
514    pub fn scale_for_dpi(&mut self, scale_factor: f32) {
515        self.horizontal.scale_for_dpi(scale_factor);
516        self.vertical.scale_for_dpi(scale_factor);
517    }
518}
519
520impl PrintAsCssValue for StyleBackgroundPosition {
521    fn print_as_css_value(&self) -> String {
522        format!(
523            "{} {}",
524            self.horizontal.print_as_css_value(),
525            self.vertical.print_as_css_value()
526        )
527    }
528}
529impl PrintAsCssValue for StyleBackgroundPositionVec {
530    fn print_as_css_value(&self) -> String {
531        self.iter()
532            .map(PrintAsCssValue::print_as_css_value)
533            .collect::<Vec<_>>()
534            .join(", ")
535    }
536}
537
538// Formatting to Rust code for StyleBackgroundPositionVec
539impl crate::codegen::format::FormatAsRustCode for StyleBackgroundPositionVec {
540    fn format_as_rust_code(&self, _tabs: usize) -> String {
541        format!(
542            "StyleBackgroundPositionVec::from_const_slice(STYLE_BACKGROUND_POSITION_{}_ITEMS)",
543            self.get_hash()
544        )
545    }
546}
547#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
548/// Horizontal component of `background-position`: a keyword or exact pixel value.
549#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
550#[repr(C, u8)]
551pub enum BackgroundPositionHorizontal {
552    Left,
553    Center,
554    Right,
555    Exact(PixelValue),
556}
557
558impl BackgroundPositionHorizontal {
559    pub fn scale_for_dpi(&mut self, scale_factor: f32) {
560        if let Self::Exact(s) = self {
561            s.scale_for_dpi(scale_factor);
562        }
563    }
564}
565
566impl PrintAsCssValue for BackgroundPositionHorizontal {
567    fn print_as_css_value(&self) -> String {
568        match self {
569            Self::Left => "left".to_string(),
570            Self::Center => "center".to_string(),
571            Self::Right => "right".to_string(),
572            Self::Exact(px) => px.print_as_css_value(),
573        }
574    }
575}
576#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
577/// Vertical component of `background-position`: a keyword or exact pixel value.
578#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
579#[repr(C, u8)]
580pub enum BackgroundPositionVertical {
581    Top,
582    Center,
583    Bottom,
584    Exact(PixelValue),
585}
586
587impl BackgroundPositionVertical {
588    pub fn scale_for_dpi(&mut self, scale_factor: f32) {
589        if let Self::Exact(s) = self {
590            s.scale_for_dpi(scale_factor);
591        }
592    }
593}
594
595impl PrintAsCssValue for BackgroundPositionVertical {
596    fn print_as_css_value(&self) -> String {
597        match self {
598            Self::Top => "top".to_string(),
599            Self::Center => "center".to_string(),
600            Self::Bottom => "bottom".to_string(),
601            Self::Exact(px) => px.print_as_css_value(),
602        }
603    }
604}
605#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
606/// The `background-size` property: `contain`, `cover`, or an exact size.
607#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
608#[repr(C, u8)]
609#[derive(Default)]
610pub enum StyleBackgroundSize {
611    ExactSize(PixelValueSize),
612    #[default]
613    Contain,
614    Cover,
615}
616
617impl_option!(
618    StyleBackgroundSize,
619    OptionStyleBackgroundSize,
620    copy = false,
621    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
622);
623
624/// Two-dimensional size in `PixelValue` units (width, height)
625/// Used for background-size and similar properties
626#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
627#[repr(C)]
628pub struct PixelValueSize {
629    pub width: PixelValue,
630    pub height: PixelValue,
631}
632
633impl_vec!(StyleBackgroundSize, StyleBackgroundSizeVec, StyleBackgroundSizeVecDestructor, StyleBackgroundSizeVecDestructorType, StyleBackgroundSizeVecSlice, OptionStyleBackgroundSize);
634impl_vec_debug!(StyleBackgroundSize, StyleBackgroundSizeVec);
635impl_vec_partialord!(StyleBackgroundSize, StyleBackgroundSizeVec);
636impl_vec_ord!(StyleBackgroundSize, StyleBackgroundSizeVec);
637impl_vec_clone!(
638    StyleBackgroundSize,
639    StyleBackgroundSizeVec,
640    StyleBackgroundSizeVecDestructor
641);
642impl_vec_partialeq!(StyleBackgroundSize, StyleBackgroundSizeVec);
643impl_vec_eq!(StyleBackgroundSize, StyleBackgroundSizeVec);
644impl_vec_hash!(StyleBackgroundSize, StyleBackgroundSizeVec);
645
646impl StyleBackgroundSize {
647    pub fn scale_for_dpi(&mut self, scale_factor: f32) {
648        if let Self::ExactSize(size) = self {
649            size.width.scale_for_dpi(scale_factor);
650            size.height.scale_for_dpi(scale_factor);
651        }
652    }
653}
654
655impl PrintAsCssValue for StyleBackgroundSize {
656    fn print_as_css_value(&self) -> String {
657        match self {
658            Self::Contain => "contain".to_string(),
659            Self::Cover => "cover".to_string(),
660            Self::ExactSize(size) => {
661                format!(
662                    "{} {}",
663                    size.width.print_as_css_value(),
664                    size.height.print_as_css_value()
665                )
666            }
667        }
668    }
669}
670impl PrintAsCssValue for StyleBackgroundSizeVec {
671    fn print_as_css_value(&self) -> String {
672        self.iter()
673            .map(PrintAsCssValue::print_as_css_value)
674            .collect::<Vec<_>>()
675            .join(", ")
676    }
677}
678
679/// The `background-repeat` property.
680#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
681#[repr(C)]
682#[derive(Default)]
683pub enum StyleBackgroundRepeat {
684    NoRepeat,
685    #[default]
686    PatternRepeat,
687    RepeatX,
688    RepeatY,
689}
690
691impl_option!(
692    StyleBackgroundRepeat,
693    OptionStyleBackgroundRepeat,
694    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
695);
696impl_vec!(StyleBackgroundRepeat, StyleBackgroundRepeatVec, StyleBackgroundRepeatVecDestructor, StyleBackgroundRepeatVecDestructorType, StyleBackgroundRepeatVecSlice, OptionStyleBackgroundRepeat);
697impl_vec_debug!(StyleBackgroundRepeat, StyleBackgroundRepeatVec);
698impl_vec_partialord!(StyleBackgroundRepeat, StyleBackgroundRepeatVec);
699impl_vec_ord!(StyleBackgroundRepeat, StyleBackgroundRepeatVec);
700impl_vec_clone!(
701    StyleBackgroundRepeat,
702    StyleBackgroundRepeatVec,
703    StyleBackgroundRepeatVecDestructor
704);
705impl_vec_partialeq!(StyleBackgroundRepeat, StyleBackgroundRepeatVec);
706impl_vec_eq!(StyleBackgroundRepeat, StyleBackgroundRepeatVec);
707impl_vec_hash!(StyleBackgroundRepeat, StyleBackgroundRepeatVec);
708impl PrintAsCssValue for StyleBackgroundRepeat {
709    fn print_as_css_value(&self) -> String {
710        match self {
711            Self::NoRepeat => "no-repeat".to_string(),
712            Self::PatternRepeat => "repeat".to_string(),
713            Self::RepeatX => "repeat-x".to_string(),
714            Self::RepeatY => "repeat-y".to_string(),
715        }
716    }
717}
718impl PrintAsCssValue for StyleBackgroundRepeatVec {
719    fn print_as_css_value(&self) -> String {
720        self.iter()
721            .map(PrintAsCssValue::print_as_css_value)
722            .collect::<Vec<_>>()
723            .join(", ")
724    }
725}
726
727// --- ERROR DEFINITIONS ---
728
729#[derive(Clone, PartialEq)]
730pub enum CssBackgroundParseError<'a> {
731    Error(&'a str),
732    InvalidBackground(ParenthesisParseError<'a>),
733    UnclosedGradient(&'a str),
734    NoDirection(&'a str),
735    TooFewGradientStops(&'a str),
736    DirectionParseError(CssDirectionParseError<'a>),
737    GradientParseError(CssGradientStopParseError<'a>),
738    ConicGradient(CssConicGradientParseError<'a>),
739    ShapeParseError(CssShapeParseError<'a>),
740    ImageParseError(CssImageParseError<'a>),
741    ColorParseError(CssColorParseError<'a>),
742}
743
744impl_debug_as_display!(CssBackgroundParseError<'a>);
745impl_display! { CssBackgroundParseError<'a>, {
746    Error(e) => e,
747    InvalidBackground(val) => format!("Invalid background value: \"{}\"", val),
748    UnclosedGradient(val) => format!("Unclosed gradient: \"{}\"", val),
749    NoDirection(val) => format!("Gradient has no direction: \"{}\"", val),
750    TooFewGradientStops(val) => format!("Failed to parse gradient due to too few gradient steps: \"{}\"", val),
751    DirectionParseError(e) => format!("Failed to parse gradient direction: \"{}\"", e),
752    GradientParseError(e) => format!("Failed to parse gradient: {}", e),
753    ConicGradient(e) => format!("Failed to parse conic gradient: {}", e),
754    ShapeParseError(e) => format!("Failed to parse shape of radial gradient: {}", e),
755    ImageParseError(e) => format!("Failed to parse image() value: {}", e),
756    ColorParseError(e) => format!("Failed to parse color value: {}", e),
757}}
758
759#[cfg(feature = "parser")]
760impl_from!(
761    ParenthesisParseError<'a>,
762    CssBackgroundParseError::InvalidBackground
763);
764#[cfg(feature = "parser")]
765impl_from!(
766    CssDirectionParseError<'a>,
767    CssBackgroundParseError::DirectionParseError
768);
769#[cfg(feature = "parser")]
770impl_from!(
771    CssGradientStopParseError<'a>,
772    CssBackgroundParseError::GradientParseError
773);
774#[cfg(feature = "parser")]
775impl_from!(
776    CssShapeParseError<'a>,
777    CssBackgroundParseError::ShapeParseError
778);
779#[cfg(feature = "parser")]
780impl_from!(
781    CssImageParseError<'a>,
782    CssBackgroundParseError::ImageParseError
783);
784#[cfg(feature = "parser")]
785impl_from!(
786    CssColorParseError<'a>,
787    CssBackgroundParseError::ColorParseError
788);
789#[cfg(feature = "parser")]
790impl_from!(
791    CssConicGradientParseError<'a>,
792    CssBackgroundParseError::ConicGradient
793);
794
795#[derive(Debug, Clone, PartialEq)]
796#[repr(C, u8)]
797pub enum CssBackgroundParseErrorOwned {
798    Error(AzString),
799    InvalidBackground(ParenthesisParseErrorOwned),
800    UnclosedGradient(AzString),
801    NoDirection(AzString),
802    TooFewGradientStops(AzString),
803    DirectionParseError(CssDirectionParseErrorOwned),
804    GradientParseError(CssGradientStopParseErrorOwned),
805    ConicGradient(CssConicGradientParseErrorOwned),
806    ShapeParseError(CssShapeParseErrorOwned),
807    ImageParseError(CssImageParseErrorOwned),
808    ColorParseError(CssColorParseErrorOwned),
809}
810
811impl CssBackgroundParseError<'_> {
812    #[must_use] pub fn to_contained(&self) -> CssBackgroundParseErrorOwned {
813        match self {
814            Self::Error(s) => CssBackgroundParseErrorOwned::Error((*s).to_string().into()),
815            Self::InvalidBackground(e) => {
816                CssBackgroundParseErrorOwned::InvalidBackground(e.to_contained())
817            }
818            Self::UnclosedGradient(s) => {
819                CssBackgroundParseErrorOwned::UnclosedGradient((*s).to_string().into())
820            }
821            Self::NoDirection(s) => CssBackgroundParseErrorOwned::NoDirection((*s).to_string().into()),
822            Self::TooFewGradientStops(s) => {
823                CssBackgroundParseErrorOwned::TooFewGradientStops((*s).to_string().into())
824            }
825            Self::DirectionParseError(e) => {
826                CssBackgroundParseErrorOwned::DirectionParseError(e.to_contained())
827            }
828            Self::GradientParseError(e) => {
829                CssBackgroundParseErrorOwned::GradientParseError(e.to_contained())
830            }
831            Self::ConicGradient(e) => CssBackgroundParseErrorOwned::ConicGradient(e.to_contained()),
832            Self::ShapeParseError(e) => {
833                CssBackgroundParseErrorOwned::ShapeParseError(e.to_contained())
834            }
835            Self::ImageParseError(e) => {
836                CssBackgroundParseErrorOwned::ImageParseError(e.to_contained())
837            }
838            Self::ColorParseError(e) => {
839                CssBackgroundParseErrorOwned::ColorParseError(e.to_contained())
840            }
841        }
842    }
843}
844
845impl CssBackgroundParseErrorOwned {
846    #[must_use] pub fn to_shared(&self) -> CssBackgroundParseError<'_> {
847        match self {
848            Self::Error(s) => CssBackgroundParseError::Error(s),
849            Self::InvalidBackground(e) => CssBackgroundParseError::InvalidBackground(e.to_shared()),
850            Self::UnclosedGradient(s) => CssBackgroundParseError::UnclosedGradient(s),
851            Self::NoDirection(s) => CssBackgroundParseError::NoDirection(s),
852            Self::TooFewGradientStops(s) => CssBackgroundParseError::TooFewGradientStops(s),
853            Self::DirectionParseError(e) => {
854                CssBackgroundParseError::DirectionParseError(e.to_shared())
855            }
856            Self::GradientParseError(e) => {
857                CssBackgroundParseError::GradientParseError(e.to_shared())
858            }
859            Self::ConicGradient(e) => CssBackgroundParseError::ConicGradient(e.to_shared()),
860            Self::ShapeParseError(e) => CssBackgroundParseError::ShapeParseError(e.to_shared()),
861            Self::ImageParseError(e) => CssBackgroundParseError::ImageParseError(e.to_shared()),
862            Self::ColorParseError(e) => CssBackgroundParseError::ColorParseError(e.to_shared()),
863        }
864    }
865}
866
867#[derive(Clone, PartialEq)]
868pub enum CssGradientStopParseError<'a> {
869    Error(&'a str),
870    Percentage(PercentageParseError),
871    Angle(CssAngleValueParseError<'a>),
872    ColorParseError(CssColorParseError<'a>),
873}
874
875impl_debug_as_display!(CssGradientStopParseError<'a>);
876impl_display! { CssGradientStopParseError<'a>, {
877    Error(e) => e,
878    Percentage(e) => format!("Failed to parse offset percentage: {}", e),
879    Angle(e) => format!("Failed to parse angle: {}", e),
880    ColorParseError(e) => format!("{}", e),
881}}
882#[cfg(feature = "parser")]
883impl_from!(
884    CssColorParseError<'a>,
885    CssGradientStopParseError::ColorParseError
886);
887
888#[derive(Debug, Clone, PartialEq)]
889#[repr(C, u8)]
890pub enum CssGradientStopParseErrorOwned {
891    Error(AzString),
892    Percentage(PercentageParseErrorOwned),
893    Angle(CssAngleValueParseErrorOwned),
894    ColorParseError(CssColorParseErrorOwned),
895}
896
897impl CssGradientStopParseError<'_> {
898    #[must_use] pub fn to_contained(&self) -> CssGradientStopParseErrorOwned {
899        match self {
900            Self::Error(s) => CssGradientStopParseErrorOwned::Error((*s).to_string().into()),
901            Self::Percentage(e) => CssGradientStopParseErrorOwned::Percentage(e.to_contained()),
902            Self::Angle(e) => CssGradientStopParseErrorOwned::Angle(e.to_contained()),
903            Self::ColorParseError(e) => {
904                CssGradientStopParseErrorOwned::ColorParseError(e.to_contained())
905            }
906        }
907    }
908}
909
910impl CssGradientStopParseErrorOwned {
911    #[must_use] pub fn to_shared(&self) -> CssGradientStopParseError<'_> {
912        match self {
913            Self::Error(s) => CssGradientStopParseError::Error(s),
914            Self::Percentage(e) => CssGradientStopParseError::Percentage(e.to_shared()),
915            Self::Angle(e) => CssGradientStopParseError::Angle(e.to_shared()),
916            Self::ColorParseError(e) => CssGradientStopParseError::ColorParseError(e.to_shared()),
917        }
918    }
919}
920
921#[derive(Clone, PartialEq, Eq)]
922pub enum CssConicGradientParseError<'a> {
923    Position(CssBackgroundPositionParseError<'a>),
924    Angle(CssAngleValueParseError<'a>),
925    NoAngle(&'a str),
926}
927impl_debug_as_display!(CssConicGradientParseError<'a>);
928impl_display! { CssConicGradientParseError<'a>, {
929    Position(val) => format!("Invalid position attribute: \"{}\"", val),
930    Angle(val) => format!("Invalid angle value: \"{}\"", val),
931    NoAngle(val) => format!("Expected angle: \"{}\"", val),
932}}
933#[cfg(feature = "parser")]
934impl_from!(
935    CssAngleValueParseError<'a>,
936    CssConicGradientParseError::Angle
937);
938#[cfg(feature = "parser")]
939impl_from!(
940    CssBackgroundPositionParseError<'a>,
941    CssConicGradientParseError::Position
942);
943
944#[derive(Debug, Clone, PartialEq, Eq)]
945#[repr(C, u8)]
946pub enum CssConicGradientParseErrorOwned {
947    Position(CssBackgroundPositionParseErrorOwned),
948    Angle(CssAngleValueParseErrorOwned),
949    NoAngle(AzString),
950}
951impl CssConicGradientParseError<'_> {
952    #[must_use] pub fn to_contained(&self) -> CssConicGradientParseErrorOwned {
953        match self {
954            Self::Position(e) => CssConicGradientParseErrorOwned::Position(e.to_contained()),
955            Self::Angle(e) => CssConicGradientParseErrorOwned::Angle(e.to_contained()),
956            Self::NoAngle(s) => CssConicGradientParseErrorOwned::NoAngle((*s).to_string().into()),
957        }
958    }
959}
960impl CssConicGradientParseErrorOwned {
961    #[must_use] pub fn to_shared(&self) -> CssConicGradientParseError<'_> {
962        match self {
963            Self::Position(e) => CssConicGradientParseError::Position(e.to_shared()),
964            Self::Angle(e) => CssConicGradientParseError::Angle(e.to_shared()),
965            Self::NoAngle(s) => CssConicGradientParseError::NoAngle(s),
966        }
967    }
968}
969
970#[derive(Debug, Copy, Clone, PartialEq, Eq)]
971pub enum CssShapeParseError<'a> {
972    ShapeErr(InvalidValueErr<'a>),
973}
974impl_display! {CssShapeParseError<'a>, {
975    ShapeErr(e) => format!("\"{}\"", e.0),
976}}
977#[derive(Debug, Clone, PartialEq, Eq)]
978#[repr(C, u8)]
979pub enum CssShapeParseErrorOwned {
980    ShapeErr(InvalidValueErrOwned),
981}
982impl CssShapeParseError<'_> {
983    #[must_use] pub fn to_contained(&self) -> CssShapeParseErrorOwned {
984        match self {
985            Self::ShapeErr(err) => CssShapeParseErrorOwned::ShapeErr(err.to_contained()),
986        }
987    }
988}
989impl CssShapeParseErrorOwned {
990    #[must_use] pub fn to_shared(&self) -> CssShapeParseError<'_> {
991        match self {
992            Self::ShapeErr(err) => CssShapeParseError::ShapeErr(err.to_shared()),
993        }
994    }
995}
996
997#[derive(Debug, Clone, PartialEq, Eq)]
998pub enum CssBackgroundPositionParseError<'a> {
999    NoPosition(&'a str),
1000    TooManyComponents(&'a str),
1001    FirstComponentWrong(CssPixelValueParseError<'a>),
1002    SecondComponentWrong(CssPixelValueParseError<'a>),
1003}
1004
1005impl_display! {CssBackgroundPositionParseError<'a>, {
1006    NoPosition(e) => format!("First background position missing: \"{}\"", e),
1007    TooManyComponents(e) => format!("background-position can only have one or two components, not more: \"{}\"", e),
1008    FirstComponentWrong(e) => format!("Failed to parse first component: \"{}\"", e),
1009    SecondComponentWrong(e) => format!("Failed to parse second component: \"{}\"", e),
1010}}
1011#[derive(Debug, Clone, PartialEq, Eq)]
1012#[repr(C, u8)]
1013pub enum CssBackgroundPositionParseErrorOwned {
1014    NoPosition(AzString),
1015    TooManyComponents(AzString),
1016    FirstComponentWrong(CssPixelValueParseErrorOwned),
1017    SecondComponentWrong(CssPixelValueParseErrorOwned),
1018}
1019impl CssBackgroundPositionParseError<'_> {
1020    #[must_use] pub fn to_contained(&self) -> CssBackgroundPositionParseErrorOwned {
1021        match self {
1022            Self::NoPosition(s) => CssBackgroundPositionParseErrorOwned::NoPosition((*s).to_string().into()),
1023            Self::TooManyComponents(s) => {
1024                CssBackgroundPositionParseErrorOwned::TooManyComponents((*s).to_string().into())
1025            }
1026            Self::FirstComponentWrong(e) => {
1027                CssBackgroundPositionParseErrorOwned::FirstComponentWrong(e.to_contained())
1028            }
1029            Self::SecondComponentWrong(e) => {
1030                CssBackgroundPositionParseErrorOwned::SecondComponentWrong(e.to_contained())
1031            }
1032        }
1033    }
1034}
1035impl CssBackgroundPositionParseErrorOwned {
1036    #[must_use] pub fn to_shared(&self) -> CssBackgroundPositionParseError<'_> {
1037        match self {
1038            Self::NoPosition(s) => CssBackgroundPositionParseError::NoPosition(s),
1039            Self::TooManyComponents(s) => CssBackgroundPositionParseError::TooManyComponents(s),
1040            Self::FirstComponentWrong(e) => {
1041                CssBackgroundPositionParseError::FirstComponentWrong(e.to_shared())
1042            }
1043            Self::SecondComponentWrong(e) => {
1044                CssBackgroundPositionParseError::SecondComponentWrong(e.to_shared())
1045            }
1046        }
1047    }
1048}
1049
1050// --- PARSERS ---
1051
1052#[cfg(feature = "parser")]
1053pub mod parser {
1054    #[allow(clippy::wildcard_imports)] // parser submodule reuses the parent module's value types
1055    use super::*;
1056
1057    // the `*Gradient` suffix mirrors the CSS gradient function names this enum
1058    // parses (linear-gradient, radial-gradient, conic-gradient, …).
1059    #[allow(clippy::enum_variant_names)]
1060    #[derive(Debug, Copy, Clone, PartialEq, Eq)]
1061    enum GradientType {
1062        LinearGradient,
1063        RepeatingLinearGradient,
1064        RadialGradient,
1065        RepeatingRadialGradient,
1066        ConicGradient,
1067        RepeatingConicGradient,
1068    }
1069
1070    impl GradientType {
1071        pub(crate) const fn get_extend_mode(self) -> ExtendMode {
1072            match self {
1073                Self::LinearGradient | Self::RadialGradient | Self::ConicGradient => {
1074                    ExtendMode::Clamp
1075                }
1076                Self::RepeatingLinearGradient
1077                | Self::RepeatingRadialGradient
1078                | Self::RepeatingConicGradient => ExtendMode::Repeat,
1079            }
1080        }
1081    }
1082
1083    // -- Top-level Parsers for background-* properties --
1084
1085    /// Parses multiple backgrounds, such as "linear-gradient(red, green), url(image.png)".
1086    /// # Errors
1087    ///
1088    /// Returns an error if `input` is not a valid CSS `background-content-multiple` value.
1089    pub fn parse_style_background_content_multiple(
1090        input: &str,
1091    ) -> Result<StyleBackgroundContentVec, CssBackgroundParseError<'_>> {
1092        Ok(split_string_respect_comma(input)
1093            .iter()
1094            .map(|i| parse_style_background_content(i))
1095            .collect::<Result<Vec<_>, _>>()?
1096            .into())
1097    }
1098
1099    /// Parses a single background value, which can be a color, image, or gradient.
1100    /// # Errors
1101    ///
1102    /// Returns an error if `input` is not a valid CSS `background-content` value.
1103    pub fn parse_style_background_content(
1104        input: &str,
1105    ) -> Result<StyleBackgroundContent, CssBackgroundParseError<'_>> {
1106        match parse_parentheses(
1107            input,
1108            &[
1109                "linear-gradient",
1110                "repeating-linear-gradient",
1111                "radial-gradient",
1112                "repeating-radial-gradient",
1113                "conic-gradient",
1114                "repeating-conic-gradient",
1115                "image",
1116                "url",
1117            ],
1118        ) {
1119            Ok((background_type, brace_contents)) => {
1120                let gradient_type = match background_type {
1121                    "linear-gradient" => GradientType::LinearGradient,
1122                    "repeating-linear-gradient" => GradientType::RepeatingLinearGradient,
1123                    "radial-gradient" => GradientType::RadialGradient,
1124                    "repeating-radial-gradient" => GradientType::RepeatingRadialGradient,
1125                    "conic-gradient" => GradientType::ConicGradient,
1126                    "repeating-conic-gradient" => GradientType::RepeatingConicGradient,
1127                    "image" | "url" => {
1128                        return Ok(StyleBackgroundContent::Image(
1129                            parse_image(brace_contents)?,
1130                        ))
1131                    }
1132                    _ => unreachable!(),
1133                };
1134                parse_gradient(brace_contents, gradient_type)
1135            }
1136            // A bare `background:` value is a color. Accept system colors here too
1137            // (`system:accent`, `system:text`, ...), matching the gradient color stops
1138            // (which use `parse_color_or_system`). System colors stay unresolved and are
1139            // theme-resolved at render time. `parse_color_or_system` is a superset of
1140            // `parse_css_color`, so ordinary colors keep parsing exactly as before.
1141            Err(_) => Ok(match parse_color_or_system(input)? {
1142                ColorOrSystem::Color(c) => StyleBackgroundContent::Color(c),
1143                ColorOrSystem::System(s) => StyleBackgroundContent::SystemColor(s),
1144            }),
1145        }
1146    }
1147
1148    /// Parses multiple `background-position` values.
1149    /// # Errors
1150    ///
1151    /// Returns an error if `input` is not a valid CSS `background-position-multiple` value.
1152    pub fn parse_style_background_position_multiple(
1153        input: &str,
1154    ) -> Result<StyleBackgroundPositionVec, CssBackgroundPositionParseError<'_>> {
1155        Ok(split_string_respect_comma(input)
1156            .iter()
1157            .map(|i| parse_style_background_position(i))
1158            .collect::<Result<Vec<_>, _>>()?
1159            .into())
1160    }
1161
1162    /// Parses a single `background-position` value.
1163    /// # Errors
1164    ///
1165    /// Returns an error if `input` is not a valid CSS `background-position` value.
1166    pub fn parse_style_background_position(
1167        input: &str,
1168    ) -> Result<StyleBackgroundPosition, CssBackgroundPositionParseError<'_>> {
1169        let input = input.trim();
1170        let mut whitespace_iter = input.split_whitespace();
1171
1172        let first = whitespace_iter
1173            .next()
1174            .ok_or(CssBackgroundPositionParseError::NoPosition(input))?;
1175        let second = whitespace_iter.next();
1176
1177        if whitespace_iter.next().is_some() {
1178            return Err(CssBackgroundPositionParseError::TooManyComponents(input));
1179        }
1180
1181        // Try to parse as horizontal first, if that fails, maybe it's a vertical keyword
1182        if let Ok(horizontal) = parse_background_position_horizontal(first) {
1183            let vertical = match second {
1184                Some(s) => parse_background_position_vertical(s)
1185                    .map_err(CssBackgroundPositionParseError::SecondComponentWrong)?,
1186                None => BackgroundPositionVertical::Center,
1187            };
1188            return Ok(StyleBackgroundPosition {
1189                horizontal,
1190                vertical,
1191            });
1192        }
1193
1194        // If the first part wasn't a horizontal keyword, maybe it's a vertical one
1195        if let Ok(vertical) = parse_background_position_vertical(first) {
1196            let horizontal = match second {
1197                Some(s) => parse_background_position_horizontal(s)
1198                    .map_err(CssBackgroundPositionParseError::FirstComponentWrong)?,
1199                None => BackgroundPositionHorizontal::Center,
1200            };
1201            return Ok(StyleBackgroundPosition {
1202                horizontal,
1203                vertical,
1204            });
1205        }
1206
1207        Err(CssBackgroundPositionParseError::FirstComponentWrong(
1208            CssPixelValueParseError::InvalidPixelValue(first),
1209        ))
1210    }
1211
1212    /// Parses multiple `background-size` values.
1213    /// # Errors
1214    ///
1215    /// Returns an error if `input` is not a valid CSS `background-size-multiple` value.
1216    pub fn parse_style_background_size_multiple(
1217        input: &str,
1218    ) -> Result<StyleBackgroundSizeVec, InvalidValueErr<'_>> {
1219        Ok(split_string_respect_comma(input)
1220            .iter()
1221            .map(|i| parse_style_background_size(i))
1222            .collect::<Result<Vec<_>, _>>()?
1223            .into())
1224    }
1225
1226    /// Parses a single `background-size` value.
1227    /// # Errors
1228    ///
1229    /// Returns an error if `input` is not a valid CSS `background-size` value.
1230    pub fn parse_style_background_size(
1231        input: &str,
1232    ) -> Result<StyleBackgroundSize, InvalidValueErr<'_>> {
1233        let input = input.trim();
1234        match input {
1235            "contain" => Ok(StyleBackgroundSize::Contain),
1236            "cover" => Ok(StyleBackgroundSize::Cover),
1237            other => {
1238                let mut iter = other.split_whitespace();
1239                let x_val = iter.next().ok_or(InvalidValueErr(input))?;
1240                let x_pos = parse_pixel_value(x_val).map_err(|_| InvalidValueErr(input))?;
1241                let y_pos = match iter.next() {
1242                    Some(y_val) => parse_pixel_value(y_val).map_err(|_| InvalidValueErr(input))?,
1243                    None => x_pos, // If only one value, it applies to both width and height
1244                };
1245                Ok(StyleBackgroundSize::ExactSize(PixelValueSize {
1246                    width: x_pos,
1247                    height: y_pos,
1248                }))
1249            }
1250        }
1251    }
1252
1253    /// Parses multiple `background-repeat` values.
1254    /// # Errors
1255    ///
1256    /// Returns an error if `input` is not a valid CSS `background-repeat-multiple` value.
1257    pub fn parse_style_background_repeat_multiple(
1258        input: &str,
1259    ) -> Result<StyleBackgroundRepeatVec, InvalidValueErr<'_>> {
1260        Ok(split_string_respect_comma(input)
1261            .iter()
1262            .map(|i| parse_style_background_repeat(i))
1263            .collect::<Result<Vec<_>, _>>()?
1264            .into())
1265    }
1266
1267    /// Parses a single `background-repeat` value.
1268    /// # Errors
1269    ///
1270    /// Returns an error if `input` is not a valid CSS `background-repeat` value.
1271    pub fn parse_style_background_repeat(
1272        input: &str,
1273    ) -> Result<StyleBackgroundRepeat, InvalidValueErr<'_>> {
1274        match input.trim() {
1275            "no-repeat" => Ok(StyleBackgroundRepeat::NoRepeat),
1276            "repeat" => Ok(StyleBackgroundRepeat::PatternRepeat),
1277            "repeat-x" => Ok(StyleBackgroundRepeat::RepeatX),
1278            "repeat-y" => Ok(StyleBackgroundRepeat::RepeatY),
1279            _ => Err(InvalidValueErr(input)),
1280        }
1281    }
1282
1283    // -- Gradient Parsing Logic --
1284
1285    /// Parses the contents of a gradient function.
1286    fn parse_gradient(
1287        input: &str,
1288        gradient_type: GradientType,
1289    ) -> Result<StyleBackgroundContent, CssBackgroundParseError<'_>> {
1290        let input = input.trim();
1291        let comma_separated_items = split_string_respect_comma(input);
1292        let mut brace_iterator = comma_separated_items.iter();
1293        let first_brace_item = brace_iterator
1294            .next()
1295            .ok_or(CssBackgroundParseError::NoDirection(input))?;
1296
1297        match gradient_type {
1298            GradientType::LinearGradient | GradientType::RepeatingLinearGradient => {
1299                let mut linear_gradient = LinearGradient {
1300                    extend_mode: gradient_type.get_extend_mode(),
1301                    ..Default::default()
1302                };
1303                let mut linear_stops = Vec::new();
1304
1305                if let Ok(dir) = parse_direction(first_brace_item) {
1306                    linear_gradient.direction = dir;
1307                } else {
1308                    linear_stops.push(parse_linear_color_stop(first_brace_item)?);
1309                }
1310
1311                for item in brace_iterator {
1312                    linear_stops.push(parse_linear_color_stop(item)?);
1313                }
1314
1315                linear_gradient.stops = get_normalized_linear_stops(&linear_stops).into();
1316                Ok(StyleBackgroundContent::LinearGradient(linear_gradient))
1317            }
1318            GradientType::RadialGradient | GradientType::RepeatingRadialGradient => {
1319                // Simplified parsing: assumes shape/size/position come first, then stops.
1320                // A more robust parser would handle them in any order.
1321                let mut radial_gradient = RadialGradient {
1322                    extend_mode: gradient_type.get_extend_mode(),
1323                    ..Default::default()
1324                };
1325                let mut radial_stops = Vec::new();
1326                let mut current_item = *first_brace_item;
1327                let mut items_consumed = false;
1328
1329                // Greedily consume shape, size, position keywords
1330                loop {
1331                    let mut consumed_in_iteration = false;
1332                    let mut temp_iter = current_item.split_whitespace();
1333                    for word in temp_iter {
1334                        if let Ok(shape) = parse_shape(word) {
1335                            radial_gradient.shape = shape;
1336                            consumed_in_iteration = true;
1337                        } else if let Ok(size) = parse_radial_gradient_size(word) {
1338                            radial_gradient.size = size;
1339                            consumed_in_iteration = true;
1340                        } else if let Ok(pos) = parse_style_background_position(current_item) {
1341                            radial_gradient.position = pos;
1342                            consumed_in_iteration = true;
1343                            break; // position can have multiple words, so consume the rest of the
1344                                   // item
1345                        }
1346                    }
1347                    if consumed_in_iteration {
1348                        if let Some(next_item) = brace_iterator.next() {
1349                            current_item = next_item;
1350                            items_consumed = true;
1351                        } else {
1352                            break;
1353                        }
1354                    } else {
1355                        break;
1356                    }
1357                }
1358
1359                if items_consumed || parse_linear_color_stop(current_item).is_ok() {
1360                    radial_stops.push(parse_linear_color_stop(current_item)?);
1361                }
1362
1363                for item in brace_iterator {
1364                    radial_stops.push(parse_linear_color_stop(item)?);
1365                }
1366
1367                radial_gradient.stops = get_normalized_linear_stops(&radial_stops).into();
1368                Ok(StyleBackgroundContent::RadialGradient(radial_gradient))
1369            }
1370            GradientType::ConicGradient | GradientType::RepeatingConicGradient => {
1371                let mut conic_gradient = ConicGradient {
1372                    extend_mode: gradient_type.get_extend_mode(),
1373                    ..Default::default()
1374                };
1375                let mut conic_stops = Vec::new();
1376
1377                if let Some((angle, center)) = parse_conic_first_item(first_brace_item)? {
1378                    conic_gradient.angle = angle;
1379                    conic_gradient.center = center;
1380                } else {
1381                    conic_stops.push(parse_radial_color_stop(first_brace_item)?);
1382                }
1383
1384                for item in brace_iterator {
1385                    conic_stops.push(parse_radial_color_stop(item)?);
1386                }
1387
1388                conic_gradient.stops = get_normalized_radial_stops(&conic_stops).into();
1389                Ok(StyleBackgroundContent::ConicGradient(conic_gradient))
1390            }
1391        }
1392    }
1393
1394    // -- Gradient Parsing Helpers --
1395
1396    /// Parses color stops per W3C CSS Images Level 3:
1397    /// - "red" (no position)
1398    /// - "red 5%" (one position)
1399    /// - "red 10% 30%" (two positions - creates a hard color band)
1400    /// 
1401    /// Also supports system colors like `system:accent 50%` for theme-aware gradients.
1402    fn parse_linear_color_stop(
1403        input: &str,
1404    ) -> Result<LinearColorStop, CssGradientStopParseError<'_>> {
1405        let input = input.trim();
1406        let (color_str, offset1_str, offset2_str) = split_color_and_offsets(input);
1407
1408        let color = parse_color_or_system(color_str)?;
1409        let offset1 = match offset1_str {
1410            None => OptionPercentageValue::None,
1411            Some(s) => OptionPercentageValue::Some(
1412                parse_percentage_value(s).map_err(CssGradientStopParseError::Percentage)?,
1413            ),
1414        };
1415        let offset2 = match offset2_str {
1416            None => OptionPercentageValue::None,
1417            Some(s) => OptionPercentageValue::Some(
1418                parse_percentage_value(s).map_err(CssGradientStopParseError::Percentage)?,
1419            ),
1420        };
1421
1422        Ok(LinearColorStop {
1423            color,
1424            offset1,
1425            offset2,
1426        })
1427    }
1428
1429    /// Parses color stops per W3C CSS Images Level 3:
1430    /// - "red" (no position)
1431    /// - "red 90deg" (one position)
1432    /// - "red 45deg 90deg" (two positions - creates a hard color band)
1433    /// 
1434    /// Also supports system colors like `system:accent 90deg` for theme-aware gradients.
1435    fn parse_radial_color_stop(
1436        input: &str,
1437    ) -> Result<RadialColorStop, CssGradientStopParseError<'_>> {
1438        let input = input.trim();
1439        let (color_str, offset1_str, offset2_str) = split_color_and_offsets(input);
1440
1441        let color = parse_color_or_system(color_str)?;
1442        let offset1 = match offset1_str {
1443            None => OptionAngleValue::None,
1444            Some(s) => OptionAngleValue::Some(
1445                parse_angle_value(s).map_err(CssGradientStopParseError::Angle)?,
1446            ),
1447        };
1448        let offset2 = match offset2_str {
1449            None => OptionAngleValue::None,
1450            Some(s) => OptionAngleValue::Some(
1451                parse_angle_value(s).map_err(CssGradientStopParseError::Angle)?,
1452            ),
1453        };
1454
1455        Ok(RadialColorStop {
1456            color,
1457            offset1,
1458            offset2,
1459        })
1460    }
1461
1462    /// Helper to robustly split a string like "rgba(0,0,0,0.5) 10% 30%" into color and offset
1463    /// parts. Returns (`color_str`, offset1, offset2) where offsets are optional.
1464    ///
1465    /// Per W3C CSS Images Level 3, a color stop can have 0, 1, or 2 positions:
1466    /// - "red" -> ("red", None, None)
1467    /// - "red 50%" -> ("red", Some("50%"), None)
1468    /// - "red 10% 30%" -> ("red", Some("10%"), Some("30%"))
1469    fn split_color_and_offsets(input: &str) -> (&str, Option<&str>, Option<&str>) {
1470        // Strategy: scan from the end to find position values (contain digits + % or unit).
1471        // We need to handle complex colors like "rgba(0, 0, 0, 0.5)" that contain spaces and
1472        // digits.
1473
1474        let input = input.trim();
1475
1476        // Try to find the last position value (might be second of two)
1477        if let Some((remaining, last_offset)) = try_split_last_offset(input) {
1478            // Try to find another position value before it
1479            if let Some((color_part, first_offset)) = try_split_last_offset(remaining) {
1480                return (color_part.trim(), Some(first_offset), Some(last_offset));
1481            }
1482            return (remaining.trim(), Some(last_offset), None);
1483        }
1484
1485        (input, None, None)
1486    }
1487
1488    /// Try to split off the last whitespace-separated token if it looks like a position value.
1489    /// Returns (remaining, `offset_str`) if successful.
1490    fn try_split_last_offset(input: &str) -> Option<(&str, &str)> {
1491        let input = input.trim();
1492        if let Some(last_ws_idx) = input.rfind(char::is_whitespace) {
1493            let (potential_color, potential_offset) = input.split_at(last_ws_idx);
1494            let potential_offset = potential_offset.trim();
1495
1496            // A valid offset must contain a digit and typically ends with % or a unit
1497            // This avoids misinterpreting "to right bottom" as containing offsets
1498            if is_likely_offset(potential_offset) {
1499                return Some((potential_color, potential_offset));
1500            }
1501        }
1502        None
1503    }
1504
1505    /// Check if a string looks like a position value (percentage or length).
1506    /// Must contain a digit and typically ends with %, px, em, etc.
1507    fn is_likely_offset(s: &str) -> bool {
1508        if !s.contains(|c: char| c.is_ascii_digit()) {
1509            return false;
1510        }
1511        // Check if it ends with a known unit or %
1512        let units = [
1513            "%", "px", "em", "rem", "ex", "ch", "vw", "vh", "vmin", "vmax", "cm", "mm", "in", "pt",
1514            "pc", "deg", "rad", "grad", "turn",
1515        ];
1516        units.iter().any(|u| s.ends_with(u))
1517    }
1518
1519    /// Parses the `from <angle> at <position>` part of a conic gradient.
1520    fn parse_conic_first_item(
1521        input: &str,
1522    ) -> Result<Option<(AngleValue, StyleBackgroundPosition)>, CssConicGradientParseError<'_>> {
1523        let input = input.trim();
1524        if !input.starts_with("from") {
1525            return Ok(None);
1526        }
1527
1528        let mut parts = input["from".len()..].trim().split("at");
1529        let angle_part = parts
1530            .next()
1531            .ok_or(CssConicGradientParseError::NoAngle(input))?
1532            .trim();
1533        let angle = parse_angle_value(angle_part)?;
1534
1535        let position = match parts.next() {
1536            Some(pos_part) => parse_style_background_position(pos_part.trim())?,
1537            None => StyleBackgroundPosition::default(),
1538        };
1539
1540        Ok(Some((angle, position)))
1541    }
1542
1543    // -- Normalization Functions --
1544
1545    macro_rules! impl_get_normalized_stops {
1546        (
1547            fn $fn_name:ident($input_stop:ty) -> Vec<$output_stop:ident>,
1548            pos_type = $pos_ty:ty,
1549            default_start = $default_start:expr,
1550            default_end = $default_end:expr,
1551            pos_ctor = $pos_ctor:expr,
1552            pos_to_f32 = $pos_to_f32:expr,
1553            output_field = $out_field:ident,
1554        ) => {
1555            #[allow(clippy::suboptimal_flops)] // explicit FP; mul_add slower without +fma
1556            fn $fn_name(stops: &[$input_stop]) -> Vec<$output_stop> {
1557                if stops.is_empty() {
1558                    return Vec::new();
1559                }
1560
1561                let mut expanded: Vec<(ColorOrSystem, Option<$pos_ty>)> = Vec::new();
1562
1563                for stop in stops {
1564                    match (stop.offset1.into_option(), stop.offset2.into_option()) {
1565                        (None, _) => {
1566                            expanded.push((stop.color, None));
1567                        }
1568                        (Some(pos1), None) => {
1569                            expanded.push((stop.color, Some(pos1)));
1570                        }
1571                        (Some(pos1), Some(pos2)) => {
1572                            expanded.push((stop.color, Some(pos1)));
1573                            expanded.push((stop.color, Some(pos2)));
1574                        }
1575                    }
1576                }
1577
1578                if expanded.is_empty() {
1579                    return Vec::new();
1580                }
1581
1582                let pos_ctor: fn(f32) -> $pos_ty = $pos_ctor;
1583                let pos_to_f32: fn(&$pos_ty) -> f32 = $pos_to_f32;
1584
1585                if expanded[0].1.is_none() {
1586                    expanded[0].1 = Some(pos_ctor($default_start));
1587                }
1588                let last_idx = expanded.len() - 1;
1589                if expanded[last_idx].1.is_none() {
1590                    expanded[last_idx].1 = Some(pos_ctor($default_end));
1591                }
1592
1593                let mut max_so_far: f32 = 0.0;
1594                for (_, pos) in expanded.iter_mut() {
1595                    if let Some(p) = pos {
1596                        let val = pos_to_f32(p);
1597                        if val < max_so_far {
1598                            *p = pos_ctor(max_so_far);
1599                        } else {
1600                            max_so_far = val;
1601                        }
1602                    }
1603                }
1604
1605                let mut i = 0;
1606                while i < expanded.len() {
1607                    if expanded[i].1.is_none() {
1608                        let run_start = i;
1609                        let mut run_end = i;
1610                        while run_end < expanded.len() && expanded[run_end].1.is_none() {
1611                            run_end += 1;
1612                        }
1613
1614                        let prev_pos = if run_start > 0 {
1615                            pos_to_f32(&expanded[run_start - 1].1.unwrap())
1616                        } else {
1617                            $default_start
1618                        };
1619
1620                        let next_pos = if run_end < expanded.len() {
1621                            pos_to_f32(&expanded[run_end].1.unwrap())
1622                        } else {
1623                            $default_end
1624                        };
1625
1626                        let run_len = run_end - run_start;
1627                        let step = (next_pos - prev_pos) / crate::cast::usize_to_f32(run_len + 1);
1628
1629                        for j in 0..run_len {
1630                            expanded[run_start + j].1 =
1631                                Some(pos_ctor(prev_pos + step * crate::cast::usize_to_f32(j + 1)));
1632                        }
1633
1634                        i = run_end;
1635                    } else {
1636                        i += 1;
1637                    }
1638                }
1639
1640                expanded
1641                    .into_iter()
1642                    .map(|(color, pos)| {
1643                        $output_stop {
1644                            $out_field: pos.unwrap_or(pos_ctor($default_start)),
1645                            color,
1646                        }
1647                    })
1648                    .collect()
1649            }
1650        };
1651    }
1652
1653    impl_get_normalized_stops! {
1654        fn get_normalized_linear_stops(LinearColorStop) -> Vec<NormalizedLinearColorStop>,
1655        pos_type = PercentageValue,
1656        default_start = 0.0,
1657        default_end = 100.0,
1658        pos_ctor = (|v| PercentageValue::new(v)),
1659        pos_to_f32 = (|p: &PercentageValue| p.normalized() * 100.0),
1660        output_field = offset,
1661    }
1662
1663    impl_get_normalized_stops! {
1664        fn get_normalized_radial_stops(RadialColorStop) -> Vec<NormalizedRadialColorStop>,
1665        pos_type = AngleValue,
1666        default_start = 0.0,
1667        default_end = 360.0,
1668        pos_ctor = (|v| AngleValue::deg(v)),
1669        pos_to_f32 = (|p: &AngleValue| p.to_degrees_raw()),
1670        output_field = angle,
1671    }
1672
1673    // -- Other Background Helpers --
1674
1675    fn parse_background_position_horizontal(
1676        input: &str,
1677    ) -> Result<BackgroundPositionHorizontal, CssPixelValueParseError<'_>> {
1678        Ok(match input {
1679            "left" => BackgroundPositionHorizontal::Left,
1680            "center" => BackgroundPositionHorizontal::Center,
1681            "right" => BackgroundPositionHorizontal::Right,
1682            other => BackgroundPositionHorizontal::Exact(parse_pixel_value(other)?),
1683        })
1684    }
1685
1686    fn parse_background_position_vertical(
1687        input: &str,
1688    ) -> Result<BackgroundPositionVertical, CssPixelValueParseError<'_>> {
1689        Ok(match input {
1690            "top" => BackgroundPositionVertical::Top,
1691            "center" => BackgroundPositionVertical::Center,
1692            "bottom" => BackgroundPositionVertical::Bottom,
1693            other => BackgroundPositionVertical::Exact(parse_pixel_value(other)?),
1694        })
1695    }
1696
1697    fn parse_shape(input: &str) -> Result<Shape, CssShapeParseError<'_>> {
1698        match input.trim() {
1699            "circle" => Ok(Shape::Circle),
1700            "ellipse" => Ok(Shape::Ellipse),
1701            _ => Err(CssShapeParseError::ShapeErr(InvalidValueErr(input))),
1702        }
1703    }
1704
1705    fn parse_radial_gradient_size(
1706        input: &str,
1707    ) -> Result<RadialGradientSize, InvalidValueErr<'_>> {
1708        match input.trim() {
1709            "closest-side" => Ok(RadialGradientSize::ClosestSide),
1710            "closest-corner" => Ok(RadialGradientSize::ClosestCorner),
1711            "farthest-side" => Ok(RadialGradientSize::FarthestSide),
1712            "farthest-corner" => Ok(RadialGradientSize::FarthestCorner),
1713            _ => Err(InvalidValueErr(input)),
1714        }
1715    }
1716
1717    /// Adversarial tests. Lives inside `mod parser` (not at file scope) because
1718    /// the interesting helpers -- `parse_gradient`, `split_color_and_offsets`,
1719    /// `try_split_last_offset`, `is_likely_offset`, `parse_conic_first_item`,
1720    /// `parse_shape`, ... -- are private to this module.
1721    #[cfg(test)]
1722    #[allow(
1723        clippy::float_cmp,
1724        clippy::too_many_lines,
1725        clippy::unreadable_literal,
1726        clippy::cognitive_complexity,
1727        clippy::wildcard_imports
1728    )]
1729    mod autotest_generated {
1730        // `super::*` = the private parser helpers under test; the second glob pulls in
1731        // the value/error types from the enclosing `background` module.
1732        use super::*;
1733        use crate::props::style::background::*;
1734        use crate::{
1735            props::basic::{
1736                angle::CssAngleValueParseError,
1737                color::{CssColorParseError, OptionColorU, SystemColorRef},
1738                direction::CssDirectionParseError,
1739                error::InvalidValueErr,
1740                length::PercentageParseError,
1741                parse::{CssImageParseError, ParenthesisParseError},
1742                pixel::CssPixelValueParseError,
1743            },
1744            system::SystemColors,
1745        };
1746        use alloc::{string::ToString, vec::Vec};
1747
1748        // ---------------------------------------------------------------
1749        // fixtures
1750        // ---------------------------------------------------------------
1751
1752        /// Inputs that every `&str` parser in this file is swept over: empty,
1753        /// whitespace, garbage, boundary numbers, unbalanced braces, unicode.
1754        const ADVERSARIAL: &[&str] = &[
1755            "",
1756            " ",
1757            "   ",
1758            "\t\n\r",
1759            "\u{0}",
1760            "!!!",
1761            ";",
1762            ",",
1763            ",,",
1764            "(",
1765            ")",
1766            "()",
1767            "((((",
1768            "0",
1769            "-0",
1770            "+0",
1771            "NaN",
1772            "nan",
1773            "inf",
1774            "-inf",
1775            "1e40",
1776            "-1e40",
1777            "1e-45",
1778            "3.4028235e38",
1779            "9223372036854775807",
1780            "-9223372036854775808",
1781            "\u{1F600}",
1782            "e\u{0301}\u{0301}\u{0301}",
1783            "\u{00a0}",
1784            "red\u{00a0}50%",
1785            "  valid  ",
1786            "valid;garbage",
1787            "red;blue",
1788            "linear-gradient",
1789            "linear-gradient(",
1790            "linear-gradient()",
1791            "url(",
1792            "url()",
1793            "rgba(",
1794            "rgb(0,0,0",
1795            "to right",
1796            "circle",
1797            "from",
1798        ];
1799
1800        const ALL_SYSTEM_REFS: [SystemColorRef; 9] = [
1801            SystemColorRef::Text,
1802            SystemColorRef::Background,
1803            SystemColorRef::Accent,
1804            SystemColorRef::AccentText,
1805            SystemColorRef::ButtonFace,
1806            SystemColorRef::ButtonText,
1807            SystemColorRef::WindowBackground,
1808            SystemColorRef::SelectionBackground,
1809            SystemColorRef::SelectionText,
1810        ];
1811
1812        const ALL_GRADIENT_TYPES: [GradientType; 6] = [
1813            GradientType::LinearGradient,
1814            GradientType::RepeatingLinearGradient,
1815            GradientType::RadialGradient,
1816            GradientType::RepeatingRadialGradient,
1817            GradientType::ConicGradient,
1818            GradientType::RepeatingConicGradient,
1819        ];
1820
1821        fn blue() -> ColorU {
1822            ColorU::new_rgb(0, 0, 255)
1823        }
1824
1825        fn linear(input: &str) -> LinearGradient {
1826            match parse_style_background_content(input) {
1827                Ok(StyleBackgroundContent::LinearGradient(g)) => g,
1828                other => panic!("expected a linear gradient for {input:?}, got {other:?}"),
1829            }
1830        }
1831
1832        fn radial(input: &str) -> RadialGradient {
1833            match parse_style_background_content(input) {
1834                Ok(StyleBackgroundContent::RadialGradient(g)) => g,
1835                other => panic!("expected a radial gradient for {input:?}, got {other:?}"),
1836            }
1837        }
1838
1839        fn conic(input: &str) -> ConicGradient {
1840            match parse_style_background_content(input) {
1841                Ok(StyleBackgroundContent::ConicGradient(g)) => g,
1842                other => panic!("expected a conic gradient for {input:?}, got {other:?}"),
1843            }
1844        }
1845
1846        /// Offsets of a linear/radial gradient, in percent.
1847        fn offsets(stops: &NormalizedLinearColorStopVec) -> Vec<f32> {
1848            stops
1849                .iter()
1850                .map(|s| s.offset.normalized() * 100.0)
1851                .collect()
1852        }
1853
1854        // ---------------------------------------------------------------
1855        // serializers: Shape::fmt / RadialGradientSize::fmt
1856        // ---------------------------------------------------------------
1857
1858        #[test]
1859        fn autotest_shape_display_is_exact_and_never_empty() {
1860            assert_eq!(Shape::Ellipse.to_string(), "ellipse");
1861            assert_eq!(Shape::Circle.to_string(), "circle");
1862            assert_eq!(Shape::default(), Shape::Ellipse);
1863            assert_eq!(Shape::default().to_string(), "ellipse");
1864            for s in [Shape::Ellipse, Shape::Circle] {
1865                assert!(!s.to_string().is_empty());
1866                // The serialized form is a valid input for the parser.
1867                assert_eq!(parse_shape(&s.to_string()).unwrap(), s);
1868            }
1869        }
1870
1871        #[test]
1872        fn autotest_radial_gradient_size_display_is_exact_and_never_empty() {
1873            assert_eq!(RadialGradientSize::ClosestSide.to_string(), "closest-side");
1874            assert_eq!(
1875                RadialGradientSize::ClosestCorner.to_string(),
1876                "closest-corner"
1877            );
1878            assert_eq!(
1879                RadialGradientSize::FarthestSide.to_string(),
1880                "farthest-side"
1881            );
1882            assert_eq!(
1883                RadialGradientSize::FarthestCorner.to_string(),
1884                "farthest-corner"
1885            );
1886            assert_eq!(
1887                RadialGradientSize::default(),
1888                RadialGradientSize::FarthestCorner
1889            );
1890            for s in [
1891                RadialGradientSize::ClosestSide,
1892                RadialGradientSize::ClosestCorner,
1893                RadialGradientSize::FarthestSide,
1894                RadialGradientSize::FarthestCorner,
1895            ] {
1896                assert!(!s.to_string().is_empty());
1897                assert_eq!(parse_radial_gradient_size(&s.to_string()).unwrap(), s);
1898            }
1899        }
1900
1901        // ---------------------------------------------------------------
1902        // constructors: Normalized{Linear,Radial}ColorStop::new
1903        // ---------------------------------------------------------------
1904
1905        #[test]
1906        fn autotest_normalized_linear_stop_new_keeps_its_arguments() {
1907            let stop = NormalizedLinearColorStop::new(PercentageValue::new(42.5), ColorU::RED);
1908            assert_eq!(stop.offset.normalized() * 100.0, 42.5);
1909            assert_eq!(stop.color, ColorOrSystem::Color(ColorU::RED));
1910
1911            // Extreme offsets must not panic, and must stay finite: FloatValue
1912            // encodes f32*1000 into an isize, and `as` saturates (NaN -> 0).
1913            for f in [
1914                0.0_f32,
1915                -0.0,
1916                f32::NAN,
1917                f32::INFINITY,
1918                f32::NEG_INFINITY,
1919                f32::MAX,
1920                f32::MIN,
1921                f32::MIN_POSITIVE,
1922                -100.0,
1923                1e30,
1924            ] {
1925                let stop = NormalizedLinearColorStop::new(
1926                    PercentageValue::new(f),
1927                    ColorU::TRANSPARENT,
1928                );
1929                assert!(
1930                    stop.offset.normalized().is_finite(),
1931                    "offset went non-finite for {f}"
1932                );
1933                assert_eq!(stop.color, ColorOrSystem::Color(ColorU::TRANSPARENT));
1934            }
1935            // NaN is flushed to 0, not propagated.
1936            assert_eq!(
1937                NormalizedLinearColorStop::new(PercentageValue::new(f32::NAN), ColorU::RED).offset,
1938                PercentageValue::new(0.0)
1939            );
1940        }
1941
1942        #[test]
1943        fn autotest_normalized_radial_stop_new_keeps_its_arguments() {
1944            let stop = NormalizedRadialColorStop::new(AngleValue::deg(90.0), ColorU::RED);
1945            assert_eq!(stop.angle, AngleValue::deg(90.0));
1946            assert_eq!(stop.angle.to_degrees_raw(), 90.0);
1947            assert_eq!(stop.color, ColorOrSystem::Color(ColorU::RED));
1948
1949            for f in [
1950                0.0_f32,
1951                -0.0,
1952                f32::NAN,
1953                f32::INFINITY,
1954                f32::NEG_INFINITY,
1955                f32::MAX,
1956                f32::MIN,
1957                720.0,
1958                -360.0,
1959            ] {
1960                let stop = NormalizedRadialColorStop::new(AngleValue::deg(f), ColorU::WHITE);
1961                assert!(
1962                    stop.angle.to_degrees_raw().is_finite(),
1963                    "angle went non-finite for {f}"
1964                );
1965                assert_eq!(stop.color, ColorOrSystem::Color(ColorU::WHITE));
1966            }
1967            assert_eq!(
1968                NormalizedRadialColorStop::new(AngleValue::deg(f32::NAN), ColorU::RED).angle,
1969                AngleValue::deg(0.0)
1970            );
1971        }
1972
1973        // ---------------------------------------------------------------
1974        // Normalized{Linear,Radial}ColorStop::resolve
1975        // ---------------------------------------------------------------
1976
1977        #[test]
1978        fn autotest_resolve_concrete_color_ignores_system_colors() {
1979            let stop = NormalizedLinearColorStop::new(PercentageValue::new(0.0), ColorU::RED);
1980            assert_eq!(stop.resolve(&SystemColors::default(), ColorU::WHITE), ColorU::RED);
1981
1982            let populated = SystemColors {
1983                accent: OptionColorU::Some(ColorU::new_rgb(1, 2, 3)),
1984                ..SystemColors::default()
1985            };
1986            assert_eq!(stop.resolve(&populated, ColorU::WHITE), ColorU::RED);
1987
1988            let rstop = NormalizedRadialColorStop::new(AngleValue::deg(0.0), ColorU::RED);
1989            assert_eq!(rstop.resolve(&populated, ColorU::WHITE), ColorU::RED);
1990        }
1991
1992        #[test]
1993        fn autotest_resolve_system_stop_falls_back_for_every_variant() {
1994            let fallback = ColorU::rgba(9, 8, 7, 6);
1995            for r in ALL_SYSTEM_REFS {
1996                let lin = NormalizedLinearColorStop {
1997                    offset: PercentageValue::new(50.0),
1998                    color: ColorOrSystem::System(r),
1999                };
2000                let rad = NormalizedRadialColorStop {
2001                    angle: AngleValue::deg(180.0),
2002                    color: ColorOrSystem::System(r),
2003                };
2004                // Nothing is populated -> every variant resolves to the fallback.
2005                assert_eq!(lin.resolve(&SystemColors::default(), fallback), fallback);
2006                assert_eq!(rad.resolve(&SystemColors::default(), fallback), fallback);
2007            }
2008
2009            // A populated key resolves; the others still fall back.
2010            let accent = ColorU::new_rgb(0, 122, 255);
2011            let populated = SystemColors {
2012                accent: OptionColorU::Some(accent),
2013                ..SystemColors::default()
2014            };
2015            let stop = NormalizedLinearColorStop {
2016                offset: PercentageValue::new(0.0),
2017                color: ColorOrSystem::System(SystemColorRef::Accent),
2018            };
2019            assert_eq!(stop.resolve(&populated, fallback), accent);
2020            let other = NormalizedLinearColorStop {
2021                offset: PercentageValue::new(0.0),
2022                color: ColorOrSystem::System(SystemColorRef::ButtonText),
2023            };
2024            assert_eq!(other.resolve(&populated, fallback), fallback);
2025        }
2026
2027        // ---------------------------------------------------------------
2028        // numeric: scale_for_dpi
2029        // ---------------------------------------------------------------
2030
2031        #[test]
2032        fn autotest_background_position_horizontal_scale_for_dpi() {
2033            // Keywords are immune to scaling, for *any* factor.
2034            for f in [0.0_f32, 1.0, -1.0, f32::NAN, f32::INFINITY, f32::MIN, f32::MAX] {
2035                for keyword in [
2036                    BackgroundPositionHorizontal::Left,
2037                    BackgroundPositionHorizontal::Center,
2038                    BackgroundPositionHorizontal::Right,
2039                ] {
2040                    let mut k = keyword;
2041                    k.scale_for_dpi(f);
2042                    assert_eq!(k, keyword, "keyword mutated by scale factor {f}");
2043                }
2044            }
2045
2046            let mut exact = BackgroundPositionHorizontal::Exact(PixelValue::px(10.0));
2047            exact.scale_for_dpi(2.0);
2048            assert_eq!(exact, BackgroundPositionHorizontal::Exact(PixelValue::px(20.0)));
2049
2050            // zero, negative
2051            let mut zeroed = BackgroundPositionHorizontal::Exact(PixelValue::px(10.0));
2052            zeroed.scale_for_dpi(0.0);
2053            assert_eq!(zeroed, BackgroundPositionHorizontal::Exact(PixelValue::px(0.0)));
2054
2055            let mut negated = BackgroundPositionHorizontal::Exact(PixelValue::px(10.0));
2056            negated.scale_for_dpi(-1.0);
2057            assert_eq!(
2058                negated,
2059                BackgroundPositionHorizontal::Exact(PixelValue::px(-10.0))
2060            );
2061
2062            // NaN is flushed to 0 by the isize cast, never propagated.
2063            let mut nan = BackgroundPositionHorizontal::Exact(PixelValue::px(10.0));
2064            nan.scale_for_dpi(f32::NAN);
2065            assert_eq!(nan, BackgroundPositionHorizontal::Exact(PixelValue::px(0.0)));
2066
2067            // +-inf and MIN/MAX saturate to the isize bounds -- finite, no panic.
2068            for f in [f32::INFINITY, f32::NEG_INFINITY, f32::MAX, f32::MIN] {
2069                let mut v = BackgroundPositionHorizontal::Exact(PixelValue::px(10.0));
2070                v.scale_for_dpi(f);
2071                let BackgroundPositionHorizontal::Exact(px) = v else {
2072                    panic!("variant changed under scaling");
2073                };
2074                assert!(px.number.get().is_finite(), "non-finite result for {f}");
2075                assert_eq!(px.number.get().is_sign_negative(), f.is_sign_negative());
2076            }
2077        }
2078
2079        #[test]
2080        fn autotest_background_position_vertical_scale_for_dpi() {
2081            for f in [0.0_f32, 1.0, -1.0, f32::NAN, f32::INFINITY, f32::MIN, f32::MAX] {
2082                for keyword in [
2083                    BackgroundPositionVertical::Top,
2084                    BackgroundPositionVertical::Center,
2085                    BackgroundPositionVertical::Bottom,
2086                ] {
2087                    let mut k = keyword;
2088                    k.scale_for_dpi(f);
2089                    assert_eq!(k, keyword, "keyword mutated by scale factor {f}");
2090                }
2091            }
2092
2093            let mut exact = BackgroundPositionVertical::Exact(PixelValue::em(4.0));
2094            exact.scale_for_dpi(0.5);
2095            assert_eq!(exact, BackgroundPositionVertical::Exact(PixelValue::em(2.0)));
2096
2097            let mut nan = BackgroundPositionVertical::Exact(PixelValue::px(10.0));
2098            nan.scale_for_dpi(f32::NAN);
2099            assert_eq!(nan, BackgroundPositionVertical::Exact(PixelValue::px(0.0)));
2100
2101            // Saturation is a fixed point: scaling an already-saturated value again
2102            // must not wrap around into a negative number.
2103            let mut saturated = BackgroundPositionVertical::Exact(PixelValue::px(f32::MAX));
2104            saturated.scale_for_dpi(f32::MAX);
2105            let once = saturated;
2106            saturated.scale_for_dpi(f32::MAX);
2107            assert_eq!(saturated, once);
2108            let BackgroundPositionVertical::Exact(px) = saturated else {
2109                panic!("variant changed under scaling");
2110            };
2111            assert!(px.number.get() > 0.0);
2112            assert!(px.number.get().is_finite());
2113        }
2114
2115        #[test]
2116        fn autotest_style_background_position_scale_for_dpi_scales_both_axes() {
2117            let mut pos = StyleBackgroundPosition {
2118                horizontal: BackgroundPositionHorizontal::Exact(PixelValue::px(10.0)),
2119                vertical: BackgroundPositionVertical::Exact(PixelValue::px(20.0)),
2120            };
2121            pos.scale_for_dpi(3.0);
2122            assert_eq!(
2123                pos.horizontal,
2124                BackgroundPositionHorizontal::Exact(PixelValue::px(30.0))
2125            );
2126            assert_eq!(
2127                pos.vertical,
2128                BackgroundPositionVertical::Exact(PixelValue::px(60.0))
2129            );
2130
2131            // Scaling compounds -- pinned, because a double-applied DPI scale is a
2132            // classic layout bug.
2133            pos.scale_for_dpi(2.0);
2134            assert_eq!(
2135                pos.horizontal,
2136                BackgroundPositionHorizontal::Exact(PixelValue::px(60.0))
2137            );
2138
2139            // The all-keyword default is a fixed point for every factor.
2140            for f in [0.0_f32, 1.0, -2.5, f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
2141                let mut default = StyleBackgroundPosition::default();
2142                default.scale_for_dpi(f);
2143                assert_eq!(default, StyleBackgroundPosition::default());
2144            }
2145        }
2146
2147        #[test]
2148        fn autotest_style_background_size_scale_for_dpi() {
2149            // Contain / Cover carry no number and must survive any factor.
2150            for f in [0.0_f32, 2.0, -1.0, f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
2151                for keyword in [StyleBackgroundSize::Contain, StyleBackgroundSize::Cover] {
2152                    let mut k = keyword;
2153                    k.scale_for_dpi(f);
2154                    assert_eq!(k, keyword, "keyword mutated by scale factor {f}");
2155                }
2156            }
2157
2158            let mut size = StyleBackgroundSize::ExactSize(PixelValueSize {
2159                width: PixelValue::px(10.0),
2160                height: PixelValue::percent(50.0),
2161            });
2162            size.scale_for_dpi(2.0);
2163            assert_eq!(
2164                size,
2165                StyleBackgroundSize::ExactSize(PixelValueSize {
2166                    width: PixelValue::px(20.0),
2167                    // NOTE: percentages are scaled too, which is arguably wrong for a
2168                    // DPI change -- pinned as current behaviour.
2169                    height: PixelValue::percent(100.0),
2170                })
2171            );
2172
2173            let mut nan = StyleBackgroundSize::ExactSize(PixelValueSize {
2174                width: PixelValue::px(10.0),
2175                height: PixelValue::px(20.0),
2176            });
2177            nan.scale_for_dpi(f32::NAN);
2178            assert_eq!(
2179                nan,
2180                StyleBackgroundSize::ExactSize(PixelValueSize {
2181                    width: PixelValue::px(0.0),
2182                    height: PixelValue::px(0.0),
2183                })
2184            );
2185
2186            let mut inf = StyleBackgroundSize::ExactSize(PixelValueSize {
2187                width: PixelValue::px(1.0),
2188                height: PixelValue::px(-1.0),
2189            });
2190            inf.scale_for_dpi(f32::INFINITY);
2191            let StyleBackgroundSize::ExactSize(s) = inf else {
2192                panic!("variant changed under scaling");
2193            };
2194            assert!(s.width.number.get().is_finite() && s.width.number.get() > 0.0);
2195            assert!(s.height.number.get().is_finite() && s.height.number.get() < 0.0);
2196        }
2197
2198        // ---------------------------------------------------------------
2199        // getters: to_contained / to_shared round-trips
2200        // ---------------------------------------------------------------
2201
2202        #[test]
2203        fn autotest_css_background_parse_error_round_trips() {
2204            let errors = [
2205                CssBackgroundParseError::Error(""),
2206                CssBackgroundParseError::Error("boom \u{1F600}"),
2207                CssBackgroundParseError::InvalidBackground(ParenthesisParseError::EmptyInput),
2208                CssBackgroundParseError::InvalidBackground(
2209                    ParenthesisParseError::StopWordNotFound("nope"),
2210                ),
2211                CssBackgroundParseError::UnclosedGradient(""),
2212                CssBackgroundParseError::NoDirection("nodir"),
2213                CssBackgroundParseError::TooFewGradientStops("few"),
2214                CssBackgroundParseError::DirectionParseError(CssDirectionParseError::Error("d")),
2215                CssBackgroundParseError::DirectionParseError(
2216                    CssDirectionParseError::InvalidArguments("args"),
2217                ),
2218                CssBackgroundParseError::GradientParseError(CssGradientStopParseError::Error("g")),
2219                CssBackgroundParseError::ConicGradient(CssConicGradientParseError::NoAngle("a")),
2220                CssBackgroundParseError::ShapeParseError(CssShapeParseError::ShapeErr(
2221                    InvalidValueErr("s"),
2222                )),
2223                CssBackgroundParseError::ImageParseError(CssImageParseError::UnclosedQuotes("q")),
2224                CssBackgroundParseError::ColorParseError(CssColorParseError::InvalidColor("c")),
2225                CssBackgroundParseError::ColorParseError(CssColorParseError::EmptyInput),
2226            ];
2227            for e in &errors {
2228                let owned = e.to_contained();
2229                assert_eq!(&owned.to_shared(), e, "round-trip changed {e:?}");
2230                // Display must survive the round-trip as well.
2231                assert_eq!(
2232                    alloc::format!("{}", owned.to_shared()),
2233                    alloc::format!("{e}")
2234                );
2235            }
2236        }
2237
2238        #[test]
2239        fn autotest_error_round_trip_survives_huge_and_unicode_payloads() {
2240            let huge = "x".repeat(100_000);
2241            let weird = "\u{1F600}\u{0}\u{00a0}e\u{0301}";
2242            for s in [huge.as_str(), weird, "", " "] {
2243                let e = CssBackgroundParseError::UnclosedGradient(s);
2244                assert_eq!(e.to_contained().to_shared(), e);
2245
2246                let e = CssGradientStopParseError::Error(s);
2247                assert_eq!(e.to_contained().to_shared(), e);
2248
2249                let e = CssConicGradientParseError::NoAngle(s);
2250                assert_eq!(e.to_contained().to_shared(), e);
2251
2252                let e = CssShapeParseError::ShapeErr(InvalidValueErr(s));
2253                assert_eq!(e.to_contained().to_shared(), e);
2254
2255                let e = CssBackgroundPositionParseError::NoPosition(s);
2256                assert_eq!(e.to_contained().to_shared(), e);
2257            }
2258        }
2259
2260        #[test]
2261        fn autotest_css_gradient_stop_parse_error_round_trips() {
2262            let errors = [
2263                CssGradientStopParseError::Error("boom"),
2264                CssGradientStopParseError::Percentage(PercentageParseError::NoPercentSign),
2265                CssGradientStopParseError::Percentage(PercentageParseError::InvalidUnit(
2266                    "px".to_string().into(),
2267                )),
2268                CssGradientStopParseError::Angle(CssAngleValueParseError::EmptyString),
2269                CssGradientStopParseError::Angle(CssAngleValueParseError::InvalidAngle("q")),
2270                CssGradientStopParseError::ColorParseError(CssColorParseError::InvalidColor("c")),
2271            ];
2272            for e in &errors {
2273                assert_eq!(&e.to_contained().to_shared(), e, "round-trip changed {e:?}");
2274            }
2275        }
2276
2277        #[test]
2278        fn autotest_css_conic_and_shape_parse_error_round_trip() {
2279            let errors = [
2280                CssConicGradientParseError::NoAngle("n"),
2281                CssConicGradientParseError::Angle(CssAngleValueParseError::EmptyString),
2282                CssConicGradientParseError::Position(
2283                    CssBackgroundPositionParseError::NoPosition("p"),
2284                ),
2285            ];
2286            for e in &errors {
2287                assert_eq!(&e.to_contained().to_shared(), e);
2288            }
2289
2290            let shape = CssShapeParseError::ShapeErr(InvalidValueErr("blob"));
2291            assert_eq!(shape.to_contained().to_shared(), shape);
2292        }
2293
2294        #[test]
2295        fn autotest_css_background_position_parse_error_round_trips() {
2296            let errors = [
2297                CssBackgroundPositionParseError::NoPosition(""),
2298                CssBackgroundPositionParseError::TooManyComponents("a b c"),
2299                CssBackgroundPositionParseError::FirstComponentWrong(
2300                    CssPixelValueParseError::EmptyString,
2301                ),
2302                CssBackgroundPositionParseError::FirstComponentWrong(
2303                    CssPixelValueParseError::InvalidPixelValue("q"),
2304                ),
2305                CssBackgroundPositionParseError::SecondComponentWrong(
2306                    CssPixelValueParseError::InvalidPixelValue("\u{1F600}"),
2307                ),
2308            ];
2309            for e in &errors {
2310                assert_eq!(&e.to_contained().to_shared(), e, "round-trip changed {e:?}");
2311            }
2312        }
2313
2314        #[test]
2315        fn autotest_real_parse_errors_round_trip_through_the_owned_form() {
2316            // Errors as actually produced by the parsers, not hand-built ones.
2317            for input in ADVERSARIAL {
2318                if let Err(e) = parse_style_background_content(input) {
2319                    assert_eq!(e.to_contained().to_shared(), e, "for input {input:?}");
2320                }
2321                if let Err(e) = parse_style_background_position(input) {
2322                    assert_eq!(e.to_contained().to_shared(), e, "for input {input:?}");
2323                }
2324            }
2325        }
2326
2327        // ---------------------------------------------------------------
2328        // GradientType::get_extend_mode (private)
2329        // ---------------------------------------------------------------
2330
2331        #[test]
2332        fn autotest_get_extend_mode_is_repeat_exactly_for_the_repeating_variants() {
2333            assert_eq!(
2334                GradientType::LinearGradient.get_extend_mode(),
2335                ExtendMode::Clamp
2336            );
2337            assert_eq!(
2338                GradientType::RadialGradient.get_extend_mode(),
2339                ExtendMode::Clamp
2340            );
2341            assert_eq!(
2342                GradientType::ConicGradient.get_extend_mode(),
2343                ExtendMode::Clamp
2344            );
2345            assert_eq!(
2346                GradientType::RepeatingLinearGradient.get_extend_mode(),
2347                ExtendMode::Repeat
2348            );
2349            assert_eq!(
2350                GradientType::RepeatingRadialGradient.get_extend_mode(),
2351                ExtendMode::Repeat
2352            );
2353            assert_eq!(
2354                GradientType::RepeatingConicGradient.get_extend_mode(),
2355                ExtendMode::Repeat
2356            );
2357            // Total + pure: same input, same answer.
2358            for t in ALL_GRADIENT_TYPES {
2359                assert_eq!(t.get_extend_mode(), t.get_extend_mode());
2360            }
2361            assert_eq!(ExtendMode::default(), ExtendMode::Clamp);
2362        }
2363
2364        // ---------------------------------------------------------------
2365        // parser: parse_style_background_content
2366        // ---------------------------------------------------------------
2367
2368        #[test]
2369        fn autotest_background_content_never_panics_and_is_deterministic() {
2370            for input in ADVERSARIAL {
2371                let a = parse_style_background_content(input);
2372                let b = parse_style_background_content(input);
2373                assert_eq!(a, b, "non-deterministic for {input:?}");
2374            }
2375        }
2376
2377        #[test]
2378        fn autotest_background_content_rejects_empty_whitespace_and_garbage() {
2379            for input in ["", " ", "   ", "\t\n\r", "\u{0}", "!!!", ";", "valid;garbage"] {
2380                assert!(
2381                    parse_style_background_content(input).is_err(),
2382                    "{input:?} should not parse as a background"
2383                );
2384            }
2385        }
2386
2387        #[test]
2388        fn autotest_background_content_valid_minimal_positive_controls() {
2389            assert_eq!(
2390                parse_style_background_content("red").unwrap(),
2391                StyleBackgroundContent::Color(ColorU::RED)
2392            );
2393            // Leading/trailing whitespace is trimmed, not rejected.
2394            assert_eq!(
2395                parse_style_background_content("  red  ").unwrap(),
2396                StyleBackgroundContent::Color(ColorU::RED)
2397            );
2398            assert_eq!(
2399                parse_style_background_content("system:accent").unwrap(),
2400                StyleBackgroundContent::SystemColor(SystemColorRef::Accent)
2401            );
2402            assert_eq!(
2403                parse_style_background_content("url(a.png)").unwrap(),
2404                StyleBackgroundContent::Image("a.png".into())
2405            );
2406        }
2407
2408        #[test]
2409        fn autotest_background_content_unicode_is_rejected_without_panicking() {
2410            for input in [
2411                "\u{1F600}",
2412                "url(\u{1F600}.png)",
2413                "linear-gradient(\u{1F600}, red)",
2414                "e\u{0301}\u{0301}\u{0301}",
2415                "\u{00a0}",
2416            ] {
2417                let parsed = parse_style_background_content(input);
2418                // url() accepts any payload; everything else must be an error.
2419                if input.starts_with("url(") {
2420                    assert!(parsed.is_ok());
2421                } else {
2422                    assert!(parsed.is_err(), "{input:?} unexpectedly parsed");
2423                }
2424            }
2425        }
2426
2427        #[test]
2428        fn autotest_background_content_extremely_long_input_terminates() {
2429            let huge = "a".repeat(100_000);
2430            assert!(parse_style_background_content(&huge).is_err());
2431
2432            let huge_gradient =
2433                alloc::format!("linear-gradient({})", "red, ".repeat(2_000) + "blue");
2434            let g = linear(&huge_gradient);
2435            assert_eq!(g.stops.len(), 2_001);
2436
2437            let huge_url = alloc::format!("url({})", "a".repeat(100_000));
2438            assert!(matches!(
2439                parse_style_background_content(&huge_url),
2440                Ok(StyleBackgroundContent::Image(_))
2441            ));
2442        }
2443
2444        #[test]
2445        fn autotest_background_content_deep_nesting_does_not_stack_overflow() {
2446            let nested = alloc::format!(
2447                "linear-gradient({}red{})",
2448                "(".repeat(10_000),
2449                ")".repeat(10_000)
2450            );
2451            assert!(parse_style_background_content(&nested).is_err());
2452
2453            let unbalanced = alloc::format!("linear-gradient({}", "(".repeat(10_000));
2454            assert!(parse_style_background_content(&unbalanced).is_err());
2455        }
2456
2457        #[test]
2458        fn autotest_unclosed_gradient_reports_a_color_error_not_unclosed_gradient() {
2459            // parse_parentheses fails (no ')'), so the input falls through to the
2460            // color branch -- the `UnclosedGradient` variant is never produced here.
2461            let err = parse_style_background_content("linear-gradient(red, blue").unwrap_err();
2462            assert!(
2463                matches!(err, CssBackgroundParseError::ColorParseError(_)),
2464                "got {err:?}"
2465            );
2466        }
2467
2468        #[test]
2469        fn autotest_empty_gradient_body_is_a_no_direction_error() {
2470            let err = parse_style_background_content("linear-gradient()").unwrap_err();
2471            assert!(matches!(err, CssBackgroundParseError::NoDirection(_)), "got {err:?}");
2472            for f in [
2473                "radial-gradient()",
2474                "conic-gradient()",
2475                "repeating-linear-gradient()",
2476            ] {
2477                assert!(parse_style_background_content(f).is_err(), "{f:?}");
2478            }
2479        }
2480
2481        #[test]
2482        fn autotest_url_with_empty_payload_is_accepted_as_an_empty_image() {
2483            // Pinned: `url()` yields an empty image id rather than an error.
2484            assert_eq!(
2485                parse_style_background_content("url()").unwrap(),
2486                StyleBackgroundContent::Image("".into())
2487            );
2488        }
2489
2490        #[test]
2491        fn autotest_gradient_boundary_number_directions_stay_finite() {
2492            // "NaN" parses as a bare number -> a NaN angle, which the isize cast
2493            // flushes to 0deg. Pinned: it is silently accepted, not rejected.
2494            let g = linear("linear-gradient(NaN, red, blue)");
2495            assert_eq!(g.direction, Direction::Angle(AngleValue::deg(0.0)));
2496
2497            // Overflowing / tiny literals must not panic and must stay finite.
2498            for input in [
2499                "linear-gradient(0deg, red, blue)",
2500                "linear-gradient(-0deg, red, blue)",
2501                "linear-gradient(1e40deg, red, blue)",
2502                "linear-gradient(-1e40deg, red, blue)",
2503                "linear-gradient(1e-45deg, red, blue)",
2504                "linear-gradient(inf, red, blue)",
2505                "linear-gradient(-inf, red, blue)",
2506                "linear-gradient(9223372036854775807deg, red, blue)",
2507            ] {
2508                let g = linear(input);
2509                let Direction::Angle(a) = g.direction else {
2510                    panic!("expected an angle direction for {input:?}");
2511                };
2512                assert!(a.to_degrees_raw().is_finite(), "non-finite angle for {input:?}");
2513                assert_eq!(g.stops.len(), 2, "{input:?}");
2514            }
2515        }
2516
2517        #[test]
2518        fn autotest_gradient_stop_offsets_are_monotonic_and_finite() {
2519            for input in [
2520                "linear-gradient(red, blue)",
2521                "linear-gradient(red, green, blue)",
2522                "linear-gradient(red 50%, blue 20%)",
2523                "linear-gradient(red -50%, blue)",
2524                "linear-gradient(red 0%, yellow, green, blue 100%)",
2525                "linear-gradient(red 10% 30%, blue)",
2526                "linear-gradient(red 200%, blue 10%)",
2527                "repeating-linear-gradient(red, blue 20%)",
2528                "radial-gradient(circle, red, blue)",
2529            ] {
2530                let content = parse_style_background_content(input).unwrap();
2531                let stops = match &content {
2532                    StyleBackgroundContent::LinearGradient(g) => &g.stops,
2533                    StyleBackgroundContent::RadialGradient(g) => &g.stops,
2534                    other => panic!("unexpected content {other:?}"),
2535                };
2536                let mut prev = f32::NEG_INFINITY;
2537                for o in offsets(stops) {
2538                    assert!(o.is_finite(), "non-finite offset in {input:?}");
2539                    assert!(o >= prev, "offsets not monotonic in {input:?}: {o} < {prev}");
2540                    prev = o;
2541                }
2542            }
2543        }
2544
2545        #[test]
2546        fn autotest_negative_and_overflowing_stop_offsets_are_clamped() {
2547            // A negative first offset is clamped to the running maximum (0%).
2548            let g = linear("linear-gradient(red -50%, blue)");
2549            assert_eq!(offsets(&g.stops), alloc::vec![0.0, 100.0]);
2550
2551            // An out-of-range offset is *not* clamped down to 100% -- the later
2552            // stop is dragged up to it instead.
2553            let g = linear("linear-gradient(red 200%, blue 10%)");
2554            assert_eq!(offsets(&g.stops), alloc::vec![200.0, 200.0]);
2555        }
2556
2557        #[test]
2558        fn autotest_offsets_that_are_not_percentages_are_rejected() {
2559            // "50px" looks like an offset (is_likely_offset), but a linear stop
2560            // offset must be a percentage -> hard error, no silent fallback.
2561            let err = parse_style_background_content("linear-gradient(red 50px, blue)").unwrap_err();
2562            assert!(
2563                matches!(
2564                    err,
2565                    CssBackgroundParseError::GradientParseError(
2566                        CssGradientStopParseError::Percentage(_)
2567                    )
2568                ),
2569                "got {err:?}"
2570            );
2571
2572            // A bare number is *not* recognised as an offset at all, so the whole
2573            // token is treated as part of the color and fails to parse.
2574            assert!(parse_style_background_content("linear-gradient(red 0.5, blue)").is_err());
2575            // Neither is "NaN%" (no ASCII digit).
2576            assert!(parse_style_background_content("linear-gradient(red NaN%, blue)").is_err());
2577        }
2578
2579        #[test]
2580        fn autotest_huge_stop_offsets_do_not_produce_nan_or_inf() {
2581            let g = linear("linear-gradient(red 1e40%, blue)");
2582            assert_eq!(g.stops.len(), 2);
2583            for o in offsets(&g.stops) {
2584                assert!(o.is_finite(), "offset leaked a non-finite value: {o}");
2585            }
2586        }
2587
2588        // ---------------------------------------------------------------
2589        // parser: parse_style_background_content_multiple
2590        // ---------------------------------------------------------------
2591
2592        #[test]
2593        fn autotest_background_content_multiple_empty_input_yields_an_empty_vec() {
2594            // Pinned: empty input is *not* an error -- split_string_respect_comma
2595            // returns no items, so the result is an empty layer list.
2596            let parsed = parse_style_background_content_multiple("").unwrap();
2597            assert_eq!(parsed.len(), 0);
2598
2599            // Whitespace-only *is* an error (one empty item that fails to parse).
2600            assert!(parse_style_background_content_multiple("   ").is_err());
2601            assert!(parse_style_background_content_multiple(",").is_err());
2602            assert!(parse_style_background_content_multiple("red,,blue").is_err());
2603        }
2604
2605        #[test]
2606        fn autotest_background_content_multiple_valid_and_adversarial() {
2607            let parsed =
2608                parse_style_background_content_multiple("linear-gradient(red, blue), url(a.png)")
2609                    .unwrap();
2610            assert_eq!(parsed.len(), 2);
2611            assert!(matches!(
2612                parsed.as_slice()[0],
2613                StyleBackgroundContent::LinearGradient(_)
2614            ));
2615            assert!(matches!(
2616                parsed.as_slice()[1],
2617                StyleBackgroundContent::Image(_)
2618            ));
2619
2620            // One bad layer poisons the whole list.
2621            assert!(parse_style_background_content_multiple("red, !!!").is_err());
2622
2623            // Long repeated input terminates.
2624            let many = "red,".repeat(2_000) + "blue";
2625            assert_eq!(
2626                parse_style_background_content_multiple(&many).unwrap().len(),
2627                2_001
2628            );
2629
2630            for input in ADVERSARIAL {
2631                let a = parse_style_background_content_multiple(input);
2632                let b = parse_style_background_content_multiple(input);
2633                assert_eq!(a, b, "non-deterministic for {input:?}");
2634            }
2635        }
2636
2637        // ---------------------------------------------------------------
2638        // parser: parse_style_background_position(_multiple)
2639        // ---------------------------------------------------------------
2640
2641        #[test]
2642        fn autotest_background_position_empty_and_whitespace() {
2643            assert_eq!(
2644                parse_style_background_position(""),
2645                Err(CssBackgroundPositionParseError::NoPosition(""))
2646            );
2647            assert_eq!(
2648                parse_style_background_position("   "),
2649                Err(CssBackgroundPositionParseError::NoPosition(""))
2650            );
2651            assert_eq!(
2652                parse_style_background_position("\t\n\r"),
2653                Err(CssBackgroundPositionParseError::NoPosition(""))
2654            );
2655        }
2656
2657        #[test]
2658        fn autotest_background_position_valid_minimal_and_keyword_order() {
2659            let p = parse_style_background_position("left").unwrap();
2660            assert_eq!(p.horizontal, BackgroundPositionHorizontal::Left);
2661            assert_eq!(p.vertical, BackgroundPositionVertical::Center);
2662
2663            // A lone vertical keyword also works: the horizontal falls back to center.
2664            let p = parse_style_background_position("top").unwrap();
2665            assert_eq!(p.horizontal, BackgroundPositionHorizontal::Center);
2666            assert_eq!(p.vertical, BackgroundPositionVertical::Top);
2667
2668            // Either order is accepted for keyword pairs.
2669            assert_eq!(
2670                parse_style_background_position("left top").unwrap(),
2671                parse_style_background_position("top left").unwrap()
2672            );
2673
2674            // ... but "left right" is a vertical-slot error, not silently accepted.
2675            assert!(matches!(
2676                parse_style_background_position("left right"),
2677                Err(CssBackgroundPositionParseError::SecondComponentWrong(_))
2678            ));
2679        }
2680
2681        #[test]
2682        fn autotest_background_position_too_many_components() {
2683            assert!(matches!(
2684                parse_style_background_position("left 10px top 20px"),
2685                Err(CssBackgroundPositionParseError::TooManyComponents(_))
2686            ));
2687            assert!(matches!(
2688                parse_style_background_position("a b c"),
2689                Err(CssBackgroundPositionParseError::TooManyComponents(_))
2690            ));
2691        }
2692
2693        #[test]
2694        fn autotest_background_position_boundary_numbers_are_accepted_and_saturate() {
2695            // Pinned: parse_pixel_value accepts a bare number as px -- so "NaN" and
2696            // "inf" are *valid* background positions, flushed to 0 / saturated.
2697            assert_eq!(
2698                parse_style_background_position("NaN").unwrap().horizontal,
2699                BackgroundPositionHorizontal::Exact(PixelValue::px(0.0))
2700            );
2701            assert_eq!(
2702                parse_style_background_position("-0").unwrap().horizontal,
2703                BackgroundPositionHorizontal::Exact(PixelValue::px(0.0))
2704            );
2705            assert_eq!(
2706                parse_style_background_position("inf").unwrap().horizontal,
2707                BackgroundPositionHorizontal::Exact(PixelValue::px(f32::INFINITY))
2708            );
2709            for input in ["0", "1e40px", "-1e40px", "1e-45px", "3.4028235e38px"] {
2710                let p = parse_style_background_position(input).unwrap();
2711                let BackgroundPositionHorizontal::Exact(px) = p.horizontal else {
2712                    panic!("expected an exact value for {input:?}");
2713                };
2714                assert!(px.number.get().is_finite(), "non-finite for {input:?}");
2715            }
2716        }
2717
2718        #[test]
2719        fn autotest_background_position_garbage_unicode_and_long_input() {
2720            for input in ["garbage", "!!!", "\u{1F600}", "e\u{0301}", "left;top"] {
2721                assert!(
2722                    parse_style_background_position(input).is_err(),
2723                    "{input:?} unexpectedly parsed"
2724                );
2725            }
2726            let huge = "a".repeat(100_000);
2727            assert!(parse_style_background_position(&huge).is_err());
2728            let nested = "(".repeat(10_000);
2729            assert!(parse_style_background_position(&nested).is_err());
2730
2731            for input in ADVERSARIAL {
2732                let a = parse_style_background_position(input);
2733                let b = parse_style_background_position(input);
2734                assert_eq!(a, b, "non-deterministic for {input:?}");
2735            }
2736        }
2737
2738        #[test]
2739        fn autotest_background_position_multiple() {
2740            // Empty input -> empty vec (no error), same as the other *_multiple fns.
2741            assert_eq!(parse_style_background_position_multiple("").unwrap().len(), 0);
2742
2743            let parsed = parse_style_background_position_multiple("left top, 10px 20px").unwrap();
2744            assert_eq!(parsed.len(), 2);
2745            assert_eq!(
2746                parsed.as_slice()[1].horizontal,
2747                BackgroundPositionHorizontal::Exact(PixelValue::px(10.0))
2748            );
2749
2750            assert!(parse_style_background_position_multiple("left top, !!!").is_err());
2751            assert!(parse_style_background_position_multiple("   ").is_err());
2752
2753            let many = "left top,".repeat(2_000) + "center";
2754            assert_eq!(
2755                parse_style_background_position_multiple(&many).unwrap().len(),
2756                2_001
2757            );
2758        }
2759
2760        // ---------------------------------------------------------------
2761        // parser: parse_style_background_size(_multiple)
2762        // ---------------------------------------------------------------
2763
2764        #[test]
2765        fn autotest_background_size_empty_whitespace_and_garbage() {
2766            for input in ["", "   ", "\t\n", "auto", "!!!", "\u{1F600}", "CONTAIN", "Cover"] {
2767                assert!(
2768                    parse_style_background_size(input).is_err(),
2769                    "{input:?} unexpectedly parsed"
2770                );
2771            }
2772            let huge = "a".repeat(100_000);
2773            assert!(parse_style_background_size(&huge).is_err());
2774        }
2775
2776        #[test]
2777        fn autotest_background_size_valid_minimal_and_trimming() {
2778            assert_eq!(
2779                parse_style_background_size("  contain  ").unwrap(),
2780                StyleBackgroundSize::Contain
2781            );
2782            assert_eq!(
2783                parse_style_background_size("cover").unwrap(),
2784                StyleBackgroundSize::Cover
2785            );
2786            // A single value applies to both axes.
2787            assert_eq!(
2788                parse_style_background_size("50%").unwrap(),
2789                StyleBackgroundSize::ExactSize(PixelValueSize {
2790                    width: PixelValue::percent(50.0),
2791                    height: PixelValue::percent(50.0),
2792                })
2793            );
2794        }
2795
2796        #[test]
2797        fn autotest_background_size_silently_ignores_extra_components() {
2798            // BUG-ish, pinned: unlike background-position (TooManyComponents), a third
2799            // component is dropped on the floor instead of being rejected.
2800            assert_eq!(
2801                parse_style_background_size("10px 20px 30px").unwrap(),
2802                StyleBackgroundSize::ExactSize(PixelValueSize {
2803                    width: PixelValue::px(10.0),
2804                    height: PixelValue::px(20.0),
2805                })
2806            );
2807        }
2808
2809        #[test]
2810        fn autotest_background_size_boundary_numbers_saturate_without_panicking() {
2811            // Bare numbers parse as px, so "NaN" is accepted and flushed to 0px.
2812            assert_eq!(
2813                parse_style_background_size("NaN").unwrap(),
2814                StyleBackgroundSize::ExactSize(PixelValueSize {
2815                    width: PixelValue::px(0.0),
2816                    height: PixelValue::px(0.0),
2817                })
2818            );
2819            assert_eq!(
2820                parse_style_background_size("inf").unwrap(),
2821                StyleBackgroundSize::ExactSize(PixelValueSize {
2822                    width: PixelValue::px(f32::INFINITY),
2823                    height: PixelValue::px(f32::INFINITY),
2824                })
2825            );
2826            for input in ["0", "-0", "1e40px", "-1e40px", "1e-45px"] {
2827                let StyleBackgroundSize::ExactSize(s) =
2828                    parse_style_background_size(input).unwrap()
2829                else {
2830                    panic!("expected an exact size for {input:?}");
2831                };
2832                assert!(s.width.number.get().is_finite(), "non-finite for {input:?}");
2833                assert!(s.height.number.get().is_finite(), "non-finite for {input:?}");
2834            }
2835
2836            for input in ADVERSARIAL {
2837                let a = parse_style_background_size(input);
2838                let b = parse_style_background_size(input);
2839                assert_eq!(a, b, "non-deterministic for {input:?}");
2840            }
2841        }
2842
2843        #[test]
2844        fn autotest_background_size_multiple() {
2845            assert_eq!(parse_style_background_size_multiple("").unwrap().len(), 0);
2846
2847            let parsed = parse_style_background_size_multiple("contain, 10px 20px, cover").unwrap();
2848            assert_eq!(parsed.len(), 3);
2849            assert_eq!(parsed.as_slice()[0], StyleBackgroundSize::Contain);
2850            assert_eq!(parsed.as_slice()[2], StyleBackgroundSize::Cover);
2851
2852            assert!(parse_style_background_size_multiple("cover, auto").is_err());
2853            assert!(parse_style_background_size_multiple("   ").is_err());
2854
2855            let many = "cover,".repeat(2_000) + "contain";
2856            assert_eq!(
2857                parse_style_background_size_multiple(&many).unwrap().len(),
2858                2_001
2859            );
2860        }
2861
2862        // ---------------------------------------------------------------
2863        // parser: parse_style_background_repeat(_multiple)
2864        // ---------------------------------------------------------------
2865
2866        #[test]
2867        fn autotest_background_repeat_valid_and_invalid() {
2868            assert_eq!(
2869                parse_style_background_repeat("  repeat  ").unwrap(),
2870                StyleBackgroundRepeat::PatternRepeat
2871            );
2872            assert_eq!(
2873                parse_style_background_repeat("no-repeat").unwrap(),
2874                StyleBackgroundRepeat::NoRepeat
2875            );
2876            assert_eq!(
2877                parse_style_background_repeat("repeat-x").unwrap(),
2878                StyleBackgroundRepeat::RepeatX
2879            );
2880            assert_eq!(
2881                parse_style_background_repeat("repeat-y").unwrap(),
2882                StyleBackgroundRepeat::RepeatY
2883            );
2884            assert_eq!(StyleBackgroundRepeat::default(), StyleBackgroundRepeat::PatternRepeat);
2885
2886            for input in [
2887                "",
2888                "   ",
2889                "\t\n",
2890                "REPEAT",
2891                "Repeat",
2892                "repeat-xy",
2893                "repeat repeat",
2894                "!!!",
2895                "\u{1F600}",
2896                "0",
2897                "NaN",
2898            ] {
2899                assert!(
2900                    parse_style_background_repeat(input).is_err(),
2901                    "{input:?} unexpectedly parsed"
2902                );
2903            }
2904
2905            let huge = "repeat".repeat(20_000);
2906            assert!(parse_style_background_repeat(&huge).is_err());
2907
2908            for input in ADVERSARIAL {
2909                let a = parse_style_background_repeat(input);
2910                let b = parse_style_background_repeat(input);
2911                assert_eq!(a, b, "non-deterministic for {input:?}");
2912            }
2913        }
2914
2915        #[test]
2916        fn autotest_background_repeat_multiple() {
2917            assert_eq!(parse_style_background_repeat_multiple("").unwrap().len(), 0);
2918
2919            let parsed = parse_style_background_repeat_multiple("repeat, no-repeat").unwrap();
2920            assert_eq!(parsed.len(), 2);
2921            assert_eq!(parsed.as_slice()[0], StyleBackgroundRepeat::PatternRepeat);
2922            assert_eq!(parsed.as_slice()[1], StyleBackgroundRepeat::NoRepeat);
2923
2924            assert!(parse_style_background_repeat_multiple("repeat,,repeat").is_err());
2925            assert!(parse_style_background_repeat_multiple("   ").is_err());
2926
2927            let many = "repeat,".repeat(2_000) + "no-repeat";
2928            assert_eq!(
2929                parse_style_background_repeat_multiple(&many).unwrap().len(),
2930                2_001
2931            );
2932        }
2933
2934        // ---------------------------------------------------------------
2935        // parser: parse_gradient (private)
2936        // ---------------------------------------------------------------
2937
2938        #[test]
2939        fn autotest_parse_gradient_empty_input_is_no_direction_for_every_type() {
2940            for t in ALL_GRADIENT_TYPES {
2941                assert!(
2942                    matches!(
2943                        parse_gradient("", t),
2944                        Err(CssBackgroundParseError::NoDirection(_))
2945                    ),
2946                    "empty body accepted for {t:?}"
2947                );
2948            }
2949        }
2950
2951        #[test]
2952        fn autotest_parse_gradient_extend_mode_follows_the_gradient_type() {
2953            let StyleBackgroundContent::LinearGradient(g) =
2954                parse_gradient("red, blue", GradientType::LinearGradient).unwrap()
2955            else {
2956                panic!("expected a linear gradient");
2957            };
2958            assert_eq!(g.extend_mode, ExtendMode::Clamp);
2959
2960            let StyleBackgroundContent::LinearGradient(g) =
2961                parse_gradient("red, blue", GradientType::RepeatingLinearGradient).unwrap()
2962            else {
2963                panic!("expected a linear gradient");
2964            };
2965            assert_eq!(g.extend_mode, ExtendMode::Repeat);
2966
2967            let StyleBackgroundContent::RadialGradient(g) =
2968                parse_gradient("red, blue", GradientType::RepeatingRadialGradient).unwrap()
2969            else {
2970                panic!("expected a radial gradient");
2971            };
2972            assert_eq!(g.extend_mode, ExtendMode::Repeat);
2973
2974            let StyleBackgroundContent::ConicGradient(g) =
2975                parse_gradient("red, blue", GradientType::RepeatingConicGradient).unwrap()
2976            else {
2977                panic!("expected a conic gradient");
2978            };
2979            assert_eq!(g.extend_mode, ExtendMode::Repeat);
2980        }
2981
2982        #[test]
2983        fn autotest_parse_gradient_accepts_gradients_with_too_few_stops() {
2984            // W3C requires >= 2 color stops. Pinned: this parser happily returns
2985            // gradients with one or zero stops -- `TooFewGradientStops` is dead code.
2986            let StyleBackgroundContent::LinearGradient(g) =
2987                parse_gradient("red", GradientType::LinearGradient).unwrap()
2988            else {
2989                panic!("expected a linear gradient");
2990            };
2991            assert_eq!(g.stops.len(), 1);
2992            assert_eq!(offsets(&g.stops), alloc::vec![0.0]);
2993
2994            // A direction with no stops at all -> zero stops, still Ok.
2995            let StyleBackgroundContent::LinearGradient(g) =
2996                parse_gradient("to right", GradientType::LinearGradient).unwrap()
2997            else {
2998                panic!("expected a linear gradient");
2999            };
3000            assert_eq!(g.stops.len(), 0);
3001
3002            // Same for a radial gradient that only names a shape.
3003            let StyleBackgroundContent::RadialGradient(g) =
3004                parse_gradient("circle", GradientType::RadialGradient).unwrap()
3005            else {
3006                panic!("expected a radial gradient");
3007            };
3008            assert_eq!(g.shape, Shape::Circle);
3009            assert_eq!(g.stops.len(), 0);
3010        }
3011
3012        #[test]
3013        fn autotest_parse_gradient_never_panics_on_adversarial_input() {
3014            let huge = "a".repeat(100_000);
3015            let nested = "(".repeat(10_000) + &")".repeat(10_000);
3016            let many_commas = ",".repeat(10_000);
3017            for t in ALL_GRADIENT_TYPES {
3018                for input in ADVERSARIAL {
3019                    let a = parse_gradient(input, t);
3020                    let b = parse_gradient(input, t);
3021                    assert_eq!(a, b, "non-deterministic for {input:?} / {t:?}");
3022                }
3023                for input in [huge.as_str(), nested.as_str(), many_commas.as_str()] {
3024                    let a = parse_gradient(input, t);
3025                    let b = parse_gradient(input, t);
3026                    assert_eq!(a, b, "non-deterministic for a long input / {t:?}");
3027                }
3028                // An empty body is rejected for every gradient type.
3029                assert!(parse_gradient("", t).is_err(), "empty body accepted for {t:?}");
3030                // A comma-only body has nothing but empty stops -> always an error.
3031                assert!(
3032                    parse_gradient(&many_commas, t).is_err(),
3033                    "comma soup accepted for {t:?}"
3034                );
3035            }
3036            // Linear and conic reject junk outright. (Radial does not -- see
3037            // `autotest_radial_gradient_silently_drops_unparseable_items`.)
3038            for t in [
3039                GradientType::LinearGradient,
3040                GradientType::RepeatingLinearGradient,
3041                GradientType::ConicGradient,
3042                GradientType::RepeatingConicGradient,
3043            ] {
3044                assert!(parse_gradient(&huge, t).is_err(), "junk accepted for {t:?}");
3045                assert!(parse_gradient(&nested, t).is_err(), "junk accepted for {t:?}");
3046                assert!(parse_gradient("!!!", t).is_err(), "junk accepted for {t:?}");
3047            }
3048        }
3049
3050        #[test]
3051        fn autotest_radial_gradient_silently_drops_unparseable_items() {
3052            // BUG (pinned): in the radial branch, a comma-item that is neither a
3053            // shape/size/position *nor* a valid color stop is skipped rather than
3054            // rejected -- so pure garbage parses as a gradient with no stops, and a
3055            // junk leading item simply disappears.
3056            let StyleBackgroundContent::RadialGradient(g) =
3057                parse_gradient("!!!", GradientType::RadialGradient).unwrap()
3058            else {
3059                panic!("expected a radial gradient");
3060            };
3061            assert_eq!(g.stops.len(), 0);
3062
3063            let g = radial("radial-gradient(!!!, red)");
3064            assert_eq!(g.stops.len(), 1, "the junk item should have been dropped");
3065            assert_eq!(
3066                g.stops.as_ref()[0].color,
3067                ColorOrSystem::Color(ColorU::RED)
3068            );
3069
3070            // The same input is a hard error for a linear gradient.
3071            assert!(parse_style_background_content("linear-gradient(!!!, red)").is_err());
3072        }
3073
3074        #[test]
3075        fn autotest_radial_gradient_position_is_ignored_when_combined_with_a_shape() {
3076            // BUG (pinned): `parse_style_background_position` is handed the *whole*
3077            // comma-item ("circle at 50% 50%"), which has 4 whitespace components and
3078            // therefore fails -- so the `at <position>` part is silently dropped and
3079            // the position stays at its Left/Top default.
3080            let g = radial("radial-gradient(circle at 50% 50%, red, blue)");
3081            assert_eq!(g.shape, Shape::Circle);
3082            assert_eq!(g.position, StyleBackgroundPosition::default());
3083            assert_eq!(g.stops.len(), 2);
3084
3085            // A position on its own (no shape/size in the same item) *is* honoured.
3086            let g = radial("radial-gradient(50% 50%, red, blue)");
3087            assert_eq!(
3088                g.position,
3089                StyleBackgroundPosition {
3090                    horizontal: BackgroundPositionHorizontal::Exact(PixelValue::percent(50.0)),
3091                    vertical: BackgroundPositionVertical::Exact(PixelValue::percent(50.0)),
3092                }
3093            );
3094            assert_eq!(g.stops.len(), 2);
3095        }
3096
3097        #[test]
3098        fn autotest_radial_gradient_shape_and_size_keywords() {
3099            let g = radial("radial-gradient(circle closest-side, red, blue)");
3100            assert_eq!(g.shape, Shape::Circle);
3101            assert_eq!(g.size, RadialGradientSize::ClosestSide);
3102
3103            let g = radial("radial-gradient(ellipse farthest-side, red, blue)");
3104            assert_eq!(g.shape, Shape::Ellipse);
3105            assert_eq!(g.size, RadialGradientSize::FarthestSide);
3106
3107            // Defaults when nothing is named.
3108            let g = radial("radial-gradient(red, blue)");
3109            assert_eq!(g.shape, Shape::default());
3110            assert_eq!(g.size, RadialGradientSize::default());
3111        }
3112
3113        // ---------------------------------------------------------------
3114        // parser: parse_linear_color_stop / parse_radial_color_stop (private)
3115        // ---------------------------------------------------------------
3116
3117        #[test]
3118        fn autotest_parse_linear_color_stop_valid_minimal() {
3119            let s = parse_linear_color_stop("red").unwrap();
3120            assert_eq!(s.color, ColorOrSystem::Color(ColorU::RED));
3121            assert_eq!(s.offset1, OptionPercentageValue::None);
3122            assert_eq!(s.offset2, OptionPercentageValue::None);
3123
3124            let s = parse_linear_color_stop("  red 50%  ").unwrap();
3125            assert_eq!(
3126                s.offset1,
3127                OptionPercentageValue::Some(PercentageValue::new(50.0))
3128            );
3129            assert_eq!(s.offset2, OptionPercentageValue::None);
3130
3131            let s = parse_linear_color_stop("red 10% 30%").unwrap();
3132            assert_eq!(
3133                s.offset1,
3134                OptionPercentageValue::Some(PercentageValue::new(10.0))
3135            );
3136            assert_eq!(
3137                s.offset2,
3138                OptionPercentageValue::Some(PercentageValue::new(30.0))
3139            );
3140
3141            // Colors that themselves contain spaces and digits still split correctly.
3142            let s = parse_linear_color_stop("rgba(0, 0, 0, 0.5) 50%").unwrap();
3143            assert_eq!(
3144                s.offset1,
3145                OptionPercentageValue::Some(PercentageValue::new(50.0))
3146            );
3147
3148            // System colors are accepted as stop colors.
3149            let s = parse_linear_color_stop("system:accent 50%").unwrap();
3150            assert_eq!(s.color, ColorOrSystem::System(SystemColorRef::Accent));
3151        }
3152
3153        #[test]
3154        fn autotest_parse_linear_color_stop_rejects_junk() {
3155            for input in [
3156                "",
3157                "   ",
3158                "\t\n",
3159                "!!!",
3160                "\u{1F600}",
3161                "red 50px",       // offset must be a percentage
3162                "red 0.5",        // bare number is not recognised as an offset
3163                "red 10% 20% 30%", // three offsets -> the color part is junk
3164                "red blue",
3165            ] {
3166                assert!(
3167                    parse_linear_color_stop(input).is_err(),
3168                    "{input:?} unexpectedly parsed"
3169                );
3170            }
3171            let huge = "a".repeat(100_000);
3172            assert!(parse_linear_color_stop(&huge).is_err());
3173        }
3174
3175        #[test]
3176        fn autotest_parse_radial_color_stop_valid_and_junk() {
3177            let s = parse_radial_color_stop("red").unwrap();
3178            assert_eq!(s.color, ColorOrSystem::Color(ColorU::RED));
3179            assert_eq!(s.offset1, OptionAngleValue::None);
3180
3181            let s = parse_radial_color_stop("red 90deg").unwrap();
3182            assert_eq!(s.offset1, OptionAngleValue::Some(AngleValue::deg(90.0)));
3183            assert_eq!(s.offset2, OptionAngleValue::None);
3184
3185            let s = parse_radial_color_stop("red 45deg 90deg").unwrap();
3186            assert_eq!(s.offset1, OptionAngleValue::Some(AngleValue::deg(45.0)));
3187            assert_eq!(s.offset2, OptionAngleValue::Some(AngleValue::deg(90.0)));
3188
3189            // Pinned: a *percentage* is a valid angle for a conic stop.
3190            assert!(parse_radial_color_stop("red 50%").is_ok());
3191
3192            for input in ["", "   ", "!!!", "\u{1F600}", "red 5", "red 90deg 45deg 10deg"] {
3193                assert!(
3194                    parse_radial_color_stop(input).is_err(),
3195                    "{input:?} unexpectedly parsed"
3196                );
3197            }
3198            let huge = "a".repeat(100_000);
3199            assert!(parse_radial_color_stop(&huge).is_err());
3200        }
3201
3202        // ---------------------------------------------------------------
3203        // other: split_color_and_offsets / try_split_last_offset (private)
3204        // ---------------------------------------------------------------
3205
3206        #[test]
3207        fn autotest_split_color_and_offsets_w3c_shapes() {
3208            assert_eq!(split_color_and_offsets("red"), ("red", None, None));
3209            assert_eq!(
3210                split_color_and_offsets("red 50%"),
3211                ("red", Some("50%"), None)
3212            );
3213            assert_eq!(
3214                split_color_and_offsets("red 10% 30%"),
3215                ("red", Some("10%"), Some("30%"))
3216            );
3217            assert_eq!(
3218                split_color_and_offsets("rgba(0, 0, 0, 0.5) 10% 30%"),
3219                ("rgba(0, 0, 0, 0.5)", Some("10%"), Some("30%"))
3220            );
3221            // A direction is never mistaken for offsets (no digits).
3222            assert_eq!(
3223                split_color_and_offsets("to right bottom"),
3224                ("to right bottom", None, None)
3225            );
3226        }
3227
3228        #[test]
3229        fn autotest_split_color_and_offsets_never_panics_on_edges() {
3230            assert_eq!(split_color_and_offsets(""), ("", None, None));
3231            assert_eq!(split_color_and_offsets("   "), ("", None, None));
3232            // Multibyte input: splitting must land on a char boundary.
3233            assert_eq!(
3234                split_color_and_offsets("\u{1F600} 50%"),
3235                ("\u{1F600}", Some("50%"), None)
3236            );
3237            // A non-breaking space counts as whitespace for rfind *and* for trim.
3238            assert_eq!(
3239                split_color_and_offsets("red\u{00a0}50%"),
3240                ("red", Some("50%"), None)
3241            );
3242            let huge = "a".repeat(100_000);
3243            assert_eq!(split_color_and_offsets(&huge), (huge.as_str(), None, None));
3244
3245            for input in ADVERSARIAL {
3246                assert_eq!(
3247                    split_color_and_offsets(input),
3248                    split_color_and_offsets(input)
3249                );
3250            }
3251        }
3252
3253        #[test]
3254        fn autotest_try_split_last_offset() {
3255            assert_eq!(try_split_last_offset("red 50%"), Some(("red", "50%")));
3256            assert_eq!(try_split_last_offset("red 10px"), Some(("red", "10px")));
3257            // No whitespace -> nothing to split off.
3258            assert_eq!(try_split_last_offset("50%"), None);
3259            // Not offset-shaped.
3260            assert_eq!(try_split_last_offset("red blue"), None);
3261            assert_eq!(try_split_last_offset("red 5"), None);
3262            assert_eq!(try_split_last_offset("to right"), None);
3263            // Empty / whitespace-only.
3264            assert_eq!(try_split_last_offset(""), None);
3265            assert_eq!(try_split_last_offset("   "), None);
3266            assert_eq!(try_split_last_offset("\t\n"), None);
3267
3268            for input in ADVERSARIAL {
3269                assert_eq!(try_split_last_offset(input), try_split_last_offset(input));
3270            }
3271        }
3272
3273        // ---------------------------------------------------------------
3274        // predicate: is_likely_offset (private)
3275        // ---------------------------------------------------------------
3276
3277        #[test]
3278        fn autotest_is_likely_offset_basic_true_false() {
3279            for s in [
3280                "50%", "10px", "0.5turn", "90deg", "1rem", "2vmin", "3vmax", "4grad", "5rad",
3281                "-50%", "1e40%",
3282            ] {
3283                assert!(is_likely_offset(s), "{s:?} should look like an offset");
3284            }
3285            for s in [
3286                "", " ", "red", "px", "%", "5", "0.5", "NaN%", "to", "right", "\u{1F600}",
3287                "contain",
3288            ] {
3289                assert!(!is_likely_offset(s), "{s:?} should not look like an offset");
3290            }
3291        }
3292
3293        #[test]
3294        fn autotest_is_likely_offset_is_a_shape_check_not_a_validator() {
3295            // Pinned: it only requires "contains an ASCII digit" + "ends with a unit",
3296            // so plainly invalid tokens pass. The real parse still rejects them.
3297            assert!(is_likely_offset("abc1px"));
3298            assert!(is_likely_offset("\u{1F600}5%"));
3299            assert!(is_likely_offset("--1--px"));
3300            assert!(is_likely_offset("1%%%"));
3301            // ... and the digit check must be ASCII: an Arabic-Indic digit does not
3302            // count, so this is *not* treated as an offset.
3303            assert!(!is_likely_offset("\u{0661}%"));
3304
3305            let huge = "9".repeat(100_000) + "%";
3306            assert!(is_likely_offset(&huge));
3307            for input in ADVERSARIAL {
3308                assert_eq!(is_likely_offset(input), is_likely_offset(input));
3309            }
3310        }
3311
3312        // ---------------------------------------------------------------
3313        // parser: parse_conic_first_item (private)
3314        // ---------------------------------------------------------------
3315
3316        #[test]
3317        fn autotest_parse_conic_first_item_valid_and_absent() {
3318            // Not a "from ..." prelude -> Ok(None), so the item is a color stop.
3319            assert_eq!(parse_conic_first_item("").unwrap(), None);
3320            assert_eq!(parse_conic_first_item("red").unwrap(), None);
3321            assert_eq!(parse_conic_first_item("   ").unwrap(), None);
3322
3323            let (angle, pos) = parse_conic_first_item("from 90deg").unwrap().unwrap();
3324            assert_eq!(angle, AngleValue::deg(90.0));
3325            assert_eq!(pos, StyleBackgroundPosition::default());
3326
3327            let (angle, pos) = parse_conic_first_item("from 0deg at center").unwrap().unwrap();
3328            assert_eq!(angle, AngleValue::deg(0.0));
3329            assert_eq!(pos.horizontal, BackgroundPositionHorizontal::Center);
3330            assert_eq!(pos.vertical, BackgroundPositionVertical::Center);
3331        }
3332
3333        #[test]
3334        fn autotest_parse_conic_first_item_rejects_malformed_preludes() {
3335            // "from" with no angle.
3336            assert!(parse_conic_first_item("from").is_err());
3337            assert!(parse_conic_first_item("from at center").is_err());
3338            // Pinned quirk: any token *starting with* "from" enters the prelude branch,
3339            // so a would-be color like "fromage" becomes an angle error.
3340            assert!(parse_conic_first_item("fromage").is_err());
3341            // Too many position components.
3342            assert!(matches!(
3343                parse_conic_first_item("from 90deg at left top center"),
3344                Err(CssConicGradientParseError::Position(_))
3345            ));
3346
3347            let huge = alloc::format!("from {}", "9".repeat(100_000));
3348            let _ = parse_conic_first_item(&huge);
3349            for input in ADVERSARIAL {
3350                let a = parse_conic_first_item(input);
3351                let b = parse_conic_first_item(input);
3352                assert_eq!(a, b, "non-deterministic for {input:?}");
3353            }
3354        }
3355
3356        #[test]
3357        fn autotest_conic_gradient_end_to_end() {
3358            let g = conic("conic-gradient(from 45deg, red, blue)");
3359            assert_eq!(g.angle, AngleValue::deg(45.0));
3360            assert_eq!(g.extend_mode, ExtendMode::Clamp);
3361            assert_eq!(g.stops.len(), 2);
3362            assert_eq!(g.stops.as_ref()[0].angle.to_degrees_raw(), 0.0);
3363            assert_eq!(g.stops.as_ref()[1].angle.to_degrees_raw(), 360.0);
3364
3365            // Conic stop angles are monotonic and finite, even for silly inputs.
3366            for input in [
3367                "conic-gradient(red, blue)",
3368                "conic-gradient(red 0deg, blue 180deg, green 360deg)",
3369                "conic-gradient(red 180deg, blue 90deg)",
3370                "conic-gradient(red -90deg, blue)",
3371                "repeating-conic-gradient(red, blue 30deg)",
3372            ] {
3373                let g = conic(input);
3374                let mut prev = f32::NEG_INFINITY;
3375                for s in &g.stops {
3376                    let deg = s.angle.to_degrees_raw();
3377                    assert!(deg.is_finite(), "non-finite angle in {input:?}");
3378                    assert!(deg >= prev, "angles not monotonic in {input:?}");
3379                    prev = deg;
3380                }
3381            }
3382
3383            // An overflowing angle saturates instead of leaking inf into the stops.
3384            let g = conic("conic-gradient(red 1e40deg, blue)");
3385            assert_eq!(g.stops.len(), 2);
3386            for s in &g.stops {
3387                assert!(s.angle.to_degrees_raw().is_finite());
3388            }
3389
3390            assert!(parse_style_background_content("conic-gradient(from, red)").is_err());
3391        }
3392
3393        // ---------------------------------------------------------------
3394        // parser: parse_background_position_{horizontal,vertical} (private)
3395        // ---------------------------------------------------------------
3396
3397        #[test]
3398        fn autotest_parse_background_position_horizontal() {
3399            assert_eq!(
3400                parse_background_position_horizontal("left").unwrap(),
3401                BackgroundPositionHorizontal::Left
3402            );
3403            assert_eq!(
3404                parse_background_position_horizontal("center").unwrap(),
3405                BackgroundPositionHorizontal::Center
3406            );
3407            assert_eq!(
3408                parse_background_position_horizontal("right").unwrap(),
3409                BackgroundPositionHorizontal::Right
3410            );
3411            assert_eq!(
3412                parse_background_position_horizontal("10px").unwrap(),
3413                BackgroundPositionHorizontal::Exact(PixelValue::px(10.0))
3414            );
3415            // Vertical keywords are not horizontal ones.
3416            assert!(parse_background_position_horizontal("top").is_err());
3417            // Pinned: no trimming here (callers pass whitespace-split tokens).
3418            assert!(parse_background_position_horizontal(" left").is_err());
3419            assert!(parse_background_position_horizontal("").is_err());
3420            assert!(parse_background_position_horizontal("\u{1F600}").is_err());
3421
3422            let huge = "a".repeat(100_000);
3423            assert!(parse_background_position_horizontal(&huge).is_err());
3424        }
3425
3426        #[test]
3427        fn autotest_parse_background_position_vertical() {
3428            assert_eq!(
3429                parse_background_position_vertical("top").unwrap(),
3430                BackgroundPositionVertical::Top
3431            );
3432            assert_eq!(
3433                parse_background_position_vertical("center").unwrap(),
3434                BackgroundPositionVertical::Center
3435            );
3436            assert_eq!(
3437                parse_background_position_vertical("bottom").unwrap(),
3438                BackgroundPositionVertical::Bottom
3439            );
3440            assert_eq!(
3441                parse_background_position_vertical("-10px").unwrap(),
3442                BackgroundPositionVertical::Exact(PixelValue::px(-10.0))
3443            );
3444            assert!(parse_background_position_vertical("left").is_err());
3445            assert!(parse_background_position_vertical("").is_err());
3446            assert!(parse_background_position_vertical("\u{1F600}").is_err());
3447
3448            let huge = "a".repeat(100_000);
3449            assert!(parse_background_position_vertical(&huge).is_err());
3450        }
3451
3452        // ---------------------------------------------------------------
3453        // parser: parse_shape / parse_radial_gradient_size (private)
3454        // ---------------------------------------------------------------
3455
3456        #[test]
3457        fn autotest_parse_shape() {
3458            assert_eq!(parse_shape("circle").unwrap(), Shape::Circle);
3459            assert_eq!(parse_shape("  ellipse  ").unwrap(), Shape::Ellipse);
3460            for input in ["", "   ", "Circle", "CIRCLE", "circles", "!!!", "\u{1F600}", "0"] {
3461                assert!(parse_shape(input).is_err(), "{input:?} unexpectedly parsed");
3462            }
3463            let huge = "a".repeat(100_000);
3464            assert!(parse_shape(&huge).is_err());
3465            for input in ADVERSARIAL {
3466                assert_eq!(parse_shape(input), parse_shape(input));
3467            }
3468        }
3469
3470        #[test]
3471        fn autotest_parse_radial_gradient_size() {
3472            assert_eq!(
3473                parse_radial_gradient_size("closest-side").unwrap(),
3474                RadialGradientSize::ClosestSide
3475            );
3476            assert_eq!(
3477                parse_radial_gradient_size("  closest-corner ").unwrap(),
3478                RadialGradientSize::ClosestCorner
3479            );
3480            assert_eq!(
3481                parse_radial_gradient_size("farthest-side").unwrap(),
3482                RadialGradientSize::FarthestSide
3483            );
3484            assert_eq!(
3485                parse_radial_gradient_size("farthest-corner").unwrap(),
3486                RadialGradientSize::FarthestCorner
3487            );
3488            for input in [
3489                "",
3490                "   ",
3491                "closest",
3492                "CLOSEST-SIDE",
3493                "farthest-corners",
3494                "!!!",
3495                "\u{1F600}",
3496            ] {
3497                assert!(
3498                    parse_radial_gradient_size(input).is_err(),
3499                    "{input:?} unexpectedly parsed"
3500                );
3501            }
3502            let huge = "a".repeat(100_000);
3503            assert!(parse_radial_gradient_size(&huge).is_err());
3504        }
3505
3506        // ---------------------------------------------------------------
3507        // round-trips: print_as_css_value -> parse
3508        // ---------------------------------------------------------------
3509
3510        #[test]
3511        fn autotest_round_trip_background_repeat() {
3512            for r in [
3513                StyleBackgroundRepeat::NoRepeat,
3514                StyleBackgroundRepeat::PatternRepeat,
3515                StyleBackgroundRepeat::RepeatX,
3516                StyleBackgroundRepeat::RepeatY,
3517            ] {
3518                let printed = r.print_as_css_value();
3519                assert!(!printed.is_empty());
3520                assert_eq!(parse_style_background_repeat(&printed).unwrap(), r);
3521            }
3522        }
3523
3524        #[test]
3525        fn autotest_round_trip_background_size() {
3526            for s in [
3527                StyleBackgroundSize::Contain,
3528                StyleBackgroundSize::Cover,
3529                StyleBackgroundSize::ExactSize(PixelValueSize {
3530                    width: PixelValue::px(100.0),
3531                    height: PixelValue::em(20.0),
3532                }),
3533                StyleBackgroundSize::ExactSize(PixelValueSize {
3534                    width: PixelValue::percent(50.0),
3535                    height: PixelValue::percent(50.0),
3536                }),
3537                StyleBackgroundSize::ExactSize(PixelValueSize {
3538                    width: PixelValue::px(0.0),
3539                    height: PixelValue::px(-25.5),
3540                }),
3541            ] {
3542                let printed = s.print_as_css_value();
3543                assert_eq!(
3544                    parse_style_background_size(&printed).unwrap(),
3545                    s,
3546                    "round-trip failed for {printed:?}"
3547                );
3548            }
3549        }
3550
3551        #[test]
3552        fn autotest_round_trip_background_position() {
3553            let horizontals = [
3554                BackgroundPositionHorizontal::Left,
3555                BackgroundPositionHorizontal::Center,
3556                BackgroundPositionHorizontal::Right,
3557                BackgroundPositionHorizontal::Exact(PixelValue::px(50.0)),
3558                BackgroundPositionHorizontal::Exact(PixelValue::percent(25.0)),
3559            ];
3560            let verticals = [
3561                BackgroundPositionVertical::Top,
3562                BackgroundPositionVertical::Center,
3563                BackgroundPositionVertical::Bottom,
3564                BackgroundPositionVertical::Exact(PixelValue::px(-10.0)),
3565                BackgroundPositionVertical::Exact(PixelValue::em(2.5)),
3566            ];
3567            for horizontal in horizontals {
3568                for vertical in verticals {
3569                    let pos = StyleBackgroundPosition {
3570                        horizontal,
3571                        vertical,
3572                    };
3573                    let printed = pos.print_as_css_value();
3574                    assert_eq!(
3575                        parse_style_background_position(&printed).unwrap(),
3576                        pos,
3577                        "round-trip failed for {printed:?}"
3578                    );
3579                }
3580            }
3581        }
3582
3583        #[test]
3584        fn autotest_round_trip_background_content_colors_and_images() {
3585            for content in [
3586                StyleBackgroundContent::Color(ColorU::RED),
3587                StyleBackgroundContent::Color(ColorU::TRANSPARENT),
3588                StyleBackgroundContent::Color(ColorU::rgba(1, 2, 3, 4)),
3589                StyleBackgroundContent::Color(ColorU::WHITE),
3590                StyleBackgroundContent::Image("a.png".into()),
3591                StyleBackgroundContent::Image("some/deep/path.jpeg".into()),
3592                StyleBackgroundContent::SystemColor(SystemColorRef::Accent),
3593                StyleBackgroundContent::SystemColor(SystemColorRef::SelectionText),
3594            ] {
3595                let printed = content.print_as_css_value();
3596                assert_eq!(
3597                    parse_style_background_content(&printed).unwrap(),
3598                    content,
3599                    "round-trip failed for {printed:?}"
3600                );
3601            }
3602            assert_eq!(
3603                StyleBackgroundContent::default(),
3604                StyleBackgroundContent::Color(ColorU::TRANSPARENT)
3605            );
3606        }
3607
3608        #[test]
3609        fn autotest_round_trip_gradients() {
3610            for input in [
3611                "linear-gradient(to right, red 0%, blue 100%)",
3612                "repeating-linear-gradient(to bottom, red 25%, blue 75%)",
3613                "linear-gradient(45deg, red 0%, blue 50%)",
3614                "radial-gradient(circle farthest-corner at left top, red 0%, blue 100%)",
3615                "conic-gradient(from 90deg at left top, red 0deg, blue 360deg)",
3616                "repeating-conic-gradient(from 0deg at left top, red 0deg, blue 180deg)",
3617            ] {
3618                let parsed = parse_style_background_content(input).unwrap();
3619                let printed = parsed.print_as_css_value();
3620                let reparsed = parse_style_background_content(&printed).unwrap();
3621                assert_eq!(
3622                    parsed, reparsed,
3623                    "gradient did not survive print -> parse ({printed:?})"
3624                );
3625                // ... and printing is stable across the round-trip.
3626                assert_eq!(printed, reparsed.print_as_css_value());
3627            }
3628        }
3629
3630        #[test]
3631        fn autotest_round_trip_vec_printing_is_comma_separated() {
3632            let contents = parse_style_background_content_multiple("red, blue").unwrap();
3633            assert_eq!(contents.print_as_css_value(), "#ff0000ff, #0000ffff");
3634            assert_eq!(contents.as_slice()[1], StyleBackgroundContent::Color(blue()));
3635            let reparsed =
3636                parse_style_background_content_multiple(&contents.print_as_css_value()).unwrap();
3637            assert_eq!(reparsed, contents);
3638
3639            let sizes = parse_style_background_size_multiple("contain, 10px 20px").unwrap();
3640            assert_eq!(
3641                parse_style_background_size_multiple(&sizes.print_as_css_value()).unwrap(),
3642                sizes
3643            );
3644
3645            let repeats = parse_style_background_repeat_multiple("repeat, no-repeat").unwrap();
3646            assert_eq!(
3647                parse_style_background_repeat_multiple(&repeats.print_as_css_value()).unwrap(),
3648                repeats
3649            );
3650
3651            let positions = parse_style_background_position_multiple("left top, 10px 20px").unwrap();
3652            assert_eq!(
3653                parse_style_background_position_multiple(&positions.print_as_css_value()).unwrap(),
3654                positions
3655            );
3656        }
3657
3658        #[test]
3659        fn autotest_normalized_stop_printing_is_reparseable() {
3660            let stop = NormalizedLinearColorStop::new(PercentageValue::new(25.0), ColorU::RED);
3661            assert_eq!(stop.print_as_css_value(), "#ff0000ff 25%");
3662            let reparsed = parse_linear_color_stop(&stop.print_as_css_value()).unwrap();
3663            assert_eq!(reparsed.color, stop.color);
3664            assert_eq!(
3665                reparsed.offset1,
3666                OptionPercentageValue::Some(PercentageValue::new(25.0))
3667            );
3668
3669            let rstop = NormalizedRadialColorStop::new(AngleValue::deg(90.0), blue());
3670            assert_eq!(rstop.print_as_css_value(), "#0000ffff 90deg");
3671            let reparsed = parse_radial_color_stop(&rstop.print_as_css_value()).unwrap();
3672            assert_eq!(reparsed.color, rstop.color);
3673            assert_eq!(
3674                reparsed.offset1,
3675                OptionAngleValue::Some(AngleValue::deg(90.0))
3676            );
3677
3678            // System-colored stops print their `system:*` name and re-parse.
3679            let sys = NormalizedLinearColorStop {
3680                offset: PercentageValue::new(50.0),
3681                color: ColorOrSystem::System(SystemColorRef::Accent),
3682            };
3683            assert_eq!(sys.print_as_css_value(), "system:accent 50%");
3684            assert_eq!(
3685                parse_linear_color_stop(&sys.print_as_css_value()).unwrap().color,
3686                ColorOrSystem::System(SystemColorRef::Accent)
3687            );
3688        }
3689
3690        #[test]
3691        fn autotest_empty_gradient_printing_does_not_panic() {
3692            // Default gradients have zero stops -- printing must still produce
3693            // something well-formed (and not, say, index out of bounds).
3694            let lg = StyleBackgroundContent::LinearGradient(LinearGradient::default());
3695            assert!(lg.print_as_css_value().starts_with("linear-gradient("));
3696
3697            let rg = StyleBackgroundContent::RadialGradient(RadialGradient::default());
3698            assert!(rg.print_as_css_value().starts_with("radial-gradient("));
3699
3700            let cg = StyleBackgroundContent::ConicGradient(ConicGradient::default());
3701            assert!(cg.print_as_css_value().starts_with("conic-gradient("));
3702
3703            // A gradient built from an empty stop list is also printable.
3704            let empty_stops = StyleBackgroundContent::LinearGradient(LinearGradient {
3705                extend_mode: ExtendMode::Repeat,
3706                stops: Vec::<NormalizedLinearColorStop>::new().into(),
3707                ..LinearGradient::default()
3708            });
3709            assert!(empty_stops
3710                .print_as_css_value()
3711                .starts_with("repeating-linear-gradient("));
3712        }
3713    }
3714}
3715
3716#[cfg(feature = "parser")]
3717pub use self::parser::*;
3718
3719#[cfg(all(test, feature = "parser"))]
3720mod tests {
3721    use super::*;
3722    use crate::props::basic::{DirectionCorner, DirectionCorners};
3723
3724    #[test]
3725    fn test_parse_single_background_content() {
3726        // Color
3727        assert_eq!(
3728            parse_style_background_content("red").unwrap(),
3729            StyleBackgroundContent::Color(ColorU::RED)
3730        );
3731        assert_eq!(
3732            parse_style_background_content("#ff00ff").unwrap(),
3733            StyleBackgroundContent::Color(ColorU::new_rgb(255, 0, 255))
3734        );
3735
3736        // Image
3737        assert_eq!(
3738            parse_style_background_content("url(\"image.png\")").unwrap(),
3739            StyleBackgroundContent::Image("image.png".into())
3740        );
3741
3742        // Linear Gradient
3743        let lg = parse_style_background_content("linear-gradient(to right, red, blue)").unwrap();
3744        assert!(matches!(lg, StyleBackgroundContent::LinearGradient(_)));
3745        if let StyleBackgroundContent::LinearGradient(grad) = lg {
3746            assert_eq!(grad.stops.len(), 2);
3747            assert_eq!(
3748                grad.direction,
3749                Direction::FromTo(DirectionCorners {
3750                    dir_from: DirectionCorner::Left,
3751                    dir_to: DirectionCorner::Right
3752                })
3753            );
3754        }
3755
3756        // Radial Gradient
3757        let rg = parse_style_background_content("radial-gradient(circle, white, black)").unwrap();
3758        assert!(matches!(rg, StyleBackgroundContent::RadialGradient(_)));
3759        if let StyleBackgroundContent::RadialGradient(grad) = rg {
3760            assert_eq!(grad.stops.len(), 2);
3761            assert_eq!(grad.shape, Shape::Circle);
3762        }
3763
3764        // Conic Gradient
3765        let cg = parse_style_background_content("conic-gradient(from 90deg, red, blue)").unwrap();
3766        assert!(matches!(cg, StyleBackgroundContent::ConicGradient(_)));
3767        if let StyleBackgroundContent::ConicGradient(grad) = cg {
3768            assert_eq!(grad.stops.len(), 2);
3769            assert_eq!(grad.angle, AngleValue::deg(90.0));
3770        }
3771    }
3772
3773    #[test]
3774    fn test_parse_multiple_background_content() {
3775        let result =
3776            parse_style_background_content_multiple("url(foo.png), linear-gradient(red, blue)")
3777                .unwrap();
3778        assert_eq!(result.len(), 2);
3779        assert!(matches!(
3780            result.as_slice()[0],
3781            StyleBackgroundContent::Image(_)
3782        ));
3783        assert!(matches!(
3784            result.as_slice()[1],
3785            StyleBackgroundContent::LinearGradient(_)
3786        ));
3787    }
3788
3789    #[test]
3790    fn test_parse_background_position() {
3791        // One value
3792        let result = parse_style_background_position("center").unwrap();
3793        assert_eq!(result.horizontal, BackgroundPositionHorizontal::Center);
3794        assert_eq!(result.vertical, BackgroundPositionVertical::Center);
3795
3796        let result = parse_style_background_position("25%").unwrap();
3797        assert_eq!(
3798            result.horizontal,
3799            BackgroundPositionHorizontal::Exact(PixelValue::percent(25.0))
3800        );
3801        assert_eq!(result.vertical, BackgroundPositionVertical::Center);
3802
3803        // Two values
3804        let result = parse_style_background_position("right 50px").unwrap();
3805        assert_eq!(result.horizontal, BackgroundPositionHorizontal::Right);
3806        assert_eq!(
3807            result.vertical,
3808            BackgroundPositionVertical::Exact(PixelValue::px(50.0))
3809        );
3810
3811        // Four values (not supported by this parser, should fail)
3812        assert!(parse_style_background_position("left 10px top 20px").is_err());
3813    }
3814
3815    #[test]
3816    fn test_parse_background_size() {
3817        assert_eq!(
3818            parse_style_background_size("contain").unwrap(),
3819            StyleBackgroundSize::Contain
3820        );
3821        assert_eq!(
3822            parse_style_background_size("cover").unwrap(),
3823            StyleBackgroundSize::Cover
3824        );
3825        assert_eq!(
3826            parse_style_background_size("50%").unwrap(),
3827            StyleBackgroundSize::ExactSize(PixelValueSize {
3828                width: PixelValue::percent(50.0),
3829                height: PixelValue::percent(50.0)
3830            })
3831        );
3832        assert_eq!(
3833            parse_style_background_size("100px 20em").unwrap(),
3834            StyleBackgroundSize::ExactSize(PixelValueSize {
3835                width: PixelValue::px(100.0),
3836                height: PixelValue::em(20.0)
3837            })
3838        );
3839        assert!(parse_style_background_size("auto").is_err());
3840    }
3841
3842    #[test]
3843    fn test_parse_background_repeat() {
3844        assert_eq!(
3845            parse_style_background_repeat("repeat").unwrap(),
3846            StyleBackgroundRepeat::PatternRepeat
3847        );
3848        assert_eq!(
3849            parse_style_background_repeat("repeat-x").unwrap(),
3850            StyleBackgroundRepeat::RepeatX
3851        );
3852        assert_eq!(
3853            parse_style_background_repeat("repeat-y").unwrap(),
3854            StyleBackgroundRepeat::RepeatY
3855        );
3856        assert_eq!(
3857            parse_style_background_repeat("no-repeat").unwrap(),
3858            StyleBackgroundRepeat::NoRepeat
3859        );
3860        assert!(parse_style_background_repeat("repeat-xy").is_err());
3861    }
3862
3863    // =========================================================================
3864    // W3C CSS Images Level 3 - Gradient Parsing Tests
3865    // =========================================================================
3866
3867    #[test]
3868    fn test_gradient_no_position_stops() {
3869        // "linear-gradient(red, blue)" - no positions specified
3870        let lg = parse_style_background_content("linear-gradient(red, blue)").unwrap();
3871        if let StyleBackgroundContent::LinearGradient(grad) = lg {
3872            assert_eq!(grad.stops.len(), 2);
3873            // First stop should default to 0%
3874            assert!((grad.stops.as_ref()[0].offset.normalized() - 0.0).abs() < 0.001);
3875            // Last stop should default to 100%
3876            assert!((grad.stops.as_ref()[1].offset.normalized() - 1.0).abs() < 0.001);
3877        } else {
3878            panic!("Expected LinearGradient");
3879        }
3880    }
3881
3882    #[test]
3883    fn test_gradient_single_position_stops() {
3884        // "linear-gradient(red 25%, blue 75%)" - one position per stop
3885        let lg = parse_style_background_content("linear-gradient(red 25%, blue 75%)").unwrap();
3886        if let StyleBackgroundContent::LinearGradient(grad) = lg {
3887            assert_eq!(grad.stops.len(), 2);
3888            assert!((grad.stops.as_ref()[0].offset.normalized() - 0.25).abs() < 0.001);
3889            assert!((grad.stops.as_ref()[1].offset.normalized() - 0.75).abs() < 0.001);
3890        } else {
3891            panic!("Expected LinearGradient");
3892        }
3893    }
3894
3895    #[test]
3896    fn test_gradient_multi_position_stops() {
3897        // "linear-gradient(red 10% 30%, blue)" - two positions create two stops
3898        let lg = parse_style_background_content("linear-gradient(red 10% 30%, blue)").unwrap();
3899        if let StyleBackgroundContent::LinearGradient(grad) = lg {
3900            // Should have 3 stops: red@10%, red@30%, blue@100%
3901            assert_eq!(grad.stops.len(), 3, "Expected 3 stops for multi-position");
3902            assert!((grad.stops.as_ref()[0].offset.normalized() - 0.10).abs() < 0.001);
3903            assert!((grad.stops.as_ref()[1].offset.normalized() - 0.30).abs() < 0.001);
3904            assert!((grad.stops.as_ref()[2].offset.normalized() - 1.0).abs() < 0.001);
3905            // Both first two stops should have same color (red)
3906            assert_eq!(grad.stops.as_ref()[0].color, grad.stops.as_ref()[1].color);
3907        } else {
3908            panic!("Expected LinearGradient");
3909        }
3910    }
3911
3912    #[test]
3913    fn test_gradient_three_colors_no_positions() {
3914        // "linear-gradient(red, green, blue)" - evenly distributed
3915        let lg = parse_style_background_content("linear-gradient(red, green, blue)").unwrap();
3916        if let StyleBackgroundContent::LinearGradient(grad) = lg {
3917            assert_eq!(grad.stops.len(), 3);
3918            // Positions: 0%, 50%, 100%
3919            assert!((grad.stops.as_ref()[0].offset.normalized() - 0.0).abs() < 0.001);
3920            assert!((grad.stops.as_ref()[1].offset.normalized() - 0.5).abs() < 0.001);
3921            assert!((grad.stops.as_ref()[2].offset.normalized() - 1.0).abs() < 0.001);
3922        } else {
3923            panic!("Expected LinearGradient");
3924        }
3925    }
3926
3927    #[test]
3928    fn test_gradient_fixup_ascending_order() {
3929        // "linear-gradient(red 50%, blue 20%)" - blue position < red position
3930        // W3C says: clamp to max of previous positions
3931        let lg = parse_style_background_content("linear-gradient(red 50%, blue 20%)").unwrap();
3932        if let StyleBackgroundContent::LinearGradient(grad) = lg {
3933            assert_eq!(grad.stops.len(), 2);
3934            // First stop at 50%
3935            assert!((grad.stops.as_ref()[0].offset.normalized() - 0.50).abs() < 0.001);
3936            // Second stop clamped to 50% (not 20%)
3937            assert!((grad.stops.as_ref()[1].offset.normalized() - 0.50).abs() < 0.001);
3938        } else {
3939            panic!("Expected LinearGradient");
3940        }
3941    }
3942
3943    #[test]
3944    fn test_gradient_distribute_unpositioned() {
3945        // "linear-gradient(red 0%, yellow, green, blue 100%)"
3946        // yellow and green should be distributed evenly between 0% and 100%
3947        let lg =
3948            parse_style_background_content("linear-gradient(red 0%, yellow, green, blue 100%)")
3949                .unwrap();
3950        if let StyleBackgroundContent::LinearGradient(grad) = lg {
3951            assert_eq!(grad.stops.len(), 4);
3952            // Positions: 0%, 33.3%, 66.6%, 100%
3953            assert!((grad.stops.as_ref()[0].offset.normalized() - 0.0).abs() < 0.001);
3954            assert!((grad.stops.as_ref()[1].offset.normalized() - 0.333).abs() < 0.01);
3955            assert!((grad.stops.as_ref()[2].offset.normalized() - 0.666).abs() < 0.01);
3956            assert!((grad.stops.as_ref()[3].offset.normalized() - 1.0).abs() < 0.001);
3957        } else {
3958            panic!("Expected LinearGradient");
3959        }
3960    }
3961
3962    #[test]
3963    fn test_gradient_direction_to_corner() {
3964        // "linear-gradient(to top right, red, blue)"
3965        let lg =
3966            parse_style_background_content("linear-gradient(to top right, red, blue)").unwrap();
3967        if let StyleBackgroundContent::LinearGradient(grad) = lg {
3968            assert_eq!(
3969                grad.direction,
3970                Direction::FromTo(DirectionCorners {
3971                    dir_from: DirectionCorner::BottomLeft,
3972                    dir_to: DirectionCorner::TopRight
3973                })
3974            );
3975        } else {
3976            panic!("Expected LinearGradient");
3977        }
3978    }
3979
3980    #[test]
3981    fn test_gradient_direction_angle() {
3982        // "linear-gradient(45deg, red, blue)"
3983        let lg = parse_style_background_content("linear-gradient(45deg, red, blue)").unwrap();
3984        if let StyleBackgroundContent::LinearGradient(grad) = lg {
3985            assert_eq!(grad.direction, Direction::Angle(AngleValue::deg(45.0)));
3986        } else {
3987            panic!("Expected LinearGradient");
3988        }
3989    }
3990
3991    #[test]
3992    fn test_repeating_gradient() {
3993        // "repeating-linear-gradient(red, blue 20%)"
3994        let lg =
3995            parse_style_background_content("repeating-linear-gradient(red, blue 20%)").unwrap();
3996        if let StyleBackgroundContent::LinearGradient(grad) = lg {
3997            assert_eq!(grad.extend_mode, ExtendMode::Repeat);
3998        } else {
3999            panic!("Expected LinearGradient");
4000        }
4001    }
4002
4003    #[test]
4004    fn test_radial_gradient_circle() {
4005        // "radial-gradient(circle, red, blue)"
4006        let rg = parse_style_background_content("radial-gradient(circle, red, blue)").unwrap();
4007        if let StyleBackgroundContent::RadialGradient(grad) = rg {
4008            assert_eq!(grad.shape, Shape::Circle);
4009            assert_eq!(grad.stops.len(), 2);
4010            // Check default position is center
4011            assert_eq!(grad.position.horizontal, BackgroundPositionHorizontal::Left);
4012            assert_eq!(grad.position.vertical, BackgroundPositionVertical::Top);
4013        } else {
4014            panic!("Expected RadialGradient");
4015        }
4016    }
4017
4018    #[test]
4019    fn test_radial_gradient_ellipse() {
4020        // "radial-gradient(ellipse, red, blue)"
4021        let rg = parse_style_background_content("radial-gradient(ellipse, red, blue)").unwrap();
4022        if let StyleBackgroundContent::RadialGradient(grad) = rg {
4023            assert_eq!(grad.shape, Shape::Ellipse);
4024            assert_eq!(grad.stops.len(), 2);
4025        } else {
4026            panic!("Expected RadialGradient");
4027        }
4028    }
4029
4030    #[test]
4031    fn test_radial_gradient_size_keywords() {
4032        // Test different size keywords
4033        let rg = parse_style_background_content("radial-gradient(circle closest-side, red, blue)")
4034            .unwrap();
4035        if let StyleBackgroundContent::RadialGradient(grad) = rg {
4036            assert_eq!(grad.shape, Shape::Circle);
4037            assert_eq!(grad.size, RadialGradientSize::ClosestSide);
4038        } else {
4039            panic!("Expected RadialGradient");
4040        }
4041    }
4042
4043    #[test]
4044    fn test_radial_gradient_stop_positions() {
4045        // "radial-gradient(red 0%, blue 100%)"
4046        let rg = parse_style_background_content("radial-gradient(red 0%, blue 100%)").unwrap();
4047        if let StyleBackgroundContent::RadialGradient(grad) = rg {
4048            assert_eq!(grad.stops.len(), 2);
4049            assert!((grad.stops.as_ref()[0].offset.normalized() - 0.0).abs() < 0.001);
4050            assert!((grad.stops.as_ref()[1].offset.normalized() - 1.0).abs() < 0.001);
4051        } else {
4052            panic!("Expected RadialGradient");
4053        }
4054    }
4055
4056    #[test]
4057    fn test_repeating_radial_gradient() {
4058        let rg = parse_style_background_content("repeating-radial-gradient(circle, red, blue 20%)")
4059            .unwrap();
4060        if let StyleBackgroundContent::RadialGradient(grad) = rg {
4061            assert_eq!(grad.extend_mode, ExtendMode::Repeat);
4062            assert_eq!(grad.shape, Shape::Circle);
4063        } else {
4064            panic!("Expected RadialGradient");
4065        }
4066    }
4067
4068    #[test]
4069    fn test_conic_gradient_angle() {
4070        // "conic-gradient(from 45deg, red, blue)"
4071        let cg = parse_style_background_content("conic-gradient(from 45deg, red, blue)").unwrap();
4072        if let StyleBackgroundContent::ConicGradient(grad) = cg {
4073            assert_eq!(grad.angle, AngleValue::deg(45.0));
4074            assert_eq!(grad.stops.len(), 2);
4075        } else {
4076            panic!("Expected ConicGradient");
4077        }
4078    }
4079
4080    #[test]
4081    fn test_conic_gradient_default() {
4082        // "conic-gradient(red, blue)" - no angle specified
4083        let cg = parse_style_background_content("conic-gradient(red, blue)").unwrap();
4084        if let StyleBackgroundContent::ConicGradient(grad) = cg {
4085            assert_eq!(grad.stops.len(), 2);
4086            // First stop defaults to 0deg
4087            assert!(
4088                (grad.stops.as_ref()[0].angle.to_degrees_raw() - 0.0).abs() < 0.001,
4089                "First stop should be 0deg, got {}",
4090                grad.stops.as_ref()[0].angle.to_degrees_raw()
4091            );
4092            // Last stop defaults to 360deg (use to_degrees_raw to preserve 360)
4093            assert!(
4094                (grad.stops.as_ref()[1].angle.to_degrees_raw() - 360.0).abs() < 0.001,
4095                "Last stop should be 360deg, got {}",
4096                grad.stops.as_ref()[1].angle.to_degrees_raw()
4097            );
4098        } else {
4099            panic!("Expected ConicGradient");
4100        }
4101    }
4102
4103    #[test]
4104    fn test_conic_gradient_with_positions() {
4105        // "conic-gradient(red 0deg, blue 180deg, green 360deg)"
4106        let cg =
4107            parse_style_background_content("conic-gradient(red 0deg, blue 180deg, green 360deg)")
4108                .unwrap();
4109        if let StyleBackgroundContent::ConicGradient(grad) = cg {
4110            assert_eq!(grad.stops.len(), 3);
4111            // Use to_degrees_raw() to preserve 360deg
4112            assert!(
4113                (grad.stops.as_ref()[0].angle.to_degrees_raw() - 0.0).abs() < 0.001,
4114                "First stop should be 0deg, got {}",
4115                grad.stops.as_ref()[0].angle.to_degrees_raw()
4116            );
4117            assert!(
4118                (grad.stops.as_ref()[1].angle.to_degrees_raw() - 180.0).abs() < 0.001,
4119                "Second stop should be 180deg, got {}",
4120                grad.stops.as_ref()[1].angle.to_degrees_raw()
4121            );
4122            assert!(
4123                (grad.stops.as_ref()[2].angle.to_degrees_raw() - 360.0).abs() < 0.001,
4124                "Last stop should be 360deg, got {}",
4125                grad.stops.as_ref()[2].angle.to_degrees_raw()
4126            );
4127        } else {
4128            panic!("Expected ConicGradient");
4129        }
4130    }
4131
4132    #[test]
4133    fn test_repeating_conic_gradient() {
4134        let cg =
4135            parse_style_background_content("repeating-conic-gradient(red, blue 30deg)").unwrap();
4136        if let StyleBackgroundContent::ConicGradient(grad) = cg {
4137            assert_eq!(grad.extend_mode, ExtendMode::Repeat);
4138        } else {
4139            panic!("Expected ConicGradient");
4140        }
4141    }
4142
4143    #[test]
4144    fn test_gradient_with_rgba_color() {
4145        // Test parsing gradient with rgba color (contains spaces)
4146        let lg =
4147            parse_style_background_content("linear-gradient(rgba(255,0,0,0.5), blue)").unwrap();
4148        if let StyleBackgroundContent::LinearGradient(grad) = lg {
4149            assert_eq!(grad.stops.len(), 2);
4150            // First color should have alpha of ~128 (0.5 * 255, may be 127 or 128 due to rounding)
4151            let first_color = grad.stops.as_ref()[0].color.to_color_u_default();
4152            assert!(first_color.a >= 127 && first_color.a <= 128);
4153        } else {
4154            panic!("Expected LinearGradient");
4155        }
4156    }
4157
4158    #[test]
4159    fn test_gradient_with_rgba_and_position() {
4160        // Test parsing "rgba(0,0,0,0.5) 50%"
4161        let lg =
4162            parse_style_background_content("linear-gradient(rgba(0,0,0,0.5) 50%, white)").unwrap();
4163        if let StyleBackgroundContent::LinearGradient(grad) = lg {
4164            assert_eq!(grad.stops.len(), 2);
4165            assert!((grad.stops.as_ref()[0].offset.normalized() - 0.5).abs() < 0.001);
4166        } else {
4167            panic!("Expected LinearGradient");
4168        }
4169    }
4170
4171    #[test]
4172    fn test_gradient_resolves_system_color_stop() {
4173        // A `system:accent` stop should round-trip through the parser as a
4174        // System variant and resolve against a populated `SystemColors` to
4175        // the live accent color, falling back to the supplied default when
4176        // the key is unset.
4177        use crate::props::basic::color::ColorOrSystem;
4178        use crate::system::SystemColors;
4179
4180        let lg = parse_style_background_content(
4181            "linear-gradient(red, system:accent)",
4182        )
4183        .unwrap();
4184        let StyleBackgroundContent::LinearGradient(grad) = lg else {
4185            panic!("Expected LinearGradient");
4186        };
4187        let stops = grad.stops.as_ref();
4188        assert_eq!(stops.len(), 2);
4189
4190        let accent_stop = &stops[1];
4191        assert!(matches!(accent_stop.color, ColorOrSystem::System(_)));
4192
4193        let populated = SystemColors {
4194            accent: crate::props::basic::color::OptionColorU::Some(ColorU::new_rgb(0, 122, 255)),
4195            ..SystemColors::default()
4196        };
4197
4198        let resolved = accent_stop.resolve(&populated, ColorU::TRANSPARENT);
4199        assert_eq!(resolved, ColorU::new_rgb(0, 122, 255));
4200
4201        let empty = SystemColors::default();
4202        let fallback = accent_stop.resolve(&empty, ColorU::TRANSPARENT);
4203        assert_eq!(fallback, ColorU::TRANSPARENT);
4204    }
4205}