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