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