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