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