//! CSS properties related to filters and effects. use crate::error::{ParserError, PrinterError}; use crate::printer::Printer; use crate::targets::{Browsers, Targets}; use crate::traits::{FallbackValues, IsCompatible, Parse, ToCss, Zero}; use crate::values::color::ColorFallbackKind; use crate::values::{angle::Angle, color::CssColor, length::Length, percentage::NumberOrPercentage, url::Url}; #[cfg(feature = "visitor")] use crate::visitor::Visit; use cssparser::*; use smallvec::SmallVec; /// A [filter](https://drafts.fxtf.org/filter-effects-1/#filter-functions) function. #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] #[cfg_attr( feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(tag = "type", content = "value", rename_all = "kebab-case") )] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] pub enum Filter<'i> { /// A `blur()` filter. Blur(Length), /// A `brightness()` filter. Brightness(NumberOrPercentage), /// A `contrast()` filter. Contrast(NumberOrPercentage), /// A `grayscale()` filter. Grayscale(NumberOrPercentage), /// A `hue-rotate()` filter. HueRotate(Angle), /// An `invert()` filter. Invert(NumberOrPercentage), /// An `opacity()` filter. Opacity(NumberOrPercentage), /// A `saturate()` filter. Saturate(NumberOrPercentage), /// A `sepia()` filter. Sepia(NumberOrPercentage), /// A `drop-shadow()` filter. DropShadow(DropShadow), /// A `url()` reference to an SVG filter. #[cfg_attr(feature = "serde", serde(borrow))] Url(Url<'i>), } impl<'i> Parse<'i> for Filter<'i> { fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { if let Ok(url) = input.try_parse(Url::parse) { return Ok(Filter::Url(url)); } let location = input.current_source_location(); let function = input.expect_function()?; match_ignore_ascii_case! { &function, "blur" => { input.parse_nested_block(|input| { Ok(Filter::Blur(input.try_parse(Length::parse).unwrap_or(Length::zero()))) }) }, "brightness" => { input.parse_nested_block(|input| { Ok(Filter::Brightness(input.try_parse(NumberOrPercentage::parse).unwrap_or(NumberOrPercentage::Number(1.0)))) }) }, "contrast" => { input.parse_nested_block(|input| { Ok(Filter::Contrast(input.try_parse(NumberOrPercentage::parse).unwrap_or(NumberOrPercentage::Number(1.0)))) }) }, "grayscale" => { input.parse_nested_block(|input| { Ok(Filter::Grayscale(input.try_parse(NumberOrPercentage::parse).unwrap_or(NumberOrPercentage::Number(1.0)))) }) }, "hue-rotate" => { input.parse_nested_block(|input| { // Spec has an exception for unitless zero angles: https://github.com/w3c/fxtf-drafts/issues/228 Ok(Filter::HueRotate(input.try_parse(Angle::parse_with_unitless_zero).unwrap_or(Angle::zero()))) }) }, "invert" => { input.parse_nested_block(|input| { Ok(Filter::Invert(input.try_parse(NumberOrPercentage::parse).unwrap_or(NumberOrPercentage::Number(1.0)))) }) }, "opacity" => { input.parse_nested_block(|input| { Ok(Filter::Opacity(input.try_parse(NumberOrPercentage::parse).unwrap_or(NumberOrPercentage::Number(1.0)))) }) }, "saturate" => { input.parse_nested_block(|input| { Ok(Filter::Saturate(input.try_parse(NumberOrPercentage::parse).unwrap_or(NumberOrPercentage::Number(1.0)))) }) }, "sepia" => { input.parse_nested_block(|input| { Ok(Filter::Sepia(input.try_parse(NumberOrPercentage::parse).unwrap_or(NumberOrPercentage::Number(1.0)))) }) }, "drop-shadow" => { input.parse_nested_block(|input| { Ok(Filter::DropShadow(DropShadow::parse(input)?)) }) }, _ => Err(location.new_unexpected_token_error( cssparser::Token::Ident(function.clone()) )) } } } impl<'i> ToCss for Filter<'i> { fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { match self { Filter::Blur(val) => { dest.write_str("blur(")?; if *val != Length::zero() { val.to_css(dest)?; } dest.write_char(')') } Filter::Brightness(val) => { dest.write_str("brightness(")?; let v: f32 = val.into(); if v != 1.0 { val.to_css(dest)?; } dest.write_char(')') } Filter::Contrast(val) => { dest.write_str("contrast(")?; let v: f32 = val.into(); if v != 1.0 { val.to_css(dest)?; } dest.write_char(')') } Filter::Grayscale(val) => { dest.write_str("grayscale(")?; let v: f32 = val.into(); if v != 1.0 { val.to_css(dest)?; } dest.write_char(')') } Filter::HueRotate(val) => { dest.write_str("hue-rotate(")?; if !val.is_zero() { val.to_css(dest)?; } dest.write_char(')') } Filter::Invert(val) => { dest.write_str("invert(")?; let v: f32 = val.into(); if v != 1.0 { val.to_css(dest)?; } dest.write_char(')') } Filter::Opacity(val) => { dest.write_str("opacity(")?; let v: f32 = val.into(); if v != 1.0 { val.to_css(dest)?; } dest.write_char(')') } Filter::Saturate(val) => { dest.write_str("saturate(")?; let v: f32 = val.into(); if v != 1.0 { val.to_css(dest)?; } dest.write_char(')') } Filter::Sepia(val) => { dest.write_str("sepia(")?; let v: f32 = val.into(); if v != 1.0 { val.to_css(dest)?; } dest.write_char(')') } Filter::DropShadow(val) => { dest.write_str("drop-shadow(")?; val.to_css(dest)?; dest.write_char(')') } Filter::Url(url) => url.to_css(dest), } } } impl<'i> Filter<'i> { fn get_fallback(&self, kind: ColorFallbackKind) -> Self { match self { Filter::DropShadow(shadow) => Filter::DropShadow(shadow.get_fallback(kind)), _ => self.clone(), } } } impl IsCompatible for Filter<'_> { fn is_compatible(&self, _browsers: Browsers) -> bool { true } } /// A [`drop-shadow()`](https://drafts.fxtf.org/filter-effects-1/#funcdef-filter-drop-shadow) filter function. #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(rename_all = "camelCase") )] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] pub struct DropShadow { /// The color of the drop shadow. pub color: CssColor, /// The x offset of the drop shadow. pub x_offset: Length, /// The y offset of the drop shadow. pub y_offset: Length, /// The blur radius of the drop shadow. pub blur: Length, } impl<'i> Parse<'i> for DropShadow { fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { let mut color = None; let mut lengths = None; loop { if lengths.is_none() { let value = input.try_parse::<_, _, ParseError>>(|input| { let horizontal = Length::parse(input)?; let vertical = Length::parse(input)?; let blur = input.try_parse(Length::parse).unwrap_or(Length::zero()); Ok((horizontal, vertical, blur)) }); if let Ok(value) = value { lengths = Some(value); continue; } } if color.is_none() { if let Ok(value) = input.try_parse(CssColor::parse) { color = Some(value); continue; } } break; } let lengths = lengths.ok_or(input.new_error(BasicParseErrorKind::QualifiedRuleInvalid))?; Ok(DropShadow { color: color.unwrap_or(CssColor::current_color()), x_offset: lengths.0, y_offset: lengths.1, blur: lengths.2, }) } } impl ToCss for DropShadow { fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { self.x_offset.to_css(dest)?; dest.write_char(' ')?; self.y_offset.to_css(dest)?; if self.blur != Length::zero() { dest.write_char(' ')?; self.blur.to_css(dest)?; } if self.color != CssColor::current_color() { dest.write_char(' ')?; self.color.to_css(dest)?; } Ok(()) } } impl DropShadow { fn get_fallback(&self, kind: ColorFallbackKind) -> DropShadow { DropShadow { color: self.color.get_fallback(kind), ..self.clone() } } } /// A value for the [filter](https://drafts.fxtf.org/filter-effects-1/#FilterProperty) and /// [backdrop-filter](https://drafts.fxtf.org/filter-effects-2/#BackdropFilterProperty) properties. #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] #[cfg_attr( feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(tag = "type", content = "value", rename_all = "kebab-case") )] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] pub enum FilterList<'i> { /// The `none` keyword. None, /// A list of filter functions. #[cfg_attr(feature = "serde", serde(borrow))] Filters(SmallVec<[Filter<'i>; 1]>), } impl<'i> Parse<'i> for FilterList<'i> { fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { if input.try_parse(|input| input.expect_ident_matching("none")).is_ok() { return Ok(FilterList::None); } let mut filters = SmallVec::new(); while let Ok(filter) = input.try_parse(Filter::parse) { filters.push(filter); } Ok(FilterList::Filters(filters)) } } impl<'i> ToCss for FilterList<'i> { fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { match self { FilterList::None => dest.write_str("none"), FilterList::Filters(filters) => { let mut first = true; for filter in filters { if first { first = false; } else { dest.whitespace()?; } filter.to_css(dest)?; } Ok(()) } } } } impl<'i> FallbackValues for FilterList<'i> { fn get_fallbacks(&mut self, targets: Targets) -> Vec { let mut res = Vec::new(); let mut fallbacks = ColorFallbackKind::empty(); if let FilterList::Filters(filters) = self { for shadow in filters.iter() { if let Filter::DropShadow(shadow) = &shadow { fallbacks |= shadow.color.get_necessary_fallbacks(targets); } } if fallbacks.contains(ColorFallbackKind::RGB) { res.push(FilterList::Filters( filters .iter() .map(|filter| filter.get_fallback(ColorFallbackKind::RGB)) .collect(), )); } if fallbacks.contains(ColorFallbackKind::P3) { res.push(FilterList::Filters( filters .iter() .map(|filter| filter.get_fallback(ColorFallbackKind::P3)) .collect(), )); } if fallbacks.contains(ColorFallbackKind::LAB) { for filter in filters.iter_mut() { *filter = filter.get_fallback(ColorFallbackKind::LAB); } } } res } } impl IsCompatible for FilterList<'_> { fn is_compatible(&self, _browsers: Browsers) -> bool { true } }