lightningcss/properties/
effects.rs

1//! CSS properties related to filters and effects.
2
3use crate::error::{ParserError, PrinterError};
4use crate::printer::Printer;
5use crate::targets::{Browsers, Targets};
6use crate::traits::{FallbackValues, IsCompatible, Parse, ToCss, Zero};
7use crate::values::color::ColorFallbackKind;
8use crate::values::{angle::Angle, color::CssColor, length::Length, percentage::NumberOrPercentage, url::Url};
9#[cfg(feature = "visitor")]
10use crate::visitor::Visit;
11use cssparser::*;
12use smallvec::SmallVec;
13
14/// A [filter](https://drafts.fxtf.org/filter-effects-1/#filter-functions) function.
15#[derive(Debug, Clone, PartialEq)]
16#[cfg_attr(feature = "visitor", derive(Visit))]
17#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
18#[cfg_attr(
19  feature = "serde",
20  derive(serde::Serialize, serde::Deserialize),
21  serde(tag = "type", content = "value", rename_all = "kebab-case")
22)]
23#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
24pub enum Filter<'i> {
25  /// A `blur()` filter.
26  Blur(Length),
27  /// A `brightness()` filter.
28  Brightness(NumberOrPercentage),
29  /// A `contrast()` filter.
30  Contrast(NumberOrPercentage),
31  /// A `grayscale()` filter.
32  Grayscale(NumberOrPercentage),
33  /// A `hue-rotate()` filter.
34  HueRotate(Angle),
35  /// An `invert()` filter.
36  Invert(NumberOrPercentage),
37  /// An `opacity()` filter.
38  Opacity(NumberOrPercentage),
39  /// A `saturate()` filter.
40  Saturate(NumberOrPercentage),
41  /// A `sepia()` filter.
42  Sepia(NumberOrPercentage),
43  /// A `drop-shadow()` filter.
44  DropShadow(DropShadow),
45  /// A `url()` reference to an SVG filter.
46  #[cfg_attr(feature = "serde", serde(borrow))]
47  Url(Url<'i>),
48}
49
50impl<'i> Parse<'i> for Filter<'i> {
51  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
52    if let Ok(url) = input.try_parse(Url::parse) {
53      return Ok(Filter::Url(url));
54    }
55
56    let location = input.current_source_location();
57    let function = input.expect_function()?;
58    match_ignore_ascii_case! { &function,
59      "blur" => {
60        input.parse_nested_block(|input| {
61          Ok(Filter::Blur(input.try_parse(Length::parse).unwrap_or(Length::zero())))
62        })
63      },
64      "brightness" => {
65        input.parse_nested_block(|input| {
66          Ok(Filter::Brightness(input.try_parse(NumberOrPercentage::parse).unwrap_or(NumberOrPercentage::Number(1.0))))
67        })
68      },
69      "contrast" => {
70        input.parse_nested_block(|input| {
71          Ok(Filter::Contrast(input.try_parse(NumberOrPercentage::parse).unwrap_or(NumberOrPercentage::Number(1.0))))
72        })
73      },
74      "grayscale" => {
75        input.parse_nested_block(|input| {
76          Ok(Filter::Grayscale(input.try_parse(NumberOrPercentage::parse).unwrap_or(NumberOrPercentage::Number(1.0))))
77        })
78      },
79      "hue-rotate" => {
80        input.parse_nested_block(|input| {
81          // Spec has an exception for unitless zero angles: https://github.com/w3c/fxtf-drafts/issues/228
82          Ok(Filter::HueRotate(input.try_parse(Angle::parse_with_unitless_zero).unwrap_or(Angle::zero())))
83        })
84      },
85      "invert" => {
86        input.parse_nested_block(|input| {
87          Ok(Filter::Invert(input.try_parse(NumberOrPercentage::parse).unwrap_or(NumberOrPercentage::Number(1.0))))
88        })
89      },
90      "opacity" => {
91        input.parse_nested_block(|input| {
92          Ok(Filter::Opacity(input.try_parse(NumberOrPercentage::parse).unwrap_or(NumberOrPercentage::Number(1.0))))
93        })
94      },
95      "saturate" => {
96        input.parse_nested_block(|input| {
97          Ok(Filter::Saturate(input.try_parse(NumberOrPercentage::parse).unwrap_or(NumberOrPercentage::Number(1.0))))
98        })
99      },
100      "sepia" => {
101        input.parse_nested_block(|input| {
102          Ok(Filter::Sepia(input.try_parse(NumberOrPercentage::parse).unwrap_or(NumberOrPercentage::Number(1.0))))
103        })
104      },
105      "drop-shadow" => {
106        input.parse_nested_block(|input| {
107          Ok(Filter::DropShadow(DropShadow::parse(input)?))
108        })
109      },
110      _ => Err(location.new_unexpected_token_error(
111        cssparser::Token::Ident(function.clone())
112      ))
113    }
114  }
115}
116
117impl<'i> ToCss for Filter<'i> {
118  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
119  where
120    W: std::fmt::Write,
121  {
122    match self {
123      Filter::Blur(val) => {
124        dest.write_str("blur(")?;
125        if *val != Length::zero() {
126          val.to_css(dest)?;
127        }
128        dest.write_char(')')
129      }
130      Filter::Brightness(val) => {
131        dest.write_str("brightness(")?;
132        let v: f32 = val.into();
133        if v != 1.0 {
134          val.to_css(dest)?;
135        }
136        dest.write_char(')')
137      }
138      Filter::Contrast(val) => {
139        dest.write_str("contrast(")?;
140        let v: f32 = val.into();
141        if v != 1.0 {
142          val.to_css(dest)?;
143        }
144        dest.write_char(')')
145      }
146      Filter::Grayscale(val) => {
147        dest.write_str("grayscale(")?;
148        let v: f32 = val.into();
149        if v != 1.0 {
150          val.to_css(dest)?;
151        }
152        dest.write_char(')')
153      }
154      Filter::HueRotate(val) => {
155        dest.write_str("hue-rotate(")?;
156        if !val.is_zero() {
157          val.to_css(dest)?;
158        }
159        dest.write_char(')')
160      }
161      Filter::Invert(val) => {
162        dest.write_str("invert(")?;
163        let v: f32 = val.into();
164        if v != 1.0 {
165          val.to_css(dest)?;
166        }
167        dest.write_char(')')
168      }
169      Filter::Opacity(val) => {
170        dest.write_str("opacity(")?;
171        let v: f32 = val.into();
172        if v != 1.0 {
173          val.to_css(dest)?;
174        }
175        dest.write_char(')')
176      }
177      Filter::Saturate(val) => {
178        dest.write_str("saturate(")?;
179        let v: f32 = val.into();
180        if v != 1.0 {
181          val.to_css(dest)?;
182        }
183        dest.write_char(')')
184      }
185      Filter::Sepia(val) => {
186        dest.write_str("sepia(")?;
187        let v: f32 = val.into();
188        if v != 1.0 {
189          val.to_css(dest)?;
190        }
191        dest.write_char(')')
192      }
193      Filter::DropShadow(val) => {
194        dest.write_str("drop-shadow(")?;
195        val.to_css(dest)?;
196        dest.write_char(')')
197      }
198      Filter::Url(url) => url.to_css(dest),
199    }
200  }
201}
202
203impl<'i> Filter<'i> {
204  fn get_fallback(&self, kind: ColorFallbackKind) -> Self {
205    match self {
206      Filter::DropShadow(shadow) => Filter::DropShadow(shadow.get_fallback(kind)),
207      _ => self.clone(),
208    }
209  }
210}
211
212impl IsCompatible for Filter<'_> {
213  fn is_compatible(&self, _browsers: Browsers) -> bool {
214    true
215  }
216}
217
218/// A [`drop-shadow()`](https://drafts.fxtf.org/filter-effects-1/#funcdef-filter-drop-shadow) filter function.
219#[derive(Debug, Clone, PartialEq)]
220#[cfg_attr(feature = "visitor", derive(Visit))]
221#[cfg_attr(
222  feature = "serde",
223  derive(serde::Serialize, serde::Deserialize),
224  serde(rename_all = "camelCase")
225)]
226#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
227#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
228pub struct DropShadow {
229  /// The color of the drop shadow.
230  pub color: CssColor,
231  /// The x offset of the drop shadow.
232  pub x_offset: Length,
233  /// The y offset of the drop shadow.
234  pub y_offset: Length,
235  /// The blur radius of the drop shadow.
236  pub blur: Length,
237}
238
239impl<'i> Parse<'i> for DropShadow {
240  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
241    let mut color = None;
242    let mut lengths = None;
243
244    loop {
245      if lengths.is_none() {
246        let value = input.try_parse::<_, _, ParseError<ParserError<'i>>>(|input| {
247          let horizontal = Length::parse(input)?;
248          let vertical = Length::parse(input)?;
249          let blur = input.try_parse(Length::parse).unwrap_or(Length::zero());
250          Ok((horizontal, vertical, blur))
251        });
252
253        if let Ok(value) = value {
254          lengths = Some(value);
255          continue;
256        }
257      }
258
259      if color.is_none() {
260        if let Ok(value) = input.try_parse(CssColor::parse) {
261          color = Some(value);
262          continue;
263        }
264      }
265
266      break;
267    }
268
269    let lengths = lengths.ok_or(input.new_error(BasicParseErrorKind::QualifiedRuleInvalid))?;
270    Ok(DropShadow {
271      color: color.unwrap_or(CssColor::current_color()),
272      x_offset: lengths.0,
273      y_offset: lengths.1,
274      blur: lengths.2,
275    })
276  }
277}
278
279impl ToCss for DropShadow {
280  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
281  where
282    W: std::fmt::Write,
283  {
284    self.x_offset.to_css(dest)?;
285    dest.write_char(' ')?;
286    self.y_offset.to_css(dest)?;
287
288    if self.blur != Length::zero() {
289      dest.write_char(' ')?;
290      self.blur.to_css(dest)?;
291    }
292
293    if self.color != CssColor::current_color() {
294      dest.write_char(' ')?;
295      self.color.to_css(dest)?;
296    }
297
298    Ok(())
299  }
300}
301
302impl DropShadow {
303  fn get_fallback(&self, kind: ColorFallbackKind) -> DropShadow {
304    DropShadow {
305      color: self.color.get_fallback(kind),
306      ..self.clone()
307    }
308  }
309}
310
311/// A value for the [filter](https://drafts.fxtf.org/filter-effects-1/#FilterProperty) and
312/// [backdrop-filter](https://drafts.fxtf.org/filter-effects-2/#BackdropFilterProperty) properties.
313#[derive(Debug, Clone, PartialEq)]
314#[cfg_attr(feature = "visitor", derive(Visit))]
315#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
316#[cfg_attr(
317  feature = "serde",
318  derive(serde::Serialize, serde::Deserialize),
319  serde(tag = "type", content = "value", rename_all = "kebab-case")
320)]
321#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
322pub enum FilterList<'i> {
323  /// The `none` keyword.
324  None,
325  /// A list of filter functions.
326  #[cfg_attr(feature = "serde", serde(borrow))]
327  Filters(SmallVec<[Filter<'i>; 1]>),
328}
329
330impl<'i> Parse<'i> for FilterList<'i> {
331  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
332    if input.try_parse(|input| input.expect_ident_matching("none")).is_ok() {
333      return Ok(FilterList::None);
334    }
335
336    let mut filters = SmallVec::new();
337    while let Ok(filter) = input.try_parse(Filter::parse) {
338      filters.push(filter);
339    }
340
341    Ok(FilterList::Filters(filters))
342  }
343}
344
345impl<'i> ToCss for FilterList<'i> {
346  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
347  where
348    W: std::fmt::Write,
349  {
350    match self {
351      FilterList::None => dest.write_str("none"),
352      FilterList::Filters(filters) => {
353        let mut first = true;
354        for filter in filters {
355          if first {
356            first = false;
357          } else {
358            dest.whitespace()?;
359          }
360          filter.to_css(dest)?;
361        }
362        Ok(())
363      }
364    }
365  }
366}
367
368impl<'i> FallbackValues for FilterList<'i> {
369  fn get_fallbacks(&mut self, targets: Targets) -> Vec<Self> {
370    let mut res = Vec::new();
371    let mut fallbacks = ColorFallbackKind::empty();
372    if let FilterList::Filters(filters) = self {
373      for shadow in filters.iter() {
374        if let Filter::DropShadow(shadow) = &shadow {
375          fallbacks |= shadow.color.get_necessary_fallbacks(targets);
376        }
377      }
378
379      if fallbacks.contains(ColorFallbackKind::RGB) {
380        res.push(FilterList::Filters(
381          filters
382            .iter()
383            .map(|filter| filter.get_fallback(ColorFallbackKind::RGB))
384            .collect(),
385        ));
386      }
387
388      if fallbacks.contains(ColorFallbackKind::P3) {
389        res.push(FilterList::Filters(
390          filters
391            .iter()
392            .map(|filter| filter.get_fallback(ColorFallbackKind::P3))
393            .collect(),
394        ));
395      }
396
397      if fallbacks.contains(ColorFallbackKind::LAB) {
398        for filter in filters.iter_mut() {
399          *filter = filter.get_fallback(ColorFallbackKind::LAB);
400        }
401      }
402    }
403
404    res
405  }
406}
407
408impl IsCompatible for FilterList<'_> {
409  fn is_compatible(&self, _browsers: Browsers) -> bool {
410    true
411  }
412}