lightningcss/values/
gradient.rs

1//! CSS gradient values.
2
3use super::angle::{Angle, AnglePercentage};
4use super::color::{ColorFallbackKind, CssColor};
5use super::length::{Length, LengthPercentage};
6use super::number::CSSNumber;
7use super::percentage::{DimensionPercentage, NumberOrPercentage, Percentage};
8use super::position::{HorizontalPositionKeyword, VerticalPositionKeyword};
9use super::position::{Position, PositionComponent};
10use crate::compat;
11use crate::error::{ParserError, PrinterError};
12use crate::macros::enum_property;
13use crate::prefixes::Feature;
14use crate::printer::Printer;
15use crate::targets::{should_compile, Browsers, Targets};
16use crate::traits::{IsCompatible, Parse, ToCss, TrySign, Zero};
17use crate::vendor_prefix::VendorPrefix;
18#[cfg(feature = "visitor")]
19use crate::visitor::Visit;
20use cssparser::*;
21
22#[cfg(feature = "serde")]
23use crate::serialization::ValueWrapper;
24
25/// A CSS [`<gradient>`](https://www.w3.org/TR/css-images-3/#gradients) value.
26#[derive(Debug, Clone, PartialEq)]
27#[cfg_attr(feature = "visitor", derive(Visit))]
28#[cfg_attr(
29  feature = "serde",
30  derive(serde::Serialize, serde::Deserialize),
31  serde(tag = "type", rename_all = "kebab-case")
32)]
33#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
34#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
35pub enum Gradient {
36  /// A `linear-gradient()`, and its vendor prefix.
37  Linear(LinearGradient),
38  /// A `repeating-linear-gradient()`, and its vendor prefix.
39  RepeatingLinear(LinearGradient),
40  /// A `radial-gradient()`, and its vendor prefix.
41  Radial(RadialGradient),
42  /// A `repeating-radial-gradient`, and its vendor prefix.
43  RepeatingRadial(RadialGradient),
44  /// A `conic-gradient()`.
45  Conic(ConicGradient),
46  /// A `repeating-conic-gradient()`.
47  RepeatingConic(ConicGradient),
48  /// A legacy `-webkit-gradient()`.
49  #[cfg_attr(feature = "serde", serde(rename = "webkit-gradient"))]
50  WebKitGradient(WebKitGradient),
51}
52
53impl Gradient {
54  /// Returns the vendor prefix of the gradient.
55  pub fn get_vendor_prefix(&self) -> VendorPrefix {
56    match self {
57      Gradient::Linear(LinearGradient { vendor_prefix, .. })
58      | Gradient::RepeatingLinear(LinearGradient { vendor_prefix, .. })
59      | Gradient::Radial(RadialGradient { vendor_prefix, .. })
60      | Gradient::RepeatingRadial(RadialGradient { vendor_prefix, .. }) => *vendor_prefix,
61      Gradient::WebKitGradient(_) => VendorPrefix::WebKit,
62      _ => VendorPrefix::None,
63    }
64  }
65
66  /// Returns the vendor prefixes needed for the given browser targets.
67  pub fn get_necessary_prefixes(&self, targets: Targets) -> VendorPrefix {
68    macro_rules! get_prefixes {
69      ($feature: ident, $prefix: expr) => {
70        targets.prefixes($prefix, Feature::$feature)
71      };
72    }
73
74    match self {
75      Gradient::Linear(linear) => get_prefixes!(LinearGradient, linear.vendor_prefix),
76      Gradient::RepeatingLinear(linear) => get_prefixes!(RepeatingLinearGradient, linear.vendor_prefix),
77      Gradient::Radial(radial) => get_prefixes!(RadialGradient, radial.vendor_prefix),
78      Gradient::RepeatingRadial(radial) => get_prefixes!(RepeatingRadialGradient, radial.vendor_prefix),
79      _ => VendorPrefix::None,
80    }
81  }
82
83  /// Returns a copy of the gradient with the given vendor prefix.
84  pub fn get_prefixed(&self, prefix: VendorPrefix) -> Gradient {
85    match self {
86      Gradient::Linear(linear) => Gradient::Linear(LinearGradient {
87        vendor_prefix: prefix,
88        ..linear.clone()
89      }),
90      Gradient::RepeatingLinear(linear) => Gradient::RepeatingLinear(LinearGradient {
91        vendor_prefix: prefix,
92        ..linear.clone()
93      }),
94      Gradient::Radial(radial) => Gradient::Radial(RadialGradient {
95        vendor_prefix: prefix,
96        ..radial.clone()
97      }),
98      Gradient::RepeatingRadial(radial) => Gradient::RepeatingRadial(RadialGradient {
99        vendor_prefix: prefix,
100        ..radial.clone()
101      }),
102      _ => self.clone(),
103    }
104  }
105
106  /// Attempts to convert the gradient to the legacy `-webkit-gradient()` syntax.
107  ///
108  /// Returns an error in case the conversion is not possible.
109  pub fn get_legacy_webkit(&self) -> Result<Gradient, ()> {
110    Ok(Gradient::WebKitGradient(WebKitGradient::from_standard(self)?))
111  }
112
113  /// Returns the color fallback types needed for the given browser targets.
114  pub fn get_necessary_fallbacks(&self, targets: Targets) -> ColorFallbackKind {
115    match self {
116      Gradient::Linear(LinearGradient { items, .. })
117      | Gradient::Radial(RadialGradient { items, .. })
118      | Gradient::RepeatingLinear(LinearGradient { items, .. })
119      | Gradient::RepeatingRadial(RadialGradient { items, .. }) => {
120        let mut fallbacks = ColorFallbackKind::empty();
121        for item in items {
122          fallbacks |= item.get_necessary_fallbacks(targets)
123        }
124        fallbacks
125      }
126      Gradient::Conic(ConicGradient { items, .. }) | Gradient::RepeatingConic(ConicGradient { items, .. }) => {
127        let mut fallbacks = ColorFallbackKind::empty();
128        for item in items {
129          fallbacks |= item.get_necessary_fallbacks(targets)
130        }
131        fallbacks
132      }
133      Gradient::WebKitGradient(..) => ColorFallbackKind::empty(),
134    }
135  }
136
137  /// Returns a fallback gradient for the given color fallback type.
138  pub fn get_fallback(&self, kind: ColorFallbackKind) -> Gradient {
139    match self {
140      Gradient::Linear(g) => Gradient::Linear(g.get_fallback(kind)),
141      Gradient::RepeatingLinear(g) => Gradient::RepeatingLinear(g.get_fallback(kind)),
142      Gradient::Radial(g) => Gradient::Radial(g.get_fallback(kind)),
143      Gradient::RepeatingRadial(g) => Gradient::RepeatingRadial(g.get_fallback(kind)),
144      Gradient::Conic(g) => Gradient::Conic(g.get_fallback(kind)),
145      Gradient::RepeatingConic(g) => Gradient::RepeatingConic(g.get_fallback(kind)),
146      Gradient::WebKitGradient(g) => Gradient::WebKitGradient(g.get_fallback(kind)),
147    }
148  }
149}
150
151impl<'i> Parse<'i> for Gradient {
152  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
153    let location = input.current_source_location();
154    let func = input.expect_function()?.clone();
155    input.parse_nested_block(|input| {
156      match_ignore_ascii_case! { &func,
157        "linear-gradient" => Ok(Gradient::Linear(LinearGradient::parse(input, VendorPrefix::None)?)),
158        "repeating-linear-gradient" => Ok(Gradient::RepeatingLinear(LinearGradient::parse(input, VendorPrefix::None)?)),
159        "radial-gradient" => Ok(Gradient::Radial(RadialGradient::parse(input, VendorPrefix::None)?)),
160        "repeating-radial-gradient" => Ok(Gradient::RepeatingRadial(RadialGradient::parse(input, VendorPrefix::None)?)),
161        "conic-gradient" => Ok(Gradient::Conic(ConicGradient::parse(input)?)),
162        "repeating-conic-gradient" => Ok(Gradient::RepeatingConic(ConicGradient::parse(input)?)),
163        "-webkit-linear-gradient" => Ok(Gradient::Linear(LinearGradient::parse(input, VendorPrefix::WebKit)?)),
164        "-webkit-repeating-linear-gradient" => Ok(Gradient::RepeatingLinear(LinearGradient::parse(input, VendorPrefix::WebKit)?)),
165        "-webkit-radial-gradient" => Ok(Gradient::Radial(RadialGradient::parse(input, VendorPrefix::WebKit)?)),
166        "-webkit-repeating-radial-gradient" => Ok(Gradient::RepeatingRadial(RadialGradient::parse(input, VendorPrefix::WebKit)?)),
167        "-moz-linear-gradient" => Ok(Gradient::Linear(LinearGradient::parse(input, VendorPrefix::Moz)?)),
168        "-moz-repeating-linear-gradient" => Ok(Gradient::RepeatingLinear(LinearGradient::parse(input, VendorPrefix::Moz)?)),
169        "-moz-radial-gradient" => Ok(Gradient::Radial(RadialGradient::parse(input, VendorPrefix::Moz)?)),
170        "-moz-repeating-radial-gradient" => Ok(Gradient::RepeatingRadial(RadialGradient::parse(input, VendorPrefix::Moz)?)),
171        "-o-linear-gradient" => Ok(Gradient::Linear(LinearGradient::parse(input, VendorPrefix::O)?)),
172        "-o-repeating-linear-gradient" => Ok(Gradient::RepeatingLinear(LinearGradient::parse(input, VendorPrefix::O)?)),
173        "-o-radial-gradient" => Ok(Gradient::Radial(RadialGradient::parse(input, VendorPrefix::O)?)),
174        "-o-repeating-radial-gradient" => Ok(Gradient::RepeatingRadial(RadialGradient::parse(input, VendorPrefix::O)?)),
175        "-webkit-gradient" => Ok(Gradient::WebKitGradient(WebKitGradient::parse(input)?)),
176        _ => Err(location.new_unexpected_token_error(cssparser::Token::Ident(func.clone())))
177      }
178    })
179  }
180}
181
182impl ToCss for Gradient {
183  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
184  where
185    W: std::fmt::Write,
186  {
187    let (f, prefix) = match self {
188      Gradient::Linear(g) => ("linear-gradient(", Some(g.vendor_prefix)),
189      Gradient::RepeatingLinear(g) => ("repeating-linear-gradient(", Some(g.vendor_prefix)),
190      Gradient::Radial(g) => ("radial-gradient(", Some(g.vendor_prefix)),
191      Gradient::RepeatingRadial(g) => ("repeating-radial-gradient(", Some(g.vendor_prefix)),
192      Gradient::Conic(_) => ("conic-gradient(", None),
193      Gradient::RepeatingConic(_) => ("repeating-conic-gradient(", None),
194      Gradient::WebKitGradient(_) => ("-webkit-gradient(", None),
195    };
196
197    if let Some(prefix) = prefix {
198      prefix.to_css(dest)?;
199    }
200
201    dest.write_str(f)?;
202
203    match self {
204      Gradient::Linear(linear) | Gradient::RepeatingLinear(linear) => {
205        linear.to_css(dest, linear.vendor_prefix != VendorPrefix::None)?
206      }
207      Gradient::Radial(radial) | Gradient::RepeatingRadial(radial) => radial.to_css(dest)?,
208      Gradient::Conic(conic) | Gradient::RepeatingConic(conic) => conic.to_css(dest)?,
209      Gradient::WebKitGradient(g) => g.to_css(dest)?,
210    }
211
212    dest.write_char(')')
213  }
214}
215
216/// A CSS [`linear-gradient()`](https://www.w3.org/TR/css-images-3/#linear-gradients) or `repeating-linear-gradient()`.
217#[derive(Debug, Clone, PartialEq)]
218#[cfg_attr(feature = "visitor", derive(Visit))]
219#[cfg_attr(
220  feature = "serde",
221  derive(serde::Serialize, serde::Deserialize),
222  serde(rename_all = "camelCase")
223)]
224#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
225#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
226pub struct LinearGradient {
227  /// The vendor prefixes for the gradient.
228  pub vendor_prefix: VendorPrefix,
229  /// The direction of the gradient.
230  pub direction: LineDirection,
231  /// The color stops and transition hints for the gradient.
232  pub items: Vec<GradientItem<LengthPercentage>>,
233}
234
235impl LinearGradient {
236  fn parse<'i, 't>(
237    input: &mut Parser<'i, 't>,
238    vendor_prefix: VendorPrefix,
239  ) -> Result<LinearGradient, ParseError<'i, ParserError<'i>>> {
240    let direction = if let Ok(direction) =
241      input.try_parse(|input| LineDirection::parse(input, vendor_prefix != VendorPrefix::None))
242    {
243      input.expect_comma()?;
244      direction
245    } else {
246      LineDirection::Vertical(VerticalPositionKeyword::Bottom)
247    };
248    let items = parse_items(input)?;
249    Ok(LinearGradient {
250      direction,
251      items,
252      vendor_prefix,
253    })
254  }
255
256  fn to_css<W>(&self, dest: &mut Printer<W>, is_prefixed: bool) -> Result<(), PrinterError>
257  where
258    W: std::fmt::Write,
259  {
260    let angle = match &self.direction {
261      LineDirection::Vertical(VerticalPositionKeyword::Bottom) => 180.0,
262      LineDirection::Vertical(VerticalPositionKeyword::Top) => 0.0,
263      LineDirection::Angle(angle) => angle.to_degrees(),
264      _ => -1.0,
265    };
266
267    // We can omit `to bottom` or `180deg` because it is the default.
268    if angle == 180.0 {
269      serialize_items(&self.items, dest)
270
271    // If we have `to top` or `0deg`, and all of the positions and hints are percentages,
272    // we can flip the gradient the other direction and omit the direction.
273    } else if angle == 0.0
274      && dest.minify
275      && self.items.iter().all(|item| {
276        matches!(
277          item,
278          GradientItem::Hint(LengthPercentage::Percentage(_))
279            | GradientItem::ColorStop(ColorStop {
280              position: None | Some(LengthPercentage::Percentage(_)),
281              ..
282            })
283        )
284      })
285    {
286      let items: Vec<GradientItem<LengthPercentage>> = self
287        .items
288        .iter()
289        .rev()
290        .map(|item| {
291          // Flip percentages.
292          match item {
293            GradientItem::Hint(LengthPercentage::Percentage(p)) => {
294              GradientItem::Hint(LengthPercentage::Percentage(Percentage(1.0 - p.0)))
295            }
296            GradientItem::ColorStop(ColorStop { color, position }) => GradientItem::ColorStop(ColorStop {
297              color: color.clone(),
298              position: position.clone().map(|p| match p {
299                LengthPercentage::Percentage(p) => LengthPercentage::Percentage(Percentage(1.0 - p.0)),
300                _ => unreachable!(),
301              }),
302            }),
303            _ => unreachable!(),
304          }
305        })
306        .collect();
307      serialize_items(&items, dest)
308    } else {
309      if self.direction != LineDirection::Vertical(VerticalPositionKeyword::Bottom)
310        && self.direction != LineDirection::Angle(Angle::Deg(180.0))
311      {
312        self.direction.to_css(dest, is_prefixed)?;
313        dest.delim(',', false)?;
314      }
315
316      serialize_items(&self.items, dest)
317    }
318  }
319
320  fn get_fallback(&self, kind: ColorFallbackKind) -> LinearGradient {
321    LinearGradient {
322      direction: self.direction.clone(),
323      items: self.items.iter().map(|item| item.get_fallback(kind)).collect(),
324      vendor_prefix: self.vendor_prefix,
325    }
326  }
327}
328
329impl IsCompatible for LinearGradient {
330  fn is_compatible(&self, browsers: Browsers) -> bool {
331    self.items.iter().all(|item| item.is_compatible(browsers))
332  }
333}
334
335/// A CSS [`radial-gradient()`](https://www.w3.org/TR/css-images-3/#radial-gradients) or `repeating-radial-gradient()`.
336#[derive(Debug, Clone, PartialEq)]
337#[cfg_attr(feature = "visitor", derive(Visit))]
338#[cfg_attr(
339  feature = "serde",
340  derive(serde::Serialize, serde::Deserialize),
341  serde(rename_all = "camelCase")
342)]
343#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
344#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
345pub struct RadialGradient {
346  /// The vendor prefixes for the gradient.
347  pub vendor_prefix: VendorPrefix,
348  /// The shape of the gradient.
349  pub shape: EndingShape,
350  /// The position of the gradient.
351  pub position: Position,
352  /// The color stops and transition hints for the gradient.
353  pub items: Vec<GradientItem<LengthPercentage>>,
354}
355
356impl<'i> RadialGradient {
357  fn parse<'t>(
358    input: &mut Parser<'i, 't>,
359    vendor_prefix: VendorPrefix,
360  ) -> Result<RadialGradient, ParseError<'i, ParserError<'i>>> {
361    let shape = input.try_parse(EndingShape::parse).ok();
362    let position = input
363      .try_parse(|input| {
364        input.expect_ident_matching("at")?;
365        Position::parse(input)
366      })
367      .ok();
368
369    if shape.is_some() || position.is_some() {
370      input.expect_comma()?;
371    }
372
373    let items = parse_items(input)?;
374    Ok(RadialGradient {
375      shape: shape.unwrap_or_default(),
376      position: position.unwrap_or(Position::center()),
377      items,
378      vendor_prefix,
379    })
380  }
381}
382
383impl ToCss for RadialGradient {
384  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
385  where
386    W: std::fmt::Write,
387  {
388    if self.shape != EndingShape::default() {
389      self.shape.to_css(dest)?;
390      if self.position.is_center() {
391        dest.delim(',', false)?;
392      } else {
393        dest.write_char(' ')?;
394      }
395    }
396
397    if !self.position.is_center() {
398      dest.write_str("at ")?;
399      self.position.to_css(dest)?;
400      dest.delim(',', false)?;
401    }
402
403    serialize_items(&self.items, dest)
404  }
405}
406
407impl RadialGradient {
408  fn get_fallback(&self, kind: ColorFallbackKind) -> RadialGradient {
409    RadialGradient {
410      shape: self.shape.clone(),
411      position: self.position.clone(),
412      items: self.items.iter().map(|item| item.get_fallback(kind)).collect(),
413      vendor_prefix: self.vendor_prefix,
414    }
415  }
416}
417
418impl IsCompatible for RadialGradient {
419  fn is_compatible(&self, browsers: Browsers) -> bool {
420    self.items.iter().all(|item| item.is_compatible(browsers))
421  }
422}
423
424/// The direction of a CSS `linear-gradient()`.
425///
426/// See [LinearGradient](LinearGradient).
427#[derive(Debug, Clone, PartialEq)]
428#[cfg_attr(feature = "visitor", derive(Visit))]
429#[cfg_attr(
430  feature = "serde",
431  derive(serde::Serialize, serde::Deserialize),
432  serde(tag = "type", rename_all = "kebab-case")
433)]
434#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
435pub enum LineDirection {
436  /// An angle.
437  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<Angle>"))]
438  Angle(Angle),
439  /// A horizontal position keyword, e.g. `left` or `right.
440  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<HorizontalPositionKeyword>"))]
441  Horizontal(HorizontalPositionKeyword),
442  /// A vertical posision keyword, e.g. `top` or `bottom`.
443  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<VerticalPositionKeyword>"))]
444  Vertical(VerticalPositionKeyword),
445  /// A corner, e.g. `bottom left` or `top right`.
446  Corner {
447    /// A horizontal position keyword, e.g. `left` or `right.
448    horizontal: HorizontalPositionKeyword,
449    /// A vertical posision keyword, e.g. `top` or `bottom`.
450    vertical: VerticalPositionKeyword,
451  },
452}
453
454impl LineDirection {
455  fn parse<'i, 't>(
456    input: &mut Parser<'i, 't>,
457    is_prefixed: bool,
458  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
459    // Spec allows unitless zero angles for gradients.
460    // https://w3c.github.io/csswg-drafts/css-images-3/#linear-gradient-syntax
461    if let Ok(angle) = input.try_parse(Angle::parse_with_unitless_zero) {
462      return Ok(LineDirection::Angle(angle));
463    }
464
465    if !is_prefixed {
466      input.expect_ident_matching("to")?;
467    }
468
469    if let Ok(x) = input.try_parse(HorizontalPositionKeyword::parse) {
470      if let Ok(y) = input.try_parse(VerticalPositionKeyword::parse) {
471        return Ok(LineDirection::Corner {
472          horizontal: x,
473          vertical: y,
474        });
475      }
476      return Ok(LineDirection::Horizontal(x));
477    }
478
479    let y = VerticalPositionKeyword::parse(input)?;
480    if let Ok(x) = input.try_parse(HorizontalPositionKeyword::parse) {
481      return Ok(LineDirection::Corner {
482        horizontal: x,
483        vertical: y,
484      });
485    }
486    Ok(LineDirection::Vertical(y))
487  }
488
489  fn to_css<W>(&self, dest: &mut Printer<W>, is_prefixed: bool) -> Result<(), PrinterError>
490  where
491    W: std::fmt::Write,
492  {
493    match self {
494      LineDirection::Angle(angle) => angle.to_css(dest),
495      LineDirection::Horizontal(k) => {
496        if dest.minify {
497          match k {
498            HorizontalPositionKeyword::Left => dest.write_str("270deg"),
499            HorizontalPositionKeyword::Right => dest.write_str("90deg"),
500          }
501        } else {
502          if !is_prefixed {
503            dest.write_str("to ")?;
504          }
505          k.to_css(dest)
506        }
507      }
508      LineDirection::Vertical(k) => {
509        if dest.minify {
510          match k {
511            VerticalPositionKeyword::Top => dest.write_str("0deg"),
512            VerticalPositionKeyword::Bottom => dest.write_str("180deg"),
513          }
514        } else {
515          if !is_prefixed {
516            dest.write_str("to ")?;
517          }
518          k.to_css(dest)
519        }
520      }
521      LineDirection::Corner { horizontal, vertical } => {
522        if !is_prefixed {
523          dest.write_str("to ")?;
524        }
525        vertical.to_css(dest)?;
526        dest.write_char(' ')?;
527        horizontal.to_css(dest)
528      }
529    }
530  }
531}
532
533/// A `radial-gradient()` [ending shape](https://www.w3.org/TR/css-images-3/#valdef-radial-gradient-ending-shape).
534///
535/// See [RadialGradient](RadialGradient).
536#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
537#[cfg_attr(feature = "visitor", derive(Visit))]
538#[cfg_attr(
539  feature = "serde",
540  derive(serde::Serialize, serde::Deserialize),
541  serde(tag = "type", content = "value", rename_all = "kebab-case")
542)]
543#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
544pub enum EndingShape {
545  // Note: Ellipse::parse MUST run before Circle::parse for this to be correct.
546  /// An ellipse.
547  Ellipse(Ellipse),
548  /// A circle.
549  Circle(Circle),
550}
551
552impl Default for EndingShape {
553  fn default() -> EndingShape {
554    EndingShape::Ellipse(Ellipse::Extent(ShapeExtent::FarthestCorner))
555  }
556}
557
558/// A circle ending shape for a `radial-gradient()`.
559///
560/// See [RadialGradient](RadialGradient).
561#[derive(Debug, Clone, PartialEq)]
562#[cfg_attr(feature = "visitor", derive(Visit))]
563#[cfg_attr(
564  feature = "serde",
565  derive(serde::Serialize, serde::Deserialize),
566  serde(tag = "type", content = "value", rename_all = "kebab-case")
567)]
568#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
569pub enum Circle {
570  /// A circle with a specified radius.
571  Radius(Length),
572  /// A shape extent keyword.
573  Extent(ShapeExtent),
574}
575
576impl<'i> Parse<'i> for Circle {
577  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
578    if let Ok(extent) = input.try_parse(ShapeExtent::parse) {
579      // The `circle` keyword is required. If it's not there, then it's an ellipse.
580      input.expect_ident_matching("circle")?;
581      return Ok(Circle::Extent(extent));
582    }
583
584    if let Ok(length) = input.try_parse(Length::parse) {
585      // The `circle` keyword is optional if there is only a single length.
586      // We are assuming here that Ellipse::parse ran first.
587      let _ = input.try_parse(|input| input.expect_ident_matching("circle"));
588      return Ok(Circle::Radius(length));
589    }
590
591    if input.try_parse(|input| input.expect_ident_matching("circle")).is_ok() {
592      if let Ok(extent) = input.try_parse(ShapeExtent::parse) {
593        return Ok(Circle::Extent(extent));
594      }
595
596      if let Ok(length) = input.try_parse(Length::parse) {
597        return Ok(Circle::Radius(length));
598      }
599
600      // If only the `circle` keyword was given, default to `farthest-corner`.
601      return Ok(Circle::Extent(ShapeExtent::FarthestCorner));
602    }
603
604    return Err(input.new_error_for_next_token());
605  }
606}
607
608impl ToCss for Circle {
609  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
610  where
611    W: std::fmt::Write,
612  {
613    match self {
614      Circle::Radius(r) => r.to_css(dest),
615      Circle::Extent(extent) => {
616        dest.write_str("circle")?;
617        if *extent != ShapeExtent::FarthestCorner {
618          dest.write_char(' ')?;
619          extent.to_css(dest)?;
620        }
621        Ok(())
622      }
623    }
624  }
625}
626
627/// An ellipse ending shape for a `radial-gradient()`.
628///
629/// See [RadialGradient](RadialGradient).
630#[derive(Debug, Clone, PartialEq)]
631#[cfg_attr(feature = "visitor", derive(Visit))]
632#[cfg_attr(
633  feature = "serde",
634  derive(serde::Serialize, serde::Deserialize),
635  serde(tag = "type", rename_all = "kebab-case")
636)]
637#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
638pub enum Ellipse {
639  /// An ellipse with a specified horizontal and vertical radius.
640  Size {
641    /// The x-radius of the ellipse.
642    x: LengthPercentage,
643    /// The y-radius of the ellipse.
644    y: LengthPercentage,
645  },
646  /// A shape extent keyword.
647  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<ShapeExtent>"))]
648  Extent(ShapeExtent),
649}
650
651impl<'i> Parse<'i> for Ellipse {
652  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
653    if let Ok(extent) = input.try_parse(ShapeExtent::parse) {
654      // The `ellipse` keyword is optional, but only if the `circle` keyword is not present.
655      // If it is, then we'll re-parse as a circle.
656      if input.try_parse(|input| input.expect_ident_matching("circle")).is_ok() {
657        return Err(input.new_error_for_next_token());
658      }
659      let _ = input.try_parse(|input| input.expect_ident_matching("ellipse"));
660      return Ok(Ellipse::Extent(extent));
661    }
662
663    if let Ok(x) = input.try_parse(LengthPercentage::parse) {
664      let y = LengthPercentage::parse(input)?;
665      // The `ellipse` keyword is optional if there are two lengths.
666      let _ = input.try_parse(|input| input.expect_ident_matching("ellipse"));
667      return Ok(Ellipse::Size { x, y });
668    }
669
670    if input.try_parse(|input| input.expect_ident_matching("ellipse")).is_ok() {
671      if let Ok(extent) = input.try_parse(ShapeExtent::parse) {
672        return Ok(Ellipse::Extent(extent));
673      }
674
675      if let Ok(x) = input.try_parse(LengthPercentage::parse) {
676        let y = LengthPercentage::parse(input)?;
677        return Ok(Ellipse::Size { x, y });
678      }
679
680      // Assume `farthest-corner` if only the `ellipse` keyword is present.
681      return Ok(Ellipse::Extent(ShapeExtent::FarthestCorner));
682    }
683
684    return Err(input.new_error_for_next_token());
685  }
686}
687
688impl ToCss for Ellipse {
689  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
690  where
691    W: std::fmt::Write,
692  {
693    // The `ellipse` keyword is optional, so we don't emit it.
694    match self {
695      Ellipse::Size { x, y } => {
696        x.to_css(dest)?;
697        dest.write_char(' ')?;
698        y.to_css(dest)
699      }
700      Ellipse::Extent(extent) => extent.to_css(dest),
701    }
702  }
703}
704
705enum_property! {
706  /// A shape extent for a `radial-gradient()`.
707  ///
708  /// See [RadialGradient](RadialGradient).
709  pub enum ShapeExtent {
710    /// The closest side of the box to the gradient's center.
711    ClosestSide,
712    /// The farthest side of the box from the gradient's center.
713    FarthestSide,
714    /// The closest cornder of the box to the gradient's center.
715    ClosestCorner,
716    /// The farthest corner of the box from the gradient's center.
717    FarthestCorner,
718  }
719}
720
721/// A CSS [`conic-gradient()`](https://www.w3.org/TR/css-images-4/#conic-gradients) or `repeating-conic-gradient()`.
722#[derive(Debug, Clone, PartialEq)]
723#[cfg_attr(feature = "visitor", derive(Visit))]
724#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
725#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
726#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
727pub struct ConicGradient {
728  /// The angle of the gradient.
729  pub angle: Angle,
730  /// The position of the gradient.
731  pub position: Position,
732  /// The color stops and transition hints for the gradient.
733  pub items: Vec<GradientItem<AnglePercentage>>,
734}
735
736impl ConicGradient {
737  fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
738    let angle = input.try_parse(|input| {
739      input.expect_ident_matching("from")?;
740      // Spec allows unitless zero angles for gradients.
741      // https://w3c.github.io/csswg-drafts/css-images-4/#valdef-conic-gradient-angle
742      Angle::parse_with_unitless_zero(input)
743    });
744
745    let position = input.try_parse(|input| {
746      input.expect_ident_matching("at")?;
747      Position::parse(input)
748    });
749
750    if angle.is_ok() || position.is_ok() {
751      input.expect_comma()?;
752    }
753
754    let items = parse_items(input)?;
755    Ok(ConicGradient {
756      angle: angle.unwrap_or(Angle::Deg(0.0)),
757      position: position.unwrap_or(Position::center()),
758      items,
759    })
760  }
761}
762
763impl ToCss for ConicGradient {
764  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
765  where
766    W: std::fmt::Write,
767  {
768    if !self.angle.is_zero() {
769      dest.write_str("from ")?;
770      self.angle.to_css(dest)?;
771
772      if self.position.is_center() {
773        dest.delim(',', false)?;
774      } else {
775        dest.write_char(' ')?;
776      }
777    }
778
779    if !self.position.is_center() {
780      dest.write_str("at ")?;
781      self.position.to_css(dest)?;
782      dest.delim(',', false)?;
783    }
784
785    serialize_items(&self.items, dest)
786  }
787}
788
789impl ConicGradient {
790  fn get_fallback(&self, kind: ColorFallbackKind) -> ConicGradient {
791    ConicGradient {
792      angle: self.angle.clone(),
793      position: self.position.clone(),
794      items: self.items.iter().map(|item| item.get_fallback(kind)).collect(),
795    }
796  }
797}
798
799impl IsCompatible for ConicGradient {
800  fn is_compatible(&self, browsers: Browsers) -> bool {
801    self.items.iter().all(|item| item.is_compatible(browsers))
802  }
803}
804
805/// A [`<color-stop>`](https://www.w3.org/TR/css-images-4/#color-stop-syntax) within a gradient.
806///
807/// This type is generic, and may be either a [LengthPercentage](super::length::LengthPercentage)
808/// or [Angle](super::angle::Angle) depending on what type of gradient it is within.
809#[derive(Debug, Clone, PartialEq)]
810#[cfg_attr(feature = "visitor", derive(Visit))]
811#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
812#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
813pub struct ColorStop<D> {
814  /// The color of the color stop.
815  pub color: CssColor,
816  /// The position of the color stop.
817  pub position: Option<D>,
818}
819
820impl<'i, D: Parse<'i>> Parse<'i> for ColorStop<D> {
821  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
822    let color = CssColor::parse(input)?;
823    let position = input.try_parse(D::parse).ok();
824    Ok(ColorStop { color, position })
825  }
826}
827
828impl<D: ToCss> ToCss for ColorStop<D> {
829  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
830  where
831    W: std::fmt::Write,
832  {
833    self.color.to_css(dest)?;
834    if let Some(position) = &self.position {
835      dest.write_char(' ')?;
836      position.to_css(dest)?;
837    }
838    Ok(())
839  }
840}
841
842/// Either a color stop or interpolation hint within a gradient.
843///
844/// This type is generic, and items may be either a [LengthPercentage](super::length::LengthPercentage)
845/// or [Angle](super::angle::Angle) depending on what type of gradient it is within.
846#[derive(Debug, Clone, PartialEq)]
847#[cfg_attr(feature = "visitor", derive(Visit))]
848#[cfg_attr(
849  feature = "serde",
850  derive(serde::Serialize, serde::Deserialize),
851  serde(tag = "type", rename_all = "kebab-case")
852)]
853#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
854pub enum GradientItem<D> {
855  /// A color stop.
856  ColorStop(ColorStop<D>),
857  /// A color interpolation hint.
858  #[cfg_attr(
859    feature = "serde",
860    serde(
861      bound(serialize = "D: serde::Serialize", deserialize = "D: serde::Deserialize<'de>"),
862      with = "ValueWrapper::<D>"
863    )
864  )]
865  Hint(D),
866}
867
868impl<D: ToCss> ToCss for GradientItem<D> {
869  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
870  where
871    W: std::fmt::Write,
872  {
873    match self {
874      GradientItem::ColorStop(stop) => stop.to_css(dest),
875      GradientItem::Hint(hint) => hint.to_css(dest),
876    }
877  }
878}
879
880impl<D: Clone> GradientItem<D> {
881  /// Returns the color fallback types needed for the given browser targets.
882  pub fn get_necessary_fallbacks(&self, targets: Targets) -> ColorFallbackKind {
883    match self {
884      GradientItem::ColorStop(stop) => stop.color.get_necessary_fallbacks(targets),
885      GradientItem::Hint(..) => ColorFallbackKind::empty(),
886    }
887  }
888
889  /// Returns a fallback gradient item for the given color fallback type.
890  pub fn get_fallback(&self, kind: ColorFallbackKind) -> GradientItem<D> {
891    match self {
892      GradientItem::ColorStop(stop) => GradientItem::ColorStop(ColorStop {
893        color: stop.color.get_fallback(kind),
894        position: stop.position.clone(),
895      }),
896      GradientItem::Hint(..) => self.clone(),
897    }
898  }
899}
900
901impl<D> IsCompatible for GradientItem<D> {
902  fn is_compatible(&self, browsers: Browsers) -> bool {
903    match self {
904      GradientItem::ColorStop(c) => c.color.is_compatible(browsers),
905      GradientItem::Hint(..) => compat::Feature::GradientInterpolationHints.is_compatible(browsers),
906    }
907  }
908}
909
910fn parse_items<'i, 't, D: Parse<'i>>(
911  input: &mut Parser<'i, 't>,
912) -> Result<Vec<GradientItem<D>>, ParseError<'i, ParserError<'i>>> {
913  let mut items = Vec::new();
914  let mut seen_stop = false;
915
916  loop {
917    input.parse_until_before(Delimiter::Comma, |input| {
918      if seen_stop {
919        if let Ok(hint) = input.try_parse(D::parse) {
920          seen_stop = false;
921          items.push(GradientItem::Hint(hint));
922          return Ok(());
923        }
924      }
925
926      let stop = ColorStop::parse(input)?;
927
928      if let Ok(position) = input.try_parse(D::parse) {
929        let color = stop.color.clone();
930        items.push(GradientItem::ColorStop(stop));
931
932        items.push(GradientItem::ColorStop(ColorStop {
933          color,
934          position: Some(position),
935        }))
936      } else {
937        items.push(GradientItem::ColorStop(stop));
938      }
939
940      seen_stop = true;
941      Ok(())
942    })?;
943
944    match input.next() {
945      Err(_) => break,
946      Ok(Token::Comma) => continue,
947      _ => unreachable!(),
948    }
949  }
950
951  Ok(items)
952}
953
954fn serialize_items<
955  D: ToCss + std::cmp::PartialEq<D> + std::ops::Mul<f32, Output = D> + TrySign + Clone + std::fmt::Debug,
956  W,
957>(
958  items: &Vec<GradientItem<DimensionPercentage<D>>>,
959  dest: &mut Printer<W>,
960) -> Result<(), PrinterError>
961where
962  W: std::fmt::Write,
963{
964  let mut first = true;
965  let mut last: Option<&GradientItem<DimensionPercentage<D>>> = None;
966  for item in items {
967    // Skip useless hints
968    if *item == GradientItem::Hint(DimensionPercentage::Percentage(Percentage(0.5))) {
969      continue;
970    }
971
972    // Use double position stop if the last stop is the same color and all targets support it.
973    if let Some(prev) = last {
974      if !should_compile!(dest.targets, DoublePositionGradients) {
975        match (prev, item) {
976          (
977            GradientItem::ColorStop(ColorStop {
978              position: Some(_),
979              color: ca,
980            }),
981            GradientItem::ColorStop(ColorStop {
982              position: Some(p),
983              color: cb,
984            }),
985          ) if ca == cb => {
986            dest.write_char(' ')?;
987            p.to_css(dest)?;
988            last = None;
989            continue;
990          }
991          _ => {}
992        }
993      }
994    }
995
996    if first {
997      first = false;
998    } else {
999      dest.delim(',', false)?;
1000    }
1001    item.to_css(dest)?;
1002    last = Some(item)
1003  }
1004  Ok(())
1005}
1006
1007/// A legacy `-webkit-gradient()`.
1008#[derive(Debug, Clone, PartialEq)]
1009#[cfg_attr(feature = "visitor", derive(Visit))]
1010#[cfg_attr(
1011  feature = "serde",
1012  derive(serde::Serialize, serde::Deserialize),
1013  serde(tag = "kind", rename_all = "kebab-case")
1014)]
1015#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1016#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
1017pub enum WebKitGradient {
1018  /// A linear `-webkit-gradient()`.
1019  Linear {
1020    /// The starting point of the gradient.
1021    from: WebKitGradientPoint,
1022    /// The ending point of the gradient.
1023    to: WebKitGradientPoint,
1024    /// The color stops in the gradient.
1025    stops: Vec<WebKitColorStop>,
1026  },
1027  /// A radial `-webkit-gradient()`.
1028  Radial {
1029    /// The starting point of the gradient.
1030    from: WebKitGradientPoint,
1031    /// The starting radius of the gradient.
1032    r0: CSSNumber,
1033    /// The ending point of the gradient.
1034    to: WebKitGradientPoint,
1035    /// The ending radius of the gradient.
1036    r1: CSSNumber,
1037    /// The color stops in the gradient.
1038    stops: Vec<WebKitColorStop>,
1039  },
1040}
1041
1042impl<'i> Parse<'i> for WebKitGradient {
1043  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
1044    let location = input.current_source_location();
1045    let ident = input.expect_ident_cloned()?;
1046    input.expect_comma()?;
1047
1048    match_ignore_ascii_case! { &ident,
1049      "linear" => {
1050        let from = WebKitGradientPoint::parse(input)?;
1051        input.expect_comma()?;
1052        let to = WebKitGradientPoint::parse(input)?;
1053        input.expect_comma()?;
1054        let stops = input.parse_comma_separated(WebKitColorStop::parse)?;
1055        Ok(WebKitGradient::Linear {
1056          from,
1057          to,
1058          stops
1059        })
1060      },
1061      "radial" => {
1062        let from = WebKitGradientPoint::parse(input)?;
1063        input.expect_comma()?;
1064        let r0 = CSSNumber::parse(input)?;
1065        input.expect_comma()?;
1066        let to = WebKitGradientPoint::parse(input)?;
1067        input.expect_comma()?;
1068        let r1 = CSSNumber::parse(input)?;
1069        input.expect_comma()?;
1070        let stops = input.parse_comma_separated(WebKitColorStop::parse)?;
1071        Ok(WebKitGradient::Radial {
1072          from,
1073          r0,
1074          to,
1075          r1,
1076          stops
1077        })
1078      },
1079      _ => Err(location.new_unexpected_token_error(cssparser::Token::Ident(ident.clone())))
1080    }
1081  }
1082}
1083
1084impl ToCss for WebKitGradient {
1085  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
1086  where
1087    W: std::fmt::Write,
1088  {
1089    match self {
1090      WebKitGradient::Linear { from, to, stops } => {
1091        dest.write_str("linear")?;
1092        dest.delim(',', false)?;
1093        from.to_css(dest)?;
1094        dest.delim(',', false)?;
1095        to.to_css(dest)?;
1096        for stop in stops {
1097          dest.delim(',', false)?;
1098          stop.to_css(dest)?;
1099        }
1100        Ok(())
1101      }
1102      WebKitGradient::Radial {
1103        from,
1104        r0,
1105        to,
1106        r1,
1107        stops,
1108      } => {
1109        dest.write_str("radial")?;
1110        dest.delim(',', false)?;
1111        from.to_css(dest)?;
1112        dest.delim(',', false)?;
1113        r0.to_css(dest)?;
1114        dest.delim(',', false)?;
1115        to.to_css(dest)?;
1116        dest.delim(',', false)?;
1117        r1.to_css(dest)?;
1118        for stop in stops {
1119          dest.delim(',', false)?;
1120          stop.to_css(dest)?;
1121        }
1122        Ok(())
1123      }
1124    }
1125  }
1126}
1127
1128impl WebKitGradient {
1129  fn get_fallback(&self, kind: ColorFallbackKind) -> WebKitGradient {
1130    let stops = match self {
1131      WebKitGradient::Linear { stops, .. } => stops,
1132      WebKitGradient::Radial { stops, .. } => stops,
1133    };
1134
1135    let stops = stops.iter().map(|stop| stop.get_fallback(kind)).collect();
1136
1137    match self {
1138      WebKitGradient::Linear { from, to, .. } => WebKitGradient::Linear {
1139        from: from.clone(),
1140        to: to.clone(),
1141        stops,
1142      },
1143      WebKitGradient::Radial { from, r0, to, r1, .. } => WebKitGradient::Radial {
1144        from: from.clone(),
1145        r0: *r0,
1146        to: to.clone(),
1147        r1: *r1,
1148        stops,
1149      },
1150    }
1151  }
1152}
1153
1154/// A color stop within a legacy `-webkit-gradient()`.
1155#[derive(Debug, Clone, PartialEq)]
1156#[cfg_attr(feature = "visitor", derive(Visit))]
1157#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1158#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1159pub struct WebKitColorStop {
1160  /// The color of the color stop.
1161  pub color: CssColor,
1162  /// The position of the color stop.
1163  pub position: CSSNumber,
1164}
1165
1166impl<'i> Parse<'i> for WebKitColorStop {
1167  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
1168    let location = input.current_source_location();
1169    let function = input.expect_function()?.clone();
1170    input.parse_nested_block(|input| {
1171      let position = match_ignore_ascii_case! { &function,
1172        "color-stop" => {
1173          let p = NumberOrPercentage::parse(input)?;
1174          input.expect_comma()?;
1175          (&p).into()
1176        },
1177        "from" => 0.0,
1178        "to" => 1.0,
1179        _ => return Err(location.new_unexpected_token_error(cssparser::Token::Ident(function.clone())))
1180      };
1181      let color = CssColor::parse(input)?;
1182      Ok(WebKitColorStop { color, position })
1183    })
1184  }
1185}
1186
1187impl ToCss for WebKitColorStop {
1188  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
1189  where
1190    W: std::fmt::Write,
1191  {
1192    if self.position == 0.0 {
1193      dest.write_str("from(")?;
1194      self.color.to_css(dest)?;
1195    } else if self.position == 1.0 {
1196      dest.write_str("to(")?;
1197      self.color.to_css(dest)?;
1198    } else {
1199      dest.write_str("color-stop(")?;
1200      self.position.to_css(dest)?;
1201      dest.delim(',', false)?;
1202      self.color.to_css(dest)?;
1203    }
1204    dest.write_char(')')
1205  }
1206}
1207
1208impl WebKitColorStop {
1209  fn get_fallback(&self, kind: ColorFallbackKind) -> WebKitColorStop {
1210    WebKitColorStop {
1211      color: self.color.get_fallback(kind),
1212      position: self.position,
1213    }
1214  }
1215}
1216
1217/// An x/y position within a legacy `-webkit-gradient()`.
1218#[derive(Debug, Clone, PartialEq)]
1219#[cfg_attr(feature = "visitor", derive(Visit))]
1220#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1221#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1222#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
1223pub struct WebKitGradientPoint {
1224  /// The x-position.
1225  pub x: WebKitGradientPointComponent<HorizontalPositionKeyword>,
1226  /// The y-position.
1227  pub y: WebKitGradientPointComponent<VerticalPositionKeyword>,
1228}
1229
1230impl<'i> Parse<'i> for WebKitGradientPoint {
1231  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
1232    let x = WebKitGradientPointComponent::parse(input)?;
1233    let y = WebKitGradientPointComponent::parse(input)?;
1234    Ok(WebKitGradientPoint { x, y })
1235  }
1236}
1237
1238impl ToCss for WebKitGradientPoint {
1239  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
1240  where
1241    W: std::fmt::Write,
1242  {
1243    self.x.to_css(dest)?;
1244    dest.write_char(' ')?;
1245    self.y.to_css(dest)
1246  }
1247}
1248
1249/// A keyword or number within a [WebKitGradientPoint](WebKitGradientPoint).
1250#[derive(Debug, Clone, PartialEq)]
1251#[cfg_attr(feature = "visitor", derive(Visit))]
1252#[cfg_attr(
1253  feature = "serde",
1254  derive(serde::Serialize, serde::Deserialize),
1255  serde(tag = "type", content = "value", rename_all = "kebab-case")
1256)]
1257#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1258#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
1259pub enum WebKitGradientPointComponent<S> {
1260  /// The `center` keyword.
1261  Center,
1262  /// A number or percentage.
1263  Number(NumberOrPercentage),
1264  /// A side keyword.
1265  Side(S),
1266}
1267
1268impl<'i, S: Parse<'i>> Parse<'i> for WebKitGradientPointComponent<S> {
1269  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
1270    if input.try_parse(|i| i.expect_ident_matching("center")).is_ok() {
1271      return Ok(WebKitGradientPointComponent::Center);
1272    }
1273
1274    if let Ok(lp) = input.try_parse(NumberOrPercentage::parse) {
1275      return Ok(WebKitGradientPointComponent::Number(lp));
1276    }
1277
1278    let keyword = S::parse(input)?;
1279    Ok(WebKitGradientPointComponent::Side(keyword))
1280  }
1281}
1282
1283impl<S: ToCss + Clone + Into<LengthPercentage>> ToCss for WebKitGradientPointComponent<S> {
1284  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
1285  where
1286    W: std::fmt::Write,
1287  {
1288    use WebKitGradientPointComponent::*;
1289    match &self {
1290      Center => {
1291        if dest.minify {
1292          dest.write_str("50%")
1293        } else {
1294          dest.write_str("center")
1295        }
1296      }
1297      Number(lp) => {
1298        if matches!(lp, NumberOrPercentage::Percentage(Percentage(p)) if *p == 0.0) {
1299          dest.write_char('0')
1300        } else {
1301          lp.to_css(dest)
1302        }
1303      }
1304      Side(s) => {
1305        if dest.minify {
1306          let lp: LengthPercentage = s.clone().into();
1307          lp.to_css(dest)?;
1308        } else {
1309          s.to_css(dest)?;
1310        }
1311        Ok(())
1312      }
1313    }
1314  }
1315}
1316
1317impl<S: Clone> WebKitGradientPointComponent<S> {
1318  /// Attempts to convert a standard position to a webkit gradient point.
1319  fn from_position(pos: &PositionComponent<S>) -> Result<WebKitGradientPointComponent<S>, ()> {
1320    match pos {
1321      PositionComponent::Center => Ok(WebKitGradientPointComponent::Center),
1322      PositionComponent::Length(len) => {
1323        Ok(WebKitGradientPointComponent::Number(match len {
1324          LengthPercentage::Percentage(p) => NumberOrPercentage::Percentage(p.clone()),
1325          LengthPercentage::Dimension(d) => {
1326            // Webkit gradient points can only be specified in pixels.
1327            if let Some(px) = d.to_px() {
1328              NumberOrPercentage::Number(px)
1329            } else {
1330              return Err(());
1331            }
1332          }
1333          _ => return Err(()),
1334        }))
1335      }
1336      PositionComponent::Side { side, offset } => {
1337        if offset.is_some() {
1338          return Err(());
1339        }
1340        Ok(WebKitGradientPointComponent::Side(side.clone()))
1341      }
1342    }
1343  }
1344}
1345
1346impl WebKitGradient {
1347  /// Attempts to convert a standard gradient to a legacy -webkit-gradient()
1348  pub fn from_standard(gradient: &Gradient) -> Result<WebKitGradient, ()> {
1349    match gradient {
1350      Gradient::Linear(linear) => {
1351        // Convert from line direction to a from and to point, if possible.
1352        let (from, to) = match &linear.direction {
1353          LineDirection::Horizontal(horizontal) => match horizontal {
1354            HorizontalPositionKeyword::Left => ((1.0, 0.0), (0.0, 0.0)),
1355            HorizontalPositionKeyword::Right => ((0.0, 0.0), (1.0, 0.0)),
1356          },
1357          LineDirection::Vertical(vertical) => match vertical {
1358            VerticalPositionKeyword::Top => ((0.0, 1.0), (0.0, 0.0)),
1359            VerticalPositionKeyword::Bottom => ((0.0, 0.0), (0.0, 1.0)),
1360          },
1361          LineDirection::Corner { horizontal, vertical } => match (horizontal, vertical) {
1362            (HorizontalPositionKeyword::Left, VerticalPositionKeyword::Top) => ((1.0, 1.0), (0.0, 0.0)),
1363            (HorizontalPositionKeyword::Left, VerticalPositionKeyword::Bottom) => ((1.0, 0.0), (0.0, 1.0)),
1364            (HorizontalPositionKeyword::Right, VerticalPositionKeyword::Top) => ((0.0, 1.0), (1.0, 0.0)),
1365            (HorizontalPositionKeyword::Right, VerticalPositionKeyword::Bottom) => ((0.0, 0.0), (1.0, 1.0)),
1366          },
1367          LineDirection::Angle(angle) => {
1368            let degrees = angle.to_degrees();
1369            if degrees == 0.0 {
1370              ((0.0, 1.0), (0.0, 0.0))
1371            } else if degrees == 90.0 {
1372              ((0.0, 0.0), (1.0, 0.0))
1373            } else if degrees == 180.0 {
1374              ((0.0, 0.0), (0.0, 1.0))
1375            } else if degrees == 270.0 {
1376              ((1.0, 0.0), (0.0, 0.0))
1377            } else {
1378              return Err(());
1379            }
1380          }
1381        };
1382
1383        Ok(WebKitGradient::Linear {
1384          from: WebKitGradientPoint {
1385            x: WebKitGradientPointComponent::Number(NumberOrPercentage::Percentage(Percentage(from.0))),
1386            y: WebKitGradientPointComponent::Number(NumberOrPercentage::Percentage(Percentage(from.1))),
1387          },
1388          to: WebKitGradientPoint {
1389            x: WebKitGradientPointComponent::Number(NumberOrPercentage::Percentage(Percentage(to.0))),
1390            y: WebKitGradientPointComponent::Number(NumberOrPercentage::Percentage(Percentage(to.1))),
1391          },
1392          stops: convert_stops_to_webkit(&linear.items)?,
1393        })
1394      }
1395      Gradient::Radial(radial) => {
1396        // Webkit radial gradients are always circles, not ellipses, and must be specified in pixels.
1397        let radius = match &radial.shape {
1398          EndingShape::Circle(Circle::Radius(radius)) => {
1399            if let Some(r) = radius.to_px() {
1400              r
1401            } else {
1402              return Err(());
1403            }
1404          }
1405          _ => return Err(()),
1406        };
1407
1408        let x = WebKitGradientPointComponent::from_position(&radial.position.x)?;
1409        let y = WebKitGradientPointComponent::from_position(&radial.position.y)?;
1410        let point = WebKitGradientPoint { x, y };
1411        Ok(WebKitGradient::Radial {
1412          from: point.clone(),
1413          r0: 0.0,
1414          to: point,
1415          r1: radius,
1416          stops: convert_stops_to_webkit(&radial.items)?,
1417        })
1418      }
1419      _ => Err(()),
1420    }
1421  }
1422}
1423
1424fn convert_stops_to_webkit(items: &Vec<GradientItem<LengthPercentage>>) -> Result<Vec<WebKitColorStop>, ()> {
1425  let mut stops = Vec::with_capacity(items.len());
1426  for (i, item) in items.iter().enumerate() {
1427    match item {
1428      GradientItem::ColorStop(stop) => {
1429        // webkit stops must always be percentage based, not length based.
1430        let position = if let Some(pos) = &stop.position {
1431          if let LengthPercentage::Percentage(position) = pos {
1432            position.0
1433          } else {
1434            return Err(());
1435          }
1436        } else if i == 0 {
1437          0.0
1438        } else if i == items.len() - 1 {
1439          1.0
1440        } else {
1441          return Err(());
1442        };
1443
1444        stops.push(WebKitColorStop {
1445          color: stop.color.clone(),
1446          position,
1447        })
1448      }
1449      _ => return Err(()),
1450    }
1451  }
1452
1453  Ok(stops)
1454}