/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! CSS handling for the specified value of //! [`basic-shape`][basic-shape]s //! //! [basic-shape]: https://drafts.csswg.org/css-shapes/#typedef-basic-shape use crate::derives::*; use crate::parser::{Parse, ParserContext}; use crate::values::computed::basic_shape::InsetRect as ComputedInsetRect; use crate::values::computed::{ Context, LengthPercentage as ComputedLengthPercentage, ToComputedValue, }; use crate::values::generics::basic_shape as generic; use crate::values::generics::basic_shape::{Path, PolygonCoord}; use crate::values::generics::position::GenericPositionOrAuto; use crate::values::generics::rect::Rect; use crate::values::specified::angle::Angle; use crate::values::specified::border::BorderRadius; use crate::values::specified::image::Image; use crate::values::specified::length::LengthPercentageOrAuto; use crate::values::specified::position::{Position, Side}; use crate::values::specified::url::SpecifiedUrl; use crate::values::specified::PositionComponent; use crate::values::specified::{LengthPercentage, NonNegativeLengthPercentage, SVGPathData}; use crate::values::CSSFloat; use crate::Zero; use cssparser::{match_ignore_ascii_case, Parser}; use std::fmt::{self, Write}; use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; /// A specified alias for FillRule. pub use crate::values::generics::basic_shape::FillRule; /// A specified `clip-path` value. pub type ClipPath = generic::GenericClipPath; /// A specified `shape-outside` value. pub type ShapeOutside = generic::GenericShapeOutside; /// A specified value for `at ` in circle() and ellipse(). // Note: its computed value is the same as computed::position::Position. We just want to always use // LengthPercentage as the type of its components, for basic shapes. pub type RadialPosition = generic::ShapePosition; /// A specified basic shape. pub type BasicShape = generic::GenericBasicShape; /// The specified value of `inset()`. pub type InsetRect = generic::GenericInsetRect; /// A specified circle. pub type Circle = generic::Circle; /// A specified ellipse. pub type Ellipse = generic::Ellipse; /// The specified value of `ShapeRadius`. pub type ShapeRadius = generic::ShapeRadius; /// The specified value of `Polygon`. pub type Polygon = generic::GenericPolygon; /// The specified value of `PathOrShapeFunction`. pub type PathOrShapeFunction = generic::GenericPathOrShapeFunction; /// The specified value of `ShapeCommand`. pub type ShapeCommand = generic::GenericShapeCommand; /// The specified value of `xywh()`. /// Defines a rectangle via offsets from the top and left edge of the reference box, and a /// specified width and height. /// /// The four s define, respectively, the inset from the left edge of the /// reference box, the inset from the top edge of the reference box, the width of the rectangle, /// and the height of the rectangle. /// /// https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-xywh #[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToShmem)] pub struct Xywh { /// The left edge of the reference box. pub x: LengthPercentage, /// The top edge of the reference box. pub y: LengthPercentage, /// The specified width. pub width: NonNegativeLengthPercentage, /// The specified height. pub height: NonNegativeLengthPercentage, /// The optional argument(s) define rounded corners for the inset rectangle /// using the border-radius shorthand syntax. pub round: BorderRadius, } /// Defines a rectangle via insets from the top and left edges of the reference box. /// /// https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-rect #[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToShmem)] #[repr(C)] pub struct ShapeRectFunction { /// The four s define the position of the top, right, bottom, and left edges /// of a rectangle, respectively, as insets from the top edge of the reference box (for the /// first and third values) or the left edge of the reference box (for the second and fourth /// values). /// /// An auto value makes the edge of the box coincide with the corresponding edge of the /// reference box: it’s equivalent to 0% as the first (top) or fourth (left) value, and /// equivalent to 100% as the second (right) or third (bottom) value. pub rect: Rect, /// The optional argument(s) define rounded corners for the inset rectangle /// using the border-radius shorthand syntax. pub round: BorderRadius, } /// The specified value of . /// = | | /// /// https://drafts.csswg.org/css-shapes-1/#supported-basic-shapes #[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] pub enum BasicShapeRect { /// Defines an inset rectangle via insets from each edge of the reference box. Inset(InsetRect), /// Defines a xywh function. #[css(function)] Xywh(Xywh), /// Defines a rect function. #[css(function)] Rect(ShapeRectFunction), } /// For filled shapes, we use fill-rule, and store it for path() and polygon(). /// For outline shapes, we should ignore fill-rule. /// /// https://github.com/w3c/fxtf-drafts/issues/512 /// https://github.com/w3c/csswg-drafts/issues/7390 /// https://github.com/w3c/csswg-drafts/issues/3468 pub enum ShapeType { /// The CSS property uses filled shapes. The default behavior. Filled, /// The CSS property uses outline shapes. This is especially useful for offset-path. Outline, } bitflags! { /// The flags to represent which basic shapes we would like to support. /// /// Different properties may use different subsets of : /// e.g. /// clip-path: all basic shapes. /// motion-path: all basic shapes (but ignore fill-rule). /// shape-outside: inset(), circle(), ellipse(), polygon(). /// /// Also there are some properties we don't support for now: /// shape-inside: inset(), circle(), ellipse(), polygon(). /// SVG shape-inside and shape-subtract: circle(), ellipse(), polygon(). /// /// The spec issue proposes some better ways to clarify the usage of basic shapes, so for now /// we use the bitflags to choose the supported basic shapes for each property at the parse /// time. /// https://github.com/w3c/csswg-drafts/issues/7390 #[derive(Clone, Copy)] #[repr(C)] pub struct AllowedBasicShapes: u8 { /// inset(). const INSET = 1 << 0; /// xywh(). const XYWH = 1 << 1; /// rect(). const RECT = 1 << 2; /// circle(). const CIRCLE = 1 << 3; /// ellipse(). const ELLIPSE = 1 << 4; /// polygon(). const POLYGON = 1 << 5; /// path(). const PATH = 1 << 6; /// shape(). const SHAPE = 1 << 7; /// All flags. const ALL = Self::INSET.bits() | Self::XYWH.bits() | Self::RECT.bits() | Self::CIRCLE.bits() | Self::ELLIPSE.bits() | Self::POLYGON.bits() | Self::PATH.bits() | Self::SHAPE.bits(); /// For shape-outside. const SHAPE_OUTSIDE = Self::INSET.bits() | Self::XYWH.bits() | Self::RECT.bits() | Self::CIRCLE.bits() | Self::ELLIPSE.bits() | Self::POLYGON.bits(); } } /// A helper for both clip-path and shape-outside parsing of shapes. fn parse_shape_or_box<'i, 't, R, ReferenceBox>( context: &ParserContext, input: &mut Parser<'i, 't>, to_shape: impl FnOnce(Box, ReferenceBox) -> R, to_reference_box: impl FnOnce(ReferenceBox) -> R, flags: AllowedBasicShapes, ) -> Result> where ReferenceBox: Default + Parse, { let mut shape = None; let mut ref_box = None; loop { if shape.is_none() { shape = input .try_parse(|i| BasicShape::parse(context, i, flags, ShapeType::Filled)) .ok(); } if ref_box.is_none() { ref_box = input.try_parse(|i| ReferenceBox::parse(context, i)).ok(); if ref_box.is_some() { continue; } } break; } if let Some(shp) = shape { return Ok(to_shape(Box::new(shp), ref_box.unwrap_or_default())); } match ref_box { Some(r) => Ok(to_reference_box(r)), None => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), } } impl Parse for ClipPath { #[inline] fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { return Ok(ClipPath::None); } if let Ok(url) = input.try_parse(|i| SpecifiedUrl::parse(context, i)) { return Ok(ClipPath::Url(url)); } parse_shape_or_box( context, input, ClipPath::Shape, ClipPath::Box, AllowedBasicShapes::ALL, ) } } impl Parse for ShapeOutside { #[inline] fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { // Need to parse this here so that `Image::parse_with_cors_anonymous` // doesn't parse it. if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { return Ok(ShapeOutside::None); } if let Ok(image) = input.try_parse(|i| Image::parse_with_cors_anonymous(context, i)) { debug_assert_ne!(image, Image::None); return Ok(ShapeOutside::Image(image)); } parse_shape_or_box( context, input, ShapeOutside::Shape, ShapeOutside::Box, AllowedBasicShapes::SHAPE_OUTSIDE, ) } } impl BasicShape { /// Parse with some parameters. /// 1. The supported . /// 2. The type of shapes. Should we ignore fill-rule? /// 3. The default value of `at `. pub fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, flags: AllowedBasicShapes, shape_type: ShapeType, ) -> Result> { let location = input.current_source_location(); let function = input.expect_function()?.clone(); input.parse_nested_block(move |i| { match_ignore_ascii_case! { &function, "inset" if flags.contains(AllowedBasicShapes::INSET) => { InsetRect::parse_function_arguments(context, i) .map(BasicShapeRect::Inset) .map(BasicShape::Rect) }, "xywh" if flags.contains(AllowedBasicShapes::XYWH) => { Xywh::parse_function_arguments(context, i) .map(BasicShapeRect::Xywh) .map(BasicShape::Rect) }, "rect" if flags.contains(AllowedBasicShapes::RECT) => { ShapeRectFunction::parse_function_arguments(context, i) .map(BasicShapeRect::Rect) .map(BasicShape::Rect) }, "circle" if flags.contains(AllowedBasicShapes::CIRCLE) => { Circle::parse_function_arguments(context, i) .map(BasicShape::Circle) }, "ellipse" if flags.contains(AllowedBasicShapes::ELLIPSE) => { Ellipse::parse_function_arguments(context, i) .map(BasicShape::Ellipse) }, "polygon" if flags.contains(AllowedBasicShapes::POLYGON) => { Polygon::parse_function_arguments(context, i, shape_type) .map(BasicShape::Polygon) }, "path" if flags.contains(AllowedBasicShapes::PATH) => { Path::parse_function_arguments(i, shape_type) .map(PathOrShapeFunction::Path) .map(BasicShape::PathOrShape) }, "shape" if flags.contains(AllowedBasicShapes::SHAPE) && static_prefs::pref!("layout.css.basic-shape-shape.enabled") => { generic::Shape::parse_function_arguments(context, i, shape_type) .map(PathOrShapeFunction::Shape) .map(BasicShape::PathOrShape) }, _ => Err(location .new_custom_error(StyleParseErrorKind::UnexpectedFunction(function.clone()))), } }) } } impl Parse for InsetRect { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { input.expect_function_matching("inset")?; input.parse_nested_block(|i| Self::parse_function_arguments(context, i)) } } fn parse_round<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { if input .try_parse(|i| i.expect_ident_matching("round")) .is_ok() { return BorderRadius::parse(context, input); } Ok(BorderRadius::zero()) } impl InsetRect { /// Parse the inner function arguments of `inset()` fn parse_function_arguments<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { let rect = Rect::parse_with(context, input, LengthPercentage::parse)?; let round = parse_round(context, input)?; Ok(generic::InsetRect { rect, round }) } } impl ToCss for RadialPosition { fn to_css(&self, dest: &mut CssWriter) -> fmt::Result where W: Write, { self.horizontal.to_css(dest)?; dest.write_char(' ')?; self.vertical.to_css(dest) } } fn convert_to_length_percentage(c: PositionComponent) -> LengthPercentage { use crate::values::specified::{AllowedNumericType, Percentage}; // Convert the value when parsing, to make sure we serialize it properly for both // specified and computed values. // https://drafts.csswg.org/css-shapes-1/#basic-shape-serialization match c { // Since keywords stand in for percentages, keywords without an offset // turn into percentages. PositionComponent::Center => LengthPercentage::from(Percentage::new(0.5)), PositionComponent::Side(keyword, None) => { Percentage::new(if keyword.is_start() { 0. } else { 1. }).into() }, // Per spec issue, https://github.com/w3c/csswg-drafts/issues/8695, the part of // "avoiding calc() expressions where possible" and "avoiding calc() // transformations" will be removed from the spec, and we should follow the // css-values-4 for position, i.e. we make it as length-percentage always. // https://drafts.csswg.org/css-shapes-1/#basic-shape-serialization. // https://drafts.csswg.org/css-values-4/#typedef-position PositionComponent::Side(keyword, Some(length)) => { if keyword.is_start() { length } else { length.hundred_percent_minus(AllowedNumericType::All) } }, PositionComponent::Length(length) => length, } } fn parse_at_position<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result, ParseError<'i>> { use crate::values::specified::position::Position; if input.try_parse(|i| i.expect_ident_matching("at")).is_ok() { Position::parse(context, input).map(|pos| { GenericPositionOrAuto::Position(RadialPosition::new( convert_to_length_percentage(pos.horizontal), convert_to_length_percentage(pos.vertical), )) }) } else { // `at ` is omitted. Ok(GenericPositionOrAuto::Auto) } } impl Parse for Circle { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { input.expect_function_matching("circle")?; input.parse_nested_block(|i| Self::parse_function_arguments(context, i)) } } impl Circle { fn parse_function_arguments<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { let radius = input .try_parse(|i| ShapeRadius::parse(context, i)) .unwrap_or_default(); let position = parse_at_position(context, input)?; Ok(generic::Circle { radius, position }) } } impl Parse for Ellipse { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { input.expect_function_matching("ellipse")?; input.parse_nested_block(|i| Self::parse_function_arguments(context, i)) } } impl Ellipse { fn parse_function_arguments<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { let (semiaxis_x, semiaxis_y) = input .try_parse(|i| -> Result<_, ParseError> { Ok(( ShapeRadius::parse(context, i)?, ShapeRadius::parse(context, i)?, )) }) .unwrap_or_default(); let position = parse_at_position(context, input)?; Ok(generic::Ellipse { semiaxis_x, semiaxis_y, position, }) } } fn parse_fill_rule<'i, 't>( input: &mut Parser<'i, 't>, shape_type: ShapeType, expect_comma: bool, ) -> FillRule { match shape_type { // Per [1] and [2], we ignore `` for outline shapes, so always use a default // value. // [1] https://github.com/w3c/csswg-drafts/issues/3468 // [2] https://github.com/w3c/csswg-drafts/issues/7390 // // Also, per [3] and [4], we would like the ignore `` from outline shapes, e.g. // offset-path, which means we don't parse it when setting `ShapeType::Outline`. // This should be web compatible because the shipped "offset-path:path()" doesn't have // `` and "offset-path:polygon()" is a new feature and still behind the // preference. // [3] https://github.com/w3c/fxtf-drafts/issues/512#issuecomment-1545393321 // [4] https://github.com/w3c/fxtf-drafts/issues/512#issuecomment-1555330929 ShapeType::Outline => Default::default(), ShapeType::Filled => input .try_parse(|i| -> Result<_, ParseError> { let fill = FillRule::parse(i)?; if expect_comma { i.expect_comma()?; } Ok(fill) }) .unwrap_or_default(), } } impl Parse for Polygon { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { input.expect_function_matching("polygon")?; input.parse_nested_block(|i| Self::parse_function_arguments(context, i, ShapeType::Filled)) } } impl Polygon { /// Parse the inner arguments of a `polygon` function. fn parse_function_arguments<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, shape_type: ShapeType, ) -> Result> { let fill = parse_fill_rule(input, shape_type, true /* has comma */); let coordinates = input .parse_comma_separated(|i| { Ok(PolygonCoord( LengthPercentage::parse(context, i)?, LengthPercentage::parse(context, i)?, )) })? .into(); Ok(Polygon { fill, coordinates }) } } impl Path { /// Parse the inner arguments of a `path` function. fn parse_function_arguments<'i, 't>( input: &mut Parser<'i, 't>, shape_type: ShapeType, ) -> Result> { use crate::values::specified::svg_path::AllowEmpty; let fill = parse_fill_rule(input, shape_type, true /* has comma */); let path = SVGPathData::parse(input, AllowEmpty::No)?; Ok(Path { fill, path }) } } fn round_to_css(round: &BorderRadius, dest: &mut CssWriter) -> fmt::Result where W: Write, { if !round.is_zero() { dest.write_str(" round ")?; round.to_css(dest)?; } Ok(()) } impl ToCss for Xywh { fn to_css(&self, dest: &mut CssWriter) -> fmt::Result where W: Write, { self.x.to_css(dest)?; dest.write_char(' ')?; self.y.to_css(dest)?; dest.write_char(' ')?; self.width.to_css(dest)?; dest.write_char(' ')?; self.height.to_css(dest)?; round_to_css(&self.round, dest) } } impl Xywh { /// Parse the inner function arguments of `xywh()`. fn parse_function_arguments<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { let x = LengthPercentage::parse(context, input)?; let y = LengthPercentage::parse(context, input)?; let width = NonNegativeLengthPercentage::parse(context, input)?; let height = NonNegativeLengthPercentage::parse(context, input)?; let round = parse_round(context, input)?; Ok(Xywh { x, y, width, height, round, }) } } impl ToCss for ShapeRectFunction { fn to_css(&self, dest: &mut CssWriter) -> fmt::Result where W: Write, { self.rect.0.to_css(dest)?; dest.write_char(' ')?; self.rect.1.to_css(dest)?; dest.write_char(' ')?; self.rect.2.to_css(dest)?; dest.write_char(' ')?; self.rect.3.to_css(dest)?; round_to_css(&self.round, dest) } } impl ShapeRectFunction { /// Parse the inner function arguments of `rect()`. fn parse_function_arguments<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { let rect = Rect::parse_all_components_with(context, input, LengthPercentageOrAuto::parse)?; let round = parse_round(context, input)?; Ok(ShapeRectFunction { rect, round }) } } impl ToComputedValue for BasicShapeRect { type ComputedValue = ComputedInsetRect; #[inline] fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { use crate::values::computed::LengthPercentage; use crate::values::computed::LengthPercentageOrAuto; use style_traits::values::specified::AllowedNumericType; match self { Self::Inset(ref inset) => inset.to_computed_value(context), Self::Xywh(ref xywh) => { // Given `xywh(x y w h)`, construct the equivalent inset() function, // `inset(y calc(100% - x - w) calc(100% - y - h) x)`. // // https://drafts.csswg.org/css-shapes-1/#basic-shape-computed-values // https://github.com/w3c/csswg-drafts/issues/9053 let x = xywh.x.to_computed_value(context); let y = xywh.y.to_computed_value(context); let w = xywh.width.to_computed_value(context); let h = xywh.height.to_computed_value(context); // calc(100% - x - w). let right = LengthPercentage::hundred_percent_minus_list( &[&x, &w.0], AllowedNumericType::All, ); // calc(100% - y - h). let bottom = LengthPercentage::hundred_percent_minus_list( &[&y, &h.0], AllowedNumericType::All, ); ComputedInsetRect { rect: Rect::new(y, right, bottom, x), round: xywh.round.to_computed_value(context), } }, Self::Rect(ref rect) => { // Given `rect(t r b l)`, the equivalent function is // `inset(t calc(100% - r) calc(100% - b) l)`. // // https://drafts.csswg.org/css-shapes-1/#basic-shape-computed-values fn compute_top_or_left(v: LengthPercentageOrAuto) -> LengthPercentage { match v { // it’s equivalent to 0% as the first (top) or fourth (left) value. // https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-rect LengthPercentageOrAuto::Auto => LengthPercentage::zero_percent(), LengthPercentageOrAuto::LengthPercentage(lp) => lp, } } fn compute_bottom_or_right(v: LengthPercentageOrAuto) -> LengthPercentage { match v { // It's equivalent to 100% as the second (right) or third (bottom) value. // So calc(100% - 100%) = 0%. // https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-rect LengthPercentageOrAuto::Auto => LengthPercentage::zero_percent(), LengthPercentageOrAuto::LengthPercentage(lp) => { LengthPercentage::hundred_percent_minus(lp, AllowedNumericType::All) }, } } let round = rect.round.to_computed_value(context); let rect = rect.rect.to_computed_value(context); let rect = Rect::new( compute_top_or_left(rect.0), compute_bottom_or_right(rect.1), compute_bottom_or_right(rect.2), compute_top_or_left(rect.3), ); ComputedInsetRect { rect, round } }, } } #[inline] fn from_computed_value(computed: &Self::ComputedValue) -> Self { Self::Inset(ToComputedValue::from_computed_value(computed)) } } impl generic::Shape { /// Parse the inner arguments of a `shape` function. /// shape() = shape(? from , #) fn parse_function_arguments<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, shape_type: ShapeType, ) -> Result> { let fill = parse_fill_rule(input, shape_type, false /* no following comma */); let mut first = true; let commands = input.parse_comma_separated(|i| { if first { first = false; // The starting point for the first shape-command. It adds an initial absolute // moveto to the list of path data commands, with the measured // from the top-left corner of the reference i.expect_ident_matching("from")?; Ok(ShapeCommand::Move { point: generic::CommandEndPoint::parse_endpoint_as_abs(context, i)?, }) } else { // The further path data commands. ShapeCommand::parse(context, i) } })?; // We must have one starting point and at least one following . if commands.len() < 2 { return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } Ok(Self { fill, commands: commands.into(), }) } } impl Parse for ShapeCommand { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { use crate::values::generics::basic_shape::{ ArcRadii, ArcSize, ArcSweep, AxisEndPoint, CommandEndPoint, ControlPoint, }; // = | | | // | | | close Ok(try_match_ident_ignore_ascii_case! { input, "close" => Self::Close, "move" => { let point = CommandEndPoint::parse(context, input)?; Self::Move { point } }, "line" => { let point = CommandEndPoint::parse(context, input)?; Self::Line { point } }, "hline" => { let x = AxisEndPoint::parse_hline(context, input)?; Self::HLine { x } }, "vline" => { let y = AxisEndPoint::parse_vline(context, input)?; Self::VLine { y } }, "curve" => { let point = CommandEndPoint::parse(context, input)?; input.expect_ident_matching("with")?; let control1 = ControlPoint::parse(context, input, point.is_abs())?; if input.try_parse(|i| i.expect_delim('/')).is_ok() { let control2 = ControlPoint::parse(context, input, point.is_abs())?; Self::CubicCurve { point, control1, control2, } } else { Self::QuadCurve { point, control1, } } }, "smooth" => { let point = CommandEndPoint::parse(context, input)?; if input.try_parse(|i| i.expect_ident_matching("with")).is_ok() { let control2 = ControlPoint::parse(context, input, point.is_abs())?; Self::SmoothCubic { point, control2, } } else { Self::SmoothQuad { point } } }, "arc" => { let point = CommandEndPoint::parse(context, input)?; input.expect_ident_matching("of")?; let rx = LengthPercentage::parse(context, input)?; let ry = input.try_parse(|i| LengthPercentage::parse(context, i)).ok(); let radii = ArcRadii { rx, ry: ry.into() }; // [ || || rotate ]? let mut arc_sweep = None; let mut arc_size = None; let mut rotate = None; loop { if arc_sweep.is_none() { arc_sweep = input.try_parse(ArcSweep::parse).ok(); } if arc_size.is_none() { arc_size = input.try_parse(ArcSize::parse).ok(); if arc_size.is_some() { continue; } } if rotate.is_none() && input .try_parse(|i| i.expect_ident_matching("rotate")) .is_ok() { rotate = Some(Angle::parse(context, input)?); continue; } break; } Self::Arc { point, radii, arc_sweep: arc_sweep.unwrap_or(ArcSweep::Ccw), arc_size: arc_size.unwrap_or(ArcSize::Small), rotate: rotate.unwrap_or(Angle::zero()), } }, }) } } impl Parse for generic::CoordinatePair { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { let x = LengthPercentage::parse(context, input)?; let y = LengthPercentage::parse(context, input)?; Ok(Self::new(x, y)) } } impl generic::ControlPoint { /// Parse = [ | ] fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, is_end_point_abs: bool, ) -> Result> { use generic::ControlReference; let coord = input.try_parse(|i| generic::CoordinatePair::parse(context, i)); // Parse if is_end_point_abs && coord.is_err() { let pos = Position::parse(context, input)?; return Ok(Self::Absolute(pos)); } // Parse = [from [ start | end | origin ]]? let coord = coord?; let mut reference = if is_end_point_abs { ControlReference::Origin } else { ControlReference::Start }; if input.try_parse(|i| i.expect_ident_matching("from")).is_ok() { reference = ControlReference::parse(input)?; } Ok(Self::Relative(generic::RelativeControlPoint { coord, reference, })) } } impl Parse for generic::CommandEndPoint { /// Parse = to | by fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { if ByTo::parse(input)?.is_abs() { Self::parse_endpoint_as_abs(context, input) } else { let point = generic::CoordinatePair::parse(context, input)?; Ok(Self::ByCoordinate(point)) } } } impl generic::CommandEndPoint { /// Parse = to fn parse_endpoint_as_abs<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { let point = Position::parse(context, input)?; Ok(generic::CommandEndPoint::ToPosition(point)) } } impl generic::AxisEndPoint { /// Parse pub fn parse_hline<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { use cssparser::Token; use generic::{AxisPosition, AxisPositionKeyword}; // If the command is relative, parse for only. if !ByTo::parse(input)?.is_abs() { return Ok(Self::ByCoordinate(LengthPercentage::parse(context, input)?)); } let x = AxisPosition::parse(context, input)?; if let AxisPosition::Keyword( _word @ (AxisPositionKeyword::Top | AxisPositionKeyword::Bottom | AxisPositionKeyword::YStart | AxisPositionKeyword::YEnd), ) = &x { let location = input.current_source_location(); let token = Token::Ident(x.to_css_string().into()); return Err(location.new_unexpected_token_error(token)); } Ok(Self::ToPosition(x)) } /// Parse pub fn parse_vline<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { use cssparser::Token; use generic::{AxisPosition, AxisPositionKeyword}; // If the command is relative, parse for only. if !ByTo::parse(input)?.is_abs() { return Ok(Self::ByCoordinate(LengthPercentage::parse(context, input)?)); } let y = AxisPosition::parse(context, input)?; if let AxisPosition::Keyword( _word @ (AxisPositionKeyword::Left | AxisPositionKeyword::Right | AxisPositionKeyword::XStart | AxisPositionKeyword::XEnd), ) = &y { // Return an error if we parsed a different keyword. let location = input.current_source_location(); let token = Token::Ident(y.to_css_string().into()); return Err(location.new_unexpected_token_error(token)); } Ok(Self::ToPosition(y)) } } impl ToComputedValue for generic::AxisPosition { type ComputedValue = generic::AxisPosition; fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { match self { Self::LengthPercent(lp) => { Self::ComputedValue::LengthPercent(lp.to_computed_value(context)) }, Self::Keyword(word) => { let lp = LengthPercentage::Percentage(word.as_percentage()); Self::ComputedValue::LengthPercent(lp.to_computed_value(context)) }, } } fn from_computed_value(computed: &Self::ComputedValue) -> Self { match computed { Self::ComputedValue::LengthPercent(lp) => { Self::LengthPercent(LengthPercentage::from_computed_value(lp)) }, _ => unreachable!("Invalid state: computed value cannot be a keyword."), } } } impl ToComputedValue for generic::AxisPosition { type ComputedValue = Self; fn to_computed_value(&self, _context: &Context) -> Self { *self } fn from_computed_value(computed: &Self) -> Self { *computed } } /// This determines whether the command is absolutely or relatively positioned. /// https://drafts.csswg.org/css-shapes-1/#typedef-shape-command-end-point #[derive(Clone, Copy, Debug, Parse, PartialEq)] enum ByTo { /// Command is relative to the command’s starting point. By, /// Command is relative to the top-left corner of the reference box. To, } impl ByTo { /// Return true if it is absolute, i.e. it is To. #[inline] pub fn is_abs(&self) -> bool { matches!(self, ByTo::To) } }