Bug 1823463 - Support shape() for clip-path property in style. r=emilio

Implement the style part for shape(). Besides, update some issues in the
test file, e.g. avoid using viewport height so we get the fixed result
on different devices.

I will refactor `PathCommand` to let it be a specialization of
`GenericShapeCommand` in the following path.

Differential Revision: https://phabricator.services.mozilla.com/D202882
This commit is contained in:
Boris Chiou 2024-03-18 21:20:28 +00:00
parent bf0867146e
commit b9d9282f7f
16 changed files with 725 additions and 1103 deletions

View File

@ -731,6 +731,9 @@ already_AddRefed<gfx::Path> MotionPathUtils::BuildPath(
// building its gfx::Path directly by its SVGPathData without other
// reference. https://github.com/w3c/fxtf-drafts/issues/504
return BuildSVGPath(aBasicShape.AsPath().path, aPathBuilder);
case StyleBasicShape::Tag::Shape:
// TODO: Bug 1884424. Suport shape() for offset-path.
return nullptr;
}
return nullptr;

View File

@ -2508,6 +2508,7 @@ nsFloatManager::ShapeInfo::CreateBasicShape(const StyleBasicShape& aBasicShape,
return CreateInset(aBasicShape, aShapeMargin, aFrame, aShapeBoxRect, aWM,
aContainerSize);
case StyleBasicShape::Tag::Path:
case StyleBasicShape::Tag::Shape:
MOZ_ASSERT_UNREACHABLE("Unsupported basic shape");
}
return nullptr;

View File

