Skip to main content

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