render: shape atomic generated controls
This commit is contained in:
1 parent
9ffc071ce0
commit
237ba35639
6 files changed
+271
-26
No files matched your search
@@ -128,7 +128,6 @@ impl Stylesheet {
|
||||
tree: &DomTree,
|
||||
matcher: &mut Matcher,
|
||||
nid: NodeId,
|
||||
host: &LayoutStyle,
|
||||
props: &HashMap<String, String>,
|
||||
) -> (Option<LayoutStyle>, Option<LayoutStyle>) {
|
||||
let build = |rules: &[PseudoRule], matcher: &mut Matcher| {
|
||||
@@ -142,12 +141,6 @@ impl Stylesheet {
|
||||
}
|
||||
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);
|
||||
|
||||
@@ -353,7 +353,7 @@ fn cascade_walk(
|
||||
this_props = std::rc::Rc::new(m);
|
||||
}
|
||||
let (before_pseudo, after_pseudo) =
|
||||
sheet.pseudo_styles(tree, matcher, id, &style, &this_props);
|
||||
sheet.pseudo_styles(tree, matcher, id, &this_props);
|
||||
style.before_content = before_pseudo
|
||||
.as_ref()
|
||||
.filter(|pseudo| pseudo.position != Some(taffy::Position::Absolute))
|
||||
@@ -944,6 +944,116 @@ pub(crate) fn layout_dom_with_web_fonts(
|
||||
}
|
||||
}
|
||||
|
||||
// Pseudo-elements inherit the originating element's COMPUTED
|
||||
// values. They were cascaded beside the host before this
|
||||
// top-down pass, so inheriting its still-unresolved specified
|
||||
// values there made `font-size:.875rem` disappear and left a
|
||||
// positioned generated label at the 16px paint fallback.
|
||||
// Resolve pseudo-authored relative values against the host,
|
||||
// then fill every omitted inherited property from the host's
|
||||
// now-final computed style.
|
||||
let host_color = style.color;
|
||||
let host_font_size = style.font_size.unwrap_or(parent_fs);
|
||||
let host_weight = crate::style::used_font_weight(style);
|
||||
let host_family = style.font_family.clone();
|
||||
let host_line_height = style.line_height;
|
||||
let host_transform = style.text_transform;
|
||||
let host_italic = style.font_style_italic;
|
||||
let host_text_align = style.text_align;
|
||||
let host_invisible = style.effectively_invisible;
|
||||
let settle_pseudo = |pseudo: &mut crate::LayoutStyle| {
|
||||
if let Some(expression) = pseudo.font_size_expression.as_deref() {
|
||||
pseudo.font_size = crate::style::resolve_contextual_length(
|
||||
expression,
|
||||
host_font_size,
|
||||
root_fs,
|
||||
vw,
|
||||
vh,
|
||||
host_font_size,
|
||||
);
|
||||
} else if let Some(raw) = pseudo.font_size_raw {
|
||||
pseudo.font_size = Some(match raw {
|
||||
crate::Dimension::Percent(percent) => host_font_size * percent,
|
||||
crate::Dimension::Em(value) => host_font_size * value,
|
||||
dimension => match dimension.resolve(
|
||||
host_font_size,
|
||||
root_fs,
|
||||
vw,
|
||||
vh,
|
||||
) {
|
||||
crate::Dimension::Px(pixels) => pixels,
|
||||
_ => host_font_size,
|
||||
},
|
||||
});
|
||||
} else if pseudo.font_size.is_none() {
|
||||
pseudo.font_size = Some(host_font_size);
|
||||
}
|
||||
let pseudo_em = pseudo.font_size.unwrap_or(host_font_size);
|
||||
let weight = crate::style::computed_font_weight(
|
||||
pseudo.font_weight.as_deref(),
|
||||
host_weight,
|
||||
);
|
||||
pseudo.font_weight = Some(weight.to_string());
|
||||
if pseudo.font_family.is_none() {
|
||||
pseudo.font_family = host_family.clone();
|
||||
}
|
||||
if let Some(expression) = pseudo.line_height_expression.as_deref() {
|
||||
if let Some(resolved) = crate::style::resolve_contextual_length(
|
||||
expression,
|
||||
pseudo_em,
|
||||
root_fs,
|
||||
vw,
|
||||
vh,
|
||||
pseudo_em,
|
||||
) {
|
||||
pseudo.line_height = Some(
|
||||
if crate::style::line_height_expression_is_length(expression) {
|
||||
crate::LineHeight::Px(resolved)
|
||||
} else {
|
||||
crate::LineHeight::Ratio(resolved)
|
||||
},
|
||||
);
|
||||
}
|
||||
} else if let Some(crate::LineHeight::Relative(relative)) =
|
||||
pseudo.line_height
|
||||
{
|
||||
let pixels = match relative {
|
||||
crate::Dimension::Percent(percent) => pseudo_em * percent,
|
||||
dimension => match dimension.resolve(
|
||||
pseudo_em,
|
||||
root_fs,
|
||||
vw,
|
||||
vh,
|
||||
) {
|
||||
crate::Dimension::Px(pixels) => pixels,
|
||||
_ => pseudo_em,
|
||||
},
|
||||
};
|
||||
pseudo.line_height = Some(crate::LineHeight::Px(pixels));
|
||||
} else if pseudo.line_height.is_none() {
|
||||
pseudo.line_height = host_line_height;
|
||||
}
|
||||
if pseudo.color.is_none() {
|
||||
pseudo.color = host_color;
|
||||
}
|
||||
if pseudo.text_transform.is_none() {
|
||||
pseudo.text_transform = host_transform;
|
||||
}
|
||||
if pseudo.font_style_italic.is_none() {
|
||||
pseudo.font_style_italic = host_italic;
|
||||
}
|
||||
if pseudo.text_align.is_none() {
|
||||
pseudo.text_align = host_text_align;
|
||||
}
|
||||
pseudo.effectively_invisible = host_invisible;
|
||||
};
|
||||
if let Some(pseudo) = style.before_pseudo.as_deref_mut() {
|
||||
settle_pseudo(pseudo);
|
||||
}
|
||||
if let Some(pseudo) = style.after_pseudo.as_deref_mut() {
|
||||
settle_pseudo(pseudo);
|
||||
}
|
||||
|
||||
// Containing-block width handed to this element's children is
|
||||
// its own content-box width. A definite content-box width is
|
||||
// already that value; a border-box or auto width includes
|
||||
@@ -4961,4 +5071,28 @@ mod tests {
|
||||
assert_eq!(width("border-cell"), 100.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn positioned_pseudo_inherits_the_hosts_computed_font_metrics() {
|
||||
let tree = parse_html(
|
||||
r#"<style>
|
||||
html { font-size:16px }
|
||||
button { font-size:.875rem; line-height:1.5 }
|
||||
button::after {
|
||||
content:attr(text); position:absolute; inset:1px;
|
||||
}
|
||||
</style>
|
||||
<button id="cta" text="Get Started">Get Started</button>"#,
|
||||
);
|
||||
let laid = layout_dom(&tree, (1280.0, 720.0));
|
||||
let cta = tree.get_element_by_id("cta").unwrap();
|
||||
let host = &laid.styles[&cta];
|
||||
let pseudo = host.after_pseudo.as_ref().unwrap();
|
||||
|
||||
assert_eq!(host.font_size, Some(14.0));
|
||||
assert_eq!(pseudo.font_size, host.font_size);
|
||||
assert_eq!(pseudo.font_family, host.font_family);
|
||||
assert_eq!(pseudo.font_weight, host.font_weight);
|
||||
assert_eq!(pseudo.line_height, host.line_height);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -535,6 +535,48 @@ impl TextEngine {
|
||||
self.push_shaped_item(base, spans, collector.clip_fills)
|
||||
}
|
||||
|
||||
/// Shape generated text that owns a positioned pseudo box.
|
||||
///
|
||||
/// Positioned `::before`/`::after` boxes do not participate in the taffy
|
||||
/// tree, but their text still uses the same authored webfonts, variable
|
||||
/// weight selection, transformations, and glyph rasterizer as an ordinary
|
||||
/// inline formatting context. The caller measures/finalizes/paints the
|
||||
/// returned item immediately against the pseudo's resolved content box.
|
||||
pub(crate) fn push_generated_text(
|
||||
&mut self,
|
||||
text: &str,
|
||||
style: &LayoutStyle,
|
||||
) -> Option<usize> {
|
||||
let mut collector = Collector {
|
||||
last_was_space: true,
|
||||
clip_fills: Vec::new(),
|
||||
};
|
||||
let (family, weight) = resolve_loaded_font(
|
||||
style.font_family.as_deref(),
|
||||
crate::style::used_font_weight(style),
|
||||
style.font_style_italic.unwrap_or(false),
|
||||
&self.loaded_families,
|
||||
);
|
||||
let context = base_span_ctx(style, family, weight, &mut collector);
|
||||
let attrs = SpanAttrs {
|
||||
weight: context.weight,
|
||||
italic: context.italic,
|
||||
underline: context.underline,
|
||||
color: context.color,
|
||||
family: context.family,
|
||||
clip_fill: context.clip_fill,
|
||||
};
|
||||
let mut spans = Vec::new();
|
||||
push_text(
|
||||
text,
|
||||
context.transform,
|
||||
&attrs,
|
||||
&mut spans,
|
||||
&mut collector,
|
||||
);
|
||||
self.push_shaped_item(style, spans, collector.clip_fills)
|
||||
}
|
||||
|
||||
/// Shared tail of [`try_build`] / [`try_build_run`]: shape the collected
|
||||
/// spans into a cosmic-text buffer under `base`'s font metrics and
|
||||
/// alignment, and store it as a new inline item.
|
||||
@@ -936,7 +978,9 @@ fn is_pure_text_ifc(tree: &DomTree, id: NodeId, styles: &std::collections::HashM
|
||||
// the flex-column stand-ins our UA sheet uses for td/th/center). A real
|
||||
// flex/grid row with inline children is rare and better left to taffy.
|
||||
let flow = style.display == Display::Block
|
||||
|| (style.display == Display::Flex && style.flex_direction == Some(taffy::FlexDirection::Column));
|
||||
|| style.is_inline_block
|
||||
|| (style.display == Display::Flex
|
||||
&& style.flex_direction == Some(taffy::FlexDirection::Column));
|
||||
if !flow {
|
||||
return false;
|
||||
}
|
||||
@@ -1342,6 +1386,23 @@ mod tests {
|
||||
assert_eq!(semibold.1, 700);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_only_inline_block_keeps_an_internal_shaping_context() {
|
||||
let tree = obscura_dom::parse_html(
|
||||
"<span id='icon'>ligature_name</span>",
|
||||
);
|
||||
let icon = tree.get_element_by_id("icon").unwrap();
|
||||
let mut style = LayoutStyle::default();
|
||||
style.display = Display::Inline;
|
||||
style.is_inline_block = true;
|
||||
let styles = HashMap::from([(icon, style)]);
|
||||
|
||||
assert!(
|
||||
is_pure_text_ifc(&tree, icon, &styles),
|
||||
"atomic inline participation must not disable shaping inside the box"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clip_fill_only_for_transparent_clip_text() {
|
||||
let mut s = LayoutStyle::default();
|
||||
|
||||
@@ -804,6 +804,16 @@ pub(crate) fn to_taffy_style(style: &LayoutStyle) -> Style {
|
||||
s.align_content = style.align_content;
|
||||
if let Some(jc) = style.justify_content {
|
||||
s.justify_content = Some(jc);
|
||||
} else if style.is_inline_block {
|
||||
// Taffy's inline-box stand-in is a wrapping flex row. `text-align`
|
||||
// aligns each line of an inline-block's internal formatting context;
|
||||
// map that inherited value onto the row axis without affecting how
|
||||
// the atomic box itself participates in its parent's inline flow.
|
||||
s.justify_content = match style.text_align {
|
||||
Some(taffy::AlignItems::CENTER) => Some(taffy::JustifyContent::CENTER),
|
||||
Some(taffy::AlignItems::FLEX_END) => Some(taffy::JustifyContent::FLEX_END),
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
if let Some(fg) = style.flex_grow {
|
||||
s.flex_grow = fg;
|
||||
|
||||
@@ -276,6 +276,7 @@ pub fn paint_dom(tree: &DomTree, viewport: (f32, f32), base_url: Option<&str>) -
|
||||
.flatten()
|
||||
{
|
||||
paint_positioned_pseudo(
|
||||
&mut laid.text_engine,
|
||||
&mut pixmap,
|
||||
pseudo,
|
||||
&rect,
|
||||
@@ -1745,6 +1746,7 @@ fn split_background_size_components(value: &str) -> Vec<&str> {
|
||||
}
|
||||
|
||||
fn paint_positioned_pseudo(
|
||||
text_engine: &mut crate::inline::TextEngine,
|
||||
pixmap: &mut Pixmap,
|
||||
style: &crate::LayoutStyle,
|
||||
containing_block: &crate::Rect,
|
||||
@@ -1816,10 +1818,8 @@ fn paint_positioned_pseudo(
|
||||
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 Some(item) = text_engine.push_generated_text(content, style) else { return };
|
||||
let (text_width, text_height) = text_engine.measure(item, None);
|
||||
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) => {
|
||||
@@ -1828,23 +1828,14 @@ fn paint_positioned_pseudo(
|
||||
_ => 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::CENTER) => rect.y + (rect.height - text_height) / 2.0,
|
||||
Some(taffy::AlignItems::FLEX_END | taffy::AlignItems::END) => {
|
||||
rect.y + rect.height - style.padding.bottom - line_height
|
||||
rect.y + rect.height - style.padding.bottom - text_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),
|
||||
);
|
||||
text_engine.finalize(item, (x, y), text_width, Some(visible));
|
||||
text_engine.paint_item(item, pixmap, (0.0, 0.0));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2948,6 +2939,46 @@ mod tests {
|
||||
assert!(dark_pixels > 10, "generated attr() text must be painted");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn later_positioned_pseudo_opaquely_covers_the_earlier_one() {
|
||||
let tree = parse_html(
|
||||
r#"<html><head><style>
|
||||
body { margin:0 }
|
||||
#cta {
|
||||
position:relative; width:120px; height:40px; padding:0;
|
||||
color:transparent; background:black;
|
||||
}
|
||||
#cta::before {
|
||||
content:"before";
|
||||
position:absolute; inset:1px;
|
||||
display:flex; align-items:center; justify-content:center;
|
||||
color:red; background:red;
|
||||
}
|
||||
#cta::after {
|
||||
content:"after";
|
||||
position:absolute; inset:1px;
|
||||
display:flex; align-items:center; justify-content:center;
|
||||
color:blue; background:white;
|
||||
}
|
||||
</style></head><body><button id="cta">host</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),
|
||||
"::after's opaque background must cover ::before"
|
||||
);
|
||||
let red_pixels = (1..119)
|
||||
.flat_map(|x| (1..39).map(move |y| (x, y)))
|
||||
.filter(|&(x, y)| {
|
||||
let pixel = pixmap.pixel(x, y).unwrap();
|
||||
pixel.red() > 180 && pixel.green() < 80 && pixel.blue() < 80
|
||||
})
|
||||
.count();
|
||||
assert_eq!(red_pixels, 0, "::before must not bleed through ::after");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn later_element_paints_over_earlier() {
|
||||
// A blue div nested inside a red one: both cover the origin, and blue
|
||||
|
||||
@@ -107,6 +107,14 @@ pub fn ua_style(tag: &str) -> LayoutStyle {
|
||||
} else if tag == "a" {
|
||||
style.color = Some([0, 0, 238, 255]); // blue
|
||||
style.underline = Some(true); // UA default: links are underlined
|
||||
} else if tag == "button" {
|
||||
// HTML buttons are inline-block controls whose anonymous inner
|
||||
// content is centered by the browser UA sheet. Keep the atomic outer
|
||||
// participation separate from the internal text alignment: authored
|
||||
// display/text-align declarations still cascade over both values.
|
||||
style.display = Display::Inline;
|
||||
style.is_inline_block = true;
|
||||
style.text_align = Some(taffy::AlignItems::CENTER);
|
||||
} else if tag == "input" {
|
||||
// Native text controls are atomic inline-level boxes with their own
|
||||
// platform font and intrinsic border-box dimensions; they do not
|
||||
@@ -3528,6 +3536,14 @@ mod tests {
|
||||
assert_eq!(collapsed.border_collapse, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn button_ua_style_is_a_centered_atomic_inline_box() {
|
||||
let button = ua_style("button");
|
||||
assert_eq!(button.display, crate::Display::Inline);
|
||||
assert!(button.is_inline_block);
|
||||
assert_eq!(button.text_align, Some(taffy::AlignItems::CENTER));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn border_none_clears_native_and_per_side_widths() {
|
||||
let input = compute_style("input", Some("border:none"));
|
||||
|
||||
Reference in new issue
Block a user