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                let s_x = ShapeRadius::parse(context, i)?;
442                let s_y = ShapeRadius::parse(context, i)?;
443                if !static_prefs::pref!("layout.css.ellipse-corners.enabled")
444                    && (matches!(
445                        s_x,
446                        ShapeRadius::ClosestCorner | ShapeRadius::FarthestCorner
447                    ) || matches!(
448                        s_y,
449                        ShapeRadius::ClosestCorner | ShapeRadius::FarthestCorner
450                    ))
451                {
452                    Err(i.new_custom_error(StyleParseErrorKind::UnspecifiedError))
453                } else {
454                    Ok((s_x, s_y))
455                }
456            })
457            .unwrap_or_default();
458        let position = parse_at_position(context, input)?;
459
460        Ok(generic::Ellipse {
461            semiaxis_x,
462            semiaxis_y,
463            position,
464        })
465    }
466}
467
468fn parse_fill_rule<'i, 't>(
469    input: &mut Parser<'i, 't>,
470    shape_type: ShapeType,
471    expect_comma: bool,
472) -> FillRule {
473    match shape_type {
474        // Per [1] and [2], we ignore `<fill-rule>` for outline shapes, so always use a default
475        // value.
476        // [1] https://github.com/w3c/csswg-drafts/issues/3468
477        // [2] https://github.com/w3c/csswg-drafts/issues/7390
478        //
479        // Also, per [3] and [4], we would like the ignore `<file-rule>` from outline shapes, e.g.
480        // offset-path, which means we don't parse it when setting `ShapeType::Outline`.
481        // This should be web compatible because the shipped "offset-path:path()" doesn't have
482        // `<fill-rule>` and "offset-path:polygon()" is a new feature and still behind the
483        // preference.
484        // [3] https://github.com/w3c/fxtf-drafts/issues/512#issuecomment-1545393321
485        // [4] https://github.com/w3c/fxtf-drafts/issues/512#issuecomment-1555330929
486        ShapeType::Outline => Default::default(),
487        ShapeType::Filled => input
488            .try_parse(|i| -> Result<_, ParseError> {
489                let fill = FillRule::parse(i)?;
490                if expect_comma {
491                    i.expect_comma()?;
492                }
493                Ok(fill)
494            })
495            .unwrap_or_default(),
496    }
497}
498
499impl Parse for Polygon {
500    fn parse<'i, 't>(
501        context: &ParserContext,
502        input: &mut Parser<'i, 't>,
503    ) -> Result<Self, ParseError<'i>> {
504        input.expect_function_matching("polygon")?;
505        input.parse_nested_block(|i| Self::parse_function_arguments(context, i, ShapeType::Filled))
506    }
507}
508
509impl Polygon {
510    /// Parse the inner arguments of a `polygon` function.
511    fn parse_function_arguments<'i, 't>(
512        context: &ParserContext,
513        input: &mut Parser<'i, 't>,
514        shape_type: ShapeType,
515    ) -> Result<Self, ParseError<'i>> {
516        let fill = parse_fill_rule(input, shape_type, true /* has comma */);
517        let coordinates = input
518            .parse_comma_separated(|i| {
519                Ok(PolygonCoord(
520                    LengthPercentage::parse(context, i)?,
521                    LengthPercentage::parse(context, i)?,
522                ))
523            })?
524            .into();
525
526        Ok(Polygon { fill, coordinates })
527    }
528}
529
530impl Path {
531    /// Parse the inner arguments of a `path` function.
532    fn parse_function_arguments<'i, 't>(
533        input: &mut Parser<'i, 't>,
534        shape_type: ShapeType,
535    ) -> Result<Self, ParseError<'i>> {
536        use crate::values::specified::svg_path::AllowEmpty;
537
538        let fill = parse_fill_rule(input, shape_type, true /* has comma */);
539        let path = SVGPathData::parse(input, AllowEmpty::No)?;
540        Ok(Path { fill, path })
541    }
542}
543
544fn round_to_css<W>(round: &BorderRadius, dest: &mut CssWriter<W>) -> fmt::Result
545where
546    W: Write,
547{
548    if !round.is_zero() {
549        dest.write_str(" round ")?;
550        round.to_css(dest)?;
551    }
552    Ok(())
553}
554
555impl ToCss for Xywh {
556    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
557    where
558        W: Write,
559    {
560        self.x.to_css(dest)?;
561        dest.write_char(' ')?;
562        self.y.to_css(dest)?;
563        dest.write_char(' ')?;
564        self.width.to_css(dest)?;
565        dest.write_char(' ')?;
566        self.height.to_css(dest)?;
567        round_to_css(&self.round, dest)
568    }
569}
570
571impl Xywh {
572    /// Parse the inner function arguments of `xywh()`.
573    fn parse_function_arguments<'i, 't>(
574        context: &ParserContext,
575        input: &mut Parser<'i, 't>,
576    ) -> Result<Self, ParseError<'i>> {
577        let x = LengthPercentage::parse(context, input)?;
578        let y = LengthPercentage::parse(context, input)?;
579        let width = NonNegativeLengthPercentage::parse(context, input)?;
580        let height = NonNegativeLengthPercentage::parse(context, input)?;
581        let round = parse_round(context, input)?;
582        Ok(Xywh {
583            x,
584            y,
585            width,
586            height,
587            round,
588        })
589    }
590}
591
592impl ToCss for ShapeRectFunction {
593    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
594    where
595        W: Write,
596    {
597        self.rect.0.to_css(dest)?;
598        dest.write_char(' ')?;
599        self.rect.1.to_css(dest)?;
600        dest.write_char(' ')?;
601        self.rect.2.to_css(dest)?;
602        dest.write_char(' ')?;
603        self.rect.3.to_css(dest)?;
604        round_to_css(&self.round, dest)
605    }
606}
607
608impl ShapeRectFunction {
609    /// Parse the inner function arguments of `rect()`.
610    fn parse_function_arguments<'i, 't>(
611        context: &ParserContext,
612        input: &mut Parser<'i, 't>,
613    ) -> Result<Self, ParseError<'i>> {
614        let rect = Rect::parse_all_components_with(context, input, LengthPercentageOrAuto::parse)?;
615        let round = parse_round(context, input)?;
616        Ok(ShapeRectFunction { rect, round })
617    }
618}
619
620impl ToComputedValue for BasicShapeRect {
621    type ComputedValue = ComputedInsetRect;
622
623    #[inline]
624    fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
625        use crate::values::computed::LengthPercentage;
626        use crate::values::computed::LengthPercentageOrAuto;
627        use style_traits::values::specified::AllowedNumericType;
628
629        match self {
630            Self::Inset(ref inset) => inset.to_computed_value(context),
631            Self::Xywh(ref xywh) => {
632                // Given `xywh(x y w h)`, construct the equivalent inset() function,
633                // `inset(y calc(100% - x - w) calc(100% - y - h) x)`.
634                //
635                // https://drafts.csswg.org/css-shapes-1/#basic-shape-computed-values
636                // https://github.com/w3c/csswg-drafts/issues/9053
637                let x = xywh.x.to_computed_value(context);
638                let y = xywh.y.to_computed_value(context);
639                let w = xywh.width.to_computed_value(context);
640                let h = xywh.height.to_computed_value(context);
641                // calc(100% - x - w).
642                let right = LengthPercentage::hundred_percent_minus_list(
643                    &[&x, &w.0],
644                    AllowedNumericType::All,
645                );
646                // calc(100% - y - h).
647                let bottom = LengthPercentage::hundred_percent_minus_list(
648                    &[&y, &h.0],
649                    AllowedNumericType::All,
650                );
651
652                ComputedInsetRect {
653                    rect: Rect::new(y, right, bottom, x),
654                    round: xywh.round.to_computed_value(context),
655                }
656            },
657            Self::Rect(ref rect) => {
658                // Given `rect(t r b l)`, the equivalent function is
659                // `inset(t calc(100% - r) calc(100% - b) l)`.
660                //
661                // https://drafts.csswg.org/css-shapes-1/#basic-shape-computed-values
662                fn compute_top_or_left(v: LengthPercentageOrAuto) -> LengthPercentage {
663                    match v {
664                        // it’s equivalent to 0% as the first (top) or fourth (left) value.
665                        // https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-rect
666                        LengthPercentageOrAuto::Auto => LengthPercentage::zero_percent(),
667                        LengthPercentageOrAuto::LengthPercentage(lp) => lp,
668                    }
669                }
670                fn compute_bottom_or_right(v: LengthPercentageOrAuto) -> LengthPercentage {
671                    match v {
672                        // It's equivalent to 100% as the second (right) or third (bottom) value.
673                        // So calc(100% - 100%) = 0%.
674                        // https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-rect
675                        LengthPercentageOrAuto::Auto => LengthPercentage::zero_percent(),
676                        LengthPercentageOrAuto::LengthPercentage(lp) => {
677                            LengthPercentage::hundred_percent_minus(lp, AllowedNumericType::All)
678                        },
679                    }
680                }
681
682                let round = rect.round.to_computed_value(context);
683                let rect = rect.rect.to_computed_value(context);
684                let rect = Rect::new(
685                    compute_top_or_left(rect.0),
686                    compute_bottom_or_right(rect.1),
687                    compute_bottom_or_right(rect.2),
688                    compute_top_or_left(rect.3),
689                );
690
691                ComputedInsetRect { rect, round }
692            },
693        }
694    }
695
696    #[inline]
697    fn from_computed_value(computed: &Self::ComputedValue) -> Self {
698        Self::Inset(ToComputedValue::from_computed_value(computed))
699    }
700}
701
702impl generic::Shape<Angle, Position, LengthPercentage> {
703    /// Parse the inner arguments of a `shape` function.
704    /// shape() = shape(<fill-rule>? from <coordinate-pair>, <shape-command>#)
705    fn parse_function_arguments<'i, 't>(
706        context: &ParserContext,
707        input: &mut Parser<'i, 't>,
708        shape_type: ShapeType,
709    ) -> Result<Self, ParseError<'i>> {
710        let fill = parse_fill_rule(input, shape_type, false /* no following comma */);
711
712        let mut first = true;
713        let commands = input.parse_comma_separated(|i| {
714            if first {
715                first = false;
716
717                // The starting point for the first shape-command. It adds an initial absolute
718                // moveto to the list of path data commands, with the <coordinate-pair> measured
719                // from the top-left corner of the reference
720                i.expect_ident_matching("from")?;
721                Ok(ShapeCommand::Move {
722                    point: generic::CommandEndPoint::parse_endpoint_as_abs(context, i)?,
723                })
724            } else {
725                // The further path data commands.
726                ShapeCommand::parse(context, i)
727            }
728        })?;
729
730        // We must have one starting point and at least one following <shape-command>.
731        if commands.len() < 2 {
732            return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
733        }
734
735        Ok(Self {
736            fill,
737            commands: commands.into(),
738        })
739    }
740}
741
742impl Parse for ShapeCommand {
743    fn parse<'i, 't>(
744        context: &ParserContext,
745        input: &mut Parser<'i, 't>,
746    ) -> Result<Self, ParseError<'i>> {
747        use crate::values::generics::basic_shape::{
748            ArcRadii, ArcSize, ArcSweep, AxisEndPoint, CommandEndPoint, ControlPoint,
749        };
750
751        // <shape-command> = <move-command> | <line-command> | <hv-line-command> |
752        //                   <curve-command> | <smooth-command> | <arc-command> | close
753        Ok(try_match_ident_ignore_ascii_case! { input,
754            "close" => Self::Close,
755            "move" => {
756                let point = CommandEndPoint::parse(context, input)?;
757                Self::Move { point }
758            },
759            "line" => {
760                let point = CommandEndPoint::parse(context, input)?;
761                Self::Line { point }
762            },
763            "hline" => {
764                let x = AxisEndPoint::parse_hline(context, input)?;
765                Self::HLine { x }
766            },
767            "vline" => {
768                let y = AxisEndPoint::parse_vline(context, input)?;
769                Self::VLine { y }
770            },
771            "curve" => {
772                let point = CommandEndPoint::parse(context, input)?;
773                input.expect_ident_matching("with")?;
774                let control1 = ControlPoint::parse(context, input, point.is_abs())?;
775                if input.try_parse(|i| i.expect_delim('/')).is_ok() {
776                    let control2 = ControlPoint::parse(context, input, point.is_abs())?;
777                    Self::CubicCurve {
778                        point,
779                        control1,
780                        control2,
781                    }
782                } else {
783                    Self::QuadCurve {
784                        point,
785                        control1,
786                    }
787                }
788            },
789            "smooth" => {
790                let point = CommandEndPoint::parse(context, input)?;
791                if input.try_parse(|i| i.expect_ident_matching("with")).is_ok() {
792                    let control2 = ControlPoint::parse(context, input, point.is_abs())?;
793                    Self::SmoothCubic {
794                        point,
795                        control2,
796                    }
797                } else {
798                    Self::SmoothQuad { point }
799                }
800            },
801            "arc" => {
802                let point = CommandEndPoint::parse(context, input)?;
803                input.expect_ident_matching("of")?;
804                let rx = LengthPercentage::parse(context, input)?;
805                let ry = input.try_parse(|i| LengthPercentage::parse(context, i)).ok();
806                let radii = ArcRadii { rx, ry: ry.into() };
807
808                // [<arc-sweep> || <arc-size> || rotate <angle>]?
809                let mut arc_sweep = None;
810                let mut arc_size = None;
811                let mut rotate = None;
812                loop {
813                    if arc_sweep.is_none() {
814                        arc_sweep = input.try_parse(ArcSweep::parse).ok();
815                    }
816
817                    if arc_size.is_none() {
818                        arc_size = input.try_parse(ArcSize::parse).ok();
819                        if arc_size.is_some() {
820                            continue;
821                        }
822                    }
823
824                    if rotate.is_none()
825                        && input
826                            .try_parse(|i| i.expect_ident_matching("rotate"))
827                            .is_ok()
828                    {
829                        rotate = Some(Angle::parse(context, input)?);
830                        continue;
831                    }
832                    break;
833                }
834                Self::Arc {
835                    point,
836                    radii,
837                    arc_sweep: arc_sweep.unwrap_or(ArcSweep::Ccw),
838                    arc_size: arc_size.unwrap_or(ArcSize::Small),
839                    rotate: rotate.unwrap_or(Angle::zero()),
840                }
841            },
842        })
843    }
844}
845
846impl Parse for generic::CoordinatePair<LengthPercentage> {
847    fn parse<'i, 't>(
848        context: &ParserContext,
849        input: &mut Parser<'i, 't>,
850    ) -> Result<Self, ParseError<'i>> {
851        let x = LengthPercentage::parse(context, input)?;
852        let y = LengthPercentage::parse(context, input)?;
853        Ok(Self::new(x, y))
854    }
855}
856
857impl generic::ControlPoint<Position, LengthPercentage> {
858    /// Parse <control-point> = [ <position> | <relative-control-point> ]
859    fn parse<'i, 't>(
860        context: &ParserContext,
861        input: &mut Parser<'i, 't>,
862        is_end_point_abs: bool,
863    ) -> Result<Self, ParseError<'i>> {
864        use generic::ControlReference;
865        let coord = input.try_parse(|i| generic::CoordinatePair::parse(context, i));
866
867        // Parse <position>
868        if is_end_point_abs && coord.is_err() {
869            let pos = Position::parse(context, input)?;
870            return Ok(Self::Absolute(pos));
871        }
872
873        // Parse <relative-control-point> = <coordinate-pair> [from [ start | end | origin ]]?
874        let coord = coord?;
875        let mut reference = if is_end_point_abs {
876            ControlReference::Origin
877        } else {
878            ControlReference::Start
879        };
880        if input.try_parse(|i| i.expect_ident_matching("from")).is_ok() {
881            reference = ControlReference::parse(input)?;
882        }
883
884        Ok(Self::Relative(generic::RelativeControlPoint {
885            coord,
886            reference,
887        }))
888    }
889}
890
891impl Parse for generic::CommandEndPoint<Position, LengthPercentage> {
892    /// Parse <command-end-point> = to <position> | by <coordinate-pair>
893    fn parse<'i, 't>(
894        context: &ParserContext,
895        input: &mut Parser<'i, 't>,
896    ) -> Result<Self, ParseError<'i>> {
897        if ByTo::parse(input)?.is_abs() {
898            Self::parse_endpoint_as_abs(context, input)
899        } else {
900            let point = generic::CoordinatePair::parse(context, input)?;
901            Ok(Self::ByCoordinate(point))
902        }
903    }
904}
905
906impl generic::CommandEndPoint<Position, LengthPercentage> {
907    /// Parse <command-end-point> = to <position>
908    fn parse_endpoint_as_abs<'i, 't>(
909        context: &ParserContext,
910        input: &mut Parser<'i, 't>,
911    ) -> Result<Self, ParseError<'i>> {
912        let point = Position::parse(context, input)?;
913        Ok(generic::CommandEndPoint::ToPosition(point))
914    }
915}
916
917impl generic::AxisEndPoint<LengthPercentage> {
918    /// Parse <horizontal-line-command>
919    pub fn parse_hline<'i, 't>(
920        context: &ParserContext,
921        input: &mut Parser<'i, 't>,
922    ) -> Result<Self, ParseError<'i>> {
923        use cssparser::Token;
924        use generic::{AxisPosition, AxisPositionKeyword};
925
926        // If the command is relative, parse for <length-percentage> only.
927        if !ByTo::parse(input)?.is_abs() {
928            return Ok(Self::ByCoordinate(LengthPercentage::parse(context, input)?));
929        }
930
931        let x = AxisPosition::parse(context, input)?;
932        if let AxisPosition::Keyword(
933            _word @ (AxisPositionKeyword::Top
934            | AxisPositionKeyword::Bottom
935            | AxisPositionKeyword::YStart
936            | AxisPositionKeyword::YEnd),
937        ) = &x
938        {
939            let location = input.current_source_location();
940            let token = Token::Ident(x.to_css_string().into());
941            return Err(location.new_unexpected_token_error(token));
942        }
943        Ok(Self::ToPosition(x))
944    }
945
946    /// Parse <vertical-line-command>
947    pub fn parse_vline<'i, 't>(
948        context: &ParserContext,
949        input: &mut Parser<'i, 't>,
950    ) -> Result<Self, ParseError<'i>> {
951        use cssparser::Token;
952        use generic::{AxisPosition, AxisPositionKeyword};
953
954        // If the command is relative, parse for <length-percentage> only.
955        if !ByTo::parse(input)?.is_abs() {
956            return Ok(Self::ByCoordinate(LengthPercentage::parse(context, input)?));
957        }
958
959        let y = AxisPosition::parse(context, input)?;
960        if let AxisPosition::Keyword(
961            _word @ (AxisPositionKeyword::Left
962            | AxisPositionKeyword::Right
963            | AxisPositionKeyword::XStart
964            | AxisPositionKeyword::XEnd),
965        ) = &y
966        {
967            // Return an error if we parsed a different keyword.
968            let location = input.current_source_location();
969            let token = Token::Ident(y.to_css_string().into());
970            return Err(location.new_unexpected_token_error(token));
971        }
972        Ok(Self::ToPosition(y))
973    }
974}
975
976impl ToComputedValue for generic::AxisPosition<LengthPercentage> {
977    type ComputedValue = generic::AxisPosition<ComputedLengthPercentage>;
978
979    fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
980        match self {
981            Self::LengthPercent(lp) => {
982                Self::ComputedValue::LengthPercent(lp.to_computed_value(context))
983            },
984            Self::Keyword(word) => {
985                let lp =
986                    LengthPercentage::Percentage(NoCalcPercentage::new(word.as_percentage().0));
987                Self::ComputedValue::LengthPercent(lp.to_computed_value(context))
988            },
989        }
990    }
991
992    fn from_computed_value(computed: &Self::ComputedValue) -> Self {
993        match computed {
994            Self::ComputedValue::LengthPercent(lp) => {
995                Self::LengthPercent(LengthPercentage::from_computed_value(lp))
996            },
997            _ => unreachable!("Invalid state: computed value cannot be a keyword."),
998        }
999    }
1000}
1001
1002impl ToComputedValue for generic::AxisPosition<CSSFloat> {
1003    type ComputedValue = Self;
1004
1005    fn to_computed_value(&self, _context: &Context) -> Self {
1006        *self
1007    }
1008
1009    fn from_computed_value(computed: &Self) -> Self {
1010        *computed
1011    }
1012}
1013
1014/// This determines whether the command is absolutely or relatively positioned.
1015/// https://drafts.csswg.org/css-shapes-1/#typedef-shape-command-end-point
1016#[derive(Clone, Copy, Debug, Parse, PartialEq)]
1017enum ByTo {
1018    /// Command is relative to the command’s starting point.
1019    By,
1020    /// Command is relative to the top-left corner of the reference box.
1021    To,
1022}
1023
1024impl ByTo {
1025    /// Return true if it is absolute, i.e. it is To.
1026    #[inline]
1027    pub fn is_abs(&self) -> bool {
1028        matches!(self, ByTo::To)
1029    }
1030}