Skip to main content

azul_css/props/style/
filter.rs

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