tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

basic_shape.rs (38950B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
      4 
      5 //! CSS handling for the specified value of
      6 //! [`basic-shape`][basic-shape]s
      7 //!
      8 //! [basic-shape]: https://drafts.csswg.org/css-shapes/#typedef-basic-shape
      9 
     10 use crate::derives::*;
     11 use crate::parser::{Parse, ParserContext};
     12 use crate::values::computed::basic_shape::InsetRect as ComputedInsetRect;
     13 use crate::values::computed::{
     14    Context, LengthPercentage as ComputedLengthPercentage, ToComputedValue,
     15 };
     16 use crate::values::generics::basic_shape as generic;
     17 use crate::values::generics::basic_shape::{Path, PolygonCoord};
     18 use crate::values::generics::position::GenericPositionOrAuto;
     19 use crate::values::generics::rect::Rect;
     20 use crate::values::specified::angle::Angle;
     21 use crate::values::specified::border::BorderRadius;
     22 use crate::values::specified::image::Image;
     23 use crate::values::specified::length::LengthPercentageOrAuto;
     24 use crate::values::specified::position::{Position, Side};
     25 use crate::values::specified::url::SpecifiedUrl;
     26 use crate::values::specified::PositionComponent;
     27 use crate::values::specified::{LengthPercentage, NonNegativeLengthPercentage, SVGPathData};
     28 use crate::values::CSSFloat;
     29 use crate::Zero;
     30 use cssparser::{match_ignore_ascii_case, Parser};
     31 use std::fmt::{self, Write};
     32 use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
     33 
     34 /// A specified alias for FillRule.
     35 pub use crate::values::generics::basic_shape::FillRule;
     36 
     37 /// A specified `clip-path` value.
     38 pub type ClipPath = generic::GenericClipPath<BasicShape, SpecifiedUrl>;
     39 
     40 /// A specified `shape-outside` value.
     41 pub type ShapeOutside = generic::GenericShapeOutside<BasicShape, Image>;
     42 
     43 /// A specified value for `at <position>` in circle() and ellipse().
     44 // Note: its computed value is the same as computed::position::Position. We just want to always use
     45 // LengthPercentage as the type of its components, for basic shapes.
     46 pub type RadialPosition = generic::ShapePosition<LengthPercentage>;
     47 
     48 /// A specified basic shape.
     49 pub type BasicShape = generic::GenericBasicShape<Angle, Position, LengthPercentage, BasicShapeRect>;
     50 
     51 /// The specified value of `inset()`.
     52 pub type InsetRect = generic::GenericInsetRect<LengthPercentage>;
     53 
     54 /// A specified circle.
     55 pub type Circle = generic::Circle<LengthPercentage>;
     56 
     57 /// A specified ellipse.
     58 pub type Ellipse = generic::Ellipse<LengthPercentage>;
     59 
     60 /// The specified value of `ShapeRadius`.
     61 pub type ShapeRadius = generic::ShapeRadius<LengthPercentage>;
     62 
     63 /// The specified value of `Polygon`.
     64 pub type Polygon = generic::GenericPolygon<LengthPercentage>;
     65 
     66 /// The specified value of `PathOrShapeFunction`.
     67 pub type PathOrShapeFunction =
     68    generic::GenericPathOrShapeFunction<Angle, Position, LengthPercentage>;
     69 
     70 /// The specified value of `ShapeCommand`.
     71 pub type ShapeCommand = generic::GenericShapeCommand<Angle, Position, LengthPercentage>;
     72 
     73 /// The specified value of `xywh()`.
     74 /// Defines a rectangle via offsets from the top and left edge of the reference box, and a
     75 /// specified width and height.
     76 ///
     77 /// The four <length-percentage>s define, respectively, the inset from the left edge of the
     78 /// reference box, the inset from the top edge of the reference box, the width of the rectangle,
     79 /// and the height of the rectangle.
     80 ///
     81 /// https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-xywh
     82 #[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToShmem)]
     83 pub struct Xywh {
     84    /// The left edge of the reference box.
     85    pub x: LengthPercentage,
     86    /// The top edge of the reference box.
     87    pub y: LengthPercentage,
     88    /// The specified width.
     89    pub width: NonNegativeLengthPercentage,
     90    /// The specified height.
     91    pub height: NonNegativeLengthPercentage,
     92    /// The optional <border-radius> argument(s) define rounded corners for the inset rectangle
     93    /// using the border-radius shorthand syntax.
     94    pub round: BorderRadius,
     95 }
     96 
     97 /// Defines a rectangle via insets from the top and left edges of the reference box.
     98 ///
     99 /// https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-rect
    100 #[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToShmem)]
    101 #[repr(C)]
    102 pub struct ShapeRectFunction {
    103    /// The four <length-percentage>s define the position of the top, right, bottom, and left edges
    104    /// of a rectangle, respectively, as insets from the top edge of the reference box (for the
    105    /// first and third values) or the left edge of the reference box (for the second and fourth
    106    /// values).
    107    ///
    108    /// An auto value makes the edge of the box coincide with the corresponding edge of the
    109    /// reference box: it’s equivalent to 0% as the first (top) or fourth (left) value, and
    110    /// equivalent to 100% as the second (right) or third (bottom) value.
    111    pub rect: Rect<LengthPercentageOrAuto>,
    112    /// The optional <border-radius> argument(s) define rounded corners for the inset rectangle
    113    /// using the border-radius shorthand syntax.
    114    pub round: BorderRadius,
    115 }
    116 
    117 /// The specified value of <basic-shape-rect>.
    118 /// <basic-shape-rect> = <inset()> | <rect()> | <xywh()>
    119 ///
    120 /// https://drafts.csswg.org/css-shapes-1/#supported-basic-shapes
    121 #[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
    122 pub enum BasicShapeRect {
    123    /// Defines an inset rectangle via insets from each edge of the reference box.
    124    Inset(InsetRect),
    125    /// Defines a xywh function.
    126    #[css(function)]
    127    Xywh(Xywh),
    128    /// Defines a rect function.
    129    #[css(function)]
    130    Rect(ShapeRectFunction),
    131 }
    132 
    133 /// For filled shapes, we use fill-rule, and store it for path() and polygon().
    134 /// For outline shapes, we should ignore fill-rule.
    135 ///
    136 /// https://github.com/w3c/fxtf-drafts/issues/512
    137 /// https://github.com/w3c/csswg-drafts/issues/7390
    138 /// https://github.com/w3c/csswg-drafts/issues/3468
    139 pub enum ShapeType {
    140    /// The CSS property uses filled shapes. The default behavior.
    141    Filled,
    142    /// The CSS property uses outline shapes. This is especially useful for offset-path.
    143    Outline,
    144 }
    145 
    146 bitflags! {
    147    /// The flags to represent which basic shapes we would like to support.
    148    ///
    149    /// Different properties may use different subsets of <basic-shape>:
    150    /// e.g.
    151    /// clip-path: all basic shapes.
    152    /// motion-path: all basic shapes (but ignore fill-rule).
    153    /// shape-outside: inset(), circle(), ellipse(), polygon().
    154    ///
    155    /// Also there are some properties we don't support for now:
    156    /// shape-inside: inset(), circle(), ellipse(), polygon().
    157    /// SVG shape-inside and shape-subtract: circle(), ellipse(), polygon().
    158    ///
    159    /// The spec issue proposes some better ways to clarify the usage of basic shapes, so for now
    160    /// we use the bitflags to choose the supported basic shapes for each property at the parse
    161    /// time.
    162    /// https://github.com/w3c/csswg-drafts/issues/7390
    163    #[derive(Clone, Copy)]
    164    #[repr(C)]
    165    pub struct AllowedBasicShapes: u8 {
    166        /// inset().
    167        const INSET = 1 << 0;
    168        /// xywh().
    169        const XYWH = 1 << 1;
    170        /// rect().
    171        const RECT = 1 << 2;
    172        /// circle().
    173        const CIRCLE = 1 << 3;
    174        /// ellipse().
    175        const ELLIPSE = 1 << 4;
    176        /// polygon().
    177        const POLYGON = 1 << 5;
    178        /// path().
    179        const PATH = 1 << 6;
    180        /// shape().
    181        const SHAPE = 1 << 7;
    182 
    183        /// All flags.
    184        const ALL =
    185            Self::INSET.bits() |
    186            Self::XYWH.bits() |
    187            Self::RECT.bits() |
    188            Self::CIRCLE.bits() |
    189            Self::ELLIPSE.bits() |
    190            Self::POLYGON.bits() |
    191            Self::PATH.bits() |
    192            Self::SHAPE.bits();
    193 
    194        /// For shape-outside.
    195        const SHAPE_OUTSIDE =
    196            Self::INSET.bits() |
    197            Self::CIRCLE.bits() |
    198            Self::ELLIPSE.bits() |
    199            Self::POLYGON.bits();
    200    }
    201 }
    202 
    203 /// A helper for both clip-path and shape-outside parsing of shapes.
    204 fn parse_shape_or_box<'i, 't, R, ReferenceBox>(
    205    context: &ParserContext,
    206    input: &mut Parser<'i, 't>,
    207    to_shape: impl FnOnce(Box<BasicShape>, ReferenceBox) -> R,
    208    to_reference_box: impl FnOnce(ReferenceBox) -> R,
    209    flags: AllowedBasicShapes,
    210 ) -> Result<R, ParseError<'i>>
    211 where
    212    ReferenceBox: Default + Parse,
    213 {
    214    let mut shape = None;
    215    let mut ref_box = None;
    216    loop {
    217        if shape.is_none() {
    218            shape = input
    219                .try_parse(|i| BasicShape::parse(context, i, flags, ShapeType::Filled))
    220                .ok();
    221        }
    222 
    223        if ref_box.is_none() {
    224            ref_box = input.try_parse(|i| ReferenceBox::parse(context, i)).ok();
    225            if ref_box.is_some() {
    226                continue;
    227            }
    228        }
    229        break;
    230    }
    231 
    232    if let Some(shp) = shape {
    233        return Ok(to_shape(Box::new(shp), ref_box.unwrap_or_default()));
    234    }
    235 
    236    match ref_box {
    237        Some(r) => Ok(to_reference_box(r)),
    238        None => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)),
    239    }
    240 }
    241 
    242 impl Parse for ClipPath {
    243    #[inline]
    244    fn parse<'i, 't>(
    245        context: &ParserContext,
    246        input: &mut Parser<'i, 't>,
    247    ) -> Result<Self, ParseError<'i>> {
    248        if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
    249            return Ok(ClipPath::None);
    250        }
    251 
    252        if let Ok(url) = input.try_parse(|i| SpecifiedUrl::parse(context, i)) {
    253            return Ok(ClipPath::Url(url));
    254        }
    255 
    256        parse_shape_or_box(
    257            context,
    258            input,
    259            ClipPath::Shape,
    260            ClipPath::Box,
    261            AllowedBasicShapes::ALL,
    262        )
    263    }
    264 }
    265 
    266 impl Parse for ShapeOutside {
    267    #[inline]
    268    fn parse<'i, 't>(
    269        context: &ParserContext,
    270        input: &mut Parser<'i, 't>,
    271    ) -> Result<Self, ParseError<'i>> {
    272        // Need to parse this here so that `Image::parse_with_cors_anonymous`
    273        // doesn't parse it.
    274        if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
    275            return Ok(ShapeOutside::None);
    276        }
    277 
    278        if let Ok(image) = input.try_parse(|i| Image::parse_with_cors_anonymous(context, i)) {
    279            debug_assert_ne!(image, Image::None);
    280            return Ok(ShapeOutside::Image(image));
    281        }
    282 
    283        parse_shape_or_box(
    284            context,
    285            input,
    286            ShapeOutside::Shape,
    287            ShapeOutside::Box,
    288            AllowedBasicShapes::SHAPE_OUTSIDE,
    289        )
    290    }
    291 }
    292 
    293 impl BasicShape {
    294    /// Parse with some parameters.
    295    /// 1. The supported <basic-shape>.
    296    /// 2. The type of shapes. Should we ignore fill-rule?
    297    /// 3. The default value of `at <position>`.
    298    pub fn parse<'i, 't>(
    299        context: &ParserContext,
    300        input: &mut Parser<'i, 't>,
    301        flags: AllowedBasicShapes,
    302        shape_type: ShapeType,
    303    ) -> Result<Self, ParseError<'i>> {
    304        let location = input.current_source_location();
    305        let function = input.expect_function()?.clone();
    306        input.parse_nested_block(move |i| {
    307            match_ignore_ascii_case! { &function,
    308                "inset" if flags.contains(AllowedBasicShapes::INSET) => {
    309                    InsetRect::parse_function_arguments(context, i)
    310                        .map(BasicShapeRect::Inset)
    311                        .map(BasicShape::Rect)
    312                },
    313                "xywh" if flags.contains(AllowedBasicShapes::XYWH) => {
    314                    Xywh::parse_function_arguments(context, i)
    315                        .map(BasicShapeRect::Xywh)
    316                        .map(BasicShape::Rect)
    317                },
    318                "rect" if flags.contains(AllowedBasicShapes::RECT) => {
    319                    ShapeRectFunction::parse_function_arguments(context, i)
    320                        .map(BasicShapeRect::Rect)
    321                        .map(BasicShape::Rect)
    322                },
    323                "circle" if flags.contains(AllowedBasicShapes::CIRCLE) => {
    324                    Circle::parse_function_arguments(context, i)
    325                        .map(BasicShape::Circle)
    326                },
    327                "ellipse" if flags.contains(AllowedBasicShapes::ELLIPSE) => {
    328                    Ellipse::parse_function_arguments(context, i)
    329                        .map(BasicShape::Ellipse)
    330                },
    331                "polygon" if flags.contains(AllowedBasicShapes::POLYGON) => {
    332                    Polygon::parse_function_arguments(context, i, shape_type)
    333                        .map(BasicShape::Polygon)
    334                },
    335                "path" if flags.contains(AllowedBasicShapes::PATH) => {
    336                    Path::parse_function_arguments(i, shape_type)
    337                        .map(PathOrShapeFunction::Path)
    338                        .map(BasicShape::PathOrShape)
    339                },
    340                "shape"
    341                    if flags.contains(AllowedBasicShapes::SHAPE)
    342                        && static_prefs::pref!("layout.css.basic-shape-shape.enabled") =>
    343                {
    344                    generic::Shape::parse_function_arguments(context, i, shape_type)
    345                        .map(PathOrShapeFunction::Shape)
    346                        .map(BasicShape::PathOrShape)
    347                },
    348                _ => Err(location
    349                    .new_custom_error(StyleParseErrorKind::UnexpectedFunction(function.clone()))),
    350            }
    351        })
    352    }
    353 }
    354 
    355 impl Parse for InsetRect {
    356    fn parse<'i, 't>(
    357        context: &ParserContext,
    358        input: &mut Parser<'i, 't>,
    359    ) -> Result<Self, ParseError<'i>> {
    360        input.expect_function_matching("inset")?;
    361        input.parse_nested_block(|i| Self::parse_function_arguments(context, i))
    362    }
    363 }
    364 
    365 fn parse_round<'i, 't>(
    366    context: &ParserContext,
    367    input: &mut Parser<'i, 't>,
    368 ) -> Result<BorderRadius, ParseError<'i>> {
    369    if input
    370        .try_parse(|i| i.expect_ident_matching("round"))
    371        .is_ok()
    372    {
    373        return BorderRadius::parse(context, input);
    374    }
    375 
    376    Ok(BorderRadius::zero())
    377 }
    378 
    379 impl InsetRect {
    380    /// Parse the inner function arguments of `inset()`
    381    fn parse_function_arguments<'i, 't>(
    382        context: &ParserContext,
    383        input: &mut Parser<'i, 't>,
    384    ) -> Result<Self, ParseError<'i>> {
    385        let rect = Rect::parse_with(context, input, LengthPercentage::parse)?;
    386        let round = parse_round(context, input)?;
    387        Ok(generic::InsetRect { rect, round })
    388    }
    389 }
    390 
    391 impl ToCss for RadialPosition {
    392    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
    393    where
    394        W: Write,
    395    {
    396        self.horizontal.to_css(dest)?;
    397        dest.write_char(' ')?;
    398        self.vertical.to_css(dest)
    399    }
    400 }
    401 
    402 fn convert_to_length_percentage<S: Side>(c: PositionComponent<S>) -> LengthPercentage {
    403    use crate::values::specified::{AllowedNumericType, Percentage};
    404    // Convert the value when parsing, to make sure we serialize it properly for both
    405    // specified and computed values.
    406    // https://drafts.csswg.org/css-shapes-1/#basic-shape-serialization
    407    match c {
    408        // Since <position> keywords stand in for percentages, keywords without an offset
    409        // turn into percentages.
    410        PositionComponent::Center => LengthPercentage::from(Percentage::new(0.5)),
    411        PositionComponent::Side(keyword, None) => {
    412            Percentage::new(if keyword.is_start() { 0. } else { 1. }).into()
    413        },
    414        // Per spec issue, https://github.com/w3c/csswg-drafts/issues/8695, the part of
    415        // "avoiding calc() expressions where possible" and "avoiding calc()
    416        // transformations" will be removed from the spec, and we should follow the
    417        // css-values-4 for position, i.e. we make it as length-percentage always.
    418        // https://drafts.csswg.org/css-shapes-1/#basic-shape-serialization.
    419        // https://drafts.csswg.org/css-values-4/#typedef-position
    420        PositionComponent::Side(keyword, Some(length)) => {
    421            if keyword.is_start() {
    422                length
    423            } else {
    424                length.hundred_percent_minus(AllowedNumericType::All)
    425            }
    426        },
    427        PositionComponent::Length(length) => length,
    428    }
    429 }
    430 
    431 fn parse_at_position<'i, 't>(
    432    context: &ParserContext,
    433    input: &mut Parser<'i, 't>,
    434 ) -> Result<GenericPositionOrAuto<RadialPosition>, ParseError<'i>> {
    435    use crate::values::specified::position::Position;
    436    if input.try_parse(|i| i.expect_ident_matching("at")).is_ok() {
    437        Position::parse(context, input).map(|pos| {
    438            GenericPositionOrAuto::Position(RadialPosition::new(
    439                convert_to_length_percentage(pos.horizontal),
    440                convert_to_length_percentage(pos.vertical),
    441            ))
    442        })
    443    } else {
    444        // `at <position>` is omitted.
    445        Ok(GenericPositionOrAuto::Auto)
    446    }
    447 }
    448 
    449 impl Parse for Circle {
    450    fn parse<'i, 't>(
    451        context: &ParserContext,
    452        input: &mut Parser<'i, 't>,
    453    ) -> Result<Self, ParseError<'i>> {
    454        input.expect_function_matching("circle")?;
    455        input.parse_nested_block(|i| Self::parse_function_arguments(context, i))
    456    }
    457 }
    458 
    459 impl Circle {
    460    fn parse_function_arguments<'i, 't>(
    461        context: &ParserContext,
    462        input: &mut Parser<'i, 't>,
    463    ) -> Result<Self, ParseError<'i>> {
    464        let radius = input
    465            .try_parse(|i| ShapeRadius::parse(context, i))
    466            .unwrap_or_default();
    467        let position = parse_at_position(context, input)?;
    468 
    469        Ok(generic::Circle { radius, position })
    470    }
    471 }
    472 
    473 impl Parse for Ellipse {
    474    fn parse<'i, 't>(
    475        context: &ParserContext,
    476        input: &mut Parser<'i, 't>,
    477    ) -> Result<Self, ParseError<'i>> {
    478        input.expect_function_matching("ellipse")?;
    479        input.parse_nested_block(|i| Self::parse_function_arguments(context, i))
    480    }
    481 }
    482 
    483 impl Ellipse {
    484    fn parse_function_arguments<'i, 't>(
    485        context: &ParserContext,
    486        input: &mut Parser<'i, 't>,
    487    ) -> Result<Self, ParseError<'i>> {
    488        let (semiaxis_x, semiaxis_y) = input
    489            .try_parse(|i| -> Result<_, ParseError> {
    490                Ok((
    491                    ShapeRadius::parse(context, i)?,
    492                    ShapeRadius::parse(context, i)?,
    493                ))
    494            })
    495            .unwrap_or_default();
    496        let position = parse_at_position(context, input)?;
    497 
    498        Ok(generic::Ellipse {
    499            semiaxis_x,
    500            semiaxis_y,
    501            position,
    502        })
    503    }
    504 }
    505 
    506 fn parse_fill_rule<'i, 't>(
    507    input: &mut Parser<'i, 't>,
    508    shape_type: ShapeType,
    509    expect_comma: bool,
    510 ) -> FillRule {
    511    match shape_type {
    512        // Per [1] and [2], we ignore `<fill-rule>` for outline shapes, so always use a default
    513        // value.
    514        // [1] https://github.com/w3c/csswg-drafts/issues/3468
    515        // [2] https://github.com/w3c/csswg-drafts/issues/7390
    516        //
    517        // Also, per [3] and [4], we would like the ignore `<file-rule>` from outline shapes, e.g.
    518        // offset-path, which means we don't parse it when setting `ShapeType::Outline`.
    519        // This should be web compatible because the shipped "offset-path:path()" doesn't have
    520        // `<fill-rule>` and "offset-path:polygon()" is a new feature and still behind the
    521        // preference.
    522        // [3] https://github.com/w3c/fxtf-drafts/issues/512#issuecomment-1545393321
    523        // [4] https://github.com/w3c/fxtf-drafts/issues/512#issuecomment-1555330929
    524        ShapeType::Outline => Default::default(),
    525        ShapeType::Filled => input
    526            .try_parse(|i| -> Result<_, ParseError> {
    527                let fill = FillRule::parse(i)?;
    528                if expect_comma {
    529                    i.expect_comma()?;
    530                }
    531                Ok(fill)
    532            })
    533            .unwrap_or_default(),
    534    }
    535 }
    536 
    537 impl Parse for Polygon {
    538    fn parse<'i, 't>(
    539        context: &ParserContext,
    540        input: &mut Parser<'i, 't>,
    541    ) -> Result<Self, ParseError<'i>> {
    542        input.expect_function_matching("polygon")?;
    543        input.parse_nested_block(|i| Self::parse_function_arguments(context, i, ShapeType::Filled))
    544    }
    545 }
    546 
    547 impl Polygon {
    548    /// Parse the inner arguments of a `polygon` function.
    549    fn parse_function_arguments<'i, 't>(
    550        context: &ParserContext,
    551        input: &mut Parser<'i, 't>,
    552        shape_type: ShapeType,
    553    ) -> Result<Self, ParseError<'i>> {
    554        let fill = parse_fill_rule(input, shape_type, true /* has comma */);
    555        let coordinates = input
    556            .parse_comma_separated(|i| {
    557                Ok(PolygonCoord(
    558                    LengthPercentage::parse(context, i)?,
    559                    LengthPercentage::parse(context, i)?,
    560                ))
    561            })?
    562            .into();
    563 
    564        Ok(Polygon { fill, coordinates })
    565    }
    566 }
    567 
    568 impl Path {
    569    /// Parse the inner arguments of a `path` function.
    570    fn parse_function_arguments<'i, 't>(
    571        input: &mut Parser<'i, 't>,
    572        shape_type: ShapeType,
    573    ) -> Result<Self, ParseError<'i>> {
    574        use crate::values::specified::svg_path::AllowEmpty;
    575 
    576        let fill = parse_fill_rule(input, shape_type, true /* has comma */);
    577        let path = SVGPathData::parse(input, AllowEmpty::No)?;
    578        Ok(Path { fill, path })
    579    }
    580 }
    581 
    582 fn round_to_css<W>(round: &BorderRadius, dest: &mut CssWriter<W>) -> fmt::Result
    583 where
    584    W: Write,
    585 {
    586    if !round.is_zero() {
    587        dest.write_str(" round ")?;
    588        round.to_css(dest)?;
    589    }
    590    Ok(())
    591 }
    592 
    593 impl ToCss for Xywh {
    594    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
    595    where
    596        W: Write,
    597    {
    598        self.x.to_css(dest)?;
    599        dest.write_char(' ')?;
    600        self.y.to_css(dest)?;
    601        dest.write_char(' ')?;
    602        self.width.to_css(dest)?;
    603        dest.write_char(' ')?;
    604        self.height.to_css(dest)?;
    605        round_to_css(&self.round, dest)
    606    }
    607 }
    608 
    609 impl Xywh {
    610    /// Parse the inner function arguments of `xywh()`.
    611    fn parse_function_arguments<'i, 't>(
    612        context: &ParserContext,
    613        input: &mut Parser<'i, 't>,
    614    ) -> Result<Self, ParseError<'i>> {
    615        let x = LengthPercentage::parse(context, input)?;
    616        let y = LengthPercentage::parse(context, input)?;
    617        let width = NonNegativeLengthPercentage::parse(context, input)?;
    618        let height = NonNegativeLengthPercentage::parse(context, input)?;
    619        let round = parse_round(context, input)?;
    620        Ok(Xywh {
    621            x,
    622            y,
    623            width,
    624            height,
    625            round,
    626        })
    627    }
    628 }
    629 
    630 impl ToCss for ShapeRectFunction {
    631    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
    632    where
    633        W: Write,
    634    {
    635        self.rect.0.to_css(dest)?;
    636        dest.write_char(' ')?;
    637        self.rect.1.to_css(dest)?;
    638        dest.write_char(' ')?;
    639        self.rect.2.to_css(dest)?;
    640        dest.write_char(' ')?;
    641        self.rect.3.to_css(dest)?;
    642        round_to_css(&self.round, dest)
    643    }
    644 }
    645 
    646 impl ShapeRectFunction {
    647    /// Parse the inner function arguments of `rect()`.
    648    fn parse_function_arguments<'i, 't>(
    649        context: &ParserContext,
    650        input: &mut Parser<'i, 't>,
    651    ) -> Result<Self, ParseError<'i>> {
    652        let rect = Rect::parse_all_components_with(context, input, LengthPercentageOrAuto::parse)?;
    653        let round = parse_round(context, input)?;
    654        Ok(ShapeRectFunction { rect, round })
    655    }
    656 }
    657 
    658 impl ToComputedValue for BasicShapeRect {
    659    type ComputedValue = ComputedInsetRect;
    660 
    661    #[inline]
    662    fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
    663        use crate::values::computed::LengthPercentage;
    664        use crate::values::computed::LengthPercentageOrAuto;
    665        use style_traits::values::specified::AllowedNumericType;
    666 
    667        match self {
    668            Self::Inset(ref inset) => inset.to_computed_value(context),
    669            Self::Xywh(ref xywh) => {
    670                // Given `xywh(x y w h)`, construct the equivalent inset() function,
    671                // `inset(y calc(100% - x - w) calc(100% - y - h) x)`.
    672                //
    673                // https://drafts.csswg.org/css-shapes-1/#basic-shape-computed-values
    674                // https://github.com/w3c/csswg-drafts/issues/9053
    675                let x = xywh.x.to_computed_value(context);
    676                let y = xywh.y.to_computed_value(context);
    677                let w = xywh.width.to_computed_value(context);
    678                let h = xywh.height.to_computed_value(context);
    679                // calc(100% - x - w).
    680                let right = LengthPercentage::hundred_percent_minus_list(
    681                    &[&x, &w.0],
    682                    AllowedNumericType::All,
    683                );
    684                // calc(100% - y - h).
    685                let bottom = LengthPercentage::hundred_percent_minus_list(
    686                    &[&y, &h.0],
    687                    AllowedNumericType::All,
    688                );
    689 
    690                ComputedInsetRect {
    691                    rect: Rect::new(y, right, bottom, x),
    692                    round: xywh.round.to_computed_value(context),
    693                }
    694            },
    695            Self::Rect(ref rect) => {
    696                // Given `rect(t r b l)`, the equivalent function is
    697                // `inset(t calc(100% - r) calc(100% - b) l)`.
    698                //
    699                // https://drafts.csswg.org/css-shapes-1/#basic-shape-computed-values
    700                fn compute_top_or_left(v: LengthPercentageOrAuto) -> LengthPercentage {
    701                    match v {
    702                        // it’s equivalent to 0% as the first (top) or fourth (left) value.
    703                        // https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-rect
    704                        LengthPercentageOrAuto::Auto => LengthPercentage::zero_percent(),
    705                        LengthPercentageOrAuto::LengthPercentage(lp) => lp,
    706                    }
    707                }
    708                fn compute_bottom_or_right(v: LengthPercentageOrAuto) -> LengthPercentage {
    709                    match v {
    710                        // It's equivalent to 100% as the second (right) or third (bottom) value.
    711                        // So calc(100% - 100%) = 0%.
    712                        // https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-rect
    713                        LengthPercentageOrAuto::Auto => LengthPercentage::zero_percent(),
    714                        LengthPercentageOrAuto::LengthPercentage(lp) => {
    715                            LengthPercentage::hundred_percent_minus(lp, AllowedNumericType::All)
    716                        },
    717                    }
    718                }
    719 
    720                let round = rect.round.to_computed_value(context);
    721                let rect = rect.rect.to_computed_value(context);
    722                let rect = Rect::new(
    723                    compute_top_or_left(rect.0),
    724                    compute_bottom_or_right(rect.1),
    725                    compute_bottom_or_right(rect.2),
    726                    compute_top_or_left(rect.3),
    727                );
    728 
    729                ComputedInsetRect { rect, round }
    730            },
    731        }
    732    }
    733 
    734    #[inline]
    735    fn from_computed_value(computed: &Self::ComputedValue) -> Self {
    736        Self::Inset(ToComputedValue::from_computed_value(computed))
    737    }
    738 }
    739 
    740 impl generic::Shape<Angle, Position, LengthPercentage> {
    741    /// Parse the inner arguments of a `shape` function.
    742    /// shape() = shape(<fill-rule>? from <coordinate-pair>, <shape-command>#)
    743    fn parse_function_arguments<'i, 't>(
    744        context: &ParserContext,
    745        input: &mut Parser<'i, 't>,
    746        shape_type: ShapeType,
    747    ) -> Result<Self, ParseError<'i>> {
    748        let fill = parse_fill_rule(input, shape_type, false /* no following comma */);
    749 
    750        let mut first = true;
    751        let commands = input.parse_comma_separated(|i| {
    752            if first {
    753                first = false;
    754 
    755                // The starting point for the first shape-command. It adds an initial absolute
    756                // moveto to the list of path data commands, with the <coordinate-pair> measured
    757                // from the top-left corner of the reference
    758                i.expect_ident_matching("from")?;
    759                Ok(ShapeCommand::Move {
    760                    point: generic::CommandEndPoint::parse_endpoint_as_abs(context, i)?,
    761                })
    762            } else {
    763                // The further path data commands.
    764                ShapeCommand::parse(context, i)
    765            }
    766        })?;
    767 
    768        // We must have one starting point and at least one following <shape-command>.
    769        if commands.len() < 2 {
    770            return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
    771        }
    772 
    773        Ok(Self {
    774            fill,
    775            commands: commands.into(),
    776        })
    777    }
    778 }
    779 
    780 impl Parse for ShapeCommand {
    781    fn parse<'i, 't>(
    782        context: &ParserContext,
    783        input: &mut Parser<'i, 't>,
    784    ) -> Result<Self, ParseError<'i>> {
    785        use crate::values::generics::basic_shape::{
    786            ArcRadii, ArcSize, ArcSweep, AxisEndPoint, CommandEndPoint, ControlPoint,
    787        };
    788 
    789        // <shape-command> = <move-command> | <line-command> | <hv-line-command> |
    790        //                   <curve-command> | <smooth-command> | <arc-command> | close
    791        Ok(try_match_ident_ignore_ascii_case! { input,
    792            "close" => Self::Close,
    793            "move" => {
    794                let point = CommandEndPoint::parse(context, input)?;
    795                Self::Move { point }
    796            },
    797            "line" => {
    798                let point = CommandEndPoint::parse(context, input)?;
    799                Self::Line { point }
    800            },
    801            "hline" => {
    802                let x = AxisEndPoint::parse_hline(context, input)?;
    803                Self::HLine { x }
    804            },
    805            "vline" => {
    806                let y = AxisEndPoint::parse_vline(context, input)?;
    807                Self::VLine { y }
    808            },
    809            "curve" => {
    810                let point = CommandEndPoint::parse(context, input)?;
    811                input.expect_ident_matching("with")?;
    812                let control1 = ControlPoint::parse(context, input, point.is_abs())?;
    813                if input.try_parse(|i| i.expect_delim('/')).is_ok() {
    814                    let control2 = ControlPoint::parse(context, input, point.is_abs())?;
    815                    Self::CubicCurve {
    816                        point,
    817                        control1,
    818                        control2,
    819                    }
    820                } else {
    821                    Self::QuadCurve {
    822                        point,
    823                        control1,
    824                    }
    825                }
    826            },
    827            "smooth" => {
    828                let point = CommandEndPoint::parse(context, input)?;
    829                if input.try_parse(|i| i.expect_ident_matching("with")).is_ok() {
    830                    let control2 = ControlPoint::parse(context, input, point.is_abs())?;
    831                    Self::SmoothCubic {
    832                        point,
    833                        control2,
    834                    }
    835                } else {
    836                    Self::SmoothQuad { point }
    837                }
    838            },
    839            "arc" => {
    840                let point = CommandEndPoint::parse(context, input)?;
    841                input.expect_ident_matching("of")?;
    842                let rx = LengthPercentage::parse(context, input)?;
    843                let ry = input.try_parse(|i| LengthPercentage::parse(context, i)).ok();
    844                let radii = ArcRadii { rx, ry: ry.into() };
    845 
    846                // [<arc-sweep> || <arc-size> || rotate <angle>]?
    847                let mut arc_sweep = None;
    848                let mut arc_size = None;
    849                let mut rotate = None;
    850                loop {
    851                    if arc_sweep.is_none() {
    852                        arc_sweep = input.try_parse(ArcSweep::parse).ok();
    853                    }
    854 
    855                    if arc_size.is_none() {
    856                        arc_size = input.try_parse(ArcSize::parse).ok();
    857                        if arc_size.is_some() {
    858                            continue;
    859                        }
    860                    }
    861 
    862                    if rotate.is_none()
    863                        && input
    864                            .try_parse(|i| i.expect_ident_matching("rotate"))
    865                            .is_ok()
    866                    {
    867                        rotate = Some(Angle::parse(context, input)?);
    868                        continue;
    869                    }
    870                    break;
    871                }
    872                Self::Arc {
    873                    point,
    874                    radii,
    875                    arc_sweep: arc_sweep.unwrap_or(ArcSweep::Ccw),
    876                    arc_size: arc_size.unwrap_or(ArcSize::Small),
    877                    rotate: rotate.unwrap_or(Angle::zero()),
    878                }
    879            },
    880        })
    881    }
    882 }
    883 
    884 impl Parse for generic::CoordinatePair<LengthPercentage> {
    885    fn parse<'i, 't>(
    886        context: &ParserContext,
    887        input: &mut Parser<'i, 't>,
    888    ) -> Result<Self, ParseError<'i>> {
    889        let x = LengthPercentage::parse(context, input)?;
    890        let y = LengthPercentage::parse(context, input)?;
    891        Ok(Self::new(x, y))
    892    }
    893 }
    894 
    895 impl generic::ControlPoint<Position, LengthPercentage> {
    896    /// Parse <control-point> = [ <position> | <relative-control-point> ]
    897    fn parse<'i, 't>(
    898        context: &ParserContext,
    899        input: &mut Parser<'i, 't>,
    900        is_end_point_abs: bool,
    901    ) -> Result<Self, ParseError<'i>> {
    902        use generic::ControlReference;
    903        let coord = input.try_parse(|i| generic::CoordinatePair::parse(context, i));
    904 
    905        // Parse <position>
    906        if is_end_point_abs && coord.is_err() {
    907            let pos = Position::parse(context, input)?;
    908            return Ok(Self::Absolute(pos));
    909        }
    910 
    911        // Parse <relative-control-point> = <coordinate-pair> [from [ start | end | origin ]]?
    912        let coord = coord?;
    913        let mut reference = if is_end_point_abs {
    914            ControlReference::Origin
    915        } else {
    916            ControlReference::Start
    917        };
    918        if input.try_parse(|i| i.expect_ident_matching("from")).is_ok() {
    919            reference = ControlReference::parse(input)?;
    920        }
    921 
    922        Ok(Self::Relative(generic::RelativeControlPoint {
    923            coord,
    924            reference,
    925        }))
    926    }
    927 }
    928 
    929 impl Parse for generic::CommandEndPoint<Position, LengthPercentage> {
    930    /// Parse <command-end-point> = to <position> | by <coordinate-pair>
    931    fn parse<'i, 't>(
    932        context: &ParserContext,
    933        input: &mut Parser<'i, 't>,
    934    ) -> Result<Self, ParseError<'i>> {
    935        if ByTo::parse(input)?.is_abs() {
    936            Self::parse_endpoint_as_abs(context, input)
    937        } else {
    938            let point = generic::CoordinatePair::parse(context, input)?;
    939            Ok(Self::ByCoordinate(point))
    940        }
    941    }
    942 }
    943 
    944 impl generic::CommandEndPoint<Position, LengthPercentage> {
    945    /// Parse <command-end-point> = to <position>
    946    fn parse_endpoint_as_abs<'i, 't>(
    947        context: &ParserContext,
    948        input: &mut Parser<'i, 't>,
    949    ) -> Result<Self, ParseError<'i>> {
    950        let point = Position::parse(context, input)?;
    951        Ok(generic::CommandEndPoint::ToPosition(point))
    952    }
    953 }
    954 
    955 impl generic::AxisEndPoint<LengthPercentage> {
    956    /// Parse <horizontal-line-command>
    957    pub fn parse_hline<'i, 't>(
    958        context: &ParserContext,
    959        input: &mut Parser<'i, 't>,
    960    ) -> Result<Self, ParseError<'i>> {
    961        use cssparser::Token;
    962        use generic::{AxisPosition, AxisPositionKeyword};
    963 
    964        // If the command is relative, parse for <length-percentage> only.
    965        if !ByTo::parse(input)?.is_abs() {
    966            return Ok(Self::ByCoordinate(LengthPercentage::parse(context, input)?));
    967        }
    968 
    969        let x = AxisPosition::parse(context, input)?;
    970        if let AxisPosition::Keyword(
    971            _word @ (AxisPositionKeyword::Top
    972            | AxisPositionKeyword::Bottom
    973            | AxisPositionKeyword::YStart
    974            | AxisPositionKeyword::YEnd),
    975        ) = &x
    976        {
    977            let location = input.current_source_location();
    978            let token = Token::Ident(x.to_css_string().into());
    979            return Err(location.new_unexpected_token_error(token));
    980        }
    981        Ok(Self::ToPosition(x))
    982    }
    983 
    984    /// Parse <vertical-line-command>
    985    pub fn parse_vline<'i, 't>(
    986        context: &ParserContext,
    987        input: &mut Parser<'i, 't>,
    988    ) -> Result<Self, ParseError<'i>> {
    989        use cssparser::Token;
    990        use generic::{AxisPosition, AxisPositionKeyword};
    991 
    992        // If the command is relative, parse for <length-percentage> only.
    993        if !ByTo::parse(input)?.is_abs() {
    994            return Ok(Self::ByCoordinate(LengthPercentage::parse(context, input)?));
    995        }
    996 
    997        let y = AxisPosition::parse(context, input)?;
    998        if let AxisPosition::Keyword(
    999            _word @ (AxisPositionKeyword::Left
   1000            | AxisPositionKeyword::Right
   1001            | AxisPositionKeyword::XStart
   1002            | AxisPositionKeyword::XEnd),
   1003        ) = &y
   1004        {
   1005            // Return an error if we parsed a different keyword.
   1006            let location = input.current_source_location();
   1007            let token = Token::Ident(y.to_css_string().into());
   1008            return Err(location.new_unexpected_token_error(token));
   1009        }
   1010        Ok(Self::ToPosition(y))
   1011    }
   1012 }
   1013 
   1014 impl ToComputedValue for generic::AxisPosition<LengthPercentage> {
   1015    type ComputedValue = generic::AxisPosition<ComputedLengthPercentage>;
   1016 
   1017    fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
   1018        match self {
   1019            Self::LengthPercent(lp) => {
   1020                Self::ComputedValue::LengthPercent(lp.to_computed_value(context))
   1021            },
   1022            Self::Keyword(word) => {
   1023                let lp = LengthPercentage::Percentage(word.as_percentage());
   1024                Self::ComputedValue::LengthPercent(lp.to_computed_value(context))
   1025            },
   1026        }
   1027    }
   1028 
   1029    fn from_computed_value(computed: &Self::ComputedValue) -> Self {
   1030        match computed {
   1031            Self::ComputedValue::LengthPercent(lp) => {
   1032                Self::LengthPercent(LengthPercentage::from_computed_value(lp))
   1033            },
   1034            _ => unreachable!("Invalid state: computed value cannot be a keyword."),
   1035        }
   1036    }
   1037 }
   1038 
   1039 impl ToComputedValue for generic::AxisPosition<CSSFloat> {
   1040    type ComputedValue = Self;
   1041 
   1042    fn to_computed_value(&self, _context: &Context) -> Self {
   1043        *self
   1044    }
   1045 
   1046    fn from_computed_value(computed: &Self) -> Self {
   1047        *computed
   1048    }
   1049 }
   1050 
   1051 /// This determines whether the command is absolutely or relatively positioned.
   1052 /// https://drafts.csswg.org/css-shapes-1/#typedef-shape-command-end-point
   1053 #[derive(Clone, Copy, Debug, Parse, PartialEq)]
   1054 enum ByTo {
   1055    /// Command is relative to the command’s starting point.
   1056    By,
   1057    /// Command is relative to the top-left corner of the reference box.
   1058    To,
   1059 }
   1060 
   1061 impl ByTo {
   1062    /// Return true if it is absolute, i.e. it is To.
   1063    #[inline]
   1064    pub fn is_abs(&self) -> bool {
   1065        matches!(self, ByTo::To)
   1066    }
   1067 }