fix(layout): preserve grid auto sizing before aspect ratio transfer

Keep normal distinct from auto at the style boundary and consume the shared Taffy grid sizing contract. Cover alignment, limits, intrinsic sizes, margins, writing modes, and actual child formatting contexts with 288 screenshot geometry checks.
This commit is contained in:
ldm0
2026-09-10 00:12:57 +08:00
parent 82e198d264
commit d49d43a4a4
6 changed files with 138 additions and 11 deletions
Generated
+1 -1
View File
@@ -4842,7 +4842,7 @@ dependencies = [
[[package]]
name = "taffy"
version = "0.13.0"
source = "git+https://github.com/ldm0/taffy?rev=b5a55e2b3b7aa3b71ae9f8dfb124fb018c4a2cf2#b5a55e2b3b7aa3b71ae9f8dfb124fb018c4a2cf2"
source = "git+https://github.com/ldm0/taffy?rev=7d52b6d0d1e33497df83940b6d6ca33f09c947ff#7d52b6d0d1e33497df83940b6d6ca33f09c947ff"
dependencies = [
"arrayvec",
"serde",
+1 -1
View File
@@ -102,7 +102,7 @@ stylo_atoms = { git = "https://github.com/ldm0/stylo", rev = "cfa170da90c467e937
stylo_dom = { git = "https://github.com/ldm0/stylo", rev = "cfa170da90c467e93786ec0fa5526bd83f201933" }
stylo_static_prefs = { git = "https://github.com/ldm0/stylo", rev = "cfa170da90c467e93786ec0fa5526bd83f201933" }
stylo_traits = { git = "https://github.com/ldm0/stylo", rev = "cfa170da90c467e93786ec0fa5526bd83f201933" }
taffy = { git = "https://github.com/ldm0/taffy", rev = "b5a55e2b3b7aa3b71ae9f8dfb124fb018c4a2cf2" }
taffy = { git = "https://github.com/ldm0/taffy", rev = "7d52b6d0d1e33497df83940b6d6ca33f09c947ff" }
[workspace.lints.clippy]
disallowed_methods = "deny"
+9 -3
View File
@@ -233,10 +233,15 @@ impl FlexCrossAxisStaticContext {
}
}
AlignItemsKeyword::FlexEnd if !wrap_reverse => LogicalStaticEdge::End,
AlignItemsKeyword::FlexStart | AlignItemsKeyword::Stretch if wrap_reverse => {
AlignItemsKeyword::Normal
| AlignItemsKeyword::FlexStart
| AlignItemsKeyword::Stretch
if wrap_reverse =>
{
LogicalStaticEdge::End
}
AlignItemsKeyword::Start
AlignItemsKeyword::Normal
| AlignItemsKeyword::Start
| AlignItemsKeyword::FlexStart
| AlignItemsKeyword::FlexEnd
| AlignItemsKeyword::Baseline
@@ -286,7 +291,8 @@ pub(crate) fn grid_static_alignment(
LogicalStaticEdge::End
}
}
AlignItemsKeyword::Start
AlignItemsKeyword::Normal
| AlignItemsKeyword::Start
| AlignItemsKeyword::FlexStart
| AlignItemsKeyword::Baseline
| AlignItemsKeyword::Stretch => LogicalStaticEdge::Start,
+10 -6
View File
@@ -2650,16 +2650,18 @@ fn stylo_vertical_align(
)
}
/// Preserve the baseline preference at the style boundary. The pinned Blitz
/// converter predates Taffy's last-baseline support and lowers it to `end`.
/// Preserve normal versus auto and both baseline preferences at the style
/// boundary. The pinned Blitz converter predates these Taffy distinctions.
/// Both normal-flow and positioned alignment must retain the same CSS value.
pub(crate) fn taffy_item_alignment(
flags: style::values::specified::align::AlignFlags,
) -> Option<taffy::AlignItems> {
if flags.value() == style::values::specified::align::AlignFlags::LAST_BASELINE {
Some(taffy::AlignItems::LAST_BASELINE)
} else {
stylo_taffy::convert::item_alignment(flags)
match flags.value() {
style::values::specified::align::AlignFlags::NORMAL => Some(taffy::AlignItems::NORMAL),
style::values::specified::align::AlignFlags::LAST_BASELINE => {
Some(taffy::AlignItems::LAST_BASELINE)
}
_ => stylo_taffy::convert::item_alignment(flags),
}
}
@@ -2681,7 +2683,9 @@ mod alignment_tests {
#[test]
fn item_alignment_keeps_baseline_preference_and_positional_safety_distinct() {
assert_eq!(taffy_item_alignment(AlignFlags::AUTO), None);
for (flags, expected) in [
(AlignFlags::NORMAL, AlignItems::NORMAL),
(AlignFlags::BASELINE, AlignItems::BASELINE),
(AlignFlags::LAST_BASELINE, AlignItems::LAST_BASELINE),
(AlignFlags::END, AlignItems::END),
@@ -1,5 +1,44 @@
use super::*;
#[tokio::test(flavor = "current_thread")]
async fn screenshot_resolves_grid_stretch_before_ratio_dependent_sizes() {
run_page_vm_async_test(async move {
let loader = crate::network::ResourceRequestClient::new(&FetchConfig::default())?;
let mut page = test_page_vm_with_loader_and_document_url(
&loader,
Vec::new(),
Url::parse("https://example.com/grid-item-sizing.html")?,
);
page.vm_mut()
.set_layout_policy(moli_page_types::LayoutPolicy::OnDemand);
let fixture = include_str!("../../../../tests/fixtures/grid-item-sizing.html");
page.vm_mut().eval(&format!(
"document.open();document.write({});document.close()",
serde_json::to_string(fixture)?,
))?;
page.vm_mut()
.prime_document_lifecycle_processing_and_record_stylesheet_network_results();
page.vm_mut()
.screenshot_layout_snapshot(moli_layout::PaintViewport::new(1500, 1800, 1.0))?
.expect("grid sizing fixture must retain a layout root");
let checks: serde_json::Value = serde_json::from_str(
&page
.vm_mut()
.eval("JSON.stringify(collectGridItemSizingChecks())")?,
)?;
let checks = checks.as_array().expect("grid sizing checks");
assert_eq!(checks.len(), 288);
let failures: Vec<_> = checks
.iter()
.filter(|check| check["actual"] != check["expected"])
.collect();
assert!(failures.is_empty(), "grid sizing mismatches: {failures:#?}");
Ok::<_, anyhow::Error>(())
})
.await
.expect("grid automatic sizing fixture should run");
}
/// WPT abspos-005/012/013 with authored stretch, limits and percentage-basis
/// controls across all formatting owners, including inline containing blocks.
#[tokio::test(flavor = "current_thread")]
+78
View File
@@ -0,0 +1,78 @@
<!doctype html>
<meta charset="utf-8">
<title>Grid auto sizing preserves alignment and ratio constraint sources</title>
<link rel="help" href="https://drafts.csswg.org/css-grid-2/#grid-item-sizing">
<style>
html, body { margin:0; padding:0; }
html { overflow:hidden; }
.grid { position:absolute; display:grid; grid-template:300px / 240px; font:0/0 sans-serif; }
.item { background:green; aspect-ratio:2; }
</style>
<body>
<script>
// Expected border-box dimensions in the child's logical axes; no fonts,
// network resources or measured reference boxes participate in expectations.
const cases = [
['normal', '', '', [240,120]],
['explicit-both', '', 'place-self:stretch', [240,300]],
['explicit-inline', '', 'justify-self:stretch', [240,120]],
['explicit-block', '', 'align-self:stretch', [600,300]],
['specified-inline-normal', '', 'inline-size:100px', [100,50]],
['specified-inline-stretch', '', 'inline-size:100px;align-self:stretch', [100,300]],
['specified-block-normal', '', 'block-size:100px', [200,100]],
['specified-block-stretch', '', 'block-size:100px;justify-self:stretch', [240,100]],
['parent-stretch', 'place-items:stretch', '', [240,300]],
['auto-inherits', 'place-items:stretch', 'place-self:auto', [240,300]],
['normal-overrides-parent', 'place-items:stretch', 'place-self:normal', [240,120]],
['normal-block-overrides-parent', 'place-items:stretch', 'align-self:normal', [240,120]],
['auto-inline-margins', '', 'place-self:stretch;block-size:100px;margin-inline:auto', [200,100]],
['auto-block-margins', '', 'place-self:stretch;inline-size:100px;margin-block:auto', [100,50]],
['asymmetric-margins', '', 'place-self:stretch;margin-inline:10px 30px;margin-block:5px 15px', [200,280]],
['negative-margins', '', 'place-self:stretch;margin-inline:-10px -30px;margin-block:-5px -15px', [280,320]],
['content-box-edges', '', 'inline-size:100px;padding:5px;border:2px solid black', [114,64]],
['stretched-content-box-edges', '', 'inline-size:100px;align-self:stretch;padding:5px;border:2px solid black', [114,300]],
['max-block-stretch', '', 'place-self:stretch;max-block-size:75px', [240,75]],
['min-block-stretch', '', 'place-self:stretch;min-block-size:400px', [240,400]],
['max-inline-stretch', '', 'place-self:stretch;max-inline-size:100px', [100,300]],
['min-inline-stretch', '', 'place-self:stretch;min-inline-size:400px', [400,300]],
['intrinsic-inline-stretch', '', 'inline-size:max-content;align-self:stretch', [600,300]],
['intrinsic-block-stretch', '', 'block-size:max-content;justify-self:stretch', [240,120]],
['normal-content-small', '', '', [240,120], [80,20]],
['normal-content-large', '', '', [240,180], [80,180]],
['stretched-content-overflow', '', 'place-self:stretch', [240,300], [80,500]],
['start-content', '', 'place-self:start', [80,40], [80,20]],
['center-content', '', 'place-self:center', [80,40], [80,20]],
['auto-margins-content', '', 'margin:auto', [80,40], [80,20]],
['intrinsic-inline-content', '', 'inline-size:max-content;block-size:100px', [300,100], [300,20]],
['intrinsic-block-content', '', 'inline-size:100px;block-size:max-content', [100,180], [80,180]],
];
let index = 0;
for (const mode of ['horizontal-tb','vertical-rl','vertical-lr']) {
for (const display of ['block','flex','grid']) {
for (const [name, parentStyle, style, expected, contentSize] of cases) {
const grid = document.createElement('div');
grid.className = 'grid';
grid.style.cssText = `left:${index%3*500}px;top:${Math.floor(index/3)*450}px;writing-mode:${mode};${parentStyle}`;
const item = document.createElement('div');
item.className = 'item';
item.id = `${mode}-${display}-${name}`;
item.style.cssText = `display:${display};aspect-ratio:${mode === 'horizontal-tb' ? '2' : '1/2'};${style}`;
item.dataset.expected = JSON.stringify(mode === 'horizontal-tb' ? expected : [expected[1],expected[0]]);
if (contentSize) {
const content = document.createElement('div');
content.style.cssText = `inline-size:${contentSize[0]}px;block-size:${contentSize[1]}px`;
item.append(content);
}
grid.append(item);
document.body.append(grid);
index++;
}
}
}
function collectGridItemSizingChecks() {
return Array.from(document.querySelectorAll('.item'), item => {
const rect = item.getBoundingClientRect();
return {id:item.id, actual:[rect.width,rect.height], expected:JSON.parse(item.dataset.expected)};
});
}
</script>