Resolve percentage border radii at paint time
This commit is contained in:
1 parent
1a5f0bbc7b
commit
157d2bfaed
4 files changed
+281
-58
No files matched your search
@@ -4998,7 +4998,7 @@ fn pseudo_requires_generated_box(style: &crate::LayoutStyle, content: Option<&st
|
|||||||
|| style.background_image.is_some()
|
|| style.background_image.is_some()
|
||||||
|| style.mask_image.is_some()
|
|| style.mask_image.is_some()
|
||||||
|| style.box_shadow.is_some()
|
|| style.box_shadow.is_some()
|
||||||
|| style.border_radius > 0.0
|
|| !style.border_radius.is_zero()
|
||||||
|| style.overflow_hidden
|
|| style.overflow_hidden
|
||||||
|| style.transform_translate.is_some()
|
|| style.transform_translate.is_some()
|
||||||
|| style.transform_scale.is_some()
|
|| style.transform_scale.is_some()
|
||||||
|
|||||||
@@ -204,6 +204,37 @@ pub struct FontVariationSetting {
|
|||||||
pub value: f32,
|
pub value: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The supported uniform `border-radius` value as a CSS
|
||||||
|
/// `<length-percentage>`. Percentages resolve independently against the
|
||||||
|
/// border box's width and height, so `50%` produces an ellipse on a rectangle
|
||||||
|
/// and a circle on a square.
|
||||||
|
#[derive(Debug, Clone, Copy, Default, PartialEq)]
|
||||||
|
pub struct BorderRadius {
|
||||||
|
length: f32,
|
||||||
|
percentage: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BorderRadius {
|
||||||
|
pub const fn pixels(length: f32) -> Self {
|
||||||
|
Self { length, percentage: 0.0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn percentage(percentage: f32) -> Self {
|
||||||
|
Self { length: 0.0, percentage }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve(self, width: f32, height: f32) -> (f32, f32) {
|
||||||
|
(
|
||||||
|
(self.length + self.percentage * width).max(0.0),
|
||||||
|
(self.length + self.percentage * height).max(0.0),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_zero(self) -> bool {
|
||||||
|
self.length == 0.0 && self.percentage == 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The subset of CSS that influences box layout. Expanded in later phases.
|
/// The subset of CSS that influences box layout. Expanded in later phases.
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct LayoutStyle {
|
pub struct LayoutStyle {
|
||||||
@@ -301,8 +332,8 @@ pub struct LayoutStyle {
|
|||||||
pub padding_expressions: [Option<String>; 4],
|
pub padding_expressions: [Option<String>; 4],
|
||||||
pub border: Edges,
|
pub border: Edges,
|
||||||
/// `border-radius` (uniform; the first value of the shorthand). Rounds the
|
/// `border-radius` (uniform; the first value of the shorthand). Rounds the
|
||||||
/// background fill and border. In px after resolution.
|
/// background fill, replaced content, border, and shadow.
|
||||||
pub border_radius: f32,
|
pub border_radius: BorderRadius,
|
||||||
/// RGBA for the paint step. Parsed always (cheap), used only with `paint`.
|
/// RGBA for the paint step. Parsed always (cheap), used only with `paint`.
|
||||||
pub background_color: Option<[u8; 4]>,
|
pub background_color: Option<[u8; 4]>,
|
||||||
/// `linear-gradient(...)` background: (angle in degrees clockwise from 12
|
/// `linear-gradient(...)` background: (angle in degrees clockwise from 12
|
||||||
|
|||||||
@@ -720,9 +720,17 @@ fn paint_laid_dom_scrolled(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Box path (rounded if border-radius), reused for gradient/color fill.
|
// Box path (rounded if border-radius), reused for gradient/color fill.
|
||||||
let r = style.border_radius;
|
let radius = style.border_radius.resolve(rect.width, rect.height);
|
||||||
let bg_path = || if r > 0.5 {
|
let has_radius = radius.0 > 0.5 && radius.1 > 0.5;
|
||||||
rounded_rect_path(visible_rect.x, visible_rect.y, visible_rect.width, visible_rect.height, r)
|
let bg_path = || if has_radius {
|
||||||
|
rounded_rect_path(
|
||||||
|
visible_rect.x,
|
||||||
|
visible_rect.y,
|
||||||
|
visible_rect.width,
|
||||||
|
visible_rect.height,
|
||||||
|
radius.0,
|
||||||
|
radius.1,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
let mut pb = PathBuilder::new();
|
let mut pb = PathBuilder::new();
|
||||||
pb.push_rect(box_rect);
|
pb.push_rect(box_rect);
|
||||||
@@ -737,7 +745,7 @@ fn paint_laid_dom_scrolled(
|
|||||||
if let Some(path) = bg_path() {
|
if let Some(path) = bg_path() {
|
||||||
let mut paint = Paint::default();
|
let mut paint = Paint::default();
|
||||||
paint.set_color(Color::from_rgba8(bg[0], bg[1], bg[2], bg[3]));
|
paint.set_color(Color::from_rgba8(bg[0], bg[1], bg[2], bg[3]));
|
||||||
paint.anti_alias = r > 0.5;
|
paint.anti_alias = has_radius;
|
||||||
pixmap.fill_path(
|
pixmap.fill_path(
|
||||||
&path,
|
&path,
|
||||||
&paint,
|
&paint,
|
||||||
@@ -759,7 +767,14 @@ fn paint_laid_dom_scrolled(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some((angle, center, stops)) = &style.background_conic_gradient {
|
if let Some((angle, center, stops)) = &style.background_conic_gradient {
|
||||||
paint_conic_gradient(&mut pixmap, &visible_rect, *angle, *center, stops);
|
paint_conic_gradient(
|
||||||
|
&mut pixmap,
|
||||||
|
&visible_rect,
|
||||||
|
radius,
|
||||||
|
*angle,
|
||||||
|
*center,
|
||||||
|
stops,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if let Some((angle, stops)) = &style.background_gradient {
|
if let Some((angle, stops)) = &style.background_gradient {
|
||||||
if let Some(path) = bg_path() {
|
if let Some(path) = bg_path() {
|
||||||
@@ -774,6 +789,7 @@ fn paint_laid_dom_scrolled(
|
|||||||
mask_url,
|
mask_url,
|
||||||
base_url,
|
base_url,
|
||||||
&visible_rect,
|
&visible_rect,
|
||||||
|
radius,
|
||||||
fill,
|
fill,
|
||||||
style.background_radial_gradient.as_ref(),
|
style.background_radial_gradient.as_ref(),
|
||||||
style.background_gradient.as_ref(),
|
style.background_gradient.as_ref(),
|
||||||
@@ -815,6 +831,7 @@ fn paint_laid_dom_scrolled(
|
|||||||
&mut pixmap,
|
&mut pixmap,
|
||||||
image_cache,
|
image_cache,
|
||||||
None,
|
None,
|
||||||
|
radius,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -842,10 +859,17 @@ fn paint_laid_dom_scrolled(
|
|||||||
&& style.border.right == style.border.bottom
|
&& style.border.right == style.border.bottom
|
||||||
&& style.border.bottom == style.border.left
|
&& style.border.bottom == style.border.left
|
||||||
&& style.border.top > 0.0;
|
&& style.border.top > 0.0;
|
||||||
if style.border_radius > 0.5 && uniform_border {
|
if has_radius && uniform_border {
|
||||||
let bc = style.border_color.or(style.color).unwrap_or([0, 0, 0, 255]);
|
let bc = style.border_color.or(style.color).unwrap_or([0, 0, 0, 255]);
|
||||||
let w = style.border.top;
|
let w = style.border.top;
|
||||||
if let Some(path) = rounded_rect_path(rect.x + w / 2.0, rect.y + w / 2.0, rect.width - w, rect.height - w, (style.border_radius - w / 2.0).max(0.0)) {
|
if let Some(path) = rounded_rect_path(
|
||||||
|
rect.x + w / 2.0,
|
||||||
|
rect.y + w / 2.0,
|
||||||
|
rect.width - w,
|
||||||
|
rect.height - w,
|
||||||
|
(radius.0 - w / 2.0).max(0.0),
|
||||||
|
(radius.1 - w / 2.0).max(0.0),
|
||||||
|
) {
|
||||||
let mut paint = Paint::default();
|
let mut paint = Paint::default();
|
||||||
paint.set_color(Color::from_rgba8(bc[0], bc[1], bc[2], bc[3]));
|
paint.set_color(Color::from_rgba8(bc[0], bc[1], bc[2], bc[3]));
|
||||||
paint.anti_alias = true;
|
paint.anti_alias = true;
|
||||||
@@ -901,6 +925,7 @@ fn paint_laid_dom_scrolled(
|
|||||||
&mut pixmap,
|
&mut pixmap,
|
||||||
image_cache,
|
image_cache,
|
||||||
projected_image,
|
projected_image,
|
||||||
|
radius,
|
||||||
);
|
);
|
||||||
// Fall back when the image itself did not paint, following
|
// Fall back when the image itself did not paint, following
|
||||||
// what browsers show for a broken image: a non-empty alt
|
// what browsers show for a broken image: a non-empty alt
|
||||||
@@ -972,8 +997,16 @@ fn paint_laid_dom_scrolled(
|
|||||||
rect.height as u32,
|
rect.height as u32,
|
||||||
&svg_fonts,
|
&svg_fonts,
|
||||||
) {
|
) {
|
||||||
let mask = clip
|
let mask = if clip.is_some() || has_radius {
|
||||||
.and_then(|_| rect_clip_mask(pixmap.width(), pixmap.height(), &visible_rect));
|
rounded_box_clip_mask(
|
||||||
|
pixmap.width(),
|
||||||
|
pixmap.height(),
|
||||||
|
&visible_rect,
|
||||||
|
radius,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
pixmap.draw_pixmap(
|
pixmap.draw_pixmap(
|
||||||
rect.x as i32,
|
rect.x as i32,
|
||||||
rect.y as i32,
|
rect.y as i32,
|
||||||
@@ -1451,23 +1484,40 @@ fn collect_projected_image_transforms(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A closed rounded-rectangle path, corners approximated by quadratic curves
|
/// A closed rounded-rectangle path, corners approximated by quadratic curves
|
||||||
/// (visually indistinguishable from true arcs at typical UI radii). `r` is
|
/// (visually indistinguishable from true arcs at typical UI radii). The
|
||||||
/// clamped so it never exceeds half the shorter side.
|
/// horizontal and vertical radii are scaled together when necessary, matching
|
||||||
fn rounded_rect_path(x: f32, y: f32, w: f32, h: f32, r: f32) -> Option<tiny_skia::Path> {
|
/// CSS's overlap rule while preserving percentage ellipses.
|
||||||
|
fn rounded_rect_path(
|
||||||
|
x: f32,
|
||||||
|
y: f32,
|
||||||
|
w: f32,
|
||||||
|
h: f32,
|
||||||
|
rx: f32,
|
||||||
|
ry: f32,
|
||||||
|
) -> Option<tiny_skia::Path> {
|
||||||
if w <= 0.0 || h <= 0.0 {
|
if w <= 0.0 || h <= 0.0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let r = r.min(w / 2.0).min(h / 2.0).max(0.0);
|
let mut rx = rx.max(0.0);
|
||||||
|
let mut ry = ry.max(0.0);
|
||||||
|
if rx <= f32::EPSILON || ry <= f32::EPSILON {
|
||||||
|
let mut pb = PathBuilder::new();
|
||||||
|
pb.push_rect(Rect::from_xywh(x, y, w, h)?);
|
||||||
|
return pb.finish();
|
||||||
|
}
|
||||||
|
let scale = 1.0f32.min(w / (2.0 * rx)).min(h / (2.0 * ry));
|
||||||
|
rx *= scale;
|
||||||
|
ry *= scale;
|
||||||
let mut pb = PathBuilder::new();
|
let mut pb = PathBuilder::new();
|
||||||
pb.move_to(x + r, y);
|
pb.move_to(x + rx, y);
|
||||||
pb.line_to(x + w - r, y);
|
pb.line_to(x + w - rx, y);
|
||||||
pb.quad_to(x + w, y, x + w, y + r);
|
pb.quad_to(x + w, y, x + w, y + ry);
|
||||||
pb.line_to(x + w, y + h - r);
|
pb.line_to(x + w, y + h - ry);
|
||||||
pb.quad_to(x + w, y + h, x + w - r, y + h);
|
pb.quad_to(x + w, y + h, x + w - rx, y + h);
|
||||||
pb.line_to(x + r, y + h);
|
pb.line_to(x + rx, y + h);
|
||||||
pb.quad_to(x, y + h, x, y + h - r);
|
pb.quad_to(x, y + h, x, y + h - ry);
|
||||||
pb.line_to(x, y + r);
|
pb.line_to(x, y + ry);
|
||||||
pb.quad_to(x, y, x + r, y);
|
pb.quad_to(x, y, x + rx, y);
|
||||||
pb.close();
|
pb.close();
|
||||||
pb.finish()
|
pb.finish()
|
||||||
}
|
}
|
||||||
@@ -1486,7 +1536,7 @@ fn paint_box_shadow(
|
|||||||
pixmap: &mut Pixmap,
|
pixmap: &mut Pixmap,
|
||||||
shadow: &crate::BoxShadow,
|
shadow: &crate::BoxShadow,
|
||||||
rect: &crate::Rect,
|
rect: &crate::Rect,
|
||||||
border_radius: f32,
|
border_radius: crate::BorderRadius,
|
||||||
clip: Option<crate::Rect>,
|
clip: Option<crate::Rect>,
|
||||||
) {
|
) {
|
||||||
if shadow.inset || shadow.color[3] == 0 {
|
if shadow.inset || shadow.color[3] == 0 {
|
||||||
@@ -1500,7 +1550,9 @@ fn paint_box_shadow(
|
|||||||
if w0 <= 0.0 || h0 <= 0.0 {
|
if w0 <= 0.0 || h0 <= 0.0 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let r0 = (border_radius + spread).max(0.0);
|
let radius = border_radius.resolve(rect.width, rect.height);
|
||||||
|
let rx0 = (radius.0 + spread).max(0.0);
|
||||||
|
let ry0 = (radius.1 + spread).max(0.0);
|
||||||
let blur = shadow.blur.max(0.0);
|
let blur = shadow.blur.max(0.0);
|
||||||
// Ancestor overflow clip: build a mask once and reuse it for every layer.
|
// Ancestor overflow clip: build a mask once and reuse it for every layer.
|
||||||
let mask = match clip {
|
let mask = match clip {
|
||||||
@@ -1515,7 +1567,7 @@ fn paint_box_shadow(
|
|||||||
let color = shadow.color;
|
let color = shadow.color;
|
||||||
if blur < 0.5 {
|
if blur < 0.5 {
|
||||||
// No blur: a single crisp, offset (and spread) rounded rect.
|
// No blur: a single crisp, offset (and spread) rounded rect.
|
||||||
fill_shadow_rect(pixmap, x0, y0, w0, h0, r0, color, mask.as_ref());
|
fill_shadow_rect(pixmap, x0, y0, w0, h0, rx0, ry0, color, mask.as_ref());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let steps: u32 = (blur.ceil() as u32).clamp(2, 24);
|
let steps: u32 = (blur.ceil() as u32).clamp(2, 24);
|
||||||
@@ -1529,7 +1581,17 @@ fn paint_box_shadow(
|
|||||||
// j = 0 is the solid core (expansion 0); j = steps-1 reaches the blur
|
// j = 0 is the solid core (expansion 0); j = steps-1 reaches the blur
|
||||||
// radius. Larger rects paint first, smaller (more-covered) ones on top.
|
// radius. Larger rects paint first, smaller (more-covered) ones on top.
|
||||||
let e = blur * (j as f32) / ((steps - 1) as f32);
|
let e = blur * (j as f32) / ((steps - 1) as f32);
|
||||||
fill_shadow_rect(pixmap, x0 - e, y0 - e, w0 + 2.0 * e, h0 + 2.0 * e, r0 + e, layer_color, mask.as_ref());
|
fill_shadow_rect(
|
||||||
|
pixmap,
|
||||||
|
x0 - e,
|
||||||
|
y0 - e,
|
||||||
|
w0 + 2.0 * e,
|
||||||
|
h0 + 2.0 * e,
|
||||||
|
rx0 + e,
|
||||||
|
ry0 + e,
|
||||||
|
layer_color,
|
||||||
|
mask.as_ref(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1541,15 +1603,16 @@ fn fill_shadow_rect(
|
|||||||
y: f32,
|
y: f32,
|
||||||
w: f32,
|
w: f32,
|
||||||
h: f32,
|
h: f32,
|
||||||
radius: f32,
|
radius_x: f32,
|
||||||
|
radius_y: f32,
|
||||||
color: [u8; 4],
|
color: [u8; 4],
|
||||||
mask: Option<&tiny_skia::Mask>,
|
mask: Option<&tiny_skia::Mask>,
|
||||||
) {
|
) {
|
||||||
if w <= 0.0 || h <= 0.0 || color[3] == 0 {
|
if w <= 0.0 || h <= 0.0 || color[3] == 0 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let path = if radius > 0.5 {
|
let path = if radius_x > 0.5 && radius_y > 0.5 {
|
||||||
match rounded_rect_path(x, y, w, h, radius) {
|
match rounded_rect_path(x, y, w, h, radius_x, radius_y) {
|
||||||
Some(p) => p,
|
Some(p) => p,
|
||||||
None => return,
|
None => return,
|
||||||
}
|
}
|
||||||
@@ -2443,6 +2506,7 @@ fn paint_radial_gradient(
|
|||||||
fn paint_conic_gradient(
|
fn paint_conic_gradient(
|
||||||
pixmap: &mut Pixmap,
|
pixmap: &mut Pixmap,
|
||||||
rect: &crate::Rect,
|
rect: &crate::Rect,
|
||||||
|
border_radius: (f32, f32),
|
||||||
angle: f32,
|
angle: f32,
|
||||||
center: (f32, f32),
|
center: (f32, f32),
|
||||||
stops: &[([u8; 4], Option<f32>)],
|
stops: &[([u8; 4], Option<f32>)],
|
||||||
@@ -2469,13 +2533,23 @@ fn paint_conic_gradient(
|
|||||||
layer.pixels_mut()[(y * width + x) as usize] = premultiplied(color);
|
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
|
||||||
|
};
|
||||||
pixmap.draw_pixmap(
|
pixmap.draw_pixmap(
|
||||||
rect.x.floor() as i32,
|
rect.x.floor() as i32,
|
||||||
rect.y.floor() as i32,
|
rect.y.floor() as i32,
|
||||||
layer.as_ref(),
|
layer.as_ref(),
|
||||||
&tiny_skia::PixmapPaint::default(),
|
&tiny_skia::PixmapPaint::default(),
|
||||||
Transform::identity(),
|
Transform::identity(),
|
||||||
None,
|
clip.as_ref(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2813,13 +2887,16 @@ fn paint_in_flow_generated_box(
|
|||||||
if let Some(shadow) = style.box_shadow {
|
if let Some(shadow) = style.box_shadow {
|
||||||
paint_box_shadow(pixmap, &shadow, &rect, style.border_radius, clip);
|
paint_box_shadow(pixmap, &shadow, &rect, style.border_radius, clip);
|
||||||
}
|
}
|
||||||
let path = if style.border_radius > 0.5 {
|
let radius = style.border_radius.resolve(rect.width, rect.height);
|
||||||
|
let has_radius = radius.0 > 0.5 && radius.1 > 0.5;
|
||||||
|
let path = if has_radius {
|
||||||
rounded_rect_path(
|
rounded_rect_path(
|
||||||
visible.x,
|
visible.x,
|
||||||
visible.y,
|
visible.y,
|
||||||
visible.width,
|
visible.width,
|
||||||
visible.height,
|
visible.height,
|
||||||
style.border_radius,
|
radius.0,
|
||||||
|
radius.1,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Rect::from_xywh(visible.x, visible.y, visible.width, visible.height).and_then(|rect| {
|
Rect::from_xywh(visible.x, visible.y, visible.width, visible.height).and_then(|rect| {
|
||||||
@@ -2833,7 +2910,7 @@ fn paint_in_flow_generated_box(
|
|||||||
if let Some(color) = style.background_color {
|
if let Some(color) = style.background_color {
|
||||||
let mut paint = Paint::default();
|
let mut paint = Paint::default();
|
||||||
paint.set_color(Color::from_rgba8(color[0], color[1], color[2], color[3]));
|
paint.set_color(Color::from_rgba8(color[0], color[1], color[2], color[3]));
|
||||||
paint.anti_alias = style.border_radius > 0.5;
|
paint.anti_alias = has_radius;
|
||||||
pixmap.fill_path(
|
pixmap.fill_path(
|
||||||
&path,
|
&path,
|
||||||
&paint,
|
&paint,
|
||||||
@@ -2846,7 +2923,7 @@ fn paint_in_flow_generated_box(
|
|||||||
paint_radial_gradient(pixmap, &path, &rect, *center, stops);
|
paint_radial_gradient(pixmap, &path, &rect, *center, stops);
|
||||||
}
|
}
|
||||||
if let Some((angle, center, stops)) = &style.background_conic_gradient {
|
if let Some((angle, center, stops)) = &style.background_conic_gradient {
|
||||||
paint_conic_gradient(pixmap, &rect, *angle, *center, stops);
|
paint_conic_gradient(pixmap, &rect, radius, *angle, *center, stops);
|
||||||
}
|
}
|
||||||
if let Some((angle, stops)) = &style.background_gradient {
|
if let Some((angle, stops)) = &style.background_gradient {
|
||||||
paint_linear_gradient(pixmap, &path, &rect, *angle, stops);
|
paint_linear_gradient(pixmap, &path, &rect, *angle, stops);
|
||||||
@@ -2861,6 +2938,7 @@ fn paint_in_flow_generated_box(
|
|||||||
mask_url,
|
mask_url,
|
||||||
base_url,
|
base_url,
|
||||||
&visible,
|
&visible,
|
||||||
|
radius,
|
||||||
fill,
|
fill,
|
||||||
style.background_radial_gradient.as_ref(),
|
style.background_radial_gradient.as_ref(),
|
||||||
style.background_gradient.as_ref(),
|
style.background_gradient.as_ref(),
|
||||||
@@ -2893,6 +2971,7 @@ fn paint_in_flow_generated_box(
|
|||||||
pixmap,
|
pixmap,
|
||||||
image_cache,
|
image_cache,
|
||||||
None,
|
None,
|
||||||
|
radius,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3010,8 +3089,17 @@ fn paint_positioned_pseudo(
|
|||||||
None => Some(rect),
|
None => Some(rect),
|
||||||
};
|
};
|
||||||
let Some(visible) = visible else { return };
|
let Some(visible) = visible else { return };
|
||||||
let path = if style.border_radius > 0.5 {
|
let radius = style.border_radius.resolve(rect.width, rect.height);
|
||||||
rounded_rect_path(visible.x, visible.y, visible.width, visible.height, style.border_radius)
|
let has_radius = radius.0 > 0.5 && radius.1 > 0.5;
|
||||||
|
let path = if has_radius {
|
||||||
|
rounded_rect_path(
|
||||||
|
visible.x,
|
||||||
|
visible.y,
|
||||||
|
visible.width,
|
||||||
|
visible.height,
|
||||||
|
radius.0,
|
||||||
|
radius.1,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
Rect::from_xywh(visible.x, visible.y, visible.width, visible.height).and_then(|rect| {
|
Rect::from_xywh(visible.x, visible.y, visible.width, visible.height).and_then(|rect| {
|
||||||
let mut builder = PathBuilder::new();
|
let mut builder = PathBuilder::new();
|
||||||
@@ -3036,7 +3124,7 @@ fn paint_positioned_pseudo(
|
|||||||
paint_radial_gradient(pixmap, &path, &rect, *center, stops);
|
paint_radial_gradient(pixmap, &path, &rect, *center, stops);
|
||||||
}
|
}
|
||||||
if let Some((angle, center, stops)) = &style.background_conic_gradient {
|
if let Some((angle, center, stops)) = &style.background_conic_gradient {
|
||||||
paint_conic_gradient(pixmap, &rect, *angle, *center, stops);
|
paint_conic_gradient(pixmap, &rect, radius, *angle, *center, stops);
|
||||||
}
|
}
|
||||||
if let Some((angle, stops)) = &style.background_gradient {
|
if let Some((angle, stops)) = &style.background_gradient {
|
||||||
paint_linear_gradient(pixmap, &path, &rect, *angle, stops);
|
paint_linear_gradient(pixmap, &path, &rect, *angle, stops);
|
||||||
@@ -3051,6 +3139,7 @@ fn paint_positioned_pseudo(
|
|||||||
mask_url,
|
mask_url,
|
||||||
base_url,
|
base_url,
|
||||||
&visible,
|
&visible,
|
||||||
|
radius,
|
||||||
fill,
|
fill,
|
||||||
style.background_radial_gradient.as_ref(),
|
style.background_radial_gradient.as_ref(),
|
||||||
style.background_gradient.as_ref(),
|
style.background_gradient.as_ref(),
|
||||||
@@ -3083,6 +3172,7 @@ fn paint_positioned_pseudo(
|
|||||||
pixmap,
|
pixmap,
|
||||||
image_cache,
|
image_cache,
|
||||||
None,
|
None,
|
||||||
|
radius,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3457,6 +3547,7 @@ fn paint_image(
|
|||||||
pixmap: &mut Pixmap,
|
pixmap: &mut Pixmap,
|
||||||
cache: &mut RenderResourceCache,
|
cache: &mut RenderResourceCache,
|
||||||
transform: Option<ImageAffine>,
|
transform: Option<ImageAffine>,
|
||||||
|
clip_radius: (f32, f32),
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if rect.width <= 0.0 || rect.height <= 0.0 {
|
if rect.width <= 0.0 || rect.height <= 0.0 {
|
||||||
return false;
|
return false;
|
||||||
@@ -3494,12 +3585,19 @@ fn paint_image(
|
|||||||
// intersected with the ancestor overflow clip): `Cover`/`None` can size
|
// intersected with the ancestor overflow clip): `Cover`/`None` can size
|
||||||
// the image past the box, and an ancestor clip can cut into the box
|
// 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.
|
// itself. Only the fully-inside case takes the unmasked fast path.
|
||||||
let clip = if dest.width > visible_rect.width + 0.5
|
let has_radius = clip_radius.0 > 0.5 && clip_radius.1 > 0.5;
|
||||||
|
let clip = if has_radius
|
||||||
|
|| dest.width > visible_rect.width + 0.5
|
||||||
|| dest.height > visible_rect.height + 0.5
|
|| dest.height > visible_rect.height + 0.5
|
||||||
|| dest.x < visible_rect.x - 0.5
|
|| dest.x < visible_rect.x - 0.5
|
||||||
|| dest.y < visible_rect.y - 0.5
|
|| dest.y < visible_rect.y - 0.5
|
||||||
{
|
{
|
||||||
box_clip_mask(pixmap.width(), pixmap.height(), visible_rect)
|
rounded_box_clip_mask(
|
||||||
|
pixmap.width(),
|
||||||
|
pixmap.height(),
|
||||||
|
visible_rect,
|
||||||
|
clip_radius,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
@@ -3577,6 +3675,31 @@ fn box_clip_mask(pw: u32, ph: u32, rect: &crate::Rect) -> Option<tiny_skia::Mask
|
|||||||
Some(mask)
|
Some(mask)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A full-pixmap clip mask for a border box with a uniform elliptical radius.
|
||||||
|
/// Used by replaced and background images, whose raster content otherwise
|
||||||
|
/// ignores the owner's rounded corners.
|
||||||
|
fn rounded_box_clip_mask(
|
||||||
|
pw: u32,
|
||||||
|
ph: u32,
|
||||||
|
rect: &crate::Rect,
|
||||||
|
radius: (f32, f32),
|
||||||
|
) -> Option<tiny_skia::Mask> {
|
||||||
|
if radius.0 <= 0.5 || radius.1 <= 0.5 {
|
||||||
|
return box_clip_mask(pw, ph, rect);
|
||||||
|
}
|
||||||
|
let mut mask = tiny_skia::Mask::new(pw, ph)?;
|
||||||
|
let path = rounded_rect_path(
|
||||||
|
rect.x,
|
||||||
|
rect.y,
|
||||||
|
rect.width,
|
||||||
|
rect.height,
|
||||||
|
radius.0,
|
||||||
|
radius.1,
|
||||||
|
)?;
|
||||||
|
mask.fill_path(&path, FillRule::Winding, true, Transform::identity());
|
||||||
|
Some(mask)
|
||||||
|
}
|
||||||
|
|
||||||
/// Sniff SVG content: either an XML/SVG prolog, or a bare `<svg` root tag
|
/// 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).
|
/// (both are valid, and image responses commonly omit the XML declaration).
|
||||||
fn is_svg(bytes: &[u8]) -> bool {
|
fn is_svg(bytes: &[u8]) -> bool {
|
||||||
@@ -4262,18 +4385,6 @@ fn element_end(sprite: &str, start: usize, name: &str) -> Option<usize> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A full-pixmap alpha mask that is opaque only inside `rect`, used to clip a
|
|
||||||
/// blit (e.g. an inline svg raster) to an ancestor's overflow-hidden region.
|
|
||||||
fn rect_clip_mask(width: u32, height: u32, rect: &crate::Rect) -> Option<tiny_skia::Mask> {
|
|
||||||
let mut mask = tiny_skia::Mask::new(width, height)?;
|
|
||||||
let r = Rect::from_xywh(rect.x, rect.y, rect.width, rect.height)?;
|
|
||||||
let mut pb = PathBuilder::new();
|
|
||||||
pb.push_rect(r);
|
|
||||||
let path = pb.finish()?;
|
|
||||||
mask.fill_path(&path, FillRule::Winding, true, Transform::identity());
|
|
||||||
Some(mask)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Paint a `mask-image`: the ubiquitous "colored, scalable icon" pattern,
|
/// Paint a `mask-image`: the ubiquitous "colored, scalable icon" pattern,
|
||||||
/// where an SVG shape is used purely as a stencil and tinted by
|
/// where an SVG shape is used purely as a stencil and tinted by
|
||||||
/// `background-color`/`color` rather than carrying its own colors. Fetches
|
/// `background-color`/`color` rather than carrying its own colors. Fetches
|
||||||
@@ -4284,6 +4395,7 @@ fn paint_mask(
|
|||||||
src: &str,
|
src: &str,
|
||||||
base_url: Option<&str>,
|
base_url: Option<&str>,
|
||||||
rect: &crate::Rect,
|
rect: &crate::Rect,
|
||||||
|
border_radius: (f32, f32),
|
||||||
fill: [u8; 4],
|
fill: [u8; 4],
|
||||||
radial_gradient: Option<&((f32, f32), Vec<([u8; 4], Option<f32>)>)>,
|
radial_gradient: Option<&((f32, f32), Vec<([u8; 4], Option<f32>)>)>,
|
||||||
linear_gradient: Option<&(f32, Vec<([u8; 4], Option<f32>)>)>,
|
linear_gradient: Option<&(f32, Vec<([u8; 4], Option<f32>)>)>,
|
||||||
@@ -4364,13 +4476,23 @@ fn paint_mask(
|
|||||||
premultiplied(color);
|
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
|
||||||
|
};
|
||||||
pixmap.draw_pixmap(
|
pixmap.draw_pixmap(
|
||||||
rect.x.floor() as i32,
|
rect.x.floor() as i32,
|
||||||
rect.y.floor() as i32,
|
rect.y.floor() as i32,
|
||||||
recolored.as_ref(),
|
recolored.as_ref(),
|
||||||
&tiny_skia::PixmapPaint::default(),
|
&tiny_skia::PixmapPaint::default(),
|
||||||
Transform::identity(),
|
Transform::identity(),
|
||||||
None,
|
clip.as_ref(),
|
||||||
);
|
);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -4664,6 +4786,47 @@ mod tests {
|
|||||||
assert_eq!(outside.blue(), 255);
|
assert_eq!(outside.blue(), 255);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn percentage_border_radius_paints_circles_ellipses_and_replaced_clips() {
|
||||||
|
let tree = parse_html(
|
||||||
|
r##"<html><body style="margin:0">
|
||||||
|
<div id="circle" style="position:absolute;left:0;top:0;width:40px;height:40px;
|
||||||
|
border-radius:50%;background:#ff0000"></div>
|
||||||
|
<div id="ellipse" style="position:absolute;left:50px;top:0;width:80px;height:40px;
|
||||||
|
border-radius:50%;background:#0000ff"></div>
|
||||||
|
<div id="pill" style="position:absolute;left:140px;top:0;width:80px;height:40px;
|
||||||
|
border-radius:20px;background:#00aa00"></div>
|
||||||
|
<img alt="" src="data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='40'%20height='40'%3E%3Crect%20width='40'%20height='40'%20fill='%23800080'/%3E%3C/svg%3E"
|
||||||
|
style="position:absolute;left:230px;top:0;width:40px;height:40px;border-radius:50%">
|
||||||
|
</body></html>"##,
|
||||||
|
);
|
||||||
|
let pixmap = paint_dom(&tree, (280.0, 50.0), None).expect("pixmap");
|
||||||
|
let is_white = |x, y| {
|
||||||
|
let pixel = pixmap.pixel(x, y).expect("pixel");
|
||||||
|
pixel.red() > 245 && pixel.green() > 245 && pixel.blue() > 245
|
||||||
|
};
|
||||||
|
assert!(is_white(1, 1), "a square 50% radius must clear its corner");
|
||||||
|
let circle_top = pixmap.pixel(20, 1).expect("circle top");
|
||||||
|
assert!(circle_top.red() > 200 && circle_top.green() < 40 && circle_top.blue() < 40);
|
||||||
|
|
||||||
|
assert!(is_white(60, 3), "a rectangular 50% radius must use a 40x20 elliptical corner");
|
||||||
|
let ellipse_top = pixmap.pixel(90, 1).expect("ellipse top");
|
||||||
|
let ellipse_left = pixmap.pixel(51, 20).expect("ellipse left");
|
||||||
|
for pixel in [ellipse_top, ellipse_left] {
|
||||||
|
assert!(pixel.blue() > 200 && pixel.red() < 40 && pixel.green() < 40);
|
||||||
|
}
|
||||||
|
|
||||||
|
let pill_corner = pixmap.pixel(150, 3).expect("pixel radius pill corner");
|
||||||
|
assert!(
|
||||||
|
pill_corner.green() > 100 && pill_corner.red() < 40 && pill_corner.blue() < 40,
|
||||||
|
"a 20px radius must remain circular rather than resolving like 50%: {pill_corner:?}"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(is_white(231, 1), "a circular replaced image must clip its raster corner");
|
||||||
|
let image_center = pixmap.pixel(250, 20).expect("image center");
|
||||||
|
assert!(image_center.red() > 80 && image_center.blue() > 80 && image_center.green() < 40);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn auto_background_size_uses_intrinsic_dimensions_and_position() {
|
fn auto_background_size_uses_intrinsic_dimensions_and_position() {
|
||||||
let tree = parse_html(
|
let tree = parse_html(
|
||||||
|
|||||||
@@ -733,9 +733,26 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
|
|||||||
"padding-block-end" => set_padding_side(style, 2, value),
|
"padding-block-end" => set_padding_side(style, 2, value),
|
||||||
"border-radius" => {
|
"border-radius" => {
|
||||||
// Uniform radius from the first value (ignore per-corner / the
|
// Uniform radius from the first value (ignore per-corner / the
|
||||||
// `/` vertical-radius form; the common case is one length).
|
// `/` vertical-radius form; the common case is one
|
||||||
if let Some(r) = value.split(['/', ' ']).next().and_then(|t| px(t)) {
|
// <length-percentage>). A percentage must remain unresolved until
|
||||||
style.border_radius = r;
|
// paint knows both axes of the final border box.
|
||||||
|
let token = value.split('/').next().and_then(|part| part.split_whitespace().next());
|
||||||
|
if matches!(
|
||||||
|
value.trim().to_ascii_lowercase().as_str(),
|
||||||
|
"initial" | "unset" | "revert" | "revert-layer"
|
||||||
|
) {
|
||||||
|
style.border_radius = crate::BorderRadius::default();
|
||||||
|
} else if let Some(percentage) = token
|
||||||
|
.and_then(|token| token.strip_suffix('%'))
|
||||||
|
.and_then(|number| number.parse::<f32>().ok())
|
||||||
|
.filter(|number| number.is_finite() && *number >= 0.0)
|
||||||
|
{
|
||||||
|
style.border_radius = crate::BorderRadius::percentage(percentage / 100.0);
|
||||||
|
} else if let Some(length) = token
|
||||||
|
.and_then(px)
|
||||||
|
.filter(|length| length.is_finite() && *length >= 0.0)
|
||||||
|
{
|
||||||
|
style.border_radius = crate::BorderRadius::pixels(length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"border" => {
|
"border" => {
|
||||||
@@ -5029,6 +5046,18 @@ mod tests {
|
|||||||
assert_eq!(side.border.left, 0.0);
|
assert_eq!(side.border.left, 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn uniform_border_radius_retains_percentage_until_box_resolution() {
|
||||||
|
let percentage = compute_style("div", Some("border-radius:50%"));
|
||||||
|
assert_eq!(percentage.border_radius.resolve(80.0, 40.0), (40.0, 20.0));
|
||||||
|
|
||||||
|
let pixels = compute_style("div", Some("border-radius:20px"));
|
||||||
|
assert_eq!(pixels.border_radius.resolve(80.0, 40.0), (20.0, 20.0));
|
||||||
|
|
||||||
|
let reset = compute_style("div", Some("border-radius:50%;border-radius:initial"));
|
||||||
|
assert!(reset.border_radius.is_zero());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn item_self_alignment_parses_and_resets() {
|
fn item_self_alignment_parses_and_resets() {
|
||||||
let aligned = compute_style(
|
let aligned = compute_style(
|
||||||
|
|||||||
Reference in new issue
Block a user