blob: 4225dc283373d6b641d63e720c17201e69632162 [file] [log] [blame]
/*
* Copyright (C) 2019 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "TableFormattingContext.h"
#if ENABLE(LAYOUT_FORMATTING_CONTEXT)
#include "DisplayBox.h"
#include "InvalidationState.h"
#include "LayoutBox.h"
#include "LayoutChildIterator.h"
#include "LayoutContext.h"
#include "LayoutInitialContainingBlock.h"
#include "TableFormattingState.h"
#include <wtf/IsoMallocInlines.h>
namespace WebCore {
namespace Layout {
WTF_MAKE_ISO_ALLOCATED_IMPL(TableFormattingContext);
// https://www.w3.org/TR/css-tables-3/#table-layout-algorithm
TableFormattingContext::TableFormattingContext(const ContainerBox& formattingContextRoot, TableFormattingState& formattingState)
: FormattingContext(formattingContextRoot, formattingState)
{
}
void TableFormattingContext::layoutInFlowContent(InvalidationState&, const ConstraintsForInFlowContent& constraints)
{
auto availableHorizontalSpace = constraints.horizontal.logicalWidth;
auto availableVerticalSpace = constraints.vertical.logicalHeight;
// 1. Compute width and height for the grid.
computeAndDistributeExtraHorizontalSpace(availableHorizontalSpace);
computeAndDistributeExtraVerticalSpace(availableHorizontalSpace, availableVerticalSpace);
// 2. Finalize cells.
setUsedGeometryForCells(availableHorizontalSpace);
// 3. Finalize rows.
setUsedGeometryForRows(availableHorizontalSpace);
// 4. Finalize sections.
setUsedGeometryForSections(constraints);
}
void TableFormattingContext::setUsedGeometryForCells(LayoutUnit availableHorizontalSpace)
{
auto& grid = formattingState().tableGrid();
auto& columnList = grid.columns().list();
auto& rowList = grid.rows().list();
// Final table cell layout. At this point all percentage values can be resolved.
for (auto& cell : grid.cells()) {
auto& cellBox = cell->box();
auto& cellDisplayBox = formattingState().displayBox(cellBox);
cellDisplayBox.setTop(rowList[cell->startRow()].logicalTop());
cellDisplayBox.setLeft(columnList[cell->startColumn()].logicalLeft());
auto availableVerticalSpace = rowList[cell->startRow()].logicalHeight();
for (size_t rowIndex = cell->startRow() + 1; rowIndex < cell->endRow(); ++rowIndex)
availableVerticalSpace += rowList[rowIndex].logicalHeight();
layoutCell(*cell, availableHorizontalSpace, availableVerticalSpace);
// FIXME: Find out if it is ok to use the regular padding here to align the content box inside a tall cell or we need to
// use some kind of intrinsic padding similar to RenderTableCell.
auto paddingTop = cellDisplayBox.paddingTop().valueOr(LayoutUnit { });
auto paddingBottom = cellDisplayBox.paddingBottom().valueOr(LayoutUnit { });
auto intrinsicPaddingTop = LayoutUnit { };
auto intrinsicPaddingBottom = LayoutUnit { };
switch (cellBox.style().verticalAlign()) {
case VerticalAlign::Middle: {
auto intrinsicVerticalPadding = std::max(0_lu, availableVerticalSpace - cellDisplayBox.verticalMarginBorderAndPadding() - cellDisplayBox.contentBoxHeight());
intrinsicPaddingTop = intrinsicVerticalPadding / 2;
intrinsicPaddingBottom = intrinsicVerticalPadding / 2;
break;
}
case VerticalAlign::Baseline: {
auto rowBaselineOffset = LayoutUnit { rowList[cell->startRow()].baselineOffset() };
auto cellBaselineOffset = LayoutUnit { cell->baselineOffset() };
intrinsicPaddingTop = std::max(0_lu, rowBaselineOffset - cellBaselineOffset - cellDisplayBox.borderTop());
intrinsicPaddingBottom = std::max(0_lu, availableVerticalSpace - cellDisplayBox.verticalMarginBorderAndPadding() - intrinsicPaddingTop - cellDisplayBox.contentBoxHeight());
break;
}
default:
ASSERT_NOT_IMPLEMENTED_YET();
break;
}
cellDisplayBox.setVerticalPadding({ paddingTop + intrinsicPaddingTop, paddingBottom + intrinsicPaddingBottom });
}
}
void TableFormattingContext::setUsedGeometryForRows(LayoutUnit availableHorizontalSpace)
{
auto& grid = formattingState().tableGrid();
auto rowWidth = grid.columns().logicalWidth() + 2 * grid.horizontalSpacing();
auto rowLogicalTop = grid.verticalSpacing();
for (auto& row : grid.rows().list()) {
auto& rowBox = row.box();
auto& rowDisplayBox = formattingState().displayBox(rowBox);
computeBorderAndPadding(rowBox, HorizontalConstraints { { }, availableHorizontalSpace });
// Internal table elements do not have margins.
rowDisplayBox.setHorizontalMargin({ });
rowDisplayBox.setHorizontalComputedMargin({ });
rowDisplayBox.setVerticalMargin({ { }, { } });
rowDisplayBox.setContentBoxHeight(row.logicalHeight());
rowDisplayBox.setContentBoxWidth(rowWidth);
rowDisplayBox.setTop(rowLogicalTop);
rowDisplayBox.setLeft({ });
row.setLogicalTop(rowLogicalTop);
rowLogicalTop += row.logicalHeight() + grid.verticalSpacing();
}
}
void TableFormattingContext::setUsedGeometryForSections(const ConstraintsForInFlowContent& constraints)
{
auto& grid = formattingState().tableGrid();
auto sectionWidth = grid.columns().logicalWidth() + 2 * grid.horizontalSpacing();
auto logicalTop = constraints.vertical.logicalTop;
for (auto& section : childrenOfType<ContainerBox>(root())) {
auto& sectionDisplayBox = formattingState().displayBox(section);
computeBorderAndPadding(section, HorizontalConstraints { { }, constraints.horizontal.logicalWidth });
// Internal table elements do not have margins.
sectionDisplayBox.setHorizontalMargin({ });
sectionDisplayBox.setHorizontalComputedMargin({ });
sectionDisplayBox.setVerticalMargin({ { }, { } });
sectionDisplayBox.setContentBoxWidth(sectionWidth);
sectionDisplayBox.setContentBoxHeight(grid.rows().list().last().logicalBottom() + grid.verticalSpacing());
sectionDisplayBox.setLeft(constraints.horizontal.logicalLeft);
sectionDisplayBox.setTop(logicalTop);
logicalTop += sectionDisplayBox.height();
}
}
void TableFormattingContext::layoutCell(const TableGrid::Cell& cell, LayoutUnit availableHorizontalSpace, Optional<LayoutUnit> usedCellHeight)
{
ASSERT(cell.box().establishesBlockFormattingContext());
auto& cellBox = cell.box();
auto& cellDisplayBox = formattingState().displayBox(cellBox);
computeBorderAndPadding(cellBox, HorizontalConstraints { { }, availableHorizontalSpace });
// Internal table elements do not have margins.
cellDisplayBox.setHorizontalMargin({ });
cellDisplayBox.setHorizontalComputedMargin({ });
cellDisplayBox.setVerticalMargin({ { }, { } });
auto availableSpaceForContent = [&] {
auto& grid = formattingState().tableGrid();
auto& columnList = grid.columns().list();
auto logicalWidth = LayoutUnit { };
for (auto columnIndex = cell.startColumn(); columnIndex < cell.endColumn(); ++columnIndex)
logicalWidth += columnList.at(columnIndex).logicalWidth();
// No column spacing when spanning.
logicalWidth += (cell.columnSpan() - 1) * grid.horizontalSpacing();
return logicalWidth - cellDisplayBox.horizontalMarginBorderAndPadding();
}();
cellDisplayBox.setContentBoxWidth(availableSpaceForContent);
if (cellBox.hasInFlowOrFloatingChild()) {
auto invalidationState = InvalidationState { };
auto constraintsForCellContent = geometry().constraintsForInFlowContent(cellBox);
constraintsForCellContent.vertical.logicalHeight = usedCellHeight;
LayoutContext::createFormattingContext(cellBox, layoutState())->layoutInFlowContent(invalidationState, constraintsForCellContent);
}
cellDisplayBox.setContentBoxHeight(geometry().cellHeigh(cellBox));
}
FormattingContext::IntrinsicWidthConstraints TableFormattingContext::computedIntrinsicWidthConstraints()
{
// Tables have a slighty different concept of shrink to fit. It's really only different with non-auto "width" values, where
// a generic shrink-to fit block level box like a float box would be just sized to the computed value of "width", tables
// can actually be streched way over.
auto& grid = formattingState().tableGrid();
if (auto computedWidthConstraints = grid.widthConstraints())
return *computedWidthConstraints;
// 1. Ensure each cell slot is occupied by at least one cell.
ensureTableGrid();
// 2. Compute the minimum/maximum width of each column.
auto computedWidthConstraints = computedPreferredWidthForColumns();
grid.setWidthConstraints(computedWidthConstraints);
return computedWidthConstraints;
}
void TableFormattingContext::ensureTableGrid()
{
auto& tableBox = root();
auto& tableGrid = formattingState().tableGrid();
auto& tableStyle = tableBox.style();
auto shouldApplyBorderSpacing = tableStyle.borderCollapse() == BorderCollapse::Separate;
tableGrid.setHorizontalSpacing(LayoutUnit { shouldApplyBorderSpacing ? tableStyle.horizontalBorderSpacing() : 0 });
tableGrid.setVerticalSpacing(LayoutUnit { shouldApplyBorderSpacing ? tableStyle.verticalBorderSpacing() : 0 });
auto* firstChild = tableBox.firstChild();
const Box* tableCaption = nullptr;
const Box* colgroup = nullptr;
// Table caption is an optional element; if used, it is always the first child of a <table>.
if (firstChild->isTableCaption())
tableCaption = firstChild;
// The <colgroup> must appear after any optional <caption> element but before any <thead>, <th>, <tbody>, <tfoot> and <tr> element.
auto* colgroupCandidate = firstChild;
if (tableCaption)
colgroupCandidate = tableCaption->nextSibling();
if (colgroupCandidate->isTableColumnGroup())
colgroup = colgroupCandidate;
if (colgroup) {
auto& columns = tableGrid.columns();
for (auto* column = downcast<ContainerBox>(*colgroup).firstChild(); column; column = column->nextSibling()) {
ASSERT(column->isTableColumn());
auto columnSpanCount = column->columnSpan();
ASSERT(columnSpanCount > 0);
while (columnSpanCount--)
columns.addColumn(downcast<ContainerBox>(*column));
}
}
auto* firstSection = colgroup ? colgroup->nextSibling() : tableCaption ? tableCaption->nextSibling() : firstChild;
for (auto* section = firstSection; section; section = section->nextSibling()) {
ASSERT(section->isTableHeader() || section->isTableBody() || section->isTableFooter());
for (auto* row = downcast<ContainerBox>(*section).firstChild(); row; row = row->nextSibling()) {
ASSERT(row->isTableRow());
for (auto* cell = downcast<ContainerBox>(*row).firstChild(); cell; cell = cell->nextSibling()) {
ASSERT(cell->isTableCell());
tableGrid.appendCell(downcast<ContainerBox>(*cell));
}
}
}
}
FormattingContext::IntrinsicWidthConstraints TableFormattingContext::computedPreferredWidthForColumns()
{
auto& formattingState = this->formattingState();
auto& grid = formattingState.tableGrid();
ASSERT(!grid.widthConstraints());
// Column preferred width computation as follows:
// 1. Collect each cells' width constraints
// 2. Collect fixed column widths set by <colgroup>'s and <col>s
// 3. Find the min/max width for each columns using the cell constraints and the <col> fixed widths but ignore column spans.
// 4. Distribute column spanning cells min/max widths.
// 5. Add them all up and return the computed min/max widths.
for (auto& cell : grid.cells()) {
auto& cellBox = cell->box();
ASSERT(cellBox.establishesBlockFormattingContext());
auto intrinsicWidth = formattingState.intrinsicWidthConstraintsForBox(cellBox);
if (!intrinsicWidth) {
intrinsicWidth = geometry().intrinsicWidthConstraintsForCell(cellBox);
formattingState.setIntrinsicWidthConstraintsForBox(cellBox, *intrinsicWidth);
}
// Spanner cells put their intrinsic widths on the initial slots.
grid.slot(cell->position())->setWidthConstraints(*intrinsicWidth);
}
// 2. Collect the fixed width <col>s.
auto& columnList = grid.columns().list();
Vector<Optional<LayoutUnit>> fixedWidthColumns;
for (auto& column : columnList) {
auto fixedWidth = [&] () -> Optional<LayoutUnit> {
auto* columnBox = column.box();
if (!columnBox) {
// Anoynmous columns don't have associated layout boxes and can't have fixed col size.
return { };
}
if (auto width = columnBox->columnWidth())
return width;
return geometry().computedColumnWidth(*columnBox);
};
fixedWidthColumns.append(fixedWidth());
}
Vector<FormattingContext::IntrinsicWidthConstraints> columnIntrinsicWidths(columnList.size());
// 3. Collect he min/max width for each column but ignore column spans for now.
Vector<SlotPosition> spanningCellPositionList;
size_t numberOfActualColumns = 0;
for (size_t columnIndex = 0; columnIndex < columnList.size(); ++columnIndex) {
auto columnHasNonSpannedCell = false;
for (size_t rowIndex = 0; rowIndex < grid.rows().size(); ++rowIndex) {
auto& slot = *grid.slot({ columnIndex, rowIndex });
if (slot.isColumnSpanned())
continue;
columnHasNonSpannedCell = true;
if (slot.hasColumnSpan()) {
spanningCellPositionList.append({ columnIndex, rowIndex });
continue;
}
auto columnFixedWidth = fixedWidthColumns[columnIndex];
auto widthConstraints = !columnFixedWidth ? slot.widthConstraints() : FormattingContext::IntrinsicWidthConstraints { *columnFixedWidth, *columnFixedWidth };
columnIntrinsicWidths[columnIndex].minimum = std::max(widthConstraints.minimum, columnIntrinsicWidths[columnIndex].minimum);
columnIntrinsicWidths[columnIndex].maximum = std::max(widthConstraints.maximum, columnIntrinsicWidths[columnIndex].maximum);
}
if (columnHasNonSpannedCell)
++numberOfActualColumns;
}
// 4. Distribute the spanning min/max widths.
for (auto spanningCellPosition : spanningCellPositionList) {
auto& slot = *grid.slot(spanningCellPosition);
auto& cell = slot.cell();
ASSERT(slot.hasColumnSpan());
auto widthConstraintsToDistribute = slot.widthConstraints();
for (size_t columnSpanIndex = cell.startColumn(); columnSpanIndex < cell.endColumn(); ++columnSpanIndex)
widthConstraintsToDistribute -= columnIntrinsicWidths[columnSpanIndex];
// <table style="border-spacing: 50px"><tr><td colspan=2>long long text</td></tr><tr><td>lo</td><td>xt</td><tr></table>
// [long long text]
// [lo] [xt]
// While it looks like the spanning cell has to distribute all its spanning width, the border-spacing takes most of the space and
// no distribution is needed at all.
widthConstraintsToDistribute -= (cell.columnSpan() - 1) * grid.horizontalSpacing();
// FIXME: Check if fixed width columns should be skipped here.
widthConstraintsToDistribute.minimum = std::max(LayoutUnit { }, widthConstraintsToDistribute.minimum / cell.columnSpan());
widthConstraintsToDistribute.maximum = std::max(LayoutUnit { }, widthConstraintsToDistribute.maximum / cell.columnSpan());
if (widthConstraintsToDistribute.minimum || widthConstraintsToDistribute.maximum) {
for (size_t columnSpanIndex = cell.startColumn(); columnSpanIndex < cell.endColumn(); ++columnSpanIndex)
columnIntrinsicWidths[columnSpanIndex] += widthConstraintsToDistribute;
}
}
// 5. The final table min/max widths is just the accumulated column constraints.
auto tableWidthConstraints = IntrinsicWidthConstraints { };
for (auto& columnIntrinsicWidth : columnIntrinsicWidths)
tableWidthConstraints += columnIntrinsicWidth;
// Exapand the preferred width with leading and trailing cell spacing (note that column spanners count as one cell).
tableWidthConstraints += (numberOfActualColumns + 1) * grid.horizontalSpacing();
return tableWidthConstraints;
}
struct ColumnSpan {
static size_t hasSpan(const TableGrid::Slot& slot) { return slot.hasColumnSpan(); }
static size_t isSpanned(const TableGrid::Slot& slot) { return slot.isColumnSpanned(); }
static size_t spanCount(const TableGrid::Cell& cell) { return cell.columnSpan(); }
static size_t startSpan(const TableGrid::Cell& cell) { return cell.startColumn(); }
static size_t endSpan(const TableGrid::Cell& cell) { return cell.endColumn(); }
static size_t index(size_t columnIndex, size_t /*rowIndex*/) { return columnIndex; }
static size_t size(const TableGrid& grid) { return grid.columns().size(); }
static LayoutUnit spacing(const TableGrid& grid) { return grid.horizontalSpacing(); }
};
struct RowSpan {
static size_t hasSpan(const TableGrid::Slot& slot) { return slot.hasRowSpan(); }
static size_t isSpanned(const TableGrid::Slot& slot) { return slot.isRowSpanned(); }
static size_t spanCount(const TableGrid::Cell& cell) { return cell.rowSpan(); }
static size_t startSpan(const TableGrid::Cell& cell) { return cell.startRow(); }
static size_t endSpan(const TableGrid::Cell& cell) { return cell.endRow(); }
static size_t index(size_t /*columnIndex*/, size_t rowIndex) { return rowIndex; }
static size_t size(const TableGrid& grid) { return grid.rows().size(); }
static LayoutUnit spacing(const TableGrid& grid) { return grid.verticalSpacing(); }
};
struct GridSpace {
bool isEmpty() const { return !value; }
// Initial width/height for column/row we start the distribution width (usually a minumum width).
float value { 0 };
// The base to compute the distribution ratio. It normally matches the [value] but in some cases we use the maximum value to distribute the extra space.
float distributionBase { 0 };
};
inline static GridSpace max(const GridSpace& a, const GridSpace& b)
{
return { std::max(a.value, b.value), std::max(a.distributionBase, b.distributionBase) };
}
inline static GridSpace& operator-(GridSpace& a, const GridSpace& b)
{
a.value = std::max(0.0f, a.value - b.value);
a.distributionBase = std::max(0.0f, a.distributionBase - b.distributionBase);
return a;
}
inline static GridSpace& operator+=(GridSpace& a, const GridSpace& b)
{
a.value += b.value;
a.distributionBase += b.distributionBase;
return a;
}
inline static GridSpace& operator-=(GridSpace& a, const GridSpace& b)
{
return a - b;
}
inline static GridSpace& operator/(GridSpace& a, unsigned value)
{
a.value /= value;
a.distributionBase /= value;
return a;
}
using DistributedSpaces = Vector<float>;
template <typename SpanType>
static DistributedSpaces distributeAvailableSpace(const TableGrid& grid, LayoutUnit availableSpace, const WTF::Function<GridSpace(const TableGrid::Slot&, size_t)>& slotSpace)
{
struct ResolvedItem {
GridSpace slotSpace;
bool isFixed { false };
};
auto& columns = grid.columns();
auto& rows = grid.rows();
// 1. Collect the non-spanning spaces first. They are used for the final distribution as well as for distributing the spanning space.
Vector<Optional<ResolvedItem>> resolvedItems(SpanType::size(grid));
for (size_t columnIndex = 0; columnIndex < columns.size(); ++columnIndex) {
for (size_t rowIndex = 0; rowIndex < rows.size(); ++rowIndex) {
auto& slot = *grid.slot({ columnIndex, rowIndex });
if (SpanType::hasSpan(slot) || SpanType::isSpanned(slot))
continue;
auto index = SpanType::index(columnIndex, rowIndex);
if (!resolvedItems[index])
resolvedItems[index] = ResolvedItem { };
resolvedItems[index]->slotSpace = max(resolvedItems[index]->slotSpace, slotSpace(slot, index));
}
}
// 2. Collect the spanning cells.
struct SpanningCell {
SlotPosition position;
GridSpace unresolvedSpace;
};
Vector<SpanningCell> spanningCells;
for (size_t rowIndex = 0; rowIndex < rows.size(); ++rowIndex) {
for (size_t columnIndex = 0; columnIndex < columns.size(); ++columnIndex) {
auto& slot = *grid.slot({ columnIndex, rowIndex });
if (SpanType::hasSpan(slot))
spanningCells.append({ { columnIndex, rowIndex }, slotSpace(slot, SpanType::index(columnIndex, rowIndex)) });
}
}
// We need these spanning cells in the order of the number of columns/rows they span so that
// we can resolve overlapping spans starting with the shorter ones e.g.
// <td colspan=4>#a</td><td>#b</td>
// <td colspan=2>#c</td><td colspan=3>#d</td>
std::sort(spanningCells.begin(), spanningCells.end(), [&] (auto& a, auto& b) {
return SpanType::spanCount(grid.slot(a.position)->cell()) < SpanType::spanCount(grid.slot(b.position)->cell());
});
// 3. Distribute the spanning cells' mimimum space across the columns/rows using the non-spanning spaces.
// e.g. [ 1 ][ 5 ][ 1 ]
// [ 9 ][ 1 ]
// The initial widths are: [ 2 ][ 7 ][ 1 ]
for (auto spanningCell : spanningCells) {
auto& cell = grid.slot(spanningCell.position)->cell();
auto unresolvedSpanningSpace = spanningCell.unresolvedSpace;
if (!resolvedItems[SpanType::startSpan(cell)] || !resolvedItems[SpanType::endSpan(cell) - 1]) {
// <td colspan=4>#a</td><td>#b</td>
// <td colspan=2>#c</td><td colspan=3>#d</td>
// Unresolved columns are: 1 2 3 4
// 1. Take colspan=2 (shortest span) and resolve column 1 and 2
// 2. Take colspan=3 and resolve column 3 and 4 (5 is resolved because it already has a non-spanning cell).
// 3. colspan=4 needs no resolving because all the spanned columns (1 2 3 4) have already been resolved.
auto unresolvedColumnCount = cell.columnSpan();
for (auto spanIndex = SpanType::startSpan(cell); spanIndex < SpanType::endSpan(cell); ++spanIndex) {
if (!resolvedItems[spanIndex])
continue;
ASSERT(unresolvedColumnCount);
--unresolvedColumnCount;
unresolvedSpanningSpace -= resolvedItems[spanIndex]->slotSpace;
}
ASSERT(unresolvedColumnCount);
auto equalSpaceForSpannedColumns = unresolvedSpanningSpace / unresolvedColumnCount;
for (auto spanIndex = SpanType::startSpan(cell); spanIndex < SpanType::endSpan(cell); ++spanIndex) {
if (resolvedItems[spanIndex])
continue;
resolvedItems[spanIndex] = ResolvedItem { equalSpaceForSpannedColumns, false };
}
} else {
// 1. Collect the non-spaning resolved spaces.
// 2. Distribute the extra space among the spanned columns/rows based on the resolved space values.
// e.g. spanning width: [ 9 ]. Resolved widths for the spanned columns: [ 1 ] [ 2 ]
// New resolved widths: [ 3 ] [ 6 ].
auto resolvedSpanningSpace = GridSpace { };
for (auto spanIndex = SpanType::startSpan(cell); spanIndex < SpanType::endSpan(cell); ++spanIndex)
resolvedSpanningSpace += resolvedItems[spanIndex]->slotSpace;
if (resolvedSpanningSpace.value >= unresolvedSpanningSpace.value) {
// The spanning cell fits the spanned columns/rows just fine. Nothing to distribute.
continue;
}
auto spacing = SpanType::spacing(grid) * (SpanType::spanCount(cell) - 1);
auto spaceToDistribute = unresolvedSpanningSpace - GridSpace { spacing, spacing } - resolvedSpanningSpace;
if (!spaceToDistribute.isEmpty()) {
auto distributionRatio = spaceToDistribute.distributionBase / resolvedSpanningSpace.distributionBase;
for (auto spanIndex = SpanType::startSpan(cell); spanIndex < SpanType::endSpan(cell); ++spanIndex)
resolvedItems[spanIndex]->slotSpace += GridSpace { resolvedItems[spanIndex]->slotSpace.value * distributionRatio, resolvedItems[spanIndex]->slotSpace.distributionBase * distributionRatio};
}
}
}
// 4. Distribute the extra space using the final resolved widths.
#if ASSERT_ENABLED
// We have to have all the spaces resolved at this point.
for (auto& resolvedItem : resolvedItems)
ASSERT(resolvedItem);
#endif
// Fixed size cells don't participate in available space distribution.
auto adjustabledSpace = GridSpace { };
for (auto& resolvedItem : resolvedItems) {
if (resolvedItem->isFixed)
continue;
adjustabledSpace += resolvedItem->slotSpace;
}
DistributedSpaces distributedSpaces(resolvedItems.size());
float spaceToDistribute = availableSpace - adjustabledSpace.value - ((resolvedItems.size() + 1) * SpanType::spacing(grid));
// Essentially the remaining space to distribute should never be negative. LayoutUnit::epsilon() is required to compensate for LayoutUnit's low precision.
ASSERT(spaceToDistribute >= -LayoutUnit::epsilon() * resolvedItems.size());
// Distribute the extra space based on the resolved spaces.
auto distributionRatio = spaceToDistribute / adjustabledSpace.distributionBase;
for (size_t index = 0; index < resolvedItems.size(); ++index) {
auto slotSpace = resolvedItems[index]->slotSpace.value;
auto needsSpaceDistribution = spaceToDistribute && !resolvedItems[index]->isFixed;
distributedSpaces[index] = slotSpace;
if (!needsSpaceDistribution)
continue;
distributedSpaces[index] += resolvedItems[index]->slotSpace.distributionBase * distributionRatio;
}
return distributedSpaces;
}
void TableFormattingContext::computeAndDistributeExtraHorizontalSpace(LayoutUnit availableHorizontalSpace)
{
auto& grid = formattingState().tableGrid();
auto& columns = grid.columns();
auto tableWidthConstraints = *grid.widthConstraints();
enum class ColumnWidthBalancingBase { MinimumWidth, MaximumWidth };
auto computeColumnWidths = [&] (auto columnWidthBalancingBase, auto extraHorizontalSpace) {
auto distributedSpaces = distributeAvailableSpace<ColumnSpan>(grid, extraHorizontalSpace, [&] (const TableGrid::Slot& slot, size_t columnIndex) {
auto& column = columns.list()[columnIndex];
auto columnBoxFixedWidth = column.box() ? column.box()->columnWidth().valueOr(0_lu) : 0_lu;
if (columnWidthBalancingBase == ColumnWidthBalancingBase::MinimumWidth) {
auto minimumWidth = std::max<float>(slot.widthConstraints().minimum, columnBoxFixedWidth);
return GridSpace { minimumWidth, minimumWidth };
}
// When the column has a fixed width cell, the maximum width balancing is based on the minimum width.
auto minimumWidth = std::max<float>(slot.widthConstraints().minimum, columnBoxFixedWidth);
auto maximumWidth = std::max<float>(slot.widthConstraints().maximum, columnBoxFixedWidth);
if (column.isFixedWidth())
return GridSpace { minimumWidth, maximumWidth };
return GridSpace { maximumWidth, maximumWidth };
});
// Set final horizontal position and width.
auto columnLogicalLeft = grid.horizontalSpacing();
for (size_t columnIndex = 0; columnIndex < columns.size(); ++columnIndex) {
auto& column = columns.list()[columnIndex];
auto columnWidth = LayoutUnit { distributedSpaces[columnIndex] };
column.setLogicalLeft(columnLogicalLeft);
column.setLogicalWidth(columnWidth);
columnLogicalLeft += columnWidth + grid.horizontalSpacing();
}
};
auto columnWidthBalancingBase = availableHorizontalSpace == tableWidthConstraints.maximum ? ColumnWidthBalancingBase::MaximumWidth : ColumnWidthBalancingBase::MinimumWidth;
computeColumnWidths(columnWidthBalancingBase, availableHorizontalSpace);
}
void TableFormattingContext::computeAndDistributeExtraVerticalSpace(LayoutUnit availableHorizontalSpace, Optional<LayoutUnit> availableVerticalSpace)
{
auto& grid = formattingState().tableGrid();
auto& columns = grid.columns().list();
auto& rows = grid.rows();
struct RowHeight {
InlineLayoutUnit height() const { return ascent + descent; }
InlineLayoutUnit ascent { 0 };
InlineLayoutUnit descent { 0 };
};
Vector<RowHeight> rowHeight(rows.size());
Vector<SlotPosition> spanningRowPositionList;
LayoutUnit tableUsedHeight;
// 1. Collect initial, basline aligned row heights.
for (size_t rowIndex = 0; rowIndex < rows.size(); ++rowIndex) {
auto maximumColumnAscent = InlineLayoutUnit { };
auto maximumColumnDescent = InlineLayoutUnit { };
for (size_t columnIndex = 0; columnIndex < columns.size(); ++columnIndex) {
auto& slot = *grid.slot({ columnIndex, rowIndex });
if (slot.isRowSpanned())
continue;
layoutCell(slot.cell(), availableHorizontalSpace);
if (slot.hasRowSpan())
continue;
// The minimum height of a row (without spanning-related height distribution) is defined as the height of an hypothetical
// linebox containing the cells originating in the row.
auto& cell = slot.cell();
auto& cellBox = cell.box();
cell.setBaselineOffset(geometry().usedBaselineForCell(cellBox));
maximumColumnAscent = std::max(maximumColumnAscent, cell.baselineOffset());
maximumColumnDescent = std::max(maximumColumnDescent, geometryForBox(cellBox).height() - cell.baselineOffset());
}
// <tr style="height: 10px"> is considered as min height.
rowHeight[rowIndex] = { maximumColumnAscent, maximumColumnDescent };
tableUsedHeight += maximumColumnAscent + maximumColumnDescent;
}
// FIXME: Collect spanning row maximum heights.
// Distribute extra space if the table is supposed to be taller than the sum of the row heights.
tableUsedHeight += (rows.size() + 1) * grid.verticalSpacing();
auto availableSpace = std::max(availableVerticalSpace.valueOr(0_lu), tableUsedHeight);
auto distributedSpaces = distributeAvailableSpace<RowSpan>(grid, availableSpace, [&] (const TableGrid::Slot& slot, size_t rowIndex) {
if (slot.hasRowSpan())
return GridSpace { geometryForBox(slot.cell().box()).height(), geometryForBox(slot.cell().box()).height() };
auto computedRowHeight = geometry().computedHeight(rows.list()[rowIndex].box(), { });
auto height = std::max<float>(rowHeight[rowIndex].height(), computedRowHeight.valueOr(0_lu));
return GridSpace { height, height };
});
auto rowLogicalTop = grid.verticalSpacing();
for (size_t rowIndex = 0; rowIndex < rows.size(); ++rowIndex) {
auto& row = grid.rows().list()[rowIndex];
auto rowUsedHeight = LayoutUnit { distributedSpaces[rowIndex] };
row.setLogicalHeight(rowUsedHeight);
row.setBaselineOffset(rowHeight[rowIndex].ascent);
row.setLogicalTop(rowLogicalTop);
rowLogicalTop += rowUsedHeight + grid.verticalSpacing();
}
}
}
}
#endif