diff --git a/Libraries/LibWeb/Rust/src/layout/formatting_context.rs b/Libraries/LibWeb/Rust/src/layout/formatting_context.rs index e9ac8835c8fc9..a82d6d4106c7b 100644 --- a/Libraries/LibWeb/Rust/src/layout/formatting_context.rs +++ b/Libraries/LibWeb/Rust/src/layout/formatting_context.rs @@ -1618,6 +1618,13 @@ pub(crate) fn layout_inside_child<'pass>( force_independent_context_run: bool, ) -> ChildLayoutOutcome { let facts = run.state.node_facts(&run.callbacks, child); + if let Some((padding_top, padding_bottom)) = input.sizing.table_cell_intrinsic_block_padding { + debug_assert!(facts.is_table_cell()); + debug_assert!(matches!(input.participation, ParticipationInParentFormattingContext::Item)); + let used = run.state.used_values(&run.callbacks, child); + used.padding_top.set(used.padding_top.get() + padding_top); + used.padding_bottom.set(used.padding_bottom.get() + padding_bottom); + } let used = run.state.try_used_values(&run.callbacks, child); if !force_independent_context_run && layout_mode == LayoutMode::IntrinsicSizing diff --git a/Libraries/LibWeb/Rust/src/layout/geometry.rs b/Libraries/LibWeb/Rust/src/layout/geometry.rs index 7bb61fb702990..75c3caf818e3f 100644 --- a/Libraries/LibWeb/Rust/src/layout/geometry.rs +++ b/Libraries/LibWeb/Rust/src/layout/geometry.rs @@ -118,6 +118,9 @@ pub(crate) struct RootSizingDirectives { pub(crate) forced_content_inline_size: Option, pub(crate) forced_content_block_size: Option, pub(crate) forced_min_border_box_block_size: Option, + // Input-only: the table formatting context supplies the cell's intrinsic block padding + // (the vertical-alignment stretch) before laying out the cell's contents. + pub(crate) table_cell_intrinsic_block_padding: Option<(CssPixels, CssPixels)>, // Input-only: the wrapper's BFC tells the table formatting context where the table box's // content box sits in the wrapper, so rows and row groups (whose containing block is the // wrapper) can be placed in wrapper coordinates. diff --git a/Libraries/LibWeb/Rust/src/layout/table_formatting_context.rs b/Libraries/LibWeb/Rust/src/layout/table_formatting_context.rs index ad9fe4c1ed844..a8f77ea74ded3 100644 --- a/Libraries/LibWeb/Rust/src/layout/table_formatting_context.rs +++ b/Libraries/LibWeb/Rust/src/layout/table_formatting_context.rs @@ -781,6 +781,9 @@ struct TableFormattingContext<'pass> { min_border_box_block_size_from_flex_item: Option, needs_fixed_mode_row_measurement: bool, cells: Vec, + cell_inside_layout_inputs: Vec, + cell_pre_layout_content_block_sizes: Vec, + deferred_cell_inside_layouts: Vec, columns: Vec, rows: Vec, derived_baselines_of_root_box: Cell, @@ -840,6 +843,9 @@ impl<'pass> TableFormattingContext<'pass> { min_border_box_block_size_from_flex_item: None, needs_fixed_mode_row_measurement: false, cells: Vec::new(), + cell_inside_layout_inputs: Vec::new(), + cell_pre_layout_content_block_sizes: Vec::new(), + deferred_cell_inside_layouts: Vec::new(), columns: Vec::new(), rows: Vec::new(), derived_baselines_of_root_box: Cell::new(DerivedBaselines::default()), @@ -1838,6 +1844,9 @@ impl<'pass> TableFormattingContext<'pass> { // Determine the number of rows/columns the table requires. let table_grid = calculate_table_grid(self, self.table_box); self.cells = table_grid.cells; + self.cell_inside_layout_inputs = vec![AvailableSpace::default(); self.cells.len()]; + self.cell_pre_layout_content_block_sizes = vec![CssPixels::default(); self.cells.len()]; + self.deferred_cell_inside_layouts = vec![false; self.cells.len()]; self.rows = table_grid.rows; self.columns = vec![Column::default(); table_grid.column_count]; for cell in &self.cells { @@ -1903,6 +1912,7 @@ impl<'pass> TableFormattingContext<'pass> { cell: TableCell, input: AvailableSpace, adopt_automatic_content_block_size: bool, + intrinsic_block_padding: Option<(CssPixels, CssPixels)>, ) { let layout_input = LayoutInput { available_space: input, @@ -1910,6 +1920,7 @@ impl<'pass> TableFormattingContext<'pass> { content_box_position_in_bfc_root: None, sizing: RootSizingDirectives { adopt_automatic_content_block_size, + table_cell_intrinsic_block_padding: intrinsic_block_padding, ..RootSizingDirectives::default() }, participation: ParticipationInParentFormattingContext::Item, @@ -1930,6 +1941,7 @@ impl<'pass> TableFormattingContext<'pass> { cell: TableCell, used: &UsedValues, inner: AvailableSpace, + adopt_automatic_content_block_size: bool, ) -> Option { // The table formatting context owns the cell's outer geometry. Seed the inputs // needed to lay out its contents without copying placement or layout outputs. @@ -1963,11 +1975,17 @@ impl<'pass> TableFormattingContext<'pass> { let result = measurement.run_with_layout_mode( cell.box_, self.layout_mode, - LayoutInput::new(inner, ContainingBlockConstraints::default(), ParticipationInParentFormattingContext::Item), + LayoutInput { + available_space: inner, + containing_block_constraints: ContainingBlockConstraints::default(), + content_box_position_in_bfc_root: None, + sizing: RootSizingDirectives { + adopt_automatic_content_block_size, + ..RootSizingDirectives::default() + }, + participation: ParticipationInParentFormattingContext::Item, + }, ); - measurement - .root_used() - .set_content_block_size(result.automatic_content_block_size); Some(MeasuredCellContent { content_block_size: result.automatic_content_block_size, first_baseline: crate::layout::box_baseline( @@ -2036,16 +2054,22 @@ impl<'pass> TableFormattingContext<'pass> { let outer_space = self.available_space; let inner = used.available_inner_space_or_constraints_from(outer_space); + self.cell_inside_layout_inputs[cell_index] = inner; + self.cell_pre_layout_content_block_sizes[cell_index] = used.content_block_size.get(); + let defer_inside_layout = style.height().is_percentage() + || style.vertical_align_is_keyword() + || self.anonymous_cell_wraps_flex_or_grid(cell); + self.deferred_cell_inside_layouts[cell_index] = defer_inside_layout; let mut measured_baseline = None; - if style.height().is_percentage() { - // This cell's final inside layout happens in the second pass below; measure its + if defer_inside_layout { + // This cell's final inside layout happens once row heights are final; measure its // content in a throwaway state instead of laying out the committing state twice. - if let Some(measured) = self.measure_cell(cell, used, inner) { + if let Some(measured) = self.measure_cell(cell, used, inner, true) { used.set_content_block_size(measured.content_block_size); measured_baseline = Some(measured.first_baseline); } } else { - self.layout_inside_cell(run, cell, inner, true); + self.layout_inside_cell(run, cell, inner, true, None); } if self.needs_fixed_mode_row_measurement { let min_size = style.min_height().to_px(participant_block_basis); @@ -2144,6 +2168,7 @@ impl<'pass> TableFormattingContext<'pass> { let cell_size = style.height().to_px(self.table_block_size); let used = self.used_values(cell.box_); used.set_content_block_size(cell_size - used.border_box_top(collapsed) - used.border_box_bottom(collapsed)); + self.cell_pre_layout_content_block_sizes[cell_index] = used.content_block_size.get(); if !self.rows[cell.row_index].is_collapsed { self.rows[cell.row_index].reference_block_size = self.rows[cell.row_index].reference_block_size.max(cell_size); @@ -2156,10 +2181,12 @@ impl<'pass> TableFormattingContext<'pass> { + inline_spacing * (cell.column_span - 1), ); let inner = used.available_inner_space_or_constraints_from(self.available_space); - // The first pass only measured this cell in a throwaway state; this is its one and - // only inside layout in the committing state. - self.layout_inside_cell(run, cell, inner, false); - let baseline = self.box_baseline(cell.box_); + self.cell_inside_layout_inputs[cell_index] = inner; + // The first pass measured this cell at its automatic block size; measure it again at + // the percentage-resolved size to preserve the baseline its final inside layout will use. + let baseline = self + .measure_cell(cell, used, inner, false) + .map_or_else(|| self.box_baseline(cell.box_), |measured| measured.first_baseline); self.cells[cell_index].baseline = baseline; if !self.rows[cell.row_index].is_collapsed { let border_size = used.border_box_block_size(collapsed); @@ -2283,6 +2310,81 @@ impl<'pass> TableFormattingContext<'pass> { self.table_block_size = self.table_block_size.max(total); } + fn layout_deferred_cells_inside(&mut self, run: &FormattingContextRun<'pass>) { + // Deferred cells get their one and only committing inside layout here, once row + // block sizes are final. + let collapsed = self.style_facts(self.table_box).border_collapse() != BORDER_COLLAPSE_SEPARATE; + for cell_index in 0..self.cells.len() { + if !self.deferred_cell_inside_layouts[cell_index] { + continue; + } + let cell = self.cells[cell_index]; + let adopt_automatic_content_block_size = !self.style_facts(cell.box_).height().is_percentage(); + let intrinsic_block_padding = self.cell_intrinsic_block_padding(cell, collapsed); + let used = self.used_values(cell.box_); + let measured_content_block_size = used.content_block_size.get(); + // The first pass adopted the measured automatic block size so row sizing could read + // it; restore the pre-layout size so the cell's children resolve percentages against + // the same basis the measurement saw. + used.set_content_block_size(self.cell_pre_layout_content_block_sizes[cell_index]); + let inner = self.cell_inside_layout_inputs[cell_index]; + self.layout_inside_cell(run, cell, inner, adopt_automatic_content_block_size, intrinsic_block_padding); + if adopt_automatic_content_block_size { + debug_assert_eq!( + used.content_block_size.get(), + measured_content_block_size, + "measured and committed automatic block sizes diverged" + ); + } + } + } + + fn cell_intrinsic_block_padding(&mut self, cell: TableCell, collapsed: bool) -> Option<(CssPixels, CssPixels)> { + let row_size = self.compute_row_content_block_size(cell); + let used = self.used_values(cell.box_); + let style = self.style_facts(cell.box_); + // When a table cell is an anonymous wrapper around a flex or grid container (e.g., a with display:flex is + // wrapped in an anonymous table-cell box per CSS Tables 3), the cell should be aligned to the top. This allows + // the flex/grid container to fill the cell and handle alignment of its children via its own properties. + if self.anonymous_cell_wraps_flex_or_grid(cell) { + return Some((CssPixels::default(), row_size - used.border_box_block_size(collapsed))); + } + if !style.vertical_align_is_keyword() { + return None; + } + // The following image shows various alignment lines of a row: + // https://www.w3.org/TR/css-tables-3/images/cell-align-explainer.png + // https://drafts.csswg.org/css2/#height-layout + // In the context of tables, values for vertical-align have the following meanings: + match style.vertical_align_keyword() { + vertical_align::MIDDLE => { + // The center of the cell is aligned with the center of the rows it spans. + let difference = row_size - used.border_box_block_size(collapsed); + Some((difference / 2, difference / 2)) + } + vertical_align::TOP => { + // The top of the cell box is aligned with the top of the first row it spans. + Some((CssPixels::default(), row_size - used.border_box_block_size(collapsed))) + } + vertical_align::BOTTOM => { + // The bottom of the cell box is aligned with the bottom of the last row it spans. + Some((row_size - used.border_box_block_size(collapsed), CssPixels::default())) + } + vertical_align::SUB + | vertical_align::SUPER + | vertical_align::TEXT_BOTTOM + | vertical_align::TEXT_TOP + | vertical_align::BASELINE => { + // These values do not apply to cells; the cell is aligned at the baseline instead. + + // The baseline of the cell is put at the same height as the baseline of the first of the rows it spans. + let padding_top = self.rows[cell.row_index].baseline - cell.baseline; + Some((padding_top, row_size - (used.border_box_block_size(collapsed) + padding_top))) + } + _ => panic!("invalid vertical-align keyword"), + } + } + fn compute_row_content_block_size(&mut self, cell: TableCell) -> CssPixels { // The block size of a cell is the sum of all spanned rows, as described in // https://www.w3.org/TR/css-tables-3/#bounding-box-assignment @@ -2313,7 +2415,7 @@ impl<'pass> TableFormattingContext<'pass> { span + self.border_spacing_block() * (cell.row_span - 1) } - fn anonymous_cell_wraps_flex_or_grid(&mut self, cell: TableCell) -> bool { + fn anonymous_cell_wraps_flex_or_grid(&self, cell: TableCell) -> bool { if !self.node_facts(cell.box_).is_anonymous() { return false; } @@ -2337,53 +2439,6 @@ impl<'pass> TableFormattingContext<'pass> { let cell = self.cells[cell_index]; let used = self.used_values(cell.box_); let row_used = self.used_values(self.rows[cell.row_index].box_); - let row_size = self.compute_row_content_block_size(cell); - let style = self.style_facts(cell.box_); - // When a table cell is an anonymous wrapper around a flex or grid container (e.g., a with display:flex is - // wrapped in an anonymous table-cell box per CSS Tables 3), the cell should be aligned to the top. This allows - // the flex/grid container to fill the cell and handle alignment of its children via its own properties. - let anonymous_wrapper = self.anonymous_cell_wraps_flex_or_grid(cell); - if anonymous_wrapper { - used.padding_bottom - .set(used.padding_bottom.get() + row_size - used.border_box_block_size(collapsed)); - } else if style.vertical_align_is_keyword() { - // The following image shows various alignment lines of a row: - // https://www.w3.org/TR/css-tables-3/images/cell-align-explainer.png - // https://drafts.csswg.org/css2/#height-layout - // In the context of tables, values for vertical-align have the following meanings: - match style.vertical_align_keyword() { - vertical_align::MIDDLE => { - // The center of the cell is aligned with the center of the rows it spans. - let difference = row_size - used.border_box_block_size(collapsed); - used.padding_top.set(used.padding_top.get() + difference / 2); - used.padding_bottom.set(used.padding_bottom.get() + difference / 2); - } - vertical_align::TOP => { - // The top of the cell box is aligned with the top of the first row it spans. - used.padding_bottom - .set(used.padding_bottom.get() + row_size - used.border_box_block_size(collapsed)); - } - vertical_align::BOTTOM => { - // The bottom of the cell box is aligned with the bottom of the last row it spans. - used.padding_top - .set(used.padding_top.get() + row_size - used.border_box_block_size(collapsed)); - } - vertical_align::SUB - | vertical_align::SUPER - | vertical_align::TEXT_BOTTOM - | vertical_align::TEXT_TOP - | vertical_align::BASELINE => { - // These values do not apply to cells; the cell is aligned at the baseline instead. - - // The baseline of the cell is put at the same height as the baseline of the first of the rows it spans. - used.padding_top - .set(used.padding_top.get() + self.rows[cell.row_index].baseline - cell.baseline); - used.padding_bottom - .set(used.padding_bottom.get() + row_size - used.border_box_block_size(collapsed)); - } - _ => panic!("invalid vertical-align keyword"), - } - } // Compute cell position as specified by https://www.w3.org/TR/css-tables-3/#bounding-box-assignment: // left/top location is the sum of: // - for top: the height reserved for top captions (including margins), if any @@ -2440,6 +2495,7 @@ impl<'pass> TableFormattingContext<'pass> { self.compute_table_block_size(run); self.distribute_block_size_to_rows(); self.position_row_boxes(); + self.layout_deferred_cells_inside(run); self.position_cell_boxes(); table_used.set_content_block_size(self.table_block_size); // Derive baselines for the table internals bottom-up (rows, then row groups, then the table box)