Skip to main content

azul_css/props/style/
filter.rs

1//! CSS properties for graphical effects like blur, drop-shadow, etc.
2
3use alloc::{
4    string::{String, ToString},
5    vec::Vec,
6};
7use core::{fmt, num::ParseFloatError};
8
9#[cfg(feature = "parser")]
10use crate::props::basic::{
11    error::{InvalidValueErr, InvalidValueErrOwned},
12    length::parse_float_value,
13    parse::{parse_parentheses, ParenthesisParseError, ParenthesisParseErrorOwned},
14};
15use crate::{
16    format_rust_code::GetHash,
17    props::{
18        basic::{
19            angle::{AngleValue, parse_angle_value, CssAngleValueParseError, CssAngleValueParseErrorOwned},
20            color::{parse_css_color, ColorU, CssColorParseError, CssColorParseErrorOwned},
21            length::{FloatValue, PercentageParseError, PercentageValue},
22            pixel::{
23                parse_pixel_value, CssPixelValueParseError, CssPixelValueParseErrorOwned,
24                PixelValue,
25            },
26        },
27        formatter::PrintAsCssValue,
28        style::{
29            box_shadow::{
30                parse_style_box_shadow, CssShadowParseError, CssShadowParseErrorOwned,
31                StyleBoxShadow,
32            },
33            effects::{parse_style_mix_blend_mode, MixBlendModeParseError, StyleMixBlendMode},
34        },
35    },
36};
37
38// --- TYPE DEFINITIONS ---
39
40#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
41#[repr(C, u8)]
42pub enum StyleFilter {
43    Blend(StyleMixBlendMode),
44    Flood(ColorU),
45    Blur(StyleBlur),
46    Opacity(PercentageValue),
47    ColorMatrix(StyleColorMatrix),
48    DropShadow(StyleBoxShadow),
49    ComponentTransfer,
50    Offset(StyleFilterOffset),
51    Composite(StyleCompositeFilter),
52    // Standard CSS filter functions
53    Brightness(PercentageValue),
54    Contrast(PercentageValue),
55    Grayscale(PercentageValue),
56    HueRotate(AngleValue),
57    Invert(PercentageValue),
58    Saturate(PercentageValue),
59    Sepia(PercentageValue),
60}
61
62impl_option!(
63    StyleFilter,
64    OptionStyleFilter,
65    copy = false,
66    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
67);
68
69impl_vec!(StyleFilter, StyleFilterVec, StyleFilterVecDestructor, StyleFilterVecDestructorType, StyleFilterVecSlice, OptionStyleFilter);
70impl_vec_clone!(StyleFilter, StyleFilterVec, StyleFilterVecDestructor);
71impl_vec_debug!(StyleFilter, StyleFilterVec);
72impl_vec_eq!(StyleFilter, StyleFilterVec);
73impl_vec_ord!(StyleFilter, StyleFilterVec);
74impl_vec_hash!(StyleFilter, StyleFilterVec);
75impl_vec_partialeq!(StyleFilter, StyleFilterVec);
76impl_vec_partialord!(StyleFilter, StyleFilterVec);
77
78#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
79#[repr(C)]
80pub struct StyleBlur {
81    pub width: PixelValue,
82    pub height: PixelValue,
83}
84
85/// Color matrix with 20 float values for color transformation.
86/// Layout: 4 rows × 5 columns (RGBA + offset for each channel)
87#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
88#[repr(C)]
89pub struct StyleColorMatrix {
90    pub m0: FloatValue,
91    pub m1: FloatValue,
92    pub m2: FloatValue,
93    pub m3: FloatValue,
94    pub m4: FloatValue,
95    pub m5: FloatValue,
96    pub m6: FloatValue,
97    pub m7: FloatValue,
98    pub m8: FloatValue,
99    pub m9: FloatValue,
100    pub m10: FloatValue,
101    pub m11: FloatValue,
102    pub m12: FloatValue,
103    pub m13: FloatValue,
104    pub m14: FloatValue,
105    pub m15: FloatValue,
106    pub m16: FloatValue,
107    pub m17: FloatValue,
108    pub m18: FloatValue,
109    pub m19: FloatValue,
110}
111
112impl StyleColorMatrix {
113    /// Returns the matrix values as a slice for iteration
114    pub fn as_slice(&self) -> [FloatValue; 20] {
115        [
116            self.m0, self.m1, self.m2, self.m3, self.m4, self.m5, self.m6, self.m7, self.m8,
117            self.m9, self.m10, self.m11, self.m12, self.m13, self.m14, self.m15, self.m16,
118            self.m17, self.m18, self.m19,
119        ]
120    }
121}
122
123#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
124#[repr(C)]
125pub struct StyleFilterOffset {
126    pub x: PixelValue,
127    pub y: PixelValue,
128}
129
130/// Arithmetic coefficients for composite filter (k1, k2, k3, k4).
131/// Result = k1*i1*i2 + k2*i1 + k3*i2 + k4
132#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
133#[repr(C)]
134pub struct ArithmeticCoefficients {
135    pub k1: FloatValue,
136    pub k2: FloatValue,
137    pub k3: FloatValue,
138    pub k4: FloatValue,
139}
140
141#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
142#[repr(C, u8)]
143pub enum StyleCompositeFilter {
144    Over,
145    In,
146    Atop,
147    Out,
148    Xor,
149    Lighter,
150    Arithmetic(ArithmeticCoefficients),
151}
152
153// --- PRINTING IMPLEMENTATIONS ---
154
155impl PrintAsCssValue for StyleFilterVec {
156    fn print_as_css_value(&self) -> String {
157        self.as_ref()
158            .iter()
159            .map(|f| f.print_as_css_value())
160            .collect::<Vec<_>>()
161            .join(" ")
162    }
163}
164
165// Formatting to Rust code for StyleFilterVec
166impl crate::format_rust_code::FormatAsRustCode for StyleFilterVec {
167    fn format_as_rust_code(&self, _tabs: usize) -> String {
168        format!(
169            "StyleFilterVec::from_const_slice(STYLE_FILTER_{}_ITEMS)",
170            self.get_hash()
171        )
172    }
173}
174
175impl PrintAsCssValue for StyleFilter {
176    fn print_as_css_value(&self) -> String {
177        match self {
178            StyleFilter::Blend(mode) => format!("blend({})", mode.print_as_css_value()),
179            StyleFilter::Flood(c) => format!("flood({})", c.to_hash()),
180            StyleFilter::Blur(c) => {
181                if c.width == c.height {
182                    format!("blur({})", c.width)
183                } else {
184                    format!("blur({} {})", c.width, c.height)
185                }
186            }
187            StyleFilter::Opacity(c) => format!("opacity({})", c),
188            StyleFilter::ColorMatrix(c) => format!(
189                "color-matrix({})",
190                c.as_slice()
191                    .iter()
192                    .map(|s| format!("{}", s))
193                    .collect::<Vec<_>>()
194                    .join(" ")
195            ),
196            StyleFilter::DropShadow(shadow) => {
197                format!("drop-shadow({})", shadow.print_as_css_value())
198            }
199            StyleFilter::ComponentTransfer => "component-transfer".to_string(),
200            StyleFilter::Offset(o) => format!("offset({} {})", o.x, o.y),
201            StyleFilter::Composite(c) => format!("composite({})", c.print_as_css_value()),
202            StyleFilter::Brightness(v) => format!("brightness({})", v),
203            StyleFilter::Contrast(v) => format!("contrast({})", v),
204            StyleFilter::Grayscale(v) => format!("grayscale({})", v),
205            StyleFilter::HueRotate(a) => format!("hue-rotate({})", a),
206            StyleFilter::Invert(v) => format!("invert({})", v),
207            StyleFilter::Saturate(v) => format!("saturate({})", v),
208            StyleFilter::Sepia(v) => format!("sepia({})", v),
209        }
210    }
211}
212
213impl PrintAsCssValue for StyleCompositeFilter {
214    fn print_as_css_value(&self) -> String {
215        match self {
216            StyleCompositeFilter::Over => "over".to_string(),
217            StyleCompositeFilter::In => "in".to_string(),
218            StyleCompositeFilter::Atop => "atop".to_string(),
219            StyleCompositeFilter::Out => "out".to_string(),
220            StyleCompositeFilter::Xor => "xor".to_string(),
221            StyleCompositeFilter::Lighter => "lighter".to_string(),
222            StyleCompositeFilter::Arithmetic(fv) => {
223                format!("arithmetic {} {} {} {}", fv.k1, fv.k2, fv.k3, fv.k4)
224            }
225        }
226    }
227}
228
229// --- PARSER ---
230
231#[cfg(feature = "parser")]
232mod parser {
233    use super::*;
234    use crate::props::basic::parse_percentage_value;
235
236    // -- Top-level Filter Error --
237
238    #[derive(Clone, PartialEq)]
239    pub enum CssStyleFilterParseError<'a> {
240        InvalidFilter(&'a str),
241        InvalidParenthesis(ParenthesisParseError<'a>),
242        Shadow(CssShadowParseError<'a>),
243        BlendMode(InvalidValueErr<'a>),
244        Color(CssColorParseError<'a>),
245        Opacity(PercentageParseError),
246        Blur(CssStyleBlurParseError<'a>),
247        ColorMatrix(CssStyleColorMatrixParseError<'a>),
248        Offset(CssStyleFilterOffsetParseError<'a>),
249        Composite(CssStyleCompositeFilterParseError<'a>),
250        Angle(CssAngleValueParseError<'a>),
251    }
252
253    impl_debug_as_display!(CssStyleFilterParseError<'a>);
254    impl_display! { CssStyleFilterParseError<'a>, {
255        InvalidFilter(e) => format!("Invalid filter function: \"{}\"", e),
256        InvalidParenthesis(e) => format!("Invalid filter syntax - parenthesis error: {}", e),
257        Shadow(e) => format!("Error parsing drop-shadow(): {}", e),
258        BlendMode(e) => format!("Error parsing blend(): invalid value \"{}\"", e.0),
259        Color(e) => format!("Error parsing flood(): {}", e),
260        Opacity(e) => format!("Error parsing opacity(): {}", e),
261        Blur(e) => format!("Error parsing blur(): {}", e),
262        ColorMatrix(e) => format!("Error parsing color-matrix(): {}", e),
263        Offset(e) => format!("Error parsing offset(): {}", e),
264        Composite(e) => format!("Error parsing composite(): {}", e),
265        Angle(e) => format!("Error parsing hue-rotate(): {}", e),
266    }}
267
268    impl_from!(
269        ParenthesisParseError<'a>,
270        CssStyleFilterParseError::InvalidParenthesis
271    );
272    impl_from!(InvalidValueErr<'a>, CssStyleFilterParseError::BlendMode);
273    impl_from!(CssStyleBlurParseError<'a>, CssStyleFilterParseError::Blur);
274    impl_from!(CssColorParseError<'a>, CssStyleFilterParseError::Color);
275    impl_from!(
276        CssStyleColorMatrixParseError<'a>,
277        CssStyleFilterParseError::ColorMatrix
278    );
279    impl_from!(
280        CssStyleFilterOffsetParseError<'a>,
281        CssStyleFilterParseError::Offset
282    );
283    impl_from!(
284        CssStyleCompositeFilterParseError<'a>,
285        CssStyleFilterParseError::Composite
286    );
287    impl_from!(CssShadowParseError<'a>, CssStyleFilterParseError::Shadow);
288    impl_from!(CssAngleValueParseError<'a>, CssStyleFilterParseError::Angle);
289
290    impl<'a> From<PercentageParseError> for CssStyleFilterParseError<'a> {
291        fn from(p: PercentageParseError) -> Self {
292            Self::Opacity(p)
293        }
294    }
295
296    impl<'a> From<MixBlendModeParseError<'a>> for CssStyleFilterParseError<'a> {
297        fn from(e: MixBlendModeParseError<'a>) -> Self {
298            // Extract the InvalidValueErr from the MixBlendModeParseError
299            match e {
300                MixBlendModeParseError::InvalidValue(err) => Self::BlendMode(err),
301            }
302        }
303    }
304
305    #[derive(Debug, Clone, PartialEq)]
306    pub enum CssStyleFilterParseErrorOwned {
307        InvalidFilter(String),
308        InvalidParenthesis(ParenthesisParseErrorOwned),
309        Shadow(CssShadowParseErrorOwned),
310        BlendMode(InvalidValueErrOwned),
311        Color(CssColorParseErrorOwned),
312        Opacity(PercentageParseError),
313        Blur(CssStyleBlurParseErrorOwned),
314        ColorMatrix(CssStyleColorMatrixParseErrorOwned),
315        Offset(CssStyleFilterOffsetParseErrorOwned),
316        Composite(CssStyleCompositeFilterParseErrorOwned),
317        Angle(CssAngleValueParseErrorOwned),
318    }
319
320    impl<'a> CssStyleFilterParseError<'a> {
321        pub fn to_contained(&self) -> CssStyleFilterParseErrorOwned {
322            match self {
323                Self::InvalidFilter(s) => {
324                    CssStyleFilterParseErrorOwned::InvalidFilter(s.to_string())
325                }
326                Self::InvalidParenthesis(e) => {
327                    CssStyleFilterParseErrorOwned::InvalidParenthesis(e.to_contained())
328                }
329                Self::Shadow(e) => CssStyleFilterParseErrorOwned::Shadow(e.to_contained()),
330                Self::BlendMode(e) => CssStyleFilterParseErrorOwned::BlendMode(e.to_contained()),
331                Self::Color(e) => CssStyleFilterParseErrorOwned::Color(e.to_contained()),
332                Self::Opacity(e) => CssStyleFilterParseErrorOwned::Opacity(e.clone()),
333                Self::Blur(e) => CssStyleFilterParseErrorOwned::Blur(e.to_contained()),
334                Self::ColorMatrix(e) => {
335                    CssStyleFilterParseErrorOwned::ColorMatrix(e.to_contained())
336                }
337                Self::Offset(e) => CssStyleFilterParseErrorOwned::Offset(e.to_contained()),
338                Self::Composite(e) => CssStyleFilterParseErrorOwned::Composite(e.to_contained()),
339                Self::Angle(e) => CssStyleFilterParseErrorOwned::Angle(e.to_contained()),
340            }
341        }
342    }
343
344    impl CssStyleFilterParseErrorOwned {
345        pub fn to_shared<'a>(&'a self) -> CssStyleFilterParseError<'a> {
346            match self {
347                Self::InvalidFilter(s) => CssStyleFilterParseError::InvalidFilter(s),
348                Self::InvalidParenthesis(e) => {
349                    CssStyleFilterParseError::InvalidParenthesis(e.to_shared())
350                }
351                Self::Shadow(e) => CssStyleFilterParseError::Shadow(e.to_shared()),
352                Self::BlendMode(e) => CssStyleFilterParseError::BlendMode(e.to_shared()),
353                Self::Color(e) => CssStyleFilterParseError::Color(e.to_shared()),
354                Self::Opacity(e) => CssStyleFilterParseError::Opacity(e.clone()),
355                Self::Blur(e) => CssStyleFilterParseError::Blur(e.to_shared()),
356                Self::ColorMatrix(e) => CssStyleFilterParseError::ColorMatrix(e.to_shared()),
357                Self::Offset(e) => CssStyleFilterParseError::Offset(e.to_shared()),
358                Self::Composite(e) => CssStyleFilterParseError::Composite(e.to_shared()),
359                Self::Angle(e) => CssStyleFilterParseError::Angle(e.to_shared()),
360            }
361        }
362    }
363
364    // -- Sub-Errors for each filter function --
365
366    #[derive(Clone, PartialEq)]
367    pub enum CssStyleBlurParseError<'a> {
368        Pixel(CssPixelValueParseError<'a>),
369        TooManyComponents(&'a str),
370    }
371
372    impl_debug_as_display!(CssStyleBlurParseError<'a>);
373    impl_display! { CssStyleBlurParseError<'a>, {
374        Pixel(e) => format!("Invalid pixel value: {}", e),
375        TooManyComponents(input) => format!("Expected 1 or 2 components, got more: \"{}\"", input),
376    }}
377    impl_from!(CssPixelValueParseError<'a>, CssStyleBlurParseError::Pixel);
378
379    #[derive(Debug, Clone, PartialEq)]
380    pub enum CssStyleBlurParseErrorOwned {
381        Pixel(CssPixelValueParseErrorOwned),
382        TooManyComponents(String),
383    }
384
385    impl<'a> CssStyleBlurParseError<'a> {
386        pub fn to_contained(&self) -> CssStyleBlurParseErrorOwned {
387            match self {
388                Self::Pixel(e) => CssStyleBlurParseErrorOwned::Pixel(e.to_contained()),
389                Self::TooManyComponents(s) => {
390                    CssStyleBlurParseErrorOwned::TooManyComponents(s.to_string())
391                }
392            }
393        }
394    }
395
396    impl CssStyleBlurParseErrorOwned {
397        pub fn to_shared<'a>(&'a self) -> CssStyleBlurParseError<'a> {
398            match self {
399                Self::Pixel(e) => CssStyleBlurParseError::Pixel(e.to_shared()),
400                Self::TooManyComponents(s) => CssStyleBlurParseError::TooManyComponents(s),
401            }
402        }
403    }
404
405    #[derive(Clone, PartialEq)]
406    pub enum CssStyleColorMatrixParseError<'a> {
407        Float(ParseFloatError),
408        WrongNumberOfComponents {
409            expected: usize,
410            got: usize,
411            input: &'a str,
412        },
413    }
414
415    impl_debug_as_display!(CssStyleColorMatrixParseError<'a>);
416    impl_display! { CssStyleColorMatrixParseError<'a>, {
417        Float(e) => format!("Error parsing floating-point value: {}", e),
418        WrongNumberOfComponents { expected, got, input } => format!("Expected {} components, got {}: \"{}\"", expected, got, input),
419    }}
420    impl<'a> From<ParseFloatError> for CssStyleColorMatrixParseError<'a> {
421        fn from(p: ParseFloatError) -> Self {
422            Self::Float(p)
423        }
424    }
425
426    #[derive(Debug, Clone, PartialEq)]
427    pub enum CssStyleColorMatrixParseErrorOwned {
428        Float(ParseFloatError),
429        WrongNumberOfComponents {
430            expected: usize,
431            got: usize,
432            input: String,
433        },
434    }
435
436    impl<'a> CssStyleColorMatrixParseError<'a> {
437        pub fn to_contained(&self) -> CssStyleColorMatrixParseErrorOwned {
438            match self {
439                Self::Float(e) => CssStyleColorMatrixParseErrorOwned::Float(e.clone()),
440                Self::WrongNumberOfComponents {
441                    expected,
442                    got,
443                    input,
444                } => CssStyleColorMatrixParseErrorOwned::WrongNumberOfComponents {
445                    expected: *expected,
446                    got: *got,
447                    input: input.to_string(),
448                },
449            }
450        }
451    }
452
453    impl CssStyleColorMatrixParseErrorOwned {
454        pub fn to_shared<'a>(&'a self) -> CssStyleColorMatrixParseError<'a> {
455            match self {
456                Self::Float(e) => CssStyleColorMatrixParseError::Float(e.clone()),
457                Self::WrongNumberOfComponents {
458                    expected,
459                    got,
460                    input,
461                } => CssStyleColorMatrixParseError::WrongNumberOfComponents {
462                    expected: *expected,
463                    got: *got,
464                    input,
465                },
466            }
467        }
468    }
469
470    #[derive(Clone, PartialEq)]
471    pub enum CssStyleFilterOffsetParseError<'a> {
472        Pixel(CssPixelValueParseError<'a>),
473        WrongNumberOfComponents {
474            expected: usize,
475            got: usize,
476            input: &'a str,
477        },
478    }
479
480    impl_debug_as_display!(CssStyleFilterOffsetParseError<'a>);
481    impl_display! { CssStyleFilterOffsetParseError<'a>, {
482        Pixel(e) => format!("Invalid pixel value: {}", e),
483        WrongNumberOfComponents { expected, got, input } => format!("Expected {} components, got {}: \"{}\"", expected, got, input),
484    }}
485    impl_from!(
486        CssPixelValueParseError<'a>,
487        CssStyleFilterOffsetParseError::Pixel
488    );
489
490    #[derive(Debug, Clone, PartialEq)]
491    pub enum CssStyleFilterOffsetParseErrorOwned {
492        Pixel(CssPixelValueParseErrorOwned),
493        WrongNumberOfComponents {
494            expected: usize,
495            got: usize,
496            input: String,
497        },
498    }
499
500    impl<'a> CssStyleFilterOffsetParseError<'a> {
501        pub fn to_contained(&self) -> CssStyleFilterOffsetParseErrorOwned {
502            match self {
503                Self::Pixel(e) => CssStyleFilterOffsetParseErrorOwned::Pixel(e.to_contained()),
504                Self::WrongNumberOfComponents {
505                    expected,
506                    got,
507                    input,
508                } => CssStyleFilterOffsetParseErrorOwned::WrongNumberOfComponents {
509                    expected: *expected,
510                    got: *got,
511                    input: input.to_string(),
512                },
513            }
514        }
515    }
516
517    impl CssStyleFilterOffsetParseErrorOwned {
518        pub fn to_shared<'a>(&'a self) -> CssStyleFilterOffsetParseError<'a> {
519            match self {
520                Self::Pixel(e) => CssStyleFilterOffsetParseError::Pixel(e.to_shared()),
521                Self::WrongNumberOfComponents {
522                    expected,
523                    got,
524                    input,
525                } => CssStyleFilterOffsetParseError::WrongNumberOfComponents {
526                    expected: *expected,
527                    got: *got,
528                    input,
529                },
530            }
531        }
532    }
533
534    #[derive(Clone, PartialEq)]
535    pub enum CssStyleCompositeFilterParseError<'a> {
536        Invalid(InvalidValueErr<'a>),
537        Float(ParseFloatError),
538        WrongNumberOfComponents {
539            expected: usize,
540            got: usize,
541            input: &'a str,
542        },
543    }
544
545    impl_debug_as_display!(CssStyleCompositeFilterParseError<'a>);
546    impl_display! { CssStyleCompositeFilterParseError<'a>, {
547        Invalid(s) => format!("Invalid composite operator: {}", s.0),
548        Float(e) => format!("Error parsing floating-point value for arithmetic(): {}", e),
549        WrongNumberOfComponents { expected, got, input } => format!("Expected {} components for arithmetic(), got {}: \"{}\"", expected, got, input),
550    }}
551    impl_from!(
552        InvalidValueErr<'a>,
553        CssStyleCompositeFilterParseError::Invalid
554    );
555    impl<'a> From<ParseFloatError> for CssStyleCompositeFilterParseError<'a> {
556        fn from(p: ParseFloatError) -> Self {
557            Self::Float(p)
558        }
559    }
560
561    #[derive(Debug, Clone, PartialEq)]
562    pub enum CssStyleCompositeFilterParseErrorOwned {
563        Invalid(InvalidValueErrOwned),
564        Float(ParseFloatError),
565        WrongNumberOfComponents {
566            expected: usize,
567            got: usize,
568            input: String,
569        },
570    }
571
572    impl<'a> CssStyleCompositeFilterParseError<'a> {
573        pub fn to_contained(&self) -> CssStyleCompositeFilterParseErrorOwned {
574            match self {
575                Self::Invalid(e) => {
576                    CssStyleCompositeFilterParseErrorOwned::Invalid(e.to_contained())
577                }
578                Self::Float(e) => CssStyleCompositeFilterParseErrorOwned::Float(e.clone()),
579                Self::WrongNumberOfComponents {
580                    expected,
581                    got,
582                    input,
583                } => CssStyleCompositeFilterParseErrorOwned::WrongNumberOfComponents {
584                    expected: *expected,
585                    got: *got,
586                    input: input.to_string(),
587                },
588            }
589        }
590    }
591
592    impl CssStyleCompositeFilterParseErrorOwned {
593        pub fn to_shared<'a>(&'a self) -> CssStyleCompositeFilterParseError<'a> {
594            match self {
595                Self::Invalid(e) => CssStyleCompositeFilterParseError::Invalid(e.to_shared()),
596                Self::Float(e) => CssStyleCompositeFilterParseError::Float(e.clone()),
597                Self::WrongNumberOfComponents {
598                    expected,
599                    got,
600                    input,
601                } => CssStyleCompositeFilterParseError::WrongNumberOfComponents {
602                    expected: *expected,
603                    got: *got,
604                    input,
605                },
606            }
607        }
608    }
609
610    // -- Parser Implementation --
611
612    /// Parses a space-separated list of filter functions.
613    pub fn parse_style_filter_vec<'a>(
614        input: &'a str,
615    ) -> Result<StyleFilterVec, CssStyleFilterParseError<'a>> {
616        let mut filters = Vec::new();
617        let mut remaining = input.trim();
618        while !remaining.is_empty() {
619            let (filter, rest) = parse_one_filter_function(remaining)?;
620            filters.push(filter);
621            remaining = rest.trim_start();
622        }
623        Ok(filters.into())
624    }
625
626    /// Parses one `function(...)` from the beginning of a string and returns the parsed
627    /// filter and the rest of the string.
628    fn parse_one_filter_function<'a>(
629        input: &'a str,
630    ) -> Result<(StyleFilter, &'a str), CssStyleFilterParseError<'a>> {
631        let open_paren = input
632            .find('(')
633            .ok_or(CssStyleFilterParseError::InvalidFilter(input))?;
634        let func_name = &input[..open_paren];
635
636        let mut balance = 1;
637        let mut close_paren = 0;
638        for (i, c) in input.char_indices().skip(open_paren + 1) {
639            if c == '(' {
640                balance += 1;
641            } else if c == ')' {
642                balance -= 1;
643                if balance == 0 {
644                    close_paren = i;
645                    break;
646                }
647            }
648        }
649
650        if balance != 0 {
651            return Err(ParenthesisParseError::UnclosedBraces.into());
652        }
653
654        let full_function = &input[..=close_paren];
655        let rest = &input[(close_paren + 1)..];
656
657        let filter = parse_style_filter(full_function)?;
658        Ok((filter, rest))
659    }
660
661    /// Parses a single filter function string, like `blur(5px)`.
662    pub fn parse_style_filter<'a>(
663        input: &'a str,
664    ) -> Result<StyleFilter, CssStyleFilterParseError<'a>> {
665        let (filter_type, filter_values) = parse_parentheses(
666            input,
667            &[
668                "blend",
669                "flood",
670                "blur",
671                "opacity",
672                "color-matrix",
673                "drop-shadow",
674                "component-transfer",
675                "offset",
676                "composite",
677                "brightness",
678                "contrast",
679                "grayscale",
680                "hue-rotate",
681                "invert",
682                "saturate",
683                "sepia",
684            ],
685        )?;
686
687        match filter_type {
688            "blend" => Ok(StyleFilter::Blend(parse_style_mix_blend_mode(
689                filter_values,
690            )?)),
691            "flood" => Ok(StyleFilter::Flood(parse_css_color(filter_values)?)),
692            "blur" => Ok(StyleFilter::Blur(parse_style_blur(filter_values)?)),
693            "opacity" => {
694                let val = parse_percentage_value(filter_values)?;
695                // CSS filter opacity must be between 0 and 1 (or 0% to 100%)
696                let normalized = val.normalized();
697                if normalized < 0.0 || normalized > 1.0 {
698                    return Err(CssStyleFilterParseError::Opacity(
699                        PercentageParseError::InvalidUnit(filter_values.to_string().into()),
700                    ));
701                }
702                Ok(StyleFilter::Opacity(val))
703            }
704            "color-matrix" => Ok(StyleFilter::ColorMatrix(parse_color_matrix(filter_values)?)),
705            "drop-shadow" => Ok(StyleFilter::DropShadow(parse_style_box_shadow(
706                filter_values,
707            )?)),
708            "component-transfer" => Ok(StyleFilter::ComponentTransfer),
709            "offset" => Ok(StyleFilter::Offset(parse_filter_offset(filter_values)?)),
710            "composite" => Ok(StyleFilter::Composite(parse_filter_composite(
711                filter_values,
712            )?)),
713            "brightness" => Ok(StyleFilter::Brightness(parse_percentage_value(filter_values)?)),
714            "contrast" => Ok(StyleFilter::Contrast(parse_percentage_value(filter_values)?)),
715            "grayscale" => Ok(StyleFilter::Grayscale(parse_percentage_value(filter_values)?)),
716            "hue-rotate" => Ok(StyleFilter::HueRotate(parse_angle_value(filter_values)?)),
717            "invert" => Ok(StyleFilter::Invert(parse_percentage_value(filter_values)?)),
718            "saturate" => Ok(StyleFilter::Saturate(parse_percentage_value(filter_values)?)),
719            "sepia" => Ok(StyleFilter::Sepia(parse_percentage_value(filter_values)?)),
720            _ => unreachable!(),
721        }
722    }
723
724    fn parse_style_blur<'a>(input: &'a str) -> Result<StyleBlur, CssStyleBlurParseError<'a>> {
725        let mut iter = input.split_whitespace();
726        let width_str = iter.next().unwrap_or("");
727        let height_str = iter.next();
728
729        if iter.next().is_some() {
730            return Err(CssStyleBlurParseError::TooManyComponents(input));
731        }
732
733        let width = parse_pixel_value(width_str)?;
734        let height = match height_str {
735            Some(s) => parse_pixel_value(s)?,
736            None => width, // If only one value is given, use it for both
737        };
738
739        Ok(StyleBlur { width, height })
740    }
741
742    fn parse_color_matrix<'a>(
743        input: &'a str,
744    ) -> Result<StyleColorMatrix, CssStyleColorMatrixParseError<'a>> {
745        let components: Vec<_> = input.split_whitespace().collect();
746        if components.len() != 20 {
747            return Err(CssStyleColorMatrixParseError::WrongNumberOfComponents {
748                expected: 20,
749                got: components.len(),
750                input,
751            });
752        }
753
754        let mut values = [FloatValue::const_new(0); 20];
755        for (i, comp) in components.iter().enumerate() {
756            values[i] = parse_float_value(comp)?;
757        }
758
759        Ok(StyleColorMatrix {
760            m0: values[0],
761            m1: values[1],
762            m2: values[2],
763            m3: values[3],
764            m4: values[4],
765            m5: values[5],
766            m6: values[6],
767            m7: values[7],
768            m8: values[8],
769            m9: values[9],
770            m10: values[10],
771            m11: values[11],
772            m12: values[12],
773            m13: values[13],
774            m14: values[14],
775            m15: values[15],
776            m16: values[16],
777            m17: values[17],
778            m18: values[18],
779            m19: values[19],
780        })
781    }
782
783    fn parse_filter_offset<'a>(
784        input: &'a str,
785    ) -> Result<StyleFilterOffset, CssStyleFilterOffsetParseError<'a>> {
786        let components: Vec<_> = input.split_whitespace().collect();
787        if components.len() != 2 {
788            return Err(CssStyleFilterOffsetParseError::WrongNumberOfComponents {
789                expected: 2,
790                got: components.len(),
791                input,
792            });
793        }
794
795        let x = parse_pixel_value(components[0])?;
796        let y = parse_pixel_value(components[1])?;
797
798        Ok(StyleFilterOffset { x, y })
799    }
800
801    fn parse_filter_composite<'a>(
802        input: &'a str,
803    ) -> Result<StyleCompositeFilter, CssStyleCompositeFilterParseError<'a>> {
804        let mut iter = input.split_whitespace();
805        let operator = iter.next().unwrap_or("");
806
807        match operator {
808            "over" => Ok(StyleCompositeFilter::Over),
809            "in" => Ok(StyleCompositeFilter::In),
810            "atop" => Ok(StyleCompositeFilter::Atop),
811            "out" => Ok(StyleCompositeFilter::Out),
812            "xor" => Ok(StyleCompositeFilter::Xor),
813            "lighter" => Ok(StyleCompositeFilter::Lighter),
814            "arithmetic" => {
815                let mut values = [FloatValue::const_new(0); 4];
816                for (i, val) in values.iter_mut().enumerate() {
817                    let s = iter.next().ok_or(
818                        CssStyleCompositeFilterParseError::WrongNumberOfComponents {
819                            expected: 4,
820                            got: i,
821                            input,
822                        },
823                    )?;
824                    *val = parse_float_value(s)?;
825                }
826                Ok(StyleCompositeFilter::Arithmetic(ArithmeticCoefficients {
827                    k1: values[0],
828                    k2: values[1],
829                    k3: values[2],
830                    k4: values[3],
831                }))
832            }
833            _ => Err(InvalidValueErr(operator).into()),
834        }
835    }
836}
837#[cfg(feature = "parser")]
838pub use parser::{
839    parse_style_filter_vec, CssStyleBlurParseError, CssStyleBlurParseErrorOwned,
840    CssStyleColorMatrixParseError, CssStyleColorMatrixParseErrorOwned,
841    CssStyleCompositeFilterParseError, CssStyleCompositeFilterParseErrorOwned,
842    CssStyleFilterOffsetParseError, CssStyleFilterOffsetParseErrorOwned, CssStyleFilterParseError,
843    CssStyleFilterParseErrorOwned,
844};
845
846#[cfg(all(test, feature = "parser"))]
847mod tests {
848    use super::*;
849    use crate::props::style::filter::parser::parse_style_filter;
850
851    #[test]
852    fn test_parse_single_filter_functions() {
853        // Blur
854        let blur = parse_style_filter("blur(5px)").unwrap();
855        assert!(matches!(blur, StyleFilter::Blur(_)));
856        if let StyleFilter::Blur(b) = blur {
857            assert_eq!(b.width, PixelValue::px(5.0));
858            assert_eq!(b.height, PixelValue::px(5.0));
859        }
860
861        // Blur with two values
862        let blur2 = parse_style_filter("blur(2px 4px)").unwrap();
863        if let StyleFilter::Blur(b) = blur2 {
864            assert_eq!(b.width, PixelValue::px(2.0));
865            assert_eq!(b.height, PixelValue::px(4.0));
866        }
867
868        // Drop Shadow
869        let shadow = parse_style_filter("drop-shadow(10px 5px 5px #888)").unwrap();
870        assert!(matches!(shadow, StyleFilter::DropShadow(_)));
871        if let StyleFilter::DropShadow(s) = shadow {
872            assert_eq!(s.offset_x.inner, PixelValue::px(10.0));
873            assert_eq!(s.blur_radius.inner, PixelValue::px(5.0));
874            assert_eq!(s.color, ColorU::new_rgb(0x88, 0x88, 0x88));
875        }
876
877        // Opacity
878        let opacity = parse_style_filter("opacity(50%)").unwrap();
879        assert!(matches!(opacity, StyleFilter::Opacity(_)));
880        if let StyleFilter::Opacity(p) = opacity {
881            assert_eq!(p.normalized(), 0.5);
882        }
883
884        // Flood
885        let flood = parse_style_filter("flood(red)").unwrap();
886        assert_eq!(flood, StyleFilter::Flood(ColorU::RED));
887
888        // Composite
889        let composite = parse_style_filter("composite(in)").unwrap();
890        assert_eq!(composite, StyleFilter::Composite(StyleCompositeFilter::In));
891
892        // Offset
893        let offset = parse_style_filter("offset(10px 20%)").unwrap();
894        if let StyleFilter::Offset(o) = offset {
895            assert_eq!(o.x, PixelValue::px(10.0));
896            assert_eq!(o.y, PixelValue::percent(20.0));
897        }
898    }
899
900    #[test]
901    fn test_parse_filter_vec() {
902        let filters =
903            parse_style_filter_vec("blur(5px) drop-shadow(10px 5px #888) opacity(0.8)").unwrap();
904        assert_eq!(filters.len(), 3);
905        assert!(matches!(filters.as_slice()[0], StyleFilter::Blur(_)));
906        assert!(matches!(filters.as_slice()[1], StyleFilter::DropShadow(_)));
907        assert!(matches!(filters.as_slice()[2], StyleFilter::Opacity(_)));
908    }
909
910    #[test]
911    fn test_parse_filter_errors() {
912        // Invalid function name
913        assert!(parse_style_filter_vec("blurry(5px)").is_err());
914        // Incorrect arguments
915        assert!(parse_style_filter_vec("blur(5px 10px 15px)").is_err());
916        assert!(parse_style_filter_vec("opacity(2)").is_err()); // opacity must be % or 0-1
917                                                                // Unclosed parenthesis
918        assert!(parse_style_filter_vec("blur(5px").is_err());
919    }
920}