@ -1,6 +1,7 @@
[DEFAULT]
prefs = [
"layout.css.basic-shape-rect.enabled=true",
"layout.css.basic-shape-shape.enabled=true",
"layout.css.basic-shape-xywh.enabled=true",
"layout.css.properties-and-values.enabled=true",
"layout.css.transition-behavior.enabled=true",

View File

@ -217,10 +217,13 @@ function do_test() {
ok(testValues(values, expected), "property box-shadow's values");
// Regression test for bug 1255379.
var shapeFunction = [ "close", "evenodd", "nonzero", "by", "to", "cw", "ccw",
"small", "large" ];
var expected = [ "inherit", "initial", "unset", "revert", "revert-layer",
"none", "url", "polygon", "circle", "ellipse", "inset",
"path", "rect", "xywh", "fill-box", "stroke-box", "view-box",
"margin-box", "border-box", "padding-box", "content-box" ];
"path", "rect", "xywh", "fill-box", "stroke-box",
"view-box", "margin-box", "border-box", "padding-box",
"content-box", ...shapeFunction ];
var values = InspectorUtils.getCSSValuesForProperty("clip-path");
ok(testValues(values, expected), "property clip-path's values");

View File

@ -515,6 +515,9 @@ cbindgen-types = [
{ gecko = "StyleBasicShape", servo = "crate::values::computed::basic_shape::BasicShape" },
{ gecko = "StyleGenericInsetRect", servo = "crate::values::generics::basic_shape::InsetRect" },
{ gecko = "StyleInsetRect", servo = "crate::values::computed::basic_shape::InsetRect" },
{ gecko = "StyleShape", servo = "crate::values::computed::basic_shape::Shape" },
{ gecko = "StyleShapeCommand", servo = "crate::values::computed::basic_shape::ShapeCommand" },
{ gecko = "StyleGenericShapeCommand", servo = "crate::values::generics::basic_shape::ShapeCommand" },
{ gecko = "StyleArcSlice", servo = "style_traits::arc_slice::ArcSlice" },
{ gecko = "StyleForgottenArcSlicePtr", servo = "style_traits::arc_slice::ForgottenArcSlicePtr" },
{ gecko = "StyleOwnedSlice", servo = "style_traits::owned_slice::OwnedSlice" },

View File

@ -16,6 +16,7 @@ prefs = [
"layout.css.scroll-driven-animations.enabled=true",
"layout.css.animation-composition.enabled=true",
"layout.css.basic-shape-rect.enabled=true",
"layout.css.basic-shape-shape.enabled=true",
"layout.css.basic-shape-xywh.enabled=true",
"layout.css.transform-box-content-stroke.enabled=true",
"layout.css.transition-behavior.enabled=true",

View File

@ -1035,6 +1035,32 @@ if (IsCSSPropertyPrefEnabled("layout.css.basic-shape-rect.enabled")) {
);
}
var basicShapeShapeValues = [];
if (IsCSSPropertyPrefEnabled("layout.css.basic-shape-shape.enabled")) {
basicShapeShapeValues.push(
"shape(evenodd from 0px 0px, line to 10px 10px)",
"shape(nonzero from 0px 0px, line to 10px 10px)",
"shape(from 0px 0%, line to 10px 10%)",
"shape(from 10px 10px, move by 10px 5px, line by 20px 40%, close)",
"shape(from 10px 10px, hline by 10px, vline to 5rem)",
"shape(from 10px 10px, vline by 5%, hline to 1vw)",
"shape(from 10px 10px, curve to 50px 20px via 10rem 1%)",
"shape(from 10px 10px, smooth to 50px 20px via 10rem 1%)",
"shape(from 10% 1rem, arc to 50px 1pt of 20% cw large rotate 25deg)"
);
// It's fine to include this for properties which don't support shape(),
// e.g. shape-outside, because they must reject these values.
basicShapeInvalidValues.push(
"shape()",
"shape(evenodd, from 0px 0px)",
"shape(from 0px 0px line to 10px 10px)",
"shape(from 0px 0px)",
"shape(close)",
"shape(nonzero, close)"
);
}
if (/* mozGradientsEnabled */ true) {
// Maybe one day :(
// Extend gradient lists with valid/invalid moz-prefixed expressions:
@ -8827,7 +8853,8 @@ var gCSSProperties = {
.concat(basicShapeSVGBoxValues)
.concat(basicShapeOtherValues)
.concat(basicShapeOtherValuesWithFillRule)
.concat(basicShapeXywhRectValues),
.concat(basicShapeXywhRectValues)
.concat(basicShapeShapeValues),
invalid_values: [
"path(nonzero)",
"path(abs, 'M 10 10 L 10 10 z')",

View File

@ -123,6 +123,9 @@ already_AddRefed<Path> CSSClipPathInstance::CreateClipPath(
return CreateClipPathInset(aDrawTarget, r);
case StyleBasicShape::Tag::Path:
return CreateClipPathPath(aDrawTarget, r);
case StyleBasicShape::Tag::Shape:
// TODO: Support shape() in this patch series.
return nullptr;
default:
MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE("Unexpected shape type");
}

View File

@ -8477,6 +8477,13 @@
mirror: always
rust: true
# Is support for shape() enabled?
- name: layout.css.basic-shape-shape.enabled
type: RelaxedAtomicBool
value: @IS_NIGHTLY_BUILD@
mirror: always
rust: true
# Is support for xywh() enabled?
- name: layout.css.basic-shape-xywh.enabled
type: RelaxedAtomicBool

View File

@ -7,6 +7,7 @@
//!
//! [basic-shape]: https://drafts.csswg.org/css-shapes/#typedef-basic-shape
use crate::values::computed::angle::Angle;
use crate::values::computed::url::ComputedUrl;
use crate::values::computed::{Image, LengthPercentage, NonNegativeLengthPercentage, Position};
use crate::values::generics::basic_shape as generic;
@ -21,8 +22,13 @@ pub type ClipPath = generic::GenericClipPath<BasicShape, ComputedUrl>;
pub type ShapeOutside = generic::GenericShapeOutside<BasicShape, Image>;
/// A computed basic shape.
pub type BasicShape =
generic::GenericBasicShape<Position, LengthPercentage, NonNegativeLengthPercentage, InsetRect>;
pub type BasicShape = generic::GenericBasicShape<
Angle,
Position,
LengthPercentage,
NonNegativeLengthPercentage,
InsetRect,
>;
/// The computed value of `inset()`.
pub type InsetRect = generic::GenericInsetRect<LengthPercentage, NonNegativeLengthPercentage>;
@ -35,3 +41,9 @@ pub type Ellipse = generic::Ellipse<Position, NonNegativeLengthPercentage>;
/// The computed value of `ShapeRadius`.
pub type ShapeRadius = generic::GenericShapeRadius<NonNegativeLengthPercentage>;
/// The computed value of `shape()`.
pub type Shape = generic::Shape<Angle, LengthPercentage>;
/// The computed value of `ShapeCommand`.
pub type ShapeCommand = generic::GenericShapeCommand<Angle, LengthPercentage>;

View File

@ -181,8 +181,13 @@ pub use self::GenericShapeOutside as ShapeOutside;
ToShmem,
)]
#[repr(C, u8)]
pub enum GenericBasicShape<Position, LengthPercentage, NonNegativeLengthPercentage, BasicShapeRect>
{
pub enum GenericBasicShape<
Angle,
Position,
LengthPercentage,
NonNegativeLengthPercentage,
BasicShapeRect,
> {
/// The <basic-shape-rect>.
Rect(BasicShapeRect),
/// Defines a circle with a center and a radius.
@ -201,8 +206,8 @@ pub enum GenericBasicShape<Position, LengthPercentage, NonNegativeLengthPercenta
Polygon(GenericPolygon<LengthPercentage>),
/// Defines a path with SVG path syntax.
Path(Path),
// TODO: Bug 1823463. Add shape().
// https://drafts.csswg.org/css-shapes-2/#shape-function
/// Defines a shape function, which is identical to path(() but it uses the CSS syntax.
Shape(#[css(field_bound)] Shape<Angle, LengthPercentage>),
}
pub use self::GenericBasicShape as BasicShape;
@ -397,9 +402,9 @@ pub enum FillRule {
Evenodd,
}
/// The path function defined in css-shape-2.
/// The path function.
///
/// https://drafts.csswg.org/css-shapes-2/#funcdef-path
/// https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-path
#[derive(
Animate,
Clone,
@ -565,3 +570,454 @@ impl Default for FillRule {
fn is_default<T: Default + PartialEq>(fill: &T) -> bool {
*fill == Default::default()
}
/// The shape function defined in css-shape-2.
/// shape() = shape(<fill-rule>? from <coordinate-pair>, <shape-command>#)
///
/// https://drafts.csswg.org/css-shapes-2/#shape-function
#[derive(
Clone,
Debug,
Deserialize,
MallocSizeOf,
PartialEq,
Serialize,
SpecifiedValueInfo,
ToAnimatedValue,
ToComputedValue,
ToResolvedValue,
ToShmem,
)]
#[repr(C)]
pub struct Shape<Angle, LengthPercentage> {
/// The filling rule for this shape.
pub fill: FillRule,
/// The shape command data. Note that the starting point will be the first command in this
/// slice.
// Note: The first command is always GenericShapeCommand::Move.
pub commands: crate::OwnedSlice<GenericShapeCommand<Angle, LengthPercentage>>,
}
impl<Angle, LengthPercentage> Animate for Shape<Angle, LengthPercentage>
where
Angle: Animate,
LengthPercentage: Animate,
{
fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
if self.fill != other.fill {
return Err(());
}
let commands =
lists::by_computed_value::animate(&self.commands, &other.commands, procedure)?;
Ok(Self {
fill: self.fill,
commands,
})
}
}
impl<Angle, LengthPercentage> ComputeSquaredDistance for Shape<Angle, LengthPercentage>
where
Angle: ComputeSquaredDistance,
LengthPercentage: ComputeSquaredDistance,
{
fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
if self.fill != other.fill {
return Err(());
}
lists::by_computed_value::squared_distance(&self.commands, &other.commands)
}
}
impl<Angle, LengthPercentage> ToCss for Shape<Angle, LengthPercentage>
where
Angle: ToCss + Zero,
LengthPercentage: PartialEq + ToCss,
{
fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
where
W: Write,
{
use style_traits::values::SequenceWriter;
// Per spec, we must have the first move command and at least one following command.
debug_assert!(self.commands.len() > 1);
dest.write_str("shape(")?;
if !is_default(&self.fill) {
self.fill.to_css(dest)?;
dest.write_char(' ')?;
}
dest.write_str("from ")?;
match self.commands[0] {
ShapeCommand::Move {
by_to: _,
ref point,
} => point.to_css(dest)?,
_ => unreachable!("The first command must be move"),
}
dest.write_str(", ")?;
{
let mut writer = SequenceWriter::new(dest, ", ");
for command in self.commands.iter().skip(1) {
writer.item(command)?;
}
}
dest.write_char(')')
}
}
/// This is a more general shape(path) command type, for both shape() and path().
///
/// https://www.w3.org/TR/SVG11/paths.html#PathData
/// https://drafts.csswg.org/css-shapes-2/#shape-function
#[derive(
Animate,
Clone,
ComputeSquaredDistance,
Copy,
Debug,
Deserialize,
MallocSizeOf,
PartialEq,
Serialize,
SpecifiedValueInfo,
ToAnimatedValue,
ToAnimatedZero,
ToComputedValue,
ToResolvedValue,
ToShmem,
)]
#[allow(missing_docs)]
#[repr(C, u8)]
pub enum GenericShapeCommand<Angle, LengthPercentage> {
/// The move command.
Move {
by_to: ByTo,
point: CoordinatePair<LengthPercentage>,
},
/// The line command.
Line {
by_to: ByTo,
point: CoordinatePair<LengthPercentage>,
},
/// The hline command.
HLine { by_to: ByTo, x: LengthPercentage },
/// The vline command.
VLine { by_to: ByTo, y: LengthPercentage },
/// The cubic Bézier curve command.
CubicCurve {
by_to: ByTo,
point: CoordinatePair<LengthPercentage>,
control1: CoordinatePair<LengthPercentage>,
control2: CoordinatePair<LengthPercentage>,
},
/// The quadratic Bézier curve command.
QuadCurve {
by_to: ByTo,
point: CoordinatePair<LengthPercentage>,
control1: CoordinatePair<LengthPercentage>,
},
/// The smooth command.
SmoothCubic {
by_to: ByTo,
point: CoordinatePair<LengthPercentage>,
control2: CoordinatePair<LengthPercentage>,
},
/// The smooth quadratic Bézier curve command.
SmoothQuad {
by_to: ByTo,
point: CoordinatePair<LengthPercentage>,
},
/// The arc command.
Arc {
by_to: ByTo,
point: CoordinatePair<LengthPercentage>,
radii: CoordinatePair<LengthPercentage>,
arc_sweep: ArcSweep,
arc_size: ArcSize,
rotate: Angle,
},
/// The closepath command.
Close,
}
pub use self::GenericShapeCommand as ShapeCommand;
impl<Angle, LengthPercentage> ToCss for ShapeCommand<Angle, LengthPercentage>
where
Angle: ToCss + Zero,
LengthPercentage: PartialEq + ToCss,
{
fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
where
W: fmt::Write,
{
use self::ShapeCommand::*;
match *self {
Move { by_to, ref point } => {
dest.write_str("move ")?;
by_to.to_css(dest)?;
dest.write_char(' ')?;
point.to_css(dest)
},
Line { by_to, ref point } => {
dest.write_str("line ")?;
by_to.to_css(dest)?;
dest.write_char(' ')?;
point.to_css(dest)
},
HLine { by_to, ref x } => {
dest.write_str("hline ")?;
by_to.to_css(dest)?;
dest.write_char(' ')?;
x.to_css(dest)
},
VLine { by_to, ref y } => {
dest.write_str("vline ")?;
by_to.to_css(dest)?;
dest.write_char(' ')?;
y.to_css(dest)
},
CubicCurve {
by_to,
ref point,
ref control1,
ref control2,
} => {
dest.write_str("curve ")?;
by_to.to_css(dest)?;
dest.write_char(' ')?;
point.to_css(dest)?;
dest.write_str(" via ")?;
control1.to_css(dest)?;
dest.write_char(' ')?;
control2.to_css(dest)
},
QuadCurve {
by_to,
ref point,
ref control1,
} => {
dest.write_str("curve ")?;
by_to.to_css(dest)?;
dest.write_char(' ')?;
point.to_css(dest)?;
dest.write_str(" via ")?;
control1.to_css(dest)
},
SmoothCubic {
by_to,
ref point,
ref control2,
} => {
dest.write_str("smooth ")?;
by_to.to_css(dest)?;
dest.write_char(' ')?;
point.to_css(dest)?;
dest.write_str(" via ")?;
control2.to_css(dest)
},
SmoothQuad { by_to, ref point } => {
dest.write_str("smooth ")?;
by_to.to_css(dest)?;
dest.write_char(' ')?;
point.to_css(dest)
},
Arc {
by_to,
ref point,
ref radii,
arc_sweep,
arc_size,
ref rotate,
} => {
dest.write_str("arc ")?;
by_to.to_css(dest)?;
dest.write_char(' ')?;
point.to_css(dest)?;
dest.write_str(" of ")?;
radii.x.to_css(dest)?;
if radii.x != radii.y {
dest.write_char(' ')?;
radii.y.to_css(dest)?;
}
if matches!(arc_sweep, ArcSweep::Cw) {
dest.write_str(" cw")?;
}
if matches!(arc_size, ArcSize::Large) {
dest.write_str(" large")?;
}
if !rotate.is_zero() {
dest.write_str(" rotate ")?;
rotate.to_css(dest)?;
}
Ok(())
},
Close => dest.write_str("close"),
}
}
}
/// This indicates the command is absolute or relative.
/// https://drafts.csswg.org/css-shapes-2/#typedef-shape-by-to
#[derive(
Animate,
Clone,
ComputeSquaredDistance,
Copy,
Debug,
Deserialize,
MallocSizeOf,
Parse,
PartialEq,
Serialize,
SpecifiedValueInfo,
ToAnimatedValue,
ToAnimatedZero,
ToComputedValue,
ToCss,
ToResolvedValue,
ToShmem,
)]
#[repr(u8)]
pub enum ByTo {
/// This indicates that the <coordinate-pair>s are relative to the commands starting point.
By,
/// This relative to the top-left corner of the reference box.
To,
}
/// Defines a pair of coordinates, representing a rightward and downward offset, respectively, from
/// a specified reference point. Percentages are resolved against the width or height,
/// respectively, of the reference box.
/// https://drafts.csswg.org/css-shapes-2/#typedef-shape-coordinate-pair
#[allow(missing_docs)]
#[derive(
Animate,
Clone,
ComputeSquaredDistance,
Copy,
Debug,
Deserialize,
MallocSizeOf,
PartialEq,
Serialize,
SpecifiedValueInfo,
ToAnimatedValue,
ToAnimatedZero,
ToComputedValue,
ToCss,
ToResolvedValue,
ToShmem,
)]
#[repr(C)]
pub struct CoordinatePair<LengthPercentage> {
x: LengthPercentage,
y: LengthPercentage,
}
impl<LengthPercentage> CoordinatePair<LengthPercentage> {
/// Create a CoordinatePair.
#[inline]
pub fn new(x: LengthPercentage, y: LengthPercentage) -> Self {
Self { x, y }
}
}
/// This indicates that the arc that is traced around the ellipse clockwise or counter-clockwise
/// from the center.
/// https://drafts.csswg.org/css-shapes-2/#typedef-shape-arc-sweep
#[derive(
Clone,
Copy,
Debug,
Deserialize,
MallocSizeOf,
Parse,
PartialEq,
Serialize,
SpecifiedValueInfo,
ToAnimatedValue,
ToAnimatedZero,
ToComputedValue,
ToCss,
ToResolvedValue,
ToShmem,
)]
#[repr(u8)]
pub enum ArcSweep {
/// Counter-clockwise. The default value. (This also represents 0 in the svg path.)
Ccw = 0,
/// Clockwise. (This also represents 1 in the svg path.)
Cw = 1,
}
impl Animate for ArcSweep {
fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
// If an arc command has different <arc-sweep> between its starting and ending list, then
// the interpolated result uses cw for any progress value between 0 and 1.
(*self as i32)
.animate(&(*other as i32), procedure)
.map(|v| if v > 0 { ArcSweep::Cw } else { ArcSweep::Ccw })
}
}
impl ComputeSquaredDistance for ArcSweep {
fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
(*self as i32).compute_squared_distance(&(*other as i32))
}
}
/// This indicates that the larger or smaller, respectively, of the two possible arcs must be
/// chosen.
/// https://drafts.csswg.org/css-shapes-2/#typedef-shape-arc-size
#[derive(
Clone,
Copy,
Debug,
Deserialize,
MallocSizeOf,
Parse,
PartialEq,
Serialize,
SpecifiedValueInfo,
ToAnimatedValue,
ToAnimatedZero,
ToComputedValue,
ToCss,
ToResolvedValue,
ToShmem,
)]
#[repr(u8)]
pub enum ArcSize {
/// Choose the small one. The default value. (This also represents 0 in the svg path.)
Small = 0,
/// Choose the large one. (This also represents 1 in the svg path.)
Large = 1,
}
impl Animate for ArcSize {
fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
// If it has different <arc-size> keywords, then the interpolated result uses large for any
// progress value between 0 and 1.
(*self as i32)
.animate(&(*other as i32), procedure)
.map(|v| {
if v > 0 {
ArcSize::Large
} else {
ArcSize::Small
}
})
}
}
impl ComputeSquaredDistance for ArcSize {
fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
(*self as i32).compute_squared_distance(&(*other as i32))
}
}

