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