blob: 22d748eb5741d0169362c0beb0f5fcfa8f64513f [file] [log] [blame]
//! CSS gradient values.
use super::angle::{Angle, AnglePercentage};
use super::color::{ColorFallbackKind, CssColor};
use super::length::{Length, LengthPercentage};
use super::number::CSSNumber;
use super::percentage::{DimensionPercentage, NumberOrPercentage, Percentage};
use super::position::{HorizontalPositionKeyword, VerticalPositionKeyword};
use super::position::{Position, PositionComponent};
use crate::compat;
use crate::error::{ParserError, PrinterError};
use crate::macros::enum_property;
use crate::prefixes::Feature;
use crate::printer::Printer;
use crate::targets::{should_compile, Browsers, Targets};
use crate::traits::{IsCompatible, Parse, ToCss, TrySign, Zero};
use crate::vendor_prefix::VendorPrefix;
#[cfg(feature = "visitor")]
use crate::visitor::Visit;
use cssparser::*;
use std::f32::consts::PI;
#[cfg(feature = "serde")]
use crate::serialization::ValueWrapper;
/// A CSS [`<gradient>`](https://www.w3.org/TR/css-images-3/#gradients) value.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(tag = "type", rename_all = "kebab-case")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
pub enum Gradient {
/// A `linear-gradient()`, and its vendor prefix.
Linear(LinearGradient),
/// A `repeating-linear-gradient()`, and its vendor prefix.
RepeatingLinear(LinearGradient),
/// A `radial-gradient()`, and its vendor prefix.
Radial(RadialGradient),
/// A `repeating-radial-gradient`, and its vendor prefix.
RepeatingRadial(RadialGradient),
/// A `conic-gradient()`.
Conic(ConicGradient),
/// A `repeating-conic-gradient()`.
RepeatingConic(ConicGradient),
/// A legacy `-webkit-gradient()`.
#[cfg_attr(feature = "serde", serde(rename = "webkit-gradient"))]
WebKitGradient(WebKitGradient),
}
impl Gradient {
/// Returns the vendor prefix of the gradient.
pub fn get_vendor_prefix(&self) -> VendorPrefix {
match self {
Gradient::Linear(LinearGradient { vendor_prefix, .. })
| Gradient::RepeatingLinear(LinearGradient { vendor_prefix, .. })
| Gradient::Radial(RadialGradient { vendor_prefix, .. })
| Gradient::RepeatingRadial(RadialGradient { vendor_prefix, .. }) => *vendor_prefix,
Gradient::WebKitGradient(_) => VendorPrefix::WebKit,
_ => VendorPrefix::None,
}
}
/// Returns the vendor prefixes needed for the given browser targets.
pub fn get_necessary_prefixes(&self, targets: Targets) -> VendorPrefix {
macro_rules! get_prefixes {
($feature: ident, $prefix: expr) => {
targets.prefixes($prefix, Feature::$feature)
};
}
match self {
Gradient::Linear(linear) => get_prefixes!(LinearGradient, linear.vendor_prefix),
Gradient::RepeatingLinear(linear) => get_prefixes!(RepeatingLinearGradient, linear.vendor_prefix),
Gradient::Radial(radial) => get_prefixes!(RadialGradient, radial.vendor_prefix),
Gradient::RepeatingRadial(radial) => get_prefixes!(RepeatingRadialGradient, radial.vendor_prefix),
_ => VendorPrefix::None,
}
}
/// Returns a copy of the gradient with the given vendor prefix.
pub fn get_prefixed(&self, prefix: VendorPrefix) -> Gradient {
match self {
Gradient::Linear(linear) => {
let mut new_linear = linear.clone();
let needs_legacy_direction = linear.vendor_prefix == VendorPrefix::None && prefix != VendorPrefix::None;
if needs_legacy_direction {
new_linear.direction = convert_to_legacy_direction(&new_linear.direction);
}
new_linear.vendor_prefix = prefix;
Gradient::Linear(new_linear)
}
Gradient::RepeatingLinear(linear) => {
let mut new_linear = linear.clone();
let needs_legacy_direction = linear.vendor_prefix == VendorPrefix::None && prefix != VendorPrefix::None;
if needs_legacy_direction {
new_linear.direction = convert_to_legacy_direction(&new_linear.direction);
}
new_linear.vendor_prefix = prefix;
Gradient::RepeatingLinear(new_linear)
}
Gradient::Radial(radial) => Gradient::Radial(RadialGradient {
vendor_prefix: prefix,
..radial.clone()
}),
Gradient::RepeatingRadial(radial) => Gradient::RepeatingRadial(RadialGradient {
vendor_prefix: prefix,
..radial.clone()
}),
_ => self.clone(),
}
}
/// Attempts to convert the gradient to the legacy `-webkit-gradient()` syntax.
///
/// Returns an error in case the conversion is not possible.
pub fn get_legacy_webkit(&self) -> Result<Gradient, ()> {
Ok(Gradient::WebKitGradient(WebKitGradient::from_standard(self)?))
}
/// Returns the color fallback types needed for the given browser targets.
pub fn get_necessary_fallbacks(&self, targets: Targets) -> ColorFallbackKind {
match self {
Gradient::Linear(LinearGradient { items, .. })
| Gradient::Radial(RadialGradient { items, .. })
| Gradient::RepeatingLinear(LinearGradient { items, .. })
| Gradient::RepeatingRadial(RadialGradient { items, .. }) => {
let mut fallbacks = ColorFallbackKind::empty();
for item in items {
fallbacks |= item.get_necessary_fallbacks(targets)
}
fallbacks
}
Gradient::Conic(ConicGradient { items, .. }) | Gradient::RepeatingConic(ConicGradient { items, .. }) => {
let mut fallbacks = ColorFallbackKind::empty();
for item in items {
fallbacks |= item.get_necessary_fallbacks(targets)
}
fallbacks
}
Gradient::WebKitGradient(..) => ColorFallbackKind::empty(),
}
}
/// Returns a fallback gradient for the given color fallback type.
pub fn get_fallback(&self, kind: ColorFallbackKind) -> Gradient {
match self {
Gradient::Linear(g) => Gradient::Linear(g.get_fallback(kind)),
Gradient::RepeatingLinear(g) => Gradient::RepeatingLinear(g.get_fallback(kind)),
Gradient::Radial(g) => Gradient::Radial(g.get_fallback(kind)),
Gradient::RepeatingRadial(g) => Gradient::RepeatingRadial(g.get_fallback(kind)),
Gradient::Conic(g) => Gradient::Conic(g.get_fallback(kind)),
Gradient::RepeatingConic(g) => Gradient::RepeatingConic(g.get_fallback(kind)),
Gradient::WebKitGradient(g) => Gradient::WebKitGradient(g.get_fallback(kind)),
}
}
}
impl<'i> Parse<'i> for Gradient {
fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
let location = input.current_source_location();
let func = input.expect_function()?.clone();
input.parse_nested_block(|input| {
match_ignore_ascii_case! { &func,
"linear-gradient" => Ok(Gradient::Linear(LinearGradient::parse(input, VendorPrefix::None)?)),
"repeating-linear-gradient" => Ok(Gradient::RepeatingLinear(LinearGradient::parse(input, VendorPrefix::None)?)),
"radial-gradient" => Ok(Gradient::Radial(RadialGradient::parse(input, VendorPrefix::None)?)),
"repeating-radial-gradient" => Ok(Gradient::RepeatingRadial(RadialGradient::parse(input, VendorPrefix::None)?)),
"conic-gradient" => Ok(Gradient::Conic(ConicGradient::parse(input)?)),
"repeating-conic-gradient" => Ok(Gradient::RepeatingConic(ConicGradient::parse(input)?)),
"-webkit-linear-gradient" => Ok(Gradient::Linear(LinearGradient::parse(input, VendorPrefix::WebKit)?)),
"-webkit-repeating-linear-gradient" => Ok(Gradient::RepeatingLinear(LinearGradient::parse(input, VendorPrefix::WebKit)?)),
"-webkit-radial-gradient" => Ok(Gradient::Radial(RadialGradient::parse(input, VendorPrefix::WebKit)?)),
"-webkit-repeating-radial-gradient" => Ok(Gradient::RepeatingRadial(RadialGradient::parse(input, VendorPrefix::WebKit)?)),
"-moz-linear-gradient" => Ok(Gradient::Linear(LinearGradient::parse(input, VendorPrefix::Moz)?)),
"-moz-repeating-linear-gradient" => Ok(Gradient::RepeatingLinear(LinearGradient::parse(input, VendorPrefix::Moz)?)),
"-moz-radial-gradient" => Ok(Gradient::Radial(RadialGradient::parse(input, VendorPrefix::Moz)?)),
"-moz-repeating-radial-gradient" => Ok(Gradient::RepeatingRadial(RadialGradient::parse(input, VendorPrefix::Moz)?)),
"-o-linear-gradient" => Ok(Gradient::Linear(LinearGradient::parse(input, VendorPrefix::O)?)),
"-o-repeating-linear-gradient" => Ok(Gradient::RepeatingLinear(LinearGradient::parse(input, VendorPrefix::O)?)),
"-o-radial-gradient" => Ok(Gradient::Radial(RadialGradient::parse(input, VendorPrefix::O)?)),
"-o-repeating-radial-gradient" => Ok(Gradient::RepeatingRadial(RadialGradient::parse(input, VendorPrefix::O)?)),
"-webkit-gradient" => Ok(Gradient::WebKitGradient(WebKitGradient::parse(input)?)),
_ => Err(location.new_unexpected_token_error(cssparser::Token::Ident(func.clone())))
}
})
}
}
impl ToCss for Gradient {
fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
let (f, prefix) = match self {
Gradient::Linear(g) => ("linear-gradient(", Some(g.vendor_prefix)),
Gradient::RepeatingLinear(g) => ("repeating-linear-gradient(", Some(g.vendor_prefix)),
Gradient::Radial(g) => ("radial-gradient(", Some(g.vendor_prefix)),
Gradient::RepeatingRadial(g) => ("repeating-radial-gradient(", Some(g.vendor_prefix)),
Gradient::Conic(_) => ("conic-gradient(", None),
Gradient::RepeatingConic(_) => ("repeating-conic-gradient(", None),
Gradient::WebKitGradient(_) => ("-webkit-gradient(", None),
};
if let Some(prefix) = prefix {
prefix.to_css(dest)?;
}
dest.write_str(f)?;
match self {
Gradient::Linear(linear) | Gradient::RepeatingLinear(linear) => {
linear.to_css(dest, linear.vendor_prefix != VendorPrefix::None)?
}
Gradient::Radial(radial) | Gradient::RepeatingRadial(radial) => radial.to_css(dest)?,
Gradient::Conic(conic) | Gradient::RepeatingConic(conic) => conic.to_css(dest)?,
Gradient::WebKitGradient(g) => g.to_css(dest)?,
}
dest.write_char(')')
}
}
/// A CSS [`linear-gradient()`](https://www.w3.org/TR/css-images-3/#linear-gradients) or `repeating-linear-gradient()`.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(rename_all = "camelCase")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
pub struct LinearGradient {
/// The vendor prefixes for the gradient.
pub vendor_prefix: VendorPrefix,
/// The direction of the gradient.
pub direction: LineDirection,
/// The color stops and transition hints for the gradient.
pub items: Vec<GradientItem<LengthPercentage>>,
}
impl LinearGradient {
fn parse<'i, 't>(
input: &mut Parser<'i, 't>,
vendor_prefix: VendorPrefix,
) -> Result<LinearGradient, ParseError<'i, ParserError<'i>>> {
let direction = if let Ok(direction) =
input.try_parse(|input| LineDirection::parse(input, vendor_prefix != VendorPrefix::None))
{
input.expect_comma()?;
direction
} else {
LineDirection::Vertical(VerticalPositionKeyword::Bottom)
};
let items = parse_items(input)?;
Ok(LinearGradient {
direction,
items,
vendor_prefix,
})
}
fn to_css<W>(&self, dest: &mut Printer<W>, is_prefixed: bool) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
let angle = match &self.direction {
LineDirection::Vertical(VerticalPositionKeyword::Bottom) => 180.0,
LineDirection::Vertical(VerticalPositionKeyword::Top) => 0.0,
LineDirection::Angle(angle) => angle.to_degrees(),
_ => -1.0,
};
// We can omit `to bottom` or `180deg` because it is the default.
if angle == 180.0 {
serialize_items(&self.items, dest)
// If we have `to top` or `0deg`, and all of the positions and hints are percentages,
// we can flip the gradient the other direction and omit the direction.
} else if angle == 0.0
&& dest.minify
&& self.items.iter().all(|item| {
matches!(
item,
GradientItem::Hint(LengthPercentage::Percentage(_))
| GradientItem::ColorStop(ColorStop {
position: None | Some(LengthPercentage::Percentage(_)),
..
})
)
})
{
let items: Vec<GradientItem<LengthPercentage>> = self
.items
.iter()
.rev()
.map(|item| {
// Flip percentages.
match item {
GradientItem::Hint(LengthPercentage::Percentage(p)) => {
GradientItem::Hint(LengthPercentage::Percentage(Percentage(1.0 - p.0)))
}
GradientItem::ColorStop(ColorStop { color, position }) => GradientItem::ColorStop(ColorStop {
color: color.clone(),
position: position.clone().map(|p| match p {
LengthPercentage::Percentage(p) => LengthPercentage::Percentage(Percentage(1.0 - p.0)),
_ => unreachable!(),
}),
}),
_ => unreachable!(),
}
})
.collect();
serialize_items(&items, dest)
} else {
if self.direction != LineDirection::Vertical(VerticalPositionKeyword::Bottom)
&& self.direction != LineDirection::Angle(Angle::Deg(180.0))
{
self.direction.to_css(dest, is_prefixed)?;
dest.delim(',', false)?;
}
serialize_items(&self.items, dest)
}
}
fn get_fallback(&self, kind: ColorFallbackKind) -> LinearGradient {
LinearGradient {
direction: self.direction.clone(),
items: self.items.iter().map(|item| item.get_fallback(kind)).collect(),
vendor_prefix: self.vendor_prefix,
}
}
}
impl IsCompatible for LinearGradient {
fn is_compatible(&self, browsers: Browsers) -> bool {
self.items.iter().all(|item| item.is_compatible(browsers))
}
}
/// A CSS [`radial-gradient()`](https://www.w3.org/TR/css-images-3/#radial-gradients) or `repeating-radial-gradient()`.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(rename_all = "camelCase")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
pub struct RadialGradient {
/// The vendor prefixes for the gradient.
pub vendor_prefix: VendorPrefix,
/// The shape of the gradient.
pub shape: EndingShape,
/// The position of the gradient.
pub position: Position,
/// The color stops and transition hints for the gradient.
pub items: Vec<GradientItem<LengthPercentage>>,
}
impl<'i> RadialGradient {
fn parse<'t>(
input: &mut Parser<'i, 't>,
vendor_prefix: VendorPrefix,
) -> Result<RadialGradient, ParseError<'i, ParserError<'i>>> {
let shape = input.try_parse(EndingShape::parse).ok();
let position = input
.try_parse(|input| {
input.expect_ident_matching("at")?;
Position::parse(input)
})
.ok();
if shape.is_some() || position.is_some() {
input.expect_comma()?;
}
let items = parse_items(input)?;
Ok(RadialGradient {
shape: shape.unwrap_or_default(),
position: position.unwrap_or(Position::center()),
items,
vendor_prefix,
})
}
}
impl ToCss for RadialGradient {
fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
if self.shape != EndingShape::default() {
self.shape.to_css(dest)?;
if self.position.is_center() {
dest.delim(',', false)?;
} else {
dest.write_char(' ')?;
}
}
if !self.position.is_center() {
dest.write_str("at ")?;
self.position.to_css(dest)?;
dest.delim(',', false)?;
}
serialize_items(&self.items, dest)
}
}
impl RadialGradient {
fn get_fallback(&self, kind: ColorFallbackKind) -> RadialGradient {
RadialGradient {
shape: self.shape.clone(),
position: self.position.clone(),
items: self.items.iter().map(|item| item.get_fallback(kind)).collect(),
vendor_prefix: self.vendor_prefix,
}
}
}
impl IsCompatible for RadialGradient {
fn is_compatible(&self, browsers: Browsers) -> bool {
self.items.iter().all(|item| item.is_compatible(browsers))
}
}
/// The direction of a CSS `linear-gradient()`.
///
/// See [LinearGradient](LinearGradient).
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(tag = "type", rename_all = "kebab-case")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub enum LineDirection {
/// An angle.
#[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<Angle>"))]
Angle(Angle),
/// A horizontal position keyword, e.g. `left` or `right.
#[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<HorizontalPositionKeyword>"))]
Horizontal(HorizontalPositionKeyword),
/// A vertical posision keyword, e.g. `top` or `bottom`.
#[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<VerticalPositionKeyword>"))]
Vertical(VerticalPositionKeyword),
/// A corner, e.g. `bottom left` or `top right`.
Corner {
/// A horizontal position keyword, e.g. `left` or `right.
horizontal: HorizontalPositionKeyword,
/// A vertical posision keyword, e.g. `top` or `bottom`.
vertical: VerticalPositionKeyword,
},
}
impl LineDirection {
fn parse<'i, 't>(
input: &mut Parser<'i, 't>,
is_prefixed: bool,
) -> Result<Self, ParseError<'i, ParserError<'i>>> {
// Spec allows unitless zero angles for gradients.
// https://w3c.github.io/csswg-drafts/css-images-3/#linear-gradient-syntax
if let Ok(angle) = input.try_parse(Angle::parse_with_unitless_zero) {
return Ok(LineDirection::Angle(angle));
}
if !is_prefixed {
input.expect_ident_matching("to")?;
}
if let Ok(x) = input.try_parse(HorizontalPositionKeyword::parse) {
if let Ok(y) = input.try_parse(VerticalPositionKeyword::parse) {
return Ok(LineDirection::Corner {
horizontal: x,
vertical: y,
});
}
return Ok(LineDirection::Horizontal(x));
}
let y = VerticalPositionKeyword::parse(input)?;
if let Ok(x) = input.try_parse(HorizontalPositionKeyword::parse) {
return Ok(LineDirection::Corner {
horizontal: x,
vertical: y,
});
}
Ok(LineDirection::Vertical(y))
}
fn to_css<W>(&self, dest: &mut Printer<W>, is_prefixed: bool) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
match self {
LineDirection::Angle(angle) => angle.to_css(dest),
LineDirection::Horizontal(k) => {
if dest.minify {
match k {
HorizontalPositionKeyword::Left => dest.write_str("270deg"),
HorizontalPositionKeyword::Right => dest.write_str("90deg"),
}
} else {
if !is_prefixed {
dest.write_str("to ")?;
}
k.to_css(dest)
}
}
LineDirection::Vertical(k) => {
if dest.minify {
match k {
VerticalPositionKeyword::Top => dest.write_str("0deg"),
VerticalPositionKeyword::Bottom => dest.write_str("180deg"),
}
} else {
if !is_prefixed {
dest.write_str("to ")?;
}
k.to_css(dest)
}
}
LineDirection::Corner { horizontal, vertical } => {
if !is_prefixed {
dest.write_str("to ")?;
}
vertical.to_css(dest)?;
dest.write_char(' ')?;
horizontal.to_css(dest)
}
}
}
}
/// Converts a standard gradient direction to its legacy vendor-prefixed form.
///
/// Inverts keyword-based directions (e.g., `to bottom` → `top`) for compatibility
/// with legacy prefixed syntaxes.
///
/// See: https://github.com/parcel-bundler/lightningcss/issues/918
fn convert_to_legacy_direction(direction: &LineDirection) -> LineDirection {
match direction {
LineDirection::Horizontal(HorizontalPositionKeyword::Left) => {
LineDirection::Horizontal(HorizontalPositionKeyword::Right)
}
LineDirection::Horizontal(HorizontalPositionKeyword::Right) => {
LineDirection::Horizontal(HorizontalPositionKeyword::Left)
}
LineDirection::Vertical(VerticalPositionKeyword::Top) => {
LineDirection::Vertical(VerticalPositionKeyword::Bottom)
}
LineDirection::Vertical(VerticalPositionKeyword::Bottom) => {
LineDirection::Vertical(VerticalPositionKeyword::Top)
}
LineDirection::Corner { horizontal, vertical } => LineDirection::Corner {
horizontal: match horizontal {
HorizontalPositionKeyword::Left => HorizontalPositionKeyword::Right,
HorizontalPositionKeyword::Right => HorizontalPositionKeyword::Left,
},
vertical: match vertical {
VerticalPositionKeyword::Top => VerticalPositionKeyword::Bottom,
VerticalPositionKeyword::Bottom => VerticalPositionKeyword::Top,
},
},
LineDirection::Angle(angle) => {
let angle = angle.clone();
let deg = match angle {
Angle::Deg(n) => convert_to_legacy_degree(n),
Angle::Rad(n) => {
let n = n / (2.0 * PI) * 360.0;
convert_to_legacy_degree(n)
}
Angle::Grad(n) => {
let n = n / 400.0 * 360.0;
convert_to_legacy_degree(n)
}
Angle::Turn(n) => {
let n = n * 360.0;
convert_to_legacy_degree(n)
}
};
LineDirection::Angle(Angle::Deg(deg))
}
}
}
fn convert_to_legacy_degree(degree: f32) -> f32 {
// Add 90 degrees
let n = (450.0 - degree).abs() % 360.0;
// Round the number to 3 decimal places
(n * 1000.0).round() / 1000.0
}
/// A `radial-gradient()` [ending shape](https://www.w3.org/TR/css-images-3/#valdef-radial-gradient-ending-shape).
///
/// See [RadialGradient](RadialGradient).
#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(tag = "type", content = "value", rename_all = "kebab-case")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub enum EndingShape {
// Note: Ellipse::parse MUST run before Circle::parse for this to be correct.
/// An ellipse.
Ellipse(Ellipse),
/// A circle.
Circle(Circle),
}
impl Default for EndingShape {
fn default() -> EndingShape {
EndingShape::Ellipse(Ellipse::Extent(ShapeExtent::FarthestCorner))
}
}
/// A circle ending shape for a `radial-gradient()`.
///
/// See [RadialGradient](RadialGradient).
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(tag = "type", content = "value", rename_all = "kebab-case")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub enum Circle {
/// A circle with a specified radius.
Radius(Length),
/// A shape extent keyword.
Extent(ShapeExtent),
}
impl<'i> Parse<'i> for Circle {
fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
if let Ok(extent) = input.try_parse(ShapeExtent::parse) {
// The `circle` keyword is required. If it's not there, then it's an ellipse.
input.expect_ident_matching("circle")?;
return Ok(Circle::Extent(extent));
}
if let Ok(length) = input.try_parse(Length::parse) {
// The `circle` keyword is optional if there is only a single length.
// We are assuming here that Ellipse::parse ran first.
let _ = input.try_parse(|input| input.expect_ident_matching("circle"));
return Ok(Circle::Radius(length));
}
if input.try_parse(|input| input.expect_ident_matching("circle")).is_ok() {
if let Ok(extent) = input.try_parse(ShapeExtent::parse) {
return Ok(Circle::Extent(extent));
}
if let Ok(length) = input.try_parse(Length::parse) {
return Ok(Circle::Radius(length));
}
// If only the `circle` keyword was given, default to `farthest-corner`.
return Ok(Circle::Extent(ShapeExtent::FarthestCorner));
}
return Err(input.new_error_for_next_token());
}
}
impl ToCss for Circle {
fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
match self {
Circle::Radius(r) => r.to_css(dest),
Circle::Extent(extent) => {
dest.write_str("circle")?;
if *extent != ShapeExtent::FarthestCorner {
dest.write_char(' ')?;
extent.to_css(dest)?;
}
Ok(())
}
}
}
}
/// An ellipse ending shape for a `radial-gradient()`.
///
/// See [RadialGradient](RadialGradient).
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(tag = "type", rename_all = "kebab-case")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub enum Ellipse {
/// An ellipse with a specified horizontal and vertical radius.
Size {
/// The x-radius of the ellipse.
x: LengthPercentage,
/// The y-radius of the ellipse.
y: LengthPercentage,
},
/// A shape extent keyword.
#[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<ShapeExtent>"))]
Extent(ShapeExtent),
}
impl<'i> Parse<'i> for Ellipse {
fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
if let Ok(extent) = input.try_parse(ShapeExtent::parse) {
// The `ellipse` keyword is optional, but only if the `circle` keyword is not present.
// If it is, then we'll re-parse as a circle.
if input.try_parse(|input| input.expect_ident_matching("circle")).is_ok() {
return Err(input.new_error_for_next_token());
}
let _ = input.try_parse(|input| input.expect_ident_matching("ellipse"));
return Ok(Ellipse::Extent(extent));
}
if let Ok(x) = input.try_parse(LengthPercentage::parse) {
let y = LengthPercentage::parse(input)?;
// The `ellipse` keyword is optional if there are two lengths.
let _ = input.try_parse(|input| input.expect_ident_matching("ellipse"));
return Ok(Ellipse::Size { x, y });
}
if input.try_parse(|input| input.expect_ident_matching("ellipse")).is_ok() {
if let Ok(extent) = input.try_parse(ShapeExtent::parse) {
return Ok(Ellipse::Extent(extent));
}
if let Ok(x) = input.try_parse(LengthPercentage::parse) {
let y = LengthPercentage::parse(input)?;
return Ok(Ellipse::Size { x, y });
}
// Assume `farthest-corner` if only the `ellipse` keyword is present.
return Ok(Ellipse::Extent(ShapeExtent::FarthestCorner));
}
return Err(input.new_error_for_next_token());
}
}
impl ToCss for Ellipse {
fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
// The `ellipse` keyword is optional, so we don't emit it.
match self {
Ellipse::Size { x, y } => {
x.to_css(dest)?;
dest.write_char(' ')?;
y.to_css(dest)
}
Ellipse::Extent(extent) => extent.to_css(dest),
}
}
}
enum_property! {
/// A shape extent for a `radial-gradient()`.
///
/// See [RadialGradient](RadialGradient).
pub enum ShapeExtent {
/// The closest side of the box to the gradient's center.
ClosestSide,
/// The farthest side of the box from the gradient's center.
FarthestSide,
/// The closest cornder of the box to the gradient's center.
ClosestCorner,
/// The farthest corner of the box from the gradient's center.
FarthestCorner,
}
}
/// A CSS [`conic-gradient()`](https://www.w3.org/TR/css-images-4/#conic-gradients) or `repeating-conic-gradient()`.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
pub struct ConicGradient {
/// The angle of the gradient.
pub angle: Angle,
/// The position of the gradient.
pub position: Position,
/// The color stops and transition hints for the gradient.
pub items: Vec<GradientItem<AnglePercentage>>,
}
impl ConicGradient {
fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
let angle = input.try_parse(|input| {
input.expect_ident_matching("from")?;
// Spec allows unitless zero angles for gradients.
// https://w3c.github.io/csswg-drafts/css-images-4/#valdef-conic-gradient-angle
Angle::parse_with_unitless_zero(input)
});
let position = input.try_parse(|input| {
input.expect_ident_matching("at")?;
Position::parse(input)
});
if angle.is_ok() || position.is_ok() {
input.expect_comma()?;
}
let items = parse_items(input)?;
Ok(ConicGradient {
angle: angle.unwrap_or(Angle::Deg(0.0)),
position: position.unwrap_or(Position::center()),
items,
})
}
}
impl ToCss for ConicGradient {
fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
if !self.angle.is_zero() {
dest.write_str("from ")?;
self.angle.to_css(dest)?;
if self.position.is_center() {
dest.delim(',', false)?;
} else {
dest.write_char(' ')?;
}
}
if !self.position.is_center() {
dest.write_str("at ")?;
self.position.to_css(dest)?;
dest.delim(',', false)?;
}
serialize_items(&self.items, dest)
}
}
impl ConicGradient {
fn get_fallback(&self, kind: ColorFallbackKind) -> ConicGradient {
ConicGradient {
angle: self.angle.clone(),
position: self.position.clone(),
items: self.items.iter().map(|item| item.get_fallback(kind)).collect(),
}
}
}
impl IsCompatible for ConicGradient {
fn is_compatible(&self, browsers: Browsers) -> bool {
self.items.iter().all(|item| item.is_compatible(browsers))
}
}
/// A [`<color-stop>`](https://www.w3.org/TR/css-images-4/#color-stop-syntax) within a gradient.
///
/// This type is generic, and may be either a [LengthPercentage](super::length::LengthPercentage)
/// or [Angle](super::angle::Angle) depending on what type of gradient it is within.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct ColorStop<D> {
/// The color of the color stop.
pub color: CssColor,
/// The position of the color stop.
pub position: Option<D>,
}
impl<'i, D: Parse<'i>> Parse<'i> for ColorStop<D> {
fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
let color = CssColor::parse(input)?;
let position = input.try_parse(D::parse).ok();
Ok(ColorStop { color, position })
}
}
impl<D: ToCss> ToCss for ColorStop<D> {
fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
self.color.to_css(dest)?;
if let Some(position) = &self.position {
dest.write_char(' ')?;
position.to_css(dest)?;
}
Ok(())
}
}
/// Either a color stop or interpolation hint within a gradient.
///
/// This type is generic, and items may be either a [LengthPercentage](super::length::LengthPercentage)
/// or [Angle](super::angle::Angle) depending on what type of gradient it is within.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(tag = "type", rename_all = "kebab-case")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub enum GradientItem<D> {
/// A color stop.
ColorStop(ColorStop<D>),
/// A color interpolation hint.
#[cfg_attr(
feature = "serde",
serde(
bound(serialize = "D: serde::Serialize", deserialize = "D: serde::Deserialize<'de>"),
with = "ValueWrapper::<D>"
)
)]
Hint(D),
}
impl<D: ToCss> ToCss for GradientItem<D> {
fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
match self {
GradientItem::ColorStop(stop) => stop.to_css(dest),
GradientItem::Hint(hint) => hint.to_css(dest),
}
}
}
impl<D: Clone> GradientItem<D> {
/// Returns the color fallback types needed for the given browser targets.
pub fn get_necessary_fallbacks(&self, targets: Targets) -> ColorFallbackKind {
match self {
GradientItem::ColorStop(stop) => stop.color.get_necessary_fallbacks(targets),
GradientItem::Hint(..) => ColorFallbackKind::empty(),
}
}
/// Returns a fallback gradient item for the given color fallback type.
pub fn get_fallback(&self, kind: ColorFallbackKind) -> GradientItem<D> {
match self {
GradientItem::ColorStop(stop) => GradientItem::ColorStop(ColorStop {
color: stop.color.get_fallback(kind),
position: stop.position.clone(),
}),
GradientItem::Hint(..) => self.clone(),
}
}
}
impl<D> IsCompatible for GradientItem<D> {
fn is_compatible(&self, browsers: Browsers) -> bool {
match self {
GradientItem::ColorStop(c) => c.color.is_compatible(browsers),
GradientItem::Hint(..) => compat::Feature::GradientInterpolationHints.is_compatible(browsers),
}
}
}
fn parse_items<'i, 't, D: Parse<'i>>(
input: &mut Parser<'i, 't>,
) -> Result<Vec<GradientItem<D>>, ParseError<'i, ParserError<'i>>> {
let mut items = Vec::new();
let mut seen_stop = false;
loop {
input.parse_until_before(Delimiter::Comma, |input| {
if seen_stop {
if let Ok(hint) = input.try_parse(D::parse) {
seen_stop = false;
items.push(GradientItem::Hint(hint));
return Ok(());
}
}
let stop = ColorStop::parse(input)?;
if let Ok(position) = input.try_parse(D::parse) {
let color = stop.color.clone();
items.push(GradientItem::ColorStop(stop));
items.push(GradientItem::ColorStop(ColorStop {
color,
position: Some(position),
}))
} else {
items.push(GradientItem::ColorStop(stop));
}
seen_stop = true;
Ok(())
})?;
match input.next() {
Err(_) => break,
Ok(Token::Comma) => continue,
_ => unreachable!(),
}
}
Ok(items)
}
fn serialize_items<
D: ToCss + std::cmp::PartialEq<D> + std::ops::Mul<f32, Output = D> + TrySign + Clone + std::fmt::Debug,
W,
>(
items: &Vec<GradientItem<DimensionPercentage<D>>>,
dest: &mut Printer<W>,
) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
let mut first = true;
let mut last: Option<&GradientItem<DimensionPercentage<D>>> = None;
for item in items {
// Skip useless hints
if *item == GradientItem::Hint(DimensionPercentage::Percentage(Percentage(0.5))) {
continue;
}
// Use double position stop if the last stop is the same color and all targets support it.
if let Some(prev) = last {
if !should_compile!(dest.targets.current, DoublePositionGradients) {
match (prev, item) {
(
GradientItem::ColorStop(ColorStop {
position: Some(_),
color: ca,
}),
GradientItem::ColorStop(ColorStop {
position: Some(p),
color: cb,
}),
) if ca == cb => {
dest.write_char(' ')?;
p.to_css(dest)?;
last = None;
continue;
}
_ => {}
}
}
}
if first {
first = false;
} else {
dest.delim(',', false)?;
}
item.to_css(dest)?;
last = Some(item)
}
Ok(())
}
/// A legacy `-webkit-gradient()`.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(tag = "kind", rename_all = "kebab-case")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
pub enum WebKitGradient {
/// A linear `-webkit-gradient()`.
Linear {
/// The starting point of the gradient.
from: WebKitGradientPoint,
/// The ending point of the gradient.
to: WebKitGradientPoint,
/// The color stops in the gradient.
stops: Vec<WebKitColorStop>,
},
/// A radial `-webkit-gradient()`.
Radial {
/// The starting point of the gradient.
from: WebKitGradientPoint,
/// The starting radius of the gradient.
r0: CSSNumber,
/// The ending point of the gradient.
to: WebKitGradientPoint,
/// The ending radius of the gradient.
r1: CSSNumber,
/// The color stops in the gradient.
stops: Vec<WebKitColorStop>,
},
}
impl<'i> Parse<'i> for WebKitGradient {
fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
let location = input.current_source_location();
let ident = input.expect_ident_cloned()?;
input.expect_comma()?;
match_ignore_ascii_case! { &ident,
"linear" => {
let from = WebKitGradientPoint::parse(input)?;
input.expect_comma()?;
let to = WebKitGradientPoint::parse(input)?;
input.expect_comma()?;
let stops = input.parse_comma_separated(WebKitColorStop::parse)?;
Ok(WebKitGradient::Linear {
from,
to,
stops
})
},
"radial" => {
let from = WebKitGradientPoint::parse(input)?;
input.expect_comma()?;
let r0 = CSSNumber::parse(input)?;
input.expect_comma()?;
let to = WebKitGradientPoint::parse(input)?;
input.expect_comma()?;
let r1 = CSSNumber::parse(input)?;
input.expect_comma()?;
let stops = input.parse_comma_separated(WebKitColorStop::parse)?;
Ok(WebKitGradient::Radial {
from,
r0,
to,
r1,
stops
})
},
_ => Err(location.new_unexpected_token_error(cssparser::Token::Ident(ident.clone())))
}
}
}
impl ToCss for WebKitGradient {
fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
match self {
WebKitGradient::Linear { from, to, stops } => {
dest.write_str("linear")?;
dest.delim(',', false)?;
from.to_css(dest)?;
dest.delim(',', false)?;
to.to_css(dest)?;
for stop in stops {
dest.delim(',', false)?;
stop.to_css(dest)?;
}
Ok(())
}
WebKitGradient::Radial {
from,
r0,
to,
r1,
stops,
} => {
dest.write_str("radial")?;
dest.delim(',', false)?;
from.to_css(dest)?;
dest.delim(',', false)?;
r0.to_css(dest)?;
dest.delim(',', false)?;
to.to_css(dest)?;
dest.delim(',', false)?;
r1.to_css(dest)?;
for stop in stops {
dest.delim(',', false)?;
stop.to_css(dest)?;
}
Ok(())
}
}
}
}
impl WebKitGradient {
fn get_fallback(&self, kind: ColorFallbackKind) -> WebKitGradient {
let stops = match self {
WebKitGradient::Linear { stops, .. } => stops,
WebKitGradient::Radial { stops, .. } => stops,
};
let stops = stops.iter().map(|stop| stop.get_fallback(kind)).collect();
match self {
WebKitGradient::Linear { from, to, .. } => WebKitGradient::Linear {
from: from.clone(),
to: to.clone(),
stops,
},
WebKitGradient::Radial { from, r0, to, r1, .. } => WebKitGradient::Radial {
from: from.clone(),
r0: *r0,
to: to.clone(),
r1: *r1,
stops,
},
}
}
}
/// A color stop within a legacy `-webkit-gradient()`.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct WebKitColorStop {
/// The color of the color stop.
pub color: CssColor,
/// The position of the color stop.
pub position: CSSNumber,
}
impl<'i> Parse<'i> for WebKitColorStop {
fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
let location = input.current_source_location();
let function = input.expect_function()?.clone();
input.parse_nested_block(|input| {
let position = match_ignore_ascii_case! { &function,
"color-stop" => {
let p = NumberOrPercentage::parse(input)?;
input.expect_comma()?;
(&p).into()
},
"from" => 0.0,
"to" => 1.0,
_ => return Err(location.new_unexpected_token_error(cssparser::Token::Ident(function.clone())))
};
let color = CssColor::parse(input)?;
Ok(WebKitColorStop { color, position })
})
}
}
impl ToCss for WebKitColorStop {
fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
if self.position == 0.0 {
dest.write_str("from(")?;
self.color.to_css(dest)?;
} else if self.position == 1.0 {
dest.write_str("to(")?;
self.color.to_css(dest)?;
} else {
dest.write_str("color-stop(")?;
self.position.to_css(dest)?;
dest.delim(',', false)?;
self.color.to_css(dest)?;
}
dest.write_char(')')
}
}
impl WebKitColorStop {
fn get_fallback(&self, kind: ColorFallbackKind) -> WebKitColorStop {
WebKitColorStop {
color: self.color.get_fallback(kind),
position: self.position,
}
}
}
/// An x/y position within a legacy `-webkit-gradient()`.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
pub struct WebKitGradientPoint {
/// The x-position.
pub x: WebKitGradientPointComponent<HorizontalPositionKeyword>,
/// The y-position.
pub y: WebKitGradientPointComponent<VerticalPositionKeyword>,
}
impl<'i> Parse<'i> for WebKitGradientPoint {
fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
let x = WebKitGradientPointComponent::parse(input)?;
let y = WebKitGradientPointComponent::parse(input)?;
Ok(WebKitGradientPoint { x, y })
}
}
impl ToCss for WebKitGradientPoint {
fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
self.x.to_css(dest)?;
dest.write_char(' ')?;
self.y.to_css(dest)
}
}
/// A keyword or number within a [WebKitGradientPoint](WebKitGradientPoint).
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(tag = "type", content = "value", rename_all = "kebab-case")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
pub enum WebKitGradientPointComponent<S> {
/// The `center` keyword.
Center,
/// A number or percentage.
Number(NumberOrPercentage),
/// A side keyword.
Side(S),
}
impl<'i, S: Parse<'i>> Parse<'i> for WebKitGradientPointComponent<S> {
fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
if input.try_parse(|i| i.expect_ident_matching("center")).is_ok() {
return Ok(WebKitGradientPointComponent::Center);
}
if let Ok(lp) = input.try_parse(NumberOrPercentage::parse) {
return Ok(WebKitGradientPointComponent::Number(lp));
}
let keyword = S::parse(input)?;
Ok(WebKitGradientPointComponent::Side(keyword))
}
}
impl<S: ToCss + Clone + Into<LengthPercentage>> ToCss for WebKitGradientPointComponent<S> {
fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
use WebKitGradientPointComponent::*;
match &self {
Center => {
if dest.minify {
dest.write_str("50%")
} else {
dest.write_str("center")
}
}
Number(lp) => {
if matches!(lp, NumberOrPercentage::Percentage(Percentage(p)) if *p == 0.0) {
dest.write_char('0')
} else {
lp.to_css(dest)
}
}
Side(s) => {
if dest.minify {
let lp: LengthPercentage = s.clone().into();
lp.to_css(dest)?;
} else {
s.to_css(dest)?;
}
Ok(())
}
}
}
}
impl<S: Clone> WebKitGradientPointComponent<S> {
/// Attempts to convert a standard position to a webkit gradient point.
fn from_position(pos: &PositionComponent<S>) -> Result<WebKitGradientPointComponent<S>, ()> {
match pos {
PositionComponent::Center => Ok(WebKitGradientPointComponent::Center),
PositionComponent::Length(len) => {
Ok(WebKitGradientPointComponent::Number(match len {
LengthPercentage::Percentage(p) => NumberOrPercentage::Percentage(p.clone()),
LengthPercentage::Dimension(d) => {
// Webkit gradient points can only be specified in pixels.
if let Some(px) = d.to_px() {
NumberOrPercentage::Number(px)
} else {
return Err(());
}
}
_ => return Err(()),
}))
}
PositionComponent::Side { side, offset } => {
if offset.is_some() {
return Err(());
}
Ok(WebKitGradientPointComponent::Side(side.clone()))
}
}
}
}
impl WebKitGradient {
/// Attempts to convert a standard gradient to a legacy -webkit-gradient()
pub fn from_standard(gradient: &Gradient) -> Result<WebKitGradient, ()> {
match gradient {
Gradient::Linear(linear) => {
// Convert from line direction to a from and to point, if possible.
let (from, to) = match &linear.direction {
LineDirection::Horizontal(horizontal) => match horizontal {
HorizontalPositionKeyword::Left => ((1.0, 0.0), (0.0, 0.0)),
HorizontalPositionKeyword::Right => ((0.0, 0.0), (1.0, 0.0)),
},
LineDirection::Vertical(vertical) => match vertical {
VerticalPositionKeyword::Top => ((0.0, 1.0), (0.0, 0.0)),
VerticalPositionKeyword::Bottom => ((0.0, 0.0), (0.0, 1.0)),
},
LineDirection::Corner { horizontal, vertical } => match (horizontal, vertical) {
(HorizontalPositionKeyword::Left, VerticalPositionKeyword::Top) => ((1.0, 1.0), (0.0, 0.0)),
(HorizontalPositionKeyword::Left, VerticalPositionKeyword::Bottom) => ((1.0, 0.0), (0.0, 1.0)),
(HorizontalPositionKeyword::Right, VerticalPositionKeyword::Top) => ((0.0, 1.0), (1.0, 0.0)),
(HorizontalPositionKeyword::Right, VerticalPositionKeyword::Bottom) => ((0.0, 0.0), (1.0, 1.0)),
},
LineDirection::Angle(angle) => {
let degrees = angle.to_degrees();
if degrees == 0.0 {
((0.0, 1.0), (0.0, 0.0))
} else if degrees == 90.0 {
((0.0, 0.0), (1.0, 0.0))
} else if degrees == 180.0 {
((0.0, 0.0), (0.0, 1.0))
} else if degrees == 270.0 {
((1.0, 0.0), (0.0, 0.0))
} else {
return Err(());
}
}
};
Ok(WebKitGradient::Linear {
from: WebKitGradientPoint {
x: WebKitGradientPointComponent::Number(NumberOrPercentage::Percentage(Percentage(from.0))),
y: WebKitGradientPointComponent::Number(NumberOrPercentage::Percentage(Percentage(from.1))),
},
to: WebKitGradientPoint {
x: WebKitGradientPointComponent::Number(NumberOrPercentage::Percentage(Percentage(to.0))),
y: WebKitGradientPointComponent::Number(NumberOrPercentage::Percentage(Percentage(to.1))),
},
stops: convert_stops_to_webkit(&linear.items)?,
})
}
Gradient::Radial(radial) => {
// Webkit radial gradients are always circles, not ellipses, and must be specified in pixels.
let radius = match &radial.shape {
EndingShape::Circle(Circle::Radius(radius)) => {
if let Some(r) = radius.to_px() {
r
} else {
return Err(());
}
}
_ => return Err(()),
};
let x = WebKitGradientPointComponent::from_position(&radial.position.x)?;
let y = WebKitGradientPointComponent::from_position(&radial.position.y)?;
let point = WebKitGradientPoint { x, y };
Ok(WebKitGradient::Radial {
from: point.clone(),
r0: 0.0,
to: point,
r1: radius,
stops: convert_stops_to_webkit(&radial.items)?,
})
}
_ => Err(()),
}
}
}
fn convert_stops_to_webkit(items: &Vec<GradientItem<LengthPercentage>>) -> Result<Vec<WebKitColorStop>, ()> {
let mut stops = Vec::with_capacity(items.len());
for (i, item) in items.iter().enumerate() {
match item {
GradientItem::ColorStop(stop) => {
// webkit stops must always be percentage based, not length based.
let position = if let Some(pos) = &stop.position {
if let LengthPercentage::Percentage(position) = pos {
position.0
} else {
return Err(());
}
} else if i == 0 {
0.0
} else if i == items.len() - 1 {
1.0
} else {
return Err(());
};
stops.push(WebKitColorStop {
color: stop.color.clone(),
position,
})
}
_ => return Err(()),
}
}
Ok(stops)
}