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