Correct ratio-only replaced element sizing
This commit is contained in:
1 parent
99ff0601bf
commit
6d0f884ee0
7 files changed
+316
-13
No files matched your search
@@ -5317,6 +5317,33 @@ fn layout_dom_once(
|
||||
// subtree (nextjs.org's nav stretched to a full 688px, burying the
|
||||
// hero), so it is gone.
|
||||
|
||||
// Capture the definite containing width for ratio-only auto/auto
|
||||
// replaced boxes before installing their metadata. The inline layout
|
||||
// stand-in asks atomic items for max-content before its own percentage
|
||||
// width becomes definite; Gecko instead resolves this case directly
|
||||
// from the containing block's content width.
|
||||
let ratio_only_available_widths: HashMap<NodeId, f32> = intrinsic
|
||||
.iter()
|
||||
.filter_map(|(&nid, metadata)| {
|
||||
let style = styles.get(&nid)?;
|
||||
(metadata.width.is_none()
|
||||
&& metadata.height.is_none()
|
||||
&& metadata.ratio.is_some()
|
||||
&& style.width == crate::Dimension::Auto
|
||||
&& style.height == crate::Dimension::Auto)
|
||||
.then(|| {
|
||||
reliable_ratio_only_available_width(
|
||||
tree,
|
||||
nid,
|
||||
&styles,
|
||||
initial_cb_width,
|
||||
)
|
||||
.map(|width| (nid, width))
|
||||
})
|
||||
.flatten()
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Apply fetched intrinsic image sizes. A replaced element with no
|
||||
// explicit dimensions must size from its intrinsic pixels (else it is
|
||||
// 0x0 and never paints); with one dimension given, the aspect ratio
|
||||
@@ -5329,6 +5356,7 @@ fn layout_dom_once(
|
||||
if let Some(s) = styles.get_mut(&nid) {
|
||||
s.intrinsic_size = metadata.natural_size();
|
||||
s.replaced_intrinsic = Some(metadata);
|
||||
s.ratio_only_available_width = ratio_only_available_widths.get(&nid).copied();
|
||||
if (s.aspect_ratio.is_none() || s.aspect_ratio_is_mapped)
|
||||
&& metadata.ratio.is_some()
|
||||
{
|
||||
@@ -8620,6 +8648,7 @@ fn is_flattenable_inline(
|
||||
};
|
||||
style.display == crate::Display::Inline
|
||||
&& !style.is_inline_block
|
||||
&& !style.is_replaced_box
|
||||
&& style.before_pseudo.is_none()
|
||||
&& style.after_pseudo.is_none()
|
||||
&& style.background_color.is_none()
|
||||
@@ -9219,6 +9248,152 @@ fn reliable_normal_flow_content_width(
|
||||
Some(content_width.max(0.0))
|
||||
}
|
||||
|
||||
/// Resolve an authored definite width even when the box's placement is owned
|
||||
/// by float or positioned layout. A pixel width is independent of placement;
|
||||
/// a percentage is definite only when the containing block can itself be
|
||||
/// resolved without layout. Auto widths and functional size expressions stay
|
||||
/// on the ordinary Taffy path.
|
||||
fn reliable_declared_content_width(
|
||||
tree: &DomTree,
|
||||
id: NodeId,
|
||||
styles: &HashMap<NodeId, crate::LayoutStyle>,
|
||||
initial_cb_width: f32,
|
||||
depth: usize,
|
||||
) -> Option<f32> {
|
||||
if depth > 4096 {
|
||||
return None;
|
||||
}
|
||||
let style = styles.get(&id)?;
|
||||
if style.width_fit_content
|
||||
|| style.size_expressions[0].is_some()
|
||||
|| style.size_expressions[2].is_some()
|
||||
|| style.size_expressions[4].is_some()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let needs_containing_width = matches!(
|
||||
style.width,
|
||||
crate::Dimension::Percent(_)
|
||||
) || matches!(style.min_width, crate::Dimension::Percent(_))
|
||||
|| matches!(style.max_width, crate::Dimension::Percent(_));
|
||||
let containing_width = needs_containing_width.then(|| {
|
||||
let mut parent = tree.get_node(id).and_then(|node| node.parent);
|
||||
while parent.is_some_and(|parent_id| {
|
||||
styles
|
||||
.get(&parent_id)
|
||||
.is_some_and(|parent_style| parent_style.display_contents)
|
||||
}) {
|
||||
parent = parent
|
||||
.and_then(|parent_id| tree.get_node(parent_id).and_then(|node| node.parent));
|
||||
}
|
||||
if let Some(parent_id) = parent {
|
||||
reliable_normal_flow_content_width(
|
||||
tree,
|
||||
parent_id,
|
||||
styles,
|
||||
initial_cb_width,
|
||||
depth + 1,
|
||||
)
|
||||
.or_else(|| {
|
||||
reliable_declared_content_width(
|
||||
tree,
|
||||
parent_id,
|
||||
styles,
|
||||
initial_cb_width,
|
||||
depth + 1,
|
||||
)
|
||||
})
|
||||
} else {
|
||||
Some(initial_cb_width)
|
||||
}
|
||||
});
|
||||
let containing_width = match containing_width {
|
||||
Some(Some(width)) => Some(width),
|
||||
Some(None) => return None,
|
||||
None => None,
|
||||
};
|
||||
|
||||
let horizontal_edges =
|
||||
style.padding.left + style.padding.right + style.border.left + style.border.right;
|
||||
let declared_content = |dimension: crate::Dimension| match dimension {
|
||||
crate::Dimension::Px(value) => Some(if style.box_sizing == crate::BoxSizing::ContentBox {
|
||||
value
|
||||
} else {
|
||||
(value - horizontal_edges).max(0.0)
|
||||
}),
|
||||
crate::Dimension::Percent(percent) => {
|
||||
let value = containing_width? * percent;
|
||||
Some(if style.box_sizing == crate::BoxSizing::ContentBox {
|
||||
value
|
||||
} else {
|
||||
(value - horizontal_edges).max(0.0)
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let mut content_width = declared_content(style.width)?;
|
||||
if let Some(minimum) = declared_content(style.min_width) {
|
||||
content_width = content_width.max(minimum);
|
||||
}
|
||||
if let Some(maximum) = declared_content(style.max_width) {
|
||||
content_width = content_width.min(maximum);
|
||||
}
|
||||
Some(content_width.max(0.0))
|
||||
}
|
||||
|
||||
/// Resolve the containing-block content width used by CSS's ratio-only
|
||||
/// auto/auto replaced sizing branch. Decoration-free inline ancestors do not
|
||||
/// establish containing blocks, so skip them (and display:contents) before
|
||||
/// using the existing conservative ordinary-flow width resolver. A definite
|
||||
/// authored width is also safe for floated or positioned containing boxes;
|
||||
/// their auto-sized allocations still return `None` and remain layout-owned.
|
||||
fn reliable_ratio_only_available_width(
|
||||
tree: &DomTree,
|
||||
id: NodeId,
|
||||
styles: &HashMap<NodeId, crate::LayoutStyle>,
|
||||
initial_cb_width: f32,
|
||||
) -> Option<f32> {
|
||||
let image = styles.get(&id)?;
|
||||
let mut parent = tree.get_node(id).and_then(|node| node.parent);
|
||||
while let Some(parent_id) = parent {
|
||||
let Some(parent_style) = styles.get(&parent_id) else {
|
||||
parent = tree
|
||||
.get_node(parent_id)
|
||||
.and_then(|node| node.parent);
|
||||
continue;
|
||||
};
|
||||
if parent_style.display_contents
|
||||
|| (parent_style.display == crate::Display::Inline
|
||||
&& !parent_style.is_inline_block)
|
||||
{
|
||||
parent = tree
|
||||
.get_node(parent_id)
|
||||
.and_then(|node| node.parent);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
let containing_width = if let Some(parent_id) = parent {
|
||||
reliable_normal_flow_content_width(tree, parent_id, styles, initial_cb_width, 0)
|
||||
.or_else(|| {
|
||||
reliable_declared_content_width(tree, parent_id, styles, initial_cb_width, 0)
|
||||
})?
|
||||
} else {
|
||||
initial_cb_width
|
||||
};
|
||||
let horizontal_edges =
|
||||
image.padding.left + image.padding.right + image.border.left + image.border.right;
|
||||
Some(
|
||||
(containing_width
|
||||
- image.margin.left
|
||||
- image.margin.right
|
||||
- horizontal_edges)
|
||||
.max(0.0),
|
||||
)
|
||||
}
|
||||
|
||||
fn reliable_table_available_width(
|
||||
tree: &DomTree,
|
||||
id: NodeId,
|
||||
|
||||
@@ -745,6 +745,10 @@ struct ReplacedItem {
|
||||
/// CSS replaced sizing stretch-fits this case to a definite available
|
||||
/// inline size instead of using the 300x150 default-object contribution.
|
||||
ratio_only: bool,
|
||||
/// Definite normal-flow containing width captured before Taffy's intrinsic
|
||||
/// flex-item contribution pass. Only populated for ratio-only auto/auto
|
||||
/// replaced boxes; decoded raster dimensions never use it.
|
||||
ratio_only_available_width: Option<f32>,
|
||||
/// CSS Sizing's cyclic-percentage rule makes a proper replaced element's
|
||||
/// inline min-content contribution zero when its preferred or maximum
|
||||
/// inline size contains a percentage. The natural size still participates
|
||||
@@ -801,6 +805,7 @@ impl ReplacedItem {
|
||||
ratio_only: intrinsic.width.is_none()
|
||||
&& intrinsic.height.is_none()
|
||||
&& explicit_ratio.is_some(),
|
||||
ratio_only_available_width: style.ratio_only_available_width,
|
||||
zero_inline_min_content: matches!(style.width, Dimension::Percent(_))
|
||||
|| matches!(style.max_width, Dimension::Percent(_))
|
||||
|| expression_has_percentage(0)
|
||||
@@ -1541,9 +1546,11 @@ impl TextEngine {
|
||||
&& replaced.preferred_width.is_none()
|
||||
&& replaced.preferred_height.is_none()
|
||||
&& known.width.is_none())
|
||||
.then(|| match available.width {
|
||||
taffy::AvailableSpace::Definite(width) => Some(width.max(0.0)),
|
||||
_ => None,
|
||||
.then(|| {
|
||||
match available.width {
|
||||
taffy::AvailableSpace::Definite(width) => Some(width.max(0.0)),
|
||||
_ => replaced.ratio_only_available_width,
|
||||
}
|
||||
})
|
||||
.flatten();
|
||||
let mut size = replaced.size(taffy::Size {
|
||||
|
||||
@@ -861,6 +861,12 @@ pub struct LayoutStyle {
|
||||
/// SVG can expose only one dimension or a `viewBox` ratio, distinctions
|
||||
/// that the stable public `intrinsic_size` tuple cannot represent.
|
||||
pub(crate) replaced_intrinsic: Option<ReplacedIntrinsic>,
|
||||
/// Definite content-box width available to an auto/auto ratio-only
|
||||
/// replaced element in ordinary block flow. Taffy's flex-row stand-in for
|
||||
/// an inline formatting context asks atomic children for max-content first,
|
||||
/// so its measure callback otherwise never sees the definite line width
|
||||
/// that CSS replaced sizing uses for this special case.
|
||||
pub(crate) ratio_only_available_width: Option<f32>,
|
||||
/// The current ratio came from HTML width/height presentation hints
|
||||
/// (`<img>` or its selected `<picture><source>`), rather than authored
|
||||
/// CSS. A decoded image's natural ratio replaces this provisional ratio.
|
||||
|
||||
@@ -1146,6 +1146,7 @@ impl PreparedRender {
|
||||
(crate::Display::Block, true) => "inline-block",
|
||||
(crate::Display::Flex, false) => "flex",
|
||||
(crate::Display::Grid, false) => "grid",
|
||||
(crate::Display::Inline, true) => "inline-block",
|
||||
(crate::Display::Inline, false) => "inline",
|
||||
_ => "block",
|
||||
}
|
||||
@@ -9701,6 +9702,83 @@ mod tests {
|
||||
assert_eq!((explicit.width, explicit.height), (120.0, 80.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ratio_only_inline_images_stretch_fit_the_definite_line_width() {
|
||||
let source = "data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20576%20576'%3E%3Crect%20width='576'%20height='576'%20fill='%23ff80ab'/%3E%3C/svg%3E";
|
||||
let raster = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAADCAYAAAC56t6BAAAAFklEQVR4nGP8z8Dwn4GBgYGJAQrgDAAxOwIE7x6DkQAAAABJRU5ErkJggg==";
|
||||
let tree = parse_html(&format!(
|
||||
r#"<html><head><style>
|
||||
html,body {{ margin:0 }}
|
||||
.case {{ width:370px; line-height:0 }}
|
||||
#authored-inline {{ display:inline }}
|
||||
#authored-block {{ display:block }}
|
||||
#authored-inline-block {{ display:inline-block }}
|
||||
#positioned {{ position:absolute; left:0; top:1900px; width:340px;
|
||||
min-width:360px; max-width:380px; box-sizing:border-box;
|
||||
padding:10px; border:10px solid transparent }}
|
||||
#float-row {{ width:500px }}
|
||||
#float-column {{ float:left; width:40%; box-sizing:border-box;
|
||||
padding-left:10px; padding-right:10px }}
|
||||
</style></head><body>
|
||||
<div class="case"><img id="direct" src="{source}"></div>
|
||||
<div class="case"><a><img id="anchored" src="{source}"></a></div>
|
||||
<div class="case"><img id="authored-inline" src="{source}"></div>
|
||||
<div class="case"><img id="authored-block" src="{source}"></div>
|
||||
<div class="case"><img id="authored-inline-block" src="{source}"></div>
|
||||
<div class="case"><img id="intrinsic-raster" src="{raster}"></div>
|
||||
<div id="positioned"><img id="positioned-image" src="{source}"></div>
|
||||
<div id="float-row"><div id="float-column"><img id="float-image" src="{source}"></div></div>
|
||||
</body></html>"#
|
||||
));
|
||||
let mut resources = RenderResourceCache::default();
|
||||
let prepared =
|
||||
prepare_dom(&tree, (500.0, 2400.0), None, &mut resources).expect("image matrix");
|
||||
let node = |selector| tree.query_selector(selector).unwrap().unwrap();
|
||||
let rect = |selector| prepared.layout.rects[&node(selector)];
|
||||
|
||||
for selector in [
|
||||
"#direct",
|
||||
"#anchored",
|
||||
"#authored-inline",
|
||||
"#authored-block",
|
||||
"#authored-inline-block",
|
||||
] {
|
||||
let image = rect(selector);
|
||||
assert_eq!(
|
||||
(image.width, image.height),
|
||||
(370.0, 370.0),
|
||||
"ratio-only auto/auto sizing for {selector}: {image:?}"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
(rect("#intrinsic-raster").width, rect("#intrinsic-raster").height),
|
||||
(2.0, 3.0),
|
||||
"a decoded image with real intrinsic axes must not stretch-fit"
|
||||
);
|
||||
assert_eq!(
|
||||
(rect("#positioned-image").width, rect("#positioned-image").height),
|
||||
(320.0, 320.0),
|
||||
"a definite positioned border-box honors edges and min/max widths"
|
||||
);
|
||||
assert_eq!(
|
||||
(rect("#float-image").width, rect("#float-image").height),
|
||||
(180.0, 180.0),
|
||||
"a floated percentage column resolves against its reliable containing width"
|
||||
);
|
||||
|
||||
let display = |selector| {
|
||||
prepared
|
||||
.computed_style(node(selector))
|
||||
.unwrap()["display"]
|
||||
.clone()
|
||||
};
|
||||
assert_eq!(display("#direct"), "inline");
|
||||
assert_eq!(display("#anchored"), "inline");
|
||||
assert_eq!(display("#authored-inline"), "inline");
|
||||
assert_eq!(display("#authored-block"), "block");
|
||||
assert_eq!(display("#authored-inline-block"), "inline-block");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_placeholders_honor_default_author_color_opacity_and_value_state() {
|
||||
let tree = parse_html(
|
||||
|
||||
@@ -287,15 +287,14 @@ pub fn ua_style(tag: &str) -> LayoutStyle {
|
||||
style.font_weight = Some("bold".to_string());
|
||||
}
|
||||
} else if tag == "img" {
|
||||
// Images are inline-level replaced elements by default. Model the
|
||||
// atomic inner box with the same inline-block marker used by native
|
||||
// controls, so an icon between text fragments participates in their
|
||||
// line instead of splitting the parent into block runs. Chromium's UA
|
||||
// sheet does not impose a responsive max-width: author/reset CSS must
|
||||
// opt into that. A synthetic `max-width:100%` here distorts deliberately
|
||||
// oversized art whose height and intrinsic ratio establish its width.
|
||||
// Images are inline-level replaced elements by default. Replacement
|
||||
// already makes the box atomic; `is_inline_block` is reserved for an
|
||||
// authored inline-block outer display and native controls. Chromium's
|
||||
// UA sheet does not impose a responsive max-width: author/reset CSS
|
||||
// must 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.is_inline_block = true;
|
||||
}
|
||||
style
|
||||
}
|
||||
@@ -8934,7 +8933,7 @@ mod tests {
|
||||
|
||||
let native_image = compute_style("img", None);
|
||||
assert_eq!(native_image.display, Display::Inline);
|
||||
assert!(native_image.is_inline_block);
|
||||
assert!(!native_image.is_inline_block);
|
||||
|
||||
let block_image = compute_style("img", Some("display:block"));
|
||||
assert_eq!(block_image.display, Display::Block);
|
||||
@@ -8990,7 +8989,7 @@ mod tests {
|
||||
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!(!image.is_inline_block);
|
||||
assert_eq!(image.max_width, crate::Dimension::Auto);
|
||||
|
||||
let authored = compute_style("img", Some("max-width:100%"));
|
||||
|
||||
Reference in new issue
Block a user