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