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::derives::*;
11use crate::parser::{Parse, ParserContext};
12use crate::values::computed::basic_shape::InsetRect as ComputedInsetRect;
13use crate::values::computed::{
14    Context, LengthPercentage as ComputedLengthPercentage, ToComputedValue,
15};
16use crate::values::generics::basic_shape as generic;
17use crate::values::generics::basic_shape::{Path, PolygonCoord};
18use crate::values::generics::position::GenericPositionOrAuto;
19use crate::values::generics::rect::Rect;
20use crate::values::specified::angle::Angle;
21use crate::values::specified::border::BorderRadius;
22use crate::values::specified::image::Image;
23use crate::values::specified::length::LengthPercentageOrAuto;
24use crate::values::specified::position::{Position, Side};
25use crate::values::specified::url::SpecifiedUrl;
26use crate::values::specified::PositionComponent;
27use crate::values::specified::{LengthPercentage, NonNegativeLengthPercentage, SVGPathData};
28use crate::values::CSSFloat;
29use crate::Zero;
30use cssparser::{match_ignore_ascii_case, Parser};
31use std::fmt::{self, Write};
32use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
33
34/// A specified alias for FillRule.
35pub use crate::values::generics::basic_shape::FillRule;
36
37/// A specified `clip-path` value.
38pub type ClipPath = generic::GenericClipPath<BasicShape, SpecifiedUrl>;
39
40/// A specified `shape-outside` value.
41pub type ShapeOutside = generic::GenericShapeOutside<BasicShape, Image>;
42
43/// A specified value for `at <position>` in circle() and ellipse().
44// Note: its computed value is the same as computed::position::Position. We just want to always use
45// LengthPercentage as the type of its components, for basic shapes.
46pub type RadialPosition = generic::ShapePosition<LengthPercentage>;
47
48/// A specified basic shape.
49pub type BasicShape = generic::GenericBasicShape<Angle, Position, LengthPercentage, BasicShapeRect>;
50
51/// The specified value of `inset()`.
52pub type InsetRect = generic::GenericInsetRect<LengthPercentage>;
53
54/// A specified circle.
55pub type Circle = generic::Circle<LengthPercentage>;
56
57/// A specified ellipse.
58pub type Ellipse = generic::Ellipse<LengthPercentage>;
59
60/// The specified value of `ShapeRadius`.
61pub type ShapeRadius = generic::ShapeRadius<LengthPercentage>;
62
63/// The specified value of `Polygon`.
64pub type Polygon = generic::GenericPolygon<LengthPercentage>;
65
66/// The specified value of `PathOrShapeFunction`.
67pub type PathOrShapeFunction =
68    generic::GenericPathOrShapeFunction<Angle, Position, LengthPercentage>;
69
70/// The specified value of `ShapeCommand`.
71pub type ShapeCommand = generic::GenericShapeCommand<Angle, Position, LengthPercentage>;
72
73/// The specified value of `xywh()`.
74/// Defines a rectangle via offsets from the top and left edge of the reference box, and a
75/// specified width and height.
76///
77/// The four <length-percentage>s define, respectively, the inset from the left edge of the
78/// reference box, the inset from the top edge of the reference box, the width of the rectangle,
79/// and the height of the rectangle.
80///
81/// https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-xywh
82#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToShmem)]
83pub struct Xywh {
84    /// The left edge of the reference box.
85    pub x: LengthPercentage,
86    /// The top edge of the reference box.
87    pub y: LengthPercentage,
88    /// The specified width.
89    pub width: NonNegativeLengthPercentage,
90    /// The specified height.
91    pub height: NonNegativeLengthPercentage,
92    /// The optional <border-radius> argument(s) define rounded corners for the inset rectangle
93    /// using the border-radius shorthand syntax.
94    pub round: BorderRadius,
95}
96
97/// Defines a rectangle via insets from the top and left edges of the reference box.
98///
99/// https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-rect
100#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToShmem)]
101#[repr(C)]
102pub struct ShapeRectFunction {
103    /// The four <length-percentage>s define the position of the top, right, bottom, and left edges
104    /// of a rectangle, respectively, as insets from the top edge of the reference box (for the
105    /// first and third values) or the left edge of the reference box (for the second and fourth
106    /// values).
107    ///
108    /// An auto value makes the edge of the box coincide with the corresponding edge of the
109    /// reference box: it’s equivalent to 0% as the first (top) or fourth (left) value, and
110    /// equivalent to 100% as the second (right) or third (bottom) value.
111    pub rect: Rect<LengthPercentageOrAuto>,
112    /// The optional <border-radius> argument(s) define rounded corners for the inset rectangle
113    /// using the border-radius shorthand syntax.
114    pub round: BorderRadius,
115}
116
117/// The specified value of <basic-shape-rect>.
118/// <basic-shape-rect> = <inset()> | <rect()> | <xywh()>
119///
120/// https://drafts.csswg.org/css-shapes-1/#supported-basic-shapes
121#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
122pub enum BasicShapeRect {
123    /// Defines an inset rectangle via insets from each edge of the reference box.
124    Inset(InsetRect),
125    /// Defines a xywh function.
126    #[css(function)]
127    Xywh(Xywh),
128    /// Defines a rect function.
129    #[css(function)]
130    Rect(ShapeRectFunction),
131}
132
133/// For filled shapes, we use fill-rule, and store it for path() and polygon().
134/// For outline shapes, we should ignore fill-rule.
135///
136/// https://github.com/w3c/fxtf-drafts/issues/512
137/// https://github.com/w3c/csswg-drafts/issues/7390
138/// https://github.com/w3c/csswg-drafts/issues/3468
139pub enum ShapeType {
140    /// The CSS property uses filled shapes. The default behavior.
141    Filled,
142    /// The CSS property uses outline shapes. This is especially useful for offset-path.
143    Outline,
144}
145
146bitflags! {
147    /// The flags to represent which basic shapes we would like to support.
148    ///
149    /// Different properties may use different subsets of <basic-shape>:
150    /// e.g.
151    /// clip-path: all basic shapes.
152    /// motion-path: all basic shapes (but ignore fill-rule).
153    /// shape-outside: inset(), circle(), ellipse(), polygon().
154    ///
155    /// Also there are some properties we don't support for now:
156    /// shape-inside: inset(), circle(), ellipse(), polygon().
157    /// SVG shape-inside and shape-subtract: circle(), ellipse(), polygon().
158    ///
159    /// The spec issue proposes some better ways to clarify the usage of basic shapes, so for now
160    /// we use the bitflags to choose the supported basic shapes for each property at the parse
161    /// time.
162    /// https://github.com/w3c/csswg-drafts/issues/7390
163    #[derive(Clone, Copy)]
164    #[repr(C)]
165    pub struct AllowedBasicShapes: u8 {
166        /// inset().
167        const INSET = 1 << 0;
168        /// xywh().
169        const XYWH = 1 << 1;
170        /// rect().
171        const RECT = 1 << 2;
172        /// circle().
173        const CIRCLE = 1 << 3;
174        /// ellipse().
175        const ELLIPSE = 1 << 4;
176        /// polygon().
177        const POLYGON = 1 << 5;
178        /// path().
179        const PATH = 1 << 6;
180        /// shape().
181        const SHAPE = 1 << 7;
182
183        /// All flags.
184        const ALL =
185            Self::INSET.bits() |
186            Self::XYWH.bits() |
187            Self::RECT.bits() |
188            Self::CIRCLE.bits() |
189            Self::ELLIPSE.bits() |
190            Self::POLYGON.bits() |
191            Self::PATH.bits() |
192            Self::SHAPE.bits();
193
194        /// For shape-outside.
195        const SHAPE_OUTSIDE =
196            Self::INSET.bits() |
197            Self::CIRCLE.bits() |
198            Self::ELLIPSE.bits() |
199            Self::POLYGON.bits();
200    }
201}
202
203/// A helper for both clip-path and shape-outside parsing of shapes.
204fn parse_shape_or_box<'i, 't, R, ReferenceBox>(
205    context: &ParserContext,
206    input: &mut Parser<'i, 't>,
207    to_shape: impl FnOnce(Box<BasicShape>, ReferenceBox) -> R,
208    to_reference_box: impl FnOnce(ReferenceBox) -> R,
209    flags: AllowedBasicShapes,
210) -> Result<R, ParseError<'i>>
211where
212    ReferenceBox: Default + Parse,
213{
214    let mut shape = None;
215    let mut ref_box = None;
216    loop {
217        if shape.is_none() {
218            shape = input
219                .try_parse(|i| BasicShape::parse(context, i, flags, ShapeType::Filled))
220                .ok();
221        }
222
223        if ref_box.is_none() {
224            ref_box = input.try_parse(|i| ReferenceBox::parse(context, i)).ok();
225            if ref_box.is_some() {
226                continue;
227            }
228        }
229        break;
230    }
231
232    if let Some(shp) = shape {
233        return Ok(to_shape(Box::new(shp), ref_box.unwrap_or_default()));
234    }
235
236    match ref_box {
237        Some(r) => Ok(to_reference_box(r)),
238        None => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)),
239    }
240}
241
242impl Parse for ClipPath {
243    #[inline]
244    fn parse<'i, 't>(
245        context: &ParserContext,
246        input: &mut Parser<'i, 't>,
247    ) -> Result<Self, ParseError<'i>> {
248        if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
249            return Ok(ClipPath::None);
250        }
251
252        if let Ok(url) = input.try_parse(|i| SpecifiedUrl::parse(context, i)) {
253            return Ok(ClipPath::Url(url));
254        }
255
256        parse_shape_or_box(
257            context,
258            input,
259            ClipPath::Shape,
260            ClipPath::Box,
261            AllowedBasicShapes::ALL,
262        )
263    }
264}
265
266impl Parse for ShapeOutside {
267    #[inline]
268    fn parse<'i, 't>(
269        context: &ParserContext,
270        input: &mut Parser<'i, 't>,
271    ) -> Result<Self, ParseError<'i>> {
272        // Need to parse this here so that `Image::parse_with_cors_anonymous`
273        // doesn't parse it.
274        if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
275            return Ok(ShapeOutside::None);
276        }
277
278        if let Ok(image) = input.try_parse(|i| Image::parse_with_cors_anonymous(context, i)) {
279            debug_assert_ne!(image, Image::None);
280            return Ok(ShapeOutside::Image(image));
281        }
282
283        parse_shape_or_box(
284            context,
285            input,
286            ShapeOutside::Shape,
287            ShapeOutside::Box,
288            AllowedBasicShapes::SHAPE_OUTSIDE,
289        )
290    }
291}
292
293impl BasicShape {
294    /// Parse with some parameters.
295    /// 1. The supported <basic-shape>.
296    /// 2. The type of shapes. Should we ignore fill-rule?
297    /// 3. The default value of `at <position>`.
298    pub fn parse<'i, 't>(
299        context: &ParserContext,
300        input: &mut Parser<'i, 't>,
301        flags: AllowedBasicShapes,
302        shape_type: ShapeType,
303    ) -> Result<Self, ParseError<'i>> {
304        let location = input.current_source_location();
305        let function = input.expect_function()?.clone();
306        input.parse_nested_block(move |i| {
307            match_ignore_ascii_case! { &function,
308                "inset" if flags.contains(AllowedBasicShapes::INSET) => {
309                    InsetRect::parse_function_arguments(context, i)
310                        .map(BasicShapeRect::Inset)
311                        .map(BasicShape::Rect)
312                },
313                "xywh" if flags.contains(AllowedBasicShapes::XYWH) => {
314                    Xywh::parse_function_arguments(context, i)
315                        .map(BasicShapeRect::Xywh)
316                        .map(BasicShape::Rect)
317                },
318                "rect" if flags.contains(AllowedBasicShapes::RECT) => {
319                    ShapeRectFunction::parse_function_arguments(context, i)
320                        .map(BasicShapeRect::Rect)
321                        .map(BasicShape::Rect)
322                },
323                "circle" if flags.contains(AllowedBasicShapes::CIRCLE) => {
324                    Circle::parse_function_arguments(context, i)
325                        .map(BasicShape::Circle)
326                },
327                "ellipse" if flags.contains(AllowedBasicShapes::ELLIPSE) => {
328                    Ellipse::parse_function_arguments(context, i)
329                        .map(BasicShape::Ellipse)
330                },
331                "polygon" if flags.contains(AllowedBasicShapes::POLYGON) => {
332                    Polygon::parse_function_arguments(context, i, shape_type)
333                        .map(BasicShape::Polygon)
334                },
335                "path" if flags.contains(AllowedBasicShapes::PATH) => {
336                    Path::parse_function_arguments(i, shape_type)
337                        .map(PathOrShapeFunction::Path)
338                        .map(BasicShape::PathOrShape)
339                },
340                "shape"
341                    if flags.contains(AllowedBasicShapes::SHAPE)
342                        && static_prefs::pref!("layout.css.basic-shape-shape.enabled") =>
343                {
344                    generic::Shape::parse_function_arguments(context, i, shape_type)
345                        .map(PathOrShapeFunction::Shape)
346                        .map(BasicShape::PathOrShape)
347                },
348                _ => Err(location
349                    .new_custom_error(StyleParseErrorKind::UnexpectedFunction(function.clone()))),
350            }
351        })
352    }
353}
354
355impl Parse for InsetRect {
356    fn parse<'i, 't>(
357        context: &ParserContext,
358        input: &mut Parser<'i, 't>,
359    ) -> Result<Self, ParseError<'i>> {
360        input.expect_function_matching("inset")?;
361        input.parse_nested_block(|i| Self::parse_function_arguments(context, i))
362    }
363}
364
365fn parse_round<'i, 't>(
366    context: &ParserContext,
367    input: &mut Parser<'i, 't>,
368) -> Result<BorderRadius, ParseError<'i>> {
369    if input
370        .try_parse(|i| i.expect_ident_matching("round"))
371        .is_ok()
372    {
373        return BorderRadius::parse(context, input);
374    }
375
376    Ok(BorderRadius::zero())
377}
378
379impl InsetRect {
380    /// Parse the inner function arguments of `inset()`
381    fn parse_function_arguments<'i, 't>(
382        context: &ParserContext,
383        input: &mut Parser<'i, 't>,
384    ) -> Result<Self, ParseError<'i>> {
385        let rect = Rect::parse_with(context, input, LengthPercentage::parse)?;
386        let round = parse_round(context, input)?;
387        Ok(generic::InsetRect { rect, round })
388    }
389}
390
391impl ToCss for RadialPosition {
392    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
393    where
394        W: Write,
395    {
396        self.horizontal.to_css(dest)?;
397        dest.write_char(' ')?;
398        self.vertical.to_css(dest)
399    }
400}
401
402fn convert_to_length_percentage<S: Side>(c: PositionComponent<S>) -> LengthPercentage {
403    use crate::values::specified::{AllowedNumericType, Percentage};
404    // Convert the value when parsing, to make sure we serialize it properly for both
405    // specified and computed values.
406    // https://drafts.csswg.org/css-shapes-1/#basic-shape-serialization
407    match c {
408        // Since <position> keywords stand in for percentages, keywords without an offset
409        // turn into percentages.
410        PositionComponent::Center => LengthPercentage::from(Percentage::new(0.5)),
411        PositionComponent::Side(keyword, None) => {
412            Percentage::new(if keyword.is_start() { 0. } else { 1. }).into()
413        },
414        // Per spec issue, https://github.com/w3c/csswg-drafts/issues/8695, the part of
415        // "avoiding calc() expressions where possible" and "avoiding calc()
416        // transformations" will be removed from the spec, and we should follow the
417        // css-values-4 for position, i.e. we make it as length-percentage always.
418        // https://drafts.csswg.org/css-shapes-1/#basic-shape-serialization.
419        // https://drafts.csswg.org/css-values-4/#typedef-position
420        PositionComponent::Side(keyword, Some(length)) => {
421            if keyword.is_start() {
422                length
423            } else {
424                length.hundred_percent_minus(AllowedNumericType::All)
425            }
426        },
427        PositionComponent::Length(length) => length,
428    }
429}
430
431fn parse_at_position<'i, 't>(
432    context: &ParserContext,
433    input: &mut Parser<'i, 't>,
434) -> Result<GenericPositionOrAuto<RadialPosition>, ParseError<'i>> {
435    use crate::values::specified::position::Position;
436    if input.try_parse(|i| i.expect_ident_matching("at")).is_ok() {
437        Position::parse(context, input).map(|pos| {
438            GenericPositionOrAuto::Position(RadialPosition::new(
439                convert_to_length_percentage(pos.horizontal),
440                convert_to_length_percentage(pos.vertical),
441            ))
442        })
443    } else {
444        // `at <position>` is omitted.
445        Ok(GenericPositionOrAuto::Auto)
446    }
447}
448
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, Position, LengthPercentage> {
741    /// Parse the inner arguments of a `shape` function.
742    /// shape() = shape(<fill-rule>? from <coordinate-pair>, <shape-command>#)
743    fn parse_function_arguments<'i, 't>(
744        context: &ParserContext,
745        input: &mut Parser<'i, 't>,
746        shape_type: ShapeType,
747    ) -> Result<Self, ParseError<'i>> {
748        let fill = parse_fill_rule(input, shape_type, false /* no following comma */);
749
750        let mut first = true;
751        let commands = input.parse_comma_separated(|i| {
752            if first {
753                first = false;
754
755                // The starting point for the first shape-command. It adds an initial absolute
756                // moveto to the list of path data commands, with the <coordinate-pair> measured
757                // from the top-left corner of the reference
758                i.expect_ident_matching("from")?;
759                Ok(ShapeCommand::Move {
760                    point: generic::CommandEndPoint::parse_endpoint_as_abs(context, i)?,
761                })
762            } else {
763                // The further path data commands.
764                ShapeCommand::parse(context, i)
765            }
766        })?;
767
768        // We must have one starting point and at least one following <shape-command>.
769        if commands.len() < 2 {
770            return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
771        }
772
773        Ok(Self {
774            fill,
775            commands: commands.into(),
776        })
777    }
778}
779
780impl Parse for ShapeCommand {
781    fn parse<'i, 't>(
782        context: &ParserContext,
783        input: &mut Parser<'i, 't>,
784    ) -> Result<Self, ParseError<'i>> {
785        use crate::values::generics::basic_shape::{
786            ArcRadii, ArcSize, ArcSweep, AxisEndPoint, CommandEndPoint, ControlPoint,
787        };
788
789        // <shape-command> = <move-command> | <line-command> | <hv-line-command> |
790        //                   <curve-command> | <smooth-command> | <arc-command> | close
791        Ok(try_match_ident_ignore_ascii_case! { input,
792            "close" => Self::Close,
793            "move" => {
794                let point = CommandEndPoint::parse(context, input)?;
795                Self::Move { point }
796            },
797            "line" => {
798                let point = CommandEndPoint::parse(context, input)?;
799                Self::Line { point }
800            },
801            "hline" => {
802                let x = AxisEndPoint::parse_hline(context, input)?;
803                Self::HLine { x }
804            },
805            "vline" => {
806                let y = AxisEndPoint::parse_vline(context, input)?;
807                Self::VLine { y }
808            },
809            "curve" => {
810                let point = CommandEndPoint::parse(context, input)?;
811                input.expect_ident_matching("with")?;
812                let control1 = ControlPoint::parse(context, input, point.is_abs())?;
813                if input.try_parse(|i| i.expect_delim('/')).is_ok() {
814                    let control2 = ControlPoint::parse(context, input, point.is_abs())?;
815                    Self::CubicCurve {
816                        point,
817                        control1,
818                        control2,
819                    }
820                } else {
821                    Self::QuadCurve {
822                        point,
823                        control1,
824                    }
825                }
826            },
827            "smooth" => {
828                let point = CommandEndPoint::parse(context, input)?;
829                if input.try_parse(|i| i.expect_ident_matching("with")).is_ok() {
830                    let control2 = ControlPoint::parse(context, input, point.is_abs())?;
831                    Self::SmoothCubic {
832                        point,
833                        control2,
834                    }
835                } else {
836                    Self::SmoothQuad { point }
837                }
838            },
839            "arc" => {
840                let point = CommandEndPoint::parse(context, input)?;
841                input.expect_ident_matching("of")?;
842                let rx = LengthPercentage::parse(context, input)?;
843                let ry = input.try_parse(|i| LengthPercentage::parse(context, i)).ok();
844                let radii = ArcRadii { rx, ry: ry.into() };
845
846                // [<arc-sweep> || <arc-size> || rotate <angle>]?
847                let mut arc_sweep = None;
848                let mut arc_size = None;
849                let mut rotate = None;
850                loop {
851                    if arc_sweep.is_none() {
852                        arc_sweep = input.try_parse(ArcSweep::parse).ok();
853                    }
854
855                    if arc_size.is_none() {
856                        arc_size = input.try_parse(ArcSize::parse).ok();
857                        if arc_size.is_some() {
858                            continue;
859                        }
860                    }
861
862                    if rotate.is_none()
863                        && input
864                            .try_parse(|i| i.expect_ident_matching("rotate"))
865                            .is_ok()
866                    {
867                        rotate = Some(Angle::parse(context, input)?);
868                        continue;
869                    }
870                    break;
871                }
872                Self::Arc {
873                    point,
874                    radii,
875                    arc_sweep: arc_sweep.unwrap_or(ArcSweep::Ccw),
876                    arc_size: arc_size.unwrap_or(ArcSize::Small),
877                    rotate: rotate.unwrap_or(Angle::zero()),
878                }
879            },
880        })
881    }
882}
883
884impl Parse for generic::CoordinatePair<LengthPercentage> {
885    fn parse<'i, 't>(
886        context: &ParserContext,
887        input: &mut Parser<'i, 't>,
888    ) -> Result<Self, ParseError<'i>> {
889        let x = LengthPercentage::parse(context, input)?;
890        let y = LengthPercentage::parse(context, input)?;
891        Ok(Self::new(x, y))
892    }
893}
894
895impl generic::ControlPoint<Position, LengthPercentage> {
896    /// Parse <control-point> = [ <position> | <relative-control-point> ]
897    fn parse<'i, 't>(
898        context: &ParserContext,
899        input: &mut Parser<'i, 't>,
900        is_end_point_abs: bool,
901    ) -> Result<Self, ParseError<'i>> {
902        use generic::ControlReference;
903        let coord = input.try_parse(|i| generic::CoordinatePair::parse(context, i));
904
905        // Parse <position>
906        if is_end_point_abs && coord.is_err() {
907            let pos = Position::parse(context, input)?;
908            return Ok(Self::Absolute(pos));
909        }
910
911        // Parse <relative-control-point> = <coordinate-pair> [from [ start | end | origin ]]?
912        let coord = coord?;
913        let mut reference = if is_end_point_abs {
914            ControlReference::Origin
915        } else {
916            ControlReference::Start
917        };
918        if input.try_parse(|i| i.expect_ident_matching("from")).is_ok() {
919            reference = ControlReference::parse(input)?;
920        }
921
922        Ok(Self::Relative(generic::RelativeControlPoint {
923            coord,
924            reference,
925        }))
926    }
927}
928
929impl Parse for generic::CommandEndPoint<Position, LengthPercentage> {
930    /// Parse <command-end-point> = to <position> | by <coordinate-pair>
931    fn parse<'i, 't>(
932        context: &ParserContext,
933        input: &mut Parser<'i, 't>,
934    ) -> Result<Self, ParseError<'i>> {
935        if ByTo::parse(input)?.is_abs() {
936            Self::parse_endpoint_as_abs(context, input)
937        } else {
938            let point = generic::CoordinatePair::parse(context, input)?;
939            Ok(Self::ByCoordinate(point))
940        }
941    }
942}
943
944impl generic::CommandEndPoint<Position, LengthPercentage> {
945    /// Parse <command-end-point> = to <position>
946    fn parse_endpoint_as_abs<'i, 't>(
947        context: &ParserContext,
948        input: &mut Parser<'i, 't>,
949    ) -> Result<Self, ParseError<'i>> {
950        let point = Position::parse(context, input)?;
951        Ok(generic::CommandEndPoint::ToPosition(point))
952    }
953}
954
955impl generic::AxisEndPoint<LengthPercentage> {
956    /// Parse <horizontal-line-command>
957    pub fn parse_hline<'i, 't>(
958        context: &ParserContext,
959        input: &mut Parser<'i, 't>,
960    ) -> Result<Self, ParseError<'i>> {
961        use cssparser::Token;
962        use generic::{AxisPosition, AxisPositionKeyword};
963
964        // If the command is relative, parse for <length-percentage> only.
965        if !ByTo::parse(input)?.is_abs() {
966            return Ok(Self::ByCoordinate(LengthPercentage::parse(context, input)?));
967        }
968
969        let x = AxisPosition::parse(context, input)?;
970        if let AxisPosition::Keyword(
971            _word @ (AxisPositionKeyword::Top
972            | AxisPositionKeyword::Bottom
973            | AxisPositionKeyword::YStart
974            | AxisPositionKeyword::YEnd),
975        ) = &x
976        {
977            let location = input.current_source_location();
978            let token = Token::Ident(x.to_css_string().into());
979            return Err(location.new_unexpected_token_error(token));
980        }
981        Ok(Self::ToPosition(x))
982    }
983
984    /// Parse <vertical-line-command>
985    pub fn parse_vline<'i, 't>(
986        context: &ParserContext,
987        input: &mut Parser<'i, 't>,
988    ) -> Result<Self, ParseError<'i>> {
989        use cssparser::Token;
990        use generic::{AxisPosition, AxisPositionKeyword};
991
992        // If the command is relative, parse for <length-percentage> only.
993        if !ByTo::parse(input)?.is_abs() {
994            return Ok(Self::ByCoordinate(LengthPercentage::parse(context, input)?));
995        }
996
997        let y = AxisPosition::parse(context, input)?;
998        if let AxisPosition::Keyword(
999            _word @ (AxisPositionKeyword::Left
1000            | AxisPositionKeyword::Right
1001            | AxisPositionKeyword::XStart
1002            | AxisPositionKeyword::XEnd),
1003        ) = &y
1004        {
1005            // Return an error if we parsed a different keyword.
1006            let location = input.current_source_location();
1007            let token = Token::Ident(y.to_css_string().into());
1008            return Err(location.new_unexpected_token_error(token));
1009        }
1010        Ok(Self::ToPosition(y))
1011    }
1012}
1013
1014impl ToComputedValue for generic::AxisPosition<LengthPercentage> {
1015    type ComputedValue = generic::AxisPosition<ComputedLengthPercentage>;
1016
1017    fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
1018        match self {
1019            Self::LengthPercent(lp) => {
1020                Self::ComputedValue::LengthPercent(lp.to_computed_value(context))
1021            },
1022            Self::Keyword(word) => {
1023                let lp = LengthPercentage::Percentage(word.as_percentage());
1024                Self::ComputedValue::LengthPercent(lp.to_computed_value(context))
1025            },
1026        }
1027    }
1028
1029    fn from_computed_value(computed: &Self::ComputedValue) -> Self {
1030        match computed {
1031            Self::ComputedValue::LengthPercent(lp) => {
1032                Self::LengthPercent(LengthPercentage::from_computed_value(lp))
1033            },
1034            _ => unreachable!("Invalid state: computed value cannot be a keyword."),
1035        }
1036    }
1037}
1038
1039impl ToComputedValue for generic::AxisPosition<CSSFloat> {
1040    type ComputedValue = Self;
1041
1042    fn to_computed_value(&self, _context: &Context) -> Self {
1043        *self
1044    }
1045
1046    fn from_computed_value(computed: &Self) -> Self {
1047        *computed
1048    }
1049}
1050
1051/// This determines whether the command is absolutely or relatively positioned.
1052/// https://drafts.csswg.org/css-shapes-1/#typedef-shape-command-end-point
1053#[derive(Clone, Copy, Debug, Parse, PartialEq)]
1054enum ByTo {
1055    /// Command is relative to the command’s starting point.
1056    By,
1057    /// Command is relative to the top-left corner of the reference box.
1058    To,
1059}
1060
1061impl ByTo {
1062    /// Return true if it is absolute, i.e. it is To.
1063    #[inline]
1064    pub fn is_abs(&self) -> bool {
1065        matches!(self, ByTo::To)
1066    }
1067}