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"
313                    if flags.contains(AllowedBasicShapes::XYWH)
314                        && static_prefs::pref!("layout.css.basic-shape-xywh.enabled") =>
315                {
316                    Xywh::parse_function_arguments(context, i)
317                        .map(BasicShapeRect::Xywh)
318                        .map(BasicShape::Rect)
319                },
320                "rect"
321                    if flags.contains(AllowedBasicShapes::RECT)
322                        && static_prefs::pref!("layout.css.basic-shape-rect.enabled") =>
323                {
324                    ShapeRectFunction::parse_function_arguments(context, i)
325                        .map(BasicShapeRect::Rect)
326                        .map(BasicShape::Rect)
327                },
328                "circle" if flags.contains(AllowedBasicShapes::CIRCLE) => {
329                    Circle::parse_function_arguments(context, i)
330                        .map(BasicShape::Circle)
331                },
332                "ellipse" if flags.contains(AllowedBasicShapes::ELLIPSE) => {
333                    Ellipse::parse_function_arguments(context, i)
334                        .map(BasicShape::Ellipse)
335                },
336                "polygon" if flags.contains(AllowedBasicShapes::POLYGON) => {
337                    Polygon::parse_function_arguments(context, i, shape_type)
338                        .map(BasicShape::Polygon)
339                },
340                "path" if flags.contains(AllowedBasicShapes::PATH) => {
341                    Path::parse_function_arguments(i, shape_type)
342                        .map(PathOrShapeFunction::Path)
343                        .map(BasicShape::PathOrShape)
344                },
345                "shape"
346                    if flags.contains(AllowedBasicShapes::SHAPE)
347                        && static_prefs::pref!("layout.css.basic-shape-shape.enabled") =>
348                {
349                    generic::Shape::parse_function_arguments(context, i, shape_type)
350                        .map(PathOrShapeFunction::Shape)
351                        .map(BasicShape::PathOrShape)
352                },
353                _ => Err(location
354                    .new_custom_error(StyleParseErrorKind::UnexpectedFunction(function.clone()))),
355            }
356        })
357    }
358}
359
360impl Parse for InsetRect {
361    fn parse<'i, 't>(
362        context: &ParserContext,
363        input: &mut Parser<'i, 't>,
364    ) -> Result<Self, ParseError<'i>> {
365        input.expect_function_matching("inset")?;
366        input.parse_nested_block(|i| Self::parse_function_arguments(context, i))
367    }
368}
369
370fn parse_round<'i, 't>(
371    context: &ParserContext,
372    input: &mut Parser<'i, 't>,
373) -> Result<BorderRadius, ParseError<'i>> {
374    if input
375        .try_parse(|i| i.expect_ident_matching("round"))
376        .is_ok()
377    {
378        return BorderRadius::parse(context, input);
379    }
380
381    Ok(BorderRadius::zero())
382}
383
384impl InsetRect {
385    /// Parse the inner function arguments of `inset()`
386    fn parse_function_arguments<'i, 't>(
387        context: &ParserContext,
388        input: &mut Parser<'i, 't>,
389    ) -> Result<Self, ParseError<'i>> {
390        let rect = Rect::parse_with(context, input, LengthPercentage::parse)?;
391        let round = parse_round(context, input)?;
392        Ok(generic::InsetRect { rect, round })
393    }
394}
395
396impl ToCss for ShapePosition {
397    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
398    where
399        W: Write,
400    {
401        self.horizontal.to_css(dest)?;
402        dest.write_char(' ')?;
403        self.vertical.to_css(dest)
404    }
405}
406
407fn parse_at_position<'i, 't>(
408    context: &ParserContext,
409    input: &mut Parser<'i, 't>,
410) -> Result<GenericPositionOrAuto<ShapePosition>, ParseError<'i>> {
411    use crate::values::specified::position::{Position, Side};
412    use crate::values::specified::{AllowedNumericType, Percentage, PositionComponent};
413
414    fn convert_to_length_percentage<S: Side>(c: PositionComponent<S>) -> LengthPercentage {
415        // Convert the value when parsing, to make sure we serialize it properly for both
416        // specified and computed values.
417        // https://drafts.csswg.org/css-shapes-1/#basic-shape-serialization
418        match c {
419            // Since <position> keywords stand in for percentages, keywords without an offset
420            // turn into percentages.
421            PositionComponent::Center => LengthPercentage::from(Percentage::new(0.5)),
422            PositionComponent::Side(keyword, None) => {
423                Percentage::new(if keyword.is_start() { 0. } else { 1. }).into()
424            },
425            // Per spec issue, https://github.com/w3c/csswg-drafts/issues/8695, the part of
426            // "avoiding calc() expressions where possible" and "avoiding calc()
427            // transformations" will be removed from the spec, and we should follow the
428            // css-values-4 for position, i.e. we make it as length-percentage always.
429            // https://drafts.csswg.org/css-shapes-1/#basic-shape-serialization.
430            // https://drafts.csswg.org/css-values-4/#typedef-position
431            PositionComponent::Side(keyword, Some(length)) => {
432                if keyword.is_start() {
433                    length
434                } else {
435                    length.hundred_percent_minus(AllowedNumericType::All)
436                }
437            },
438            PositionComponent::Length(length) => length,
439        }
440    }
441
442    if input.try_parse(|i| i.expect_ident_matching("at")).is_ok() {
443        Position::parse(context, input).map(|pos| {
444            GenericPositionOrAuto::Position(ShapePosition::new(
445                convert_to_length_percentage(pos.horizontal),
446                convert_to_length_percentage(pos.vertical),
447            ))
448        })
449    } else {
450        // `at <position>` is omitted.
451        Ok(GenericPositionOrAuto::Auto)
452    }
453}
454
455impl Parse for Circle {
456    fn parse<'i, 't>(
457        context: &ParserContext,
458        input: &mut Parser<'i, 't>,
459    ) -> Result<Self, ParseError<'i>> {
460        input.expect_function_matching("circle")?;
461        input.parse_nested_block(|i| Self::parse_function_arguments(context, i))
462    }
463}
464
465impl Circle {
466    fn parse_function_arguments<'i, 't>(
467        context: &ParserContext,
468        input: &mut Parser<'i, 't>,
469    ) -> Result<Self, ParseError<'i>> {
470        let radius = input
471            .try_parse(|i| ShapeRadius::parse(context, i))
472            .unwrap_or_default();
473        let position = parse_at_position(context, input)?;
474
475        Ok(generic::Circle { radius, position })
476    }
477}
478
479impl Parse for Ellipse {
480    fn parse<'i, 't>(
481        context: &ParserContext,
482        input: &mut Parser<'i, 't>,
483    ) -> Result<Self, ParseError<'i>> {
484        input.expect_function_matching("ellipse")?;
485        input.parse_nested_block(|i| Self::parse_function_arguments(context, i))
486    }
487}
488
489impl Ellipse {
490    fn parse_function_arguments<'i, 't>(
491        context: &ParserContext,
492        input: &mut Parser<'i, 't>,
493    ) -> Result<Self, ParseError<'i>> {
494        let (semiaxis_x, semiaxis_y) = input
495            .try_parse(|i| -> Result<_, ParseError> {
496                Ok((
497                    ShapeRadius::parse(context, i)?,
498                    ShapeRadius::parse(context, i)?,
499                ))
500            })
501            .unwrap_or_default();
502        let position = parse_at_position(context, input)?;
503
504        Ok(generic::Ellipse {
505            semiaxis_x,
506            semiaxis_y,
507            position,
508        })
509    }
510}
511
512fn parse_fill_rule<'i, 't>(
513    input: &mut Parser<'i, 't>,
514    shape_type: ShapeType,
515    expect_comma: bool,
516) -> FillRule {
517    match shape_type {
518        // Per [1] and [2], we ignore `<fill-rule>` for outline shapes, so always use a default
519        // value.
520        // [1] https://github.com/w3c/csswg-drafts/issues/3468
521        // [2] https://github.com/w3c/csswg-drafts/issues/7390
522        //
523        // Also, per [3] and [4], we would like the ignore `<file-rule>` from outline shapes, e.g.
524        // offset-path, which means we don't parse it when setting `ShapeType::Outline`.
525        // This should be web compatible because the shipped "offset-path:path()" doesn't have
526        // `<fill-rule>` and "offset-path:polygon()" is a new feature and still behind the
527        // preference.
528        // [3] https://github.com/w3c/fxtf-drafts/issues/512#issuecomment-1545393321
529        // [4] https://github.com/w3c/fxtf-drafts/issues/512#issuecomment-1555330929
530        ShapeType::Outline => Default::default(),
531        ShapeType::Filled => input
532            .try_parse(|i| -> Result<_, ParseError> {
533                let fill = FillRule::parse(i)?;
534                if expect_comma {
535                    i.expect_comma()?;
536                }
537                Ok(fill)
538            })
539            .unwrap_or_default(),
540    }
541}
542
543impl Parse for Polygon {
544    fn parse<'i, 't>(
545        context: &ParserContext,
546        input: &mut Parser<'i, 't>,
547    ) -> Result<Self, ParseError<'i>> {
548        input.expect_function_matching("polygon")?;
549        input.parse_nested_block(|i| Self::parse_function_arguments(context, i, ShapeType::Filled))
550    }
551}
552
553impl Polygon {
554    /// Parse the inner arguments of a `polygon` function.
555    fn parse_function_arguments<'i, 't>(
556        context: &ParserContext,
557        input: &mut Parser<'i, 't>,
558        shape_type: ShapeType,
559    ) -> Result<Self, ParseError<'i>> {
560        let fill = parse_fill_rule(input, shape_type, true /* has comma */);
561        let coordinates = input
562            .parse_comma_separated(|i| {
563                Ok(PolygonCoord(
564                    LengthPercentage::parse(context, i)?,
565                    LengthPercentage::parse(context, i)?,
566                ))
567            })?
568            .into();
569
570        Ok(Polygon { fill, coordinates })
571    }
572}
573
574impl Path {
575    /// Parse the inner arguments of a `path` function.
576    fn parse_function_arguments<'i, 't>(
577        input: &mut Parser<'i, 't>,
578        shape_type: ShapeType,
579    ) -> Result<Self, ParseError<'i>> {
580        use crate::values::specified::svg_path::AllowEmpty;
581
582        let fill = parse_fill_rule(input, shape_type, true /* has comma */);
583        let path = SVGPathData::parse(input, AllowEmpty::No)?;
584        Ok(Path { fill, path })
585    }
586}
587
588fn round_to_css<W>(round: &BorderRadius, dest: &mut CssWriter<W>) -> fmt::Result
589where
590    W: Write,
591{
592    if !round.is_zero() {
593        dest.write_str(" round ")?;
594        round.to_css(dest)?;
595    }
596    Ok(())
597}
598
599impl ToCss for Xywh {
600    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
601    where
602        W: Write,
603    {
604        self.x.to_css(dest)?;
605        dest.write_char(' ')?;
606        self.y.to_css(dest)?;
607        dest.write_char(' ')?;
608        self.width.to_css(dest)?;
609        dest.write_char(' ')?;
610        self.height.to_css(dest)?;
611        round_to_css(&self.round, dest)
612    }
613}
614
615impl Xywh {
616    /// Parse the inner function arguments of `xywh()`.
617    fn parse_function_arguments<'i, 't>(
618        context: &ParserContext,
619        input: &mut Parser<'i, 't>,
620    ) -> Result<Self, ParseError<'i>> {
621        let x = LengthPercentage::parse(context, input)?;
622        let y = LengthPercentage::parse(context, input)?;
623        let width = NonNegativeLengthPercentage::parse(context, input)?;
624        let height = NonNegativeLengthPercentage::parse(context, input)?;
625        let round = parse_round(context, input)?;
626        Ok(Xywh {
627            x,
628            y,
629            width,
630            height,
631            round,
632        })
633    }
634}
635
636impl ToCss for ShapeRectFunction {
637    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
638    where
639        W: Write,
640    {
641        self.rect.0.to_css(dest)?;
642        dest.write_char(' ')?;
643        self.rect.1.to_css(dest)?;
644        dest.write_char(' ')?;
645        self.rect.2.to_css(dest)?;
646        dest.write_char(' ')?;
647        self.rect.3.to_css(dest)?;
648        round_to_css(&self.round, dest)
649    }
650}
651
652impl ShapeRectFunction {
653    /// Parse the inner function arguments of `rect()`.
654    fn parse_function_arguments<'i, 't>(
655        context: &ParserContext,
656        input: &mut Parser<'i, 't>,
657    ) -> Result<Self, ParseError<'i>> {
658        let rect = Rect::parse_all_components_with(context, input, LengthPercentageOrAuto::parse)?;
659        let round = parse_round(context, input)?;
660        Ok(ShapeRectFunction { rect, round })
661    }
662}
663
664impl ToComputedValue for BasicShapeRect {
665    type ComputedValue = ComputedInsetRect;
666
667    #[inline]
668    fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
669        use crate::values::computed::LengthPercentage;
670        use crate::values::computed::LengthPercentageOrAuto;
671        use style_traits::values::specified::AllowedNumericType;
672
673        match self {
674            Self::Inset(ref inset) => inset.to_computed_value(context),
675            Self::Xywh(ref xywh) => {
676                // Given `xywh(x y w h)`, construct the equivalent inset() function,
677                // `inset(y calc(100% - x - w) calc(100% - y - h) x)`.
678                //
679                // https://drafts.csswg.org/css-shapes-1/#basic-shape-computed-values
680                // https://github.com/w3c/csswg-drafts/issues/9053
681                let x = xywh.x.to_computed_value(context);
682                let y = xywh.y.to_computed_value(context);
683                let w = xywh.width.to_computed_value(context);
684                let h = xywh.height.to_computed_value(context);
685                // calc(100% - x - w).
686                let right = LengthPercentage::hundred_percent_minus_list(
687                    &[&x, &w.0],
688                    AllowedNumericType::All,
689                );
690                // calc(100% - y - h).
691                let bottom = LengthPercentage::hundred_percent_minus_list(
692                    &[&y, &h.0],
693                    AllowedNumericType::All,
694                );
695
696                ComputedInsetRect {
697                    rect: Rect::new(y, right, bottom, x),
698                    round: xywh.round.to_computed_value(context),
699                }
700            },
701            Self::Rect(ref rect) => {
702                // Given `rect(t r b l)`, the equivalent function is
703                // `inset(t calc(100% - r) calc(100% - b) l)`.
704                //
705                // https://drafts.csswg.org/css-shapes-1/#basic-shape-computed-values
706                fn compute_top_or_left(v: LengthPercentageOrAuto) -> LengthPercentage {
707                    match v {
708                        // it’s equivalent to 0% as the first (top) or fourth (left) value.
709                        // https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-rect
710                        LengthPercentageOrAuto::Auto => LengthPercentage::zero_percent(),
711                        LengthPercentageOrAuto::LengthPercentage(lp) => lp,
712                    }
713                }
714                fn compute_bottom_or_right(v: LengthPercentageOrAuto) -> LengthPercentage {
715                    match v {
716                        // It's equivalent to 100% as the second (right) or third (bottom) value.
717                        // So calc(100% - 100%) = 0%.
718                        // https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-rect
719                        LengthPercentageOrAuto::Auto => LengthPercentage::zero_percent(),
720                        LengthPercentageOrAuto::LengthPercentage(lp) => {
721                            LengthPercentage::hundred_percent_minus(lp, AllowedNumericType::All)
722                        },
723                    }
724                }
725
726                let round = rect.round.to_computed_value(context);
727                let rect = rect.rect.to_computed_value(context);
728                let rect = Rect::new(
729                    compute_top_or_left(rect.0),
730                    compute_bottom_or_right(rect.1),
731                    compute_bottom_or_right(rect.2),
732                    compute_top_or_left(rect.3),
733                );
734
735                ComputedInsetRect { rect, round }
736            },
737        }
738    }
739
740    #[inline]
741    fn from_computed_value(computed: &Self::ComputedValue) -> Self {
742        Self::Inset(ToComputedValue::from_computed_value(computed))
743    }
744}
745
746impl generic::Shape<Angle, LengthPercentage> {
747    /// Parse the inner arguments of a `shape` function.
748    /// shape() = shape(<fill-rule>? from <coordinate-pair>, <shape-command>#)
749    fn parse_function_arguments<'i, 't>(
750        context: &ParserContext,
751        input: &mut Parser<'i, 't>,
752        shape_type: ShapeType,
753    ) -> Result<Self, ParseError<'i>> {
754        let fill = parse_fill_rule(input, shape_type, false /* no following comma */);
755
756        let mut first = true;
757        let commands = input.parse_comma_separated(|i| {
758            if first {
759                first = false;
760
761                // The starting point for the first shape-command. It adds an initial absolute
762                // moveto to the list of path data commands, with the <coordinate-pair> measured
763                // from the top-left corner of the reference
764                i.expect_ident_matching("from")?;
765                Ok(ShapeCommand::Move {
766                    by_to: generic::ByTo::To,
767                    point: generic::CoordinatePair::parse(context, i)?,
768                })
769            } else {
770                // The further path data commands.
771                ShapeCommand::parse(context, i)
772            }
773        })?;
774
775        // We must have one starting point and at least one following <shape-command>.
776        if commands.len() < 2 {
777            return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
778        }
779
780        Ok(Self {
781            fill,
782            commands: commands.into(),
783        })
784    }
785}
786
787impl Parse for ShapeCommand {
788    fn parse<'i, 't>(
789        context: &ParserContext,
790        input: &mut Parser<'i, 't>,
791    ) -> Result<Self, ParseError<'i>> {
792        use crate::values::generics::basic_shape::{ArcSize, ArcSweep, ByTo, CoordinatePair};
793
794        // <shape-command> = <move-command> | <line-command> | <hv-line-command> |
795        //                   <curve-command> | <smooth-command> | <arc-command> | close
796        Ok(try_match_ident_ignore_ascii_case! { input,
797            "close" => Self::Close,
798            "move" => {
799                let by_to = ByTo::parse(input)?;
800                let point = CoordinatePair::parse(context, input)?;
801                Self::Move { by_to, point }
802            },
803            "line" => {
804                let by_to = ByTo::parse(input)?;
805                let point = CoordinatePair::parse(context, input)?;
806                Self::Line { by_to, point }
807            },
808            "hline" => {
809                let by_to = ByTo::parse(input)?;
810                let x = LengthPercentage::parse(context, input)?;
811                Self::HLine { by_to, x }
812            },
813            "vline" => {
814                let by_to = ByTo::parse(input)?;
815                let y = LengthPercentage::parse(context, input)?;
816                Self::VLine { by_to, y }
817            },
818            "curve" => {
819                let by_to = ByTo::parse(input)?;
820                let point = CoordinatePair::parse(context, input)?;
821                input.expect_ident_matching("via")?;
822                let control1 = CoordinatePair::parse(context, input)?;
823                match input.try_parse(|i| CoordinatePair::parse(context, i)) {
824                    Ok(control2) => Self::CubicCurve {
825                        by_to,
826                        point,
827                        control1,
828                        control2,
829                    },
830                    Err(_) => Self::QuadCurve {
831                        by_to,
832                        point,
833                        control1,
834                    },
835                }
836            },
837            "smooth" => {
838                let by_to = ByTo::parse(input)?;
839                let point = CoordinatePair::parse(context, input)?;
840                if input.try_parse(|i| i.expect_ident_matching("via")).is_ok() {
841                    let control2 = CoordinatePair::parse(context, input)?;
842                    Self::SmoothCubic {
843                        by_to,
844                        point,
845                        control2,
846                    }
847                } else {
848                    Self::SmoothQuad { by_to, point }
849                }
850            },
851            "arc" => {
852                let by_to = ByTo::parse(input)?;
853                let point = CoordinatePair::parse(context, input)?;
854                input.expect_ident_matching("of")?;
855                let rx = LengthPercentage::parse(context, input)?;
856                let ry = input
857                    .try_parse(|i| LengthPercentage::parse(context, i))
858                    .unwrap_or(rx.clone());
859                let radii = CoordinatePair::new(rx, ry);
860
861                // [<arc-sweep> || <arc-size> || rotate <angle>]?
862                let mut arc_sweep = None;
863                let mut arc_size = None;
864                let mut rotate = None;
865                loop {
866                    if arc_sweep.is_none() {
867                        arc_sweep = input.try_parse(ArcSweep::parse).ok();
868                    }
869
870                    if arc_size.is_none() {
871                        arc_size = input.try_parse(ArcSize::parse).ok();
872                        if arc_size.is_some() {
873                            continue;
874                        }
875                    }
876
877                    if rotate.is_none()
878                        && input
879                            .try_parse(|i| i.expect_ident_matching("rotate"))
880                            .is_ok()
881                    {
882                        rotate = Some(Angle::parse(context, input)?);
883                        continue;
884                    }
885                    break;
886                }
887                Self::Arc {
888                    by_to,
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}