Skip to main content

lightningcss/properties/
effects.rs

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