View File

@ -14,6 +14,7 @@ use crate::values::generics::basic_shape as generic;
use crate::values::generics::basic_shape::{Path, PolygonCoord};
use crate::values::generics::position::{GenericPosition, GenericPositionOrAuto};
use crate::values::generics::rect::Rect;
use crate::values::specified::angle::Angle;
use crate::values::specified::border::BorderRadius;
use crate::values::specified::image::Image;
use crate::values::specified::length::LengthPercentageOrAuto;
@ -40,6 +41,7 @@ pub type ShapePosition = GenericPosition<LengthPercentage, LengthPercentage>;
/// A specified basic shape.
pub type BasicShape = generic::GenericBasicShape<
Angle,
ShapePosition,
LengthPercentage,
NonNegativeLengthPercentage,
@ -61,6 +63,9 @@ pub type ShapeRadius = generic::ShapeRadius<NonNegativeLengthPercentage>;
/// The specified value of `Polygon`.
pub type Polygon = generic::GenericPolygon<LengthPercentage>;
/// The specified value of `ShapeCommand`.
pub type ShapeCommand = generic::GenericShapeCommand<Angle, LengthPercentage>;
/// The specified value of `xywh()`.
/// Defines a rectangle via offsets from the top and left edge of the reference box, and a
/// specified width and height.
@ -168,8 +173,8 @@ bitflags! {
const POLYGON = 1 << 5;
/// path().
const PATH = 1 << 6;
// TODO: Bug 1823463. Add shape().
// const SHAPE = 1 << 7;
/// shape().
const SHAPE = 1 << 7;
/// All flags.
const ALL =
@ -248,7 +253,9 @@ impl Parse for ClipPath {
input,
ClipPath::Shape,
ClipPath::Box,
AllowedBasicShapes::ALL,
// TODO: Bug 1884424. We will merge SHAPE into ALL when supporting shape() on
// offset-path.
AllowedBasicShapes::ALL | AllowedBasicShapes::SHAPE,
)
}
}
@ -331,6 +338,13 @@ impl BasicShape {
"path" if flags.contains(AllowedBasicShapes::PATH) => {
Path::parse_function_arguments(i, shape_type).map(BasicShape::Path)
},
"shape"
if flags.contains(AllowedBasicShapes::SHAPE)
&& static_prefs::pref!("layout.css.basic-shape-shape.enabled") =>
{
generic::Shape::parse_function_arguments(context, i, shape_type)
.map(BasicShape::Shape)
},
_ => Err(location
.new_custom_error(StyleParseErrorKind::UnexpectedFunction(function.clone()))),
}
@ -490,7 +504,11 @@ impl Ellipse {
}
}
fn parse_fill_rule<'i, 't>(input: &mut Parser<'i, 't>, shape_type: ShapeType) -> FillRule {
fn parse_fill_rule<'i, 't>(
input: &mut Parser<'i, 't>,
shape_type: ShapeType,
expect_comma: bool,
) -> FillRule {
match shape_type {
// Per [1] and [2], we ignore `<fill-rule>` for outline shapes, so always use a default
// value.
@ -508,7 +526,9 @@ fn parse_fill_rule<'i, 't>(input: &mut Parser<'i, 't>, shape_type: ShapeType) ->
ShapeType::Filled => input
.try_parse(|i| -> Result<_, ParseError> {
let fill = FillRule::parse(i)?;
i.expect_comma()?;
if expect_comma {
i.expect_comma()?;
}
Ok(fill)
})
.unwrap_or_default(),
@ -532,7 +552,7 @@ impl Polygon {
input: &mut Parser<'i, 't>,
shape_type: ShapeType,
) -> Result<Self, ParseError<'i>> {
let fill = parse_fill_rule(input, shape_type);
let fill = parse_fill_rule(input, shape_type, true /* has comma */);
let coordinates = input
.parse_comma_separated(|i| {
Ok(PolygonCoord(
@ -554,7 +574,7 @@ impl Path {
) -> Result<Self, ParseError<'i>> {
use crate::values::specified::svg_path::AllowEmpty;
let fill = parse_fill_rule(input, shape_type);
let fill = parse_fill_rule(input, shape_type, true /* has comma */);
let path = SVGPathData::parse(input, AllowEmpty::No)?;
Ok(Path { fill, path })
}
@ -717,3 +737,168 @@ impl ToComputedValue for BasicShapeRect {
Self::Inset(ToComputedValue::from_computed_value(computed))
}
}
impl generic::Shape<Angle, LengthPercentage> {
/// Parse the inner arguments of a `shape` function.
/// shape() = shape(<fill-rule>? from <coordinate-pair>, <shape-command>#)
fn parse_function_arguments<'i, 't>(
context: &ParserContext,
input: &mut Parser<'i, 't>,
shape_type: ShapeType,
) -> Result<Self, ParseError<'i>> {
let fill = parse_fill_rule(input, shape_type, false /* no following comma */);
let mut first = true;
let commands = input.parse_comma_separated(|i| {
if first {
first = false;
// The starting point for the first shape-command. It adds an initial absolute
// moveto to the list of path data commands, with the <coordinate-pair> measured
// from the top-left corner of the reference
i.expect_ident_matching("from")?;
Ok(ShapeCommand::Move {
by_to: generic::ByTo::To,
point: generic::CoordinatePair::parse(context, i)?,
})
} else {
// The further path data commands.
ShapeCommand::parse(context, i)
}
})?;
// We must have one starting point and at least one following <shape-command>.
if commands.len() < 2 {
return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
}
Ok(Self {
fill,
commands: commands.into(),
})
}
}
impl Parse for ShapeCommand {
fn parse<'i, 't>(
context: &ParserContext,
input: &mut Parser<'i, 't>,
) -> Result<Self, ParseError<'i>> {
use crate::values::generics::basic_shape::{ArcSize, ArcSweep, ByTo, CoordinatePair};
// <shape-command> = <move-command> | <line-command> | <hv-line-command> |
// <curve-command> | <smooth-command> | <arc-command> | close
Ok(try_match_ident_ignore_ascii_case! { input,
"close" => Self::Close,
"move" => {
let by_to = ByTo::parse(input)?;
let point = CoordinatePair::parse(context, input)?;
Self::Move { by_to, point }
},
"line" => {
let by_to = ByTo::parse(input)?;
let point = CoordinatePair::parse(context, input)?;
Self::Line { by_to, point }
},
"hline" => {
let by_to = ByTo::parse(input)?;
let x = LengthPercentage::parse(context, input)?;
Self::HLine { by_to, x }
},
"vline" => {
let by_to = ByTo::parse(input)?;
let y = LengthPercentage::parse(context, input)?;
Self::VLine { by_to, y }
},
"curve" => {
let by_to = ByTo::parse(input)?;
let point = CoordinatePair::parse(context, input)?;
input.expect_ident_matching("via")?;
let control1 = CoordinatePair::parse(context, input)?;
match input.try_parse(|i| CoordinatePair::parse(context, i)) {
Ok(control2) => Self::CubicCurve {
by_to,
point,
control1,
control2,
},
Err(_) => Self::QuadCurve {
by_to,
point,
control1,
},
}
},
"smooth" => {
let by_to = ByTo::parse(input)?;
let point = CoordinatePair::parse(context, input)?;
if input.try_parse(|i| i.expect_ident_matching("via")).is_ok() {
let control2 = CoordinatePair::parse(context, input)?;
Self::SmoothCubic {
by_to,
point,
control2,
}
} else {
Self::SmoothQuad { by_to, point }
}
},
"arc" => {
let by_to = ByTo::parse(input)?;
let point = CoordinatePair::parse(context, input)?;
input.expect_ident_matching("of")?;
let rx = LengthPercentage::parse(context, input)?;
let ry = input
.try_parse(|i| LengthPercentage::parse(context, i))
.unwrap_or(rx.clone());
let radii = CoordinatePair::new(rx, ry);
// [<arc-sweep> || <arc-size> || rotate <angle>]?
let mut arc_sweep = None;
let mut arc_size = None;
let mut rotate = None;
loop {
if arc_sweep.is_none() {
arc_sweep = input.try_parse(ArcSweep::parse).ok();
}
if arc_size.is_none() {
arc_size = input.try_parse(ArcSize::parse).ok();
if arc_size.is_some() {
continue;
}
}
if rotate.is_none()
&& input
.try_parse(|i| i.expect_ident_matching("rotate"))
.is_ok()
{
rotate = Some(Angle::parse(context, input)?);
continue;
}
break;
}
Self::Arc {
by_to,
point,
radii,
arc_sweep: arc_sweep.unwrap_or(ArcSweep::Ccw),
arc_size: arc_size.unwrap_or(ArcSize::Small),
rotate: rotate.unwrap_or(Angle::zero()),
}
},
})
}
}
impl Parse for generic::CoordinatePair<LengthPercentage> {
fn parse<'i, 't>(
context: &ParserContext,
input: &mut Parser<'i, 't>,
) -> Result<Self, ParseError<'i>> {
let x = LengthPercentage::parse(context, input)?;
let y = LengthPercentage::parse(context, input)?;
Ok(Self::new(x, y))
}
}

View File

@ -1 +1 @@
prefs: [layout.css.basic-shape-rect.enabled:true, layout.css.basic-shape-xywh.enabled:true]
prefs: [layout.css.basic-shape-rect.enabled:true, layout.css.basic-shape-shape.enabled:true, layout.css.basic-shape-xywh.enabled:true]

View File

@ -1,60 +0,0 @@
[clip-path-shape-parsing.html]
[e.style['clip-path'\] = "shape(from 0px 0px, line to 10px 10px)" should set the property value]
expected: FAIL
[e.style['clip-path'\] = "shape(evenodd from 0px 0px, line to 10px 10px)" should set the property value]
expected: FAIL
[e.style['clip-path'\] = "shape(nonzero from 0px 0px, line to 10px 10px)" should set the property value]
expected: FAIL
[e.style['clip-path'\] = "shape( from 0px 0px, line to 10px 10px )" should set the property value]
expected: FAIL
[e.style['clip-path'\] = "shape(from 1em 50%, line to 10px 10px)" should set the property value]
expected: FAIL
[e.style['clip-path'\] = "shape(EvenOdd from 0px 0Px, CLOSE)" should set the property value]
expected: FAIL
[e.style['clip-path'\] = "shape(from 1ch 50px, line to 10rem 10vh)" should set the property value]
expected: FAIL
[e.style['clip-path'\] = "shape(from 1ch -50px, line to -10% 12px)" should set the property value]
expected: FAIL
[e.style['clip-path'\] = "shape(from 10px 10px, move by 10px 5px, line by 20px 40%, close)" should set the property value]
expected: FAIL
[e.style['clip-path'\] = "shape(from 10px 10px, hline by 10px, vline to 5rem)" should set the property value]
expected: FAIL
[e.style['clip-path'\] = "shape(from 10px 10px, vline by 5%, hline to 1vw)" should set the property value]
expected: FAIL
[e.style['clip-path'\] = "shape(from 10px 10px, curve to 50px 20px via 10rem 1%)" should set the property value]
expected: FAIL
[e.style['clip-path'\] = "shape(from 10px 10px, curve to 50px 20px via 10rem 1px 20vh 1ch)" should set the property value]
expected: FAIL
[e.style['clip-path'\] = "shape(from 10px 10px, curve by 50px 20px via 10rem 1px 20vh 1ch)" should set the property value]
expected: FAIL
[e.style['clip-path'\] = "shape(from 10px 10px, smooth to 50px 20px via 10rem 1%)" should set the property value]
expected: FAIL
[e.style['clip-path'\] = "shape(from 10px 10px, smooth to 50px 1pt)" should set the property value]
expected: FAIL
[e.style['clip-path'\] = "shape(from 10px 10px, arc to 50px 1pt of 10px 10px)" should set the property value]
expected: FAIL
[e.style['clip-path'\] = "shape(from 10px 10px, arc to 50px 1pt of 10px 10px small rotate 0deg)" should set the property value]
expected: FAIL
[e.style['clip-path'\] = "shape(from 10% 1rem, arc to 50px 1pt of 20% cw large rotate 25deg)" should set the property value]
expected: FAIL
[e.style['clip-path'\] = "shape(evenodd from 0px 0px, close)" should set the property value]
expected: FAIL

View File

@ -1,10 +1,10 @@
<!DOCTYPE html>
<meta charset="UTF-8">
<meta charset="UTF-8">
<title>clip-path-interpolation</title>
<link rel="stylesheet" type="text/css" href="/fonts/ahem.css" />
<link rel="help" href="https://drafts.fxtf.org/css-masking-1/#the-clip-path">
<meta name="assert" content="clip-path supports animation">
<link rel="help" href="https://drafts.csswg.org/css-shapes-2/#interpolating-shape">
<meta name="assert" content="clip-path supports animation for shape()">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
@ -149,14 +149,14 @@ test_interpolation({
{at: -0.3, expect: 'shape(from 2% 2px, arc to 18% -12px of 7px 17px ccw small, arc by 12% -2px of 33px 33px rotate -42deg cw large , arc to 25% 20px of 10px 5px ccw small)'},
{at: 0, expect: 'shape(from 5% 5px, arc to 15% -15px of 10px 20px, arc by 15% -5px of 30px cw rotate 30deg large, arc to 25% 20px of 10px 5px small)'},
{at: 0.3, expect: 'shape(from 8% 8px, arc to 12% -18px of 13px 23px ccw small, arc by 18% -8px of 27px 27px rotate 102deg cw large, arc to 25% 20px of 10px 5px ccw small )'},
{at: 0.5, expect: 'shape(from 10% 10px, arc to 10% -20px of 15px 25px ccw small, arc by 20% -10px of 25px rotate 150deg cw small, arc to 25% 20px of 10px 5px cw small)'},
{at: 0.5, expect: 'shape(from 10% 10px, arc to 10% -20px of 15px 25px ccw small, arc by 20% -10px of 25px rotate 150deg cw large, arc to 25% 20px of 10px 5px cw small)'},
{at: 1, expect: 'shape(from 15% 15px, arc to 5% -25px of 20px 30px, arc by 25% -15px of 20px rotate 270deg cw small, arc to 25% 20px of 10px 5px cw small)'},
{at: 1.5, expect: 'shape(from 20% 20px, arc to 0% -30px of 25px 35px ccw small, arc by 30% -20px of 15px rotate 390deg cw small, arc to 25% 20px of 10px 5px cw small)'},
]);
test_interpolation({
property: 'clip-path',
from: 'shape(from 5px -5%, hline to 10px, vline by 10rem, hline by 1vh, close, vline by 3pt)',
from: 'shape(from 5px -5%, hline to 10px, vline by 10rem, hline by 8.25px, close, vline by 3pt)',
to: 'shape(from -5px 5px, hline to 20px, vline by 10%, hline by 1em, close, vline by 6pt)',
}, [
{at: -0.3, expect: 'shape(from 8px calc(-6.5% - 1.5px), hline to 7px, vline by calc(-3% + 208px), hline by 5.92px, close, vline by 2.8px)'},