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