Resolve percentage geometry in flex and SVG layout
This commit is contained in:
1 parent
640839f360
commit
12d90423cf
2 files changed
+139
-18
No files matched your search
@@ -10693,11 +10693,18 @@ fn apply_container_size_containment(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone)]
|
||||||
struct DeferredCyclicInlineSize {
|
struct DeferredCyclicInlineSize {
|
||||||
node: NodeId,
|
node: NodeId,
|
||||||
flex_item: NodeId,
|
flex_item: NodeId,
|
||||||
slot: usize,
|
slot: usize,
|
||||||
|
source: DeferredCyclicInlineSource,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
enum DeferredCyclicInlineSource {
|
||||||
|
Expression(String),
|
||||||
|
Percent(f32),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// During a flex item's intrinsic-size calculation, percentages in descendant
|
/// During a flex item's intrinsic-size calculation, percentages in descendant
|
||||||
@@ -10717,22 +10724,36 @@ fn defer_cyclic_flex_inline_sizes(
|
|||||||
vw: f32,
|
vw: f32,
|
||||||
vh: f32,
|
vh: f32,
|
||||||
) -> Vec<DeferredCyclicInlineSize> {
|
) -> Vec<DeferredCyclicInlineSize> {
|
||||||
let candidates: Vec<(NodeId, usize, String)> = styles
|
let candidates: Vec<(NodeId, usize, DeferredCyclicInlineSource)> = styles
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(_, style)| !style.ignores_used_box_sizes())
|
.filter(|(_, style)| !style.ignores_used_box_sizes())
|
||||||
.flat_map(|(&id, style)| {
|
.flat_map(|(&id, style)| {
|
||||||
[0usize, 2, 4].into_iter().filter_map(move |slot| {
|
[0usize, 2, 4].into_iter().filter_map(move |slot| {
|
||||||
style.size_expressions[slot]
|
let functional = style.size_expressions[slot]
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.filter(|expression| expression.contains('%'))
|
.filter(|expression| expression.contains('%'))
|
||||||
.cloned()
|
.cloned()
|
||||||
.map(|expression| (id, slot, expression))
|
.map(DeferredCyclicInlineSource::Expression);
|
||||||
|
let dimension = match slot {
|
||||||
|
0 => style.width,
|
||||||
|
2 => style.min_width,
|
||||||
|
4 => style.max_width,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
functional
|
||||||
|
.or_else(|| match dimension {
|
||||||
|
crate::Dimension::Percent(percent) => {
|
||||||
|
Some(DeferredCyclicInlineSource::Percent(percent))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.map(|source| (id, slot, source))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let mut deferred = Vec::new();
|
let mut deferred = Vec::new();
|
||||||
|
|
||||||
for (id, slot, expression) in candidates {
|
for (id, slot, source) in candidates {
|
||||||
// Start at the expression's containing-box chain, not the sized
|
// Start at the expression's containing-box chain, not the sized
|
||||||
// node itself. A percentage-sized direct child of a nested flex
|
// node itself. A percentage-sized direct child of a nested flex
|
||||||
// container makes that container's contribution cyclic when the
|
// container makes that container's contribution cyclic when the
|
||||||
@@ -10781,8 +10802,13 @@ fn defer_cyclic_flex_inline_sizes(
|
|||||||
.get(&id)
|
.get(&id)
|
||||||
.and_then(|style| style.font_size)
|
.and_then(|style| style.font_size)
|
||||||
.unwrap_or(root_fs);
|
.unwrap_or(root_fs);
|
||||||
let Some(intrinsic) =
|
let intrinsic = match &source {
|
||||||
crate::style::resolve_contextual_length(&expression, em, root_fs, vw, vh, 0.0)
|
DeferredCyclicInlineSource::Expression(expression) => {
|
||||||
|
crate::style::resolve_contextual_length(expression, em, root_fs, vw, vh, 0.0)
|
||||||
|
}
|
||||||
|
DeferredCyclicInlineSource::Percent(_) => Some(0.0),
|
||||||
|
};
|
||||||
|
let Some(intrinsic) = intrinsic
|
||||||
else {
|
else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
@@ -10802,6 +10828,7 @@ fn defer_cyclic_flex_inline_sizes(
|
|||||||
node: id,
|
node: id,
|
||||||
flex_item,
|
flex_item,
|
||||||
slot,
|
slot,
|
||||||
|
source,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -10852,12 +10879,6 @@ fn resolve_deferred_flex_inline_sizes(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for entry in deferred {
|
for entry in deferred {
|
||||||
let Some(expression) = styles
|
|
||||||
.get(&entry.node)
|
|
||||||
.and_then(|style| style.size_expressions[entry.slot].clone())
|
|
||||||
else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let mut containing = tree.get_node(entry.node).and_then(|node| node.parent);
|
let mut containing = tree.get_node(entry.node).and_then(|node| node.parent);
|
||||||
let basis = loop {
|
let basis = loop {
|
||||||
let Some(parent) = containing else {
|
let Some(parent) = containing else {
|
||||||
@@ -10887,8 +10908,20 @@ fn resolve_deferred_flex_inline_sizes(
|
|||||||
.get(&entry.node)
|
.get(&entry.node)
|
||||||
.and_then(|style| style.font_size)
|
.and_then(|style| style.font_size)
|
||||||
.unwrap_or(root_fs);
|
.unwrap_or(root_fs);
|
||||||
let Some(value) =
|
let value = match &entry.source {
|
||||||
crate::style::resolve_contextual_length(&expression, em, root_fs, vw, vh, basis)
|
DeferredCyclicInlineSource::Expression(expression) => {
|
||||||
|
crate::style::resolve_contextual_length(
|
||||||
|
expression,
|
||||||
|
em,
|
||||||
|
root_fs,
|
||||||
|
vw,
|
||||||
|
vh,
|
||||||
|
basis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DeferredCyclicInlineSource::Percent(percent) => Some(basis * percent),
|
||||||
|
};
|
||||||
|
let Some(value) = value
|
||||||
else {
|
else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
@@ -11080,14 +11113,18 @@ fn build(
|
|||||||
.aspect_ratio
|
.aspect_ratio
|
||||||
.filter(|ratio| ratio.is_finite() && *ratio > 0.0)
|
.filter(|ratio| ratio.is_finite() && *ratio > 0.0)
|
||||||
{
|
{
|
||||||
// A percentage inline size inside an auto grid track is cyclic
|
// A 100% inline size inside an auto grid track is cyclic
|
||||||
// during intrinsic sizing: it behaves as auto for the contribution
|
// during intrinsic sizing: it behaves as auto for the contribution
|
||||||
// pass, then fills the final track. Taffy 0.12 keeps the intrinsic
|
// pass, then fills the final track. Taffy 0.12 keeps the intrinsic
|
||||||
// fallback width instead of revisiting that percentage. Express
|
// fallback width instead of revisiting that percentage. Express
|
||||||
// the final fill as grid self-stretch so the known content width
|
// the final fill as grid self-stretch so the known content width
|
||||||
// reaches the replaced-item measure callback.
|
// reaches the replaced-item measure callback. This substitution is
|
||||||
if matches!(style.width, crate::Dimension::Percent(_))
|
// only equivalent for an in-flow grid item at exactly 100%; an
|
||||||
|
// abs-pos SVG or a smaller percentage must keep its containing-
|
||||||
|
// block-relative authored size.
|
||||||
|
if matches!(style.width, crate::Dimension::Percent(percent) if percent == 1.0)
|
||||||
&& matches!(style.height, crate::Dimension::Auto)
|
&& matches!(style.height, crate::Dimension::Auto)
|
||||||
|
&& is_in_flow_grid_item(tree, id, style, styles)
|
||||||
{
|
{
|
||||||
taffy_style.size.width = taffy::Dimension::auto();
|
taffy_style.size.width = taffy::Dimension::auto();
|
||||||
taffy_style.min_size.width = taffy::Dimension::length(0.0);
|
taffy_style.min_size.width = taffy::Dimension::length(0.0);
|
||||||
|
|||||||
@@ -1661,6 +1661,31 @@ fn ratio_only_inline_svg_sizes_inside_an_auto_grid_row() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A percentage-sized absolutely positioned SVG resolves against its
|
||||||
|
/// containing block like any other replaced element. The special intrinsic
|
||||||
|
/// sizing used for a ratio-only SVG in an auto grid track must not turn this
|
||||||
|
/// authored percentage into grid-style self-stretch (or the 300px default
|
||||||
|
/// object size wins instead).
|
||||||
|
#[test]
|
||||||
|
fn absolute_inline_svg_keeps_percentage_width_and_view_box_ratio() {
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"<html><head><style>
|
||||||
|
html,body{margin:0}
|
||||||
|
#container{position:relative;width:800px;height:300px}
|
||||||
|
#art{display:block;position:absolute;right:0;top:0;width:20%;height:auto}
|
||||||
|
</style></head><body>
|
||||||
|
<div id="container">
|
||||||
|
<svg id="art" width="162" height="162" viewBox="0 0 162 162"></svg>
|
||||||
|
</div>
|
||||||
|
</body></html>"#,
|
||||||
|
);
|
||||||
|
let layout = layout_dom(&tree, (1000.0, 600.0));
|
||||||
|
let art = layout.rects[&tree.get_element_by_id("art").unwrap()];
|
||||||
|
assert!((art.x - 640.0).abs() < 0.01, "percentage width must anchor from the right: {art:?}");
|
||||||
|
assert!((art.width - 160.0).abs() < 0.01, "percentage width: {art:?}");
|
||||||
|
assert!((art.height - 160.0).abs() < 0.01, "viewBox ratio: {art:?}");
|
||||||
|
}
|
||||||
|
|
||||||
/// Chromium 150 includes collapsed descendant margins in a grid item's
|
/// Chromium 150 includes collapsed descendant margins in a grid item's
|
||||||
/// intrinsic block-size. The nested 64px margin reaches through `section`,
|
/// intrinsic block-size. The nested 64px margin reaches through `section`,
|
||||||
/// making the first item 200px tall; the second row therefore begins after
|
/// making the first item 200px tall; the second row therefore begins after
|
||||||
@@ -3385,6 +3410,65 @@ fn cyclic_descendant_percentages_do_not_inflate_a_flex_items_intrinsic_minimum()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Chromium resolves both spellings to the same 163px content width: the
|
||||||
|
/// percentage is cyclic while the link's flex-item width is being measured,
|
||||||
|
/// then resolves against that link's final width. Carbon Ads uses the bare
|
||||||
|
/// spelling; treating its 500px image as a permanent intrinsic floor widened
|
||||||
|
/// Bootstrap's 195px sidebar card to 532px.
|
||||||
|
#[test]
|
||||||
|
fn bare_and_functional_cyclic_image_percentages_use_the_final_flex_width() {
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"
|
||||||
|
<style>
|
||||||
|
* { box-sizing:border-box }
|
||||||
|
html, body { margin:0 }
|
||||||
|
.stack { display:flex; flex-flow:column wrap; width:195px }
|
||||||
|
.wrap { display:flex; flex-wrap:wrap; padding:16px }
|
||||||
|
.link { display:block; position:relative; flex:1 0 100%; overflow:hidden }
|
||||||
|
.image { display:block; height:auto }
|
||||||
|
#bare-image { width:100% }
|
||||||
|
#functional-image { width:calc(100%) }
|
||||||
|
</style>
|
||||||
|
<div class="stack"><div id="bare-wrap" class="wrap">
|
||||||
|
<a id="bare-link" class="link">
|
||||||
|
<img id="bare-image" class="image" width="100" height="100">
|
||||||
|
</a>
|
||||||
|
</div></div>
|
||||||
|
<div class="stack"><div id="functional-wrap" class="wrap">
|
||||||
|
<a id="functional-link" class="link">
|
||||||
|
<img id="functional-image" class="image" width="100" height="100">
|
||||||
|
</a>
|
||||||
|
</div></div>
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
let bare_image = tree.get_element_by_id("bare-image").unwrap();
|
||||||
|
let functional_image = tree.get_element_by_id("functional-image").unwrap();
|
||||||
|
let intrinsic = HashMap::from([
|
||||||
|
(bare_image, (500.0, 500.0)),
|
||||||
|
(functional_image, (500.0, 500.0)),
|
||||||
|
]);
|
||||||
|
let layout = layout_dom_with_images(&tree, (1280.0, 720.0), &intrinsic);
|
||||||
|
let rect = |id| layout.rects[&tree.get_element_by_id(id).unwrap()];
|
||||||
|
|
||||||
|
for (prefix, wrap_id, link_id, image_id) in [
|
||||||
|
("bare", "bare-wrap", "bare-link", "bare-image"),
|
||||||
|
(
|
||||||
|
"functional",
|
||||||
|
"functional-wrap",
|
||||||
|
"functional-link",
|
||||||
|
"functional-image",
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
let wrap = rect(wrap_id);
|
||||||
|
let link = rect(link_id);
|
||||||
|
let image = rect(image_id);
|
||||||
|
assert_eq!(wrap.width, 195.0, "{prefix} percentage wrapper: {wrap:?}");
|
||||||
|
assert_eq!(link.width, 163.0, "{prefix} percentage link: {link:?}");
|
||||||
|
assert_eq!(image.width, 163.0, "{prefix} percentage image: {image:?}");
|
||||||
|
assert_eq!(image.height, 163.0, "{prefix} image ratio: {image:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The cyclic percentage affects only the min-content contribution. With
|
/// The cyclic percentage affects only the min-content contribution. With
|
||||||
/// positive free space, the same boxes retain their natural max-content bases.
|
/// positive free space, the same boxes retain their natural max-content bases.
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in new issue
Block a user