Close modern layout and image geometry gaps
This commit is contained in:
1 parent
1a0c7096fc
commit
52e973afc9
8 files changed
+1545
-114
No files matched your search
Generated
+2
@@ -1539,6 +1539,8 @@ checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"byteorder-lite",
|
"byteorder-lite",
|
||||||
|
"color_quant",
|
||||||
|
"gif",
|
||||||
"image-webp",
|
"image-webp",
|
||||||
"moxcms",
|
"moxcms",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ cosmic-text = { version = "=0.14.2", default-features = false, features = ["std"
|
|||||||
# and the renderer consume the same canonical variation coordinates for
|
# and the renderer consume the same canonical variation coordinates for
|
||||||
# shaping and rasterization.
|
# shaping and rasterization.
|
||||||
swash = { version = "=0.2.0", default-features = false, features = ["std", "scale", "render"], optional = true }
|
swash = { version = "=0.2.0", default-features = false, features = ["std", "scale", "render"], optional = true }
|
||||||
image = { version = "0.25", default-features = false, features = ["jpeg", "png", "webp"], optional = true }
|
image = { version = "0.25", default-features = false, features = ["bmp", "gif", "ico", "jpeg", "png", "webp"], optional = true }
|
||||||
ureq = { version = "2.9", default-features = false, features = ["tls"], optional = true }
|
ureq = { version = "2.9", default-features = false, features = ["tls"], optional = true }
|
||||||
base64 = { workspace = true, optional = true }
|
base64 = { workspace = true, optional = true }
|
||||||
# SVG rasterization for <img src="*.svg">, inline <svg>, and background-image
|
# SVG rasterization for <img src="*.svg">, inline <svg>, and background-image
|
||||||
|
|||||||
@@ -126,13 +126,13 @@ fn native_button_intrinsic_content(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for child in tree.children(id) {
|
for child in rendered_children(tree, id) {
|
||||||
walk(tree, child, styles, font_size, content);
|
walk(tree, child, styles, font_size, content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut content = NativeButtonIntrinsicContent::default();
|
let mut content = NativeButtonIntrinsicContent::default();
|
||||||
for child in tree.children(root) {
|
for child in rendered_children(tree, root) {
|
||||||
walk(tree, child, styles, font_size, &mut content);
|
walk(tree, child, styles, font_size, &mut content);
|
||||||
}
|
}
|
||||||
content
|
content
|
||||||
@@ -1104,7 +1104,7 @@ fn resolve_clip_rects(
|
|||||||
}
|
}
|
||||||
_ => inherited,
|
_ => inherited,
|
||||||
};
|
};
|
||||||
for cid in tree.children(id) {
|
for cid in rendered_children(tree, id) {
|
||||||
resolve_clip_rects(
|
resolve_clip_rects(
|
||||||
tree,
|
tree,
|
||||||
cid,
|
cid,
|
||||||
@@ -1305,22 +1305,7 @@ fn apply_picture_source_hints(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(kind) = source.get_attribute("type") {
|
if let Some(kind) = source.get_attribute("type") {
|
||||||
let kind = kind.trim().to_ascii_lowercase();
|
if !crate::source_type_supported(kind) {
|
||||||
if !kind.is_empty()
|
|
||||||
&& !matches!(
|
|
||||||
kind.as_str(),
|
|
||||||
"image/avif"
|
|
||||||
| "image/bmp"
|
|
||||||
| "image/gif"
|
|
||||||
| "image/jpeg"
|
|
||||||
| "image/jpg"
|
|
||||||
| "image/png"
|
|
||||||
| "image/svg+xml"
|
|
||||||
| "image/webp"
|
|
||||||
| "image/x-icon"
|
|
||||||
| "image/vnd.microsoft.icon"
|
|
||||||
)
|
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1670,7 +1655,7 @@ fn resolve_css_counters(tree: &DomTree, styles: &mut HashMap<NodeId, crate::Layo
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut child_scopes = Vec::new();
|
let mut child_scopes = Vec::new();
|
||||||
for child in tree.children(id) {
|
for child in rendered_children(tree, id) {
|
||||||
child_scopes.extend(walk(tree, child, styles, counters));
|
child_scopes.extend(walk(tree, child, styles, counters));
|
||||||
}
|
}
|
||||||
counters.pop_created(&child_scopes);
|
counters.pop_created(&child_scopes);
|
||||||
@@ -2054,6 +2039,11 @@ fn layout_dom_once(
|
|||||||
// Top-down inheritance of the properties CSS inherits by default.
|
// Top-down inheritance of the properties CSS inherits by default.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct Inherited {
|
struct Inherited {
|
||||||
|
display: crate::Display,
|
||||||
|
display_contents: bool,
|
||||||
|
is_inline_block: bool,
|
||||||
|
flow_root: bool,
|
||||||
|
is_table_box: bool,
|
||||||
color: Option<[u8; 4]>,
|
color: Option<[u8; 4]>,
|
||||||
font_size: Option<f32>,
|
font_size: Option<f32>,
|
||||||
font_weight: u16,
|
font_weight: u16,
|
||||||
@@ -2094,6 +2084,13 @@ fn layout_dom_once(
|
|||||||
impl Default for Inherited {
|
impl Default for Inherited {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Inherited {
|
Inherited {
|
||||||
|
// CSS Display's initial outer/inner value is inline flow.
|
||||||
|
// The root is blockified after computed values settle.
|
||||||
|
display: crate::Display::Inline,
|
||||||
|
display_contents: false,
|
||||||
|
is_inline_block: false,
|
||||||
|
flow_root: false,
|
||||||
|
is_table_box: false,
|
||||||
color: None,
|
color: None,
|
||||||
font_size: None,
|
font_size: None,
|
||||||
font_weight: 400,
|
font_weight: 400,
|
||||||
@@ -2169,6 +2166,22 @@ fn layout_dom_once(
|
|||||||
let mut child_cb_width = inh.cb_width;
|
let mut child_cb_width = inh.cb_width;
|
||||||
let mut child_cb_height_definite = false;
|
let mut child_cb_height_definite = false;
|
||||||
if let Some(style) = styles.get_mut(&id) {
|
if let Some(style) = styles.get_mut(&id) {
|
||||||
|
if style.display_inherit {
|
||||||
|
style.display = inh.display;
|
||||||
|
style.display_contents = inh.display_contents;
|
||||||
|
style.is_inline_block = inh.is_inline_block;
|
||||||
|
style.flow_root = inh.flow_root;
|
||||||
|
style.is_table_box = inh.is_table_box;
|
||||||
|
// `internal_flex_container` describes an engine-only UA
|
||||||
|
// approximation, not the computed CSS display value.
|
||||||
|
style.internal_flex_container = false;
|
||||||
|
style.display_inherit = false;
|
||||||
|
}
|
||||||
|
inh.display = style.display;
|
||||||
|
inh.display_contents = style.display_contents;
|
||||||
|
inh.is_inline_block = style.is_inline_block;
|
||||||
|
inh.flow_root = style.flow_root;
|
||||||
|
inh.is_table_box = style.is_table_box;
|
||||||
match style.color { Some(c) => inh.color = Some(c), None => style.color = inh.color }
|
match style.color { Some(c) => inh.color = Some(c), None => style.color = inh.color }
|
||||||
// Resolve a relative font-size against the PARENT (em/%) or
|
// Resolve a relative font-size against the PARENT (em/%) or
|
||||||
// ROOT (rem) font-size before inheriting it downward.
|
// ROOT (rem) font-size before inheriting it downward.
|
||||||
@@ -2602,6 +2615,11 @@ fn layout_dom_once(
|
|||||||
let host_text_align = style.text_align;
|
let host_text_align = style.text_align;
|
||||||
let host_text_indent = style.text_indent;
|
let host_text_indent = style.text_indent;
|
||||||
let host_invisible = style.effectively_invisible;
|
let host_invisible = style.effectively_invisible;
|
||||||
|
let host_display = style.display;
|
||||||
|
let host_display_contents = style.display_contents;
|
||||||
|
let host_is_inline_block = style.is_inline_block;
|
||||||
|
let host_flow_root = style.flow_root;
|
||||||
|
let host_is_table_box = style.is_table_box;
|
||||||
let host_overflow_x = if style.overflow_scroll_x {
|
let host_overflow_x = if style.overflow_scroll_x {
|
||||||
2
|
2
|
||||||
} else if style.overflow_clip_x {
|
} else if style.overflow_clip_x {
|
||||||
@@ -2617,6 +2635,15 @@ fn layout_dom_once(
|
|||||||
0
|
0
|
||||||
};
|
};
|
||||||
let settle_pseudo = |pseudo: &mut crate::LayoutStyle| {
|
let settle_pseudo = |pseudo: &mut crate::LayoutStyle| {
|
||||||
|
if pseudo.display_inherit {
|
||||||
|
pseudo.display = host_display;
|
||||||
|
pseudo.display_contents = host_display_contents;
|
||||||
|
pseudo.is_inline_block = host_is_inline_block;
|
||||||
|
pseudo.flow_root = host_flow_root;
|
||||||
|
pseudo.is_table_box = host_is_table_box;
|
||||||
|
pseudo.internal_flex_container = false;
|
||||||
|
pseudo.display_inherit = false;
|
||||||
|
}
|
||||||
if pseudo.overflow_inherit_x {
|
if pseudo.overflow_inherit_x {
|
||||||
pseudo.overflow_specified_x = host_overflow_x;
|
pseudo.overflow_specified_x = host_overflow_x;
|
||||||
pseudo.overflow_inherit_x = false;
|
pseudo.overflow_inherit_x = false;
|
||||||
@@ -3185,8 +3212,8 @@ fn layout_dom_once(
|
|||||||
// Apply fetched intrinsic image sizes. A replaced element with no
|
// Apply fetched intrinsic image sizes. A replaced element with no
|
||||||
// explicit dimensions must size from its intrinsic pixels (else it is
|
// explicit dimensions must size from its intrinsic pixels (else it is
|
||||||
// 0x0 and never paints); with one dimension given, the aspect ratio
|
// 0x0 and never paints); with one dimension given, the aspect ratio
|
||||||
// fills the other. `max-width:100%` (a UA default for img) still caps
|
// fills the other. An authored `max-width:100%` still caps it to the
|
||||||
// it to the container, and the aspect ratio keeps it proportional.
|
// container, and the aspect ratio keeps it proportional.
|
||||||
for (&nid, &(iw, ih)) in intrinsic {
|
for (&nid, &(iw, ih)) in intrinsic {
|
||||||
if iw <= 0.0 || ih <= 0.0 {
|
if iw <= 0.0 || ih <= 0.0 {
|
||||||
continue;
|
continue;
|
||||||
@@ -4126,7 +4153,7 @@ fn resolve_grid_areas(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for cid in tree.children(id) {
|
for cid in rendered_children(tree, id) {
|
||||||
let Some(cstyle) = styles.get_mut(&cid) else { continue };
|
let Some(cstyle) = styles.get_mut(&cid) else { continue };
|
||||||
let Some(name) = cstyle.grid_area_name.clone() else { continue };
|
let Some(name) = cstyle.grid_area_name.clone() else { continue };
|
||||||
if let Some(&(r0, r1, c0, c1)) = spans.get(&name) {
|
if let Some(&(r0, r1, c0, c1)) = spans.get(&name) {
|
||||||
@@ -4146,7 +4173,7 @@ fn resolve_grid_areas(
|
|||||||
// Named grid lines: resolve children placed with `grid-column`/`grid-row`
|
// Named grid lines: resolve children placed with `grid-column`/`grid-row`
|
||||||
// values that reference a line name against this container's maps.
|
// values that reference a line name against this container's maps.
|
||||||
if col_lines.is_some() || row_lines.is_some() {
|
if col_lines.is_some() || row_lines.is_some() {
|
||||||
for cid in tree.children(id) {
|
for cid in rendered_children(tree, id) {
|
||||||
let Some(cstyle) = styles.get_mut(&cid) else { continue };
|
let Some(cstyle) = styles.get_mut(&cid) else { continue };
|
||||||
if let (Some(raw), Some(map)) = (cstyle.grid_column_raw.clone(), &col_lines) {
|
if let (Some(raw), Some(map)) = (cstyle.grid_column_raw.clone(), &col_lines) {
|
||||||
if let Some(l) = resolve_named_placement(&raw, map) {
|
if let Some(l) = resolve_named_placement(&raw, map) {
|
||||||
@@ -4978,7 +5005,7 @@ fn apply_float_continuations(
|
|||||||
let mut current = continuation.owner;
|
let mut current = continuation.owner;
|
||||||
while let Some(parent) = tree.get_node(current).and_then(|node| node.parent)
|
while let Some(parent) = tree.get_node(current).and_then(|node| node.parent)
|
||||||
{
|
{
|
||||||
let siblings = tree.children(parent);
|
let siblings = rendered_children(tree, parent);
|
||||||
let Some(index) = siblings.iter().position(|candidate| *candidate == current)
|
let Some(index) = siblings.iter().position(|candidate| *candidate == current)
|
||||||
else {
|
else {
|
||||||
break;
|
break;
|
||||||
@@ -5507,12 +5534,47 @@ fn resolve_static_positions_and_reparent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Does `id` have any direct child that is inline-level (a non-whitespace
|
/// Return the DOM children that generate boxes for `id`.
|
||||||
/// text node, or an element whose resolved display is `Inline`)? Used to
|
pub(crate) fn rendered_children(tree: &DomTree, id: NodeId) -> Vec<NodeId> {
|
||||||
/// decide whether a block container needs the flex-row-wrap approximation of
|
let Some(node) = tree.get_node(id) else {
|
||||||
/// an inline formatting context.
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let is_closed_html_details = node.as_element().is_some_and(|name| {
|
||||||
|
name.ns.as_ref() == "http://www.w3.org/1999/xhtml"
|
||||||
|
&& name.local.as_ref() == "details"
|
||||||
|
&& node.get_attribute("open").is_none()
|
||||||
|
});
|
||||||
|
if !is_closed_html_details {
|
||||||
|
return tree.children(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTML gives a closed <details> a rendered child list containing only its
|
||||||
|
// first direct <summary> element child. Source text before that summary,
|
||||||
|
// non-summary elements, later summaries, and all of their descendants
|
||||||
|
// remain in the DOM but generate no boxes until the `open` attribute is
|
||||||
|
// present. Filtering at the box-tree boundary keeps both layout
|
||||||
|
// classification and paint/text-run construction from observing the
|
||||||
|
// hidden subtree.
|
||||||
|
tree.children(id)
|
||||||
|
.into_iter()
|
||||||
|
.find(|child| {
|
||||||
|
tree.get_node(*child).is_some_and(|child| {
|
||||||
|
child.as_element().is_some_and(|name| {
|
||||||
|
name.ns.as_ref() == "http://www.w3.org/1999/xhtml"
|
||||||
|
&& name.local.as_ref() == "summary"
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.into_iter()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Does `id` have any direct rendered child that is inline-level (a
|
||||||
|
/// non-whitespace text node, or an element whose resolved display is
|
||||||
|
/// `Inline`)? Used to decide whether a block container needs the flex-row-wrap
|
||||||
|
/// approximation of an inline formatting context.
|
||||||
fn has_inline_content(tree: &DomTree, id: NodeId, styles: &HashMap<NodeId, crate::LayoutStyle>) -> bool {
|
fn has_inline_content(tree: &DomTree, id: NodeId, styles: &HashMap<NodeId, crate::LayoutStyle>) -> bool {
|
||||||
tree.children(id).into_iter().any(|cid| {
|
rendered_children(tree, id).into_iter().any(|cid| {
|
||||||
let Some(node) = tree.get_node(cid) else { return false };
|
let Some(node) = tree.get_node(cid) else { return false };
|
||||||
match &node.data {
|
match &node.data {
|
||||||
obscura_dom::tree::NodeData::Text { contents } => !contents.trim().is_empty(),
|
obscura_dom::tree::NodeData::Text { contents } => !contents.trim().is_empty(),
|
||||||
@@ -5562,7 +5624,7 @@ fn blockify_layout_children(
|
|||||||
parent: NodeId,
|
parent: NodeId,
|
||||||
styles: &mut HashMap<NodeId, crate::LayoutStyle>,
|
styles: &mut HashMap<NodeId, crate::LayoutStyle>,
|
||||||
) {
|
) {
|
||||||
for child in tree.children(parent) {
|
for child in rendered_children(tree, parent) {
|
||||||
let transparent = styles
|
let transparent = styles
|
||||||
.get(&child)
|
.get(&child)
|
||||||
.is_some_and(|style| style.display_contents && style.display != crate::Display::None);
|
.is_some_and(|style| style.display_contents && style.display != crate::Display::None);
|
||||||
@@ -5612,8 +5674,7 @@ fn build_any(
|
|||||||
.map(|s| s.display_contents && s.display != crate::Display::None)
|
.map(|s| s.display_contents && s.display != crate::Display::None)
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
if splices_children {
|
if splices_children {
|
||||||
return tree
|
return rendered_children(tree, id)
|
||||||
.children(id)
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flat_map(|cid| build_any(tree, cid, taffy_tree, id_map, words, engine, ifc_items, styles))
|
.flat_map(|cid| build_any(tree, cid, taffy_tree, id_map, words, engine, ifc_items, styles))
|
||||||
.collect();
|
.collect();
|
||||||
@@ -5635,8 +5696,7 @@ fn build_any(
|
|||||||
// wrapper's children into the caller's list (same trick already used
|
// wrapper's children into the caller's list (same trick already used
|
||||||
// for text nodes in `build_text_words`) fixes this at the root: the
|
// for text nodes in `build_text_words`) fixes this at the root: the
|
||||||
// words become flat siblings that wrap only at the real block level.
|
// words become flat siblings that wrap only at the real block level.
|
||||||
return tree
|
return rendered_children(tree, id)
|
||||||
.children(id)
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flat_map(|cid| build_any(tree, cid, taffy_tree, id_map, words, engine, ifc_items, styles))
|
.flat_map(|cid| build_any(tree, cid, taffy_tree, id_map, words, engine, ifc_items, styles))
|
||||||
.collect();
|
.collect();
|
||||||
@@ -7245,7 +7305,7 @@ fn build(
|
|||||||
// wrapping for explicitly constrained inline-blocks and for ones
|
// wrapping for explicitly constrained inline-blocks and for ones
|
||||||
// with real in-flow block children; those need their inner block
|
// with real in-flow block children; those need their inner block
|
||||||
// formatting rather than a single max-content line.
|
// formatting rather than a single max-content line.
|
||||||
let has_in_flow_block_child = tree.children(id).iter().any(|child| {
|
let has_in_flow_block_child = rendered_children(tree, id).iter().any(|child| {
|
||||||
styles
|
styles
|
||||||
.get(child)
|
.get(child)
|
||||||
.map_or(false, is_in_flow_block_level)
|
.map_or(false, is_in_flow_block_level)
|
||||||
@@ -7339,11 +7399,15 @@ fn build(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// When both preferred axes are auto and all min/max constraints
|
// An auto/auto replaced box starts from its intrinsic width rather
|
||||||
// are definite, resolve CSS2's ratio-preserving constraint table
|
// than the fill-available width of an ordinary auto-width block.
|
||||||
// before handing the leaf to taffy. Otherwise taffy's block
|
// Seed that preferred width before handing the leaf to taffy, or
|
||||||
// stretch supplies a containing-block width as a known dimension
|
// block layout passes the containing-block width as a known
|
||||||
// and independently clamps the height, distorting the ratio.
|
// dimension and stretches a source-less/content:url image to the
|
||||||
|
// viewport. Definite constraints additionally resolve CSS2's
|
||||||
|
// ratio-preserving constraint table up front; without constraints,
|
||||||
|
// leave height auto so flex shrink can still transfer through the
|
||||||
|
// preferred aspect ratio.
|
||||||
let has_definite_constraint = [
|
let has_definite_constraint = [
|
||||||
style.min_width,
|
style.min_width,
|
||||||
style.min_height,
|
style.min_height,
|
||||||
@@ -7366,14 +7430,15 @@ fn build(
|
|||||||
.any(|expression| expression.contains('%'));
|
.any(|expression| expression.contains('%'));
|
||||||
if matches!(style.width, crate::Dimension::Auto)
|
if matches!(style.width, crate::Dimension::Auto)
|
||||||
&& matches!(style.height, crate::Dimension::Auto)
|
&& matches!(style.height, crate::Dimension::Auto)
|
||||||
&& has_definite_constraint
|
|
||||||
&& !has_percentage_constraint
|
&& !has_percentage_constraint
|
||||||
{
|
{
|
||||||
let constrained =
|
let constrained =
|
||||||
crate::inline::constrained_auto_replaced_size(width, height, style);
|
crate::inline::constrained_auto_replaced_size(width, height, style);
|
||||||
taffy_style.size.width = taffy::Dimension::length(constrained.width);
|
taffy_style.size.width = taffy::Dimension::length(constrained.width);
|
||||||
|
if has_definite_constraint {
|
||||||
taffy_style.size.height = taffy::Dimension::length(constrained.height);
|
taffy_style.size.height = taffy::Dimension::length(constrained.height);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// When an auto axis has a definite min/max constraint, the
|
// When an auto axis has a definite min/max constraint, the
|
||||||
// measured replaced leaf must own intrinsic-ratio transfer so it
|
// measured replaced leaf must own intrinsic-ratio transfer so it
|
||||||
@@ -7414,6 +7479,10 @@ fn build(
|
|||||||
// correct path for paragraphs/headings/labels/cells of text; the flex-wrap
|
// correct path for paragraphs/headings/labels/cells of text; the flex-wrap
|
||||||
// approximation below only handles the leftovers (mixed inline + atomic
|
// approximation below only handles the leftovers (mixed inline + atomic
|
||||||
// boxes, and layout-only builds where `try_build` always declines).
|
// boxes, and layout-only builds where `try_build` always declines).
|
||||||
|
let is_closed_html_details = _name.ns.as_ref() == "http://www.w3.org/1999/xhtml"
|
||||||
|
&& _name.local.as_ref() == "details"
|
||||||
|
&& node.get_attribute("open").is_none();
|
||||||
|
if !is_closed_html_details {
|
||||||
if let Some(item) = engine.try_build(tree, id, styles) {
|
if let Some(item) = engine.try_build(tree, id, styles) {
|
||||||
if style.display == crate::Display::Block && style.width == crate::Dimension::Auto {
|
if style.display == crate::Display::Block && style.width == crate::Dimension::Auto {
|
||||||
// A pure-text block is still a fill-available block. Its shaped
|
// A pure-text block is still a fill-available block. Its shaped
|
||||||
@@ -7427,6 +7496,7 @@ fn build(
|
|||||||
ifc_items.whole.insert(id, item);
|
ifc_items.whole.insert(id, item);
|
||||||
return Some(leaf);
|
return Some(leaf);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Taffy has no real inline formatting context: its native Block layout
|
// Taffy has no real inline formatting context: its native Block layout
|
||||||
// treats every direct child as its own full-width block box, stacked
|
// treats every direct child as its own full-width block box, stacked
|
||||||
@@ -7452,7 +7522,7 @@ fn build(
|
|||||||
|| has_in_flow_generated_pseudo(style.before_pseudo.as_deref())
|
|| has_in_flow_generated_pseudo(style.before_pseudo.as_deref())
|
||||||
|| has_in_flow_generated_pseudo(style.after_pseudo.as_deref());
|
|| has_in_flow_generated_pseudo(style.after_pseudo.as_deref());
|
||||||
|
|
||||||
let mut dom_children = tree.children(id);
|
let mut dom_children = rendered_children(tree, id);
|
||||||
// A boxless inline wrapper is transparent to our approximated box tree.
|
// A boxless inline wrapper is transparent to our approximated box tree.
|
||||||
// Flatten it before deciding whether this block has mixed inline/block
|
// Flatten it before deciding whether this block has mixed inline/block
|
||||||
// content, not only later while building children. Otherwise
|
// content, not only later while building children. Otherwise
|
||||||
@@ -8105,7 +8175,7 @@ fn needs_column_flex_text_fit_content_cap(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let has_direct_text = tree.children(id).into_iter().any(|child| {
|
let has_direct_text = rendered_children(tree, id).into_iter().any(|child| {
|
||||||
tree.get_node(child).map_or(false, |node| {
|
tree.get_node(child).map_or(false, |node| {
|
||||||
matches!(
|
matches!(
|
||||||
&node.data,
|
&node.data,
|
||||||
@@ -8159,7 +8229,7 @@ fn flatten_contents_children(
|
|||||||
for &cid in children {
|
for &cid in children {
|
||||||
let splices = styles.get(&cid).map(|s| s.display_contents && s.display != crate::Display::None).unwrap_or(false);
|
let splices = styles.get(&cid).map(|s| s.display_contents && s.display != crate::Display::None).unwrap_or(false);
|
||||||
if splices {
|
if splices {
|
||||||
let kids = tree.children(cid);
|
let kids = rendered_children(tree, cid);
|
||||||
flatten_contents_children(tree, &kids, styles, out);
|
flatten_contents_children(tree, &kids, styles, out);
|
||||||
} else {
|
} else {
|
||||||
out.push(cid);
|
out.push(cid);
|
||||||
@@ -8198,7 +8268,7 @@ fn inline_wraps_only_in_flow_blocks(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut saw_block = false;
|
let mut saw_block = false;
|
||||||
for cid in tree.children(id) {
|
for cid in rendered_children(tree, id) {
|
||||||
let Some(node) = tree.get_node(cid) else {
|
let Some(node) = tree.get_node(cid) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
@@ -8255,7 +8325,7 @@ fn flatten_boxless_inline_children(
|
|||||||
|| is_flattenable_inline(tree, cid, styles)
|
|| is_flattenable_inline(tree, cid, styles)
|
||||||
|| inline_wraps_only_in_flow_blocks(tree, cid, styles)
|
|| inline_wraps_only_in_flow_blocks(tree, cid, styles)
|
||||||
{
|
{
|
||||||
let kids = tree.children(cid);
|
let kids = rendered_children(tree, cid);
|
||||||
flatten_boxless_inline_children(tree, &kids, styles, out);
|
flatten_boxless_inline_children(tree, &kids, styles, out);
|
||||||
} else {
|
} else {
|
||||||
out.push(cid);
|
out.push(cid);
|
||||||
|
|||||||
@@ -1410,7 +1410,7 @@ fn collect_spans(
|
|||||||
c: &mut Collector,
|
c: &mut Collector,
|
||||||
loaded_families: &HashMap<String, LoadedFamily>,
|
loaded_families: &HashMap<String, LoadedFamily>,
|
||||||
) {
|
) {
|
||||||
for cid in tree.children(id) {
|
for cid in crate::dom::rendered_children(tree, id) {
|
||||||
collect_node_spans(
|
collect_node_spans(
|
||||||
tree,
|
tree,
|
||||||
cid,
|
cid,
|
||||||
@@ -1795,7 +1795,7 @@ fn is_pure_text_ifc(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let mut has_text = false;
|
let mut has_text = false;
|
||||||
let children = tree.children(id);
|
let children = crate::dom::rendered_children(tree, id);
|
||||||
if children.is_empty() {
|
if children.is_empty() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1869,7 +1869,7 @@ fn inline_child_ok(tree: &DomTree, cid: NodeId, styles: &std::collections::HashM
|
|||||||
if !foldable_inline {
|
if !foldable_inline {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
for gc in tree.children(cid) {
|
for gc in crate::dom::rendered_children(tree, cid) {
|
||||||
if !inline_child_ok(tree, gc, styles, has_text) {
|
if !inline_child_ok(tree, gc, styles, has_text) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,65 @@ pub use dom::{
|
|||||||
layout_dom, layout_dom_with_images, layout_dom_with_resources, DomLayout, StickyLayout,
|
layout_dom, layout_dom_with_images, layout_dom_with_resources, DomLayout, StickyLayout,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Whether an image MIME type names a format supported by the renderer build.
|
||||||
|
///
|
||||||
|
/// Compare the MIME essence case-insensitively and ignore parameters. HTML
|
||||||
|
/// allows a `<source type>` value to include parameters, and HTTP MIME types
|
||||||
|
/// are ASCII case-insensitive. Keep this list aligned with the `image` crate
|
||||||
|
/// features in `Cargo.toml` plus the SVG path in `paint.rs`.
|
||||||
|
pub(crate) fn source_type_supported(value: &str) -> bool {
|
||||||
|
let essence = value.split_once(';').map_or(value, |(essence, _)| essence).trim();
|
||||||
|
[
|
||||||
|
"image/apng",
|
||||||
|
"image/bmp",
|
||||||
|
"image/gif",
|
||||||
|
"image/jpeg",
|
||||||
|
"image/jpg",
|
||||||
|
"image/png",
|
||||||
|
"image/svg+xml",
|
||||||
|
"image/vnd.microsoft.icon",
|
||||||
|
"image/webp",
|
||||||
|
"image/x-icon",
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.any(|supported| essence.eq_ignore_ascii_case(supported))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod image_capability_tests {
|
||||||
|
use super::source_type_supported;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn image_mime_filter_uses_case_insensitive_essence_and_parameters() {
|
||||||
|
for supported in [
|
||||||
|
"image/apng",
|
||||||
|
"image/bmp",
|
||||||
|
"image/gif",
|
||||||
|
"image/jpeg",
|
||||||
|
"image/jpg",
|
||||||
|
"image/png",
|
||||||
|
"image/svg+xml",
|
||||||
|
"image/vnd.microsoft.icon",
|
||||||
|
"image/webp",
|
||||||
|
"image/x-icon",
|
||||||
|
" IMAGE/WEBP ; codecs=lossless ",
|
||||||
|
"Image/Svg+Xml;charset=utf-8",
|
||||||
|
] {
|
||||||
|
assert!(source_type_supported(supported), "{supported}");
|
||||||
|
}
|
||||||
|
for unsupported in [
|
||||||
|
"",
|
||||||
|
"image/avif",
|
||||||
|
"image/jxl",
|
||||||
|
"image/png,image/webp",
|
||||||
|
"text/html",
|
||||||
|
";image/png",
|
||||||
|
] {
|
||||||
|
assert!(!source_type_supported(unsupported), "{unsupported}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "paint")]
|
#[cfg(feature = "paint")]
|
||||||
mod paint;
|
mod paint;
|
||||||
#[cfg(feature = "paint")]
|
#[cfg(feature = "paint")]
|
||||||
@@ -381,6 +440,12 @@ pub enum GeneratedContentItem {
|
|||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct LayoutStyle {
|
pub struct LayoutStyle {
|
||||||
pub display: Display,
|
pub display: Display,
|
||||||
|
/// The specified `display` value was the CSS-wide `inherit` keyword.
|
||||||
|
///
|
||||||
|
/// `display` is normally non-inherited, so the cascade cannot resolve this
|
||||||
|
/// until the parent's computed outer/inner display is known. The DOM
|
||||||
|
/// top-down pass copies that provenance and then clears this marker.
|
||||||
|
pub(crate) display_inherit: bool,
|
||||||
/// Computed CSS `container-type` (not inherited).
|
/// Computed CSS `container-type` (not inherited).
|
||||||
pub container_type: ContainerType,
|
pub container_type: ContainerType,
|
||||||
/// The specified value was the CSS-wide `inherit` keyword. Resolved
|
/// The specified value was the CSS-wide `inherit` keyword. Resolved
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ use crate::dom::layout_dom_with_web_fonts;
|
|||||||
const DEFAULT_RESOURCE_CACHE_ENTRIES: usize = 512;
|
const DEFAULT_RESOURCE_CACHE_ENTRIES: usize = 512;
|
||||||
const DEFAULT_RESOURCE_CACHE_BYTES: usize = 64 * 1024 * 1024;
|
const DEFAULT_RESOURCE_CACHE_BYTES: usize = 64 * 1024 * 1024;
|
||||||
const MISSING_RESOURCE_RETRY_AFTER: std::time::Duration = std::time::Duration::from_secs(2);
|
const MISSING_RESOURCE_RETRY_AFTER: std::time::Duration = std::time::Duration::from_secs(2);
|
||||||
|
/// Exact formats the renderer can decode. Do not use `image/*` or `*/*` here:
|
||||||
|
/// either wildcard permits a content-negotiating server to choose AVIF,
|
||||||
|
/// JPEG-XL, or another format that this build cannot rasterize.
|
||||||
|
const IMAGE_ACCEPT: &str = "image/webp,image/apng,image/svg+xml,image/png,image/jpeg,image/gif,image/bmp,image/x-icon,image/vnd.microsoft.icon";
|
||||||
|
|
||||||
/// Synchronous byte loader used by [`RenderResourceCache`]. The default
|
/// Synchronous byte loader used by [`RenderResourceCache`]. The default
|
||||||
/// implementation uses Obscura's pooled image agent; tests and embedding
|
/// implementation uses Obscura's pooled image agent; tests and embedding
|
||||||
@@ -376,6 +380,247 @@ impl PreparedRender {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Unscaled padding-box size used by CSSOM View's `clientWidth` and
|
||||||
|
/// `clientHeight`. Layout rects are border boxes, so remove the resolved
|
||||||
|
/// borders but retain padding. This deliberately ignores visual
|
||||||
|
/// transforms: client metrics describe layout geometry, unlike
|
||||||
|
/// `getBoundingClientRect()`.
|
||||||
|
pub fn client_size(
|
||||||
|
&self,
|
||||||
|
id: obscura_dom::tree::NodeId,
|
||||||
|
) -> Option<(f32, f32)> {
|
||||||
|
let rect = self.layout.rects.get(&id)?;
|
||||||
|
let style = self.layout.styles.get(&id)?;
|
||||||
|
Some((
|
||||||
|
(rect.width - style.border.left - style.border.right).max(0.0),
|
||||||
|
(rect.height - style.border.top - style.border.bottom).max(0.0),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A compact CSSOM snapshot derived from the same final cascade and
|
||||||
|
/// layout used by paint and geometry. Keeping this on `PreparedRender`
|
||||||
|
/// lets script fetch all high-traffic computed properties in one op,
|
||||||
|
/// without rebuilding layout once per property access.
|
||||||
|
pub fn computed_style(
|
||||||
|
&self,
|
||||||
|
id: obscura_dom::tree::NodeId,
|
||||||
|
) -> Option<HashMap<&'static str, String>> {
|
||||||
|
let style = self.layout.styles.get(&id)?;
|
||||||
|
let rect = self.layout.rects.get(&id);
|
||||||
|
let mut out = HashMap::new();
|
||||||
|
|
||||||
|
let display = if style.display_contents {
|
||||||
|
"contents"
|
||||||
|
} else if style.display == crate::Display::None {
|
||||||
|
"none"
|
||||||
|
} else if style.internal_flex_container {
|
||||||
|
"block"
|
||||||
|
} else {
|
||||||
|
match (style.display, style.is_inline_block) {
|
||||||
|
(crate::Display::Flex, true) => "inline-flex",
|
||||||
|
(crate::Display::Grid, true) => "inline-grid",
|
||||||
|
(crate::Display::Block, true) => "inline-block",
|
||||||
|
(crate::Display::Flex, false) => "flex",
|
||||||
|
(crate::Display::Grid, false) => "grid",
|
||||||
|
(crate::Display::Inline, false) => "inline",
|
||||||
|
_ => "block",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
out.insert("display", display.to_string());
|
||||||
|
out.insert(
|
||||||
|
"position",
|
||||||
|
if style.position_fixed {
|
||||||
|
"fixed"
|
||||||
|
} else if style.position_sticky {
|
||||||
|
"sticky"
|
||||||
|
} else {
|
||||||
|
match style.position {
|
||||||
|
Some(taffy::Position::Absolute) => "absolute",
|
||||||
|
Some(taffy::Position::Relative) => "relative",
|
||||||
|
_ => "static",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
out.insert(
|
||||||
|
"z-index",
|
||||||
|
style.z_index.map_or_else(|| "auto".to_string(), |v| v.to_string()),
|
||||||
|
);
|
||||||
|
out.insert(
|
||||||
|
"visibility",
|
||||||
|
if style.visibility_hidden.unwrap_or(false) {
|
||||||
|
"hidden"
|
||||||
|
} else {
|
||||||
|
"visible"
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
out.insert("opacity", css_number(style.opacity.unwrap_or(1.0)));
|
||||||
|
out.insert(
|
||||||
|
"background-color",
|
||||||
|
css_color(style.background_color.unwrap_or([0, 0, 0, 0])),
|
||||||
|
);
|
||||||
|
out.insert("color", css_color(style.color.unwrap_or([0, 0, 0, 255])));
|
||||||
|
|
||||||
|
if let Some(rect) = rect {
|
||||||
|
let horizontal_non_content =
|
||||||
|
style.border.left + style.border.right + style.padding.left + style.padding.right;
|
||||||
|
let vertical_non_content =
|
||||||
|
style.border.top + style.border.bottom + style.padding.top + style.padding.bottom;
|
||||||
|
let (width, height) = match style.box_sizing {
|
||||||
|
crate::BoxSizing::BorderBox => (rect.width, rect.height),
|
||||||
|
_ => (
|
||||||
|
(rect.width - horizontal_non_content).max(0.0),
|
||||||
|
(rect.height - vertical_non_content).max(0.0),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
out.insert("width", css_px(width));
|
||||||
|
out.insert("height", css_px(height));
|
||||||
|
} else {
|
||||||
|
out.insert("width", dimension_css(style.width, "auto"));
|
||||||
|
out.insert("height", dimension_css(style.height, "auto"));
|
||||||
|
}
|
||||||
|
out.insert("min-width", dimension_css(style.min_width, "auto"));
|
||||||
|
out.insert("min-height", dimension_css(style.min_height, "auto"));
|
||||||
|
out.insert("max-width", dimension_css(style.max_width, "none"));
|
||||||
|
out.insert("max-height", dimension_css(style.max_height, "none"));
|
||||||
|
out.insert(
|
||||||
|
"box-sizing",
|
||||||
|
if style.box_sizing == crate::BoxSizing::BorderBox {
|
||||||
|
"border-box"
|
||||||
|
} else {
|
||||||
|
"content-box"
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let overflow_axis = |specified: u8, clipped: bool, scroll: bool| {
|
||||||
|
if scroll {
|
||||||
|
// The compact layout model intentionally merges
|
||||||
|
// hidden/auto/scroll for clipping. `auto` is the least
|
||||||
|
// surprising computed scroll-container value.
|
||||||
|
"auto"
|
||||||
|
} else if specified == 1 || clipped {
|
||||||
|
"clip"
|
||||||
|
} else {
|
||||||
|
"visible"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
out.insert(
|
||||||
|
"overflow-x",
|
||||||
|
overflow_axis(
|
||||||
|
style.overflow_specified_x,
|
||||||
|
style.overflow_clip_x,
|
||||||
|
style.overflow_scroll_x,
|
||||||
|
)
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
out.insert(
|
||||||
|
"overflow-y",
|
||||||
|
overflow_axis(
|
||||||
|
style.overflow_specified_y,
|
||||||
|
style.overflow_clip_y,
|
||||||
|
style.overflow_scroll_y,
|
||||||
|
)
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (name, value, auto) in [
|
||||||
|
("margin-top", style.margin.top, style.margin_auto[0]),
|
||||||
|
("margin-right", style.margin.right, style.margin_auto[1]),
|
||||||
|
("margin-bottom", style.margin.bottom, style.margin_auto[2]),
|
||||||
|
("margin-left", style.margin.left, style.margin_auto[3]),
|
||||||
|
] {
|
||||||
|
out.insert(name, if auto { "auto".to_string() } else { css_px(value) });
|
||||||
|
}
|
||||||
|
for (name, value) in [
|
||||||
|
("padding-top", style.padding.top),
|
||||||
|
("padding-right", style.padding.right),
|
||||||
|
("padding-bottom", style.padding.bottom),
|
||||||
|
("padding-left", style.padding.left),
|
||||||
|
("border-top-width", style.border.top),
|
||||||
|
("border-right-width", style.border.right),
|
||||||
|
("border-bottom-width", style.border.bottom),
|
||||||
|
("border-left-width", style.border.left),
|
||||||
|
] {
|
||||||
|
out.insert(name, css_px(value));
|
||||||
|
}
|
||||||
|
let border_color = css_color(style.border_color.or(style.color).unwrap_or([0, 0, 0, 255]));
|
||||||
|
out.insert("border-top-color", border_color.clone());
|
||||||
|
out.insert("border-right-color", border_color.clone());
|
||||||
|
out.insert("border-bottom-color", border_color.clone());
|
||||||
|
out.insert("border-left-color", border_color);
|
||||||
|
for (name, width) in [
|
||||||
|
("border-top-style", style.border.top),
|
||||||
|
("border-right-style", style.border.right),
|
||||||
|
("border-bottom-style", style.border.bottom),
|
||||||
|
("border-left-style", style.border.left),
|
||||||
|
] {
|
||||||
|
out.insert(name, if width > 0.0 { "solid" } else { "none" }.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
out.insert(
|
||||||
|
"flex-direction",
|
||||||
|
match style.flex_direction.unwrap_or(taffy::FlexDirection::Row) {
|
||||||
|
taffy::FlexDirection::Row => "row",
|
||||||
|
taffy::FlexDirection::RowReverse => "row-reverse",
|
||||||
|
taffy::FlexDirection::Column => "column",
|
||||||
|
taffy::FlexDirection::ColumnReverse => "column-reverse",
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
out.insert(
|
||||||
|
"flex-wrap",
|
||||||
|
match style.flex_wrap.unwrap_or(taffy::FlexWrap::NoWrap) {
|
||||||
|
taffy::FlexWrap::NoWrap => "nowrap",
|
||||||
|
taffy::FlexWrap::Wrap => "wrap",
|
||||||
|
taffy::FlexWrap::WrapReverse => "wrap-reverse",
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
out.insert(
|
||||||
|
"align-items",
|
||||||
|
style
|
||||||
|
.align_items
|
||||||
|
.map_or_else(|| "normal".to_string(), align_items_css),
|
||||||
|
);
|
||||||
|
out.insert(
|
||||||
|
"justify-items",
|
||||||
|
style
|
||||||
|
.justify_items
|
||||||
|
.map_or_else(|| "normal".to_string(), align_items_css),
|
||||||
|
);
|
||||||
|
out.insert(
|
||||||
|
"justify-content",
|
||||||
|
style
|
||||||
|
.justify_content
|
||||||
|
.map_or_else(|| "normal".to_string(), align_content_css),
|
||||||
|
);
|
||||||
|
out.insert(
|
||||||
|
"align-content",
|
||||||
|
style
|
||||||
|
.align_content
|
||||||
|
.map_or_else(|| "normal".to_string(), align_content_css),
|
||||||
|
);
|
||||||
|
out.insert("column-gap", style.column_gap.map_or_else(|| "normal".to_string(), css_px));
|
||||||
|
out.insert("row-gap", style.row_gap.map_or_else(|| "normal".to_string(), css_px));
|
||||||
|
out.insert(
|
||||||
|
"grid-auto-flow",
|
||||||
|
match style.grid_auto_flow.unwrap_or(taffy::GridAutoFlow::Row) {
|
||||||
|
taffy::GridAutoFlow::Row => "row",
|
||||||
|
taffy::GridAutoFlow::Column => "column",
|
||||||
|
taffy::GridAutoFlow::RowDense => "row dense",
|
||||||
|
taffy::GridAutoFlow::ColumnDense => "column dense",
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
out.insert("transform", transform_css(style));
|
||||||
|
out
|
||||||
|
.insert("transform-origin", transform_origin_css(style, rect));
|
||||||
|
Some(out)
|
||||||
|
}
|
||||||
|
|
||||||
/// Border box in the current root viewport. This is the read-only geometry
|
/// Border box in the current root viewport. This is the read-only geometry
|
||||||
/// path used by a later CSSOM integration and shares paint's clamped scroll,
|
/// path used by a later CSSOM integration and shares paint's clamped scroll,
|
||||||
/// fixed-subtree, and sticky-positioning derivatives.
|
/// fixed-subtree, and sticky-positioning derivatives.
|
||||||
@@ -402,6 +647,127 @@ impl PreparedRender {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn css_number(value: f32) -> String {
|
||||||
|
if value == 0.0 {
|
||||||
|
"0".to_string()
|
||||||
|
} else {
|
||||||
|
value.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn css_px(value: f32) -> String {
|
||||||
|
format!("{}px", css_number(if value == 0.0 { 0.0 } else { value }))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dimension_css(value: crate::Dimension, auto: &str) -> String {
|
||||||
|
match value {
|
||||||
|
crate::Dimension::Auto => auto.to_string(),
|
||||||
|
crate::Dimension::Px(v) => css_px(v),
|
||||||
|
crate::Dimension::Percent(v) => format!("{}%", css_number(v * 100.0)),
|
||||||
|
crate::Dimension::Em(v) => format!("{}em", css_number(v)),
|
||||||
|
crate::Dimension::Ex(v) => format!("{}ex", css_number(v)),
|
||||||
|
crate::Dimension::Rem(v) => format!("{}rem", css_number(v)),
|
||||||
|
crate::Dimension::Vw(v) => format!("{}vw", css_number(v)),
|
||||||
|
crate::Dimension::Vh(v) => format!("{}vh", css_number(v)),
|
||||||
|
crate::Dimension::Vmin(v) => format!("{}vmin", css_number(v)),
|
||||||
|
crate::Dimension::Vmax(v) => format!("{}vmax", css_number(v)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn css_color([r, g, b, a]: [u8; 4]) -> String {
|
||||||
|
if a == 255 {
|
||||||
|
format!("rgb({r}, {g}, {b})")
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"rgba({r}, {g}, {b}, {})",
|
||||||
|
css_number(a as f32 / 255.0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn align_items_css(value: taffy::AlignItems) -> String {
|
||||||
|
let keyword = match value.keyword {
|
||||||
|
taffy::AlignItemsKeyword::Start => "start",
|
||||||
|
taffy::AlignItemsKeyword::End => "end",
|
||||||
|
taffy::AlignItemsKeyword::FlexStart => "flex-start",
|
||||||
|
taffy::AlignItemsKeyword::FlexEnd => "flex-end",
|
||||||
|
taffy::AlignItemsKeyword::Center => "center",
|
||||||
|
taffy::AlignItemsKeyword::Baseline => "baseline",
|
||||||
|
taffy::AlignItemsKeyword::Stretch => "stretch",
|
||||||
|
};
|
||||||
|
if value.safety == taffy::AlignmentSafety::Safe {
|
||||||
|
format!("safe {keyword}")
|
||||||
|
} else {
|
||||||
|
keyword.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn align_content_css(value: taffy::AlignContent) -> String {
|
||||||
|
let keyword = match value.keyword {
|
||||||
|
taffy::AlignContentKeyword::Start => "start",
|
||||||
|
taffy::AlignContentKeyword::End => "end",
|
||||||
|
taffy::AlignContentKeyword::FlexStart => "flex-start",
|
||||||
|
taffy::AlignContentKeyword::FlexEnd => "flex-end",
|
||||||
|
taffy::AlignContentKeyword::Center => "center",
|
||||||
|
taffy::AlignContentKeyword::Stretch => "stretch",
|
||||||
|
taffy::AlignContentKeyword::SpaceBetween => "space-between",
|
||||||
|
taffy::AlignContentKeyword::SpaceEvenly => "space-evenly",
|
||||||
|
taffy::AlignContentKeyword::SpaceAround => "space-around",
|
||||||
|
};
|
||||||
|
if value.safety == taffy::AlignmentSafety::Safe {
|
||||||
|
format!("safe {keyword}")
|
||||||
|
} else {
|
||||||
|
keyword.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transform_css(style: &crate::LayoutStyle) -> String {
|
||||||
|
let mut parts = Vec::new();
|
||||||
|
if let Some((x, y)) = style.transform_translate {
|
||||||
|
parts.push(format!(
|
||||||
|
"translate({}, {})",
|
||||||
|
dimension_css(x, "0px"),
|
||||||
|
dimension_css(y, "0px")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some((x, y)) = style.individual_translate {
|
||||||
|
parts.push(format!(
|
||||||
|
"translate({}, {})",
|
||||||
|
dimension_css(x, "0px"),
|
||||||
|
dimension_css(y, "0px")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some((x, y)) = style.transform_scale {
|
||||||
|
parts.push(format!("scale({}, {})", css_number(x), css_number(y)));
|
||||||
|
}
|
||||||
|
if let Some([a, b, c, d]) = style.transform_projection {
|
||||||
|
parts.push(format!(
|
||||||
|
"matrix({}, {}, {}, {}, 0, 0)",
|
||||||
|
css_number(a),
|
||||||
|
css_number(b),
|
||||||
|
css_number(c),
|
||||||
|
css_number(d)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if parts.is_empty() {
|
||||||
|
"none".to_string()
|
||||||
|
} else {
|
||||||
|
parts.join(" ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transform_origin_css(style: &crate::LayoutStyle, rect: Option<&crate::Rect>) -> String {
|
||||||
|
let (x, y) = style
|
||||||
|
.transform_origin
|
||||||
|
.unwrap_or((crate::Dimension::Percent(0.5), crate::Dimension::Percent(0.5)));
|
||||||
|
let resolve = |value: crate::Dimension, axis: f32| match value {
|
||||||
|
crate::Dimension::Percent(fraction) => css_px(fraction * axis),
|
||||||
|
other => dimension_css(other, "0px"),
|
||||||
|
};
|
||||||
|
let (width, height) = rect.map_or((0.0, 0.0), |rect| (rect.width, rect.height));
|
||||||
|
format!("{} {}", resolve(x, width), resolve(y, height))
|
||||||
|
}
|
||||||
|
|
||||||
/// Render `tree` at `viewport` (width, height) in CSS pixels to a Pixmap, or
|
/// Render `tree` at `viewport` (width, height) in CSS pixels to a Pixmap, or
|
||||||
/// None if the viewport is zero-sized. `base_url`, when given, resolves the
|
/// None if the viewport is zero-sized. `base_url`, when given, resolves the
|
||||||
/// relative image URLs (`<img src="logo.svg">`) that make up the overwhelming
|
/// relative image URLs (`<img src="logo.svg">`) that make up the overwhelming
|
||||||
@@ -525,9 +891,48 @@ pub fn paint_prepared(
|
|||||||
&mut prepared.layout,
|
&mut prepared.layout,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The used `z-index` for an element which participates in stacking order.
|
||||||
|
///
|
||||||
|
/// A non-auto z-index applies not only to positioned boxes, but also to
|
||||||
|
/// otherwise-static flex and grid items. `display:contents` boxes are skipped
|
||||||
|
/// while finding the item's formatting-context parent because they generate no
|
||||||
|
/// box of their own.
|
||||||
|
fn stacking_z_index(
|
||||||
|
tree: &DomTree,
|
||||||
|
laid: &crate::DomLayout,
|
||||||
|
id: obscura_dom::tree::NodeId,
|
||||||
|
) -> Option<i32> {
|
||||||
|
let style = laid.styles.get(&id)?;
|
||||||
|
if style.display == crate::Display::None || style.display_contents {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let z = style.z_index?;
|
||||||
|
if style.position.is_some() {
|
||||||
|
return Some(z);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut parent = tree.get_node(id).and_then(|node| node.parent);
|
||||||
|
while let Some(parent_id) = parent {
|
||||||
|
let Some(parent_style) = laid.styles.get(&parent_id) else {
|
||||||
|
parent = tree.get_node(parent_id).and_then(|node| node.parent);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if parent_style.display_contents {
|
||||||
|
parent = tree.get_node(parent_id).and_then(|node| node.parent);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let is_flex_or_grid = parent_style.display == crate::Display::Grid
|
||||||
|
|| (parent_style.display == crate::Display::Flex
|
||||||
|
&& !parent_style.internal_flex_container);
|
||||||
|
return is_flex_or_grid.then_some(z);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
/// Paint an already prepared layout without changing its document-space
|
/// Paint an already prepared layout without changing its document-space
|
||||||
/// geometry. Root scrolling and sticky positioning are per-shot visual state,
|
/// geometry. Root scrolling and sticky positioning are per-shot visual state,
|
||||||
/// so alternating captures can safely reuse the same layout.
|
/// so alternating captures can safely reuse the same layout.
|
||||||
@@ -546,6 +951,7 @@ fn paint_laid_dom_scrolled(
|
|||||||
laid: &mut crate::DomLayout,
|
laid: &mut crate::DomLayout,
|
||||||
paint_root: Option<obscura_dom::tree::NodeId>,
|
paint_root: Option<obscura_dom::tree::NodeId>,
|
||||||
suppress_opacity_for: Option<obscura_dom::tree::NodeId>,
|
suppress_opacity_for: Option<obscura_dom::tree::NodeId>,
|
||||||
|
suppress_stacking_for: Option<obscura_dom::tree::NodeId>,
|
||||||
) -> Option<Pixmap> {
|
) -> Option<Pixmap> {
|
||||||
let scroll_state =
|
let scroll_state =
|
||||||
ScrollPaintState::new(viewport, scroll, content_size, viewport_fixed, sticky);
|
ScrollPaintState::new(viewport, scroll, content_size, viewport_fixed, sticky);
|
||||||
@@ -582,14 +988,13 @@ fn paint_laid_dom_scrolled(
|
|||||||
// path free of any added cost.
|
// path free of any added cost.
|
||||||
|
|
||||||
// Paint order: tree order for the normal flow (later elements paint over
|
// Paint order: tree order for the normal flow (later elements paint over
|
||||||
// earlier ones), except that a positioned element with a non-zero
|
// earlier ones), except that a positioned element or a flex/grid item
|
||||||
// z-index lifts its whole subtree into a separate layer: negative layers
|
// with a non-auto z-index lifts its whole subtree into an atomic stacking
|
||||||
// paint under the normal flow, positive ones above it, each sorted by
|
// unit: negative layers paint under the normal flow, non-negative ones
|
||||||
// z-index ascending (stable, so equal z keeps tree order). This is the
|
// above it, each sorted by z-index ascending (stable, so equal z keeps
|
||||||
// pragmatic core of CSS stacking contexts: dropdowns/overlays/badges
|
// tree order). The unit is recursively painted at its sorted position,
|
||||||
// (z>0) stop losing to later siblings, and z:-1 decorative backdrops
|
// preventing its backgrounds, replaced content, and shaped text from
|
||||||
// stop covering their content. Nested z roots paint inside their
|
// leaking into different global paint phases.
|
||||||
// ancestor root's subtree in tree order.
|
|
||||||
let mut neg_layers: Vec<(i32, Vec<obscura_dom::tree::NodeId>)> = Vec::new();
|
let mut neg_layers: Vec<(i32, Vec<obscura_dom::tree::NodeId>)> = Vec::new();
|
||||||
let mut pos_layers: Vec<(i32, Vec<obscura_dom::tree::NodeId>)> = Vec::new();
|
let mut pos_layers: Vec<(i32, Vec<obscura_dom::tree::NodeId>)> = Vec::new();
|
||||||
let mut normal: Vec<obscura_dom::tree::NodeId> = Vec::new();
|
let mut normal: Vec<obscura_dom::tree::NodeId> = Vec::new();
|
||||||
@@ -606,12 +1011,9 @@ fn paint_laid_dom_scrolled(
|
|||||||
.get(&nid)
|
.get(&nid)
|
||||||
.and_then(|style| style.opacity)
|
.and_then(|style| style.opacity)
|
||||||
.is_some_and(|opacity| opacity.clamp(0.0, 1.0) < 1.0);
|
.is_some_and(|opacity| opacity.clamp(0.0, 1.0) < 1.0);
|
||||||
let z = laid
|
let z = (suppress_stacking_for != Some(nid))
|
||||||
.styles
|
.then(|| stacking_z_index(tree, laid, nid))
|
||||||
.get(&nid)
|
.flatten();
|
||||||
.filter(|s| s.position.is_some())
|
|
||||||
.and_then(|s| s.z_index)
|
|
||||||
.filter(|&z| z != 0);
|
|
||||||
if is_opacity_root {
|
if is_opacity_root {
|
||||||
let mut sub = vec![nid];
|
let mut sub = vec![nid];
|
||||||
sub.extend(tree.descendants(nid));
|
sub.extend(tree.descendants(nid));
|
||||||
@@ -632,9 +1034,9 @@ fn paint_laid_dom_scrolled(
|
|||||||
consumed.insert(m);
|
consumed.insert(m);
|
||||||
}
|
}
|
||||||
if z < 0 {
|
if z < 0 {
|
||||||
neg_layers.push((z, sub));
|
neg_layers.push((z, vec![nid]));
|
||||||
} else {
|
} else {
|
||||||
pos_layers.push((z, sub));
|
pos_layers.push((z, vec![nid]));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
normal.push(nid);
|
normal.push(nid);
|
||||||
@@ -707,6 +1109,36 @@ fn paint_laid_dom_scrolled(
|
|||||||
if svg_subtree_skip.contains(&nid) {
|
if svg_subtree_skip.contains(&nid) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if suppress_stacking_for != Some(nid)
|
||||||
|
&& stacking_z_index(tree, laid, nid).is_some()
|
||||||
|
{
|
||||||
|
opacity_subtree_skip.insert(nid);
|
||||||
|
opacity_subtree_skip.extend(tree.descendants(nid));
|
||||||
|
// A stacking context is one structural paint item in its parent.
|
||||||
|
// Re-enter the same display-list builder with this root suppressed
|
||||||
|
// so all of its decorations, descendants, and shaped inline text
|
||||||
|
// finish before the next sibling stacking unit starts. Passing the
|
||||||
|
// existing pixmap avoids a viewport-sized intermediate surface;
|
||||||
|
// opacity still takes the isolated-surface path below when needed.
|
||||||
|
pixmap = paint_laid_dom_scrolled(
|
||||||
|
tree,
|
||||||
|
viewport,
|
||||||
|
base_url,
|
||||||
|
scroll,
|
||||||
|
pixmap,
|
||||||
|
image_cache,
|
||||||
|
selected_images,
|
||||||
|
svg_fonts,
|
||||||
|
content_size,
|
||||||
|
viewport_fixed,
|
||||||
|
sticky,
|
||||||
|
laid,
|
||||||
|
Some(nid),
|
||||||
|
suppress_opacity_for,
|
||||||
|
Some(nid),
|
||||||
|
)?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let node = match tree.get_node(nid) {
|
let node = match tree.get_node(nid) {
|
||||||
Some(n) => n,
|
Some(n) => n,
|
||||||
None => continue,
|
None => continue,
|
||||||
@@ -769,6 +1201,7 @@ fn paint_laid_dom_scrolled(
|
|||||||
laid,
|
laid,
|
||||||
Some(nid),
|
Some(nid),
|
||||||
Some(nid),
|
Some(nid),
|
||||||
|
suppress_stacking_for,
|
||||||
)?;
|
)?;
|
||||||
let group_paint = tiny_skia::PixmapPaint {
|
let group_paint = tiny_skia::PixmapPaint {
|
||||||
opacity: own_opacity,
|
opacity: own_opacity,
|
||||||
@@ -2606,12 +3039,11 @@ fn split_css_top_level(value: &str, separator: char) -> Vec<&str> {
|
|||||||
fn http_get_bytes(url: &str) -> Option<Vec<u8>> {
|
fn http_get_bytes(url: &str) -> Option<Vec<u8>> {
|
||||||
let mut backoff = std::time::Duration::from_millis(200);
|
let mut backoff = std::time::Duration::from_millis(200);
|
||||||
for attempt in 0..3 {
|
for attempt in 0..3 {
|
||||||
// A browser-like Accept advertises the modern image formats and is what
|
// Advertise only formats that this build can decode. Content-negotiating
|
||||||
// content-negotiating CDNs expect; some UA-gated hosts also reject a
|
// CDNs otherwise commonly choose AVIF and leave the image blank.
|
||||||
// request with no Accept header outright.
|
|
||||||
let res = image_agent()
|
let res = image_agent()
|
||||||
.get(url)
|
.get(url)
|
||||||
.set("Accept", "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8")
|
.set("Accept", IMAGE_ACCEPT)
|
||||||
.call();
|
.call();
|
||||||
match res {
|
match res {
|
||||||
Ok(resp) => {
|
Ok(resp) => {
|
||||||
@@ -2689,7 +3121,7 @@ fn percent_decode(s: &str) -> Vec<u8> {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decode raster image bytes (jpeg/png/webp) to a premultiplied-alpha pixmap
|
/// Decode raster image bytes (GIF/JPEG/PNG/WebP) to a premultiplied-alpha pixmap
|
||||||
/// resized to `w`x`h`.
|
/// resized to `w`x`h`.
|
||||||
fn raster_to_pixmap(bytes: &[u8], w: u32, h: u32) -> Option<Pixmap> {
|
fn raster_to_pixmap(bytes: &[u8], w: u32, h: u32) -> Option<Pixmap> {
|
||||||
let img = image::load_from_memory(bytes).ok()?.to_rgba8();
|
let img = image::load_from_memory(bytes).ok()?.to_rgba8();
|
||||||
@@ -4147,7 +4579,7 @@ fn picture_source_url(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(t) = child.get_attribute("type") {
|
if let Some(t) = child.get_attribute("type") {
|
||||||
if !source_type_supported(t) {
|
if !crate::source_type_supported(t) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4166,17 +4598,6 @@ fn picture_source_url(
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether a `<source type=...>` names an image format this build can decode.
|
|
||||||
/// AVIF/JPEG-XL are intentionally excluded: the `image` crate cannot decode
|
|
||||||
/// them here, so a decodable `<img>` fallback must win over such a source.
|
|
||||||
fn source_type_supported(t: &str) -> bool {
|
|
||||||
matches!(
|
|
||||||
t.trim().to_ascii_lowercase().as_str(),
|
|
||||||
"image/jpeg" | "image/jpg" | "image/png" | "image/gif" | "image/webp"
|
|
||||||
| "image/bmp" | "image/svg+xml" | "image/x-icon" | "image/vnd.microsoft.icon"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Pick one URL from a `srcset` list, matching the WebKit/Blink selection:
|
/// Pick one URL from a `srcset` list, matching the WebKit/Blink selection:
|
||||||
/// normalize each `w` descriptor to an effective density (`w / source-size`,
|
/// normalize each `w` descriptor to an effective density (`w / source-size`,
|
||||||
/// with the source-size taken from `sizes` or falling back to the viewport
|
/// with the source-size taken from `sizes` or falling back to the viewport
|
||||||
@@ -5405,6 +5826,101 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use obscura_dom::tree_sink::parse_html;
|
use obscura_dom::tree_sink::parse_html;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn image_accept_advertises_exactly_decodable_mime_types() {
|
||||||
|
assert!(!IMAGE_ACCEPT.contains('*'));
|
||||||
|
assert!(!IMAGE_ACCEPT.to_ascii_lowercase().contains("avif"));
|
||||||
|
for mime in IMAGE_ACCEPT.split(',') {
|
||||||
|
assert!(
|
||||||
|
crate::source_type_supported(mime),
|
||||||
|
"advertised MIME type must be decodable: {mime}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for required in [
|
||||||
|
"image/webp",
|
||||||
|
"image/apng",
|
||||||
|
"image/svg+xml",
|
||||||
|
"image/png",
|
||||||
|
"image/jpeg",
|
||||||
|
"image/gif",
|
||||||
|
"image/bmp",
|
||||||
|
"image/x-icon",
|
||||||
|
"image/vnd.microsoft.icon",
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
IMAGE_ACCEPT.split(',').any(|mime| mime == required),
|
||||||
|
"missing supported MIME type {required}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn picture_type_filter_skips_avif_and_accepts_parameterized_mime_essence() {
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"<picture>
|
||||||
|
<source type="IMAGE/AVIF; codecs=av01" srcset="unsupported.avif">
|
||||||
|
<source type=" Image/WebP ; codecs=lossless " srcset="supported.webp">
|
||||||
|
<img id="hero" src="fallback.png">
|
||||||
|
</picture>"#,
|
||||||
|
);
|
||||||
|
let hero = tree.get_element_by_id("hero").expect("hero");
|
||||||
|
assert_eq!(
|
||||||
|
picture_source_url(&tree, hero, (800.0, 600.0)),
|
||||||
|
Some(("supported.webp".to_string(), 1.0))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn enabled_bmp_and_ico_formats_have_metadata_and_full_raster_decode() {
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
let source = image::DynamicImage::ImageRgba8(image::RgbaImage::from_pixel(
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
image::Rgba([20, 40, 60, 255]),
|
||||||
|
));
|
||||||
|
for format in [image::ImageFormat::Bmp, image::ImageFormat::Ico] {
|
||||||
|
let mut encoded = Cursor::new(Vec::new());
|
||||||
|
source
|
||||||
|
.write_to(&mut encoded, format)
|
||||||
|
.expect("encode fixture");
|
||||||
|
let encoded = encoded.into_inner();
|
||||||
|
assert_eq!(image_dimensions(&encoded), Some((2, 3)), "{format:?}");
|
||||||
|
let raster = raster_to_pixmap(&encoded, 2, 3).expect("decode raster");
|
||||||
|
assert_eq!((raster.width(), raster.height()), (2, 3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gif_placeholders_have_intrinsic_pixels_and_valid_gifs_decode() {
|
||||||
|
// Apple's lazy-picture system (and many generic lazy loaders) uses a
|
||||||
|
// transparent 1x1 GIF as the selected source until the real candidate
|
||||||
|
// enters its preload range. It is still a successfully decoded image:
|
||||||
|
// Chromium reports complete=true and naturalWidth=1.
|
||||||
|
const TRANSPARENT_GIF: &[u8] = &[
|
||||||
|
0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00, 0x01, 0x00, 0x70,
|
||||||
|
0x00, 0x00, 0x21, 0xf9, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x2c,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x02,
|
||||||
|
0x44, 0x01, 0x00, 0x3b,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Apple's deliberately palette-less placeholder is accepted by
|
||||||
|
// browsers for source selection. Header metadata is sufficient for
|
||||||
|
// complete/naturalWidth; visually it contributes no pixels.
|
||||||
|
assert_eq!(image_dimensions(TRANSPARENT_GIF), Some((1, 1)));
|
||||||
|
|
||||||
|
const VISIBLE_GIF: &[u8] = &[
|
||||||
|
0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00, 0x01, 0x00, 0x80,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x2c, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x01, 0x4c, 0x00,
|
||||||
|
0x3b,
|
||||||
|
];
|
||||||
|
let decoded = raster_to_pixmap(VISIBLE_GIF, 1, 1).expect("decode GIF");
|
||||||
|
assert_eq!(decoded.width(), 1);
|
||||||
|
assert_eq!(decoded.height(), 1);
|
||||||
|
assert_eq!(decoded.pixel(0, 0).expect("pixel").alpha(), 255);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scrolled_viewport_moves_document_content_but_not_fixed_subtrees() {
|
fn scrolled_viewport_moves_document_content_but_not_fixed_subtrees() {
|
||||||
let tree = parse_html(
|
let tree = parse_html(
|
||||||
@@ -6672,6 +7188,89 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn static_flex_and_grid_item_z_index_paints_each_subtree_atomically() {
|
||||||
|
let blue_image = "data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='100'%20height='60'%3E%3Crect%20width='100'%20height='60'%20fill='blue'/%3E%3C/svg%3E";
|
||||||
|
let tree = parse_html(&format!(
|
||||||
|
r#"<html><head><style>
|
||||||
|
body {{ margin:0 }}
|
||||||
|
.row {{
|
||||||
|
position:relative; width:120px; height:70px;
|
||||||
|
display:flex; align-items:flex-start;
|
||||||
|
}}
|
||||||
|
.grid {{
|
||||||
|
width:120px; height:70px; display:grid;
|
||||||
|
grid-template-columns:100px;
|
||||||
|
grid-template-rows:60px;
|
||||||
|
}}
|
||||||
|
.front {{
|
||||||
|
box-sizing:border-box; width:100px; height:60px;
|
||||||
|
z-index:2; background:#ff0000; border:5px solid #00ff00;
|
||||||
|
color:#000000; font-size:24px; line-height:30px;
|
||||||
|
}}
|
||||||
|
.cover {{
|
||||||
|
position:absolute; z-index:auto; left:0; top:0;
|
||||||
|
width:100px; height:60px; background:#0000ff;
|
||||||
|
}}
|
||||||
|
.cell {{ grid-area:1 / 1; }}
|
||||||
|
img.cell {{ width:100px; height:60px; }}
|
||||||
|
</style></head><body>
|
||||||
|
<div class="row">
|
||||||
|
<div class="front">FLEX</div>
|
||||||
|
<div class="cover"></div>
|
||||||
|
</div>
|
||||||
|
<div class="grid">
|
||||||
|
<div class="front cell">GRID</div>
|
||||||
|
<img class="cell" alt="" src="{blue_image}">
|
||||||
|
</div>
|
||||||
|
<div class="row" style="height:60px">
|
||||||
|
<div class="front" style="z-index:1">LOW</div>
|
||||||
|
<div class="cover" style="z-index:3"></div>
|
||||||
|
</div>
|
||||||
|
</body></html>"#
|
||||||
|
));
|
||||||
|
let pixmap = paint_dom(&tree, (140.0, 210.0), None).expect("pixmap");
|
||||||
|
|
||||||
|
let is_green = |x, y| {
|
||||||
|
let pixel = pixmap.pixel(x, y).expect("border pixel");
|
||||||
|
pixel.green() > 230 && pixel.red() < 30 && pixel.blue() < 30
|
||||||
|
};
|
||||||
|
let is_red = |x, y| {
|
||||||
|
let pixel = pixmap.pixel(x, y).expect("background pixel");
|
||||||
|
pixel.red() > 230 && pixel.green() < 30 && pixel.blue() < 30
|
||||||
|
};
|
||||||
|
assert!(
|
||||||
|
is_green(2, 30) && is_red(80, 50),
|
||||||
|
"the static flex item's border/background must paint above its later absolute sibling"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
is_green(2, 100) && is_red(80, 120),
|
||||||
|
"the static grid item's border/background must paint above its later image sibling"
|
||||||
|
);
|
||||||
|
|
||||||
|
for (label, y0) in [("flex", 0), ("grid", 70)] {
|
||||||
|
let dark_ink = (8..92)
|
||||||
|
.flat_map(|x| (y0 + 8..y0 + 42).map(move |y| (x, y)))
|
||||||
|
.filter(|&(x, y)| {
|
||||||
|
let pixel = pixmap.pixel(x, y).expect("text pixel");
|
||||||
|
pixel.red() < 40 && pixel.green() < 40 && pixel.blue() < 40
|
||||||
|
})
|
||||||
|
.count();
|
||||||
|
assert!(
|
||||||
|
dark_ink > 20,
|
||||||
|
"{label} item text must remain inside the raised atomic subtree, found {dark_ink} dark pixels"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
(0..60).all(|dy| (0..100).all(|x| {
|
||||||
|
let pixel = pixmap.pixel(x, 140 + dy).expect("covered pixel");
|
||||||
|
pixel.blue() > 230 && pixel.red() < 30 && pixel.green() < 30
|
||||||
|
})),
|
||||||
|
"a higher-z sibling must cover the lower item's background, border, and shaped text as one unit"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn overflow_clipping_keeps_physical_axes_independent() {
|
fn overflow_clipping_keeps_physical_axes_independent() {
|
||||||
let tree = parse_html(
|
let tree = parse_html(
|
||||||
|
|||||||
@@ -229,25 +229,17 @@ pub fn ua_style(tag: &str) -> LayoutStyle {
|
|||||||
if tag == "th" {
|
if tag == "th" {
|
||||||
style.font_weight = Some("bold".to_string());
|
style.font_weight = Some("bold".to_string());
|
||||||
}
|
}
|
||||||
} else if tag == "img" || tag == "figure" {
|
} else if tag == "img" {
|
||||||
if tag == "img" {
|
|
||||||
// Images are inline-level replaced elements by default. Model the
|
// Images are inline-level replaced elements by default. Model the
|
||||||
// atomic inner box with the same inline-block marker used by
|
// atomic inner box with the same inline-block marker used by native
|
||||||
// native controls, so an icon between text fragments participates
|
// controls, so an icon between text fragments participates in their
|
||||||
// in their line instead of splitting the parent into block runs.
|
// line instead of splitting the parent into block runs. Chromium's UA
|
||||||
// An authored display declaration clears this marker in the
|
// sheet does not impose a responsive max-width: author/reset CSS must
|
||||||
// cascade before applying its requested display.
|
// opt into that. A synthetic `max-width:100%` here distorts deliberately
|
||||||
|
// oversized art whose height and intrinsic ratio establish its width.
|
||||||
style.display = Display::Inline;
|
style.display = Display::Inline;
|
||||||
style.is_inline_block = true;
|
style.is_inline_block = true;
|
||||||
}
|
}
|
||||||
// Near-universal reset: images fit their container instead of
|
|
||||||
// overflowing. Wikipedia (and most sites) set `img{max-width:100%}`;
|
|
||||||
// making it a UA default prevents a fixed-width thumbnail (e.g. a
|
|
||||||
// 250px infobox image) from spilling out of a narrower box (a 200px
|
|
||||||
// taxobox) when we have not applied the site's own rule.
|
|
||||||
style.max_width = crate::Dimension::Percent(1.0);
|
|
||||||
style.flex_shrink = Some(1.0);
|
|
||||||
}
|
|
||||||
style
|
style
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -643,11 +635,9 @@ fn unitless_math_tokens<'i, 't>(
|
|||||||
fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
|
fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
|
||||||
match name {
|
match name {
|
||||||
"display" => {
|
"display" => {
|
||||||
if value != "contents" {
|
let value = value.trim().to_ascii_lowercase();
|
||||||
style.display_contents = false;
|
|
||||||
}
|
|
||||||
if matches!(
|
if matches!(
|
||||||
value,
|
value.as_str(),
|
||||||
"none"
|
"none"
|
||||||
| "flex"
|
| "flex"
|
||||||
| "inline-flex"
|
| "inline-flex"
|
||||||
@@ -660,13 +650,22 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
|
|||||||
| "table"
|
| "table"
|
||||||
| "inline-table"
|
| "inline-table"
|
||||||
| "contents"
|
| "contents"
|
||||||
|
| "inherit"
|
||||||
|
| "initial"
|
||||||
|
| "unset"
|
||||||
) {
|
) {
|
||||||
|
// Every valid authored display value replaces the complete
|
||||||
|
// outer/inner display pair. In particular, it must not retain
|
||||||
|
// the UA table/control approximation merely because its
|
||||||
|
// internal enum happens to equal the newly specified value.
|
||||||
style.internal_flex_container = false;
|
style.internal_flex_container = false;
|
||||||
style.is_table_box = false;
|
style.is_table_box = false;
|
||||||
style.is_inline_block = false;
|
style.is_inline_block = false;
|
||||||
style.flow_root = false;
|
style.flow_root = false;
|
||||||
|
style.display_contents = false;
|
||||||
|
style.display_inherit = false;
|
||||||
}
|
}
|
||||||
match value {
|
match value.as_str() {
|
||||||
"none" => style.display = crate::Display::None,
|
"none" => style.display = crate::Display::None,
|
||||||
"flex" => style.display = crate::Display::Flex,
|
"flex" => style.display = crate::Display::Flex,
|
||||||
"inline-flex" => {
|
"inline-flex" => {
|
||||||
@@ -712,6 +711,19 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
|
|||||||
style.display = crate::Display::Block;
|
style.display = crate::Display::Block;
|
||||||
style.display_contents = true;
|
style.display_contents = true;
|
||||||
}
|
}
|
||||||
|
"inherit" => {
|
||||||
|
// Keep a valid initial-display placeholder until the
|
||||||
|
// parent computed style is available. The top-down pass
|
||||||
|
// copies the parent's outer and inner display provenance,
|
||||||
|
// but never its engine-only native-layout flags.
|
||||||
|
style.display = crate::Display::Inline;
|
||||||
|
style.display_inherit = true;
|
||||||
|
}
|
||||||
|
"initial" | "unset" => {
|
||||||
|
// `display` is not inherited and its CSS initial value is
|
||||||
|
// `inline`, independent of the element's HTML UA display.
|
||||||
|
style.display = crate::Display::Inline;
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1530,14 +1542,7 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
|
|||||||
"grid-template" => parse_grid_template(style, value),
|
"grid-template" => parse_grid_template(style, value),
|
||||||
"grid" => parse_grid_shorthand(style, value),
|
"grid" => parse_grid_shorthand(style, value),
|
||||||
"grid-auto-flow" => style.grid_auto_flow = parse_grid_auto_flow(value),
|
"grid-auto-flow" => style.grid_auto_flow = parse_grid_auto_flow(value),
|
||||||
"grid-area" => {
|
"grid-area" => set_grid_area(style, value),
|
||||||
// Named area (single ident) or line form `r/c/r/c`. We only resolve
|
|
||||||
// the named-area case here; line forms are handled by grid-row/column.
|
|
||||||
let v = value.trim();
|
|
||||||
if !v.contains('/') && !v.is_empty() {
|
|
||||||
style.grid_area_name = Some(v.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"grid-column" => set_grid_placement(style, value, true),
|
"grid-column" => set_grid_placement(style, value, true),
|
||||||
"grid-row" => set_grid_placement(style, value, false),
|
"grid-row" => set_grid_placement(style, value, false),
|
||||||
"grid-column-start" => set_grid_placement_side(style, value, true, true),
|
"grid-column-start" => set_grid_placement_side(style, value, true, true),
|
||||||
@@ -1871,6 +1876,17 @@ pub(crate) fn supports_declaration(name: &str, value: &str) -> bool {
|
|||||||
"overflow" | "overflow-x" | "overflow-y" => {
|
"overflow" | "overflow-x" | "overflow-y" => {
|
||||||
parse_overflow_declaration(&name, value).is_some()
|
parse_overflow_declaration(&name, value).is_some()
|
||||||
}
|
}
|
||||||
|
"grid-area" => {
|
||||||
|
let parts = split_top_level(value, '/')
|
||||||
|
.into_iter()
|
||||||
|
.map(str::trim)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
!parts.is_empty()
|
||||||
|
&& parts.len() <= 4
|
||||||
|
&& parts.iter().all(|part| {
|
||||||
|
!part.is_empty() && parse_grid_line_kind(part).is_some()
|
||||||
|
})
|
||||||
|
}
|
||||||
"color" | "-webkit-text-fill-color" | "background-color" | "border-color" => {
|
"color" | "-webkit-text-fill-color" | "background-color" | "border-color" => {
|
||||||
parse_color(value).is_some()
|
parse_color(value).is_some()
|
||||||
}
|
}
|
||||||
@@ -2880,6 +2896,226 @@ fn parse_grid_auto_flow(value: &str) -> Option<taffy::GridAutoFlow> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Expand the `grid-area` shorthand into its row/column placements.
|
||||||
|
///
|
||||||
|
/// The four slash-separated components are, in order, row-start,
|
||||||
|
/// column-start, row-end, and column-end. Missing end values repeat a
|
||||||
|
/// custom-ident start (the named-area form) and otherwise become `auto`, per
|
||||||
|
/// CSS Grid. Keeping the expansion in the computed style is important for the
|
||||||
|
/// common overlay idiom `grid-area: 1 / 1 / 1 / 1`; treating every slash form
|
||||||
|
/// as unsupported makes each child auto-place into a new implicit row.
|
||||||
|
fn set_grid_area(style: &mut LayoutStyle, value: &str) {
|
||||||
|
if matches!(
|
||||||
|
value.trim().to_ascii_lowercase().as_str(),
|
||||||
|
"initial" | "inherit" | "unset" | "revert" | "revert-layer"
|
||||||
|
) {
|
||||||
|
style.grid_area_name = None;
|
||||||
|
set_grid_placement(style, "auto / auto", true);
|
||||||
|
set_grid_placement(style, "auto / auto", false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parts = split_top_level(value, '/')
|
||||||
|
.into_iter()
|
||||||
|
.map(str::trim)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if parts.is_empty()
|
||||||
|
|| parts.len() > 4
|
||||||
|
|| parts.iter().any(|part| part.is_empty())
|
||||||
|
|| parts
|
||||||
|
.iter()
|
||||||
|
.any(|part| parse_grid_line_kind(part).is_none())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let row_start = parts[0];
|
||||||
|
let column_start = parts
|
||||||
|
.get(1)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or_else(|| grid_area_omitted_side(row_start));
|
||||||
|
let row_end = parts
|
||||||
|
.get(2)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or_else(|| grid_area_omitted_side(row_start));
|
||||||
|
let column_end = parts
|
||||||
|
.get(3)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or_else(|| grid_area_omitted_side(column_start));
|
||||||
|
|
||||||
|
// Preserve the compact named-area representation used by
|
||||||
|
// `resolve_grid_areas`, while also retaining the generated `name-start` /
|
||||||
|
// `name-end` line form for grids that expose those named lines directly.
|
||||||
|
if parts.len() == 1 && is_grid_custom_ident(row_start) {
|
||||||
|
style.grid_area_name = Some(row_start.to_string());
|
||||||
|
} else {
|
||||||
|
style.grid_area_name = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
set_grid_placement(
|
||||||
|
style,
|
||||||
|
&format!("{column_start} / {column_end}"),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
set_grid_placement(style, &format!("{row_start} / {row_end}"), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn grid_area_omitted_side(start: &str) -> &str {
|
||||||
|
if is_grid_custom_ident(start) {
|
||||||
|
start
|
||||||
|
} else {
|
||||||
|
"auto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_grid_custom_ident(value: &str) -> bool {
|
||||||
|
matches!(
|
||||||
|
parse_grid_line_kind(value),
|
||||||
|
Some(GridLineKind::IdentOnly)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
enum GridLineKind {
|
||||||
|
IdentOnly,
|
||||||
|
Other,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate one `<grid-line>` without mutating the computed style. Shorthand
|
||||||
|
/// declarations are atomic: if any of their one-to-four components is invalid,
|
||||||
|
/// the entire declaration must be ignored rather than partially resetting
|
||||||
|
/// earlier winning longhands.
|
||||||
|
fn parse_grid_line_kind(value: &str) -> Option<GridLineKind> {
|
||||||
|
let mut input = cssparser::ParserInput::new(value.trim());
|
||||||
|
let mut parser = cssparser::Parser::new(&mut input);
|
||||||
|
|
||||||
|
// auto
|
||||||
|
{
|
||||||
|
let state = parser.state();
|
||||||
|
if parser
|
||||||
|
.expect_ident_cloned()
|
||||||
|
.is_ok_and(|ident| ident.eq_ignore_ascii_case("auto"))
|
||||||
|
&& parser.is_exhausted()
|
||||||
|
{
|
||||||
|
return Some(GridLineKind::Other);
|
||||||
|
}
|
||||||
|
parser.reset(&state);
|
||||||
|
}
|
||||||
|
|
||||||
|
// span && [ <positive-integer> || <custom-ident> ]
|
||||||
|
{
|
||||||
|
let state = parser.state();
|
||||||
|
if parser
|
||||||
|
.expect_ident_cloned()
|
||||||
|
.is_ok_and(|ident| ident.eq_ignore_ascii_case("span"))
|
||||||
|
{
|
||||||
|
let mut saw_integer = consume_grid_integer(&mut parser, true);
|
||||||
|
let mut saw_ident = consume_grid_custom_ident(&mut parser);
|
||||||
|
if !saw_integer {
|
||||||
|
saw_integer = consume_grid_integer(&mut parser, true);
|
||||||
|
}
|
||||||
|
if !saw_ident {
|
||||||
|
saw_ident = consume_grid_custom_ident(&mut parser);
|
||||||
|
}
|
||||||
|
if (saw_integer || saw_ident) && parser.is_exhausted() {
|
||||||
|
return Some(GridLineKind::Other);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parser.reset(&state);
|
||||||
|
}
|
||||||
|
|
||||||
|
// [ <non-zero-integer> && <custom-ident>? ], in either order.
|
||||||
|
{
|
||||||
|
let state = parser.state();
|
||||||
|
if consume_grid_integer(&mut parser, false) {
|
||||||
|
let _ = consume_grid_custom_ident(&mut parser);
|
||||||
|
if parser.is_exhausted() {
|
||||||
|
return Some(GridLineKind::Other);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parser.reset(&state);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let state = parser.state();
|
||||||
|
if consume_grid_custom_ident(&mut parser) {
|
||||||
|
if parser.is_exhausted() {
|
||||||
|
return Some(GridLineKind::IdentOnly);
|
||||||
|
}
|
||||||
|
if consume_grid_integer(&mut parser, false) && parser.is_exhausted() {
|
||||||
|
return Some(GridLineKind::Other);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parser.reset(&state);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modern engines accept an integer-valued math function as the numeric
|
||||||
|
// half of a grid line. Keep it non-ident-only so omitted shorthand sides
|
||||||
|
// become auto. Resolution remains deferred in the raw placement path.
|
||||||
|
if consume_grid_integer_math(&mut parser) {
|
||||||
|
let _ = consume_grid_custom_ident(&mut parser);
|
||||||
|
if parser.is_exhausted() {
|
||||||
|
return Some(GridLineKind::Other);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn consume_grid_integer(parser: &mut cssparser::Parser<'_, '_>, positive_only: bool) -> bool {
|
||||||
|
let state = parser.state();
|
||||||
|
let valid = parser.expect_integer().is_ok_and(|integer| {
|
||||||
|
if positive_only {
|
||||||
|
integer > 0
|
||||||
|
} else {
|
||||||
|
integer != 0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if !valid {
|
||||||
|
parser.reset(&state);
|
||||||
|
}
|
||||||
|
valid
|
||||||
|
}
|
||||||
|
|
||||||
|
fn consume_grid_custom_ident(parser: &mut cssparser::Parser<'_, '_>) -> bool {
|
||||||
|
let state = parser.state();
|
||||||
|
let valid = parser.expect_ident_cloned().is_ok_and(|ident| {
|
||||||
|
!matches!(
|
||||||
|
ident.to_ascii_lowercase().as_str(),
|
||||||
|
"auto" | "span" | "initial" | "inherit" | "unset" | "revert" | "revert-layer"
|
||||||
|
)
|
||||||
|
});
|
||||||
|
if !valid {
|
||||||
|
parser.reset(&state);
|
||||||
|
}
|
||||||
|
valid
|
||||||
|
}
|
||||||
|
|
||||||
|
fn consume_grid_integer_math(parser: &mut cssparser::Parser<'_, '_>) -> bool {
|
||||||
|
let state = parser.state();
|
||||||
|
let is_math = parser.expect_function().is_ok_and(|name| {
|
||||||
|
matches!(
|
||||||
|
name.to_ascii_lowercase().as_str(),
|
||||||
|
"calc" | "min" | "max" | "clamp"
|
||||||
|
)
|
||||||
|
});
|
||||||
|
if !is_math {
|
||||||
|
parser.reset(&state);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let valid = parser
|
||||||
|
.parse_nested_block::<_, _, ()>(|nested| {
|
||||||
|
if nested.is_exhausted() {
|
||||||
|
return Err(nested.new_custom_error(()));
|
||||||
|
}
|
||||||
|
while nested.next_including_whitespace().is_ok() {}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.is_ok();
|
||||||
|
if !valid {
|
||||||
|
parser.reset(&state);
|
||||||
|
}
|
||||||
|
valid
|
||||||
|
}
|
||||||
|
|
||||||
/// Store a `grid-column`/`grid-row` value. Numeric/`span` forms resolve to a
|
/// Store a `grid-column`/`grid-row` value. Numeric/`span` forms resolve to a
|
||||||
/// `taffy::Line` now; a value that names a grid line (`content-start /
|
/// `taffy::Line` now; a value that names a grid line (`content-start /
|
||||||
/// content-end`, or the `grid-column: content` area shorthand) is kept raw and
|
/// content-end`, or the `grid-column: content` area shorthand) is kept raw and
|
||||||
@@ -2996,7 +3232,8 @@ fn grid_line_has_name(value: &str) -> bool {
|
|||||||
if p.eq_ignore_ascii_case("auto") {
|
if p.eq_ignore_ascii_case("auto") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let rest = p.strip_prefix("span").map(str::trim).unwrap_or(p);
|
let lower = p.to_ascii_lowercase();
|
||||||
|
let rest = lower.strip_prefix("span").map(str::trim).unwrap_or(&lower);
|
||||||
rest.chars().any(|c| c.is_ascii_alphabetic())
|
rest.chars().any(|c| c.is_ascii_alphabetic())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -3016,7 +3253,8 @@ fn parse_grid_line(value: &str) -> Option<taffy::Line<taffy::GridPlacement>> {
|
|||||||
|
|
||||||
fn parse_grid_placement(value: &str) -> taffy::GridPlacement {
|
fn parse_grid_placement(value: &str) -> taffy::GridPlacement {
|
||||||
let value = value.trim();
|
let value = value.trim();
|
||||||
if let Some(span) = value.strip_prefix("span").map(str::trim) {
|
let lower = value.to_ascii_lowercase();
|
||||||
|
if let Some(span) = lower.strip_prefix("span").map(str::trim) {
|
||||||
if let Ok(span) = span.parse::<u16>() {
|
if let Ok(span) = span.parse::<u16>() {
|
||||||
return taffy::style_helpers::span(span);
|
return taffy::style_helpers::span(span);
|
||||||
}
|
}
|
||||||
@@ -3864,7 +4102,16 @@ fn split_top_level(s: &str, sep: char) -> Vec<&str> {
|
|||||||
let mut parts = Vec::new();
|
let mut parts = Vec::new();
|
||||||
let mut depth = 0i32;
|
let mut depth = 0i32;
|
||||||
let mut start = 0;
|
let mut start = 0;
|
||||||
|
let mut escaped = false;
|
||||||
for (i, c) in s.char_indices() {
|
for (i, c) in s.char_indices() {
|
||||||
|
if escaped {
|
||||||
|
escaped = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if c == '\\' {
|
||||||
|
escaped = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
match c {
|
match c {
|
||||||
'(' => depth += 1,
|
'(' => depth += 1,
|
||||||
')' => depth -= 1,
|
')' => depth -= 1,
|
||||||
@@ -5624,6 +5871,32 @@ mod tests {
|
|||||||
assert!(style.display_contents);
|
assert!(style.display_contents);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn display_css_wide_values_replace_ua_and_prior_provenance() {
|
||||||
|
let initial = compute_style("td", Some("display:initial"));
|
||||||
|
assert_eq!(initial.display, Display::Inline);
|
||||||
|
assert!(!initial.internal_flex_container);
|
||||||
|
assert!(!initial.is_inline_block);
|
||||||
|
assert!(!initial.display_contents);
|
||||||
|
|
||||||
|
let unset = compute_style("div", Some("display:flex;display:unset"));
|
||||||
|
assert_eq!(unset.display, Display::Inline);
|
||||||
|
assert!(!unset.is_inline_block);
|
||||||
|
|
||||||
|
let inherited = compute_style("td", Some("display:contents;display:inherit"));
|
||||||
|
assert_eq!(inherited.display, Display::Inline);
|
||||||
|
assert!(inherited.display_inherit);
|
||||||
|
assert!(!inherited.internal_flex_container);
|
||||||
|
assert!(!inherited.display_contents);
|
||||||
|
|
||||||
|
let important = compute_style(
|
||||||
|
"div",
|
||||||
|
Some("display:block!important;display:contents"),
|
||||||
|
);
|
||||||
|
assert_eq!(important.display, Display::Block);
|
||||||
|
assert!(!important.display_contents);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn authored_display_replaces_internal_flex_provenance() {
|
fn authored_display_replaces_internal_flex_provenance() {
|
||||||
let native_cell = compute_style("td", None);
|
let native_cell = compute_style("td", None);
|
||||||
@@ -5691,6 +5964,17 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn image_ua_style_does_not_invent_a_responsive_size_cap() {
|
||||||
|
let image = ua_style("img");
|
||||||
|
assert_eq!(image.display, crate::Display::Inline);
|
||||||
|
assert!(image.is_inline_block);
|
||||||
|
assert_eq!(image.max_width, crate::Dimension::Auto);
|
||||||
|
|
||||||
|
let authored = compute_style("img", Some("max-width:100%"));
|
||||||
|
assert_eq!(authored.max_width, crate::Dimension::Percent(1.0));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn border_none_clears_native_and_per_side_widths() {
|
fn border_none_clears_native_and_per_side_widths() {
|
||||||
let input = compute_style("input", Some("border:none"));
|
let input = compute_style("input", Some("border:none"));
|
||||||
@@ -6343,6 +6627,146 @@ mod tests {
|
|||||||
assert_eq!(reset.grid_column_raw, None);
|
assert_eq!(reset.grid_column_raw, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grid_area_expands_slash_placements_and_named_area_defaults() {
|
||||||
|
let overlay = compute_style("div", Some("grid-area:1 / 2 / 3 / 4"));
|
||||||
|
assert_eq!(
|
||||||
|
overlay.grid_row,
|
||||||
|
Some(taffy::Line {
|
||||||
|
start: taffy::style_helpers::line(1),
|
||||||
|
end: taffy::style_helpers::line(3),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
overlay.grid_column,
|
||||||
|
Some(taffy::Line {
|
||||||
|
start: taffy::style_helpers::line(2),
|
||||||
|
end: taffy::style_helpers::line(4),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(overlay.grid_area_name, None);
|
||||||
|
|
||||||
|
let named = compute_style("div", Some("grid-area:hero"));
|
||||||
|
assert_eq!(named.grid_area_name.as_deref(), Some("hero"));
|
||||||
|
assert_eq!(named.grid_row_raw.as_deref(), Some("hero / hero"));
|
||||||
|
assert_eq!(named.grid_column_raw.as_deref(), Some("hero / hero"));
|
||||||
|
|
||||||
|
let reset = compute_style("div", Some("grid-area:hero;grid-area:initial"));
|
||||||
|
assert_eq!(reset.grid_area_name, None);
|
||||||
|
assert_eq!(
|
||||||
|
reset.grid_row,
|
||||||
|
Some(taffy::Line {
|
||||||
|
start: taffy::GridPlacement::Auto,
|
||||||
|
end: taffy::GridPlacement::Auto,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
reset.grid_column,
|
||||||
|
Some(taffy::Line {
|
||||||
|
start: taffy::GridPlacement::Auto,
|
||||||
|
end: taffy::GridPlacement::Auto,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
for value in ["2 foo", "foo 2", "calc(1)"] {
|
||||||
|
let line = compute_style("div", Some(&format!("grid-area:{value}")));
|
||||||
|
let expected_row = format!("{value} / auto");
|
||||||
|
assert_eq!(
|
||||||
|
line.grid_area_name, None,
|
||||||
|
"{value} is a grid line, not an ident-only named area"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
line.grid_row_raw.as_deref(),
|
||||||
|
Some(expected_row.as_str())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
line.grid_column,
|
||||||
|
Some(taffy::Line {
|
||||||
|
start: taffy::GridPlacement::Auto,
|
||||||
|
end: taffy::GridPlacement::Auto,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let one = compute_style("div", Some("grid-area:1"));
|
||||||
|
assert_eq!(
|
||||||
|
one.grid_row,
|
||||||
|
Some(taffy::Line {
|
||||||
|
start: taffy::style_helpers::line(1),
|
||||||
|
end: taffy::GridPlacement::Auto,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
one.grid_column,
|
||||||
|
Some(taffy::Line {
|
||||||
|
start: taffy::GridPlacement::Auto,
|
||||||
|
end: taffy::GridPlacement::Auto,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
let mixed = compute_style("div", Some("grid-area:hero / 2"));
|
||||||
|
assert_eq!(mixed.grid_row_raw.as_deref(), Some("hero / hero"));
|
||||||
|
assert_eq!(
|
||||||
|
mixed.grid_column,
|
||||||
|
Some(taffy::Line {
|
||||||
|
start: taffy::style_helpers::line(2),
|
||||||
|
end: taffy::GridPlacement::Auto,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
let copied_column_ident = compute_style("div", Some("grid-area:1 / col / 3"));
|
||||||
|
assert_eq!(
|
||||||
|
copied_column_ident.grid_row,
|
||||||
|
Some(taffy::Line {
|
||||||
|
start: taffy::style_helpers::line(1),
|
||||||
|
end: taffy::style_helpers::line(3),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
copied_column_ident.grid_column_raw.as_deref(),
|
||||||
|
Some("col / col")
|
||||||
|
);
|
||||||
|
|
||||||
|
let uppercase_span = compute_style("div", Some("grid-area:SPAN 2"));
|
||||||
|
assert_eq!(
|
||||||
|
uppercase_span.grid_row,
|
||||||
|
Some(taffy::Line {
|
||||||
|
start: taffy::style_helpers::span(2),
|
||||||
|
end: taffy::GridPlacement::Auto,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
for invalid in ["0", "span", "span 0", "span -1", "foo bar", "1 / inherit", "1.5"] {
|
||||||
|
let style = compute_style(
|
||||||
|
"div",
|
||||||
|
Some(&format!("grid-area:7 / 8 / 9 / 10;grid-area:{invalid}")),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
style.grid_row,
|
||||||
|
Some(taffy::Line {
|
||||||
|
start: taffy::style_helpers::line(7),
|
||||||
|
end: taffy::style_helpers::line(9),
|
||||||
|
}),
|
||||||
|
"invalid `{invalid}` must not partially mutate the earlier shorthand"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
style.grid_column,
|
||||||
|
Some(taffy::Line {
|
||||||
|
start: taffy::style_helpers::line(8),
|
||||||
|
end: taffy::style_helpers::line(10),
|
||||||
|
}),
|
||||||
|
"invalid `{invalid}` must not partially mutate the earlier shorthand"
|
||||||
|
);
|
||||||
|
assert!(!supports_declaration("grid-area", invalid));
|
||||||
|
}
|
||||||
|
for valid in ["hero", "1", "hero / 2", "1 / col / 3", "SPAN 2", "span foo"] {
|
||||||
|
assert!(
|
||||||
|
supports_declaration("grid-area", valid),
|
||||||
|
"valid grid-area `{valid}` should pass @supports parsing"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn contextual_css_math_uses_runtime_geometry() {
|
fn contextual_css_math_uses_runtime_geometry() {
|
||||||
let context = (20.0, 16.0, 9.0, 10.0, 900.0);
|
let context = (20.0, 16.0, 9.0, 10.0, 900.0);
|
||||||
|
|||||||
@@ -2345,6 +2345,49 @@ fn auto_grid_track_freezes_at_growth_limit_during_distribution() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grid_area_line_shorthand_overlays_children_in_the_same_cell() {
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"
|
||||||
|
<style>
|
||||||
|
* { box-sizing:border-box }
|
||||||
|
html, body { margin:0 }
|
||||||
|
#grid {
|
||||||
|
display:grid;
|
||||||
|
width:320px;
|
||||||
|
grid-template-columns:320px;
|
||||||
|
grid-template-rows:180px;
|
||||||
|
}
|
||||||
|
#grid > * {
|
||||||
|
grid-area:1 / 1 / 1 / 1;
|
||||||
|
width:320px;
|
||||||
|
height:180px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div id="grid">
|
||||||
|
<div id="left"></div>
|
||||||
|
<div id="right"></div>
|
||||||
|
<div id="slider"></div>
|
||||||
|
</div>
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
let layout = layout_dom(&tree, (640.0, 480.0));
|
||||||
|
let rect = |id| layout.rects[&tree.get_element_by_id(id).unwrap()];
|
||||||
|
let grid = rect("grid");
|
||||||
|
for id in ["left", "right", "slider"] {
|
||||||
|
let child = rect(id);
|
||||||
|
assert!(
|
||||||
|
(child.x - grid.x).abs() < 0.01 && (child.y - grid.y).abs() < 0.01,
|
||||||
|
"{id} should overlay the first grid cell instead of auto-placing: \
|
||||||
|
grid={grid:?} child={child:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
(grid.height - 180.0).abs() < 0.01,
|
||||||
|
"overlaid children must not create implicit rows: {grid:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Chromium 150 oracle for the full-span nested column-subgrid pattern used by
|
/// Chromium 150 oracle for the full-span nested column-subgrid pattern used by
|
||||||
/// Mozilla's springboard/data rows. The 20px child gap is centered on the
|
/// Mozilla's springboard/data rows. The 20px child gap is centered on the
|
||||||
/// ancestor's zero-width grid lines, so descendant contributions inflate the
|
/// ancestor's zero-width grid lines, so descendant contributions inflate the
|
||||||
@@ -2655,6 +2698,13 @@ fn selected_picture_source_dimensions_override_fallback_image_ratio() {
|
|||||||
</style>
|
</style>
|
||||||
<picture>
|
<picture>
|
||||||
<source
|
<source
|
||||||
|
type="image/AVIF; codecs=av01"
|
||||||
|
media="(min-width:800px)"
|
||||||
|
srcset="unsupported.avif"
|
||||||
|
width="2000"
|
||||||
|
height="100">
|
||||||
|
<source
|
||||||
|
type=" IMAGE/PNG ; profile=srgb "
|
||||||
media="(min-width:800px)"
|
media="(min-width:800px)"
|
||||||
srcset="chosen.png"
|
srcset="chosen.png"
|
||||||
width="800"
|
width="800"
|
||||||
@@ -2784,6 +2834,66 @@ fn both_auto_replaced_max_height_preserves_intrinsic_ratio() {
|
|||||||
assert_eq!((rect.width, rect.height), (203.0, 128.0), "{rect:?}");
|
assert_eq!((rect.width, rect.height), (203.0, 128.0), "{rect:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn height_sized_oversized_image_keeps_intrinsic_ratio_without_an_author_cap() {
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"
|
||||||
|
<style>
|
||||||
|
html, body { margin:0 }
|
||||||
|
#wrapper {
|
||||||
|
position:relative;
|
||||||
|
width:1280px;
|
||||||
|
height:692px;
|
||||||
|
overflow:hidden;
|
||||||
|
}
|
||||||
|
#image {
|
||||||
|
position:absolute;
|
||||||
|
left:50%;
|
||||||
|
bottom:0;
|
||||||
|
width:auto;
|
||||||
|
height:100%;
|
||||||
|
transform:translateX(-50%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div id="wrapper"><img id="image" src="image.jpg"></div>
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
let image = tree.get_element_by_id("image").unwrap();
|
||||||
|
let intrinsic = HashMap::from([(image, (3008.0, 692.0))]);
|
||||||
|
let layout = layout_dom_with_images(&tree, (1280.0, 1400.0), &intrinsic);
|
||||||
|
let rect = layout.rects[&image];
|
||||||
|
|
||||||
|
// Layout stores the pre-transform border box at the authored 50% inset;
|
||||||
|
// paint/CSSOM applies translateX(-50%) and exposes the final x=-864.
|
||||||
|
assert!((rect.x - 640.0).abs() < 0.01, "{rect:?}");
|
||||||
|
assert!((rect.width - 3008.0).abs() < 0.01, "{rect:?}");
|
||||||
|
assert!((rect.height - 692.0).abs() < 0.01, "{rect:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn authored_percentage_max_width_still_clamps_an_oversized_image() {
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"
|
||||||
|
<style>
|
||||||
|
html, body { margin:0 }
|
||||||
|
#wrapper { width:1280px }
|
||||||
|
#image { display:block; width:auto; height:auto; max-width:100% }
|
||||||
|
</style>
|
||||||
|
<div id="wrapper"><img id="image" src="image.jpg"></div>
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
let image = tree.get_element_by_id("image").unwrap();
|
||||||
|
let intrinsic = HashMap::from([(image, (3008.0, 692.0))]);
|
||||||
|
let layout = layout_dom_with_images(&tree, (1280.0, 1400.0), &intrinsic);
|
||||||
|
let rect = layout.rects[&image];
|
||||||
|
|
||||||
|
assert!((rect.width - 1280.0).abs() < 0.01, "{rect:?}");
|
||||||
|
assert!(
|
||||||
|
(rect.height - (1280.0 * 692.0 / 3008.0)).abs() < 1.0,
|
||||||
|
"{rect:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Chromium 150 applies CSS Sizing's compressible replaced-element rule when
|
/// Chromium 150 applies CSS Sizing's compressible replaced-element rule when
|
||||||
/// `max-width:100%` is cyclic during intrinsic sizing. Each image contributes
|
/// `max-width:100%` is cyclic during intrinsic sizing. Each image contributes
|
||||||
/// zero to its column's automatic minimum, so the row can flex-shrink the
|
/// zero to its column's automatic minimum, so the row can flex-shrink the
|
||||||
@@ -3469,3 +3579,164 @@ fn fit_content_width_keeps_an_unbreakable_min_content_floor() {
|
|||||||
"min-content must win when an unbreakable token is wider than the available space: {rect:?}"
|
"min-content must win when an unbreakable token is wider than the available space: {rect:?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn closed_details_lays_out_only_its_first_direct_summary() {
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"
|
||||||
|
<body style="margin:0">
|
||||||
|
<details id="closed" style="width:200px">
|
||||||
|
source text before the summary
|
||||||
|
<div id="before" style="height:30px">
|
||||||
|
<summary id="nested">nested summary is not the details summary</summary>
|
||||||
|
</div>
|
||||||
|
<summary id="first" style="height:20px">visible summary</summary>
|
||||||
|
<div id="content" style="height:50px">
|
||||||
|
<span id="descendant">hidden descendant</span>
|
||||||
|
</div>
|
||||||
|
<summary id="second" style="height:40px">later summary is content</summary>
|
||||||
|
</details>
|
||||||
|
</body>
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
let layout = layout_dom(&tree, (400.0, 200.0));
|
||||||
|
let id = |name| tree.get_element_by_id(name).unwrap();
|
||||||
|
|
||||||
|
assert!(layout.rects.contains_key(&id("closed")));
|
||||||
|
assert!(layout.rects.contains_key(&id("first")));
|
||||||
|
for hidden in ["before", "nested", "content", "descendant", "second"] {
|
||||||
|
assert!(
|
||||||
|
!layout.rects.contains_key(&id(hidden)),
|
||||||
|
"closed details content must not generate a box: {hidden}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
(layout.rects[&id("closed")].height - 20.0).abs() < 0.1,
|
||||||
|
"only the first direct summary contributes to closed details geometry"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn open_details_lays_out_all_of_its_content() {
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"
|
||||||
|
<body style="margin:0">
|
||||||
|
<details id="open-details" open style="width:200px">
|
||||||
|
<summary id="summary" style="height:20px">visible summary</summary>
|
||||||
|
<div id="content" style="height:50px">
|
||||||
|
<span id="descendant" style="display:block;height:10px">visible descendant</span>
|
||||||
|
</div>
|
||||||
|
<summary id="second" style="height:40px">later summary is ordinary content</summary>
|
||||||
|
</details>
|
||||||
|
</body>
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
let layout = layout_dom(&tree, (400.0, 200.0));
|
||||||
|
let id = |name| tree.get_element_by_id(name).unwrap();
|
||||||
|
|
||||||
|
for visible in [
|
||||||
|
"open-details",
|
||||||
|
"summary",
|
||||||
|
"content",
|
||||||
|
"descendant",
|
||||||
|
"second",
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
layout.rects.contains_key(&id(visible)),
|
||||||
|
"open details must retain its rendered content: {visible}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
(layout.rects[&id("open-details")].height - 110.0).abs() < 0.1,
|
||||||
|
"all direct children contribute to open details geometry"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn display_inherit_transitively_preserves_contents_and_block_display() {
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"
|
||||||
|
<style>
|
||||||
|
html, body { margin:0 }
|
||||||
|
#row { display:flex; width:200px }
|
||||||
|
#contents { display:contents }
|
||||||
|
.through { display:inherit }
|
||||||
|
.item { display:block; width:40px; height:20px }
|
||||||
|
#block-parent { display:block }
|
||||||
|
#block-child { display:inherit; height:10px }
|
||||||
|
</style>
|
||||||
|
<div id="row">
|
||||||
|
<div id="contents">
|
||||||
|
<div id="inherit-one" class="through">
|
||||||
|
<div id="inherit-two" class="through">
|
||||||
|
<span id="first" class="item"></span>
|
||||||
|
<span id="second" class="item"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<section id="block-parent"><span id="block-child"></span></section>
|
||||||
|
<table><tr><td id="native-cell">
|
||||||
|
<div id="native-inherit" style="display:inherit"></div>
|
||||||
|
</td></tr></table>
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
let layout = layout_dom(&tree, (400.0, 200.0));
|
||||||
|
let id = |name| tree.get_element_by_id(name).unwrap();
|
||||||
|
|
||||||
|
for wrapper in ["contents", "inherit-one", "inherit-two"] {
|
||||||
|
let wrapper = id(wrapper);
|
||||||
|
assert!(layout.styles[&wrapper].display_contents);
|
||||||
|
assert!(
|
||||||
|
!layout.rects.contains_key(&wrapper),
|
||||||
|
"inherited display:contents wrapper must not generate a box"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let first = layout.rects[&id("first")];
|
||||||
|
let second = layout.rects[&id("second")];
|
||||||
|
assert!((first.y - second.y).abs() < 0.1);
|
||||||
|
assert!((second.x - first.x - first.width).abs() < 0.1);
|
||||||
|
|
||||||
|
let block_child = &layout.styles[&id("block-child")];
|
||||||
|
assert_eq!(block_child.display, obscura_render::Display::Block);
|
||||||
|
assert!(!block_child.display_contents);
|
||||||
|
|
||||||
|
let native_cell = &layout.styles[&id("native-cell")];
|
||||||
|
let native_inherit = &layout.styles[&id("native-inherit")];
|
||||||
|
assert!(native_cell.internal_flex_container);
|
||||||
|
assert_eq!(native_inherit.display, obscura_render::Display::Flex);
|
||||||
|
assert!(
|
||||||
|
!native_inherit.internal_flex_container,
|
||||||
|
"display inheritance must not copy the engine's UA-only table approximation"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "paint")]
|
||||||
|
#[test]
|
||||||
|
fn closed_details_content_does_not_paint() {
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"
|
||||||
|
<style>
|
||||||
|
html, body { margin:0; background:white }
|
||||||
|
summary { display:block; width:40px; height:20px; background:#00ff00 }
|
||||||
|
.content { width:40px; height:20px; background:#ff0000 }
|
||||||
|
</style>
|
||||||
|
<details>
|
||||||
|
<summary></summary>
|
||||||
|
<div class="content"></div>
|
||||||
|
</details>
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
let pixmap =
|
||||||
|
obscura_render::paint_dom(&tree, (40.0, 40.0), None).expect("details fixture paints");
|
||||||
|
let summary = pixmap.pixel(5, 5).expect("summary pixel");
|
||||||
|
assert!(
|
||||||
|
summary.green() > 240 && summary.red() < 20,
|
||||||
|
"the first summary must paint"
|
||||||
|
);
|
||||||
|
let hidden = pixmap.pixel(5, 25).expect("closed content pixel");
|
||||||
|
assert!(
|
||||||
|
hidden.red() > 240 && hidden.green() > 240 && hidden.blue() > 240,
|
||||||
|
"closed details content must leave the page background untouched"
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user