render: honor generated controls and border resets
This commit is contained in:
1 parent
2c12b5a03a
commit
9ec43134ae
4 files changed
+181
-35
No files matched your search
@@ -152,15 +152,15 @@ impl Stylesheet {
|
||||
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);
|
||||
if let Some(value) = extract_content(&expanded, tree, nid) {
|
||||
content = value;
|
||||
}
|
||||
}
|
||||
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);
|
||||
if let Some(value) = extract_content(&expanded, tree, nid) {
|
||||
content = value;
|
||||
}
|
||||
}
|
||||
style.before_content = content;
|
||||
@@ -447,30 +447,44 @@ fn strip_pseudo_element<'a>(selector: &'a str, which: &str) -> Option<&'a str> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Pull a plain quoted-string `content` value out of a declaration list
|
||||
/// (`content: ", "` -> `Some(", ")`). Deliberately narrow: real `content`
|
||||
/// also accepts `counter()`, `attr()`, `url()` and concatenations of these,
|
||||
/// none of which apply here (no counters or `attr()`-driven content on the
|
||||
/// pages this targets), so anything other than a single quoted string is
|
||||
/// left unhandled rather than guessed at. When `content` is declared more
|
||||
/// than once (the `content: X; content: X / Y` accessible-alt-text pattern),
|
||||
/// the last one wins, matching normal CSS declaration order.
|
||||
fn extract_string_content(decls: &str) -> Option<String> {
|
||||
/// Return the final valid `content` declaration in a declaration list.
|
||||
///
|
||||
/// The outer option says whether a declaration was found; the inner option is
|
||||
/// the generated text (`none`/`normal` suppress the pseudo). Along with quoted
|
||||
/// strings, support the common `attr(name)` form used by component-library
|
||||
/// buttons and badges. The attribute is resolved against the originating
|
||||
/// element, as CSS generated content requires.
|
||||
fn extract_content(decls: &str, tree: &DomTree, nid: NodeId) -> Option<Option<String>> {
|
||||
let mut result = None;
|
||||
let mut search_from = 0;
|
||||
while let Some(rel_idx) = decls[search_from..].find("content") {
|
||||
let idx = search_from + rel_idx;
|
||||
search_from = idx + "content".len();
|
||||
let after = decls[search_from..].trim_start();
|
||||
let Some(after_colon) = after.strip_prefix(':') else { continue };
|
||||
let after_colon = after_colon.trim_start();
|
||||
let Some(quote) = after_colon.chars().next() else { continue };
|
||||
if quote != '"' && quote != '\'' {
|
||||
for raw in crate::style::split_declarations(decls) {
|
||||
let Some((name, value)) = raw.split_once(':') else { continue };
|
||||
if !name.trim().eq_ignore_ascii_case("content") {
|
||||
continue;
|
||||
}
|
||||
let rest = &after_colon[quote.len_utf8()..];
|
||||
let Some(end) = rest.find(quote) else { continue };
|
||||
result = Some(unescape_css_string(&rest[..end]));
|
||||
let value = value.trim();
|
||||
if value.eq_ignore_ascii_case("none") || value.eq_ignore_ascii_case("normal") {
|
||||
result = Some(None);
|
||||
continue;
|
||||
}
|
||||
let parsed = if let Some(quote) =
|
||||
value.chars().next().filter(|ch| matches!(ch, '"' | '\''))
|
||||
{
|
||||
let rest = &value[quote.len_utf8()..];
|
||||
rest.find(quote).map(|end| unescape_css_string(&rest[..end]))
|
||||
} else if let Some(rest) = value.strip_prefix("attr(") {
|
||||
rest.find(')').map(|end| {
|
||||
tree.get_node(nid)
|
||||
.and_then(|node| {
|
||||
node.get_attribute(rest[..end].trim()).map(str::to_owned)
|
||||
})
|
||||
.unwrap_or_default()
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(parsed) = parsed {
|
||||
result = Some(Some(parsed));
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
@@ -1164,6 +1178,22 @@ fn split_media_query_list(query: &str) -> Vec<&str> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn generated_content_resolves_host_attributes_and_resets() {
|
||||
let tree = obscura_dom::parse_html(
|
||||
r#"<button id="cta" data-label="Get Started"></button>"#,
|
||||
);
|
||||
let cta = tree.query_selector("#cta").unwrap().unwrap();
|
||||
assert_eq!(
|
||||
extract_content("content:attr(data-label)", &tree, cta),
|
||||
Some(Some("Get Started".to_string()))
|
||||
);
|
||||
assert_eq!(
|
||||
extract_content(r#"content:"fallback";content:none"#, &tree, cta),
|
||||
Some(None)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keyframes_extract_only_the_final_animation_state() {
|
||||
let css = r#"
|
||||
|
||||
@@ -356,9 +356,11 @@ fn cascade_walk(
|
||||
sheet.pseudo_styles(tree, matcher, id, &style, &this_props);
|
||||
style.before_content = before_pseudo
|
||||
.as_ref()
|
||||
.filter(|pseudo| pseudo.position != Some(taffy::Position::Absolute))
|
||||
.and_then(|pseudo| pseudo.before_content.clone());
|
||||
style.after_content = after_pseudo
|
||||
.as_ref()
|
||||
.filter(|pseudo| pseudo.position != Some(taffy::Position::Absolute))
|
||||
.and_then(|pseudo| pseudo.before_content.clone());
|
||||
style.before_pseudo = before_pseudo.map(Box::new);
|
||||
style.after_pseudo = after_pseudo.map(Box::new);
|
||||
|
||||
@@ -271,7 +271,10 @@ pub fn paint_dom(tree: &DomTree, viewport: (f32, f32), base_url: Option<&str>) -
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(pseudo) = style.before_pseudo.as_deref() {
|
||||
for pseudo in [style.before_pseudo.as_deref(), style.after_pseudo.as_deref()]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
paint_positioned_pseudo(
|
||||
&mut pixmap,
|
||||
pseudo,
|
||||
@@ -1788,12 +1791,16 @@ fn paint_positioned_pseudo(
|
||||
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 path = if style.border_radius > 0.5 {
|
||||
rounded_rect_path(visible.x, visible.y, visible.width, visible.height, style.border_radius)
|
||||
} else {
|
||||
Rect::from_xywh(visible.x, visible.y, visible.width, visible.height).and_then(|rect| {
|
||||
let mut builder = PathBuilder::new();
|
||||
builder.push_rect(sk_rect);
|
||||
let Some(path) = builder.finish() else { return };
|
||||
builder.push_rect(rect);
|
||||
builder.finish()
|
||||
})
|
||||
};
|
||||
let Some(path) = path 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]));
|
||||
@@ -1808,6 +1815,37 @@ fn paint_positioned_pseudo(
|
||||
if let Some((angle, stops)) = &style.background_gradient {
|
||||
paint_linear_gradient(pixmap, &path, &rect, *angle, stops);
|
||||
}
|
||||
if let Some(content) = style.before_content.as_deref().filter(|content| !content.is_empty()) {
|
||||
let font_size = style.font_size.unwrap_or(16.0);
|
||||
let bold = crate::style::used_font_weight(style) >= 600;
|
||||
let text_width = measure_text(content, font_size, bold, style.font_family.as_deref());
|
||||
let line_height = crate::inline::used_line_height(style);
|
||||
let x = match style.justify_content {
|
||||
Some(taffy::JustifyContent::CENTER) => rect.x + (rect.width - text_width) / 2.0,
|
||||
Some(taffy::JustifyContent::FLEX_END | taffy::JustifyContent::END) => {
|
||||
rect.x + rect.width - style.padding.right - text_width
|
||||
}
|
||||
_ => rect.x + style.padding.left,
|
||||
};
|
||||
let y = match style.align_items {
|
||||
Some(taffy::AlignItems::CENTER) => rect.y + (rect.height - line_height) / 2.0,
|
||||
Some(taffy::AlignItems::FLEX_END | taffy::AlignItems::END) => {
|
||||
rect.y + rect.height - style.padding.bottom - line_height
|
||||
}
|
||||
_ => rect.y + style.padding.top,
|
||||
};
|
||||
draw_text(
|
||||
pixmap,
|
||||
content,
|
||||
x,
|
||||
y,
|
||||
style.color.unwrap_or([0, 0, 0, 255]),
|
||||
font_size,
|
||||
bold,
|
||||
style.font_family.as_deref(),
|
||||
Some(visible),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch every `<img>` once (seeding `cache` for the paint pass) and record its
|
||||
@@ -2874,6 +2912,42 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paints_positioned_attr_content_over_the_host_background() {
|
||||
let tree = parse_html(
|
||||
r#"<html><head><style>
|
||||
body { margin:0 }
|
||||
#cta {
|
||||
position:relative; width:120px; height:40px; border:0;
|
||||
padding:0; color:transparent; background:red;
|
||||
}
|
||||
#cta::before {
|
||||
content:attr(data-label);
|
||||
position:absolute; inset:1px;
|
||||
display:flex; align-items:center; justify-content:center;
|
||||
border-radius:4px; color:black; background:white;
|
||||
}
|
||||
</style></head><body>
|
||||
<button id="cta" data-label="Get Started">Get Started</button>
|
||||
</body></html>"#,
|
||||
);
|
||||
let pixmap = paint_dom(&tree, (140.0, 60.0), None).expect("pixmap");
|
||||
let inner = pixmap.pixel(5, 5).expect("inner pixel");
|
||||
assert_eq!(
|
||||
(inner.red(), inner.green(), inner.blue()),
|
||||
(255, 255, 255),
|
||||
"the generated box must cover the red host background"
|
||||
);
|
||||
let dark_pixels = (35..85)
|
||||
.flat_map(|x| (8..32).map(move |y| (x, y)))
|
||||
.filter(|&(x, y)| {
|
||||
let pixel = pixmap.pixel(x, y).unwrap();
|
||||
pixel.red() < 100 && pixel.green() < 100 && pixel.blue() < 100
|
||||
})
|
||||
.count();
|
||||
assert!(dark_pixels > 10, "generated attr() text must be painted");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn later_element_paints_over_earlier() {
|
||||
// A blue div nested inside a red one: both cover the origin, and blue
|
||||
|
||||
@@ -409,6 +409,13 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
|
||||
}
|
||||
}
|
||||
"border" => {
|
||||
if value.split_whitespace().any(|token| {
|
||||
token.eq_ignore_ascii_case("none") || token.eq_ignore_ascii_case("hidden")
|
||||
}) {
|
||||
style.border = Edges::default();
|
||||
style.border_color = None;
|
||||
return;
|
||||
}
|
||||
for p in value.split_whitespace() {
|
||||
if let Some(c) = parse_color(p) {
|
||||
style.border_color = Some(c);
|
||||
@@ -418,10 +425,18 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
|
||||
}
|
||||
}
|
||||
"border-width" => { if let Some(e) = edges(value) { style.border = e; } }
|
||||
"border-top-width" | "border-top" => set_edge(&mut style.border, Side::Top, px(value)),
|
||||
"border-right-width" | "border-right" => set_edge(&mut style.border, Side::Right, px(value)),
|
||||
"border-bottom-width" | "border-bottom" => set_edge(&mut style.border, Side::Bottom, px(value)),
|
||||
"border-left-width" | "border-left" => set_edge(&mut style.border, Side::Left, px(value)),
|
||||
"border-top-width" | "border-top" => {
|
||||
set_edge(&mut style.border, Side::Top, border_side_width(value))
|
||||
}
|
||||
"border-right-width" | "border-right" => {
|
||||
set_edge(&mut style.border, Side::Right, border_side_width(value))
|
||||
}
|
||||
"border-bottom-width" | "border-bottom" => {
|
||||
set_edge(&mut style.border, Side::Bottom, border_side_width(value))
|
||||
}
|
||||
"border-left-width" | "border-left" => {
|
||||
set_edge(&mut style.border, Side::Left, border_side_width(value))
|
||||
}
|
||||
"background-color" => style.background_color = parse_color(value),
|
||||
"background" => {
|
||||
// A shorthand resets every omitted background longhand to its
|
||||
@@ -1906,6 +1921,16 @@ fn hsl_to_rgba(h: f32, s: f32, l: f32, a: u8) -> [u8; 4] {
|
||||
|
||||
enum Side { Top, Right, Bottom, Left }
|
||||
|
||||
fn border_side_width(value: &str) -> Option<f32> {
|
||||
if value.split_whitespace().any(|token| {
|
||||
token.eq_ignore_ascii_case("none") || token.eq_ignore_ascii_case("hidden")
|
||||
}) {
|
||||
Some(0.0)
|
||||
} else {
|
||||
px(value)
|
||||
}
|
||||
}
|
||||
|
||||
fn set_edge(edges: &mut Edges, side: Side, v: Option<f32>) {
|
||||
let v = match v { Some(v) => v, None => return };
|
||||
match side {
|
||||
@@ -3503,6 +3528,21 @@ mod tests {
|
||||
assert_eq!(collapsed.border_collapse, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn border_none_clears_native_and_per_side_widths() {
|
||||
let input = compute_style("input", Some("border:none"));
|
||||
assert_eq!(input.border, Edges::default());
|
||||
|
||||
let side = compute_style(
|
||||
"div",
|
||||
Some("border:3px solid red;border-left:none"),
|
||||
);
|
||||
assert_eq!(side.border.top, 3.0);
|
||||
assert_eq!(side.border.right, 3.0);
|
||||
assert_eq!(side.border.bottom, 3.0);
|
||||
assert_eq!(side.border.left, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn item_self_alignment_parses_and_resets() {
|
||||
let aligned = compute_style(
|
||||
|
||||
Reference in new issue
Block a user