style/values/specified/
basic_shape.rs

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