Source code
Revision control
Copy as Markdown
Other Tools
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
// vim:cindent:ts=2:et:sw=2:
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
/*
* Algorithms that determine column and table inline sizes used for
* CSS2's 'table-layout: fixed'.
*/
#include "FixedTableLayoutStrategy.h"
#include "nsLayoutUtils.h"
#include "nsStyleConsts.h"
#include "nsTableFrame.h"
#include "nsTableColFrame.h"
#include "nsTableCellFrame.h"
#include "WritingModes.h"
#include <algorithm>
using namespace mozilla;
FixedTableLayoutStrategy::FixedTableLayoutStrategy(nsTableFrame* aTableFrame)
: nsITableLayoutStrategy(nsITableLayoutStrategy::Fixed),
mTableFrame(aTableFrame) {
MarkIntrinsicISizesDirty();
}
/* virtual */
FixedTableLayoutStrategy::~FixedTableLayoutStrategy() = default;
/* virtual */
nscoord FixedTableLayoutStrategy::GetMinISize(gfxContext* aRenderingContext) {
if (mMinISize != NS_INTRINSIC_ISIZE_UNKNOWN) {
return mMinISize;
}
// It's theoretically possible to do something much better here that
// depends only on the columns and the first row (where we look at
// intrinsic inline sizes inside the first row and then reverse the
// algorithm to find the narrowest inline size that would hold all of
// those intrinsic inline sizes), but it wouldn't be compatible with
// other browsers, or with the use of GetMinISize by
// nsTableFrame::ComputeSize to determine the inline size of a fixed
// layout table, since CSS2.1 says:
// The width of the table is then the greater of the value of the
// 'width' property for the table element and the sum of the column
// widths (plus cell spacing or borders).
// XXX Should we really ignore 'min-inline-size' and 'max-inline-size'?
// XXX Should we really ignore inline sizes on column groups?
nsTableCellMap* cellMap = mTableFrame->GetCellMap();
int32_t colCount = cellMap->GetColCount();
nscoord result = 0;
if (colCount > 0) {
result += mTableFrame->GetColSpacing(-1, colCount);
}
WritingMode wm = mTableFrame->GetWritingMode();
for (int32_t col = 0; col < colCount; ++col) {
nsTableColFrame* colFrame = mTableFrame->GetColFrame(col);
if (!colFrame) {
NS_ERROR("column frames out of sync with cell map");
continue;
}
nscoord spacing = mTableFrame->GetColSpacing(col);
const auto* styleISize = &colFrame->StylePosition()->ISize(wm);
if (styleISize->ConvertsToLength()) {
result += styleISize->ToLength();
} else if (styleISize->ConvertsToPercentage()) {
// do nothing
} else {
// The 'table-layout: fixed' algorithm considers only cells in the
// first row.
bool originates;
int32_t colSpan;
nsTableCellFrame* cellFrame =
cellMap->GetCellInfoAt(0, col, &originates, &colSpan);
if (cellFrame) {
styleISize = &cellFrame->StylePosition()->ISize(wm);
if (styleISize->ConvertsToLength() || styleISize->IsMinContent() ||
styleISize->IsMaxContent()) {
nscoord cellISize = nsLayoutUtils::IntrinsicForContainer(
aRenderingContext, cellFrame, IntrinsicISizeType::MinISize);
if (colSpan > 1) {
// If a column-spanning cell is in the first row, split up
// the space evenly. (XXX This isn't quite right if some of
// the columns it's in have specified inline sizes. Should
// we care?)
cellISize = ((cellISize + spacing) / colSpan) - spacing;
}
result += cellISize;
} else if (styleISize->ConvertsToPercentage()) {
if (colSpan > 1) {
// XXX Can this force columns to negative inline sizes?
result -= spacing * (colSpan - 1);
}
}
// else, for 'auto', '-moz-available', '-moz-fit-content',
// and 'calc()' with both lengths and percentages, do nothing
}
}
}
return (mMinISize = result);
}
/* virtual */
nscoord FixedTableLayoutStrategy::GetPrefISize(gfxContext* aRenderingContext,
bool aComputingSize) {
// It's theoretically possible to do something much better here that
// depends only on the columns and the first row (where we look at
// intrinsic inline sizes inside the first row and then reverse the
// algorithm to find the narrowest inline size that would hold all of
// those intrinsic inline sizes), but it wouldn't be compatible with
// other browsers.
return nscoord_MAX;
}
/* virtual */
void FixedTableLayoutStrategy::MarkIntrinsicISizesDirty() {
mMinISize = NS_INTRINSIC_ISIZE_UNKNOWN;
mLastCalcISize = nscoord_MIN;
}
static inline nscoord AllocateUnassigned(nscoord aUnassignedSpace,
float aShare) {
if (aShare == 1.0f) {
// This happens when the numbers we're dividing to get aShare are
// equal. We want to return unassignedSpace exactly, even if it
// can't be precisely round-tripped through float.
return aUnassignedSpace;
}
return NSToCoordRound(float(aUnassignedSpace) * aShare);
}
/* virtual */
void FixedTableLayoutStrategy::ComputeColumnISizes(
const ReflowInput& aReflowInput) {
nscoord tableISize = aReflowInput.ComputedISize();
if (mLastCalcISize == tableISize) {
return;
}
mLastCalcISize = tableISize;
nsTableCellMap* cellMap = mTableFrame->GetCellMap();
int32_t colCount = cellMap->GetColCount();
if (colCount == 0) {
// No Columns - nothing to compute
return;
}
// border-spacing isn't part of the basis for percentages.
tableISize -= mTableFrame->GetColSpacing(-1, colCount);
// store the old column inline sizes. We might call SetFinalISize
// multiple times on the columns, due to this we can't compare at the
// last call that the inline size has changed with respect to the last
// call to ComputeColumnISizes. In order to overcome this we store the
// old values in this array. A single call to SetFinalISize would make
// it possible to call GetFinalISize before and to compare when
// setting the final inline size.
nsTArray<nscoord> oldColISizes;
// XXX This ignores the 'min-width' and 'max-width' properties
// throughout. Then again, that's what the CSS spec says to do.
// XXX Should we really ignore widths on column groups?
uint32_t unassignedCount = 0;
nscoord unassignedSpace = tableISize;
const nscoord unassignedMarker = nscoord_MIN;
// We use the PrefPercent on the columns to store the percentages
// used to compute column inline sizes in case we need to shrink or
// expand the columns.
float pctTotal = 0.0f;
// Accumulate the total specified (non-percent) on the columns for
// distributing excess inline size to the columns.
nscoord specTotal = 0;
WritingMode wm = mTableFrame->GetWritingMode();
for (int32_t col = 0; col < colCount; ++col) {
nsTableColFrame* colFrame = mTableFrame->GetColFrame(col);
if (!colFrame) {
oldColISizes.AppendElement(0);
NS_ERROR("column frames out of sync with cell map");
continue;
}
oldColISizes.AppendElement(colFrame->GetFinalISize());
colFrame->ResetPrefPercent();
const auto* styleISize = &colFrame->StylePosition()->ISize(wm);
nscoord colISize;
if (styleISize->ConvertsToLength()) {
colISize = styleISize->ToLength();
specTotal += colISize;
} else if (styleISize->ConvertsToPercentage()) {
float pct = styleISize->ToPercentage();
colISize = NSToCoordFloor(pct * float(tableISize));
colFrame->AddPrefPercent(pct);
pctTotal += pct;
} else {
// The 'table-layout: fixed' algorithm considers only cells in the
// first row.
bool originates;
int32_t colSpan;
nsTableCellFrame* cellFrame =
cellMap->GetCellInfoAt(0, col, &originates, &colSpan);
if (cellFrame) {
const nsStylePosition* cellStylePos = cellFrame->StylePosition();
styleISize = &cellStylePos->ISize(wm);
if (styleISize->ConvertsToLength() || styleISize->IsMaxContent() ||
styleISize->IsMinContent()) {
// XXX This should use real percentage padding
// Note that the difference between MinISize and PrefISize
// shouldn't matter for any of these values of styleISize; use
// MIN_ISIZE for symmetry with GetMinISize above, just in case
// there is a difference.
colISize = nsLayoutUtils::IntrinsicForContainer(
aReflowInput.mRenderingContext, cellFrame,
IntrinsicISizeType::MinISize);
} else if (styleISize->ConvertsToPercentage()) {
// XXX This should use real percentage padding
float pct = styleISize->ToPercentage();
colISize = NSToCoordFloor(pct * float(tableISize));
if (cellStylePos->mBoxSizing == StyleBoxSizing::Content) {
nsIFrame::IntrinsicSizeOffsetData offsets =
cellFrame->IntrinsicISizeOffsets();
colISize += offsets.padding + offsets.border;
}
pct /= float(colSpan);
colFrame->AddPrefPercent(pct);
pctTotal += pct;
} else {
// 'auto', '-moz-available', '-moz-fit-content', and 'calc()'
// with percentages
colISize = unassignedMarker;
}
if (colISize != unassignedMarker) {
if (colSpan > 1) {
// If a column-spanning cell is in the first row, split up
// the space evenly. (XXX This isn't quite right if some of
// the columns it's in have specified iSizes. Should we
// care?)
nscoord spacing = mTableFrame->GetColSpacing(col);
colISize = ((colISize + spacing) / colSpan) - spacing;
if (colISize < 0) {
colISize = 0;
}
}
if (!styleISize->ConvertsToPercentage()) {
specTotal += colISize;
}
}
} else {
colISize = unassignedMarker;
}
}
colFrame->SetFinalISize(colISize);
if (colISize == unassignedMarker) {
++unassignedCount;
} else {
unassignedSpace -= colISize;
}
}
if (unassignedSpace < 0) {
if (pctTotal > 0) {
// If the columns took up too much space, reduce those that had
// percentage inline sizes. The spec doesn't say to do this, but
// we've always done it in the past, and so does WinIE6.
nscoord pctUsed = NSToCoordFloor(pctTotal * float(tableISize));
nscoord reduce = std::min(pctUsed, -unassignedSpace);
float reduceRatio = float(reduce) / pctTotal;
for (int32_t col = 0; col < colCount; ++col) {
nsTableColFrame* colFrame = mTableFrame->GetColFrame(col);
if (!colFrame) {
NS_ERROR("column frames out of sync with cell map");
continue;
}
nscoord colISize = colFrame->GetFinalISize();
colISize -= NSToCoordFloor(colFrame->GetPrefPercent() * reduceRatio);
if (colISize < 0) {
colISize = 0;
}
colFrame->SetFinalISize(colISize);
}
}
unassignedSpace = 0;
}
if (unassignedCount > 0) {
// The spec says to distribute the remaining space evenly among
// the columns.
nscoord toAssign = unassignedSpace / unassignedCount;
for (int32_t col = 0; col < colCount; ++col) {
nsTableColFrame* colFrame = mTableFrame->GetColFrame(col);
if (!colFrame) {
NS_ERROR("column frames out of sync with cell map");
continue;
}
if (colFrame->GetFinalISize() == unassignedMarker) {
colFrame->SetFinalISize(toAssign);
}
}
} else if (unassignedSpace > 0) {
// The spec doesn't say how to distribute the unassigned space.
if (specTotal > 0) {
// Distribute proportionally to non-percentage columns.
nscoord specUndist = specTotal;
for (int32_t col = 0; col < colCount; ++col) {
nsTableColFrame* colFrame = mTableFrame->GetColFrame(col);
if (!colFrame) {
NS_ERROR("column frames out of sync with cell map");
continue;
}
if (colFrame->GetPrefPercent() == 0.0f) {
NS_ASSERTION(colFrame->GetFinalISize() <= specUndist,
"inline sizes don't add up");
nscoord toAdd = AllocateUnassigned(
unassignedSpace,
float(colFrame->GetFinalISize()) / float(specUndist));
specUndist -= colFrame->GetFinalISize();
colFrame->SetFinalISize(colFrame->GetFinalISize() + toAdd);
unassignedSpace -= toAdd;
if (specUndist <= 0) {
NS_ASSERTION(specUndist == 0, "math should be exact");
break;
}
}
}
NS_ASSERTION(unassignedSpace == 0, "failed to redistribute");
} else if (pctTotal > 0) {
// Distribute proportionally to percentage columns.
float pctUndist = pctTotal;
for (int32_t col = 0; col < colCount; ++col) {
nsTableColFrame* colFrame = mTableFrame->GetColFrame(col);
if (!colFrame) {
NS_ERROR("column frames out of sync with cell map");
continue;
}
if (pctUndist < colFrame->GetPrefPercent()) {
// This can happen with floating-point math.
NS_ASSERTION(colFrame->GetPrefPercent() - pctUndist < 0.0001,
"inline sizes don't add up");
pctUndist = colFrame->GetPrefPercent();
}
nscoord toAdd = AllocateUnassigned(
unassignedSpace, colFrame->GetPrefPercent() / pctUndist);
colFrame->SetFinalISize(colFrame->GetFinalISize() + toAdd);
unassignedSpace -= toAdd;
pctUndist -= colFrame->GetPrefPercent();
if (pctUndist <= 0.0f) {
break;
}
}
NS_ASSERTION(unassignedSpace == 0, "failed to redistribute");
} else {
// Distribute equally to the zero-iSize columns.
int32_t colsRemaining = colCount;
for (int32_t col = 0; col < colCount; ++col) {
nsTableColFrame* colFrame = mTableFrame->GetColFrame(col);
if (!colFrame) {
NS_ERROR("column frames out of sync with cell map");
continue;
}
NS_ASSERTION(colFrame->GetFinalISize() == 0, "yikes");
nscoord toAdd =
AllocateUnassigned(unassignedSpace, 1.0f / float(colsRemaining));
colFrame->SetFinalISize(toAdd);
unassignedSpace -= toAdd;
--colsRemaining;
}
NS_ASSERTION(unassignedSpace == 0, "failed to redistribute");
}
}
for (int32_t col = 0; col < colCount; ++col) {
nsTableColFrame* colFrame = mTableFrame->GetColFrame(col);
if (!colFrame) {
NS_ERROR("column frames out of sync with cell map");
continue;
}
if (oldColISizes.ElementAt(col) != colFrame->GetFinalISize()) {
mTableFrame->DidResizeColumns();
break;
}
}
}