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<R, ReferenceBox>(
203    context: &ParserContext,
204    input: &mut Parser,
205    to_shape: impl FnOnce(Box<BasicShape>, ReferenceBox) -> R,
206    to_reference_box: impl FnOnce(ReferenceBox) -> R,
207    flags: AllowedBasicShapes,
208) -> Result<R, ParseError>
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(ParseError::custom(StyleParseErrorKind::UnspecifiedError)),
237    }
238}
239
240impl Parse for ClipPath {
241    #[inline]
242    fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
243        if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
244            return Ok(ClipPath::None);
245        }
246
247        if let Ok(url) = input.try_parse(|i| SpecifiedUrl::parse(context, i)) {
248            return Ok(ClipPath::Url(url));
249        }
250
251        parse_shape_or_box(
252            context,
253            input,
254            ClipPath::Shape,
255            ClipPath::Box,
256            AllowedBasicShapes::ALL,
257        )
258    }
259}
260
261impl Parse for ShapeOutside {
262    #[inline]
263    fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
264        // Need to parse this here so that `Image::parse_with_cors_anonymous`
265        // doesn't parse it.
266        if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
267            return Ok(ShapeOutside::None);
268        }
269
270        if let Ok(image) = input.try_parse(|i| Image::parse_with_cors_anonymous(context, i)) {
271            debug_assert_ne!(image, Image::None);
272            return Ok(ShapeOutside::Image(image));
273        }
274
275        parse_shape_or_box(
276            context,
277            input,
278            ShapeOutside::Shape,
279            ShapeOutside::Box,
280            AllowedBasicShapes::SHAPE_OUTSIDE,
281        )
282    }
283}
284
285impl BasicShape {
286    /// Parse with some parameters.
287    /// 1. The supported <basic-shape>.
288    /// 2. The type of shapes. Should we ignore fill-rule?
289    /// 3. The default value of `at <position>`.
290    pub fn parse(
291        context: &ParserContext,
292        input: &mut Parser,
293        flags: AllowedBasicShapes,
294        shape_type: ShapeType,
295    ) -> Result<Self, ParseError> {
296        let function = input.expect_function()?.clone();
297        input.parse_nested_block(move |i| {
298            match_ignore_ascii_case! { &function,
299                "inset" if flags.contains(AllowedBasicShapes::INSET) => {
300                    InsetRect::parse_function_arguments(context, i)
301                        .map(BasicShapeRect::Inset)
302                        .map(BasicShape::Rect)
303                },
304                "xywh" if flags.contains(AllowedBasicShapes::XYWH) => {
305                    Xywh::parse_function_arguments(context, i)
306                        .map(BasicShapeRect::Xywh)
307                        .map(BasicShape::Rect)
308                },
309                "rect" if flags.contains(AllowedBasicShapes::RECT) => {
310                    ShapeRectFunction::parse_function_arguments(context, i)
311                        .map(BasicShapeRect::Rect)
312                        .map(BasicShape::Rect)
313                },
314                "circle" if flags.contains(AllowedBasicShapes::CIRCLE) => {
315                    Circle::parse_function_arguments(context, i)
316                        .map(BasicShape::Circle)
317                },
318                "ellipse" if flags.contains(AllowedBasicShapes::ELLIPSE) => {
319                    Ellipse::parse_function_arguments(context, i)
320                        .map(BasicShape::Ellipse)
321                },
322                "polygon" if flags.contains(AllowedBasicShapes::POLYGON) => {
323                    Polygon::parse_function_arguments(context, i, shape_type)
324                        .map(BasicShape::Polygon)
325                },
326                "path" if flags.contains(AllowedBasicShapes::PATH) => {
327                    Path::parse_function_arguments(i, shape_type)
328                        .map(PathOrShapeFunction::Path)
329                        .map(BasicShape::PathOrShape)
330                },
331                "shape"
332                    if flags.contains(AllowedBasicShapes::SHAPE)
333                        && crate::pref!("layout.css.basic-shape-shape.enabled") =>
334                {
335                    generic::Shape::parse_function_arguments(context, i, shape_type)
336                        .map(PathOrShapeFunction::Shape)
337                        .map(BasicShape::PathOrShape)
338                },
339                _ => Err(ParseError::custom(StyleParseErrorKind::UnexpectedFunction)),
340            }
341        })
342    }
343}
344
345impl Parse for InsetRect {
346    fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
347        input.expect_function_matching("inset")?;
348        input.parse_nested_block(|i| Self::parse_function_arguments(context, i))
349    }
350}
351
352fn parse_round(context: &ParserContext, input: &mut Parser) -> Result<BorderRadius, ParseError> {
353    if input
354        .try_parse(|i| i.expect_ident_matching("round"))
355        .is_ok()
356    {
357        return BorderRadius::parse(context, input);
358    }
359
360    Ok(BorderRadius::zero())
361}
362
363impl InsetRect {
364    /// Parse the inner function arguments of `inset()`
365    fn parse_function_arguments(
366        context: &ParserContext,
367        input: &mut Parser,
368    ) -> Result<Self, ParseError> {
369        let rect = Rect::parse_with(context, input, LengthPercentage::parse)?;
370        let round = parse_round(context, input)?;
371        Ok(generic::InsetRect { rect, round })
372    }
373}
374
375fn parse_at_position(
376    context: &ParserContext,
377    input: &mut Parser,
378) -> Result<GenericPositionOrAuto<Position>, ParseError> {
379    if input.try_parse(|i| i.expect_ident_matching("at")).is_ok() {
380        Position::parse(context, input).map(GenericPositionOrAuto::Position)
381    } else {
382        Ok(GenericPositionOrAuto::Auto)
383    }
384}
385
386impl Parse for Circle {
387    fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
388        input.expect_function_matching("circle")?;
389        input.parse_nested_block(|i| Self::parse_function_arguments(context, i))
390    }
391}
392
393impl Circle {
394    fn parse_function_arguments(
395        context: &ParserContext,
396        input: &mut Parser,
397    ) -> Result<Self, ParseError> {
398        let radius = input
399            .try_parse(|i| ShapeRadius::parse(context, i))
400            .unwrap_or_default();
401        let position = parse_at_position(context, input)?;
402
403        Ok(generic::Circle { radius, position })
404    }
405}
406
407impl Parse for Ellipse {
408    fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
409        input.expect_function_matching("ellipse")?;
410        input.parse_nested_block(|i| Self::parse_function_arguments(context, i))
411    }
412}
413
414impl Ellipse {
415    fn parse_function_arguments(
416        context: &ParserContext,
417        input: &mut Parser,
418    ) -> Result<Self, ParseError> {
419        let (semiaxis_x, semiaxis_y) = input
420            .try_parse(|i| -> Result<_, ParseError> {
421                let s_x = ShapeRadius::parse(context, i)?;
422                let s_y = ShapeRadius::parse(context, i)?;
423                if !crate::pref!("layout.css.ellipse-corners.enabled")
424                    && (matches!(
425                        s_x,
426                        ShapeRadius::ClosestCorner | ShapeRadius::FarthestCorner
427                    ) || matches!(
428                        s_y,
429                        ShapeRadius::ClosestCorner | ShapeRadius::FarthestCorner
430                    ))
431                {
432                    Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError))
433                } else {
434                    Ok((s_x, s_y))
435                }
436            })
437            .unwrap_or_default();
438        let position = parse_at_position(context, input)?;
439
440        Ok(generic::Ellipse {
441            semiaxis_x,
442            semiaxis_y,
443            position,
444        })
445    }
446}
447
448fn parse_fill_rule(input: &mut Parser, shape_type: ShapeType, expect_comma: bool) -> FillRule {
449    match shape_type {
450        // Per [1] and [2], we ignore `<fill-rule>` for outline shapes, so always use a default
451        // value.
452        // [1] https://github.com/w3c/csswg-drafts/issues/3468
453        // [2] https://github.com/w3c/csswg-drafts/issues/7390
454        //
455        // Also, per [3] and [4], we would like the ignore `<file-rule>` from outline shapes, e.g.
456        // offset-path, which means we don't parse it when setting `ShapeType::Outline`.
457        // This should be web compatible because the shipped "offset-path:path()" doesn't have
458        // `<fill-rule>` and "offset-path:polygon()" is a new feature and still behind the
459        // preference.
460        // [3] https://github.com/w3c/fxtf-drafts/issues/512#issuecomment-1545393321
461        // [4] https://github.com/w3c/fxtf-drafts/issues/512#issuecomment-1555330929
462        ShapeType::Outline => Default::default(),
463        ShapeType::Filled => input
464            .try_parse(|i| -> Result<_, ParseError> {
465                let fill = FillRule::parse(i)?;
466                if expect_comma {
467                    i.expect_comma()?;
468                }
469                Ok(fill)
470            })
471            .unwrap_or_default(),
472    }
473}
474
475impl Parse for Polygon {
476    fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
477        input.expect_function_matching("polygon")?;
478        input.parse_nested_block(|i| Self::parse_function_arguments(context, i, ShapeType::Filled))
479    }
480}
481
482impl Polygon {
483    /// Parse the inner arguments of a `polygon` function.
484    fn parse_function_arguments(
485        context: &ParserContext,
486        input: &mut Parser,
487        shape_type: ShapeType,
488    ) -> Result<Self, ParseError> {
489        let fill = parse_fill_rule(input, shape_type, true /* has comma */);
490        let coordinates = input
491            .parse_comma_separated(|i| {
492                Ok(PolygonCoord(
493                    LengthPercentage::parse(context, i)?,
494                    LengthPercentage::parse(context, i)?,
495                ))
496            })?
497            .into();
498
499        Ok(Polygon { fill, coordinates })
500    }
501}
502
503impl Path {
504    /// Parse the inner arguments of a `path` function.
505    fn parse_function_arguments(
506        input: &mut Parser,
507        shape_type: ShapeType,
508    ) -> Result<Self, ParseError> {
509        use crate::values::specified::svg_path::AllowEmpty;
510
511        let fill = parse_fill_rule(input, shape_type, true /* has comma */);
512        let path = SVGPathData::parse(input, AllowEmpty::No)?;
513        Ok(Path { fill, path })
514    }
515}
516
517fn round_to_css<W>(round: &BorderRadius, dest: &mut CssWriter<W>) -> fmt::Result
518where
519    W: Write,
520{
521    if !round.is_zero() {
522        dest.write_str(" round ")?;
523        round.to_css(dest)?;
524    }
525    Ok(())
526}
527
528impl ToCss for Xywh {
529    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
530    where
531        W: Write,
532    {
533        self.x.to_css(dest)?;
534        dest.write_char(' ')?;
535        self.y.to_css(dest)?;
536        dest.write_char(' ')?;
537        self.width.to_css(dest)?;
538        dest.write_char(' ')?;
539        self.height.to_css(dest)?;
540        round_to_css(&self.round, dest)
541    }
542}
543
544impl Xywh {
545    /// Parse the inner function arguments of `xywh()`.
546    fn parse_function_arguments(
547        context: &ParserContext,
548        input: &mut Parser,
549    ) -> Result<Self, ParseError> {
550        let x = LengthPercentage::parse(context, input)?;
551        let y = LengthPercentage::parse(context, input)?;
552        let width = NonNegativeLengthPercentage::parse(context, input)?;
553        let height = NonNegativeLengthPercentage::parse(context, input)?;
554        let round = parse_round(context, input)?;
555        Ok(Xywh {
556            x,
557            y,
558            width,
559            height,
560            round,
561        })
562    }
563}
564
565impl ToCss for ShapeRectFunction {
566    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
567    where
568        W: Write,
569    {
570        self.rect.0.to_css(dest)?;
571        dest.write_char(' ')?;
572        self.rect.1.to_css(dest)?;
573        dest.write_char(' ')?;
574        self.rect.2.to_css(dest)?;
575        dest.write_char(' ')?;
576        self.rect.3.to_css(dest)?;
577        round_to_css(&self.round, dest)
578    }
579}
580
581impl ShapeRectFunction {
582    /// Parse the inner function arguments of `rect()`.
583    fn parse_function_arguments(
584        context: &ParserContext,
585        input: &mut Parser,
586    ) -> Result<Self, ParseError> {
587        let rect = Rect::parse_all_components_with(context, input, LengthPercentageOrAuto::parse)?;
588        let round = parse_round(context, input)?;
589        Ok(ShapeRectFunction { rect, round })
590    }
591}
592
593impl ToComputedValue for BasicShapeRect {
594    type ComputedValue = ComputedInsetRect;
595
596    #[inline]
597    fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
598        use crate::values::computed::LengthPercentage;
599        use crate::values::computed::LengthPercentageOrAuto;
600        use style_traits::values::specified::AllowedNumericType;
601
602        match self {
603            Self::Inset(inset) => inset.to_computed_value(context),
604            Self::Xywh(xywh) => {
605                // Given `xywh(x y w h)`, construct the equivalent inset() function,
606                // `inset(y calc(100% - x - w) calc(100% - y - h) x)`.
607                //
608                // https://drafts.csswg.org/css-shapes-1/#basic-shape-computed-values
609                // https://github.com/w3c/csswg-drafts/issues/9053
610                let x = xywh.x.to_computed_value(context);
611                let y = xywh.y.to_computed_value(context);
612                let w = xywh.width.to_computed_value(context);
613                let h = xywh.height.to_computed_value(context);
614                // calc(100% - x - w).
615                let right = LengthPercentage::hundred_percent_minus_list(
616                    &[&x, &w.0],
617                    AllowedNumericType::All,
618                );
619                // calc(100% - y - h).
620                let bottom = LengthPercentage::hundred_percent_minus_list(
621                    &[&y, &h.0],
622                    AllowedNumericType::All,
623                );
624
625                ComputedInsetRect {
626                    rect: Rect::new(y, right, bottom, x),
627                    round: xywh.round.to_computed_value(context),
628                }
629            },
630            Self::Rect(rect) => {
631                // Given `rect(t r b l)`, the equivalent function is
632                // `inset(t calc(100% - r) calc(100% - b) l)`.
633                //
634                // https://drafts.csswg.org/css-shapes-1/#basic-shape-computed-values
635                fn compute_top_or_left(v: LengthPercentageOrAuto) -> LengthPercentage {
636                    match v {
637                        // it’s equivalent to 0% as the first (top) or fourth (left) value.
638                        // https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-rect
639                        LengthPercentageOrAuto::Auto => LengthPercentage::zero_percent(),
640                        LengthPercentageOrAuto::LengthPercentage(lp) => lp,
641                    }
642                }
643                fn compute_bottom_or_right(v: LengthPercentageOrAuto) -> LengthPercentage {
644                    match v {
645                        // It's equivalent to 100% as the second (right) or third (bottom) value.
646                        // So calc(100% - 100%) = 0%.
647                        // https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-rect
648                        LengthPercentageOrAuto::Auto => LengthPercentage::zero_percent(),
649                        LengthPercentageOrAuto::LengthPercentage(lp) => {
650                            LengthPercentage::hundred_percent_minus(lp, AllowedNumericType::All)
651                        },
652                    }
653                }
654
655                let round = rect.round.to_computed_value(context);
656                let rect = rect.rect.to_computed_value(context);
657                let rect = Rect::new(
658                    compute_top_or_left(rect.0),
659                    compute_bottom_or_right(rect.1),
660                    compute_bottom_or_right(rect.2),
661                    compute_top_or_left(rect.3),
662                );
663
664                ComputedInsetRect { rect, round }
665            },
666        }
667    }
668
669    #[inline]
670    fn from_computed_value(computed: &Self::ComputedValue) -> Self {
671        Self::Inset(ToComputedValue::from_computed_value(computed))
672    }
673}
674
675impl generic::Shape<Angle, Position, LengthPercentage> {
676    /// Parse the inner arguments of a `shape` function.
677    /// shape() = shape(<fill-rule>? from <coordinate-pair>, <shape-command>#)
678    fn parse_function_arguments(
679        context: &ParserContext,
680        input: &mut Parser,
681        shape_type: ShapeType,
682    ) -> Result<Self, ParseError> {
683        let fill = parse_fill_rule(input, shape_type, false /* no following comma */);
684
685        let mut first = true;
686        let commands = input.parse_comma_separated(|i| {
687            if first {
688                first = false;
689
690                // The starting point for the first shape-command. It adds an initial absolute
691                // moveto to the list of path data commands, with the <coordinate-pair> measured
692                // from the top-left corner of the reference
693                i.expect_ident_matching("from")?;
694                Ok(ShapeCommand::Move {
695                    point: generic::CommandEndPoint::parse_endpoint_as_abs(context, i)?,
696                })
697            } else {
698                // The further path data commands.
699                ShapeCommand::parse(context, i)
700            }
701        })?;
702
703        // We must have one starting point and at least one following <shape-command>.
704        if commands.len() < 2 {
705            return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError));
706        }
707
708        Ok(Self {
709            fill,
710            commands: commands.into(),
711        })
712    }
713}
714
715impl Parse for ShapeCommand {
716    fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
717        use crate::values::generics::basic_shape::{
718            ArcRadii, ArcSize, ArcSweep, AxisEndPoint, CommandEndPoint, ControlPoint,
719        };
720
721        // <shape-command> = <move-command> | <line-command> | <hv-line-command> |
722        //                   <curve-command> | <smooth-command> | <arc-command> | close
723        Ok(try_match_ident_ignore_ascii_case! { input,
724            "close" => Self::Close,
725            "move" => {
726                let point = CommandEndPoint::parse(context, input)?;
727                Self::Move { point }
728            },
729            "line" => {
730                let point = CommandEndPoint::parse(context, input)?;
731                Self::Line { point }
732            },
733            "hline" => {
734                let x = AxisEndPoint::parse_hline(context, input)?;
735                Self::HLine { x }
736            },
737            "vline" => {
738                let y = AxisEndPoint::parse_vline(context, input)?;
739                Self::VLine { y }
740            },
741            "curve" => {
742                let point = CommandEndPoint::parse(context, input)?;
743                input.expect_ident_matching("with")?;
744                let control1 = ControlPoint::parse(context, input, point.is_abs())?;
745                if input.try_parse(|i| i.expect_delim('/')).is_ok() {
746                    let control2 = ControlPoint::parse(context, input, point.is_abs())?;
747                    Self::CubicCurve {
748                        point,
749                        control1,
750                        control2,
751                    }
752                } else {
753                    Self::QuadCurve {
754                        point,
755                        control1,
756                    }
757                }
758            },
759            "smooth" => {
760                let point = CommandEndPoint::parse(context, input)?;
761                if input.try_parse(|i| i.expect_ident_matching("with")).is_ok() {
762                    let control2 = ControlPoint::parse(context, input, point.is_abs())?;
763                    Self::SmoothCubic {
764                        point,
765                        control2,
766                    }
767                } else {
768                    Self::SmoothQuad { point }
769                }
770            },
771            "arc" => {
772                let point = CommandEndPoint::parse(context, input)?;
773                input.expect_ident_matching("of")?;
774                let rx = LengthPercentage::parse(context, input)?;
775                let ry = input.try_parse(|i| LengthPercentage::parse(context, i)).ok();
776                let radii = ArcRadii { rx, ry: ry.into() };
777
778                // [<arc-sweep> || <arc-size> || rotate <angle>]?
779                let mut arc_sweep = None;
780                let mut arc_size = None;
781                let mut rotate = None;
782                loop {
783                    if arc_sweep.is_none() {
784                        arc_sweep = input.try_parse(ArcSweep::parse).ok();
785                    }
786
787                    if arc_size.is_none() {
788                        arc_size = input.try_parse(ArcSize::parse).ok();
789                        if arc_size.is_some() {
790                            continue;
791                        }
792                    }
793
794                    if rotate.is_none()
795                        && input
796                            .try_parse(|i| i.expect_ident_matching("rotate"))
797                            .is_ok()
798                    {
799                        rotate = Some(Angle::parse(context, input)?);
800                        continue;
801                    }
802                    break;
803                }
804                Self::Arc {
805                    point,
806                    radii,
807                    arc_sweep: arc_sweep.unwrap_or(ArcSweep::Ccw),
808                    arc_size: arc_size.unwrap_or(ArcSize::Small),
809                    rotate: rotate.unwrap_or(Angle::zero()),
810                }
811            },
812        })
813    }
814}
815
816impl Parse for generic::CoordinatePair<LengthPercentage> {
817    fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
818        let x = LengthPercentage::parse(context, input)?;
819        let y = LengthPercentage::parse(context, input)?;
820        Ok(Self::new(x, y))
821    }
822}
823
824impl generic::ControlPoint<Position, LengthPercentage> {
825    /// Parse <control-point> = [ <position> | <relative-control-point> ]
826    fn parse(
827        context: &ParserContext,
828        input: &mut Parser,
829        is_end_point_abs: bool,
830    ) -> Result<Self, ParseError> {
831        use generic::ControlReference;
832        let coord = input.try_parse(|i| generic::CoordinatePair::parse(context, i));
833
834        // Parse <position>
835        if is_end_point_abs && coord.is_err() {
836            let pos = Position::parse(context, input)?;
837            return Ok(Self::Absolute(pos));
838        }
839
840        // Parse <relative-control-point> = <coordinate-pair> [from [ start | end | origin ]]?
841        let coord = coord?;
842        let mut reference = if is_end_point_abs {
843            ControlReference::Origin
844        } else {
845            ControlReference::Start
846        };
847        if input.try_parse(|i| i.expect_ident_matching("from")).is_ok() {
848            reference = ControlReference::parse(input)?;
849        }
850
851        Ok(Self::Relative(generic::RelativeControlPoint {
852            coord,
853            reference,
854        }))
855    }
856}
857
858impl Parse for generic::CommandEndPoint<Position, LengthPercentage> {
859    /// Parse <command-end-point> = to <position> | by <coordinate-pair>
860    fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
861        if ByTo::parse(input)?.is_abs() {
862            Self::parse_endpoint_as_abs(context, input)
863        } else {
864            let point = generic::CoordinatePair::parse(context, input)?;
865            Ok(Self::ByCoordinate(point))
866        }
867    }
868}
869
870impl generic::CommandEndPoint<Position, LengthPercentage> {
871    /// Parse <command-end-point> = to <position>
872    fn parse_endpoint_as_abs(
873        context: &ParserContext,
874        input: &mut Parser,
875    ) -> Result<Self, ParseError> {
876        let point = Position::parse(context, input)?;
877        Ok(generic::CommandEndPoint::ToPosition(point))
878    }
879}
880
881impl generic::AxisEndPoint<LengthPercentage> {
882    /// Parse <horizontal-line-command>
883    pub fn parse_hline(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
884        use cssparser::Token;
885        use generic::{AxisPosition, AxisPositionKeyword};
886
887        // If the command is relative, parse for <length-percentage> only.
888        if !ByTo::parse(input)?.is_abs() {
889            return Ok(Self::ByCoordinate(LengthPercentage::parse(context, input)?));
890        }
891
892        let x = AxisPosition::parse(context, input)?;
893        if let AxisPosition::Keyword(
894            _word @ (AxisPositionKeyword::Top
895            | AxisPositionKeyword::Bottom
896            | AxisPositionKeyword::YStart
897            | AxisPositionKeyword::YEnd),
898        ) = &x
899        {
900            let _ = Token::Ident(x.to_css_string().into());
901            return Err(ParseError::unexpected_token());
902        }
903        Ok(Self::ToPosition(x))
904    }
905
906    /// Parse <vertical-line-command>
907    pub fn parse_vline(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
908        use cssparser::Token;
909        use generic::{AxisPosition, AxisPositionKeyword};
910
911        // If the command is relative, parse for <length-percentage> only.
912        if !ByTo::parse(input)?.is_abs() {
913            return Ok(Self::ByCoordinate(LengthPercentage::parse(context, input)?));
914        }
915
916        let y = AxisPosition::parse(context, input)?;
917        if let AxisPosition::Keyword(
918            _word @ (AxisPositionKeyword::Left
919            | AxisPositionKeyword::Right
920            | AxisPositionKeyword::XStart
921            | AxisPositionKeyword::XEnd),
922        ) = &y
923        {
924            // Return an error if we parsed a different keyword.
925            let _ = Token::Ident(y.to_css_string().into());
926            return Err(ParseError::unexpected_token());
927        }
928        Ok(Self::ToPosition(y))
929    }
930}
931
932impl ToComputedValue for generic::AxisPosition<LengthPercentage> {
933    type ComputedValue = generic::AxisPosition<ComputedLengthPercentage>;
934
935    fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
936        match self {
937            Self::LengthPercent(lp) => {
938                Self::ComputedValue::LengthPercent(lp.to_computed_value(context))
939            },
940            Self::Keyword(word) => {
941                let lp =
942                    LengthPercentage::Percentage(NoCalcPercentage::new(word.as_percentage().0));
943                Self::ComputedValue::LengthPercent(lp.to_computed_value(context))
944            },
945        }
946    }
947
948    fn from_computed_value(computed: &Self::ComputedValue) -> Self {
949        match computed {
950            Self::ComputedValue::LengthPercent(lp) => {
951                Self::LengthPercent(LengthPercentage::from_computed_value(lp))
952            },
953            _ => unreachable!("Invalid state: computed value cannot be a keyword."),
954        }
955    }
956}
957
958impl ToComputedValue for generic::AxisPosition<CSSFloat> {
959    type ComputedValue = Self;
960
961    fn to_computed_value(&self, _context: &Context) -> Self {
962        *self
963    }
964
965    fn from_computed_value(computed: &Self) -> Self {
966        *computed
967    }
968}
969
970/// This determines whether the command is absolutely or relatively positioned.
971/// https://drafts.csswg.org/css-shapes-1/#typedef-shape-command-end-point
972#[derive(Clone, Copy, Debug, Parse, PartialEq)]
973enum ByTo {
974    /// Command is relative to the command’s starting point.
975    By,
976    /// Command is relative to the top-left corner of the reference box.
977    To,
978}
979
980impl ByTo {
981    /// Return true if it is absolute, i.e. it is To.
982    #[inline]
983    pub fn is_abs(&self) -> bool {
984        matches!(self, ByTo::To)
985    }
986}