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