|
| 1 | +/* |
| 2 | + * Copyright (c) Meta Platforms, Inc. and affiliates. |
| 3 | + * |
| 4 | + * This source code is licensed under the MIT license found in the |
| 5 | + * LICENSE file in the root directory of this source tree. |
| 6 | + */ |
| 7 | + |
| 8 | +#include <yoga/algorithm/grid/AutoPlacement.h> |
| 9 | + |
| 10 | +#include <yoga/algorithm/grid/LinePlacement.h> |
| 11 | +#include <yoga/algorithm/grid/OccupancyGrid.h> |
| 12 | +#include <yoga/debug/AssertFatal.h> |
| 13 | +#include <yoga/node/Node.h> |
| 14 | +#include <yoga/style/GridLine.h> |
| 15 | + |
| 16 | +#include <algorithm> |
| 17 | +#include <array> |
| 18 | +#include <cstddef> |
| 19 | +#include <cstdint> |
| 20 | +#include <unordered_map> |
| 21 | +#include <unordered_set> |
| 22 | +#include <utility> |
| 23 | +#include <vector> |
| 24 | + |
| 25 | +namespace facebook::yoga { |
| 26 | + |
| 27 | +namespace { |
| 28 | + |
| 29 | +bool isPlaceable(const Node* child) { |
| 30 | + return child->style().positionType() != PositionType::Absolute && |
| 31 | + child->style().display() != Display::None; |
| 32 | +} |
| 33 | + |
| 34 | +} // namespace |
| 35 | + |
| 36 | +// 8.5. Grid Item Placement Algorithm |
| 37 | +// https://www.w3.org/TR/css-grid-1/#auto-placement-algo |
| 38 | +AutoPlacement AutoPlacement::performAutoPlacement(Node* node) { |
| 39 | + std::vector<AutoPlacementItem> items; |
| 40 | + items.reserve(node->getChildCount()); |
| 41 | + std::unordered_set<Node*> placedItems; |
| 42 | + placedItems.reserve(node->getChildCount()); |
| 43 | + |
| 44 | + int32_t minColumnStart = 0; |
| 45 | + int32_t minRowStart = 0; |
| 46 | + int32_t maxColumnEnd = |
| 47 | + static_cast<int32_t>(node->style().gridTemplateColumns().size()); |
| 48 | + int32_t maxRowEnd = |
| 49 | + static_cast<int32_t>(node->style().gridTemplateRows().size()); |
| 50 | + OccupancyGrid occupancy; |
| 51 | + occupancy.occupied.reserve(node->getChildCount()); |
| 52 | + |
| 53 | + // Records a placed grid area and updates the running grid bounds. |
| 54 | + const auto recordGridArea = [&](const AutoPlacementItem& area) { |
| 55 | + yoga::assertFatal( |
| 56 | + area.columnEnd > area.columnStart, |
| 57 | + "Grid item column end must be greater than column start"); |
| 58 | + yoga::assertFatal( |
| 59 | + area.rowEnd > area.rowStart, |
| 60 | + "Grid item row end must be greater than row start"); |
| 61 | + items.push_back(area); |
| 62 | + placedItems.insert(area.node); |
| 63 | + occupancy.markOccupied( |
| 64 | + area.rowStart, area.rowEnd, area.columnStart, area.columnEnd); |
| 65 | + minColumnStart = std::min(minColumnStart, area.columnStart); |
| 66 | + minRowStart = std::min(minRowStart, area.rowStart); |
| 67 | + maxColumnEnd = std::max(maxColumnEnd, area.columnEnd); |
| 68 | + maxRowEnd = std::max(maxRowEnd, area.rowEnd); |
| 69 | + }; |
| 70 | + |
| 71 | + const auto explicitColumnLineCount = |
| 72 | + static_cast<int32_t>(node->style().gridTemplateColumns().size() + 1); |
| 73 | + const auto explicitRowLineCount = |
| 74 | + static_cast<int32_t>(node->style().gridTemplateRows().size() + 1); |
| 75 | + |
| 76 | + // Step 1: Position anything that is not auto-positioned. In level 1 a span is |
| 77 | + // always definite (default 1), so an item is definitely positioned when both |
| 78 | + // its row and column have at least one integer line. |
| 79 | + for (Node* child : node->getLayoutChildren()) { |
| 80 | + if (!isPlaceable(child)) { |
| 81 | + continue; |
| 82 | + } |
| 83 | + |
| 84 | + const auto& gridItemColumnStart = child->style().gridColumnStart(); |
| 85 | + const auto& gridItemColumnEnd = child->style().gridColumnEnd(); |
| 86 | + const auto& gridItemRowStart = child->style().gridRowStart(); |
| 87 | + const auto& gridItemRowEnd = child->style().gridRowEnd(); |
| 88 | + const bool hasDefiniteColumn = isDefiniteLine(gridItemColumnStart) || |
| 89 | + isDefiniteLine(gridItemColumnEnd); |
| 90 | + const bool hasDefiniteRow = |
| 91 | + isDefiniteLine(gridItemRowStart) || isDefiniteLine(gridItemRowEnd); |
| 92 | + |
| 93 | + if (hasDefiniteColumn && hasDefiniteRow) { |
| 94 | + const auto columnPlacement = resolveLinePlacement( |
| 95 | + gridItemColumnStart, gridItemColumnEnd, explicitColumnLineCount); |
| 96 | + const auto rowPlacement = resolveLinePlacement( |
| 97 | + gridItemRowStart, gridItemRowEnd, explicitRowLineCount); |
| 98 | + |
| 99 | + recordGridArea( |
| 100 | + AutoPlacementItem{ |
| 101 | + .columnStart = columnPlacement.start, |
| 102 | + .columnEnd = columnPlacement.end, |
| 103 | + .rowStart = rowPlacement.start, |
| 104 | + .rowEnd = rowPlacement.end, |
| 105 | + .node = child}); |
| 106 | + } |
| 107 | + } |
| 108 | + |
| 109 | + // Step 2: Process the items locked to a given row (definite row, no definite |
| 110 | + // column), placing each into the first non-overlapping slot to its right. |
| 111 | + std::unordered_map<int32_t, int32_t> rowStartToColumnStartCache; |
| 112 | + for (Node* child : node->getLayoutChildren()) { |
| 113 | + if (!isPlaceable(child)) { |
| 114 | + continue; |
| 115 | + } |
| 116 | + |
| 117 | + const auto& gridItemColumnStart = child->style().gridColumnStart(); |
| 118 | + const auto& gridItemColumnEnd = child->style().gridColumnEnd(); |
| 119 | + const auto& gridItemRowStart = child->style().gridRowStart(); |
| 120 | + const auto& gridItemRowEnd = child->style().gridRowEnd(); |
| 121 | + const bool hasDefiniteColumn = isDefiniteLine(gridItemColumnStart) || |
| 122 | + isDefiniteLine(gridItemColumnEnd); |
| 123 | + const bool hasDefiniteRow = |
| 124 | + isDefiniteLine(gridItemRowStart) || isDefiniteLine(gridItemRowEnd); |
| 125 | + |
| 126 | + if (hasDefiniteRow && !hasDefiniteColumn) { |
| 127 | + const auto rowPlacement = resolveLinePlacement( |
| 128 | + gridItemRowStart, gridItemRowEnd, explicitRowLineCount); |
| 129 | + const auto rowStart = rowPlacement.start; |
| 130 | + const auto rowEnd = rowPlacement.end; |
| 131 | + |
| 132 | + const auto columnSpan = |
| 133 | + resolveLinePlacement( |
| 134 | + gridItemColumnStart, gridItemColumnEnd, explicitColumnLineCount) |
| 135 | + .span; |
| 136 | + |
| 137 | + auto columnStart = rowStartToColumnStartCache.contains(rowStart) |
| 138 | + ? rowStartToColumnStartCache[rowStart] |
| 139 | + : minColumnStart; |
| 140 | + auto columnEnd = columnStart + columnSpan; |
| 141 | + |
| 142 | + while (const auto* overlap = occupancy.hasOverlap( |
| 143 | + rowStart, rowEnd, columnStart, columnEnd)) { |
| 144 | + columnStart = overlap->columnEnd; |
| 145 | + columnEnd = columnStart + columnSpan; |
| 146 | + } |
| 147 | + |
| 148 | + recordGridArea( |
| 149 | + AutoPlacementItem{ |
| 150 | + .columnStart = columnStart, |
| 151 | + .columnEnd = columnEnd, |
| 152 | + .rowStart = rowStart, |
| 153 | + .rowEnd = rowEnd, |
| 154 | + .node = child}); |
| 155 | + rowStartToColumnStartCache[rowStart] = columnEnd; |
| 156 | + } |
| 157 | + } |
| 158 | + |
| 159 | + // Step 3: Determine the columns of the implicit grid, accounting for definite |
| 160 | + // columns that extend the grid and for the largest auto-placed column span. |
| 161 | + auto largestColumnSpan = 1; |
| 162 | + for (Node* child : node->getLayoutChildren()) { |
| 163 | + if (!isPlaceable(child)) { |
| 164 | + continue; |
| 165 | + } |
| 166 | + |
| 167 | + const auto& gridItemColumnStart = child->style().gridColumnStart(); |
| 168 | + const auto& gridItemColumnEnd = child->style().gridColumnEnd(); |
| 169 | + const bool hasDefiniteColumn = isDefiniteLine(gridItemColumnStart) || |
| 170 | + isDefiniteLine(gridItemColumnEnd); |
| 171 | + |
| 172 | + const auto columnPlacement = resolveLinePlacement( |
| 173 | + gridItemColumnStart, gridItemColumnEnd, explicitColumnLineCount); |
| 174 | + if (hasDefiniteColumn) { |
| 175 | + minColumnStart = std::min(minColumnStart, columnPlacement.start); |
| 176 | + maxColumnEnd = std::max(maxColumnEnd, columnPlacement.end); |
| 177 | + } else { |
| 178 | + largestColumnSpan = std::max(largestColumnSpan, columnPlacement.span); |
| 179 | + } |
| 180 | + } |
| 181 | + |
| 182 | + // If the largest span is wider than the current grid, extend the end. |
| 183 | + const auto currentGridWidth = maxColumnEnd - minColumnStart; |
| 184 | + if (largestColumnSpan > currentGridWidth) { |
| 185 | + maxColumnEnd = minColumnStart + largestColumnSpan; |
| 186 | + } |
| 187 | + |
| 188 | + // Step 4: Position the remaining grid items using the auto-placement cursor. |
| 189 | + std::array<int32_t, 2> autoPlacementCursor = {minColumnStart, minRowStart}; |
| 190 | + for (Node* child : node->getLayoutChildren()) { |
| 191 | + if (!isPlaceable(child) || placedItems.contains(child)) { |
| 192 | + continue; |
| 193 | + } |
| 194 | + |
| 195 | + const auto& gridItemColumnStart = child->style().gridColumnStart(); |
| 196 | + const auto& gridItemColumnEnd = child->style().gridColumnEnd(); |
| 197 | + const auto& gridItemRowStart = child->style().gridRowStart(); |
| 198 | + const auto& gridItemRowEnd = child->style().gridRowEnd(); |
| 199 | + const bool hasDefiniteColumn = isDefiniteLine(gridItemColumnStart) || |
| 200 | + isDefiniteLine(gridItemColumnEnd); |
| 201 | + const bool hasDefiniteRow = |
| 202 | + isDefiniteLine(gridItemRowStart) || isDefiniteLine(gridItemRowEnd); |
| 203 | + |
| 204 | + const auto columnPlacement = resolveLinePlacement( |
| 205 | + gridItemColumnStart, gridItemColumnEnd, explicitColumnLineCount); |
| 206 | + const auto rowPlacement = resolveLinePlacement( |
| 207 | + gridItemRowStart, gridItemRowEnd, explicitRowLineCount); |
| 208 | + |
| 209 | + // If the item has a definite column position, keep it and search downward |
| 210 | + // for a row where it does not overlap an occupied cell. |
| 211 | + if (hasDefiniteColumn) { |
| 212 | + const auto columnStart = columnPlacement.start; |
| 213 | + const auto columnEnd = columnPlacement.end; |
| 214 | + |
| 215 | + // Set the cursor to the item's column-start line; if that moves the |
| 216 | + // cursor backwards, advance to the next row. |
| 217 | + const auto previousColumnPosition = autoPlacementCursor[0]; |
| 218 | + autoPlacementCursor[0] = columnStart; |
| 219 | + if (autoPlacementCursor[0] < previousColumnPosition) { |
| 220 | + autoPlacementCursor[1]++; |
| 221 | + } |
| 222 | + |
| 223 | + const auto rowSpan = rowPlacement.span; |
| 224 | + while (const auto* overlap = occupancy.hasOverlap( |
| 225 | + autoPlacementCursor[1], |
| 226 | + autoPlacementCursor[1] + rowSpan, |
| 227 | + columnStart, |
| 228 | + columnEnd)) { |
| 229 | + autoPlacementCursor[1] = overlap->rowEnd; |
| 230 | + } |
| 231 | + |
| 232 | + recordGridArea( |
| 233 | + AutoPlacementItem{ |
| 234 | + .columnStart = columnStart, |
| 235 | + .columnEnd = columnEnd, |
| 236 | + .rowStart = autoPlacementCursor[1], |
| 237 | + .rowEnd = autoPlacementCursor[1] + rowSpan, |
| 238 | + .node = child}); |
| 239 | + } |
| 240 | + // If the item has an automatic position in both axes, sweep the cursor |
| 241 | + // left-to-right then top-to-bottom until it fits. |
| 242 | + else if (!hasDefiniteRow) { |
| 243 | + const auto itemColumnSpan = columnPlacement.span; |
| 244 | + const auto itemRowSpan = rowPlacement.span; |
| 245 | + |
| 246 | + bool foundPosition = false; |
| 247 | + while (!foundPosition) { |
| 248 | + while (autoPlacementCursor[0] + itemColumnSpan <= maxColumnEnd) { |
| 249 | + const auto columnStart = autoPlacementCursor[0]; |
| 250 | + const auto columnEnd = columnStart + itemColumnSpan; |
| 251 | + const auto rowStart = autoPlacementCursor[1]; |
| 252 | + const auto rowEnd = rowStart + itemRowSpan; |
| 253 | + |
| 254 | + if (const auto* overlap = occupancy.hasOverlap( |
| 255 | + rowStart, rowEnd, columnStart, columnEnd)) { |
| 256 | + autoPlacementCursor[0] = overlap->columnEnd; |
| 257 | + } else { |
| 258 | + recordGridArea( |
| 259 | + AutoPlacementItem{ |
| 260 | + .columnStart = columnStart, |
| 261 | + .columnEnd = columnEnd, |
| 262 | + .rowStart = rowStart, |
| 263 | + .rowEnd = rowEnd, |
| 264 | + .node = child}); |
| 265 | + foundPosition = true; |
| 266 | + break; |
| 267 | + } |
| 268 | + } |
| 269 | + |
| 270 | + if (!foundPosition) { |
| 271 | + // Cursor overflowed the grid width; move to the next row. |
| 272 | + autoPlacementCursor[1]++; |
| 273 | + autoPlacementCursor[0] = minColumnStart; |
| 274 | + } |
| 275 | + } |
| 276 | + } |
| 277 | + } |
| 278 | + |
| 279 | + return AutoPlacement{ |
| 280 | + .gridItems = std::move(items), |
| 281 | + .minColumnStart = minColumnStart, |
| 282 | + .minRowStart = minRowStart, |
| 283 | + .maxColumnEnd = maxColumnEnd, |
| 284 | + .maxRowEnd = maxRowEnd}; |
| 285 | +} |
| 286 | + |
| 287 | +// 1. Runs the grid placement algorithm and normalizes the output into the |
| 288 | +// 0-based coordinate space. |
| 289 | +// 2. Builds the baseline sharing groups for each row. |
| 290 | +ResolvedAutoPlacement ResolvedAutoPlacement::resolveGridItemPlacements( |
| 291 | + Node* node) { |
| 292 | + const AutoPlacement autoPlacement = AutoPlacement::performAutoPlacement(node); |
| 293 | + |
| 294 | + const int32_t minColumnStart = autoPlacement.minColumnStart; |
| 295 | + const int32_t minRowStart = autoPlacement.minRowStart; |
| 296 | + |
| 297 | + ResolvedAutoPlacement resolved; |
| 298 | + resolved.minColumnStart = minColumnStart; |
| 299 | + resolved.minRowStart = minRowStart; |
| 300 | + resolved.maxColumnEnd = autoPlacement.maxColumnEnd; |
| 301 | + resolved.maxRowEnd = autoPlacement.maxRowEnd; |
| 302 | + |
| 303 | + resolved.gridItems.reserve(autoPlacement.gridItems.size()); |
| 304 | + |
| 305 | + const Align alignItems = node->style().alignItems(); |
| 306 | + |
| 307 | + for (const auto& placement : autoPlacement.gridItems) { |
| 308 | + resolved.gridItems.emplace_back( |
| 309 | + static_cast<size_t>(placement.columnStart - minColumnStart), |
| 310 | + static_cast<size_t>(placement.columnEnd - minColumnStart), |
| 311 | + static_cast<size_t>(placement.rowStart - minRowStart), |
| 312 | + static_cast<size_t>(placement.rowEnd - minRowStart), |
| 313 | + placement.node); |
| 314 | + |
| 315 | + GridItem& item = resolved.gridItems.back(); |
| 316 | + Align alignSelf = item.node->style().alignSelf(); |
| 317 | + if (alignSelf == Align::Auto) { |
| 318 | + alignSelf = alignItems; |
| 319 | + } |
| 320 | + // https://www.w3.org/TR/css-grid-1/#algo-baseline-shims - only items that |
| 321 | + // span a single row participate in a row's baseline sharing group. |
| 322 | + const bool spansOneRow = (item.rowEnd - item.rowStart) == 1; |
| 323 | + if (alignSelf == Align::Baseline && spansOneRow) { |
| 324 | + resolved.baselineItemGroups[item.rowStart].push_back(&item); |
| 325 | + } |
| 326 | + } |
| 327 | + |
| 328 | + return resolved; |
| 329 | +} |
| 330 | + |
| 331 | +} // namespace facebook::yoga |
0 commit comments