render: paint generated boxes with contextual backgrounds
This commit is contained in:
1 parent
eea960dd32
commit
37f6c93af9
5 files changed
+553
-60
No files matched your search
@@ -21,6 +21,13 @@ struct Rule {
|
||||
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.
|
||||
pub struct Stylesheet {
|
||||
rules: Vec<Rule>,
|
||||
@@ -32,17 +39,11 @@ pub struct Stylesheet {
|
||||
by_class: HashMap<String, Vec<usize>>,
|
||||
by_local: HashMap<String, Vec<usize>>,
|
||||
universal: Vec<usize>,
|
||||
/// `sel::before{content:"..."}` / `sel::after{...}` rules with a plain
|
||||
/// string-literal `content`, matched separately from the main cascade:
|
||||
/// our selector engine has no pseudo-element matching machinery, but a
|
||||
/// `::before`/`::after` selector's *base* (everything before the
|
||||
/// pseudo-element) is an ordinary selector we can already compile and
|
||||
/// 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)>,
|
||||
/// `sel::before` / `sel::after` rules matched against their ordinary base
|
||||
/// selector. Keeping their full declaration cascade supports both literal
|
||||
/// generated text and positioned decorative boxes.
|
||||
before_rules: Vec<PseudoRule>,
|
||||
after_rules: Vec<PseudoRule>,
|
||||
}
|
||||
|
||||
impl Stylesheet {
|
||||
@@ -68,8 +69,8 @@ impl Stylesheet {
|
||||
by_class: HashMap::new(),
|
||||
by_local: HashMap::new(),
|
||||
universal: Vec::new(),
|
||||
before_content: Vec::new(),
|
||||
after_content: Vec::new(),
|
||||
before_rules: Vec::new(),
|
||||
after_rules: Vec::new(),
|
||||
};
|
||||
let mut order = 0usize;
|
||||
for src in sources {
|
||||
@@ -79,15 +80,31 @@ impl Stylesheet {
|
||||
for (selector, decls) in parse_stylesheet_for_viewport(src, viewport) {
|
||||
let sel_trim = selector.trim();
|
||||
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)) {
|
||||
sheet.before_content.push((sel, content));
|
||||
if let Some(sel) = tree.compile_rule_selector(base) {
|
||||
let (normal_decls, important_decls) =
|
||||
crate::style::partition_declarations(&decls);
|
||||
sheet.before_rules.push(PseudoRule {
|
||||
sel,
|
||||
normal_decls,
|
||||
important_decls,
|
||||
order,
|
||||
});
|
||||
}
|
||||
order += 1;
|
||||
continue;
|
||||
}
|
||||
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)) {
|
||||
sheet.after_content.push((sel, content));
|
||||
if let Some(sel) = tree.compile_rule_selector(base) {
|
||||
let (normal_decls, important_decls) =
|
||||
crate::style::partition_declarations(&decls);
|
||||
sheet.after_rules.push(PseudoRule {
|
||||
sel,
|
||||
normal_decls,
|
||||
important_decls,
|
||||
order,
|
||||
});
|
||||
}
|
||||
order += 1;
|
||||
continue;
|
||||
}
|
||||
let Some(sel) = tree.compile_rule_selector(&selector) else { continue };
|
||||
@@ -106,26 +123,57 @@ impl Stylesheet {
|
||||
sheet
|
||||
}
|
||||
|
||||
/// The literal text (if any) that `::before`/`::after` rules inject for
|
||||
/// `nid`, as `(before, after)`. Last matching rule wins, approximating
|
||||
/// cascade order without tracking specificity for this small side table.
|
||||
pub fn pseudo_content(&self, tree: &DomTree, matcher: &mut Matcher, nid: NodeId) -> (Option<String>, Option<String>) {
|
||||
if self.before_content.is_empty() && self.after_content.is_empty() {
|
||||
return (None, None);
|
||||
pub fn pseudo_styles(
|
||||
&self,
|
||||
tree: &DomTree,
|
||||
matcher: &mut Matcher,
|
||||
nid: NodeId,
|
||||
host: &LayoutStyle,
|
||||
props: &HashMap<String, String>,
|
||||
) -> (Option<LayoutStyle>, Option<LayoutStyle>) {
|
||||
let build = |rules: &[PseudoRule], matcher: &mut Matcher| {
|
||||
let mut matched: Vec<(u32, usize, &PseudoRule)> = rules
|
||||
.iter()
|
||||
.filter(|rule| matcher.matches(tree, nid, &rule.sel))
|
||||
.map(|rule| (rule.sel.specificity(), rule.order, rule))
|
||||
.collect();
|
||||
if matched.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut before = None;
|
||||
for (sel, content) in &self.before_content {
|
||||
if matcher.matches(tree, nid, sel) {
|
||||
before = Some(content.clone());
|
||||
matched.sort_unstable_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
|
||||
let mut style = LayoutStyle::default();
|
||||
style.color = host.color;
|
||||
style.font_size = host.font_size;
|
||||
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);
|
||||
}
|
||||
}
|
||||
let mut after = None;
|
||||
for (sel, content) in &self.after_content {
|
||||
if matcher.matches(tree, nid, sel) {
|
||||
after = Some(content.clone());
|
||||
for &(_, _, rule) in &matched {
|
||||
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);
|
||||
}
|
||||
}
|
||||
(before, after)
|
||||
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 {
|
||||
|
||||
@@ -352,9 +352,16 @@ fn cascade_walk(
|
||||
) {
|
||||
this_props = std::rc::Rc::new(m);
|
||||
}
|
||||
let (before, after) = sheet.pseudo_content(tree, matcher, id);
|
||||
style.before_content = before;
|
||||
style.after_content = after;
|
||||
let (before_pseudo, after_pseudo) =
|
||||
sheet.pseudo_styles(tree, matcher, id, &style, &this_props);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -4882,6 +4889,33 @@ mod tests {
|
||||
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]
|
||||
fn table_cell_content_box_width_includes_padding_and_border_in_track() {
|
||||
let tree = parse_html(
|
||||
|
||||
@@ -251,6 +251,11 @@ pub struct LayoutStyle {
|
||||
/// o'clock per CSS, list of (rgba, optional 0..1 stop position)). Modern
|
||||
/// hero sections use gradients heavily; without this they paint white.
|
||||
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,
|
||||
/// the center is a fraction of the border box, and stops are normalized
|
||||
/// 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
|
||||
/// almost always sized).
|
||||
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
|
||||
/// the image's intrinsic dimensions rather than stretching it to the box.
|
||||
pub background_size_fit: Option<ObjectFit>,
|
||||
@@ -440,6 +448,11 @@ pub struct LayoutStyle {
|
||||
/// children, same as if it were real text content.
|
||||
pub before_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
|
||||
/// the surrounding inline flow from the outside, like plain `inline`
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
//! dependencies, so a screenshot is reproducible across hosts.
|
||||
|
||||
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};
|
||||
|
||||
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 fonts = collect_web_fonts(tree, base_url);
|
||||
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;
|
||||
// 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
|
||||
@@ -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
|
||||
// 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 let Some((angle, stops)) = &style.background_gradient {
|
||||
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(bg) = style.background_color {
|
||||
if let Some(path) = bg_path() {
|
||||
let mut paint = Paint::default();
|
||||
paint.set_color(Color::from_rgba8(bg[0], bg[1], bg[2], bg[3]));
|
||||
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,
|
||||
&rect,
|
||||
style.background_size,
|
||||
style.background_size_expression.as_deref(),
|
||||
style.background_size_fit,
|
||||
style.background_position,
|
||||
style.font_size.unwrap_or(16.0),
|
||||
root_font_size,
|
||||
viewport,
|
||||
&mut image_cache,
|
||||
) {
|
||||
// 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
|
||||
// four sharp edge rects.
|
||||
@@ -355,6 +384,11 @@ pub fn paint_dom(tree: &DomTree, viewport: (f32, f32), base_url: Option<&str>) -
|
||||
// overflow-visible region.
|
||||
if name.local.as_ref() == "svg" {
|
||||
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
|
||||
// document, outside the page's author stylesheet. Preserve the
|
||||
// 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>`,
|
||||
// and rewrite the href to a same-document `#id`. Same-document
|
||||
// `<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) {
|
||||
let mask = clip.and_then(|_| rect_clip_mask(pixmap.width(), pixmap.height(), &visible_rect));
|
||||
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 mut gs: Vec<GradientStop> = Vec::with_capacity(n);
|
||||
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);
|
||||
last = p;
|
||||
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(
|
||||
pixmap: &mut Pixmap,
|
||||
rect: &crate::Rect,
|
||||
@@ -1354,7 +1436,8 @@ fn normalized_stops(
|
||||
let count = stops.len();
|
||||
let mut normalized = Vec::with_capacity(count);
|
||||
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
|
||||
.unwrap_or_else(|| {
|
||||
if count <= 1 {
|
||||
@@ -1366,11 +1449,28 @@ fn normalized_stops(
|
||||
.clamp(0.0, 1.0)
|
||||
.max(last);
|
||||
last = position;
|
||||
normalized.push((position, *color));
|
||||
normalized.push((position, color));
|
||||
}
|
||||
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] {
|
||||
let t = t.clamp(0.0, 1.0);
|
||||
let Some(&(first_position, first_color)) = stops.first() else {
|
||||
@@ -1472,8 +1572,12 @@ fn background_image_rect(
|
||||
base_url: Option<&str>,
|
||||
box_rect: &crate::Rect,
|
||||
explicit_size: Option<(f32, f32)>,
|
||||
size_expression: Option<&str>,
|
||||
fit: Option<crate::ObjectFit>,
|
||||
position: (f32, f32),
|
||||
em: f32,
|
||||
rem: f32,
|
||||
viewport: (f32, f32),
|
||||
cache: &mut std::collections::HashMap<String, Option<Vec<u8>>>,
|
||||
) -> Option<crate::Rect> {
|
||||
let bytes = fetch_bytes(src, base_url, cache)?;
|
||||
@@ -1482,7 +1586,43 @@ fn background_image_rect(
|
||||
} else {
|
||||
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
|
||||
} else if let Some(fit) = fit {
|
||||
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
|
||||
/// intrinsic (width, height) so layout can size replaced elements that have no
|
||||
/// 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]
|
||||
fn later_element_paints_over_earlier() {
|
||||
// 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]
|
||||
fn font_face_parser_selects_ascii_subset_and_preserves_functional_src() {
|
||||
let css = r#"
|
||||
|
||||
@@ -434,14 +434,17 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
|
||||
if !value.trim().is_empty() {
|
||||
style.background_color = None;
|
||||
style.background_gradient = parse_linear_gradient(value);
|
||||
style.background_radial_gradient = parse_radial_gradient(value);
|
||||
style.background_conic_gradient = parse_conic_gradient(value);
|
||||
if style.background_gradient.is_none()
|
||||
&& style.background_radial_gradient.is_none()
|
||||
&& style.background_conic_gradient.is_none()
|
||||
{
|
||||
style.background_color = parse_color(value);
|
||||
}
|
||||
style.background_image = parse_url(value);
|
||||
style.background_size = None;
|
||||
style.background_size_expression = background_size_expression(value);
|
||||
style.background_size_fit = parse_background_size_fit(value);
|
||||
style.background_position = (0.0, 0.0);
|
||||
style.background_clip_text = false;
|
||||
@@ -449,11 +452,14 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
|
||||
}
|
||||
"background-image" => {
|
||||
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_image = parse_url(value);
|
||||
}
|
||||
"background-size" => {
|
||||
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);
|
||||
}
|
||||
"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))
|
||||
}
|
||||
|
||||
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.
|
||||
/// Angles follow CSS convention (0deg at 12 o'clock, clockwise); the center
|
||||
/// 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
|
||||
/// 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).
|
||||
@@ -3684,6 +3750,7 @@ mod tests {
|
||||
assert_eq!(s.background_conic_gradient, None);
|
||||
assert_eq!(s.background_image, 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_position, (0.0, 0.0));
|
||||
assert!(!s.background_clip_text);
|
||||
@@ -3698,6 +3765,16 @@ mod tests {
|
||||
contain.background_size_fit,
|
||||
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]
|
||||
|
||||
Reference in new issue
Block a user