Skip to main content

azul_css/props/style/
background.rs

1//! CSS properties for backgrounds, including colors, images, and gradients.
2
3use alloc::{
4    string::{String, ToString},
5    vec::Vec,
6};
7use core::fmt;
8
9#[cfg(feature = "parser")]
10use crate::props::basic::{
11    error::{InvalidValueErr, InvalidValueErrOwned},
12    parse::{
13        parse_parentheses, parse_image, split_string_respect_comma,
14        CssImageParseError, CssImageParseErrorOwned,
15        ParenthesisParseError, ParenthesisParseErrorOwned,
16    },
17    color::parse_color_or_system,
18};
19use crate::{
20    corety::AzString,
21    codegen::format::GetHash,
22    props::{
23        basic::{
24            angle::{
25                parse_angle_value, AngleValue, CssAngleValueParseError,
26                CssAngleValueParseErrorOwned, OptionAngleValue,
27            },
28            color::{ColorU, ColorOrSystem, SystemColorRef, CssColorParseError, CssColorParseErrorOwned},
29            direction::{
30                parse_direction, CssDirectionParseError, CssDirectionParseErrorOwned, Direction,
31            },
32            length::{
33                parse_percentage_value, OptionPercentageValue, PercentageParseError,
34                PercentageParseErrorOwned, PercentageValue,
35            },
36            pixel::{
37                parse_pixel_value, CssPixelValueParseError, CssPixelValueParseErrorOwned,
38                PixelValue,
39            },
40        },
41        formatter::PrintAsCssValue,
42    },
43};
44
45// --- TYPE DEFINITIONS ---
46
47/// Whether a `gradient` should be repeated or clamped to the edges.
48#[derive(Debug, Copy, Clone, PartialEq, Ord, PartialOrd, Eq, Hash)]
49#[repr(C)]
50#[derive(Default)]
51pub enum ExtendMode {
52    #[default]
53    Clamp,
54    Repeat,
55}
56
57// -- Main Background Content Type --
58
59/// A single CSS background layer: a solid color, image URL, or gradient.
60#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
61#[repr(C, u8)]
62pub enum StyleBackgroundContent {
63    LinearGradient(LinearGradient),
64    RadialGradient(RadialGradient),
65    ConicGradient(ConicGradient),
66    Image(AzString),
67    Color(ColorU),
68    /// A theme-aware system color (e.g. `background: system:accent`), kept unresolved
69    /// and resolved at render time. Mirrors the `ColorOrSystem::System` value that
70    /// gradient color stops already accept.
71    SystemColor(SystemColorRef),
72}
73
74impl_option!(
75    StyleBackgroundContent,
76    OptionStyleBackgroundContent,
77    copy = false,
78    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
79);
80
81impl_vec!(StyleBackgroundContent, StyleBackgroundContentVec, StyleBackgroundContentVecDestructor, StyleBackgroundContentVecDestructorType, StyleBackgroundContentVecSlice, OptionStyleBackgroundContent);
82impl_vec_debug!(StyleBackgroundContent, StyleBackgroundContentVec);
83impl_vec_partialord!(StyleBackgroundContent, StyleBackgroundContentVec);
84impl_vec_ord!(StyleBackgroundContent, StyleBackgroundContentVec);
85impl_vec_clone!(
86    StyleBackgroundContent,
87    StyleBackgroundContentVec,
88    StyleBackgroundContentVecDestructor
89);
90impl_vec_partialeq!(StyleBackgroundContent, StyleBackgroundContentVec);
91impl_vec_eq!(StyleBackgroundContent, StyleBackgroundContentVec);
92impl_vec_hash!(StyleBackgroundContent, StyleBackgroundContentVec);
93
94impl Default for StyleBackgroundContent {
95    fn default() -> Self {
96        Self::Color(ColorU::TRANSPARENT)
97    }
98}
99
100impl PrintAsCssValue for StyleBackgroundContent {
101    fn print_as_css_value(&self) -> String {
102        match self {
103            Self::LinearGradient(lg) => {
104                let prefix = if lg.extend_mode == ExtendMode::Repeat {
105                    "repeating-linear-gradient"
106                } else {
107                    "linear-gradient"
108                };
109                format!("{}({})", prefix, lg.print_as_css_value())
110            }
111            Self::RadialGradient(rg) => {
112                let prefix = if rg.extend_mode == ExtendMode::Repeat {
113                    "repeating-radial-gradient"
114                } else {
115                    "radial-gradient"
116                };
117                format!("{}({})", prefix, rg.print_as_css_value())
118            }
119            Self::ConicGradient(cg) => {
120                let prefix = if cg.extend_mode == ExtendMode::Repeat {
121                    "repeating-conic-gradient"
122                } else {
123                    "conic-gradient"
124                };
125                format!("{}({})", prefix, cg.print_as_css_value())
126            }
127            Self::Image(id) => format!("url(\"{}\")", id.as_str()),
128            Self::Color(c) => c.to_hash(),
129            Self::SystemColor(s) => s.as_css_str().to_string(),
130        }
131    }
132}
133
134// Formatting to Rust code for background-related vecs
135
136impl crate::codegen::format::FormatAsRustCode for StyleBackgroundContent {
137    fn format_as_rust_code(&self, _tabs: usize) -> String {
138        // Delegate to the CSS value representation for single backgrounds
139        format!("StyleBackgroundContent::from_css(\"{}\")", self.print_as_css_value())
140    }
141}
142
143impl crate::codegen::format::FormatAsRustCode for StyleBackgroundSizeVec {
144    fn format_as_rust_code(&self, _tabs: usize) -> String {
145        format!(
146            "StyleBackgroundSizeVec::from_const_slice(STYLE_BACKGROUND_SIZE_{}_ITEMS)",
147            self.get_hash()
148        )
149    }
150}
151
152impl crate::codegen::format::FormatAsRustCode for StyleBackgroundRepeatVec {
153    fn format_as_rust_code(&self, _tabs: usize) -> String {
154        format!(
155            "StyleBackgroundRepeatVec::from_const_slice(STYLE_BACKGROUND_REPEAT_{}_ITEMS)",
156            self.get_hash()
157        )
158    }
159}
160
161impl crate::codegen::format::FormatAsRustCode for StyleBackgroundContentVec {
162    fn format_as_rust_code(&self, _tabs: usize) -> String {
163        format!(
164            "StyleBackgroundContentVec::from_const_slice(STYLE_BACKGROUND_CONTENT_{}_ITEMS)",
165            self.get_hash()
166        )
167    }
168}
169
170impl PrintAsCssValue for StyleBackgroundContentVec {
171    fn print_as_css_value(&self) -> String {
172        self.as_ref()
173            .iter()
174            .map(PrintAsCssValue::print_as_css_value)
175            .collect::<Vec<_>>()
176            .join(", ")
177    }
178}
179
180// -- Gradient Types --
181
182/// A CSS `linear-gradient()` or `repeating-linear-gradient()` value.
183#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
184#[repr(C)]
185pub struct LinearGradient {
186    pub direction: Direction,
187    pub extend_mode: ExtendMode,
188    pub stops: NormalizedLinearColorStopVec,
189}
190impl Default for LinearGradient {
191    fn default() -> Self {
192        Self {
193            direction: Direction::default(),
194            extend_mode: ExtendMode::default(),
195            stops: Vec::new().into(),
196        }
197    }
198}
199impl PrintAsCssValue for LinearGradient {
200    fn print_as_css_value(&self) -> String {
201        let dir_str = self.direction.print_as_css_value();
202        let stops_str = self
203            .stops
204            .iter()
205            .map(PrintAsCssValue::print_as_css_value)
206            .collect::<Vec<_>>()
207            .join(", ");
208        if stops_str.is_empty() {
209            dir_str
210        } else {
211            format!("{dir_str}, {stops_str}")
212        }
213    }
214}
215
216/// A CSS `radial-gradient()` or `repeating-radial-gradient()` value.
217#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
218#[repr(C)]
219pub struct RadialGradient {
220    pub shape: Shape,
221    pub size: RadialGradientSize,
222    pub position: StyleBackgroundPosition,
223    pub extend_mode: ExtendMode,
224    pub stops: NormalizedLinearColorStopVec,
225}
226impl Default for RadialGradient {
227    fn default() -> Self {
228        Self {
229            shape: Shape::default(),
230            size: RadialGradientSize::default(),
231            position: StyleBackgroundPosition::default(),
232            extend_mode: ExtendMode::default(),
233            stops: Vec::new().into(),
234        }
235    }
236}
237impl PrintAsCssValue for RadialGradient {
238    fn print_as_css_value(&self) -> String {
239        let stops_str = self
240            .stops
241            .iter()
242            .map(PrintAsCssValue::print_as_css_value)
243            .collect::<Vec<_>>()
244            .join(", ");
245        format!(
246            "{} {} at {}, {}",
247            self.shape,
248            self.size,
249            self.position.print_as_css_value(),
250            stops_str
251        )
252    }
253}
254
255/// A CSS `conic-gradient()` or `repeating-conic-gradient()` value.
256#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
257#[repr(C)]
258pub struct ConicGradient {
259    pub extend_mode: ExtendMode,
260    pub center: StyleBackgroundPosition,
261    pub angle: AngleValue,
262    pub stops: NormalizedRadialColorStopVec,
263}
264impl Default for ConicGradient {
265    fn default() -> Self {
266        Self {
267            extend_mode: ExtendMode::default(),
268            center: StyleBackgroundPosition::default(),
269            angle: AngleValue::default(),
270            stops: Vec::new().into(),
271        }
272    }
273}
274impl PrintAsCssValue for ConicGradient {
275    fn print_as_css_value(&self) -> String {
276        let stops_str = self
277            .stops
278            .iter()
279            .map(PrintAsCssValue::print_as_css_value)
280            .collect::<Vec<_>>()
281            .join(", ");
282        format!(
283            "from {} at {}, {}",
284            self.angle,
285            self.center.print_as_css_value(),
286            stops_str
287        )
288    }
289}
290
291// -- Gradient Sub-types --
292
293/// The shape of a radial gradient: `circle` or `ellipse`.
294#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
295#[repr(C)]
296#[derive(Default)]
297pub enum Shape {
298    #[default]
299    Ellipse,
300    Circle,
301}
302impl fmt::Display for Shape {
303    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
304        write!(
305            f,
306            "{}",
307            match self {
308                Self::Ellipse => "ellipse",
309                Self::Circle => "circle",
310            }
311        )
312    }
313}
314
315/// The sizing keyword for a radial gradient (e.g. `closest-side`, `farthest-corner`).
316#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
317#[repr(C)]
318#[derive(Default)]
319pub enum RadialGradientSize {
320    ClosestSide,
321    ClosestCorner,
322    FarthestSide,
323    #[default]
324    FarthestCorner,
325}
326impl fmt::Display for RadialGradientSize {
327    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
328        write!(
329            f,
330            "{}",
331            match self {
332                Self::ClosestSide => "closest-side",
333                Self::ClosestCorner => "closest-corner",
334                Self::FarthestSide => "farthest-side",
335                Self::FarthestCorner => "farthest-corner",
336            }
337        )
338    }
339}
340
341#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
342#[repr(C)]
343pub struct NormalizedLinearColorStop {
344    pub offset: PercentageValue,
345    /// Color for this gradient stop. Can be a concrete color or a system color reference.
346    pub color: ColorOrSystem,
347}
348
349impl NormalizedLinearColorStop {
350    /// Create a new normalized linear color stop with a concrete color.
351    #[must_use] pub const fn new(offset: PercentageValue, color: ColorU) -> Self {
352        Self { offset, color: ColorOrSystem::color(color) }
353    }
354
355    /// Resolve the color against system colors.
356    #[must_use] pub fn resolve(&self, system_colors: &crate::system::SystemColors, fallback: ColorU) -> ColorU {
357        self.color.resolve(system_colors, fallback)
358    }
359}
360
361impl_option!(
362    NormalizedLinearColorStop,
363    OptionNormalizedLinearColorStop,
364    copy = false,
365    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
366);
367impl_vec!(NormalizedLinearColorStop, NormalizedLinearColorStopVec, NormalizedLinearColorStopVecDestructor, NormalizedLinearColorStopVecDestructorType, NormalizedLinearColorStopVecSlice, OptionNormalizedLinearColorStop);
368impl_vec_debug!(NormalizedLinearColorStop, NormalizedLinearColorStopVec);
369impl_vec_partialord!(NormalizedLinearColorStop, NormalizedLinearColorStopVec);
370impl_vec_ord!(NormalizedLinearColorStop, NormalizedLinearColorStopVec);
371impl_vec_clone!(
372    NormalizedLinearColorStop,
373    NormalizedLinearColorStopVec,
374    NormalizedLinearColorStopVecDestructor
375);
376impl_vec_partialeq!(NormalizedLinearColorStop, NormalizedLinearColorStopVec);
377impl_vec_eq!(NormalizedLinearColorStop, NormalizedLinearColorStopVec);
378impl_vec_hash!(NormalizedLinearColorStop, NormalizedLinearColorStopVec);
379impl PrintAsCssValue for NormalizedLinearColorStop {
380    fn print_as_css_value(&self) -> String {
381        match &self.color {
382            ColorOrSystem::Color(c) => format!("{} {}", c.to_hash(), self.offset),
383            ColorOrSystem::System(s) => format!("{} {}", s.as_css_str(), self.offset),
384        }
385    }
386}
387
388#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
389#[repr(C)]
390pub struct NormalizedRadialColorStop {
391    pub angle: AngleValue,
392    /// Color for this gradient stop. Can be a concrete color or a system color reference.
393    pub color: ColorOrSystem,
394}
395
396impl NormalizedRadialColorStop {
397    /// Create a new normalized radial color stop with a concrete color.
398    #[must_use] pub const fn new(angle: AngleValue, color: ColorU) -> Self {
399        Self { angle, color: ColorOrSystem::color(color) }
400    }
401
402    /// Resolve the color against system colors.
403    #[must_use] pub fn resolve(&self, system_colors: &crate::system::SystemColors, fallback: ColorU) -> ColorU {
404        self.color.resolve(system_colors, fallback)
405    }
406}
407
408impl_option!(
409    NormalizedRadialColorStop,
410    OptionNormalizedRadialColorStop,
411    copy = false,
412    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
413);
414impl_vec!(NormalizedRadialColorStop, NormalizedRadialColorStopVec, NormalizedRadialColorStopVecDestructor, NormalizedRadialColorStopVecDestructorType, NormalizedRadialColorStopVecSlice, OptionNormalizedRadialColorStop);
415impl_vec_debug!(NormalizedRadialColorStop, NormalizedRadialColorStopVec);
416impl_vec_partialord!(NormalizedRadialColorStop, NormalizedRadialColorStopVec);
417impl_vec_ord!(NormalizedRadialColorStop, NormalizedRadialColorStopVec);
418impl_vec_clone!(
419    NormalizedRadialColorStop,
420    NormalizedRadialColorStopVec,
421    NormalizedRadialColorStopVecDestructor
422);
423impl_vec_partialeq!(NormalizedRadialColorStop, NormalizedRadialColorStopVec);
424impl_vec_eq!(NormalizedRadialColorStop, NormalizedRadialColorStopVec);
425impl_vec_hash!(NormalizedRadialColorStop, NormalizedRadialColorStopVec);
426impl PrintAsCssValue for NormalizedRadialColorStop {
427    fn print_as_css_value(&self) -> String {
428        match &self.color {
429            ColorOrSystem::Color(c) => format!("{} {}", c.to_hash(), self.angle),
430            ColorOrSystem::System(s) => format!("{} {}", s.as_css_str(), self.angle),
431        }
432    }
433}
434
435/// Transient struct for parsing linear color stops before normalization.
436///
437/// Per W3C CSS Images Level 3, a color stop can have 0, 1, or 2 positions:
438/// - `red` (no position)
439/// - `red 50%` (one position)
440/// - `red 10% 30%` (two positions - creates two stops at same color)
441/// 
442/// Supports system colors like `system:accent` for theme-aware gradients.
443#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
444pub struct LinearColorStop {
445    pub color: ColorOrSystem,
446    /// First position (optional)
447    pub offset1: OptionPercentageValue,
448    /// Second position (optional, only valid if offset1 is Some)
449    /// When present, creates two color stops at the same color.
450    pub offset2: OptionPercentageValue,
451}
452
453/// Transient struct for parsing radial/conic color stops before normalization.
454///
455/// Per W3C CSS Images Level 3, a color stop can have 0, 1, or 2 positions:
456/// - `red` (no position)
457/// - `red 90deg` (one position)
458/// - `red 45deg 90deg` (two positions - creates two stops at same color)
459/// 
460/// Supports system colors like `system:accent` for theme-aware gradients.
461#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
462pub struct RadialColorStop {
463    pub color: ColorOrSystem,
464    /// First position (optional)
465    pub offset1: OptionAngleValue,
466    /// Second position (optional, only valid if offset1 is Some)
467    /// When present, creates two color stops at the same color.
468    pub offset2: OptionAngleValue,
469}
470
471// -- Other Background Properties --
472
473/// The `background-position` property (horizontal + vertical components).
474#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
475#[repr(C)]
476pub struct StyleBackgroundPosition {
477    pub horizontal: BackgroundPositionHorizontal,
478    pub vertical: BackgroundPositionVertical,
479}
480
481impl_option!(
482    StyleBackgroundPosition,
483    OptionStyleBackgroundPosition,
484    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
485);
486impl_vec!(StyleBackgroundPosition, StyleBackgroundPositionVec, StyleBackgroundPositionVecDestructor, StyleBackgroundPositionVecDestructorType, StyleBackgroundPositionVecSlice, OptionStyleBackgroundPosition);
487impl_vec_debug!(StyleBackgroundPosition, StyleBackgroundPositionVec);
488impl_vec_partialord!(StyleBackgroundPosition, StyleBackgroundPositionVec);
489impl_vec_ord!(StyleBackgroundPosition, StyleBackgroundPositionVec);
490impl_vec_clone!(
491    StyleBackgroundPosition,
492    StyleBackgroundPositionVec,
493    StyleBackgroundPositionVecDestructor
494);
495impl_vec_partialeq!(StyleBackgroundPosition, StyleBackgroundPositionVec);
496impl_vec_eq!(StyleBackgroundPosition, StyleBackgroundPositionVec);
497impl_vec_hash!(StyleBackgroundPosition, StyleBackgroundPositionVec);
498impl Default for StyleBackgroundPosition {
499    fn default() -> Self {
500        Self {
501            horizontal: BackgroundPositionHorizontal::Left,
502            vertical: BackgroundPositionVertical::Top,
503        }
504    }
505}
506
507impl StyleBackgroundPosition {
508    pub fn scale_for_dpi(&mut self, scale_factor: f32) {
509        self.horizontal.scale_for_dpi(scale_factor);
510        self.vertical.scale_for_dpi(scale_factor);
511    }
512}
513
514impl PrintAsCssValue for StyleBackgroundPosition {
515    fn print_as_css_value(&self) -> String {
516        format!(
517            "{} {}",
518            self.horizontal.print_as_css_value(),
519            self.vertical.print_as_css_value()
520        )
521    }
522}
523impl PrintAsCssValue for StyleBackgroundPositionVec {
524    fn print_as_css_value(&self) -> String {
525        self.iter()
526            .map(PrintAsCssValue::print_as_css_value)
527            .collect::<Vec<_>>()
528            .join(", ")
529    }
530}
531
532// Formatting to Rust code for StyleBackgroundPositionVec
533impl crate::codegen::format::FormatAsRustCode for StyleBackgroundPositionVec {
534    fn format_as_rust_code(&self, _tabs: usize) -> String {
535        format!(
536            "StyleBackgroundPositionVec::from_const_slice(STYLE_BACKGROUND_POSITION_{}_ITEMS)",
537            self.get_hash()
538        )
539    }
540}
541#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
542/// Horizontal component of `background-position`: a keyword or exact pixel value.
543#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
544#[repr(C, u8)]
545pub enum BackgroundPositionHorizontal {
546    Left,
547    Center,
548    Right,
549    Exact(PixelValue),
550}
551
552impl BackgroundPositionHorizontal {
553    pub fn scale_for_dpi(&mut self, scale_factor: f32) {
554        if let Self::Exact(s) = self {
555            s.scale_for_dpi(scale_factor);
556        }
557    }
558}
559
560impl PrintAsCssValue for BackgroundPositionHorizontal {
561    fn print_as_css_value(&self) -> String {
562        match self {
563            Self::Left => "left".to_string(),
564            Self::Center => "center".to_string(),
565            Self::Right => "right".to_string(),
566            Self::Exact(px) => px.print_as_css_value(),
567        }
568    }
569}
570#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
571/// Vertical component of `background-position`: a keyword or exact pixel value.
572#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
573#[repr(C, u8)]
574pub enum BackgroundPositionVertical {
575    Top,
576    Center,
577    Bottom,
578    Exact(PixelValue),
579}
580
581impl BackgroundPositionVertical {
582    pub fn scale_for_dpi(&mut self, scale_factor: f32) {
583        if let Self::Exact(s) = self {
584            s.scale_for_dpi(scale_factor);
585        }
586    }
587}
588
589impl PrintAsCssValue for BackgroundPositionVertical {
590    fn print_as_css_value(&self) -> String {
591        match self {
592            Self::Top => "top".to_string(),
593            Self::Center => "center".to_string(),
594            Self::Bottom => "bottom".to_string(),
595            Self::Exact(px) => px.print_as_css_value(),
596        }
597    }
598}
599#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
600/// The `background-size` property: `contain`, `cover`, or an exact size.
601#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
602#[repr(C, u8)]
603#[derive(Default)]
604pub enum StyleBackgroundSize {
605    ExactSize(PixelValueSize),
606    #[default]
607    Contain,
608    Cover,
609}
610
611impl_option!(
612    StyleBackgroundSize,
613    OptionStyleBackgroundSize,
614    copy = false,
615    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
616);
617
618/// Two-dimensional size in `PixelValue` units (width, height)
619/// Used for background-size and similar properties
620#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
621#[repr(C)]
622pub struct PixelValueSize {
623    pub width: PixelValue,
624    pub height: PixelValue,
625}
626
627impl_vec!(StyleBackgroundSize, StyleBackgroundSizeVec, StyleBackgroundSizeVecDestructor, StyleBackgroundSizeVecDestructorType, StyleBackgroundSizeVecSlice, OptionStyleBackgroundSize);
628impl_vec_debug!(StyleBackgroundSize, StyleBackgroundSizeVec);
629impl_vec_partialord!(StyleBackgroundSize, StyleBackgroundSizeVec);
630impl_vec_ord!(StyleBackgroundSize, StyleBackgroundSizeVec);
631impl_vec_clone!(
632    StyleBackgroundSize,
633    StyleBackgroundSizeVec,
634    StyleBackgroundSizeVecDestructor
635);
636impl_vec_partialeq!(StyleBackgroundSize, StyleBackgroundSizeVec);
637impl_vec_eq!(StyleBackgroundSize, StyleBackgroundSizeVec);
638impl_vec_hash!(StyleBackgroundSize, StyleBackgroundSizeVec);
639
640impl StyleBackgroundSize {
641    pub fn scale_for_dpi(&mut self, scale_factor: f32) {
642        if let Self::ExactSize(size) = self {
643            size.width.scale_for_dpi(scale_factor);
644            size.height.scale_for_dpi(scale_factor);
645        }
646    }
647}
648
649impl PrintAsCssValue for StyleBackgroundSize {
650    fn print_as_css_value(&self) -> String {
651        match self {
652            Self::Contain => "contain".to_string(),
653            Self::Cover => "cover".to_string(),
654            Self::ExactSize(size) => {
655                format!(
656                    "{} {}",
657                    size.width.print_as_css_value(),
658                    size.height.print_as_css_value()
659                )
660            }
661        }
662    }
663}
664impl PrintAsCssValue for StyleBackgroundSizeVec {
665    fn print_as_css_value(&self) -> String {
666        self.iter()
667            .map(PrintAsCssValue::print_as_css_value)
668            .collect::<Vec<_>>()
669            .join(", ")
670    }
671}
672
673/// The `background-repeat` property.
674#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
675#[repr(C)]
676#[derive(Default)]
677pub enum StyleBackgroundRepeat {
678    NoRepeat,
679    #[default]
680    PatternRepeat,
681    RepeatX,
682    RepeatY,
683}
684
685impl_option!(
686    StyleBackgroundRepeat,
687    OptionStyleBackgroundRepeat,
688    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
689);
690impl_vec!(StyleBackgroundRepeat, StyleBackgroundRepeatVec, StyleBackgroundRepeatVecDestructor, StyleBackgroundRepeatVecDestructorType, StyleBackgroundRepeatVecSlice, OptionStyleBackgroundRepeat);
691impl_vec_debug!(StyleBackgroundRepeat, StyleBackgroundRepeatVec);
692impl_vec_partialord!(StyleBackgroundRepeat, StyleBackgroundRepeatVec);
693impl_vec_ord!(StyleBackgroundRepeat, StyleBackgroundRepeatVec);
694impl_vec_clone!(
695    StyleBackgroundRepeat,
696    StyleBackgroundRepeatVec,
697    StyleBackgroundRepeatVecDestructor
698);
699impl_vec_partialeq!(StyleBackgroundRepeat, StyleBackgroundRepeatVec);
700impl_vec_eq!(StyleBackgroundRepeat, StyleBackgroundRepeatVec);
701impl_vec_hash!(StyleBackgroundRepeat, StyleBackgroundRepeatVec);
702impl PrintAsCssValue for StyleBackgroundRepeat {
703    fn print_as_css_value(&self) -> String {
704        match self {
705            Self::NoRepeat => "no-repeat".to_string(),
706            Self::PatternRepeat => "repeat".to_string(),
707            Self::RepeatX => "repeat-x".to_string(),
708            Self::RepeatY => "repeat-y".to_string(),
709        }
710    }
711}
712impl PrintAsCssValue for StyleBackgroundRepeatVec {
713    fn print_as_css_value(&self) -> String {
714        self.iter()
715            .map(PrintAsCssValue::print_as_css_value)
716            .collect::<Vec<_>>()
717            .join(", ")
718    }
719}
720
721// --- ERROR DEFINITIONS ---
722
723#[derive(Clone, PartialEq)]
724pub enum CssBackgroundParseError<'a> {
725    Error(&'a str),
726    InvalidBackground(ParenthesisParseError<'a>),
727    UnclosedGradient(&'a str),
728    NoDirection(&'a str),
729    TooFewGradientStops(&'a str),
730    DirectionParseError(CssDirectionParseError<'a>),
731    GradientParseError(CssGradientStopParseError<'a>),
732    ConicGradient(CssConicGradientParseError<'a>),
733    ShapeParseError(CssShapeParseError<'a>),
734    ImageParseError(CssImageParseError<'a>),
735    ColorParseError(CssColorParseError<'a>),
736}
737
738impl_debug_as_display!(CssBackgroundParseError<'a>);
739impl_display! { CssBackgroundParseError<'a>, {
740    Error(e) => e,
741    InvalidBackground(val) => format!("Invalid background value: \"{}\"", val),
742    UnclosedGradient(val) => format!("Unclosed gradient: \"{}\"", val),
743    NoDirection(val) => format!("Gradient has no direction: \"{}\"", val),
744    TooFewGradientStops(val) => format!("Failed to parse gradient due to too few gradient steps: \"{}\"", val),
745    DirectionParseError(e) => format!("Failed to parse gradient direction: \"{}\"", e),
746    GradientParseError(e) => format!("Failed to parse gradient: {}", e),
747    ConicGradient(e) => format!("Failed to parse conic gradient: {}", e),
748    ShapeParseError(e) => format!("Failed to parse shape of radial gradient: {}", e),
749    ImageParseError(e) => format!("Failed to parse image() value: {}", e),
750    ColorParseError(e) => format!("Failed to parse color value: {}", e),
751}}
752
753#[cfg(feature = "parser")]
754impl_from!(
755    ParenthesisParseError<'a>,
756    CssBackgroundParseError::InvalidBackground
757);
758#[cfg(feature = "parser")]
759impl_from!(
760    CssDirectionParseError<'a>,
761    CssBackgroundParseError::DirectionParseError
762);
763#[cfg(feature = "parser")]
764impl_from!(
765    CssGradientStopParseError<'a>,
766    CssBackgroundParseError::GradientParseError
767);
768#[cfg(feature = "parser")]
769impl_from!(
770    CssShapeParseError<'a>,
771    CssBackgroundParseError::ShapeParseError
772);
773#[cfg(feature = "parser")]
774impl_from!(
775    CssImageParseError<'a>,
776    CssBackgroundParseError::ImageParseError
777);
778#[cfg(feature = "parser")]
779impl_from!(
780    CssColorParseError<'a>,
781    CssBackgroundParseError::ColorParseError
782);
783#[cfg(feature = "parser")]
784impl_from!(
785    CssConicGradientParseError<'a>,
786    CssBackgroundParseError::ConicGradient
787);
788
789#[derive(Debug, Clone, PartialEq)]
790#[repr(C, u8)]
791pub enum CssBackgroundParseErrorOwned {
792    Error(AzString),
793    InvalidBackground(ParenthesisParseErrorOwned),
794    UnclosedGradient(AzString),
795    NoDirection(AzString),
796    TooFewGradientStops(AzString),
797    DirectionParseError(CssDirectionParseErrorOwned),
798    GradientParseError(CssGradientStopParseErrorOwned),
799    ConicGradient(CssConicGradientParseErrorOwned),
800    ShapeParseError(CssShapeParseErrorOwned),
801    ImageParseError(CssImageParseErrorOwned),
802    ColorParseError(CssColorParseErrorOwned),
803}
804
805impl CssBackgroundParseError<'_> {
806    #[must_use] pub fn to_contained(&self) -> CssBackgroundParseErrorOwned {
807        match self {
808            Self::Error(s) => CssBackgroundParseErrorOwned::Error((*s).to_string().into()),
809            Self::InvalidBackground(e) => {
810                CssBackgroundParseErrorOwned::InvalidBackground(e.to_contained())
811            }
812            Self::UnclosedGradient(s) => {
813                CssBackgroundParseErrorOwned::UnclosedGradient((*s).to_string().into())
814            }
815            Self::NoDirection(s) => CssBackgroundParseErrorOwned::NoDirection((*s).to_string().into()),
816            Self::TooFewGradientStops(s) => {
817                CssBackgroundParseErrorOwned::TooFewGradientStops((*s).to_string().into())
818            }
819            Self::DirectionParseError(e) => {
820                CssBackgroundParseErrorOwned::DirectionParseError(e.to_contained())
821            }
822            Self::GradientParseError(e) => {
823                CssBackgroundParseErrorOwned::GradientParseError(e.to_contained())
824            }
825            Self::ConicGradient(e) => CssBackgroundParseErrorOwned::ConicGradient(e.to_contained()),
826            Self::ShapeParseError(e) => {
827                CssBackgroundParseErrorOwned::ShapeParseError(e.to_contained())
828            }
829            Self::ImageParseError(e) => {
830                CssBackgroundParseErrorOwned::ImageParseError(e.to_contained())
831            }
832            Self::ColorParseError(e) => {
833                CssBackgroundParseErrorOwned::ColorParseError(e.to_contained())
834            }
835        }
836    }
837}
838
839impl CssBackgroundParseErrorOwned {
840    #[must_use] pub fn to_shared(&self) -> CssBackgroundParseError<'_> {
841        match self {
842            Self::Error(s) => CssBackgroundParseError::Error(s),
843            Self::InvalidBackground(e) => CssBackgroundParseError::InvalidBackground(e.to_shared()),
844            Self::UnclosedGradient(s) => CssBackgroundParseError::UnclosedGradient(s),
845            Self::NoDirection(s) => CssBackgroundParseError::NoDirection(s),
846            Self::TooFewGradientStops(s) => CssBackgroundParseError::TooFewGradientStops(s),
847            Self::DirectionParseError(e) => {
848                CssBackgroundParseError::DirectionParseError(e.to_shared())
849            }
850            Self::GradientParseError(e) => {
851                CssBackgroundParseError::GradientParseError(e.to_shared())
852            }
853            Self::ConicGradient(e) => CssBackgroundParseError::ConicGradient(e.to_shared()),
854            Self::ShapeParseError(e) => CssBackgroundParseError::ShapeParseError(e.to_shared()),
855            Self::ImageParseError(e) => CssBackgroundParseError::ImageParseError(e.to_shared()),
856            Self::ColorParseError(e) => CssBackgroundParseError::ColorParseError(e.to_shared()),
857        }
858    }
859}
860
861#[derive(Clone, PartialEq)]
862pub enum CssGradientStopParseError<'a> {
863    Error(&'a str),
864    Percentage(PercentageParseError),
865    Angle(CssAngleValueParseError<'a>),
866    ColorParseError(CssColorParseError<'a>),
867}
868
869impl_debug_as_display!(CssGradientStopParseError<'a>);
870impl_display! { CssGradientStopParseError<'a>, {
871    Error(e) => e,
872    Percentage(e) => format!("Failed to parse offset percentage: {}", e),
873    Angle(e) => format!("Failed to parse angle: {}", e),
874    ColorParseError(e) => format!("{}", e),
875}}
876#[cfg(feature = "parser")]
877impl_from!(
878    CssColorParseError<'a>,
879    CssGradientStopParseError::ColorParseError
880);
881
882#[derive(Debug, Clone, PartialEq)]
883#[repr(C, u8)]
884pub enum CssGradientStopParseErrorOwned {
885    Error(AzString),
886    Percentage(PercentageParseErrorOwned),
887    Angle(CssAngleValueParseErrorOwned),
888    ColorParseError(CssColorParseErrorOwned),
889}
890
891impl CssGradientStopParseError<'_> {
892    #[must_use] pub fn to_contained(&self) -> CssGradientStopParseErrorOwned {
893        match self {
894            Self::Error(s) => CssGradientStopParseErrorOwned::Error((*s).to_string().into()),
895            Self::Percentage(e) => CssGradientStopParseErrorOwned::Percentage(e.to_contained()),
896            Self::Angle(e) => CssGradientStopParseErrorOwned::Angle(e.to_contained()),
897            Self::ColorParseError(e) => {
898                CssGradientStopParseErrorOwned::ColorParseError(e.to_contained())
899            }
900        }
901    }
902}
903
904impl CssGradientStopParseErrorOwned {
905    #[must_use] pub fn to_shared(&self) -> CssGradientStopParseError<'_> {
906        match self {
907            Self::Error(s) => CssGradientStopParseError::Error(s),
908            Self::Percentage(e) => CssGradientStopParseError::Percentage(e.to_shared()),
909            Self::Angle(e) => CssGradientStopParseError::Angle(e.to_shared()),
910            Self::ColorParseError(e) => CssGradientStopParseError::ColorParseError(e.to_shared()),
911        }
912    }
913}
914
915#[derive(Clone, PartialEq, Eq)]
916pub enum CssConicGradientParseError<'a> {
917    Position(CssBackgroundPositionParseError<'a>),
918    Angle(CssAngleValueParseError<'a>),
919    NoAngle(&'a str),
920}
921impl_debug_as_display!(CssConicGradientParseError<'a>);
922impl_display! { CssConicGradientParseError<'a>, {
923    Position(val) => format!("Invalid position attribute: \"{}\"", val),
924    Angle(val) => format!("Invalid angle value: \"{}\"", val),
925    NoAngle(val) => format!("Expected angle: \"{}\"", val),
926}}
927#[cfg(feature = "parser")]
928impl_from!(
929    CssAngleValueParseError<'a>,
930    CssConicGradientParseError::Angle
931);
932#[cfg(feature = "parser")]
933impl_from!(
934    CssBackgroundPositionParseError<'a>,
935    CssConicGradientParseError::Position
936);
937
938#[derive(Debug, Clone, PartialEq, Eq)]
939#[repr(C, u8)]
940pub enum CssConicGradientParseErrorOwned {
941    Position(CssBackgroundPositionParseErrorOwned),
942    Angle(CssAngleValueParseErrorOwned),
943    NoAngle(AzString),
944}
945impl CssConicGradientParseError<'_> {
946    #[must_use] pub fn to_contained(&self) -> CssConicGradientParseErrorOwned {
947        match self {
948            Self::Position(e) => CssConicGradientParseErrorOwned::Position(e.to_contained()),
949            Self::Angle(e) => CssConicGradientParseErrorOwned::Angle(e.to_contained()),
950            Self::NoAngle(s) => CssConicGradientParseErrorOwned::NoAngle((*s).to_string().into()),
951        }
952    }
953}
954impl CssConicGradientParseErrorOwned {
955    #[must_use] pub fn to_shared(&self) -> CssConicGradientParseError<'_> {
956        match self {
957            Self::Position(e) => CssConicGradientParseError::Position(e.to_shared()),
958            Self::Angle(e) => CssConicGradientParseError::Angle(e.to_shared()),
959            Self::NoAngle(s) => CssConicGradientParseError::NoAngle(s),
960        }
961    }
962}
963
964#[derive(Debug, Copy, Clone, PartialEq, Eq)]
965pub enum CssShapeParseError<'a> {
966    ShapeErr(InvalidValueErr<'a>),
967}
968impl_display! {CssShapeParseError<'a>, {
969    ShapeErr(e) => format!("\"{}\"", e.0),
970}}
971#[derive(Debug, Clone, PartialEq, Eq)]
972#[repr(C, u8)]
973pub enum CssShapeParseErrorOwned {
974    ShapeErr(InvalidValueErrOwned),
975}
976impl CssShapeParseError<'_> {
977    #[must_use] pub fn to_contained(&self) -> CssShapeParseErrorOwned {
978        match self {
979            Self::ShapeErr(err) => CssShapeParseErrorOwned::ShapeErr(err.to_contained()),
980        }
981    }
982}
983impl CssShapeParseErrorOwned {
984    #[must_use] pub fn to_shared(&self) -> CssShapeParseError<'_> {
985        match self {
986            Self::ShapeErr(err) => CssShapeParseError::ShapeErr(err.to_shared()),
987        }
988    }
989}
990
991#[derive(Debug, Clone, PartialEq, Eq)]
992pub enum CssBackgroundPositionParseError<'a> {
993    NoPosition(&'a str),
994    TooManyComponents(&'a str),
995    FirstComponentWrong(CssPixelValueParseError<'a>),
996    SecondComponentWrong(CssPixelValueParseError<'a>),
997}
998
999impl_display! {CssBackgroundPositionParseError<'a>, {
1000    NoPosition(e) => format!("First background position missing: \"{}\"", e),
1001    TooManyComponents(e) => format!("background-position can only have one or two components, not more: \"{}\"", e),
1002    FirstComponentWrong(e) => format!("Failed to parse first component: \"{}\"", e),
1003    SecondComponentWrong(e) => format!("Failed to parse second component: \"{}\"", e),
1004}}
1005#[derive(Debug, Clone, PartialEq, Eq)]
1006#[repr(C, u8)]
1007pub enum CssBackgroundPositionParseErrorOwned {
1008    NoPosition(AzString),
1009    TooManyComponents(AzString),
1010    FirstComponentWrong(CssPixelValueParseErrorOwned),
1011    SecondComponentWrong(CssPixelValueParseErrorOwned),
1012}
1013impl CssBackgroundPositionParseError<'_> {
1014    #[must_use] pub fn to_contained(&self) -> CssBackgroundPositionParseErrorOwned {
1015        match self {
1016            Self::NoPosition(s) => CssBackgroundPositionParseErrorOwned::NoPosition((*s).to_string().into()),
1017            Self::TooManyComponents(s) => {
1018                CssBackgroundPositionParseErrorOwned::TooManyComponents((*s).to_string().into())
1019            }
1020            Self::FirstComponentWrong(e) => {
1021                CssBackgroundPositionParseErrorOwned::FirstComponentWrong(e.to_contained())
1022            }
1023            Self::SecondComponentWrong(e) => {
1024                CssBackgroundPositionParseErrorOwned::SecondComponentWrong(e.to_contained())
1025            }
1026        }
1027    }
1028}
1029impl CssBackgroundPositionParseErrorOwned {
1030    #[must_use] pub fn to_shared(&self) -> CssBackgroundPositionParseError<'_> {
1031        match self {
1032            Self::NoPosition(s) => CssBackgroundPositionParseError::NoPosition(s),
1033            Self::TooManyComponents(s) => CssBackgroundPositionParseError::TooManyComponents(s),
1034            Self::FirstComponentWrong(e) => {
1035                CssBackgroundPositionParseError::FirstComponentWrong(e.to_shared())
1036            }
1037            Self::SecondComponentWrong(e) => {
1038                CssBackgroundPositionParseError::SecondComponentWrong(e.to_shared())
1039            }
1040        }
1041    }
1042}
1043
1044// --- PARSERS ---
1045
1046#[cfg(feature = "parser")]
1047pub mod parser {
1048    #[allow(clippy::wildcard_imports)] // parser submodule reuses the parent module's value types
1049    use super::*;
1050
1051    // the `*Gradient` suffix mirrors the CSS gradient function names this enum
1052    // parses (linear-gradient, radial-gradient, conic-gradient, …).
1053    #[allow(clippy::enum_variant_names)]
1054    #[derive(Debug, Copy, Clone, PartialEq, Eq)]
1055    enum GradientType {
1056        LinearGradient,
1057        RepeatingLinearGradient,
1058        RadialGradient,
1059        RepeatingRadialGradient,
1060        ConicGradient,
1061        RepeatingConicGradient,
1062    }
1063
1064    impl GradientType {
1065        pub(crate) const fn get_extend_mode(self) -> ExtendMode {
1066            match self {
1067                Self::LinearGradient | Self::RadialGradient | Self::ConicGradient => {
1068                    ExtendMode::Clamp
1069                }
1070                Self::RepeatingLinearGradient
1071                | Self::RepeatingRadialGradient
1072                | Self::RepeatingConicGradient => ExtendMode::Repeat,
1073            }
1074        }
1075    }
1076
1077    // -- Top-level Parsers for background-* properties --
1078
1079    /// Parses multiple backgrounds, such as "linear-gradient(red, green), url(image.png)".
1080    /// # Errors
1081    ///
1082    /// Returns an error if `input` is not a valid CSS `background-content-multiple` value.
1083    pub fn parse_style_background_content_multiple(
1084        input: &str,
1085    ) -> Result<StyleBackgroundContentVec, CssBackgroundParseError<'_>> {
1086        Ok(split_string_respect_comma(input)
1087            .iter()
1088            .map(|i| parse_style_background_content(i))
1089            .collect::<Result<Vec<_>, _>>()?
1090            .into())
1091    }
1092
1093    /// Parses a single background value, which can be a color, image, or gradient.
1094    /// # Errors
1095    ///
1096    /// Returns an error if `input` is not a valid CSS `background-content` value.
1097    pub fn parse_style_background_content(
1098        input: &str,
1099    ) -> Result<StyleBackgroundContent, CssBackgroundParseError<'_>> {
1100        match parse_parentheses(
1101            input,
1102            &[
1103                "linear-gradient",
1104                "repeating-linear-gradient",
1105                "radial-gradient",
1106                "repeating-radial-gradient",
1107                "conic-gradient",
1108                "repeating-conic-gradient",
1109                "image",
1110                "url",
1111            ],
1112        ) {
1113            Ok((background_type, brace_contents)) => {
1114                let gradient_type = match background_type {
1115                    "linear-gradient" => GradientType::LinearGradient,
1116                    "repeating-linear-gradient" => GradientType::RepeatingLinearGradient,
1117                    "radial-gradient" => GradientType::RadialGradient,
1118                    "repeating-radial-gradient" => GradientType::RepeatingRadialGradient,
1119                    "conic-gradient" => GradientType::ConicGradient,
1120                    "repeating-conic-gradient" => GradientType::RepeatingConicGradient,
1121                    "image" | "url" => {
1122                        return Ok(StyleBackgroundContent::Image(
1123                            parse_image(brace_contents)?,
1124                        ))
1125                    }
1126                    _ => unreachable!(),
1127                };
1128                parse_gradient(brace_contents, gradient_type)
1129            }
1130            // A bare `background:` value is a color. Accept system colors here too
1131            // (`system:accent`, `system:text`, ...), matching the gradient color stops
1132            // (which use `parse_color_or_system`). System colors stay unresolved and are
1133            // theme-resolved at render time. `parse_color_or_system` is a superset of
1134            // `parse_css_color`, so ordinary colors keep parsing exactly as before.
1135            Err(_) => Ok(match parse_color_or_system(input)? {
1136                ColorOrSystem::Color(c) => StyleBackgroundContent::Color(c),
1137                ColorOrSystem::System(s) => StyleBackgroundContent::SystemColor(s),
1138            }),
1139        }
1140    }
1141
1142    /// Parses multiple `background-position` values.
1143    /// # Errors
1144    ///
1145    /// Returns an error if `input` is not a valid CSS `background-position-multiple` value.
1146    pub fn parse_style_background_position_multiple(
1147        input: &str,
1148    ) -> Result<StyleBackgroundPositionVec, CssBackgroundPositionParseError<'_>> {
1149        Ok(split_string_respect_comma(input)
1150            .iter()
1151            .map(|i| parse_style_background_position(i))
1152            .collect::<Result<Vec<_>, _>>()?
1153            .into())
1154    }
1155
1156    /// Parses a single `background-position` value.
1157    /// # Errors
1158    ///
1159    /// Returns an error if `input` is not a valid CSS `background-position` value.
1160    pub fn parse_style_background_position(
1161        input: &str,
1162    ) -> Result<StyleBackgroundPosition, CssBackgroundPositionParseError<'_>> {
1163        let input = input.trim();
1164        let mut whitespace_iter = input.split_whitespace();
1165
1166        let first = whitespace_iter
1167            .next()
1168            .ok_or(CssBackgroundPositionParseError::NoPosition(input))?;
1169        let second = whitespace_iter.next();
1170
1171        if whitespace_iter.next().is_some() {
1172            return Err(CssBackgroundPositionParseError::TooManyComponents(input));
1173        }
1174
1175        // Try to parse as horizontal first, if that fails, maybe it's a vertical keyword
1176        if let Ok(horizontal) = parse_background_position_horizontal(first) {
1177            let vertical = match second {
1178                Some(s) => parse_background_position_vertical(s)
1179                    .map_err(CssBackgroundPositionParseError::SecondComponentWrong)?,
1180                None => BackgroundPositionVertical::Center,
1181            };
1182            return Ok(StyleBackgroundPosition {
1183                horizontal,
1184                vertical,
1185            });
1186        }
1187
1188        // If the first part wasn't a horizontal keyword, maybe it's a vertical one
1189        if let Ok(vertical) = parse_background_position_vertical(first) {
1190            let horizontal = match second {
1191                Some(s) => parse_background_position_horizontal(s)
1192                    .map_err(CssBackgroundPositionParseError::FirstComponentWrong)?,
1193                None => BackgroundPositionHorizontal::Center,
1194            };
1195            return Ok(StyleBackgroundPosition {
1196                horizontal,
1197                vertical,
1198            });
1199        }
1200
1201        Err(CssBackgroundPositionParseError::FirstComponentWrong(
1202            CssPixelValueParseError::InvalidPixelValue(first),
1203        ))
1204    }
1205
1206    /// Parses multiple `background-size` values.
1207    /// # Errors
1208    ///
1209    /// Returns an error if `input` is not a valid CSS `background-size-multiple` value.
1210    pub fn parse_style_background_size_multiple(
1211        input: &str,
1212    ) -> Result<StyleBackgroundSizeVec, InvalidValueErr<'_>> {
1213        Ok(split_string_respect_comma(input)
1214            .iter()
1215            .map(|i| parse_style_background_size(i))
1216            .collect::<Result<Vec<_>, _>>()?
1217            .into())
1218    }
1219
1220    /// Parses a single `background-size` value.
1221    /// # Errors
1222    ///
1223    /// Returns an error if `input` is not a valid CSS `background-size` value.
1224    pub fn parse_style_background_size(
1225        input: &str,
1226    ) -> Result<StyleBackgroundSize, InvalidValueErr<'_>> {
1227        let input = input.trim();
1228        match input {
1229            "contain" => Ok(StyleBackgroundSize::Contain),
1230            "cover" => Ok(StyleBackgroundSize::Cover),
1231            other => {
1232                let mut iter = other.split_whitespace();
1233                let x_val = iter.next().ok_or(InvalidValueErr(input))?;
1234                let x_pos = parse_pixel_value(x_val).map_err(|_| InvalidValueErr(input))?;
1235                let y_pos = match iter.next() {
1236                    Some(y_val) => parse_pixel_value(y_val).map_err(|_| InvalidValueErr(input))?,
1237                    None => x_pos, // If only one value, it applies to both width and height
1238                };
1239                Ok(StyleBackgroundSize::ExactSize(PixelValueSize {
1240                    width: x_pos,
1241                    height: y_pos,
1242                }))
1243            }
1244        }
1245    }
1246
1247    /// Parses multiple `background-repeat` values.
1248    /// # Errors
1249    ///
1250    /// Returns an error if `input` is not a valid CSS `background-repeat-multiple` value.
1251    pub fn parse_style_background_repeat_multiple(
1252        input: &str,
1253    ) -> Result<StyleBackgroundRepeatVec, InvalidValueErr<'_>> {
1254        Ok(split_string_respect_comma(input)
1255            .iter()
1256            .map(|i| parse_style_background_repeat(i))
1257            .collect::<Result<Vec<_>, _>>()?
1258            .into())
1259    }
1260
1261    /// Parses a single `background-repeat` value.
1262    /// # Errors
1263    ///
1264    /// Returns an error if `input` is not a valid CSS `background-repeat` value.
1265    pub fn parse_style_background_repeat(
1266        input: &str,
1267    ) -> Result<StyleBackgroundRepeat, InvalidValueErr<'_>> {
1268        match input.trim() {
1269            "no-repeat" => Ok(StyleBackgroundRepeat::NoRepeat),
1270            "repeat" => Ok(StyleBackgroundRepeat::PatternRepeat),
1271            "repeat-x" => Ok(StyleBackgroundRepeat::RepeatX),
1272            "repeat-y" => Ok(StyleBackgroundRepeat::RepeatY),
1273            _ => Err(InvalidValueErr(input)),
1274        }
1275    }
1276
1277    // -- Gradient Parsing Logic --
1278
1279    /// Parses the contents of a gradient function.
1280    fn parse_gradient(
1281        input: &str,
1282        gradient_type: GradientType,
1283    ) -> Result<StyleBackgroundContent, CssBackgroundParseError<'_>> {
1284        let input = input.trim();
1285        let comma_separated_items = split_string_respect_comma(input);
1286        let mut brace_iterator = comma_separated_items.iter();
1287        let first_brace_item = brace_iterator
1288            .next()
1289            .ok_or(CssBackgroundParseError::NoDirection(input))?;
1290
1291        match gradient_type {
1292            GradientType::LinearGradient | GradientType::RepeatingLinearGradient => {
1293                let mut linear_gradient = LinearGradient {
1294                    extend_mode: gradient_type.get_extend_mode(),
1295                    ..Default::default()
1296                };
1297                let mut linear_stops = Vec::new();
1298
1299                if let Ok(dir) = parse_direction(first_brace_item) {
1300                    linear_gradient.direction = dir;
1301                } else {
1302                    linear_stops.push(parse_linear_color_stop(first_brace_item)?);
1303                }
1304
1305                for item in brace_iterator {
1306                    linear_stops.push(parse_linear_color_stop(item)?);
1307                }
1308
1309                linear_gradient.stops = get_normalized_linear_stops(&linear_stops).into();
1310                Ok(StyleBackgroundContent::LinearGradient(linear_gradient))
1311            }
1312            GradientType::RadialGradient | GradientType::RepeatingRadialGradient => {
1313                // Simplified parsing: assumes shape/size/position come first, then stops.
1314                // A more robust parser would handle them in any order.
1315                let mut radial_gradient = RadialGradient {
1316                    extend_mode: gradient_type.get_extend_mode(),
1317                    ..Default::default()
1318                };
1319                let mut radial_stops = Vec::new();
1320                let mut current_item = *first_brace_item;
1321                let mut items_consumed = false;
1322
1323                // Greedily consume shape, size, position keywords
1324                loop {
1325                    let mut consumed_in_iteration = false;
1326                    let mut temp_iter = current_item.split_whitespace();
1327                    for word in temp_iter {
1328                        if let Ok(shape) = parse_shape(word) {
1329                            radial_gradient.shape = shape;
1330                            consumed_in_iteration = true;
1331                        } else if let Ok(size) = parse_radial_gradient_size(word) {
1332                            radial_gradient.size = size;
1333                            consumed_in_iteration = true;
1334                        } else if let Ok(pos) = parse_style_background_position(current_item) {
1335                            radial_gradient.position = pos;
1336                            consumed_in_iteration = true;
1337                            break; // position can have multiple words, so consume the rest of the
1338                                   // item
1339                        }
1340                    }
1341                    if consumed_in_iteration {
1342                        if let Some(next_item) = brace_iterator.next() {
1343                            current_item = next_item;
1344                            items_consumed = true;
1345                        } else {
1346                            break;
1347                        }
1348                    } else {
1349                        break;
1350                    }
1351                }
1352
1353                if items_consumed || parse_linear_color_stop(current_item).is_ok() {
1354                    radial_stops.push(parse_linear_color_stop(current_item)?);
1355                }
1356
1357                for item in brace_iterator {
1358                    radial_stops.push(parse_linear_color_stop(item)?);
1359                }
1360
1361                radial_gradient.stops = get_normalized_linear_stops(&radial_stops).into();
1362                Ok(StyleBackgroundContent::RadialGradient(radial_gradient))
1363            }
1364            GradientType::ConicGradient | GradientType::RepeatingConicGradient => {
1365                let mut conic_gradient = ConicGradient {
1366                    extend_mode: gradient_type.get_extend_mode(),
1367                    ..Default::default()
1368                };
1369                let mut conic_stops = Vec::new();
1370
1371                if let Some((angle, center)) = parse_conic_first_item(first_brace_item)? {
1372                    conic_gradient.angle = angle;
1373                    conic_gradient.center = center;
1374                } else {
1375                    conic_stops.push(parse_radial_color_stop(first_brace_item)?);
1376                }
1377
1378                for item in brace_iterator {
1379                    conic_stops.push(parse_radial_color_stop(item)?);
1380                }
1381
1382                conic_gradient.stops = get_normalized_radial_stops(&conic_stops).into();
1383                Ok(StyleBackgroundContent::ConicGradient(conic_gradient))
1384            }
1385        }
1386    }
1387
1388    // -- Gradient Parsing Helpers --
1389
1390    /// Parses color stops per W3C CSS Images Level 3:
1391    /// - "red" (no position)
1392    /// - "red 5%" (one position)
1393    /// - "red 10% 30%" (two positions - creates a hard color band)
1394    /// 
1395    /// Also supports system colors like `system:accent 50%` for theme-aware gradients.
1396    fn parse_linear_color_stop(
1397        input: &str,
1398    ) -> Result<LinearColorStop, CssGradientStopParseError<'_>> {
1399        let input = input.trim();
1400        let (color_str, offset1_str, offset2_str) = split_color_and_offsets(input);
1401
1402        let color = parse_color_or_system(color_str)?;
1403        let offset1 = match offset1_str {
1404            None => OptionPercentageValue::None,
1405            Some(s) => OptionPercentageValue::Some(
1406                parse_percentage_value(s).map_err(CssGradientStopParseError::Percentage)?,
1407            ),
1408        };
1409        let offset2 = match offset2_str {
1410            None => OptionPercentageValue::None,
1411            Some(s) => OptionPercentageValue::Some(
1412                parse_percentage_value(s).map_err(CssGradientStopParseError::Percentage)?,
1413            ),
1414        };
1415
1416        Ok(LinearColorStop {
1417            color,
1418            offset1,
1419            offset2,
1420        })
1421    }
1422
1423    /// Parses color stops per W3C CSS Images Level 3:
1424    /// - "red" (no position)
1425    /// - "red 90deg" (one position)
1426    /// - "red 45deg 90deg" (two positions - creates a hard color band)
1427    /// 
1428    /// Also supports system colors like `system:accent 90deg` for theme-aware gradients.
1429    fn parse_radial_color_stop(
1430        input: &str,
1431    ) -> Result<RadialColorStop, CssGradientStopParseError<'_>> {
1432        let input = input.trim();
1433        let (color_str, offset1_str, offset2_str) = split_color_and_offsets(input);
1434
1435        let color = parse_color_or_system(color_str)?;
1436        let offset1 = match offset1_str {
1437            None => OptionAngleValue::None,
1438            Some(s) => OptionAngleValue::Some(
1439                parse_angle_value(s).map_err(CssGradientStopParseError::Angle)?,
1440            ),
1441        };
1442        let offset2 = match offset2_str {
1443            None => OptionAngleValue::None,
1444            Some(s) => OptionAngleValue::Some(
1445                parse_angle_value(s).map_err(CssGradientStopParseError::Angle)?,
1446            ),
1447        };
1448
1449        Ok(RadialColorStop {
1450            color,
1451            offset1,
1452            offset2,
1453        })
1454    }
1455
1456    /// Helper to robustly split a string like "rgba(0,0,0,0.5) 10% 30%" into color and offset
1457    /// parts. Returns (`color_str`, offset1, offset2) where offsets are optional.
1458    ///
1459    /// Per W3C CSS Images Level 3, a color stop can have 0, 1, or 2 positions:
1460    /// - "red" -> ("red", None, None)
1461    /// - "red 50%" -> ("red", Some("50%"), None)
1462    /// - "red 10% 30%" -> ("red", Some("10%"), Some("30%"))
1463    fn split_color_and_offsets(input: &str) -> (&str, Option<&str>, Option<&str>) {
1464        // Strategy: scan from the end to find position values (contain digits + % or unit).
1465        // We need to handle complex colors like "rgba(0, 0, 0, 0.5)" that contain spaces and
1466        // digits.
1467
1468        let input = input.trim();
1469
1470        // Try to find the last position value (might be second of two)
1471        if let Some((remaining, last_offset)) = try_split_last_offset(input) {
1472            // Try to find another position value before it
1473            if let Some((color_part, first_offset)) = try_split_last_offset(remaining) {
1474                return (color_part.trim(), Some(first_offset), Some(last_offset));
1475            }
1476            return (remaining.trim(), Some(last_offset), None);
1477        }
1478
1479        (input, None, None)
1480    }
1481
1482    /// Try to split off the last whitespace-separated token if it looks like a position value.
1483    /// Returns (remaining, `offset_str`) if successful.
1484    fn try_split_last_offset(input: &str) -> Option<(&str, &str)> {
1485        let input = input.trim();
1486        if let Some(last_ws_idx) = input.rfind(char::is_whitespace) {
1487            let (potential_color, potential_offset) = input.split_at(last_ws_idx);
1488            let potential_offset = potential_offset.trim();
1489
1490            // A valid offset must contain a digit and typically ends with % or a unit
1491            // This avoids misinterpreting "to right bottom" as containing offsets
1492            if is_likely_offset(potential_offset) {
1493                return Some((potential_color, potential_offset));
1494            }
1495        }
1496        None
1497    }
1498
1499    /// Check if a string looks like a position value (percentage or length).
1500    /// Must contain a digit and typically ends with %, px, em, etc.
1501    fn is_likely_offset(s: &str) -> bool {
1502        if !s.contains(|c: char| c.is_ascii_digit()) {
1503            return false;
1504        }
1505        // Check if it ends with a known unit or %
1506        let units = [
1507            "%", "px", "em", "rem", "ex", "ch", "vw", "vh", "vmin", "vmax", "cm", "mm", "in", "pt",
1508            "pc", "deg", "rad", "grad", "turn",
1509        ];
1510        units.iter().any(|u| s.ends_with(u))
1511    }
1512
1513    /// Parses the `from <angle> at <position>` part of a conic gradient.
1514    fn parse_conic_first_item(
1515        input: &str,
1516    ) -> Result<Option<(AngleValue, StyleBackgroundPosition)>, CssConicGradientParseError<'_>> {
1517        let input = input.trim();
1518        if !input.starts_with("from") {
1519            return Ok(None);
1520        }
1521
1522        let mut parts = input["from".len()..].trim().split("at");
1523        let angle_part = parts
1524            .next()
1525            .ok_or(CssConicGradientParseError::NoAngle(input))?
1526            .trim();
1527        let angle = parse_angle_value(angle_part)?;
1528
1529        let position = match parts.next() {
1530            Some(pos_part) => parse_style_background_position(pos_part.trim())?,
1531            None => StyleBackgroundPosition::default(),
1532        };
1533
1534        Ok(Some((angle, position)))
1535    }
1536
1537    // -- Normalization Functions --
1538
1539    macro_rules! impl_get_normalized_stops {
1540        (
1541            fn $fn_name:ident($input_stop:ty) -> Vec<$output_stop:ident>,
1542            pos_type = $pos_ty:ty,
1543            default_start = $default_start:expr,
1544            default_end = $default_end:expr,
1545            pos_ctor = $pos_ctor:expr,
1546            pos_to_f32 = $pos_to_f32:expr,
1547            output_field = $out_field:ident,
1548        ) => {
1549            #[allow(clippy::suboptimal_flops)] // explicit FP; mul_add slower without +fma
1550            fn $fn_name(stops: &[$input_stop]) -> Vec<$output_stop> {
1551                if stops.is_empty() {
1552                    return Vec::new();
1553                }
1554
1555                let mut expanded: Vec<(ColorOrSystem, Option<$pos_ty>)> = Vec::new();
1556
1557                for stop in stops {
1558                    match (stop.offset1.into_option(), stop.offset2.into_option()) {
1559                        (None, _) => {
1560                            expanded.push((stop.color, None));
1561                        }
1562                        (Some(pos1), None) => {
1563                            expanded.push((stop.color, Some(pos1)));
1564                        }
1565                        (Some(pos1), Some(pos2)) => {
1566                            expanded.push((stop.color, Some(pos1)));
1567                            expanded.push((stop.color, Some(pos2)));
1568                        }
1569                    }
1570                }
1571
1572                if expanded.is_empty() {
1573                    return Vec::new();
1574                }
1575
1576                let pos_ctor: fn(f32) -> $pos_ty = $pos_ctor;
1577                let pos_to_f32: fn(&$pos_ty) -> f32 = $pos_to_f32;
1578
1579                if expanded[0].1.is_none() {
1580                    expanded[0].1 = Some(pos_ctor($default_start));
1581                }
1582                let last_idx = expanded.len() - 1;
1583                if expanded[last_idx].1.is_none() {
1584                    expanded[last_idx].1 = Some(pos_ctor($default_end));
1585                }
1586
1587                let mut max_so_far: f32 = 0.0;
1588                for (_, pos) in expanded.iter_mut() {
1589                    if let Some(p) = pos {
1590                        let val = pos_to_f32(p);
1591                        if val < max_so_far {
1592                            *p = pos_ctor(max_so_far);
1593                        } else {
1594                            max_so_far = val;
1595                        }
1596                    }
1597                }
1598
1599                let mut i = 0;
1600                while i < expanded.len() {
1601                    if expanded[i].1.is_none() {
1602                        let run_start = i;
1603                        let mut run_end = i;
1604                        while run_end < expanded.len() && expanded[run_end].1.is_none() {
1605                            run_end += 1;
1606                        }
1607
1608                        let prev_pos = if run_start > 0 {
1609                            pos_to_f32(&expanded[run_start - 1].1.unwrap())
1610                        } else {
1611                            $default_start
1612                        };
1613
1614                        let next_pos = if run_end < expanded.len() {
1615                            pos_to_f32(&expanded[run_end].1.unwrap())
1616                        } else {
1617                            $default_end
1618                        };
1619
1620                        let run_len = run_end - run_start;
1621                        let step = (next_pos - prev_pos) / crate::cast::usize_to_f32(run_len + 1);
1622
1623                        for j in 0..run_len {
1624                            expanded[run_start + j].1 =
1625                                Some(pos_ctor(prev_pos + step * crate::cast::usize_to_f32(j + 1)));
1626                        }
1627
1628                        i = run_end;
1629                    } else {
1630                        i += 1;
1631                    }
1632                }
1633
1634                expanded
1635                    .into_iter()
1636                    .map(|(color, pos)| {
1637                        $output_stop {
1638                            $out_field: pos.unwrap_or(pos_ctor($default_start)),
1639                            color,
1640                        }
1641                    })
1642                    .collect()
1643            }
1644        };
1645    }
1646
1647    impl_get_normalized_stops! {
1648        fn get_normalized_linear_stops(LinearColorStop) -> Vec<NormalizedLinearColorStop>,
1649        pos_type = PercentageValue,
1650        default_start = 0.0,
1651        default_end = 100.0,
1652        pos_ctor = (|v| PercentageValue::new(v)),
1653        pos_to_f32 = (|p: &PercentageValue| p.normalized() * 100.0),
1654        output_field = offset,
1655    }
1656
1657    impl_get_normalized_stops! {
1658        fn get_normalized_radial_stops(RadialColorStop) -> Vec<NormalizedRadialColorStop>,
1659        pos_type = AngleValue,
1660        default_start = 0.0,
1661        default_end = 360.0,
1662        pos_ctor = (|v| AngleValue::deg(v)),
1663        pos_to_f32 = (|p: &AngleValue| p.to_degrees_raw()),
1664        output_field = angle,
1665    }
1666
1667    // -- Other Background Helpers --
1668
1669    fn parse_background_position_horizontal(
1670        input: &str,
1671    ) -> Result<BackgroundPositionHorizontal, CssPixelValueParseError<'_>> {
1672        Ok(match input {
1673            "left" => BackgroundPositionHorizontal::Left,
1674            "center" => BackgroundPositionHorizontal::Center,
1675            "right" => BackgroundPositionHorizontal::Right,
1676            other => BackgroundPositionHorizontal::Exact(parse_pixel_value(other)?),
1677        })
1678    }
1679
1680    fn parse_background_position_vertical(
1681        input: &str,
1682    ) -> Result<BackgroundPositionVertical, CssPixelValueParseError<'_>> {
1683        Ok(match input {
1684            "top" => BackgroundPositionVertical::Top,
1685            "center" => BackgroundPositionVertical::Center,
1686            "bottom" => BackgroundPositionVertical::Bottom,
1687            other => BackgroundPositionVertical::Exact(parse_pixel_value(other)?),
1688        })
1689    }
1690
1691    fn parse_shape(input: &str) -> Result<Shape, CssShapeParseError<'_>> {
1692        match input.trim() {
1693            "circle" => Ok(Shape::Circle),
1694            "ellipse" => Ok(Shape::Ellipse),
1695            _ => Err(CssShapeParseError::ShapeErr(InvalidValueErr(input))),
1696        }
1697    }
1698
1699    fn parse_radial_gradient_size(
1700        input: &str,
1701    ) -> Result<RadialGradientSize, InvalidValueErr<'_>> {
1702        match input.trim() {
1703            "closest-side" => Ok(RadialGradientSize::ClosestSide),
1704            "closest-corner" => Ok(RadialGradientSize::ClosestCorner),
1705            "farthest-side" => Ok(RadialGradientSize::FarthestSide),
1706            "farthest-corner" => Ok(RadialGradientSize::FarthestCorner),
1707            _ => Err(InvalidValueErr(input)),
1708        }
1709    }
1710}
1711
1712#[cfg(feature = "parser")]
1713pub use self::parser::*;
1714
1715#[cfg(all(test, feature = "parser"))]
1716mod tests {
1717    use super::*;
1718    use crate::props::basic::{DirectionCorner, DirectionCorners};
1719
1720    #[test]
1721    fn test_parse_single_background_content() {
1722        // Color
1723        assert_eq!(
1724            parse_style_background_content("red").unwrap(),
1725            StyleBackgroundContent::Color(ColorU::RED)
1726        );
1727        assert_eq!(
1728            parse_style_background_content("#ff00ff").unwrap(),
1729            StyleBackgroundContent::Color(ColorU::new_rgb(255, 0, 255))
1730        );
1731
1732        // Image
1733        assert_eq!(
1734            parse_style_background_content("url(\"image.png\")").unwrap(),
1735            StyleBackgroundContent::Image("image.png".into())
1736        );
1737
1738        // Linear Gradient
1739        let lg = parse_style_background_content("linear-gradient(to right, red, blue)").unwrap();
1740        assert!(matches!(lg, StyleBackgroundContent::LinearGradient(_)));
1741        if let StyleBackgroundContent::LinearGradient(grad) = lg {
1742            assert_eq!(grad.stops.len(), 2);
1743            assert_eq!(
1744                grad.direction,
1745                Direction::FromTo(DirectionCorners {
1746                    dir_from: DirectionCorner::Left,
1747                    dir_to: DirectionCorner::Right
1748                })
1749            );
1750        }
1751
1752        // Radial Gradient
1753        let rg = parse_style_background_content("radial-gradient(circle, white, black)").unwrap();
1754        assert!(matches!(rg, StyleBackgroundContent::RadialGradient(_)));
1755        if let StyleBackgroundContent::RadialGradient(grad) = rg {
1756            assert_eq!(grad.stops.len(), 2);
1757            assert_eq!(grad.shape, Shape::Circle);
1758        }
1759
1760        // Conic Gradient
1761        let cg = parse_style_background_content("conic-gradient(from 90deg, red, blue)").unwrap();
1762        assert!(matches!(cg, StyleBackgroundContent::ConicGradient(_)));
1763        if let StyleBackgroundContent::ConicGradient(grad) = cg {
1764            assert_eq!(grad.stops.len(), 2);
1765            assert_eq!(grad.angle, AngleValue::deg(90.0));
1766        }
1767    }
1768
1769    #[test]
1770    fn test_parse_multiple_background_content() {
1771        let result =
1772            parse_style_background_content_multiple("url(foo.png), linear-gradient(red, blue)")
1773                .unwrap();
1774        assert_eq!(result.len(), 2);
1775        assert!(matches!(
1776            result.as_slice()[0],
1777            StyleBackgroundContent::Image(_)
1778        ));
1779        assert!(matches!(
1780            result.as_slice()[1],
1781            StyleBackgroundContent::LinearGradient(_)
1782        ));
1783    }
1784
1785    #[test]
1786    fn test_parse_background_position() {
1787        // One value
1788        let result = parse_style_background_position("center").unwrap();
1789        assert_eq!(result.horizontal, BackgroundPositionHorizontal::Center);
1790        assert_eq!(result.vertical, BackgroundPositionVertical::Center);
1791
1792        let result = parse_style_background_position("25%").unwrap();
1793        assert_eq!(
1794            result.horizontal,
1795            BackgroundPositionHorizontal::Exact(PixelValue::percent(25.0))
1796        );
1797        assert_eq!(result.vertical, BackgroundPositionVertical::Center);
1798
1799        // Two values
1800        let result = parse_style_background_position("right 50px").unwrap();
1801        assert_eq!(result.horizontal, BackgroundPositionHorizontal::Right);
1802        assert_eq!(
1803            result.vertical,
1804            BackgroundPositionVertical::Exact(PixelValue::px(50.0))
1805        );
1806
1807        // Four values (not supported by this parser, should fail)
1808        assert!(parse_style_background_position("left 10px top 20px").is_err());
1809    }
1810
1811    #[test]
1812    fn test_parse_background_size() {
1813        assert_eq!(
1814            parse_style_background_size("contain").unwrap(),
1815            StyleBackgroundSize::Contain
1816        );
1817        assert_eq!(
1818            parse_style_background_size("cover").unwrap(),
1819            StyleBackgroundSize::Cover
1820        );
1821        assert_eq!(
1822            parse_style_background_size("50%").unwrap(),
1823            StyleBackgroundSize::ExactSize(PixelValueSize {
1824                width: PixelValue::percent(50.0),
1825                height: PixelValue::percent(50.0)
1826            })
1827        );
1828        assert_eq!(
1829            parse_style_background_size("100px 20em").unwrap(),
1830            StyleBackgroundSize::ExactSize(PixelValueSize {
1831                width: PixelValue::px(100.0),
1832                height: PixelValue::em(20.0)
1833            })
1834        );
1835        assert!(parse_style_background_size("auto").is_err());
1836    }
1837
1838    #[test]
1839    fn test_parse_background_repeat() {
1840        assert_eq!(
1841            parse_style_background_repeat("repeat").unwrap(),
1842            StyleBackgroundRepeat::PatternRepeat
1843        );
1844        assert_eq!(
1845            parse_style_background_repeat("repeat-x").unwrap(),
1846            StyleBackgroundRepeat::RepeatX
1847        );
1848        assert_eq!(
1849            parse_style_background_repeat("repeat-y").unwrap(),
1850            StyleBackgroundRepeat::RepeatY
1851        );
1852        assert_eq!(
1853            parse_style_background_repeat("no-repeat").unwrap(),
1854            StyleBackgroundRepeat::NoRepeat
1855        );
1856        assert!(parse_style_background_repeat("repeat-xy").is_err());
1857    }
1858
1859    // =========================================================================
1860    // W3C CSS Images Level 3 - Gradient Parsing Tests
1861    // =========================================================================
1862
1863    #[test]
1864    fn test_gradient_no_position_stops() {
1865        // "linear-gradient(red, blue)" - no positions specified
1866        let lg = parse_style_background_content("linear-gradient(red, blue)").unwrap();
1867        if let StyleBackgroundContent::LinearGradient(grad) = lg {
1868            assert_eq!(grad.stops.len(), 2);
1869            // First stop should default to 0%
1870            assert!((grad.stops.as_ref()[0].offset.normalized() - 0.0).abs() < 0.001);
1871            // Last stop should default to 100%
1872            assert!((grad.stops.as_ref()[1].offset.normalized() - 1.0).abs() < 0.001);
1873        } else {
1874            panic!("Expected LinearGradient");
1875        }
1876    }
1877
1878    #[test]
1879    fn test_gradient_single_position_stops() {
1880        // "linear-gradient(red 25%, blue 75%)" - one position per stop
1881        let lg = parse_style_background_content("linear-gradient(red 25%, blue 75%)").unwrap();
1882        if let StyleBackgroundContent::LinearGradient(grad) = lg {
1883            assert_eq!(grad.stops.len(), 2);
1884            assert!((grad.stops.as_ref()[0].offset.normalized() - 0.25).abs() < 0.001);
1885            assert!((grad.stops.as_ref()[1].offset.normalized() - 0.75).abs() < 0.001);
1886        } else {
1887            panic!("Expected LinearGradient");
1888        }
1889    }
1890
1891    #[test]
1892    fn test_gradient_multi_position_stops() {
1893        // "linear-gradient(red 10% 30%, blue)" - two positions create two stops
1894        let lg = parse_style_background_content("linear-gradient(red 10% 30%, blue)").unwrap();
1895        if let StyleBackgroundContent::LinearGradient(grad) = lg {
1896            // Should have 3 stops: red@10%, red@30%, blue@100%
1897            assert_eq!(grad.stops.len(), 3, "Expected 3 stops for multi-position");
1898            assert!((grad.stops.as_ref()[0].offset.normalized() - 0.10).abs() < 0.001);
1899            assert!((grad.stops.as_ref()[1].offset.normalized() - 0.30).abs() < 0.001);
1900            assert!((grad.stops.as_ref()[2].offset.normalized() - 1.0).abs() < 0.001);
1901            // Both first two stops should have same color (red)
1902            assert_eq!(grad.stops.as_ref()[0].color, grad.stops.as_ref()[1].color);
1903        } else {
1904            panic!("Expected LinearGradient");
1905        }
1906    }
1907
1908    #[test]
1909    fn test_gradient_three_colors_no_positions() {
1910        // "linear-gradient(red, green, blue)" - evenly distributed
1911        let lg = parse_style_background_content("linear-gradient(red, green, blue)").unwrap();
1912        if let StyleBackgroundContent::LinearGradient(grad) = lg {
1913            assert_eq!(grad.stops.len(), 3);
1914            // Positions: 0%, 50%, 100%
1915            assert!((grad.stops.as_ref()[0].offset.normalized() - 0.0).abs() < 0.001);
1916            assert!((grad.stops.as_ref()[1].offset.normalized() - 0.5).abs() < 0.001);
1917            assert!((grad.stops.as_ref()[2].offset.normalized() - 1.0).abs() < 0.001);
1918        } else {
1919            panic!("Expected LinearGradient");
1920        }
1921    }
1922
1923    #[test]
1924    fn test_gradient_fixup_ascending_order() {
1925        // "linear-gradient(red 50%, blue 20%)" - blue position < red position
1926        // W3C says: clamp to max of previous positions
1927        let lg = parse_style_background_content("linear-gradient(red 50%, blue 20%)").unwrap();
1928        if let StyleBackgroundContent::LinearGradient(grad) = lg {
1929            assert_eq!(grad.stops.len(), 2);
1930            // First stop at 50%
1931            assert!((grad.stops.as_ref()[0].offset.normalized() - 0.50).abs() < 0.001);
1932            // Second stop clamped to 50% (not 20%)
1933            assert!((grad.stops.as_ref()[1].offset.normalized() - 0.50).abs() < 0.001);
1934        } else {
1935            panic!("Expected LinearGradient");
1936        }
1937    }
1938
1939    #[test]
1940    fn test_gradient_distribute_unpositioned() {
1941        // "linear-gradient(red 0%, yellow, green, blue 100%)"
1942        // yellow and green should be distributed evenly between 0% and 100%
1943        let lg =
1944            parse_style_background_content("linear-gradient(red 0%, yellow, green, blue 100%)")
1945                .unwrap();
1946        if let StyleBackgroundContent::LinearGradient(grad) = lg {
1947            assert_eq!(grad.stops.len(), 4);
1948            // Positions: 0%, 33.3%, 66.6%, 100%
1949            assert!((grad.stops.as_ref()[0].offset.normalized() - 0.0).abs() < 0.001);
1950            assert!((grad.stops.as_ref()[1].offset.normalized() - 0.333).abs() < 0.01);
1951            assert!((grad.stops.as_ref()[2].offset.normalized() - 0.666).abs() < 0.01);
1952            assert!((grad.stops.as_ref()[3].offset.normalized() - 1.0).abs() < 0.001);
1953        } else {
1954            panic!("Expected LinearGradient");
1955        }
1956    }
1957
1958    #[test]
1959    fn test_gradient_direction_to_corner() {
1960        // "linear-gradient(to top right, red, blue)"
1961        let lg =
1962            parse_style_background_content("linear-gradient(to top right, red, blue)").unwrap();
1963        if let StyleBackgroundContent::LinearGradient(grad) = lg {
1964            assert_eq!(
1965                grad.direction,
1966                Direction::FromTo(DirectionCorners {
1967                    dir_from: DirectionCorner::BottomLeft,
1968                    dir_to: DirectionCorner::TopRight
1969                })
1970            );
1971        } else {
1972            panic!("Expected LinearGradient");
1973        }
1974    }
1975
1976    #[test]
1977    fn test_gradient_direction_angle() {
1978        // "linear-gradient(45deg, red, blue)"
1979        let lg = parse_style_background_content("linear-gradient(45deg, red, blue)").unwrap();
1980        if let StyleBackgroundContent::LinearGradient(grad) = lg {
1981            assert_eq!(grad.direction, Direction::Angle(AngleValue::deg(45.0)));
1982        } else {
1983            panic!("Expected LinearGradient");
1984        }
1985    }
1986
1987    #[test]
1988    fn test_repeating_gradient() {
1989        // "repeating-linear-gradient(red, blue 20%)"
1990        let lg =
1991            parse_style_background_content("repeating-linear-gradient(red, blue 20%)").unwrap();
1992        if let StyleBackgroundContent::LinearGradient(grad) = lg {
1993            assert_eq!(grad.extend_mode, ExtendMode::Repeat);
1994        } else {
1995            panic!("Expected LinearGradient");
1996        }
1997    }
1998
1999    #[test]
2000    fn test_radial_gradient_circle() {
2001        // "radial-gradient(circle, red, blue)"
2002        let rg = parse_style_background_content("radial-gradient(circle, red, blue)").unwrap();
2003        if let StyleBackgroundContent::RadialGradient(grad) = rg {
2004            assert_eq!(grad.shape, Shape::Circle);
2005            assert_eq!(grad.stops.len(), 2);
2006            // Check default position is center
2007            assert_eq!(grad.position.horizontal, BackgroundPositionHorizontal::Left);
2008            assert_eq!(grad.position.vertical, BackgroundPositionVertical::Top);
2009        } else {
2010            panic!("Expected RadialGradient");
2011        }
2012    }
2013
2014    #[test]
2015    fn test_radial_gradient_ellipse() {
2016        // "radial-gradient(ellipse, red, blue)"
2017        let rg = parse_style_background_content("radial-gradient(ellipse, red, blue)").unwrap();
2018        if let StyleBackgroundContent::RadialGradient(grad) = rg {
2019            assert_eq!(grad.shape, Shape::Ellipse);
2020            assert_eq!(grad.stops.len(), 2);
2021        } else {
2022            panic!("Expected RadialGradient");
2023        }
2024    }
2025
2026    #[test]
2027    fn test_radial_gradient_size_keywords() {
2028        // Test different size keywords
2029        let rg = parse_style_background_content("radial-gradient(circle closest-side, red, blue)")
2030            .unwrap();
2031        if let StyleBackgroundContent::RadialGradient(grad) = rg {
2032            assert_eq!(grad.shape, Shape::Circle);
2033            assert_eq!(grad.size, RadialGradientSize::ClosestSide);
2034        } else {
2035            panic!("Expected RadialGradient");
2036        }
2037    }
2038
2039    #[test]
2040    fn test_radial_gradient_stop_positions() {
2041        // "radial-gradient(red 0%, blue 100%)"
2042        let rg = parse_style_background_content("radial-gradient(red 0%, blue 100%)").unwrap();
2043        if let StyleBackgroundContent::RadialGradient(grad) = rg {
2044            assert_eq!(grad.stops.len(), 2);
2045            assert!((grad.stops.as_ref()[0].offset.normalized() - 0.0).abs() < 0.001);
2046            assert!((grad.stops.as_ref()[1].offset.normalized() - 1.0).abs() < 0.001);
2047        } else {
2048            panic!("Expected RadialGradient");
2049        }
2050    }
2051
2052    #[test]
2053    fn test_repeating_radial_gradient() {
2054        let rg = parse_style_background_content("repeating-radial-gradient(circle, red, blue 20%)")
2055            .unwrap();
2056        if let StyleBackgroundContent::RadialGradient(grad) = rg {
2057            assert_eq!(grad.extend_mode, ExtendMode::Repeat);
2058            assert_eq!(grad.shape, Shape::Circle);
2059        } else {
2060            panic!("Expected RadialGradient");
2061        }
2062    }
2063
2064    #[test]
2065    fn test_conic_gradient_angle() {
2066        // "conic-gradient(from 45deg, red, blue)"
2067        let cg = parse_style_background_content("conic-gradient(from 45deg, red, blue)").unwrap();
2068        if let StyleBackgroundContent::ConicGradient(grad) = cg {
2069            assert_eq!(grad.angle, AngleValue::deg(45.0));
2070            assert_eq!(grad.stops.len(), 2);
2071        } else {
2072            panic!("Expected ConicGradient");
2073        }
2074    }
2075
2076    #[test]
2077    fn test_conic_gradient_default() {
2078        // "conic-gradient(red, blue)" - no angle specified
2079        let cg = parse_style_background_content("conic-gradient(red, blue)").unwrap();
2080        if let StyleBackgroundContent::ConicGradient(grad) = cg {
2081            assert_eq!(grad.stops.len(), 2);
2082            // First stop defaults to 0deg
2083            assert!(
2084                (grad.stops.as_ref()[0].angle.to_degrees_raw() - 0.0).abs() < 0.001,
2085                "First stop should be 0deg, got {}",
2086                grad.stops.as_ref()[0].angle.to_degrees_raw()
2087            );
2088            // Last stop defaults to 360deg (use to_degrees_raw to preserve 360)
2089            assert!(
2090                (grad.stops.as_ref()[1].angle.to_degrees_raw() - 360.0).abs() < 0.001,
2091                "Last stop should be 360deg, got {}",
2092                grad.stops.as_ref()[1].angle.to_degrees_raw()
2093            );
2094        } else {
2095            panic!("Expected ConicGradient");
2096        }
2097    }
2098
2099    #[test]
2100    fn test_conic_gradient_with_positions() {
2101        // "conic-gradient(red 0deg, blue 180deg, green 360deg)"
2102        let cg =
2103            parse_style_background_content("conic-gradient(red 0deg, blue 180deg, green 360deg)")
2104                .unwrap();
2105        if let StyleBackgroundContent::ConicGradient(grad) = cg {
2106            assert_eq!(grad.stops.len(), 3);
2107            // Use to_degrees_raw() to preserve 360deg
2108            assert!(
2109                (grad.stops.as_ref()[0].angle.to_degrees_raw() - 0.0).abs() < 0.001,
2110                "First stop should be 0deg, got {}",
2111                grad.stops.as_ref()[0].angle.to_degrees_raw()
2112            );
2113            assert!(
2114                (grad.stops.as_ref()[1].angle.to_degrees_raw() - 180.0).abs() < 0.001,
2115                "Second stop should be 180deg, got {}",
2116                grad.stops.as_ref()[1].angle.to_degrees_raw()
2117            );
2118            assert!(
2119                (grad.stops.as_ref()[2].angle.to_degrees_raw() - 360.0).abs() < 0.001,
2120                "Last stop should be 360deg, got {}",
2121                grad.stops.as_ref()[2].angle.to_degrees_raw()
2122            );
2123        } else {
2124            panic!("Expected ConicGradient");
2125        }
2126    }
2127
2128    #[test]
2129    fn test_repeating_conic_gradient() {
2130        let cg =
2131            parse_style_background_content("repeating-conic-gradient(red, blue 30deg)").unwrap();
2132        if let StyleBackgroundContent::ConicGradient(grad) = cg {
2133            assert_eq!(grad.extend_mode, ExtendMode::Repeat);
2134        } else {
2135            panic!("Expected ConicGradient");
2136        }
2137    }
2138
2139    #[test]
2140    fn test_gradient_with_rgba_color() {
2141        // Test parsing gradient with rgba color (contains spaces)
2142        let lg =
2143            parse_style_background_content("linear-gradient(rgba(255,0,0,0.5), blue)").unwrap();
2144        if let StyleBackgroundContent::LinearGradient(grad) = lg {
2145            assert_eq!(grad.stops.len(), 2);
2146            // First color should have alpha of ~128 (0.5 * 255, may be 127 or 128 due to rounding)
2147            let first_color = grad.stops.as_ref()[0].color.to_color_u_default();
2148            assert!(first_color.a >= 127 && first_color.a <= 128);
2149        } else {
2150            panic!("Expected LinearGradient");
2151        }
2152    }
2153
2154    #[test]
2155    fn test_gradient_with_rgba_and_position() {
2156        // Test parsing "rgba(0,0,0,0.5) 50%"
2157        let lg =
2158            parse_style_background_content("linear-gradient(rgba(0,0,0,0.5) 50%, white)").unwrap();
2159        if let StyleBackgroundContent::LinearGradient(grad) = lg {
2160            assert_eq!(grad.stops.len(), 2);
2161            assert!((grad.stops.as_ref()[0].offset.normalized() - 0.5).abs() < 0.001);
2162        } else {
2163            panic!("Expected LinearGradient");
2164        }
2165    }
2166
2167    #[test]
2168    fn test_gradient_resolves_system_color_stop() {
2169        // A `system:accent` stop should round-trip through the parser as a
2170        // System variant and resolve against a populated `SystemColors` to
2171        // the live accent color, falling back to the supplied default when
2172        // the key is unset.
2173        use crate::props::basic::color::ColorOrSystem;
2174        use crate::system::SystemColors;
2175
2176        let lg = parse_style_background_content(
2177            "linear-gradient(red, system:accent)",
2178        )
2179        .unwrap();
2180        let StyleBackgroundContent::LinearGradient(grad) = lg else {
2181            panic!("Expected LinearGradient");
2182        };
2183        let stops = grad.stops.as_ref();
2184        assert_eq!(stops.len(), 2);
2185
2186        let accent_stop = &stops[1];
2187        assert!(matches!(accent_stop.color, ColorOrSystem::System(_)));
2188
2189        let populated = SystemColors {
2190            accent: crate::props::basic::color::OptionColorU::Some(ColorU::new_rgb(0, 122, 255)),
2191            ..SystemColors::default()
2192        };
2193
2194        let resolved = accent_stop.resolve(&populated, ColorU::TRANSPARENT);
2195        assert_eq!(resolved, ColorU::new_rgb(0, 122, 255));
2196
2197        let empty = SystemColors::default();
2198        let fallback = accent_stop.resolve(&empty, ColorU::TRANSPARENT);
2199        assert_eq!(fallback, ColorU::TRANSPARENT);
2200    }
2201}