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