render: paint generated boxes with contextual backgrounds

This commit is contained in:
SGavrl committed 2026-07-26 19:48:59 +00:00
1 parent eea960dd32
commit 37f6c93af9
5 files changed
+556 -63

No files matched your search

+83 -35
View File
@@ -21,6 +21,13 @@ struct Rule {
order: usize, order: usize,
} }
struct PseudoRule {
sel: CompiledSelector,
normal_decls: String,
important_decls: String,
order: usize,
}
/// An indexed set of author rules ready for fast per-element matching. /// An indexed set of author rules ready for fast per-element matching.
pub struct Stylesheet { pub struct Stylesheet {
rules: Vec<Rule>, rules: Vec<Rule>,
@@ -32,17 +39,11 @@ pub struct Stylesheet {
by_class: HashMap<String, Vec<usize>>, by_class: HashMap<String, Vec<usize>>,
by_local: HashMap<String, Vec<usize>>, by_local: HashMap<String, Vec<usize>>,
universal: Vec<usize>, universal: Vec<usize>,
/// `sel::before{content:"..."}` / `sel::after{...}` rules with a plain /// `sel::before` / `sel::after` rules matched against their ordinary base
/// string-literal `content`, matched separately from the main cascade: /// selector. Keeping their full declaration cascade supports both literal
/// our selector engine has no pseudo-element matching machinery, but a /// generated text and positioned decorative boxes.
/// `::before`/`::after` selector's *base* (everything before the before_rules: Vec<PseudoRule>,
/// pseudo-element) is an ordinary selector we can already compile and after_rules: Vec<PseudoRule>,
/// match. Extremely common on Wikipedia (`.hlist`/`.cslist` render as
/// comma-separated lists purely via `li::before{content:", "}`); without
/// this, adjacent list items with no real whitespace between them in the
/// DOM run together with no separator at all.
before_content: Vec<(CompiledSelector, String)>,
after_content: Vec<(CompiledSelector, String)>,
} }
impl Stylesheet { impl Stylesheet {
@@ -68,8 +69,8 @@ impl Stylesheet {
by_class: HashMap::new(), by_class: HashMap::new(),
by_local: HashMap::new(), by_local: HashMap::new(),
universal: Vec::new(), universal: Vec::new(),
before_content: Vec::new(), before_rules: Vec::new(),
after_content: Vec::new(), after_rules: Vec::new(),
}; };
let mut order = 0usize; let mut order = 0usize;
for src in sources { for src in sources {
@@ -79,15 +80,31 @@ impl Stylesheet {
for (selector, decls) in parse_stylesheet_for_viewport(src, viewport) { for (selector, decls) in parse_stylesheet_for_viewport(src, viewport) {
let sel_trim = selector.trim(); let sel_trim = selector.trim();
if let Some(base) = strip_pseudo_element(sel_trim, "before") { if let Some(base) = strip_pseudo_element(sel_trim, "before") {
if let (Some(content), Some(sel)) = (extract_string_content(&decls), tree.compile_rule_selector(base)) { if let Some(sel) = tree.compile_rule_selector(base) {
sheet.before_content.push((sel, content)); let (normal_decls, important_decls) =
crate::style::partition_declarations(&decls);
sheet.before_rules.push(PseudoRule {
sel,
normal_decls,
important_decls,
order,
});
} }
order += 1;
continue; continue;
} }
if let Some(base) = strip_pseudo_element(sel_trim, "after") { if let Some(base) = strip_pseudo_element(sel_trim, "after") {
if let (Some(content), Some(sel)) = (extract_string_content(&decls), tree.compile_rule_selector(base)) { if let Some(sel) = tree.compile_rule_selector(base) {
sheet.after_content.push((sel, content)); let (normal_decls, important_decls) =
crate::style::partition_declarations(&decls);
sheet.after_rules.push(PseudoRule {
sel,
normal_decls,
important_decls,
order,
});
} }
order += 1;
continue; continue;
} }
let Some(sel) = tree.compile_rule_selector(&selector) else { continue }; let Some(sel) = tree.compile_rule_selector(&selector) else { continue };
@@ -106,26 +123,57 @@ impl Stylesheet {
sheet sheet
} }
/// The literal text (if any) that `::before`/`::after` rules inject for pub fn pseudo_styles(
/// `nid`, as `(before, after)`. Last matching rule wins, approximating &self,
/// cascade order without tracking specificity for this small side table. tree: &DomTree,
pub fn pseudo_content(&self, tree: &DomTree, matcher: &mut Matcher, nid: NodeId) -> (Option<String>, Option<String>) { matcher: &mut Matcher,
if self.before_content.is_empty() && self.after_content.is_empty() { nid: NodeId,
return (None, None); host: &LayoutStyle,
} props: &HashMap<String, String>,
let mut before = None; ) -> (Option<LayoutStyle>, Option<LayoutStyle>) {
for (sel, content) in &self.before_content { let build = |rules: &[PseudoRule], matcher: &mut Matcher| {
if matcher.matches(tree, nid, sel) { let mut matched: Vec<(u32, usize, &PseudoRule)> = rules
before = Some(content.clone()); .iter()
.filter(|rule| matcher.matches(tree, nid, &rule.sel))
.map(|rule| (rule.sel.specificity(), rule.order, rule))
.collect();
if matched.is_empty() {
return None;
} }
} matched.sort_unstable_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
let mut after = None; let mut style = LayoutStyle::default();
for (sel, content) in &self.after_content { style.color = host.color;
if matcher.matches(tree, nid, sel) { style.font_size = host.font_size;
after = Some(content.clone()); style.font_weight = host.font_weight.clone();
style.font_family = host.font_family.clone();
style.line_height = host.line_height;
style.text_transform = host.text_transform;
let mut content = None;
for &(_, _, rule) in &matched {
let expanded = substitute_vars(&rule.normal_decls, props, 0);
crate::style::apply_declarations(&mut style, &expanded);
if let Some(value) = extract_string_content(&expanded) {
content = Some(value);
}
} }
} for &(_, _, rule) in &matched {
(before, after) let expanded = substitute_vars(&rule.important_decls, props, 0);
crate::style::apply_declarations(&mut style, &expanded);
if let Some(value) = extract_string_content(&expanded) {
content = Some(value);
}
}
style.before_content = content;
if style.before_content.is_some() {
Some(style)
} else {
None
}
};
(
build(&self.before_rules, matcher),
build(&self.after_rules, matcher),
)
} }
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
+37 -3
View File
@@ -352,9 +352,16 @@ fn cascade_walk(
) { ) {
this_props = std::rc::Rc::new(m); this_props = std::rc::Rc::new(m);
} }
let (before, after) = sheet.pseudo_content(tree, matcher, id); let (before_pseudo, after_pseudo) =
style.before_content = before; sheet.pseudo_styles(tree, matcher, id, &style, &this_props);
style.after_content = after; style.before_content = before_pseudo
.as_ref()
.and_then(|pseudo| pseudo.before_content.clone());
style.after_content = after_pseudo
.as_ref()
.and_then(|pseudo| pseudo.before_content.clone());
style.before_pseudo = before_pseudo.map(Box::new);
style.after_pseudo = after_pseudo.map(Box::new);
styles.insert(id, style); styles.insert(id, style);
} }
@@ -4882,6 +4889,33 @@ mod tests {
assert_eq!(runs[0].0.height, 32.0); assert_eq!(runs[0].0.height, 32.0);
} }
#[test]
fn light_root_rejects_dark_pseudo_gradient_variants() {
let tree = parse_html(
r#"<html class="light"><head><style>
.hero::before {
content:"";
position:absolute;
background:radial-gradient(circle, #ebf3f9, #d6dee4);
}
@media (prefers-color-scheme:dark) {
:root:not(.light) .hero::before {
background:radial-gradient(circle, #111111, #222222);
}
}
:root.dark .hero::before {
background:radial-gradient(circle, #333333, #444444);
}
</style></head><body><div class="hero"></div></body></html>"#,
);
let laid = layout_dom(&tree, (1280.0, 720.0));
let hero = tree.query_selector(".hero").unwrap().unwrap();
let pseudo = laid.styles[&hero].before_pseudo.as_ref().unwrap();
let (_, stops) = pseudo.background_radial_gradient.as_ref().unwrap();
assert_eq!(stops[0].0, [0xeb, 0xf3, 0xf9, 255]);
assert_eq!(stops[1].0, [0xd6, 0xde, 0xe4, 255]);
}
#[test] #[test]
fn table_cell_content_box_width_includes_padding_and_border_in_track() { fn table_cell_content_box_width_includes_padding_and_border_in_track() {
let tree = parse_html( let tree = parse_html(
+13
View File
@@ -251,6 +251,11 @@ pub struct LayoutStyle {
/// o'clock per CSS, list of (rgba, optional 0..1 stop position)). Modern /// o'clock per CSS, list of (rgba, optional 0..1 stop position)). Modern
/// hero sections use gradients heavily; without this they paint white. /// hero sections use gradients heavily; without this they paint white.
pub background_gradient: Option<(f32, Vec<([u8; 4], Option<f32>)>)>, pub background_gradient: Option<(f32, Vec<([u8; 4], Option<f32>)>)>,
/// First `radial-gradient(...)` layer: center in box-relative fractions
/// and color stops. It is painted below the first linear layer, matching
/// the common `linear-gradient(...), radial-gradient(...)` hero pattern.
pub background_radial_gradient:
Option<((f32, f32), Vec<([u8; 4], Option<f32>)>)>,
/// `conic-gradient(...)` background. The angle is the CSS `from` angle, /// `conic-gradient(...)` background. The angle is the CSS `from` angle,
/// the center is a fraction of the border box, and stops are normalized /// the center is a fraction of the border box, and stops are normalized
/// during paint. Conic gradients commonly provide the color source for a /// during paint. Conic gradients commonly provide the color source for a
@@ -265,6 +270,9 @@ pub struct LayoutStyle {
/// `10px` applies to both axes, matching how small square icons are /// `10px` applies to both axes, matching how small square icons are
/// almost always sized). /// almost always sized).
pub background_size: Option<(f32, f32)>, pub background_size: Option<(f32, f32)>,
/// Raw one/two-axis `background-size` expression, retained for paint-time
/// resolution against the final owner box (`calc(100% - 2rem) auto`).
pub background_size_expression: Option<String>,
/// Keyword `background-size` behavior. `None` is CSS `auto`, which uses /// Keyword `background-size` behavior. `None` is CSS `auto`, which uses
/// the image's intrinsic dimensions rather than stretching it to the box. /// the image's intrinsic dimensions rather than stretching it to the box.
pub background_size_fit: Option<ObjectFit>, pub background_size_fit: Option<ObjectFit>,
@@ -440,6 +448,11 @@ pub struct LayoutStyle {
/// children, same as if it were real text content. /// children, same as if it were real text content.
pub before_content: Option<String>, pub before_content: Option<String>,
pub after_content: Option<String>, pub after_content: Option<String>,
/// Computed boxes generated by `::before`/`::after`. Text-only pseudos
/// continue through `before_content`/`after_content`; these styles retain
/// positioned decorative boxes for layout-independent painting.
pub before_pseudo: Option<Box<LayoutStyle>>,
pub after_pseudo: Option<Box<LayoutStyle>>,
/// True for `inline-block`/`inline-flex`/`inline-grid`: participates in /// True for `inline-block`/`inline-flex`/`inline-grid`: participates in
/// the surrounding inline flow from the outside, like plain `inline` /// the surrounding inline flow from the outside, like plain `inline`
+346 -25
View File
@@ -6,7 +6,7 @@
//! dependencies, so a screenshot is reproducible across hosts. //! dependencies, so a screenshot is reproducible across hosts.
use obscura_dom::tree::DomTree; use obscura_dom::tree::DomTree;
use tiny_skia::{Color, FillRule, GradientStop, LinearGradient, Paint, PathBuilder, Pixmap, Point, Rect, SpreadMode, Transform}; use tiny_skia::{Color, FillRule, GradientStop, LinearGradient, Paint, PathBuilder, Pixmap, Point, RadialGradient, Rect, SpreadMode, Transform};
use ab_glyph::{Font, FontRef, PxScale, ScaleFont}; use ab_glyph::{Font, FontRef, PxScale, ScaleFont};
static FONT_BYTES: &[u8] = include_bytes!("../assets/liberation-sans.ttf"); static FONT_BYTES: &[u8] = include_bytes!("../assets/liberation-sans.ttf");
@@ -37,6 +37,13 @@ pub fn paint_dom(tree: &DomTree, viewport: (f32, f32), base_url: Option<&str>) -
let intrinsic = collect_image_intrinsics(tree, base_url, &mut image_cache); let intrinsic = collect_image_intrinsics(tree, base_url, &mut image_cache);
let fonts = collect_web_fonts(tree, base_url); let fonts = collect_web_fonts(tree, base_url);
let mut laid = layout_dom_with_resources(tree, viewport, &intrinsic, &fonts); let mut laid = layout_dom_with_resources(tree, viewport, &intrinsic, &fonts);
let root_font_size = tree
.query_selector("html")
.ok()
.flatten()
.and_then(|root| laid.styles.get(&root))
.and_then(|style| style.font_size)
.unwrap_or(16.0);
// Nodes that live inside an inline `<svg>` we rasterized as one document; // Nodes that live inside an inline `<svg>` we rasterized as one document;
// their painting is owned by that raster, so they are skipped in both the // their painting is owned by that raster, so they are skipped in both the
// box/text loop below and the inline-formatting loop after it (an svg // box/text loop below and the inline-formatting loop after it (an svg
@@ -180,29 +187,37 @@ pub fn paint_dom(tree: &DomTree, viewport: (f32, f32), base_url: Option<&str>) -
// `background-clip: text` clips the background to the glyphs, so it must // `background-clip: text` clips the background to the glyphs, so it must
// not paint as a box here; the text paint path fills the glyphs instead. // not paint as a box here; the text paint path fills the glyphs instead.
if style.mask_image.is_none() && !style.background_clip_text { if style.mask_image.is_none() && !style.background_clip_text {
if let Some((angle, stops)) = &style.background_gradient { if let Some(bg) = style.background_color {
if let Some(path) = bg_path() {
paint_linear_gradient(&mut pixmap, &path, &visible_rect, *angle, stops);
}
} else if let Some((angle, center, stops)) =
&style.background_conic_gradient
{
paint_conic_gradient(
&mut pixmap,
&visible_rect,
*angle,
*center,
stops,
);
} else if let Some(bg) = style.background_color {
// A masked element's background-color is the mask's fill color,
// not an ordinary box background (handled below), so this only
// runs for unmasked boxes.
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 = r > 0.5;
pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); pixmap.fill_path(
&path,
&paint,
FillRule::Winding,
Transform::identity(),
None,
);
}
}
if let Some((center, stops)) = &style.background_radial_gradient {
if let Some(path) = bg_path() {
paint_radial_gradient(
&mut pixmap,
&path,
&visible_rect,
*center,
stops,
);
}
}
if let Some((angle, center, stops)) = &style.background_conic_gradient {
paint_conic_gradient(&mut pixmap, &visible_rect, *angle, *center, stops);
}
if let Some((angle, stops)) = &style.background_gradient {
if let Some(path) = bg_path() {
paint_linear_gradient(&mut pixmap, &path, &visible_rect, *angle, stops);
} }
} }
} }
@@ -227,8 +242,12 @@ pub fn paint_dom(tree: &DomTree, viewport: (f32, f32), base_url: Option<&str>) -
base_url, base_url,
&rect, &rect,
style.background_size, style.background_size,
style.background_size_expression.as_deref(),
style.background_size_fit, style.background_size_fit,
style.background_position, style.background_position,
style.font_size.unwrap_or(16.0),
root_font_size,
viewport,
&mut image_cache, &mut image_cache,
) { ) {
// A background layer is always clipped to its owner's border // A background layer is always clipped to its owner's border
@@ -252,6 +271,16 @@ pub fn paint_dom(tree: &DomTree, viewport: (f32, f32), base_url: Option<&str>) -
} }
} }
} }
if let Some(pseudo) = style.before_pseudo.as_deref() {
paint_positioned_pseudo(
&mut pixmap,
pseudo,
&rect,
viewport,
root_font_size,
clip,
);
}
// Rounded, uniform border: stroke the rounded-rect outline instead of // Rounded, uniform border: stroke the rounded-rect outline instead of
// four sharp edge rects. // four sharp edge rects.
@@ -355,6 +384,11 @@ pub fn paint_dom(tree: &DomTree, viewport: (f32, f32), base_url: Option<&str>) -
// overflow-visible region. // overflow-visible region.
if name.local.as_ref() == "svg" { if name.local.as_ref() == "svg" {
let mut markup = serialize_svg(tree, nid); let mut markup = serialize_svg(tree, nid);
// Resolve referenced symbols before carrying the host color into
// the standalone document. A document-level/external symbol may
// itself contain `currentColor`, and therefore has to be present
// when the root color is established.
inject_external_sprites(tree, nid, base_url, &mut markup, &mut image_cache, &mut sprite_cache);
// resvg parses the serialized subtree as a standalone SVG // resvg parses the serialized subtree as a standalone SVG
// document, outside the page's author stylesheet. Preserve the // document, outside the page's author stylesheet. Preserve the
// host element's computed `color` so paths using `currentColor` // host element's computed `color` so paths using `currentColor`
@@ -368,7 +402,6 @@ pub fn paint_dom(tree: &DomTree, viewport: (f32, f32), base_url: Option<&str>) -
// the sprite, splice the referenced `<symbol>` into a local `<defs>`, // the sprite, splice the referenced `<symbol>` into a local `<defs>`,
// and rewrite the href to a same-document `#id`. Same-document // and rewrite the href to a same-document `#id`. Same-document
// `<use href="#id">` (empty url) is untouched. // `<use href="#id">` (empty url) is untouched.
inject_external_sprites(tree, nid, base_url, &mut markup, &mut image_cache, &mut sprite_cache);
if let Some(content) = render_svg(markup.as_bytes(), rect.width as u32, rect.height as u32) { if let Some(content) = render_svg(markup.as_bytes(), rect.width as u32, rect.height as u32) {
let mask = clip.and_then(|_| rect_clip_mask(pixmap.width(), pixmap.height(), &visible_rect)); let mask = clip.and_then(|_| rect_clip_mask(pixmap.width(), pixmap.height(), &visible_rect));
pixmap.draw_pixmap( pixmap.draw_pixmap(
@@ -1296,7 +1329,8 @@ fn paint_linear_gradient(pixmap: &mut Pixmap, path: &tiny_skia::Path, rect: &cra
let n = stops.len(); let n = stops.len();
let mut gs: Vec<GradientStop> = Vec::with_capacity(n); let mut gs: Vec<GradientStop> = Vec::with_capacity(n);
let mut last = 0.0f32; let mut last = 0.0f32;
for (i, (c, pos)) in stops.iter().enumerate() { for (i, (_, pos)) in stops.iter().enumerate() {
let c = gradient_stop_color(stops, i);
let p = pos.unwrap_or(i as f32 / (n - 1) as f32).clamp(0.0, 1.0).max(last); let p = pos.unwrap_or(i as f32 / (n - 1) as f32).clamp(0.0, 1.0).max(last);
last = p; last = p;
gs.push(GradientStop::new(p, Color::from_rgba8(c[0], c[1], c[2], c[3]))); gs.push(GradientStop::new(p, Color::from_rgba8(c[0], c[1], c[2], c[3])));
@@ -1309,6 +1343,54 @@ fn paint_linear_gradient(pixmap: &mut Pixmap, path: &tiny_skia::Path, rect: &cra
} }
} }
fn paint_radial_gradient(
pixmap: &mut Pixmap,
path: &tiny_skia::Path,
rect: &crate::Rect,
center: (f32, f32),
stops: &[([u8; 4], Option<f32>)],
) {
if stops.len() < 2 {
return;
}
let center = Point::from_xy(
rect.x + rect.width * center.0,
rect.y + rect.height * center.1,
);
let radius = [
(rect.x - center.x).hypot(rect.y - center.y),
(rect.x + rect.width - center.x).hypot(rect.y - center.y),
(rect.x - center.x).hypot(rect.y + rect.height - center.y),
(rect.x + rect.width - center.x).hypot(rect.y + rect.height - center.y),
]
.into_iter()
.fold(0.0, f32::max);
let normalized = normalized_stops(stops);
let gradient_stops = normalized
.into_iter()
.map(|(position, color)| {
GradientStop::new(
position,
Color::from_rgba8(color[0], color[1], color[2], color[3]),
)
})
.collect();
if let Some(shader) = RadialGradient::new(
center,
0.0,
center,
radius,
gradient_stops,
SpreadMode::Pad,
Transform::identity(),
) {
let mut paint = Paint::default();
paint.shader = shader;
paint.anti_alias = true;
pixmap.fill_path(path, &paint, FillRule::Winding, Transform::identity(), None);
}
}
fn paint_conic_gradient( fn paint_conic_gradient(
pixmap: &mut Pixmap, pixmap: &mut Pixmap,
rect: &crate::Rect, rect: &crate::Rect,
@@ -1354,7 +1436,8 @@ fn normalized_stops(
let count = stops.len(); let count = stops.len();
let mut normalized = Vec::with_capacity(count); let mut normalized = Vec::with_capacity(count);
let mut last = 0.0f32; let mut last = 0.0f32;
for (index, (color, position)) in stops.iter().enumerate() { for (index, (_, position)) in stops.iter().enumerate() {
let color = gradient_stop_color(stops, index);
let position = position let position = position
.unwrap_or_else(|| { .unwrap_or_else(|| {
if count <= 1 { if count <= 1 {
@@ -1366,11 +1449,28 @@ fn normalized_stops(
.clamp(0.0, 1.0) .clamp(0.0, 1.0)
.max(last); .max(last);
last = position; last = position;
normalized.push((position, *color)); normalized.push((position, color));
} }
normalized normalized
} }
fn gradient_stop_color(
stops: &[([u8; 4], Option<f32>)],
index: usize,
) -> [u8; 4] {
let color = stops[index].0;
if color[3] != 0 {
return color;
}
let neighbor = stops[index + 1..]
.iter()
.find(|(candidate, _)| candidate[3] != 0)
.or_else(|| stops[..index].iter().rev().find(|(candidate, _)| candidate[3] != 0));
neighbor
.map(|(neighbor, _)| [neighbor[0], neighbor[1], neighbor[2], 0])
.unwrap_or(color)
}
fn sample_normalized_stops(stops: &[(f32, [u8; 4])], t: f32) -> [u8; 4] { fn sample_normalized_stops(stops: &[(f32, [u8; 4])], t: f32) -> [u8; 4] {
let t = t.clamp(0.0, 1.0); let t = t.clamp(0.0, 1.0);
let Some(&(first_position, first_color)) = stops.first() else { let Some(&(first_position, first_color)) = stops.first() else {
@@ -1472,8 +1572,12 @@ fn background_image_rect(
base_url: Option<&str>, base_url: Option<&str>,
box_rect: &crate::Rect, box_rect: &crate::Rect,
explicit_size: Option<(f32, f32)>, explicit_size: Option<(f32, f32)>,
size_expression: Option<&str>,
fit: Option<crate::ObjectFit>, fit: Option<crate::ObjectFit>,
position: (f32, f32), position: (f32, f32),
em: f32,
rem: f32,
viewport: (f32, f32),
cache: &mut std::collections::HashMap<String, Option<Vec<u8>>>, cache: &mut std::collections::HashMap<String, Option<Vec<u8>>>,
) -> Option<crate::Rect> { ) -> Option<crate::Rect> {
let bytes = fetch_bytes(src, base_url, cache)?; let bytes = fetch_bytes(src, base_url, cache)?;
@@ -1482,7 +1586,43 @@ fn background_image_rect(
} else { } else {
image_dimensions(&bytes).map(|(width, height)| (width as f32, height as f32)) image_dimensions(&bytes).map(|(width, height)| (width as f32, height as f32))
}; };
let (width, height) = if let Some(size) = explicit_size { let expression_size = size_expression.and_then(|expression| {
let components = split_background_size_components(expression);
let width = components.first().and_then(|value| {
(!value.eq_ignore_ascii_case("auto")).then(|| {
crate::style::resolve_contextual_length(
value,
em,
rem,
viewport.0 / 100.0,
viewport.1 / 100.0,
box_rect.width,
)
})?
});
let height = components.get(1).and_then(|value| {
(!value.eq_ignore_ascii_case("auto")).then(|| {
crate::style::resolve_contextual_length(
value,
em,
rem,
viewport.0 / 100.0,
viewport.1 / 100.0,
box_rect.height,
)
})?
});
match (width, height, intrinsic) {
(Some(width), Some(height), _) => Some((width, height)),
(Some(width), None, Some((iw, ih))) => Some((width, width * ih / iw)),
(None, Some(height), Some((iw, ih))) => Some((height * iw / ih, height)),
(None, None, Some(intrinsic)) => Some(intrinsic),
_ => None,
}
});
let (width, height) = if let Some(size) = expression_size {
size
} else if let Some(size) = explicit_size {
size size
} else if let Some(fit) = fit { } else if let Some(fit) = fit {
let (iw, ih) = intrinsic?; let (iw, ih) = intrinsic?;
@@ -1506,6 +1646,102 @@ fn background_image_rect(
}) })
} }
fn split_background_size_components(value: &str) -> Vec<&str> {
let mut components = Vec::new();
let mut depth = 0i32;
let mut start = None;
for (index, ch) in value.char_indices() {
match ch {
'(' => {
depth += 1;
start.get_or_insert(index);
}
')' => depth = (depth - 1).max(0),
ch if ch.is_whitespace() && depth == 0 => {
if let Some(start) = start.take() {
components.push(value[start..index].trim());
}
}
_ => {
start.get_or_insert(index);
}
}
}
if let Some(start) = start {
components.push(value[start..].trim());
}
components
}
fn paint_positioned_pseudo(
pixmap: &mut Pixmap,
style: &crate::LayoutStyle,
containing_block: &crate::Rect,
viewport: (f32, f32),
root_font_size: f32,
ancestor_clip: Option<crate::Rect>,
) {
if style.position != Some(taffy::Position::Absolute) {
return;
}
let em = style.font_size.unwrap_or(16.0);
let resolve = |dimension: crate::Dimension, basis: f32| {
match dimension.resolve(em, root_font_size, viewport.0 / 100.0, viewport.1 / 100.0) {
crate::Dimension::Px(value) => Some(value),
crate::Dimension::Percent(value) => Some(value * basis),
_ => None,
}
};
let top = style.inset[0].and_then(|value| resolve(value, containing_block.height));
let right = style.inset[1].and_then(|value| resolve(value, containing_block.width));
let bottom = style.inset[2].and_then(|value| resolve(value, containing_block.height));
let left = style.inset[3].and_then(|value| resolve(value, containing_block.width));
let width = resolve(style.width, containing_block.width).or_else(|| {
Some(containing_block.width - left? - right?)
});
let height = resolve(style.height, containing_block.height).or_else(|| {
Some(containing_block.height - top? - bottom?)
});
let (Some(width), Some(height)) = (width, height) else { return };
if width <= 0.0 || height <= 0.0 {
return;
}
let x = left
.map(|value| containing_block.x + value)
.or_else(|| right.map(|value| containing_block.x + containing_block.width - value - width))
.unwrap_or(containing_block.x);
let y = top
.map(|value| containing_block.y + value)
.or_else(|| bottom.map(|value| containing_block.y + containing_block.height - value - height))
.unwrap_or(containing_block.y);
let rect = crate::Rect { x, y, width, height };
let visible = match ancestor_clip {
Some(clip) => rect.intersect(&clip),
None => Some(rect),
};
let Some(visible) = visible else { return };
let Some(sk_rect) = Rect::from_xywh(visible.x, visible.y, visible.width, visible.height) else {
return;
};
let mut builder = PathBuilder::new();
builder.push_rect(sk_rect);
let Some(path) = builder.finish() else { return };
if let Some(color) = style.background_color {
let mut paint = Paint::default();
paint.set_color(Color::from_rgba8(color[0], color[1], color[2], color[3]));
pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None);
}
if let Some((center, stops)) = &style.background_radial_gradient {
paint_radial_gradient(pixmap, &path, &rect, *center, stops);
}
if let Some((angle, center, stops)) = &style.background_conic_gradient {
paint_conic_gradient(pixmap, &rect, *angle, *center, stops);
}
if let Some((angle, stops)) = &style.background_gradient {
paint_linear_gradient(pixmap, &path, &rect, *angle, stops);
}
}
/// Fetch every `<img>` once (seeding `cache` for the paint pass) and record its /// Fetch every `<img>` once (seeding `cache` for the paint pass) and record its
/// intrinsic (width, height) so layout can size replaced elements that have no /// intrinsic (width, height) so layout can size replaced elements that have no
/// explicit dimensions. Keyed by the `<img>`'s NodeId. /// explicit dimensions. Keyed by the `<img>`'s NodeId.
@@ -2509,6 +2745,67 @@ mod tests {
); );
} }
#[test]
fn contextual_background_size_preserves_auto_axis_ratio() {
let source = "data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='200'%20height='50'%3E%3C/svg%3E";
let owner = crate::Rect {
x: 0.0,
y: 0.0,
width: 132.0,
height: 60.0,
};
let mut cache = std::collections::HashMap::new();
let image = background_image_rect(
source,
None,
&owner,
None,
Some("calc(100% - 2rem) auto"),
None,
(0.0, 0.5),
10.0,
10.0,
(1280.0, 720.0),
&mut cache,
)
.unwrap();
assert_eq!(image.width, 112.0);
assert_eq!(image.height, 28.0);
assert_eq!(image.y, 16.0);
}
#[test]
fn paints_positioned_empty_pseudo_background_box() {
let tree = parse_html(
r#"<html><head><style>
body { margin:0 }
#host { position:relative; width:100px; height:50px }
#host::before {
content:"";
position:absolute;
top:10px;
left:20px;
width:40px;
height:30px;
background:
linear-gradient(to bottom, transparent, #ffffff),
radial-gradient(circle at 50% 50%, #ebf3f9, #d6dee4);
}
</style></head><body><div id="host"></div></body></html>"#,
);
let pixmap = paint_dom(&tree, (120.0, 80.0), None).expect("pixmap");
let center = pixmap.pixel(40, 25).expect("pixel");
assert!(
center.red() >= 214 && center.green() >= 222 && center.blue() >= 228,
"transparent-to-white over a light radial layer must not darken it: {center:?}"
);
let outside = pixmap.pixel(5, 5).expect("pixel");
assert_eq!(
(outside.red(), outside.green(), outside.blue()),
(255, 255, 255)
);
}
#[test] #[test]
fn later_element_paints_over_earlier() { fn later_element_paints_over_earlier() {
// A blue div nested inside a red one: both cover the origin, and blue // A blue div nested inside a red one: both cover the origin, and blue
@@ -2856,6 +3153,30 @@ mod tests {
); );
} }
#[test]
fn injected_document_symbol_inherits_target_current_color() {
let tree = parse_html(
r##"<html><body>
<svg style="display:none"><symbol id="arrow" viewBox="0 0 10 10"><rect width="10" height="10" fill="currentColor"/></symbol></svg>
<svg id="icon" viewBox="0 0 10 10"><use href="#arrow"/></svg>
</body></html>"##,
);
let svg = tree.query_selector("#icon").unwrap().unwrap();
let mut markup = serialize_svg(&tree, svg);
let mut cache = std::collections::HashMap::new();
let mut sprite_cache = std::collections::HashMap::new();
inject_external_sprites(&tree, svg, None, &mut markup, &mut cache, &mut sprite_cache);
inject_svg_current_color(&mut markup, [220, 20, 60, 255]);
let pixmap = render_svg(markup.as_bytes(), 20, 20).expect("injected svg renders");
assert!(
pixmap
.pixels()
.iter()
.any(|pixel| pixel.red() > 180 && pixel.green() < 60 && pixel.blue() < 100),
"injected currentColor symbol should inherit target SVG color: {markup}",
);
}
#[test] #[test]
fn font_face_parser_selects_ascii_subset_and_preserves_functional_src() { fn font_face_parser_selects_ascii_subset_and_preserves_functional_src() {
let css = r#" let css = r#"
+77
View File
@@ -434,14 +434,17 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
if !value.trim().is_empty() { if !value.trim().is_empty() {
style.background_color = None; style.background_color = None;
style.background_gradient = parse_linear_gradient(value); style.background_gradient = parse_linear_gradient(value);
style.background_radial_gradient = parse_radial_gradient(value);
style.background_conic_gradient = parse_conic_gradient(value); style.background_conic_gradient = parse_conic_gradient(value);
if style.background_gradient.is_none() if style.background_gradient.is_none()
&& style.background_radial_gradient.is_none()
&& style.background_conic_gradient.is_none() && style.background_conic_gradient.is_none()
{ {
style.background_color = parse_color(value); style.background_color = parse_color(value);
} }
style.background_image = parse_url(value); style.background_image = parse_url(value);
style.background_size = None; style.background_size = None;
style.background_size_expression = background_size_expression(value);
style.background_size_fit = parse_background_size_fit(value); style.background_size_fit = parse_background_size_fit(value);
style.background_position = (0.0, 0.0); style.background_position = (0.0, 0.0);
style.background_clip_text = false; style.background_clip_text = false;
@@ -449,11 +452,14 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
} }
"background-image" => { "background-image" => {
style.background_gradient = parse_linear_gradient(value); style.background_gradient = parse_linear_gradient(value);
style.background_radial_gradient = parse_radial_gradient(value);
style.background_conic_gradient = parse_conic_gradient(value); style.background_conic_gradient = parse_conic_gradient(value);
style.background_image = parse_url(value); style.background_image = parse_url(value);
} }
"background-size" => { "background-size" => {
style.background_size = parse_background_size(value); style.background_size = parse_background_size(value);
style.background_size_expression =
(!value.trim().is_empty()).then(|| value.trim().to_string());
style.background_size_fit = parse_background_size_fit(value); style.background_size_fit = parse_background_size_fit(value);
} }
"background-position" => style.background_position = parse_background_position(value), "background-position" => style.background_position = parse_background_position(value),
@@ -2897,6 +2903,47 @@ fn parse_linear_gradient(value: &str) -> Option<(f32, Vec<([u8; 4], Option<f32>)
Some((angle, stops)) Some((angle, stops))
} }
fn parse_radial_gradient(
value: &str,
) -> Option<((f32, f32), Vec<([u8; 4], Option<f32>)>)> {
let lower = value.to_ascii_lowercase();
let start = lower.find("radial-gradient(")?;
let open = start + "radial-gradient(".len();
let end = find_matching_paren(&value[open..])? + open;
let parts = split_top_level(&value[open..end], ',');
if parts.is_empty() {
return None;
}
let mut center = (0.5, 0.5);
let mut stop_start = 0;
let prelude = parts[0].trim().to_ascii_lowercase();
if prelude.contains(" at ") || prelude.starts_with("at ") {
let coords = prelude
.split_once(" at ")
.map(|(_, coords)| coords)
.or_else(|| prelude.strip_prefix("at "))
.unwrap_or_default();
let mut coords = coords.split_whitespace();
if let Some(x) = coords.next().and_then(percent_fraction) {
center.0 = x;
}
if let Some(y) = coords.next().and_then(percent_fraction) {
center.1 = y;
}
stop_start = 1;
} else if parse_color(parts[0].trim()).is_none() {
stop_start = 1;
}
let mut stops = Vec::new();
for part in &parts[stop_start..] {
let (color, position) = split_color_stop(part.trim());
if let Some(color) = parse_color(color) {
stops.push((color, position));
}
}
(stops.len() >= 2).then_some((center, stops))
}
/// Parse the common `conic-gradient(from A at X Y, color P%, ...)` form. /// Parse the common `conic-gradient(from A at X Y, color P%, ...)` form.
/// Angles follow CSS convention (0deg at 12 o'clock, clockwise); the center /// Angles follow CSS convention (0deg at 12 o'clock, clockwise); the center
/// is retained as box-relative fractions for paint-time resolution. /// is retained as box-relative fractions for paint-time resolution.
@@ -3173,6 +3220,25 @@ fn parse_background_size_fit(value: &str) -> Option<crate::ObjectFit> {
} }
} }
fn background_size_expression(value: &str) -> Option<String> {
let (_, size) = value.rsplit_once('/')?;
let mut depth = 0i32;
let mut end = size.len();
for (index, ch) in size.char_indices() {
match ch {
'(' => depth += 1,
')' => depth = (depth - 1).max(0),
_ if depth == 0 && ch == ',' => {
end = index;
break;
}
_ => {}
}
}
let size = size[..end].trim();
(!size.is_empty()).then(|| size.to_string())
}
/// `background-position` keywords/lengths -> a 0.0-1.0 fraction per axis (the /// `background-position` keywords/lengths -> a 0.0-1.0 fraction per axis (the
/// fraction of the box's leftover space, after the image's own size, to /// fraction of the box's leftover space, after the image's own size, to
/// offset by: 0 = start edge, 1 = end edge, 0.5 = centered). /// offset by: 0 = start edge, 1 = end edge, 0.5 = centered).
@@ -3684,6 +3750,7 @@ mod tests {
assert_eq!(s.background_conic_gradient, None); assert_eq!(s.background_conic_gradient, None);
assert_eq!(s.background_image, None); assert_eq!(s.background_image, None);
assert_eq!(s.background_size, None); assert_eq!(s.background_size, None);
assert_eq!(s.background_size_expression, None);
assert_eq!(s.background_size_fit, None); assert_eq!(s.background_size_fit, None);
assert_eq!(s.background_position, (0.0, 0.0)); assert_eq!(s.background_position, (0.0, 0.0));
assert!(!s.background_clip_text); assert!(!s.background_clip_text);
@@ -3698,6 +3765,16 @@ mod tests {
contain.background_size_fit, contain.background_size_fit,
Some(crate::ObjectFit::Contain) Some(crate::ObjectFit::Contain)
); );
let contextual = compute_style(
"a",
Some(
"background:url(icon.svg) no-repeat 0 50% / calc(100% - 2rem) auto",
),
);
assert_eq!(
contextual.background_size_expression.as_deref(),
Some("calc(100% - 2rem) auto")
);
} }
#[test] #[test]