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