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