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