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::{GenericPosition, 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::url::SpecifiedUrl;
22use crate::values::specified::{LengthPercentage, NonNegativeLengthPercentage, SVGPathData};
23use crate::Zero;
24use cssparser::Parser;
25use std::fmt::{self, Write};
26use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
27
28/// A specified alias for FillRule.
29pub use crate::values::generics::basic_shape::FillRule;
30
31/// A specified `clip-path` value.
32pub type ClipPath = generic::GenericClipPath<BasicShape, SpecifiedUrl>;
33
34/// A specified `shape-outside` value.
35pub type ShapeOutside = generic::GenericShapeOutside<BasicShape, Image>;
36
37/// A specified value for `at <position>` in circle() and ellipse().
38// Note: its computed value is the same as computed::position::Position. We just want to always use
39// LengthPercentage as the type of its components, for basic shapes.
40pub type ShapePosition = GenericPosition<LengthPercentage, LengthPercentage>;
41
42/// A specified basic shape.
43pub type BasicShape = generic::GenericBasicShape<
44    Angle,
45    ShapePosition,
46    LengthPercentage,
47    NonNegativeLengthPercentage,
48    BasicShapeRect,
49>;
50
51/// The specified value of `inset()`.
52pub type InsetRect = generic::GenericInsetRect<LengthPercentage, NonNegativeLengthPercentage>;
53
54/// A specified circle.
55pub type Circle = generic::Circle<ShapePosition, NonNegativeLengthPercentage>;
56
57/// A specified ellipse.
58pub type Ellipse = generic::Ellipse<ShapePosition, NonNegativeLengthPercentage>;
59
60/// The specified value of `ShapeRadius`.
61pub type ShapeRadius = generic::ShapeRadius<NonNegativeLengthPercentage>;
62
63/// The specified value of `Polygon`.
64pub type Polygon = generic::GenericPolygon<LengthPercentage>;
65
66/// The specified value of `PathOrShapeFunction`.
67pub type PathOrShapeFunction = generic::GenericPathOrShapeFunction<Angle, LengthPercentage>;
68
69/// The specified value of `ShapeCommand`.
70pub type ShapeCommand = generic::GenericShapeCommand<Angle, LengthPercentage>;
71
72/// The specified value of `xywh()`.
73/// Defines a rectangle via offsets from the top and left edge of the reference box, and a
74/// specified width and height.
75///
76/// The four <length-percentage>s define, respectively, the inset from the left edge of the
77/// reference box, the inset from the top edge of the reference box, the width of the rectangle,
78/// and the height of the rectangle.
79///
80/// https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-xywh
81#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToShmem)]
82pub struct Xywh {
83    /// The left edge of the reference box.
84    pub x: LengthPercentage,
85    /// The top edge of the reference box.
86    pub y: LengthPercentage,
87    /// The specified width.
88    pub width: NonNegativeLengthPercentage,
89    /// The specified height.
90    pub height: NonNegativeLengthPercentage,
91    /// The optional <border-radius> argument(s) define rounded corners for the inset rectangle
92    /// using the border-radius shorthand syntax.
93    pub round: BorderRadius,
94}
95
96/// Defines a rectangle via insets from the top and left edges of the reference box.
97///
98/// https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-rect
99#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToShmem)]
100#[repr(C)]
101pub struct ShapeRectFunction {
102    /// The four <length-percentage>s define the position of the top, right, bottom, and left edges
103    /// of a rectangle, respectively, as insets from the top edge of the reference box (for the
104    /// first and third values) or the left edge of the reference box (for the second and fourth
105    /// values).
106    ///
107    /// An auto value makes the edge of the box coincide with the corresponding edge of the
108    /// reference box: it’s equivalent to 0% as the first (top) or fourth (left) value, and
109    /// equivalent to 100% as the second (right) or third (bottom) value.
110    pub rect: Rect<LengthPercentageOrAuto>,
111    /// The optional <border-radius> argument(s) define rounded corners for the inset rectangle
112    /// using the border-radius shorthand syntax.
113    pub round: BorderRadius,
114}
115
116/// The specified value of <basic-shape-rect>.
117/// <basic-shape-rect> = <inset()> | <rect()> | <xywh()>
118///
119/// https://drafts.csswg.org/css-shapes-1/#supported-basic-shapes
120#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
121pub enum BasicShapeRect {
122    /// Defines an inset rectangle via insets from each edge of the reference box.
123    Inset(InsetRect),
124    /// Defines a xywh function.
125    #[css(function)]
126    Xywh(Xywh),
127    /// Defines a rect function.
128    #[css(function)]
129    Rect(ShapeRectFunction),
130}
131
132/// For filled shapes, we use fill-rule, and store it for path() and polygon().
133/// For outline shapes, we should ignore fill-rule.
134///
135/// https://github.com/w3c/fxtf-drafts/issues/512
136/// https://github.com/w3c/csswg-drafts/issues/7390
137/// https://github.com/w3c/csswg-drafts/issues/3468
138pub enum ShapeType {
139    /// The CSS property uses filled shapes. The default behavior.
140    Filled,
141    /// The CSS property uses outline shapes. This is especially useful for offset-path.
142    Outline,
143}
144
145bitflags! {
146    /// The flags to represent which basic shapes we would like to support.
147    ///
148    /// Different properties may use different subsets of <basic-shape>:
149    /// e.g.
150    /// clip-path: all basic shapes.
151    /// motion-path: all basic shapes (but ignore fill-rule).
152    /// shape-outside: inset(), circle(), ellipse(), polygon().
153    ///
154    /// Also there are some properties we don't support for now:
155    /// shape-inside: inset(), circle(), ellipse(), polygon().
156    /// SVG shape-inside and shape-subtract: circle(), ellipse(), polygon().
157    ///
158    /// The spec issue proposes some better ways to clarify the usage of basic shapes, so for now
159    /// we use the bitflags to choose the supported basic shapes for each property at the parse
160    /// time.
161    /// https://github.com/w3c/csswg-drafts/issues/7390
162    #[derive(Clone, Copy)]
163    #[repr(C)]
164    pub struct AllowedBasicShapes: u8 {
165        /// inset().
166        const INSET = 1 << 0;
167        /// xywh().
168        const XYWH = 1 << 1;
169        /// rect().
170        const RECT = 1 << 2;
171        /// circle().
172        const CIRCLE = 1 << 3;
173        /// ellipse().
174        const ELLIPSE = 1 << 4;
175        /// polygon().
176        const POLYGON = 1 << 5;
177        /// path().
178        const PATH = 1 << 6;
179        /// shape().
180        const SHAPE = 1 << 7;
181
182        /// All flags.
183        const ALL =
184            Self::INSET.bits() |
185            Self::XYWH.bits() |
186            Self::RECT.bits() |
187            Self::CIRCLE.bits() |
188            Self::ELLIPSE.bits() |
189            Self::POLYGON.bits() |
190            Self::PATH.bits() |
191            Self::SHAPE.bits();
192
193        /// For shape-outside.
194        const SHAPE_OUTSIDE =
195            Self::INSET.bits() |
196            Self::CIRCLE.bits() |
197            Self::ELLIPSE.bits() |
198            Self::POLYGON.bits();
199    }
200}
201
202/// A helper for both clip-path and shape-outside parsing of shapes.
203fn parse_shape_or_box<'i, 't, R, ReferenceBox>(
204    context: &ParserContext,
205    input: &mut Parser<'i, 't>,
206    to_shape: impl FnOnce(Box<BasicShape>, ReferenceBox) -> R,
207    to_reference_box: impl FnOnce(ReferenceBox) -> R,
208    flags: AllowedBasicShapes,
209) -> Result<R, ParseError<'i>>
210where
211    ReferenceBox: Default + Parse,
212{
213    let mut shape = None;
214    let mut ref_box = None;
215    loop {
216        if shape.is_none() {
217            shape = input
218                .try_parse(|i| BasicShape::parse(context, i, flags, ShapeType::Filled))
219                .ok();
220        }
221
222        if ref_box.is_none() {
223            ref_box = input.try_parse(|i| ReferenceBox::parse(context, i)).ok();
224            if ref_box.is_some() {
225                continue;
226            }
227        }
228        break;
229    }
230
231    if let Some(shp) = shape {
232        return Ok(to_shape(Box::new(shp), ref_box.unwrap_or_default()));
233    }
234
235    match ref_box {
236        Some(r) => Ok(to_reference_box(r)),
237        None => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)),
238    }
239}
240
241impl Parse for ClipPath {
242    #[inline]
243    fn parse<'i, 't>(
244        context: &ParserContext,
245        input: &mut Parser<'i, 't>,
246    ) -> Result<Self, ParseError<'i>> {
247        if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
248            return Ok(ClipPath::None);
249        }
250
251        if let Ok(url) = input.try_parse(|i| SpecifiedUrl::parse(context, i)) {
252            return Ok(ClipPath::Url(url));
253        }
254
255        parse_shape_or_box(
256            context,
257            input,
258            ClipPath::Shape,
259            ClipPath::Box,
260            AllowedBasicShapes::ALL,
261        )
262    }
263}
264
265impl Parse for ShapeOutside {
266    #[inline]
267    fn parse<'i, 't>(
268        context: &ParserContext,
269        input: &mut Parser<'i, 't>,
270    ) -> Result<Self, ParseError<'i>> {
271        // Need to parse this here so that `Image::parse_with_cors_anonymous`
272        // doesn't parse it.
273        if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
274            return Ok(ShapeOutside::None);
275        }
276
277        if let Ok(image) = input.try_parse(|i| Image::parse_with_cors_anonymous(context, i)) {
278            debug_assert_ne!(image, Image::None);
279            return Ok(ShapeOutside::Image(image));
280        }
281
282        parse_shape_or_box(
283            context,
284            input,
285            ShapeOutside::Shape,
286            ShapeOutside::Box,
287            AllowedBasicShapes::SHAPE_OUTSIDE,
288        )
289    }
290}
291
292impl BasicShape {
293    /// Parse with some parameters.
294    /// 1. The supported <basic-shape>.
295    /// 2. The type of shapes. Should we ignore fill-rule?
296    /// 3. The default value of `at <position>`.
297    pub fn parse<'i, 't>(
298        context: &ParserContext,
299        input: &mut Parser<'i, 't>,
300        flags: AllowedBasicShapes,
301        shape_type: ShapeType,
302    ) -> Result<Self, ParseError<'i>> {
303        let location = input.current_source_location();
304        let function = input.expect_function()?.clone();
305        input.parse_nested_block(move |i| {
306            match_ignore_ascii_case! { &function,
307                "inset" if flags.contains(AllowedBasicShapes::INSET) => {
308                    InsetRect::parse_function_arguments(context, i)
309                        .map(BasicShapeRect::Inset)
310                        .map(BasicShape::Rect)
311                },
312                "xywh" if flags.contains(AllowedBasicShapes::XYWH) => {
313                    Xywh::parse_function_arguments(context, i)
314                        .map(BasicShapeRect::Xywh)
315                        .map(BasicShape::Rect)
316                },
317                "rect" if flags.contains(AllowedBasicShapes::RECT) => {
318                    ShapeRectFunction::parse_function_arguments(context, i)
319                        .map(BasicShapeRect::Rect)
320                        .map(BasicShape::Rect)
321                },
322                "circle" if flags.contains(AllowedBasicShapes::CIRCLE) => {
323                    Circle::parse_function_arguments(context, i)
324                        .map(BasicShape::Circle)
325                },
326                "ellipse" if flags.contains(AllowedBasicShapes::ELLIPSE) => {
327                    Ellipse::parse_function_arguments(context, i)
328                        .map(BasicShape::Ellipse)
329                },
330                "polygon" if flags.contains(AllowedBasicShapes::POLYGON) => {
331                    Polygon::parse_function_arguments(context, i, shape_type)
332                        .map(BasicShape::Polygon)
333                },
334                "path" if flags.contains(AllowedBasicShapes::PATH) => {
335                    Path::parse_function_arguments(i, shape_type)
336                        .map(PathOrShapeFunction::Path)
337                        .map(BasicShape::PathOrShape)
338                },
339                "shape"
340                    if flags.contains(AllowedBasicShapes::SHAPE)
341                        && static_prefs::pref!("layout.css.basic-shape-shape.enabled") =>
342                {
343                    generic::Shape::parse_function_arguments(context, i, shape_type)
344                        .map(PathOrShapeFunction::Shape)
345                        .map(BasicShape::PathOrShape)
346                },
347                _ => Err(location
348                    .new_custom_error(StyleParseErrorKind::UnexpectedFunction(function.clone()))),
349            }
350        })
351    }
352}
353
354impl Parse for InsetRect {
355    fn parse<'i, 't>(
356        context: &ParserContext,
357        input: &mut Parser<'i, 't>,
358    ) -> Result<Self, ParseError<'i>> {
359        input.expect_function_matching("inset")?;
360        input.parse_nested_block(|i| Self::parse_function_arguments(context, i))
361    }
362}
363
364fn parse_round<'i, 't>(
365    context: &ParserContext,
366    input: &mut Parser<'i, 't>,
367) -> Result<BorderRadius, ParseError<'i>> {
368    if input
369        .try_parse(|i| i.expect_ident_matching("round"))
370        .is_ok()
371    {
372        return BorderRadius::parse(context, input);
373    }
374
375    Ok(BorderRadius::zero())
376}
377
378impl InsetRect {
379    /// Parse the inner function arguments of `inset()`
380    fn parse_function_arguments<'i, 't>(
381        context: &ParserContext,
382        input: &mut Parser<'i, 't>,
383    ) -> Result<Self, ParseError<'i>> {
384        let rect = Rect::parse_with(context, input, LengthPercentage::parse)?;
385        let round = parse_round(context, input)?;
386        Ok(generic::InsetRect { rect, round })
387    }
388}
389
390impl ToCss for ShapePosition {
391    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
392    where
393        W: Write,
394    {
395        self.horizontal.to_css(dest)?;
396        dest.write_char(' ')?;
397        self.vertical.to_css(dest)
398    }
399}
400
401fn parse_at_position<'i, 't>(
402    context: &ParserContext,
403    input: &mut Parser<'i, 't>,
404) -> Result<GenericPositionOrAuto<ShapePosition>, ParseError<'i>> {
405    use crate::values::specified::position::{Position, Side};
406    use crate::values::specified::{AllowedNumericType, Percentage, PositionComponent};
407
408    fn convert_to_length_percentage<S: Side>(c: PositionComponent<S>) -> LengthPercentage {
409        // Convert the value when parsing, to make sure we serialize it properly for both
410        // specified and computed values.
411        // https://drafts.csswg.org/css-shapes-1/#basic-shape-serialization
412        match c {
413            // Since <position> keywords stand in for percentages, keywords without an offset
414            // turn into percentages.
415            PositionComponent::Center => LengthPercentage::from(Percentage::new(0.5)),
416            PositionComponent::Side(keyword, None) => {
417                Percentage::new(if keyword.is_start() { 0. } else { 1. }).into()
418            },
419            // Per spec issue, https://github.com/w3c/csswg-drafts/issues/8695, the part of
420            // "avoiding calc() expressions where possible" and "avoiding calc()
421            // transformations" will be removed from the spec, and we should follow the
422            // css-values-4 for position, i.e. we make it as length-percentage always.
423            // https://drafts.csswg.org/css-shapes-1/#basic-shape-serialization.
424            // https://drafts.csswg.org/css-values-4/#typedef-position
425            PositionComponent::Side(keyword, Some(length)) => {
426                if keyword.is_start() {
427                    length
428                } else {
429                    length.hundred_percent_minus(AllowedNumericType::All)
430                }
431            },
432            PositionComponent::Length(length) => length,
433        }
434    }
435
436    if input.try_parse(|i| i.expect_ident_matching("at")).is_ok() {
437        Position::parse(context, input).map(|pos| {
438            GenericPositionOrAuto::Position(ShapePosition::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
449impl 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
459impl 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
473impl 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
483impl 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
506fn 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
537impl 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
547impl 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
568impl 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
582fn round_to_css<W>(round: &BorderRadius, dest: &mut CssWriter<W>) -> fmt::Result
583where
584    W: Write,
585{
586    if !round.is_zero() {
587        dest.write_str(" round ")?;
588        round.to_css(dest)?;
589    }
590    Ok(())
591}
592
593impl 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
609impl 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
630impl 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
646impl 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
658impl 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
740impl generic::Shape<Angle, 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                    by_to: generic::ByTo::To,
761                    point: generic::CoordinatePair::parse(context, i)?,
762                })
763            } else {
764                // The further path data commands.
765                ShapeCommand::parse(context, i)
766            }
767        })?;
768
769        // We must have one starting point and at least one following <shape-command>.
770        if commands.len() < 2 {
771            return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
772        }
773
774        Ok(Self {
775            fill,
776            commands: commands.into(),
777        })
778    }
779}
780
781impl Parse for ShapeCommand {
782    fn parse<'i, 't>(
783        context: &ParserContext,
784        input: &mut Parser<'i, 't>,
785    ) -> Result<Self, ParseError<'i>> {
786        use crate::values::generics::basic_shape::{ArcSize, ArcSweep, ByTo, CoordinatePair};
787
788        // <shape-command> = <move-command> | <line-command> | <hv-line-command> |
789        //                   <curve-command> | <smooth-command> | <arc-command> | close
790        Ok(try_match_ident_ignore_ascii_case! { input,
791            "close" => Self::Close,
792            "move" => {
793                let by_to = ByTo::parse(input)?;
794                let point = CoordinatePair::parse(context, input)?;
795                Self::Move { by_to, point }
796            },
797            "line" => {
798                let by_to = ByTo::parse(input)?;
799                let point = CoordinatePair::parse(context, input)?;
800                Self::Line { by_to, point }
801            },
802            "hline" => {
803                let by_to = ByTo::parse(input)?;
804                let x = LengthPercentage::parse(context, input)?;
805                Self::HLine { by_to, x }
806            },
807            "vline" => {
808                let by_to = ByTo::parse(input)?;
809                let y = LengthPercentage::parse(context, input)?;
810                Self::VLine { by_to, y }
811            },
812            "curve" => {
813                let by_to = ByTo::parse(input)?;
814                let point = CoordinatePair::parse(context, input)?;
815                input.expect_ident_matching("via")?;
816                let control1 = CoordinatePair::parse(context, input)?;
817                match input.try_parse(|i| CoordinatePair::parse(context, i)) {
818                    Ok(control2) => Self::CubicCurve {
819                        by_to,
820                        point,
821                        control1,
822                        control2,
823                    },
824                    Err(_) => Self::QuadCurve {
825                        by_to,
826                        point,
827                        control1,
828                    },
829                }
830            },
831            "smooth" => {
832                let by_to = ByTo::parse(input)?;
833                let point = CoordinatePair::parse(context, input)?;
834                if input.try_parse(|i| i.expect_ident_matching("via")).is_ok() {
835                    let control2 = CoordinatePair::parse(context, input)?;
836                    Self::SmoothCubic {
837                        by_to,
838                        point,
839                        control2,
840                    }
841                } else {
842                    Self::SmoothQuad { by_to, point }
843                }
844            },
845            "arc" => {
846                let by_to = ByTo::parse(input)?;
847                let point = CoordinatePair::parse(context, input)?;
848                input.expect_ident_matching("of")?;
849                let rx = LengthPercentage::parse(context, input)?;
850                let ry = input
851                    .try_parse(|i| LengthPercentage::parse(context, i))
852                    .unwrap_or(rx.clone());
853                let radii = CoordinatePair::new(rx, ry);
854
855                // [<arc-sweep> || <arc-size> || rotate <angle>]?
856                let mut arc_sweep = None;
857                let mut arc_size = None;
858                let mut rotate = None;
859                loop {
860                    if arc_sweep.is_none() {
861                        arc_sweep = input.try_parse(ArcSweep::parse).ok();
862                    }
863
864                    if arc_size.is_none() {
865                        arc_size = input.try_parse(ArcSize::parse).ok();
866                        if arc_size.is_some() {
867                            continue;
868                        }
869                    }
870
871                    if rotate.is_none()
872                        && input
873                            .try_parse(|i| i.expect_ident_matching("rotate"))
874                            .is_ok()
875                    {
876                        rotate = Some(Angle::parse(context, input)?);
877                        continue;
878                    }
879                    break;
880                }
881                Self::Arc {
882                    by_to,
883                    point,
884                    radii,
885                    arc_sweep: arc_sweep.unwrap_or(ArcSweep::Ccw),
886                    arc_size: arc_size.unwrap_or(ArcSize::Small),
887                    rotate: rotate.unwrap_or(Angle::zero()),
888                }
889            },
890        })
891    }
892}
893
894impl Parse for generic::CoordinatePair<LengthPercentage> {
895    fn parse<'i, 't>(
896        context: &ParserContext,
897        input: &mut Parser<'i, 't>,
898    ) -> Result<Self, ParseError<'i>> {
899        let x = LengthPercentage::parse(context, input)?;
900        let y = LengthPercentage::parse(context, input)?;
901        Ok(Self::new(x, y))
902    }
903}