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,
|
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>,
|
||||||
|
) -> (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;
|
matched.sort_unstable_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
|
||||||
for (sel, content) in &self.before_content {
|
let mut style = LayoutStyle::default();
|
||||||
if matcher.matches(tree, nid, sel) {
|
style.color = host.color;
|
||||||
before = Some(content.clone());
|
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 &(_, _, rule) in &matched {
|
||||||
for (sel, content) in &self.after_content {
|
let expanded = substitute_vars(&rule.important_decls, props, 0);
|
||||||
if matcher.matches(tree, nid, sel) {
|
crate::style::apply_declarations(&mut style, &expanded);
|
||||||
after = Some(content.clone());
|
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 {
|
pub fn is_empty(&self) -> bool {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
@@ -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#"
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
Reference in new issue
Block a user