Paint polygon clip paths

This commit is contained in:
SGavrl
2026-07-31 02:46:58 +00:00
parent 72860049b8
commit 1fc18bd1ad
4 changed files with 540 additions and 48 deletions

View File

@@ -4233,6 +4233,27 @@ mod tests {
);
}
#[test]
fn supports_polygon_clip_path_activates_only_painted_shape_subset() {
let css = "@supports (clip-path:polygon(0 0,100% 0,50% 100%)){\
.transition{height:90px}\
}\
@supports (clip-path:polygon(0 0,100% 0,50% 100%) content-box){\
.unsupported{height:999px}\
}";
let rules = parse_stylesheet_for_viewport(css, (1280.0, 720.0));
assert!(
rules.iter().any(|(selector, declarations)| {
selector == ".transition" && declarations.contains("height:90px")
}),
"the supported polygon branch must enter the cascade: {rules:?}"
);
assert!(
!rules.iter().any(|(selector, _)| selector == ".unsupported"),
"an unpainted geometry-box variant must not pass @supports: {rules:?}"
);
}
#[test]
fn media_breakpoints_support_font_relative_lengths_and_ranges() {
assert!(!media_query_applies_for_viewport(

View File

@@ -235,6 +235,26 @@ impl BorderRadius {
}
}
/// Fill rule for a CSS `clip-path: polygon(...)`.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum ClipPathFillRule {
#[default]
Nonzero,
Evenodd,
}
/// The supported CSS basic-shape clip.
///
/// Coordinates retain their CSS length/percentage unit until paint, where
/// percentages resolve independently against the border box width and height.
/// This mirrors the reference-box resolution used by browser engines and
/// avoids baking responsive polygon geometry into computed style.
#[derive(Debug, Clone, PartialEq)]
pub struct ClipPathPolygon {
pub fill_rule: ClipPathFillRule,
pub points: Vec<(Dimension, Dimension)>,
}
/// The subset of CSS that influences box layout. Expanded in later phases.
#[derive(Debug, Clone, Default)]
pub struct LayoutStyle {
@@ -334,6 +354,9 @@ pub struct LayoutStyle {
/// `border-radius` (uniform; the first value of the shorthand). Rounds the
/// background fill, replaced content, border, and shadow.
pub border_radius: BorderRadius,
/// `clip-path: polygon(...)`, resolved against the final border box at
/// paint time. `None` is the computed `none` value.
pub clip_path: Option<ClipPathPolygon>,
/// RGBA for the paint step. Parsed always (cheap), used only with `paint`.
pub background_color: Option<[u8; 4]>,
/// `linear-gradient(...)` background: (angle in degrees clockwise from 12

View File

@@ -710,6 +710,17 @@ fn paint_laid_dom_scrolled(
Some(r) => r,
None => continue,
};
let clip_path_mask = style.clip_path.as_ref().and_then(|polygon| {
polygon_clip_mask(
pixmap.width(),
pixmap.height(),
polygon,
&rect,
style.font_size.unwrap_or(16.0),
root_font_size,
viewport,
)
});
// Outset box-shadow paints behind this element's own background/border.
// Geometry comes from the full (translate-adjusted) border box; the
@@ -751,7 +762,7 @@ fn paint_laid_dom_scrolled(
&paint,
FillRule::Winding,
Transform::identity(),
None,
clip_path_mask.as_ref(),
);
}
}
@@ -763,6 +774,7 @@ fn paint_laid_dom_scrolled(
&visible_rect,
*center,
stops,
clip_path_mask.as_ref(),
);
}
}
@@ -774,11 +786,19 @@ fn paint_laid_dom_scrolled(
*angle,
*center,
stops,
clip_path_mask.as_ref(),
);
}
if let Some((angle, stops)) = &style.background_gradient {
if let Some(path) = bg_path() {
paint_linear_gradient(&mut pixmap, &path, &visible_rect, *angle, stops);
paint_linear_gradient(
&mut pixmap,
&path,
&visible_rect,
*angle,
stops,
clip_path_mask.as_ref(),
);
}
}
}
@@ -796,6 +816,7 @@ fn paint_laid_dom_scrolled(
style.background_conic_gradient.as_ref(),
style.mask_size,
style.mask_repeat,
clip_path_mask.as_ref(),
&mut pixmap,
image_cache,
);
@@ -832,6 +853,7 @@ fn paint_laid_dom_scrolled(
image_cache,
None,
radius,
clip_path_mask.as_ref(),
);
}
}
@@ -874,7 +896,13 @@ fn paint_laid_dom_scrolled(
paint.set_color(Color::from_rgba8(bc[0], bc[1], bc[2], bc[3]));
paint.anti_alias = true;
let stroke = tiny_skia::Stroke { width: w, ..Default::default() };
pixmap.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
pixmap.stroke_path(
&path,
&paint,
&stroke,
Transform::identity(),
clip_path_mask.as_ref(),
);
}
} else if style.border.top > 0.0 || style.border.right > 0.0 || style.border.bottom > 0.0 || style.border.left > 0.0 {
let bc = style.border_color.or(style.color).unwrap_or([0, 0, 0, 255]);
@@ -905,7 +933,13 @@ fn paint_laid_dom_scrolled(
push_clipped(rect.x, rect.y, style.border.left, rect.height);
}
if let Some(path) = path.finish() {
pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None);
pixmap.fill_path(
&path,
&paint,
FillRule::Winding,
Transform::identity(),
clip_path_mask.as_ref(),
);
}
}
@@ -926,6 +960,7 @@ fn paint_laid_dom_scrolled(
image_cache,
projected_image,
radius,
clip_path_mask.as_ref(),
);
// Fall back when the image itself did not paint, following
// what browsers show for a broken image: a non-empty alt
@@ -2458,7 +2493,14 @@ fn raster_to_pixmap(bytes: &[u8], w: u32, h: u32) -> Option<Pixmap> {
/// 12 o'clock (0 = to top). The gradient line length uses the CSS formula so
/// the stops land where a browser puts them. Positionless stops are spread
/// evenly; positions are clamped monotonic (tiny-skia requires ascending).
fn paint_linear_gradient(pixmap: &mut Pixmap, path: &tiny_skia::Path, rect: &crate::Rect, angle: f32, stops: &[([u8; 4], Option<f32>)]) {
fn paint_linear_gradient(
pixmap: &mut Pixmap,
path: &tiny_skia::Path,
rect: &crate::Rect,
angle: f32,
stops: &[([u8; 4], Option<f32>)],
clip: Option<&tiny_skia::Mask>,
) {
if stops.len() < 2 {
return;
}
@@ -2482,7 +2524,7 @@ fn paint_linear_gradient(pixmap: &mut Pixmap, path: &tiny_skia::Path, rect: &cra
let mut paint = Paint::default();
paint.shader = shader;
paint.anti_alias = true;
pixmap.fill_path(path, &paint, FillRule::Winding, Transform::identity(), None);
pixmap.fill_path(path, &paint, FillRule::Winding, Transform::identity(), clip);
}
}
@@ -2492,6 +2534,7 @@ fn paint_radial_gradient(
rect: &crate::Rect,
center: (f32, f32),
stops: &[([u8; 4], Option<f32>)],
clip: Option<&tiny_skia::Mask>,
) {
if stops.len() < 2 {
return;
@@ -2530,7 +2573,7 @@ fn paint_radial_gradient(
let mut paint = Paint::default();
paint.shader = shader;
paint.anti_alias = true;
pixmap.fill_path(path, &paint, FillRule::Winding, Transform::identity(), None);
pixmap.fill_path(path, &paint, FillRule::Winding, Transform::identity(), clip);
}
}
@@ -2541,6 +2584,7 @@ fn paint_conic_gradient(
angle: f32,
center: (f32, f32),
stops: &[([u8; 4], Option<f32>)],
extra_clip: Option<&tiny_skia::Mask>,
) {
if rect.width <= 0.0 || rect.height <= 0.0 || stops.len() < 2 {
return;
@@ -2564,16 +2608,36 @@ fn paint_conic_gradient(
layer.pixels_mut()[(y * width + x) as usize] = premultiplied(color);
}
}
let clip = if border_radius.0 > 0.5 && border_radius.1 > 0.5 {
rounded_box_clip_mask(
pixmap.width(),
pixmap.height(),
rect,
border_radius,
)
} else {
None
};
let mut clip = extra_clip.cloned();
if border_radius.0 > 0.5 && border_radius.1 > 0.5 {
let path = rounded_rect_path(
rect.x,
rect.y,
rect.width,
rect.height,
border_radius.0,
border_radius.1,
);
match (clip.as_mut(), path) {
(Some(mask), Some(path)) => {
mask.intersect_path(
&path,
FillRule::Winding,
true,
Transform::identity(),
);
}
(None, _) => {
clip = rounded_box_clip_mask(
pixmap.width(),
pixmap.height(),
rect,
border_radius,
);
}
_ => {}
}
}
pixmap.draw_pixmap(
rect.x.floor() as i32,
rect.y.floor() as i32,
@@ -2920,6 +2984,17 @@ fn paint_in_flow_generated_box(
}
let radius = style.border_radius.resolve(rect.width, rect.height);
let has_radius = radius.0 > 0.5 && radius.1 > 0.5;
let clip_path_mask = style.clip_path.as_ref().and_then(|polygon| {
polygon_clip_mask(
pixmap.width(),
pixmap.height(),
polygon,
&rect,
style.font_size.unwrap_or(16.0),
root_font_size,
viewport,
)
});
let path = if has_radius {
rounded_rect_path(
visible.x,
@@ -2947,17 +3022,39 @@ fn paint_in_flow_generated_box(
&paint,
FillRule::Winding,
Transform::identity(),
None,
clip_path_mask.as_ref(),
);
}
if let Some((center, stops)) = &style.background_radial_gradient {
paint_radial_gradient(pixmap, &path, &rect, *center, stops);
paint_radial_gradient(
pixmap,
&path,
&rect,
*center,
stops,
clip_path_mask.as_ref(),
);
}
if let Some((angle, center, stops)) = &style.background_conic_gradient {
paint_conic_gradient(pixmap, &rect, radius, *angle, *center, stops);
paint_conic_gradient(
pixmap,
&rect,
radius,
*angle,
*center,
stops,
clip_path_mask.as_ref(),
);
}
if let Some((angle, stops)) = &style.background_gradient {
paint_linear_gradient(pixmap, &path, &rect, *angle, stops);
paint_linear_gradient(
pixmap,
&path,
&rect,
*angle,
stops,
clip_path_mask.as_ref(),
);
}
}
if let Some(mask_url) = &style.mask_image {
@@ -2976,6 +3073,7 @@ fn paint_in_flow_generated_box(
style.background_conic_gradient.as_ref(),
style.mask_size,
style.mask_repeat,
clip_path_mask.as_ref(),
pixmap,
image_cache,
);
@@ -3003,6 +3101,7 @@ fn paint_in_flow_generated_box(
image_cache,
None,
radius,
clip_path_mask.as_ref(),
);
}
}
@@ -3065,7 +3164,7 @@ fn paint_in_flow_generated_box(
&border_paint,
FillRule::Winding,
Transform::identity(),
None,
clip_path_mask.as_ref(),
);
}
}
@@ -3122,6 +3221,17 @@ fn paint_positioned_pseudo(
let Some(visible) = visible else { return };
let radius = style.border_radius.resolve(rect.width, rect.height);
let has_radius = radius.0 > 0.5 && radius.1 > 0.5;
let clip_path_mask = style.clip_path.as_ref().and_then(|polygon| {
polygon_clip_mask(
pixmap.width(),
pixmap.height(),
polygon,
&rect,
em,
root_font_size,
viewport,
)
});
let path = if has_radius {
rounded_rect_path(
visible.x,
@@ -3148,17 +3258,39 @@ fn paint_positioned_pseudo(
&paint,
FillRule::Winding,
Transform::identity(),
None,
clip_path_mask.as_ref(),
);
}
if let Some((center, stops)) = &style.background_radial_gradient {
paint_radial_gradient(pixmap, &path, &rect, *center, stops);
paint_radial_gradient(
pixmap,
&path,
&rect,
*center,
stops,
clip_path_mask.as_ref(),
);
}
if let Some((angle, center, stops)) = &style.background_conic_gradient {
paint_conic_gradient(pixmap, &rect, radius, *angle, *center, stops);
paint_conic_gradient(
pixmap,
&rect,
radius,
*angle,
*center,
stops,
clip_path_mask.as_ref(),
);
}
if let Some((angle, stops)) = &style.background_gradient {
paint_linear_gradient(pixmap, &path, &rect, *angle, stops);
paint_linear_gradient(
pixmap,
&path,
&rect,
*angle,
stops,
clip_path_mask.as_ref(),
);
}
}
if let Some(mask_url) = &style.mask_image {
@@ -3177,6 +3309,7 @@ fn paint_positioned_pseudo(
style.background_conic_gradient.as_ref(),
style.mask_size,
style.mask_repeat,
clip_path_mask.as_ref(),
pixmap,
image_cache,
);
@@ -3204,6 +3337,7 @@ fn paint_positioned_pseudo(
image_cache,
None,
radius,
clip_path_mask.as_ref(),
);
}
}
@@ -3579,6 +3713,7 @@ fn paint_image(
cache: &mut RenderResourceCache,
transform: Option<ImageAffine>,
clip_radius: (f32, f32),
extra_clip: Option<&tiny_skia::Mask>,
) -> bool {
if rect.width <= 0.0 || rect.height <= 0.0 {
return false;
@@ -3617,21 +3752,53 @@ fn paint_image(
// the image past the box, and an ancestor clip can cut into the box
// itself. Only the fully-inside case takes the unmasked fast path.
let has_radius = clip_radius.0 > 0.5 && clip_radius.1 > 0.5;
let clip = if has_radius
let needs_box_clip = has_radius
|| dest.width > visible_rect.width + 0.5
|| dest.height > visible_rect.height + 0.5
|| dest.x < visible_rect.x - 0.5
|| dest.y < visible_rect.y - 0.5
{
rounded_box_clip_mask(
pixmap.width(),
pixmap.height(),
visible_rect,
clip_radius,
)
} else {
None
};
|| dest.y < visible_rect.y - 0.5;
let mut clip = extra_clip.cloned();
if needs_box_clip {
let path = if has_radius {
rounded_rect_path(
visible_rect.x,
visible_rect.y,
visible_rect.width,
visible_rect.height,
clip_radius.0,
clip_radius.1,
)
} else {
Rect::from_xywh(
visible_rect.x,
visible_rect.y,
visible_rect.width,
visible_rect.height,
)
.and_then(|rect| {
let mut builder = PathBuilder::new();
builder.push_rect(rect);
builder.finish()
})
};
match (clip.as_mut(), path) {
(Some(mask), Some(path)) => mask.intersect_path(
&path,
FillRule::Winding,
true,
Transform::identity(),
),
(None, _) => {
clip = rounded_box_clip_mask(
pixmap.width(),
pixmap.height(),
visible_rect,
clip_radius,
);
}
_ => {}
}
}
pixmap.draw_pixmap(
dest.x as i32,
dest.y as i32,
@@ -3731,6 +3898,57 @@ fn rounded_box_clip_mask(
Some(mask)
}
/// Resolve a CSS polygon against its border-box reference and rasterize it as
/// a full-surface alpha clip. Gecko's basic-shape path builder likewise
/// resolves each x percentage against the reference width and each y
/// percentage against its height before closing the path.
fn polygon_clip_mask(
pw: u32,
ph: u32,
polygon: &crate::ClipPathPolygon,
rect: &crate::Rect,
em: f32,
rem: f32,
viewport: (f32, f32),
) -> Option<tiny_skia::Mask> {
let resolve = |coordinate: crate::Dimension, basis: f32| {
match coordinate.resolve(
em,
rem,
viewport.0 / 100.0,
viewport.1 / 100.0,
) {
crate::Dimension::Px(value) => Some(value),
crate::Dimension::Percent(value) => Some(value * basis),
_ => None,
}
};
let mut builder = PathBuilder::new();
for (index, &(x, y)) in polygon.points.iter().enumerate() {
let x = rect.x + resolve(x, rect.width)?;
let y = rect.y + resolve(y, rect.height)?;
if index == 0 {
builder.move_to(x, y);
} else {
builder.line_to(x, y);
}
}
builder.close();
let path = builder.finish();
let fill_rule = match polygon.fill_rule {
crate::ClipPathFillRule::Nonzero => FillRule::Winding,
crate::ClipPathFillRule::Evenodd => FillRule::EvenOdd,
};
let mut mask = tiny_skia::Mask::new(pw, ph)?;
// Degenerate polygons are valid CSS basic shapes but have no enclosed
// area. Keep the newly zeroed mask in that case: treating it as "no mask"
// would incorrectly reveal the whole element.
if let Some(path) = path {
mask.fill_path(&path, fill_rule, true, Transform::identity());
}
Some(mask)
}
/// Sniff SVG content: either an XML/SVG prolog, or a bare `<svg` root tag
/// (both are valid, and image responses commonly omit the XML declaration).
fn is_svg(bytes: &[u8]) -> bool {
@@ -4433,6 +4651,7 @@ fn paint_mask(
conic_gradient: Option<&(f32, (f32, f32), Vec<([u8; 4], Option<f32>)>)>,
mask_size: Option<(f32, f32)>,
mask_repeat: Option<(bool, bool)>,
extra_clip: Option<&tiny_skia::Mask>,
pixmap: &mut Pixmap,
cache: &mut RenderResourceCache,
) -> bool {
@@ -4507,16 +4726,34 @@ fn paint_mask(
premultiplied(color);
}
}
let clip = if border_radius.0 > 0.5 && border_radius.1 > 0.5 {
rounded_box_clip_mask(
pixmap.width(),
pixmap.height(),
rect,
border_radius,
)
} else {
None
};
let mut clip = extra_clip.cloned();
if border_radius.0 > 0.5 && border_radius.1 > 0.5 {
let path = rounded_rect_path(
rect.x,
rect.y,
rect.width,
rect.height,
border_radius.0,
border_radius.1,
);
match (clip.as_mut(), path) {
(Some(mask), Some(path)) => mask.intersect_path(
&path,
FillRule::Winding,
true,
Transform::identity(),
),
(None, _) => {
clip = rounded_box_clip_mask(
pixmap.width(),
pixmap.height(),
rect,
border_radius,
);
}
_ => {}
}
}
pixmap.draw_pixmap(
rect.x.floor() as i32,
rect.y.floor() as i32,
@@ -5044,6 +5281,96 @@ mod tests {
);
}
#[test]
fn polygon_clip_path_paints_responsive_geometry_on_elements_and_pseudos() {
let tree = parse_html(
r#"<html><head><style>
html, body { margin:0 }
@supports (clip-path:polygon(0 0,100% 0,50% 100%)) {
.triangle, #in-flow::before, #positioned::after {
clip-path:polygon(0 0,100% 0,50% 100%);
}
}
.triangle { position:absolute; left:0; top:0;
width:80px; height:80px; background:#f00 }
#in-flow { position:absolute; left:100px; top:0 }
#in-flow::before { content:""; display:block;
width:80px; height:80px; background:#0a0 }
#positioned { position:absolute; left:200px; top:0;
width:80px; height:80px }
#positioned::after { content:""; position:absolute; inset:0;
background:#00f }
</style></head><body>
<div class="triangle"></div>
<div id="in-flow"></div>
<div id="positioned"></div>
</body></html>"#,
);
let pixmap = paint_dom(&tree, (300.0, 100.0), None).expect("pixmap");
for (name, left, channel) in [
("ordinary element", 0, 0),
("in-flow pseudo", 100, 1),
("positioned pseudo", 200, 2),
] {
let inside = pixmap.pixel(left + 40, 65).expect("inside pixel");
let colored = match channel {
0 => inside.red() > 180 && inside.green() < 80 && inside.blue() < 80,
1 => inside.green() > 100 && inside.red() < 80 && inside.blue() < 80,
_ => inside.blue() > 180 && inside.red() < 80 && inside.green() < 80,
};
assert!(colored, "{name} must paint inside its percentage polygon: {inside:?}");
let outside = pixmap.pixel(left + 4, 65).expect("outside pixel");
assert_eq!(
(outside.red(), outside.green(), outside.blue()),
(255, 255, 255),
"{name} must be clipped outside its triangle: {outside:?}"
);
}
}
#[test]
fn polygon_clip_path_honors_evenodd_fill_rule() {
let tree = parse_html(
r#"<html><body style="margin:0">
<div style="width:100px;height:100px;background:#e00;
clip-path:polygon(evenodd,
0 0,100% 0,100% 100%,0 100%,0 25%,
75% 25%,75% 75%,25% 75%,25% 25%,0 25%)"></div>
</body></html>"#,
);
let pixmap = paint_dom(&tree, (120.0, 120.0), None).expect("pixmap");
let shell = pixmap.pixel(10, 50).expect("outer shell");
assert!(
shell.red() > 180 && shell.green() < 80,
"the outer contour must remain painted: {shell:?}"
);
let hole = pixmap.pixel(50, 50).expect("inner hole");
assert_eq!(
(hole.red(), hole.green(), hole.blue()),
(255, 255, 255),
"evenodd must cut the inner winding out of the clip: {hole:?}"
);
}
#[test]
fn degenerate_polygon_clip_path_clips_the_element_away() {
let tree = parse_html(
r#"<html><body style="margin:0">
<div style="width:80px;height:80px;background:red;
clip-path:polygon(20px 20px)"></div>
</body></html>"#,
);
let pixmap = paint_dom(&tree, (100.0, 100.0), None).expect("pixmap");
for (x, y) in [(10, 10), (20, 20), (40, 40)] {
let pixel = pixmap.pixel(x, y).expect("pixel");
assert_eq!(
(pixel.red(), pixel.green(), pixel.blue()),
(255, 255, 255),
"a zero-area polygon must not fall back to an unclipped box: {pixel:?}"
);
}
}
#[test]
fn paints_generated_style_images_and_sizes_content_url_as_replaced_content() {
let tree = parse_html(

View File

@@ -755,6 +755,17 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
style.border_radius = crate::BorderRadius::pixels(length);
}
}
"clip-path" | "-webkit-clip-path" => {
let lower = value.trim().to_ascii_lowercase();
if matches!(
lower.as_str(),
"none" | "initial" | "unset" | "revert" | "revert-layer"
) {
style.clip_path = None;
} else if let Some(polygon) = parse_clip_path_polygon(value) {
style.clip_path = Some(polygon);
}
}
"border" => {
if value.split_whitespace().any(|token| {
token.eq_ignore_ascii_case("none") || token.eq_ignore_ascii_case("hidden")
@@ -1521,6 +1532,8 @@ pub(crate) fn supports_declaration(name: &str, value: &str) -> bool {
| "padding-block-start"
| "padding-block-end"
| "border-radius"
| "clip-path"
| "-webkit-clip-path"
| "border"
| "border-width"
| "border-top-width"
@@ -1703,6 +1716,9 @@ pub(crate) fn supports_declaration(name: &str, value: &str) -> bool {
"animation-delay" => parse_animation_time_ms(value).is_some(),
"animation-iteration-count" => parse_animation_iteration_count(value).is_some(),
"animation-direction" => parse_animation_direction(value).is_some(),
"clip-path" | "-webkit-clip-path" => {
value.eq_ignore_ascii_case("none") || parse_clip_path_polygon(value).is_some()
}
"animation-fill-mode" => parse_animation_fill_mode(value).is_some(),
"animation-play-state" => parse_animation_play_state(value).is_some(),
"float" => matches!(
@@ -4103,6 +4119,71 @@ fn dimension_value(tok: &str) -> crate::Dimension {
Dimension::Auto
}
/// Parse the currently painted basic-shape subset of `clip-path`.
///
/// The default (and only supported) geometry box is `border-box`. Rejecting
/// the other reference boxes here keeps `@supports` aligned with the geometry
/// we actually paint. Functional coordinates are likewise rejected until
/// their unresolved math can be retained through computed style.
fn parse_clip_path_polygon(value: &str) -> Option<crate::ClipPathPolygon> {
let value = value.trim();
let lower = value.to_ascii_lowercase();
if !lower.starts_with("polygon(") {
return None;
}
let close = value.rfind(')')?;
let suffix = value[close + 1..].trim();
if !suffix.is_empty() && !suffix.eq_ignore_ascii_case("border-box") {
return None;
}
let inner = value["polygon(".len()..close].trim();
let mut components = split_top_level(inner, ',');
let mut fill_rule = crate::ClipPathFillRule::Nonzero;
if let Some(first) = components.first().map(|component| component.trim()) {
if first.eq_ignore_ascii_case("evenodd") {
fill_rule = crate::ClipPathFillRule::Evenodd;
components.remove(0);
} else if first.eq_ignore_ascii_case("nonzero") {
components.remove(0);
}
}
if components.is_empty() {
return None;
}
let mut points = Vec::with_capacity(components.len());
for component in components {
let coordinates = split_ws_paren(component);
let [x, y] = coordinates.as_slice() else {
return None;
};
if x.contains('(') || y.contains('(') {
return None;
}
let x = dimension_value(x);
let y = dimension_value(y);
if matches!(x, crate::Dimension::Auto) || matches!(y, crate::Dimension::Auto) {
return None;
}
let finite = |coordinate: crate::Dimension| match coordinate {
crate::Dimension::Auto => false,
crate::Dimension::Px(value)
| crate::Dimension::Percent(value)
| crate::Dimension::Em(value)
| crate::Dimension::Ex(value)
| crate::Dimension::Rem(value)
| crate::Dimension::Vw(value)
| crate::Dimension::Vh(value)
| crate::Dimension::Vmin(value)
| crate::Dimension::Vmax(value) => value.is_finite(),
};
if !finite(x) || !finite(y) {
return None;
}
points.push((x, y));
}
Some(crate::ClipPathPolygon { fill_rule, points })
}
/// Parse every length token in a value as px (for box shorthands).
fn edges(value: &str) -> Option<Edges> {
let dims: Vec<f32> = value.split_whitespace().filter_map(px_value).collect();
@@ -4879,6 +4960,46 @@ fn edges_from(dims: Vec<f32>) -> Option<Edges> {
mod tests {
use super::*;
#[test]
fn clip_path_polygon_supports_only_geometry_it_can_resolve() {
assert!(supports_declaration(
"clip-path",
"polygon(0 0, 100% 0, 50% 90px)"
));
assert!(supports_declaration(
"-webkit-clip-path",
"polygon(evenodd, 0 0, 10rem 0, 10rem 10vh) border-box"
));
for unsupported in [
"polygon(0 0, 100% 0, 50% 100%) content-box",
"content-box polygon(0 0, 100% 0, 50% 100%)",
"polygon(0, 100% 0)",
"polygon(evenodd 0 0, 100% 0)",
"polygon(0 0, calc(100% - 1px) 0, 0 100%)",
"circle(50%)",
] {
assert!(
!supports_declaration("clip-path", unsupported),
"@supports must not advertise unpainted clip geometry: {unsupported}"
);
}
let style = compute_style(
"div",
Some("clip-path:polygon(evenodd, -10px 0, 100% 0, 50% 2em)"),
);
let polygon = style.clip_path.expect("computed polygon");
assert_eq!(polygon.fill_rule, crate::ClipPathFillRule::Evenodd);
assert_eq!(
polygon.points,
vec![
(crate::Dimension::Px(-10.0), crate::Dimension::Px(0.0)),
(crate::Dimension::Percent(1.0), crate::Dimension::Px(0.0)),
(crate::Dimension::Percent(0.5), crate::Dimension::Em(2.0)),
]
);
}
#[test]
fn parses_display_and_size() {
let s = compute_style("div", Some("display: flex; width: 200px; height: 50px"));