1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 // vim:cindent:ts=2:et:sw=2:
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7 /*
8 * Algorithms that determine column and table inline sizes used for
9 * CSS2's 'table-layout: fixed'.
10 */
11
12 #include "FixedTableLayoutStrategy.h"
13 #include "nsStyleConsts.h"
14 #include "nsTableFrame.h"
15 #include "nsTableColFrame.h"
16 #include "nsTableCellFrame.h"
17 #include "WritingModes.h"
18 #include <algorithm>
19
20 using namespace mozilla;
21
FixedTableLayoutStrategy(nsTableFrame * aTableFrame)22 FixedTableLayoutStrategy::FixedTableLayoutStrategy(nsTableFrame* aTableFrame)
23 : nsITableLayoutStrategy(nsITableLayoutStrategy::Fixed),
24 mTableFrame(aTableFrame) {
25 MarkIntrinsicISizesDirty();
26 }
27
28 /* virtual */
29 FixedTableLayoutStrategy::~FixedTableLayoutStrategy() = default;
30
31 /* virtual */
GetMinISize(gfxContext * aRenderingContext)32 nscoord FixedTableLayoutStrategy::GetMinISize(gfxContext* aRenderingContext) {
33 DISPLAY_MIN_INLINE_SIZE(mTableFrame, mMinISize);
34 if (mMinISize != NS_INTRINSIC_ISIZE_UNKNOWN) {
35 return mMinISize;
36 }
37
38 // It's theoretically possible to do something much better here that
39 // depends only on the columns and the first row (where we look at
40 // intrinsic inline sizes inside the first row and then reverse the
41 // algorithm to find the narrowest inline size that would hold all of
42 // those intrinsic inline sizes), but it wouldn't be compatible with
43 // other browsers, or with the use of GetMinISize by
44 // nsTableFrame::ComputeSize to determine the inline size of a fixed
45 // layout table, since CSS2.1 says:
46 // The width of the table is then the greater of the value of the
47 // 'width' property for the table element and the sum of the column
48 // widths (plus cell spacing or borders).
49
50 // XXX Should we really ignore 'min-inline-size' and 'max-inline-size'?
51 // XXX Should we really ignore inline sizes on column groups?
52
53 nsTableCellMap* cellMap = mTableFrame->GetCellMap();
54 int32_t colCount = cellMap->GetColCount();
55
56 nscoord result = 0;
57
58 if (colCount > 0) {
59 result += mTableFrame->GetColSpacing(-1, colCount);
60 }
61
62 WritingMode wm = mTableFrame->GetWritingMode();
63 for (int32_t col = 0; col < colCount; ++col) {
64 nsTableColFrame* colFrame = mTableFrame->GetColFrame(col);
65 if (!colFrame) {
66 NS_ERROR("column frames out of sync with cell map");
67 continue;
68 }
69 nscoord spacing = mTableFrame->GetColSpacing(col);
70 const auto* styleISize = &colFrame->StylePosition()->ISize(wm);
71 if (styleISize->ConvertsToLength()) {
72 result +=
73 colFrame->ComputeISizeValue(aRenderingContext, 0, 0, 0, *styleISize);
74 } else if (styleISize->ConvertsToPercentage()) {
75 // do nothing
76 } else {
77 NS_ASSERTION(styleISize->IsAuto() || styleISize->IsExtremumLength() ||
78 styleISize->HasLengthAndPercentage(),
79 "bad inline size");
80
81 // The 'table-layout: fixed' algorithm considers only cells in the
82 // first row.
83 bool originates;
84 int32_t colSpan;
85 nsTableCellFrame* cellFrame =
86 cellMap->GetCellInfoAt(0, col, &originates, &colSpan);
87 if (cellFrame) {
88 styleISize = &cellFrame->StylePosition()->ISize(wm);
89 if (styleISize->ConvertsToLength() ||
90 (styleISize->IsExtremumLength() &&
91 (styleISize->AsExtremumLength() ==
92 StyleExtremumLength::MaxContent ||
93 styleISize->AsExtremumLength() ==
94 StyleExtremumLength::MinContent))) {
95 nscoord cellISize = nsLayoutUtils::IntrinsicForContainer(
96 aRenderingContext, cellFrame, nsLayoutUtils::MIN_ISIZE);
97 if (colSpan > 1) {
98 // If a column-spanning cell is in the first row, split up
99 // the space evenly. (XXX This isn't quite right if some of
100 // the columns it's in have specified inline sizes. Should
101 // we care?)
102 cellISize = ((cellISize + spacing) / colSpan) - spacing;
103 }
104 result += cellISize;
105 } else if (styleISize->ConvertsToPercentage()) {
106 if (colSpan > 1) {
107 // XXX Can this force columns to negative inline sizes?
108 result -= spacing * (colSpan - 1);
109 }
110 }
111 // else, for 'auto', '-moz-available', '-moz-fit-content',
112 // and 'calc()' with both lengths and percentages, do nothing
113 }
114 }
115 }
116
117 return (mMinISize = result);
118 }
119
120 /* virtual */
GetPrefISize(gfxContext * aRenderingContext,bool aComputingSize)121 nscoord FixedTableLayoutStrategy::GetPrefISize(gfxContext* aRenderingContext,
122 bool aComputingSize) {
123 // It's theoretically possible to do something much better here that
124 // depends only on the columns and the first row (where we look at
125 // intrinsic inline sizes inside the first row and then reverse the
126 // algorithm to find the narrowest inline size that would hold all of
127 // those intrinsic inline sizes), but it wouldn't be compatible with
128 // other browsers.
129 nscoord result = nscoord_MAX;
130 DISPLAY_PREF_INLINE_SIZE(mTableFrame, result);
131 return result;
132 }
133
134 /* virtual */
MarkIntrinsicISizesDirty()135 void FixedTableLayoutStrategy::MarkIntrinsicISizesDirty() {
136 mMinISize = NS_INTRINSIC_ISIZE_UNKNOWN;
137 mLastCalcISize = nscoord_MIN;
138 }
139
AllocateUnassigned(nscoord aUnassignedSpace,float aShare)140 static inline nscoord AllocateUnassigned(nscoord aUnassignedSpace,
141 float aShare) {
142 if (aShare == 1.0f) {
143 // This happens when the numbers we're dividing to get aShare are
144 // equal. We want to return unassignedSpace exactly, even if it
145 // can't be precisely round-tripped through float.
146 return aUnassignedSpace;
147 }
148 return NSToCoordRound(float(aUnassignedSpace) * aShare);
149 }
150
151 /* virtual */
ComputeColumnISizes(const ReflowInput & aReflowInput)152 void FixedTableLayoutStrategy::ComputeColumnISizes(
153 const ReflowInput& aReflowInput) {
154 nscoord tableISize = aReflowInput.ComputedISize();
155
156 if (mLastCalcISize == tableISize) {
157 return;
158 }
159 mLastCalcISize = tableISize;
160
161 nsTableCellMap* cellMap = mTableFrame->GetCellMap();
162 int32_t colCount = cellMap->GetColCount();
163
164 if (colCount == 0) {
165 // No Columns - nothing to compute
166 return;
167 }
168
169 // border-spacing isn't part of the basis for percentages.
170 tableISize -= mTableFrame->GetColSpacing(-1, colCount);
171
172 // store the old column inline sizes. We might call SetFinalISize
173 // multiple times on the columns, due to this we can't compare at the
174 // last call that the inline size has changed with respect to the last
175 // call to ComputeColumnISizes. In order to overcome this we store the
176 // old values in this array. A single call to SetFinalISize would make
177 // it possible to call GetFinalISize before and to compare when
178 // setting the final inline size.
179 nsTArray<nscoord> oldColISizes;
180
181 // XXX This ignores the 'min-width' and 'max-width' properties
182 // throughout. Then again, that's what the CSS spec says to do.
183
184 // XXX Should we really ignore widths on column groups?
185
186 uint32_t unassignedCount = 0;
187 nscoord unassignedSpace = tableISize;
188 const nscoord unassignedMarker = nscoord_MIN;
189
190 // We use the PrefPercent on the columns to store the percentages
191 // used to compute column inline sizes in case we need to shrink or
192 // expand the columns.
193 float pctTotal = 0.0f;
194
195 // Accumulate the total specified (non-percent) on the columns for
196 // distributing excess inline size to the columns.
197 nscoord specTotal = 0;
198
199 WritingMode wm = mTableFrame->GetWritingMode();
200 for (int32_t col = 0; col < colCount; ++col) {
201 nsTableColFrame* colFrame = mTableFrame->GetColFrame(col);
202 if (!colFrame) {
203 oldColISizes.AppendElement(0);
204 NS_ERROR("column frames out of sync with cell map");
205 continue;
206 }
207 oldColISizes.AppendElement(colFrame->GetFinalISize());
208 colFrame->ResetPrefPercent();
209 const auto* styleISize = &colFrame->StylePosition()->ISize(wm);
210 nscoord colISize;
211 if (styleISize->ConvertsToLength()) {
212 colISize = colFrame->ComputeISizeValue(aReflowInput.mRenderingContext, 0,
213 0, 0, *styleISize);
214 specTotal += colISize;
215 } else if (styleISize->ConvertsToPercentage()) {
216 float pct = styleISize->ToPercentage();
217 colISize = NSToCoordFloor(pct * float(tableISize));
218 colFrame->AddPrefPercent(pct);
219 pctTotal += pct;
220 } else {
221 NS_ASSERTION(styleISize->IsAuto() || styleISize->IsExtremumLength() ||
222 (styleISize->IsLengthPercentage() &&
223 !styleISize->ConvertsToLength()),
224 "bad inline size");
225
226 // The 'table-layout: fixed' algorithm considers only cells in the
227 // first row.
228 bool originates;
229 int32_t colSpan;
230 nsTableCellFrame* cellFrame =
231 cellMap->GetCellInfoAt(0, col, &originates, &colSpan);
232 if (cellFrame) {
233 const nsStylePosition* cellStylePos = cellFrame->StylePosition();
234 styleISize = &cellStylePos->ISize(wm);
235 if (styleISize->ConvertsToLength() ||
236 (styleISize->IsExtremumLength() &&
237 (styleISize->AsExtremumLength() ==
238 StyleExtremumLength::MaxContent ||
239 styleISize->AsExtremumLength() ==
240 StyleExtremumLength::MinContent))) {
241 // XXX This should use real percentage padding
242 // Note that the difference between MIN_ISIZE and PREF_ISIZE
243 // shouldn't matter for any of these values of styleISize; use
244 // MIN_ISIZE for symmetry with GetMinISize above, just in case
245 // there is a difference.
246 colISize = nsLayoutUtils::IntrinsicForContainer(
247 aReflowInput.mRenderingContext, cellFrame,
248 nsLayoutUtils::MIN_ISIZE);
249 } else if (styleISize->ConvertsToPercentage()) {
250 // XXX This should use real percentage padding
251 float pct = styleISize->ToPercentage();
252 colISize = NSToCoordFloor(pct * float(tableISize));
253
254 if (cellStylePos->mBoxSizing == StyleBoxSizing::Content) {
255 nsIFrame::IntrinsicSizeOffsetData offsets =
256 cellFrame->IntrinsicISizeOffsets();
257 colISize += offsets.padding + offsets.border;
258 }
259
260 pct /= float(colSpan);
261 colFrame->AddPrefPercent(pct);
262 pctTotal += pct;
263 } else {
264 // 'auto', '-moz-available', '-moz-fit-content', and 'calc()'
265 // with percentages
266 colISize = unassignedMarker;
267 }
268 if (colISize != unassignedMarker) {
269 if (colSpan > 1) {
270 // If a column-spanning cell is in the first row, split up
271 // the space evenly. (XXX This isn't quite right if some of
272 // the columns it's in have specified iSizes. Should we
273 // care?)
274 nscoord spacing = mTableFrame->GetColSpacing(col);
275 colISize = ((colISize + spacing) / colSpan) - spacing;
276 if (colISize < 0) {
277 colISize = 0;
278 }
279 }
280 if (!styleISize->ConvertsToPercentage()) {
281 specTotal += colISize;
282 }
283 }
284 } else {
285 colISize = unassignedMarker;
286 }
287 }
288
289 colFrame->SetFinalISize(colISize);
290
291 if (colISize == unassignedMarker) {
292 ++unassignedCount;
293 } else {
294 unassignedSpace -= colISize;
295 }
296 }
297
298 if (unassignedSpace < 0) {
299 if (pctTotal > 0) {
300 // If the columns took up too much space, reduce those that had
301 // percentage inline sizes. The spec doesn't say to do this, but
302 // we've always done it in the past, and so does WinIE6.
303 nscoord pctUsed = NSToCoordFloor(pctTotal * float(tableISize));
304 nscoord reduce = std::min(pctUsed, -unassignedSpace);
305 float reduceRatio = float(reduce) / pctTotal;
306 for (int32_t col = 0; col < colCount; ++col) {
307 nsTableColFrame* colFrame = mTableFrame->GetColFrame(col);
308 if (!colFrame) {
309 NS_ERROR("column frames out of sync with cell map");
310 continue;
311 }
312 nscoord colISize = colFrame->GetFinalISize();
313 colISize -= NSToCoordFloor(colFrame->GetPrefPercent() * reduceRatio);
314 if (colISize < 0) {
315 colISize = 0;
316 }
317 colFrame->SetFinalISize(colISize);
318 }
319 }
320 unassignedSpace = 0;
321 }
322
323 if (unassignedCount > 0) {
324 // The spec says to distribute the remaining space evenly among
325 // the columns.
326 nscoord toAssign = unassignedSpace / unassignedCount;
327 for (int32_t col = 0; col < colCount; ++col) {
328 nsTableColFrame* colFrame = mTableFrame->GetColFrame(col);
329 if (!colFrame) {
330 NS_ERROR("column frames out of sync with cell map");
331 continue;
332 }
333 if (colFrame->GetFinalISize() == unassignedMarker) {
334 colFrame->SetFinalISize(toAssign);
335 }
336 }
337 } else if (unassignedSpace > 0) {
338 // The spec doesn't say how to distribute the unassigned space.
339 if (specTotal > 0) {
340 // Distribute proportionally to non-percentage columns.
341 nscoord specUndist = specTotal;
342 for (int32_t col = 0; col < colCount; ++col) {
343 nsTableColFrame* colFrame = mTableFrame->GetColFrame(col);
344 if (!colFrame) {
345 NS_ERROR("column frames out of sync with cell map");
346 continue;
347 }
348 if (colFrame->GetPrefPercent() == 0.0f) {
349 NS_ASSERTION(colFrame->GetFinalISize() <= specUndist,
350 "inline sizes don't add up");
351 nscoord toAdd = AllocateUnassigned(
352 unassignedSpace,
353 float(colFrame->GetFinalISize()) / float(specUndist));
354 specUndist -= colFrame->GetFinalISize();
355 colFrame->SetFinalISize(colFrame->GetFinalISize() + toAdd);
356 unassignedSpace -= toAdd;
357 if (specUndist <= 0) {
358 NS_ASSERTION(specUndist == 0, "math should be exact");
359 break;
360 }
361 }
362 }
363 NS_ASSERTION(unassignedSpace == 0, "failed to redistribute");
364 } else if (pctTotal > 0) {
365 // Distribute proportionally to percentage columns.
366 float pctUndist = pctTotal;
367 for (int32_t col = 0; col < colCount; ++col) {
368 nsTableColFrame* colFrame = mTableFrame->GetColFrame(col);
369 if (!colFrame) {
370 NS_ERROR("column frames out of sync with cell map");
371 continue;
372 }
373 if (pctUndist < colFrame->GetPrefPercent()) {
374 // This can happen with floating-point math.
375 NS_ASSERTION(colFrame->GetPrefPercent() - pctUndist < 0.0001,
376 "inline sizes don't add up");
377 pctUndist = colFrame->GetPrefPercent();
378 }
379 nscoord toAdd = AllocateUnassigned(
380 unassignedSpace, colFrame->GetPrefPercent() / pctUndist);
381 colFrame->SetFinalISize(colFrame->GetFinalISize() + toAdd);
382 unassignedSpace -= toAdd;
383 pctUndist -= colFrame->GetPrefPercent();
384 if (pctUndist <= 0.0f) {
385 break;
386 }
387 }
388 NS_ASSERTION(unassignedSpace == 0, "failed to redistribute");
389 } else {
390 // Distribute equally to the zero-iSize columns.
391 int32_t colsRemaining = colCount;
392 for (int32_t col = 0; col < colCount; ++col) {
393 nsTableColFrame* colFrame = mTableFrame->GetColFrame(col);
394 if (!colFrame) {
395 NS_ERROR("column frames out of sync with cell map");
396 continue;
397 }
398 NS_ASSERTION(colFrame->GetFinalISize() == 0, "yikes");
399 nscoord toAdd =
400 AllocateUnassigned(unassignedSpace, 1.0f / float(colsRemaining));
401 colFrame->SetFinalISize(toAdd);
402 unassignedSpace -= toAdd;
403 --colsRemaining;
404 }
405 NS_ASSERTION(unassignedSpace == 0, "failed to redistribute");
406 }
407 }
408 for (int32_t col = 0; col < colCount; ++col) {
409 nsTableColFrame* colFrame = mTableFrame->GetColFrame(col);
410 if (!colFrame) {
411 NS_ERROR("column frames out of sync with cell map");
412 continue;
413 }
414 if (oldColISizes.ElementAt(col) != colFrame->GetFinalISize()) {
415 mTableFrame->DidResizeColumns();
416 break;
417 }
418 }
419 }
420