lightningcss/properties/
masking.rs

1//! CSS properties related to clipping and masking.
2
3use super::background::{BackgroundRepeat, BackgroundSize};
4use super::border_image::{BorderImage, BorderImageRepeat, BorderImageSideWidth, BorderImageSlice};
5use super::PropertyId;
6use crate::context::PropertyHandlerContext;
7use crate::declaration::{DeclarationBlock, DeclarationList};
8use crate::error::{ParserError, PrinterError};
9use crate::macros::{define_list_shorthand, define_shorthand, enum_property, property_bitflags};
10use crate::prefixes::Feature;
11use crate::printer::Printer;
12use crate::properties::Property;
13use crate::targets::{Browsers, Targets};
14use crate::traits::{FallbackValues, IsCompatible, Parse, PropertyHandler, Shorthand, ToCss};
15use crate::values::image::ImageFallback;
16use crate::values::length::LengthOrNumber;
17use crate::values::rect::Rect;
18use crate::values::{image::Image, position::Position, shape::BasicShape, url::Url};
19use crate::vendor_prefix::VendorPrefix;
20#[cfg(feature = "visitor")]
21use crate::visitor::Visit;
22use cssparser::*;
23use itertools::izip;
24use smallvec::SmallVec;
25
26enum_property! {
27  /// A value for the [mask-type](https://www.w3.org/TR/css-masking-1/#the-mask-type) property.
28  pub enum MaskType {
29    /// The luminance values of the mask is used.
30    Luminance,
31    /// The alpha values of the mask is used.
32    Alpha,
33  }
34}
35
36enum_property! {
37  /// A value for the [mask-mode](https://www.w3.org/TR/css-masking-1/#the-mask-mode) property.
38  pub enum MaskMode {
39    /// The luminance values of the mask image is used.
40    Luminance,
41    /// The alpha values of the mask image is used.
42    Alpha,
43    /// If an SVG source is used, the value matches the `mask-type` property. Otherwise, the alpha values are used.
44    MatchSource,
45  }
46}
47
48impl Default for MaskMode {
49  fn default() -> MaskMode {
50    MaskMode::MatchSource
51  }
52}
53
54enum_property! {
55  /// A value for the [-webkit-mask-source-type](https://github.com/WebKit/WebKit/blob/6eece09a1c31e47489811edd003d1e36910e9fd3/Source/WebCore/css/CSSProperties.json#L6578-L6587)
56  /// property.
57  ///
58  /// See also [MaskMode](MaskMode).
59  pub enum WebKitMaskSourceType {
60    /// Equivalent to `match-source` in the standard `mask-mode` syntax.
61    Auto,
62    /// The luminance values of the mask image is used.
63    Luminance,
64    /// The alpha values of the mask image is used.
65    Alpha,
66  }
67}
68
69impl From<MaskMode> for WebKitMaskSourceType {
70  fn from(mode: MaskMode) -> WebKitMaskSourceType {
71    match mode {
72      MaskMode::Luminance => WebKitMaskSourceType::Luminance,
73      MaskMode::Alpha => WebKitMaskSourceType::Alpha,
74      MaskMode::MatchSource => WebKitMaskSourceType::Auto,
75    }
76  }
77}
78
79enum_property! {
80  /// A [`<geometry-box>`](https://www.w3.org/TR/css-masking-1/#typedef-geometry-box) value
81  /// as used in the `mask-clip` and `clip-path` properties.
82  pub enum GeometryBox {
83    /// The painted content is clipped to the content box.
84    BorderBox,
85    /// The painted content is clipped to the padding box.
86    PaddingBox,
87    /// The painted content is clipped to the border box.
88    ContentBox,
89    /// The painted content is clipped to the margin box.
90    MarginBox,
91    /// The painted content is clipped to the object bounding box.
92    FillBox,
93    /// The painted content is clipped to the stroke bounding box.
94    StrokeBox,
95    /// Uses the nearest SVG viewport as reference box.
96    ViewBox,
97  }
98}
99
100impl Default for GeometryBox {
101  fn default() -> GeometryBox {
102    GeometryBox::BorderBox
103  }
104}
105
106/// A value for the [mask-clip](https://www.w3.org/TR/css-masking-1/#the-mask-clip) property.
107#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
108#[cfg_attr(feature = "visitor", derive(Visit))]
109#[cfg_attr(
110  feature = "serde",
111  derive(serde::Serialize, serde::Deserialize),
112  serde(tag = "type", content = "value", rename_all = "kebab-case")
113)]
114#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
115#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
116pub enum MaskClip {
117  /// A geometry box.
118  GeometryBox(GeometryBox),
119  /// The painted content is not clipped.
120  NoClip,
121}
122
123impl IsCompatible for MaskClip {
124  fn is_compatible(&self, browsers: Browsers) -> bool {
125    match self {
126      MaskClip::GeometryBox(g) => g.is_compatible(browsers),
127      MaskClip::NoClip => true,
128    }
129  }
130}
131
132impl Into<MaskClip> for GeometryBox {
133  fn into(self) -> MaskClip {
134    MaskClip::GeometryBox(self.clone())
135  }
136}
137
138impl IsCompatible for GeometryBox {
139  fn is_compatible(&self, _browsers: Browsers) -> bool {
140    true
141  }
142}
143
144enum_property! {
145  /// A value for the [mask-composite](https://www.w3.org/TR/css-masking-1/#the-mask-composite) property.
146  pub enum MaskComposite {
147    /// The source is placed over the destination.
148    Add,
149    /// The source is placed, where it falls outside of the destination.
150    Subtract,
151    /// The parts of source that overlap the destination, replace the destination.
152    Intersect,
153    /// The non-overlapping regions of source and destination are combined.
154    Exclude,
155  }
156}
157
158impl Default for MaskComposite {
159  fn default() -> MaskComposite {
160    MaskComposite::Add
161  }
162}
163
164enum_property! {
165  /// A value for the [-webkit-mask-composite](https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-mask-composite)
166  /// property.
167  ///
168  /// See also [MaskComposite](MaskComposite).
169  #[allow(missing_docs)]
170  pub enum WebKitMaskComposite {
171    Clear,
172    Copy,
173    /// Equivalent to `add` in the standard `mask-composite` syntax.
174    SourceOver,
175    /// Equivalent to `intersect` in the standard `mask-composite` syntax.
176    SourceIn,
177    /// Equivalent to `subtract` in the standard `mask-composite` syntax.
178    SourceOut,
179    SourceAtop,
180    DestinationOver,
181    DestinationIn,
182    DestinationOut,
183    DestinationAtop,
184    /// Equivalent to `exclude` in the standard `mask-composite` syntax.
185    Xor,
186  }
187}
188
189impl From<MaskComposite> for WebKitMaskComposite {
190  fn from(composite: MaskComposite) -> WebKitMaskComposite {
191    match composite {
192      MaskComposite::Add => WebKitMaskComposite::SourceOver,
193      MaskComposite::Subtract => WebKitMaskComposite::SourceOut,
194      MaskComposite::Intersect => WebKitMaskComposite::SourceIn,
195      MaskComposite::Exclude => WebKitMaskComposite::Xor,
196    }
197  }
198}
199
200define_list_shorthand! {
201  /// A value for the [mask](https://www.w3.org/TR/css-masking-1/#the-mask) shorthand property.
202  pub struct Mask<'i>(VendorPrefix) {
203    /// The mask image.
204    #[cfg_attr(feature = "serde", serde(borrow))]
205    image: MaskImage(Image<'i>, VendorPrefix),
206    /// The position of the mask.
207    position: MaskPosition(Position, VendorPrefix),
208    /// The size of the mask image.
209    size: MaskSize(BackgroundSize, VendorPrefix),
210    /// How the mask repeats.
211    repeat: MaskRepeat(BackgroundRepeat, VendorPrefix),
212    /// The box in which the mask is clipped.
213    clip: MaskClip(MaskClip, VendorPrefix),
214    /// The origin of the mask.
215    origin: MaskOrigin(GeometryBox, VendorPrefix),
216    /// How the mask is composited with the element.
217    composite: MaskComposite(MaskComposite),
218    /// How the mask image is interpreted.
219    mode: MaskMode(MaskMode),
220  }
221}
222
223impl<'i> Parse<'i> for Mask<'i> {
224  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
225    let mut image: Option<Image> = None;
226    let mut position: Option<Position> = None;
227    let mut size: Option<BackgroundSize> = None;
228    let mut repeat: Option<BackgroundRepeat> = None;
229    let mut clip: Option<MaskClip> = None;
230    let mut origin: Option<GeometryBox> = None;
231    let mut composite: Option<MaskComposite> = None;
232    let mut mode: Option<MaskMode> = None;
233
234    loop {
235      if image.is_none() {
236        if let Ok(value) = input.try_parse(Image::parse) {
237          image = Some(value);
238          continue;
239        }
240      }
241
242      if position.is_none() {
243        if let Ok(value) = input.try_parse(Position::parse) {
244          position = Some(value);
245          size = input
246            .try_parse(|input| {
247              input.expect_delim('/')?;
248              BackgroundSize::parse(input)
249            })
250            .ok();
251          continue;
252        }
253      }
254
255      if repeat.is_none() {
256        if let Ok(value) = input.try_parse(BackgroundRepeat::parse) {
257          repeat = Some(value);
258          continue;
259        }
260      }
261
262      if origin.is_none() {
263        if let Ok(value) = input.try_parse(GeometryBox::parse) {
264          origin = Some(value);
265          continue;
266        }
267      }
268
269      if clip.is_none() {
270        if let Ok(value) = input.try_parse(MaskClip::parse) {
271          clip = Some(value);
272          continue;
273        }
274      }
275
276      if composite.is_none() {
277        if let Ok(value) = input.try_parse(MaskComposite::parse) {
278          composite = Some(value);
279          continue;
280        }
281      }
282
283      if mode.is_none() {
284        if let Ok(value) = input.try_parse(MaskMode::parse) {
285          mode = Some(value);
286          continue;
287        }
288      }
289
290      break;
291    }
292
293    if clip.is_none() {
294      if let Some(origin) = origin {
295        clip = Some(origin.into());
296      }
297    }
298
299    Ok(Mask {
300      image: image.unwrap_or_default(),
301      position: position.unwrap_or_default(),
302      repeat: repeat.unwrap_or_default(),
303      size: size.unwrap_or_default(),
304      origin: origin.unwrap_or(GeometryBox::BorderBox),
305      clip: clip.unwrap_or(GeometryBox::BorderBox.into()),
306      composite: composite.unwrap_or(MaskComposite::Add),
307      mode: mode.unwrap_or(MaskMode::MatchSource),
308    })
309  }
310}
311
312impl<'i> ToCss for Mask<'i> {
313  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
314  where
315    W: std::fmt::Write,
316  {
317    self.image.to_css(dest)?;
318
319    if self.position != Position::default() || self.size != BackgroundSize::default() {
320      dest.write_char(' ')?;
321      self.position.to_css(dest)?;
322
323      if self.size != BackgroundSize::default() {
324        dest.delim('/', true)?;
325        self.size.to_css(dest)?;
326      }
327    }
328
329    if self.repeat != BackgroundRepeat::default() {
330      dest.write_char(' ')?;
331      self.repeat.to_css(dest)?;
332    }
333
334    if self.origin != GeometryBox::BorderBox || self.clip != GeometryBox::BorderBox.into() {
335      dest.write_char(' ')?;
336      self.origin.to_css(dest)?;
337
338      if self.clip != self.origin.into() {
339        dest.write_char(' ')?;
340        self.clip.to_css(dest)?;
341      }
342    }
343
344    if self.composite != MaskComposite::default() {
345      dest.write_char(' ')?;
346      self.composite.to_css(dest)?;
347    }
348
349    if self.mode != MaskMode::default() {
350      dest.write_char(' ')?;
351      self.mode.to_css(dest)?;
352    }
353
354    Ok(())
355  }
356}
357
358// TODO: shorthand handler?
359impl<'i> ImageFallback<'i> for Mask<'i> {
360  #[inline]
361  fn get_image(&self) -> &Image<'i> {
362    &self.image
363  }
364
365  #[inline]
366  fn with_image(&self, image: Image<'i>) -> Self {
367    Mask { image, ..self.clone() }
368  }
369}
370
371/// A value for the [clip-path](https://www.w3.org/TR/css-masking-1/#the-clip-path) property.
372#[derive(Debug, Clone, PartialEq)]
373#[cfg_attr(feature = "visitor", derive(Visit))]
374#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
375#[cfg_attr(
376  feature = "serde",
377  derive(serde::Serialize, serde::Deserialize),
378  serde(tag = "type", rename_all = "kebab-case")
379)]
380#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
381pub enum ClipPath<'i> {
382  /// No clip path.
383  None,
384  /// A url reference to an SVG path element.
385  #[cfg_attr(feature = "serde", serde(borrow, with = "crate::serialization::ValueWrapper::<Url>"))]
386  Url(Url<'i>),
387  /// A basic shape, positioned according to the reference box.
388  #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
389  Shape {
390    /// A basic shape.
391    shape: Box<BasicShape>,
392    /// A reference box that the shape is positioned according to.
393    reference_box: GeometryBox,
394  },
395  /// A reference box.
396  #[cfg_attr(feature = "serde", serde(with = "crate::serialization::ValueWrapper::<GeometryBox>"))]
397  Box(GeometryBox),
398}
399
400impl<'i> Parse<'i> for ClipPath<'i> {
401  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
402    if let Ok(url) = input.try_parse(Url::parse) {
403      return Ok(ClipPath::Url(url));
404    }
405
406    if let Ok(shape) = input.try_parse(BasicShape::parse) {
407      let b = input.try_parse(GeometryBox::parse).unwrap_or_default();
408      return Ok(ClipPath::Shape {
409        shape: Box::new(shape),
410        reference_box: b,
411      });
412    }
413
414    if let Ok(b) = input.try_parse(GeometryBox::parse) {
415      if let Ok(shape) = input.try_parse(BasicShape::parse) {
416        return Ok(ClipPath::Shape {
417          shape: Box::new(shape),
418          reference_box: b,
419        });
420      }
421      return Ok(ClipPath::Box(b));
422    }
423
424    input.expect_ident_matching("none")?;
425    Ok(ClipPath::None)
426  }
427}
428
429impl<'i> ToCss for ClipPath<'i> {
430  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
431  where
432    W: std::fmt::Write,
433  {
434    match self {
435      ClipPath::None => dest.write_str("none"),
436      ClipPath::Url(url) => url.to_css(dest),
437      ClipPath::Shape {
438        shape,
439        reference_box: b,
440      } => {
441        shape.to_css(dest)?;
442        if *b != GeometryBox::default() {
443          dest.write_char(' ')?;
444          b.to_css(dest)?;
445        }
446        Ok(())
447      }
448      ClipPath::Box(b) => b.to_css(dest),
449    }
450  }
451}
452
453enum_property! {
454  /// A value for the [mask-border-mode](https://www.w3.org/TR/css-masking-1/#the-mask-border-mode) property.
455  pub enum MaskBorderMode {
456    /// The luminance values of the mask image is used.
457    Luminance,
458    /// The alpha values of the mask image is used.
459    Alpha,
460  }
461}
462
463impl Default for MaskBorderMode {
464  fn default() -> MaskBorderMode {
465    MaskBorderMode::Alpha
466  }
467}
468
469define_shorthand! {
470  /// A value for the [mask-border](https://www.w3.org/TR/css-masking-1/#the-mask-border) shorthand property.
471  #[derive(Default)]
472  pub struct MaskBorder<'i> {
473    /// The mask image.
474    #[cfg_attr(feature = "serde", serde(borrow))]
475    source: MaskBorderSource(Image<'i>),
476    /// The offsets that define where the image is sliced.
477    slice: MaskBorderSlice(BorderImageSlice),
478    /// The width of the mask image.
479    width: MaskBorderWidth(Rect<BorderImageSideWidth>),
480    /// The amount that the image extends beyond the border box.
481    outset: MaskBorderOutset(Rect<LengthOrNumber>),
482    /// How the mask image is scaled and tiled.
483    repeat: MaskBorderRepeat(BorderImageRepeat),
484    /// How the mask image is interpreted.
485    mode: MaskBorderMode(MaskBorderMode),
486  }
487}
488
489impl<'i> Parse<'i> for MaskBorder<'i> {
490  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
491    let mut mode: Option<MaskBorderMode> = None;
492    let border_image = BorderImage::parse_with_callback(input, |input| {
493      if mode.is_none() {
494        if let Ok(value) = input.try_parse(MaskBorderMode::parse) {
495          mode = Some(value);
496          return true;
497        }
498      }
499      false
500    });
501
502    if border_image.is_ok() || mode.is_some() {
503      let border_image = border_image.unwrap_or_default();
504      Ok(MaskBorder {
505        source: border_image.source,
506        slice: border_image.slice,
507        width: border_image.width,
508        outset: border_image.outset,
509        repeat: border_image.repeat,
510        mode: mode.unwrap_or_default(),
511      })
512    } else {
513      Err(input.new_custom_error(ParserError::InvalidDeclaration))
514    }
515  }
516}
517
518impl<'i> ToCss for MaskBorder<'i> {
519  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
520  where
521    W: std::fmt::Write,
522  {
523    BorderImage::to_css_internal(&self.source, &self.slice, &self.width, &self.outset, &self.repeat, dest)?;
524    if self.mode != MaskBorderMode::default() {
525      dest.write_char(' ')?;
526      self.mode.to_css(dest)?;
527    }
528    Ok(())
529  }
530}
531
532impl<'i> FallbackValues for MaskBorder<'i> {
533  fn get_fallbacks(&mut self, targets: Targets) -> Vec<Self> {
534    self
535      .source
536      .get_fallbacks(targets)
537      .into_iter()
538      .map(|source| MaskBorder { source, ..self.clone() })
539      .collect()
540  }
541}
542
543impl<'i> Into<BorderImage<'i>> for MaskBorder<'i> {
544  fn into(self) -> BorderImage<'i> {
545    BorderImage {
546      source: self.source,
547      slice: self.slice,
548      width: self.width,
549      outset: self.outset,
550      repeat: self.repeat,
551    }
552  }
553}
554
555property_bitflags! {
556  #[derive(Default, Debug)]
557  struct MaskProperty: u16 {
558    const MaskImage(_vp) = 1 << 0;
559    const MaskPosition(_vp) = 1 << 1;
560    const MaskSize(_vp) = 1 << 2;
561    const MaskRepeat(_vp) = 1 << 3;
562    const MaskClip(_vp) = 1 << 4;
563    const MaskOrigin(_vp) = 1 << 5;
564    const MaskComposite = 1 << 6;
565    const MaskMode = 1 << 7;
566    const Mask(_vp) = Self::MaskImage.bits() | Self::MaskPosition.bits() | Self::MaskSize.bits() | Self::MaskRepeat.bits() | Self::MaskClip.bits() | Self::MaskOrigin.bits() | Self::MaskComposite.bits() | Self::MaskMode.bits();
567
568    const MaskBorderSource = 1 << 7;
569    const MaskBorderMode = 1 << 8;
570    const MaskBorderSlice = 1 << 9;
571    const MaskBorderWidth = 1 << 10;
572    const MaskBorderOutset = 1 << 11;
573    const MaskBorderRepeat = 1 << 12;
574    const MaskBorder = Self::MaskBorderSource.bits() | Self::MaskBorderMode.bits() | Self::MaskBorderSlice.bits() | Self::MaskBorderWidth.bits() | Self::MaskBorderOutset.bits() | Self::MaskBorderRepeat.bits();
575  }
576}
577
578#[derive(Default)]
579pub(crate) struct MaskHandler<'i> {
580  images: Option<(SmallVec<[Image<'i>; 1]>, VendorPrefix)>,
581  positions: Option<(SmallVec<[Position; 1]>, VendorPrefix)>,
582  sizes: Option<(SmallVec<[BackgroundSize; 1]>, VendorPrefix)>,
583  repeats: Option<(SmallVec<[BackgroundRepeat; 1]>, VendorPrefix)>,
584  clips: Option<(SmallVec<[MaskClip; 1]>, VendorPrefix)>,
585  origins: Option<(SmallVec<[GeometryBox; 1]>, VendorPrefix)>,
586  composites: Option<SmallVec<[MaskComposite; 1]>>,
587  modes: Option<SmallVec<[MaskMode; 1]>>,
588  border_source: Option<(Image<'i>, VendorPrefix)>,
589  border_mode: Option<MaskBorderMode>,
590  border_slice: Option<(BorderImageSlice, VendorPrefix)>,
591  border_width: Option<(Rect<BorderImageSideWidth>, VendorPrefix)>,
592  border_outset: Option<(Rect<LengthOrNumber>, VendorPrefix)>,
593  border_repeat: Option<(BorderImageRepeat, VendorPrefix)>,
594  flushed_properties: MaskProperty,
595  has_any: bool,
596}
597
598impl<'i> PropertyHandler<'i> for MaskHandler<'i> {
599  fn handle_property(
600    &mut self,
601    property: &Property<'i>,
602    dest: &mut DeclarationList<'i>,
603    context: &mut PropertyHandlerContext<'i, '_>,
604  ) -> bool {
605    macro_rules! maybe_flush {
606      ($prop: ident, $val: expr, $vp: expr) => {{
607        // If two vendor prefixes for the same property have different
608        // values, we need to flush what we have immediately to preserve order.
609        if let Some((val, prefixes)) = &self.$prop {
610          if val != $val && !prefixes.contains(*$vp) {
611            self.flush(dest, context);
612          }
613        }
614
615        if self.$prop.is_some() && matches!(context.targets.browsers, Some(targets) if !$val.is_compatible(targets)) {
616          self.flush(dest, context);
617        }
618      }};
619    }
620
621    macro_rules! property {
622      ($prop: ident, $val: expr, $vp: expr) => {{
623        maybe_flush!($prop, $val, $vp);
624
625        // Otherwise, update the value and add the prefix.
626        if let Some((val, prefixes)) = &mut self.$prop {
627          *val = $val.clone();
628          *prefixes |= *$vp;
629        } else {
630          self.$prop = Some(($val.clone(), *$vp));
631          self.has_any = true;
632        }
633      }};
634    }
635
636    macro_rules! border_shorthand {
637      ($val: expr, $vp: expr) => {
638        let source = $val.source.clone();
639        maybe_flush!(border_source, &source, &$vp);
640
641        let slice = $val.slice.clone();
642        maybe_flush!(border_slice, &slice, &$vp);
643
644        let width = $val.width.clone();
645        maybe_flush!(border_width, &width, &$vp);
646
647        let outset = $val.outset.clone();
648        maybe_flush!(border_outset, &outset, &$vp);
649
650        let repeat = $val.repeat.clone();
651        maybe_flush!(border_repeat, &repeat, &$vp);
652
653        property!(border_source, &source, &$vp);
654        property!(border_slice, &slice, &$vp);
655        property!(border_width, &width, &$vp);
656        property!(border_outset, &outset, &$vp);
657        property!(border_repeat, &repeat, &$vp);
658      };
659    }
660
661    match property {
662      Property::MaskImage(val, vp) => property!(images, val, vp),
663      Property::MaskPosition(val, vp) => property!(positions, val, vp),
664      Property::MaskSize(val, vp) => property!(sizes, val, vp),
665      Property::MaskRepeat(val, vp) => property!(repeats, val, vp),
666      Property::MaskClip(val, vp) => property!(clips, val, vp),
667      Property::MaskOrigin(val, vp) => property!(origins, val, vp),
668      Property::MaskComposite(val) => self.composites = Some(val.clone()),
669      Property::MaskMode(val) => self.modes = Some(val.clone()),
670      Property::Mask(val, prefix) => {
671        let images = val.iter().map(|b| b.image.clone()).collect();
672        maybe_flush!(images, &images, prefix);
673
674        let positions = val.iter().map(|b| b.position.clone()).collect();
675        maybe_flush!(positions, &positions, prefix);
676
677        let sizes = val.iter().map(|b| b.size.clone()).collect();
678        maybe_flush!(sizes, &sizes, prefix);
679
680        let repeats = val.iter().map(|b| b.repeat.clone()).collect();
681        maybe_flush!(repeats, &repeats, prefix);
682
683        let clips = val.iter().map(|b| b.clip.clone()).collect();
684        maybe_flush!(clips, &clips, prefix);
685
686        let origins = val.iter().map(|b| b.origin.clone()).collect();
687        maybe_flush!(origins, &origins, prefix);
688
689        self.composites = Some(val.iter().map(|b| b.composite.clone()).collect());
690        self.modes = Some(val.iter().map(|b| b.mode.clone()).collect());
691
692        property!(images, &images, prefix);
693        property!(positions, &positions, prefix);
694        property!(sizes, &sizes, prefix);
695        property!(repeats, &repeats, prefix);
696        property!(clips, &clips, prefix);
697        property!(origins, &origins, prefix);
698      }
699      Property::Unparsed(val) if is_mask_property(&val.property_id) => {
700        self.flush(dest, context);
701        let mut unparsed = val.get_prefixed(context.targets, Feature::Mask);
702        context.add_unparsed_fallbacks(&mut unparsed);
703        self
704          .flushed_properties
705          .insert(MaskProperty::try_from(&val.property_id).unwrap());
706        dest.push(Property::Unparsed(unparsed));
707      }
708      Property::MaskBorderSource(val) => property!(border_source, val, &VendorPrefix::None),
709      Property::WebKitMaskBoxImageSource(val, _) => property!(border_source, val, &VendorPrefix::WebKit),
710      Property::MaskBorderMode(val) => self.border_mode = Some(val.clone()),
711      Property::MaskBorderSlice(val) => property!(border_slice, val, &VendorPrefix::None),
712      Property::WebKitMaskBoxImageSlice(val, _) => property!(border_slice, val, &VendorPrefix::WebKit),
713      Property::MaskBorderWidth(val) => property!(border_width, val, &VendorPrefix::None),
714      Property::WebKitMaskBoxImageWidth(val, _) => property!(border_width, val, &VendorPrefix::WebKit),
715      Property::MaskBorderOutset(val) => property!(border_outset, val, &VendorPrefix::None),
716      Property::WebKitMaskBoxImageOutset(val, _) => property!(border_outset, val, &VendorPrefix::WebKit),
717      Property::MaskBorderRepeat(val) => property!(border_repeat, val, &VendorPrefix::None),
718      Property::WebKitMaskBoxImageRepeat(val, _) => property!(border_repeat, val, &VendorPrefix::WebKit),
719      Property::MaskBorder(val) => {
720        border_shorthand!(val, VendorPrefix::None);
721        self.border_mode = Some(val.mode.clone());
722      }
723      Property::WebKitMaskBoxImage(val, _) => {
724        border_shorthand!(val, VendorPrefix::WebKit);
725      }
726      Property::Unparsed(val) if is_mask_border_property(&val.property_id) => {
727        self.flush(dest, context);
728        // Add vendor prefixes and expand color fallbacks.
729        let mut val = val.clone();
730        let prefix = context
731          .targets
732          .prefixes(val.property_id.prefix().or_none(), Feature::MaskBorder);
733        if prefix.contains(VendorPrefix::WebKit) {
734          if let Some(property_id) = get_webkit_mask_property(&val.property_id) {
735            let mut clone = val.clone();
736            clone.property_id = property_id;
737            context.add_unparsed_fallbacks(&mut clone);
738            dest.push(Property::Unparsed(clone));
739          }
740        }
741
742        context.add_unparsed_fallbacks(&mut val);
743        self
744          .flushed_properties
745          .insert(MaskProperty::try_from(&val.property_id).unwrap());
746        dest.push(Property::Unparsed(val));
747      }
748      _ => return false,
749    }
750
751    self.has_any = true;
752    true
753  }
754
755  fn finalize(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
756    self.flush(dest, context);
757    self.flushed_properties = MaskProperty::empty();
758  }
759}
760
761impl<'i> MaskHandler<'i> {
762  fn flush(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
763    if !self.has_any {
764      return;
765    }
766
767    self.has_any = false;
768
769    self.flush_mask(dest, context);
770    self.flush_mask_border(dest, context);
771  }
772
773  fn flush_mask(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
774    let mut images = std::mem::take(&mut self.images);
775    let mut positions = std::mem::take(&mut self.positions);
776    let mut sizes = std::mem::take(&mut self.sizes);
777    let mut repeats = std::mem::take(&mut self.repeats);
778    let mut clips = std::mem::take(&mut self.clips);
779    let mut origins = std::mem::take(&mut self.origins);
780    let mut composites = std::mem::take(&mut self.composites);
781    let mut modes = std::mem::take(&mut self.modes);
782
783    if let (
784      Some((images, images_vp)),
785      Some((positions, positions_vp)),
786      Some((sizes, sizes_vp)),
787      Some((repeats, repeats_vp)),
788      Some((clips, clips_vp)),
789      Some((origins, origins_vp)),
790      Some(composites_val),
791      Some(mode_vals),
792    ) = (
793      &mut images,
794      &mut positions,
795      &mut sizes,
796      &mut repeats,
797      &mut clips,
798      &mut origins,
799      &mut composites,
800      &mut modes,
801    ) {
802      // Only use shorthand syntax if the number of masks matches on all properties.
803      let len = images.len();
804      let intersection = *images_vp & *positions_vp & *sizes_vp & *repeats_vp & *clips_vp & *origins_vp;
805      if !intersection.is_empty()
806        && positions.len() == len
807        && sizes.len() == len
808        && repeats.len() == len
809        && clips.len() == len
810        && origins.len() == len
811        && composites_val.len() == len
812        && mode_vals.len() == len
813      {
814        let mut masks: SmallVec<[Mask<'i>; 1]> = izip!(
815          images.drain(..),
816          positions.drain(..),
817          sizes.drain(..),
818          repeats.drain(..),
819          clips.drain(..),
820          origins.drain(..),
821          composites_val.drain(..),
822          mode_vals.drain(..)
823        )
824        .map(|(image, position, size, repeat, clip, origin, composite, mode)| Mask {
825          image,
826          position,
827          size,
828          repeat,
829          clip,
830          origin,
831          composite,
832          mode,
833        })
834        .collect();
835
836        let mut prefix = context.targets.prefixes(intersection, Feature::Mask);
837        if !self.flushed_properties.intersects(MaskProperty::Mask) {
838          for fallback in masks.get_fallbacks(context.targets) {
839            // Match prefix of fallback. e.g. -webkit-linear-gradient
840            // can only be used in -webkit-mask-image.
841            // However, if mask-image is unprefixed, gradients can still be.
842            let mut p = fallback
843              .iter()
844              .fold(VendorPrefix::empty(), |p, mask| p | mask.image.get_vendor_prefix())
845              - VendorPrefix::None
846              & prefix;
847            if p.is_empty() {
848              p = prefix;
849            }
850            self.flush_mask_shorthand(fallback, p, dest);
851          }
852
853          let p = masks
854            .iter()
855            .fold(VendorPrefix::empty(), |p, mask| p | mask.image.get_vendor_prefix())
856            - VendorPrefix::None
857            & prefix;
858          if !p.is_empty() {
859            prefix = p;
860          }
861        }
862
863        self.flush_mask_shorthand(masks, prefix, dest);
864        self.flushed_properties.insert(MaskProperty::Mask);
865
866        images_vp.remove(intersection);
867        positions_vp.remove(intersection);
868        sizes_vp.remove(intersection);
869        repeats_vp.remove(intersection);
870        clips_vp.remove(intersection);
871        origins_vp.remove(intersection);
872        composites = None;
873        modes = None;
874      }
875    }
876
877    macro_rules! prop {
878      ($var: ident, $property: ident) => {
879        if let Some((val, vp)) = $var {
880          if !vp.is_empty() {
881            let prefix = context.targets.prefixes(vp, Feature::$property);
882            dest.push(Property::$property(val, prefix));
883            self.flushed_properties.insert(MaskProperty::$property);
884          }
885        }
886      };
887    }
888
889    if let Some((mut images, vp)) = images {
890      if !vp.is_empty() {
891        let mut prefix = vp;
892        if !self.flushed_properties.contains(MaskProperty::MaskImage) {
893          prefix = context.targets.prefixes(prefix, Feature::MaskImage);
894          for fallback in images.get_fallbacks(context.targets) {
895            // Match prefix of fallback. e.g. -webkit-linear-gradient
896            // can only be used in -webkit-mask-image.
897            // However, if mask-image is unprefixed, gradients can still be.
898            let mut p = fallback
899              .iter()
900              .fold(VendorPrefix::empty(), |p, image| p | image.get_vendor_prefix())
901              - VendorPrefix::None
902              & prefix;
903            if p.is_empty() {
904              p = prefix;
905            }
906            dest.push(Property::MaskImage(fallback, p))
907          }
908
909          let p = images
910            .iter()
911            .fold(VendorPrefix::empty(), |p, image| p | image.get_vendor_prefix())
912            - VendorPrefix::None
913            & prefix;
914          if !p.is_empty() {
915            prefix = p;
916          }
917        }
918
919        dest.push(Property::MaskImage(images, prefix));
920        self.flushed_properties.insert(MaskProperty::MaskImage);
921      }
922    }
923
924    prop!(positions, MaskPosition);
925    prop!(sizes, MaskSize);
926    prop!(repeats, MaskRepeat);
927    prop!(clips, MaskClip);
928    prop!(origins, MaskOrigin);
929
930    if let Some(composites) = composites {
931      let prefix = context.targets.prefixes(VendorPrefix::None, Feature::MaskComposite);
932      if prefix.contains(VendorPrefix::WebKit) {
933        dest.push(Property::WebKitMaskComposite(
934          composites.iter().map(|c| (*c).into()).collect(),
935        ));
936      }
937
938      dest.push(Property::MaskComposite(composites));
939      self.flushed_properties.insert(MaskProperty::MaskComposite);
940    }
941
942    if let Some(modes) = modes {
943      let prefix = context.targets.prefixes(VendorPrefix::None, Feature::Mask);
944      if prefix.contains(VendorPrefix::WebKit) {
945        dest.push(Property::WebKitMaskSourceType(
946          modes.iter().map(|c| (*c).into()).collect(),
947          VendorPrefix::WebKit,
948        ));
949      }
950
951      dest.push(Property::MaskMode(modes));
952      self.flushed_properties.insert(MaskProperty::MaskMode);
953    }
954  }
955
956  fn flush_mask_shorthand(
957    &self,
958    masks: SmallVec<[Mask<'i>; 1]>,
959    prefix: VendorPrefix,
960    dest: &mut DeclarationList<'i>,
961  ) {
962    if prefix.contains(VendorPrefix::WebKit)
963      && masks
964        .iter()
965        .any(|mask| mask.composite != MaskComposite::default() || mask.mode != MaskMode::default())
966    {
967      // Prefixed shorthand syntax did not support mask-composite or mask-mode. These map to different webkit-specific properties.
968      // -webkit-mask-composite uses a different syntax than mask-composite.
969      // -webkit-mask-source-type is equivalent to mask-mode, but only supported in Safari, not Chrome.
970      let mut webkit = masks.clone();
971      let mut composites: SmallVec<[WebKitMaskComposite; 1]> = SmallVec::new();
972      let mut modes: SmallVec<[WebKitMaskSourceType; 1]> = SmallVec::new();
973      let mut needs_composites = false;
974      let mut needs_modes = false;
975      for mask in &mut webkit {
976        let composite = std::mem::take(&mut mask.composite);
977        if composite != MaskComposite::default() {
978          needs_composites = true;
979        }
980        composites.push(composite.into());
981
982        let mode = std::mem::take(&mut mask.mode);
983        if mode != MaskMode::default() {
984          needs_modes = true;
985        }
986        modes.push(mode.into());
987      }
988
989      dest.push(Property::Mask(webkit, VendorPrefix::WebKit));
990      if needs_composites {
991        dest.push(Property::WebKitMaskComposite(composites));
992      }
993      if needs_modes {
994        dest.push(Property::WebKitMaskSourceType(modes, VendorPrefix::WebKit));
995      }
996
997      let prefix = prefix - VendorPrefix::WebKit;
998      if !prefix.is_empty() {
999        dest.push(Property::Mask(masks, prefix));
1000      }
1001    } else {
1002      dest.push(Property::Mask(masks, prefix));
1003    }
1004  }
1005
1006  fn flush_mask_border(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
1007    let mut source = std::mem::take(&mut self.border_source);
1008    let mut slice = std::mem::take(&mut self.border_slice);
1009    let mut width = std::mem::take(&mut self.border_width);
1010    let mut outset = std::mem::take(&mut self.border_outset);
1011    let mut repeat = std::mem::take(&mut self.border_repeat);
1012    let mut mode = std::mem::take(&mut self.border_mode);
1013
1014    if let (
1015      Some((source, source_vp)),
1016      Some((slice, slice_vp)),
1017      Some((width, width_vp)),
1018      Some((outset, outset_vp)),
1019      Some((repeat, repeat_vp)),
1020    ) = (&mut source, &mut slice, &mut width, &mut outset, &mut repeat)
1021    {
1022      let intersection = *source_vp & *slice_vp & *width_vp & *outset_vp & *repeat_vp;
1023      if !intersection.is_empty() && (!intersection.contains(VendorPrefix::None) || mode.is_some()) {
1024        let mut mask_border = MaskBorder {
1025          source: source.clone(),
1026          slice: slice.clone(),
1027          width: width.clone(),
1028          outset: outset.clone(),
1029          repeat: repeat.clone(),
1030          mode: mode.unwrap_or_default(),
1031        };
1032
1033        let mut prefix = context.targets.prefixes(intersection, Feature::MaskBorder);
1034        if !self.flushed_properties.intersects(MaskProperty::MaskBorder) {
1035          // Get vendor prefix and color fallbacks.
1036          let fallbacks = mask_border.get_fallbacks(context.targets);
1037          for fallback in fallbacks {
1038            let mut p = fallback.source.get_vendor_prefix() - VendorPrefix::None & prefix;
1039            if p.is_empty() {
1040              p = prefix;
1041            }
1042
1043            if p.contains(VendorPrefix::WebKit) {
1044              dest.push(Property::WebKitMaskBoxImage(
1045                fallback.clone().into(),
1046                VendorPrefix::WebKit,
1047              ));
1048            }
1049
1050            if p.contains(VendorPrefix::None) {
1051              dest.push(Property::MaskBorder(fallback));
1052            }
1053          }
1054        }
1055
1056        let p = mask_border.source.get_vendor_prefix() - VendorPrefix::None & prefix;
1057        if !p.is_empty() {
1058          prefix = p;
1059        }
1060
1061        if prefix.contains(VendorPrefix::WebKit) {
1062          dest.push(Property::WebKitMaskBoxImage(
1063            mask_border.clone().into(),
1064            VendorPrefix::WebKit,
1065          ));
1066        }
1067
1068        if prefix.contains(VendorPrefix::None) {
1069          dest.push(Property::MaskBorder(mask_border));
1070          self.flushed_properties.insert(MaskProperty::MaskBorder);
1071          mode = None;
1072        }
1073
1074        source_vp.remove(intersection);
1075        slice_vp.remove(intersection);
1076        width_vp.remove(intersection);
1077        outset_vp.remove(intersection);
1078        repeat_vp.remove(intersection);
1079      }
1080    }
1081
1082    if let Some((mut source, mut prefix)) = source {
1083      prefix = context.targets.prefixes(prefix, Feature::MaskBorderSource);
1084
1085      if !self.flushed_properties.contains(MaskProperty::MaskBorderSource) {
1086        // Get vendor prefix and color fallbacks.
1087        let fallbacks = source.get_fallbacks(context.targets);
1088        for fallback in fallbacks {
1089          if prefix.contains(VendorPrefix::WebKit) {
1090            dest.push(Property::WebKitMaskBoxImageSource(
1091              fallback.clone(),
1092              VendorPrefix::WebKit,
1093            ));
1094          }
1095
1096          if prefix.contains(VendorPrefix::None) {
1097            dest.push(Property::MaskBorderSource(fallback));
1098          }
1099        }
1100      }
1101
1102      if prefix.contains(VendorPrefix::WebKit) {
1103        dest.push(Property::WebKitMaskBoxImageSource(source.clone(), VendorPrefix::WebKit));
1104      }
1105
1106      if prefix.contains(VendorPrefix::None) {
1107        dest.push(Property::MaskBorderSource(source));
1108        self.flushed_properties.insert(MaskProperty::MaskBorderSource);
1109      }
1110    }
1111
1112    macro_rules! prop {
1113      ($val: expr, $prop: ident, $webkit: ident) => {
1114        if let Some((val, mut prefix)) = $val {
1115          prefix = context.targets.prefixes(prefix, Feature::$prop);
1116          if prefix.contains(VendorPrefix::WebKit) {
1117            dest.push(Property::$webkit(val.clone(), VendorPrefix::WebKit));
1118          }
1119
1120          if prefix.contains(VendorPrefix::None) {
1121            dest.push(Property::$prop(val));
1122          }
1123          self.flushed_properties.insert(MaskProperty::$prop);
1124        }
1125      };
1126    }
1127
1128    prop!(slice, MaskBorderSlice, WebKitMaskBoxImageSlice);
1129    prop!(width, MaskBorderWidth, WebKitMaskBoxImageWidth);
1130    prop!(outset, MaskBorderOutset, WebKitMaskBoxImageOutset);
1131    prop!(repeat, MaskBorderRepeat, WebKitMaskBoxImageRepeat);
1132
1133    if let Some(mode) = mode {
1134      dest.push(Property::MaskBorderMode(mode));
1135      self.flushed_properties.insert(MaskProperty::MaskBorderMode);
1136    }
1137  }
1138}
1139
1140#[inline]
1141fn is_mask_property(property_id: &PropertyId) -> bool {
1142  match property_id {
1143    PropertyId::MaskImage(_)
1144    | PropertyId::MaskPosition(_)
1145    | PropertyId::MaskSize(_)
1146    | PropertyId::MaskRepeat(_)
1147    | PropertyId::MaskClip(_)
1148    | PropertyId::MaskOrigin(_)
1149    | PropertyId::MaskComposite
1150    | PropertyId::MaskMode
1151    | PropertyId::Mask(_) => true,
1152    _ => false,
1153  }
1154}
1155
1156#[inline]
1157fn is_mask_border_property(property_id: &PropertyId) -> bool {
1158  match property_id {
1159    PropertyId::MaskBorderSource
1160    | PropertyId::MaskBorderSlice
1161    | PropertyId::MaskBorderWidth
1162    | PropertyId::MaskBorderOutset
1163    | PropertyId::MaskBorderRepeat
1164    | PropertyId::MaskBorderMode
1165    | PropertyId::MaskBorder => true,
1166    _ => false,
1167  }
1168}
1169
1170#[inline]
1171pub(crate) fn get_webkit_mask_property(property_id: &PropertyId) -> Option<PropertyId<'static>> {
1172  Some(match property_id {
1173    PropertyId::MaskBorderSource => PropertyId::WebKitMaskBoxImageSource(VendorPrefix::WebKit),
1174    PropertyId::MaskBorderSlice => PropertyId::WebKitMaskBoxImageSlice(VendorPrefix::WebKit),
1175    PropertyId::MaskBorderWidth => PropertyId::WebKitMaskBoxImageWidth(VendorPrefix::WebKit),
1176    PropertyId::MaskBorderOutset => PropertyId::WebKitMaskBoxImageOutset(VendorPrefix::WebKit),
1177    PropertyId::MaskBorderRepeat => PropertyId::WebKitMaskBoxImageRepeat(VendorPrefix::WebKit),
1178    PropertyId::MaskBorder => PropertyId::WebKitMaskBoxImage(VendorPrefix::WebKit),
1179    PropertyId::MaskComposite => PropertyId::WebKitMaskComposite,
1180    PropertyId::MaskMode => PropertyId::WebKitMaskSourceType(VendorPrefix::WebKit),
1181    _ => return None,
1182  })
1183}