From d66bc2167bc48a42e99a7b14b44213f6f4c21ea6 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Fri, 18 Sep 2026 15:09:07 +0800 Subject: [PATCH] Fix table cell baselines and float vertical alignment --- Cargo.lock | 2 +- Cargo.toml | 2 +- moli-layout/src/table/block.rs | 43 ++------ moli-layout/src/taffy_tree.rs | 15 +++ moli-layout/src/taffy_tree/measurement.rs | 4 + .../runtime/page_vm/tests/rendering_update.rs | 99 +++++++++++++++++++ 6 files changed, 126 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4b045247c8..35c781c3a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5155,7 +5155,7 @@ dependencies = [ [[package]] name = "taffy" version = "0.13.0" -source = "git+https://github.com/ldm0/taffy?rev=acc16f5b0d570656c504a44b52f1736f05991a40#acc16f5b0d570656c504a44b52f1736f05991a40" +source = "git+https://github.com/ldm0/taffy?rev=645bf5a49df607a8bff34ec90c5343901d04f10e#645bf5a49df607a8bff34ec90c5343901d04f10e" dependencies = [ "arrayvec", "serde", diff --git a/Cargo.toml b/Cargo.toml index a6860a40ed..b9d0916850 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -106,7 +106,7 @@ stylo_atoms = { git = "https://github.com/ldm0/stylo", rev = "671d13d31b3b00e7b1 stylo_dom = { git = "https://github.com/ldm0/stylo", rev = "671d13d31b3b00e7b1e0088ba2b36a4e35c0858c" } stylo_static_prefs = { git = "https://github.com/ldm0/stylo", rev = "671d13d31b3b00e7b1e0088ba2b36a4e35c0858c" } stylo_traits = { git = "https://github.com/ldm0/stylo", rev = "671d13d31b3b00e7b1e0088ba2b36a4e35c0858c" } -taffy = { git = "https://github.com/ldm0/taffy", rev = "acc16f5b0d570656c504a44b52f1736f05991a40" } +taffy = { git = "https://github.com/ldm0/taffy", rev = "645bf5a49df607a8bff34ec90c5343901d04f10e" } [workspace.lints.clippy] disallowed_methods = "deny" diff --git a/moli-layout/src/table/block.rs b/moli-layout/src/table/block.rs index 85851e8e00..1073768cdf 100644 --- a/moli-layout/src/table/block.rs +++ b/moli-layout/src/table/block.rs @@ -241,7 +241,7 @@ impl TableContext { let natural = mode.to_logical(output.size).block_size; let cell = &mut self.cells[index]; let baseline = if mode.is_horizontal() { - output.first_baselines.y + output.first_baselines.y.or(output.block_content_end) } else { output.first_baselines.x }; @@ -532,44 +532,13 @@ where world.compute_child_layout(cell.to_taffy(), inputs) }; world.table_cell_percentage_height = previous; - let free = (layout.size - mode.to_logical(output.size).block_size).max(0.0); - let alignment = world.boxes[cell.index()].style.taffy.align_content; - let offset = if layout.baseline.is_some() { - 0.0 - } else { - crate::taffy_tree::single_subject_block_alignment_offset(alignment, free) - }; - if inputs.run_mode == RunMode::PerformLayout && offset != 0.0 { - shift_cell_contents(world, cell, offset); - } - if let Some(baseline) = &mut output.first_baselines.y { - *baseline += offset; - } - if let Some(baseline) = &mut output.last_baselines.y { - *baseline += offset; - } if layout.baseline.is_some() && output.first_baselines.y.is_none() { - // Cells with no line baseline use the content's block-end edge. This - // must be sampled again after percentage descendants have laid out. - let padding = world.boxes[cell.index()] - .style - .taffy - .padding - .resolve_or_zero(inputs.parent_size.width, resolve_stylo_calc_value); - output.first_baselines.y = Some(if !layout.definite { - let border = world.boxes[cell.index()] - .style - .taffy - .border - .resolve_or_zero(inputs.parent_size.width, resolve_stylo_calc_value); - // The natural border box includes empty block children whose - // overflow content size can be zero (for example an empty group). - (layout.natural_size - padding.bottom - border.bottom).max(0.0) - } else { - (output.content_size.height - padding.bottom).max(0.0) - }); + // This is measured again with final percentage constraints, but before + // relative positioning or absolute descendants contribute overflow. + output.first_baselines.y = output.block_content_end; } - output.content_size.height += offset; + // Block/inline layout has already aligned the content group inside this + // final geometry. Only baseline alignment needs a later table-wide shift. set_block(mode, &mut output.size, layout.size); output } diff --git a/moli-layout/src/taffy_tree.rs b/moli-layout/src/taffy_tree.rs index 3740112246..cf15f9eea8 100644 --- a/moli-layout/src/taffy_tree.rs +++ b/moli-layout/src/taffy_tree.rs @@ -1766,6 +1766,14 @@ where } } + fn block_alignment_includes_floats(&self, node_id: NodeId) -> bool { + !self.is_viewport_taffy_node(node_id) + && matches!( + self.boxes[LayoutBoxId::from_taffy(node_id).index()].kind, + LayoutBoxKind::TableCell | LayoutBoxKind::AnonymousTableCell + ) + } + fn compute_block_child_layout( &mut self, node_id: NodeId, @@ -2202,6 +2210,13 @@ where content_box_height - measurement.alignment_block_size, ); measurement.translate_block_axis(block_offset); + output.block_content_end = Some( + measurement.alignment_block_size + + padding.top + + border.top + + scrollbar_insets.top + + block_offset, + ); output.content_size.height = output.content_size.height.max( measurement.alignment_block_size + padding.top diff --git a/moli-layout/src/taffy_tree/measurement.rs b/moli-layout/src/taffy_tree/measurement.rs index 2107326551..c359abfa3c 100644 --- a/moli-layout/src/taffy_tree/measurement.rs +++ b/moli-layout/src/taffy_tree/measurement.rs @@ -110,6 +110,10 @@ impl LayoutBlockContainer for FullMeasurementTree<' .get_block_percentage_resolution_height(node_id, height) } + fn block_alignment_includes_floats(&self, node_id: NodeId) -> bool { + self.0.block_alignment_includes_floats(node_id) + } + fn compute_block_child_layout( &mut self, node_id: NodeId, diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update.rs b/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update.rs index 381a4fa01c..2b94d765a3 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update.rs @@ -2950,6 +2950,105 @@ return [outer.height,rect('inner').height,rect('percent').height, }).await.expect("nested percentage baseline should match Chromium"); } +#[tokio::test(flavor = "current_thread")] +async fn table_cell_synthetic_baselines_ignore_positioned_overflow() { + 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/table-cell-flow-baseline.html")?, + ); + page.vm_mut().set_layout_policy(moli_page_types::LayoutPolicy::OnDemand); + page.vm_mut().eval(r#" +document.head.innerHTML = ``; +document.body.innerHTML = ` + +
+
+
+
`; +"#)?; + for relative in [false, true, false] { + for height in [100, 160, 100] { + page.vm_mut().eval(&format!(r#" +document.getElementById('cell').style.height='{height}px'; +document.getElementById('overlay').style.display='{}'; +document.getElementById('content').style.cssText='height:10px;width:10px;{}'; +"#, if relative { "none" } else { "block" }, if relative { "position:relative;top:50%" } else { "" }))?; + for _ in 0..2 { + page.vm_mut().screenshot_layout_snapshot(moli_layout::PaintViewport::new(320, 240, 1.0))?.expect("table root"); + let actual = page.vm_mut().eval(r#" +['cell','peer','content','overlay'].map(id => { + const r=document.getElementById(id).getBoundingClientRect(); return [r.y,r.height].join(','); +}).join('|') +"#)?; + let content_y = 10 + if relative { height / 2 } else { 0 }; + let overlay_height = if relative { 0 } else { height }; + assert_eq!(actual, format!("0,{height}|0,20|{content_y},10|0,{overlay_height}"), + "relative={relative}: synthetic baselines use normal-flow geometry, while positioned descendants retain final cell geometry"); + } + } + } + Ok::<_, anyhow::Error>(()) + }).await.expect("positioned overflow must not move adjacent cell baselines"); +} + +#[tokio::test(flavor = "current_thread")] +async fn table_cell_vertical_alignment_includes_floats() { + 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/table-cell-float-alignment.html")?, + ); + page.vm_mut().set_layout_policy(moli_page_types::LayoutPolicy::OnDemand); + page.vm_mut().eval(r#" +document.head.innerHTML = ``; +document.body.innerHTML = ` +
+
+
+
+
`; +"#)?; + for align in ["middle", "bottom"] { + for mixed in [false, true] { + for padding in [0, 5] { + for height in [100, 160, 100] { + page.vm_mut().eval(&format!(r#" +document.getElementById('cell').style.cssText='position:relative;vertical-align:{align};padding:{padding}px'; +document.getElementById('block').style.display='{}'; +document.getElementById('tall').style.height='{height}px'; +"#, if mixed { "block" } else { "none" }))?; + for _ in 0..2 { + page.vm_mut().screenshot_layout_snapshot(moli_layout::PaintViewport::new(320, 240, 1.0))?.expect("table root"); + let actual = page.vm_mut().eval(r#" +['cell','float','block','overlay'].map(id => { + const r=document.getElementById(id).getBoundingClientRect(); return [r.y,r.height].join(','); +}).join('|') +"#)?; + let free = height - 2 * padding - 30; + let y = padding + if align == "middle" { free / 2 } else { free }; + let block = if mixed { format!("{y},20") } else { "0,0".to_owned() }; + assert_eq!(actual, format!("0,{height}|{y},30|{block}|0,{height}"), + "{align}, mixed={mixed}, padding={padding}: align floats and normal flow together without shifting the absolute overlay"); + } + } + } + } + } + Ok::<_, anyhow::Error>(()) + }).await.expect("table-cell vertical alignment must include floats"); +} + #[tokio::test(flavor = "current_thread")] async fn screenshot_table_row_heights_match_chromium() { run_page_vm_async_test(async move {