Skip to content

Commit 30a5aca

Browse files
CSS Grid algorithm 2/N: auto-placement
Summary: Implements the CSS Grid auto placement algorithm ([§8.5](https://www.w3.org/TR/css-grid-1/#auto-placement-algo)). ## How? - **AutoPlacement** — the four-step placement algorithm. - **LinePlacement** — grid-line resolution ([§8.3](https://www.w3.org/TR/css-grid-1/#grid-placement-errors)), including invalid-input handling: `line 0` is treated as `auto`, and `span 0` / negative spans clamp to `1`. - **OccupancyGrid** — overlap tracking for placed items. - Unit tests (line resolution), algorithm tests, and regression tests for the invalid-input cases. ## Not yet implemented These will be introduced later. Default `grid-auto-flow` is `row` so it won't be an issue. - **Dense packing** (`grid-auto-flow: dense`) - **Column flow** (`grid-auto-flow: column`) ## Notes - Stacked on react/yoga#1923 (data structures); until that lands this diff also shows its `GridItem.h` / `GridTrack.h` — those belong to react/yoga#1923. cc - rozele This a small chunk from react/yoga#1894 PR. X-link: react/yoga#1994 Differential Revision: D111722622 Pulled By: rozele
1 parent 09e669d commit 30a5aca

6 files changed

Lines changed: 603 additions & 1 deletion

File tree

packages/react-native/ReactCommon/yoga/yoga/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ include(${YOGA_ROOT}/cmake/project-defaults.cmake)
2020

2121
file(GLOB SOURCES CONFIGURE_DEPENDS
2222
${CMAKE_CURRENT_SOURCE_DIR}/*.cpp
23-
${CMAKE_CURRENT_SOURCE_DIR}/**/*.cpp)
23+
${CMAKE_CURRENT_SOURCE_DIR}/**/*.cpp
24+
${CMAKE_CURRENT_SOURCE_DIR}/algorithm/grid/*.cpp)
2425

2526
add_library(yogacore STATIC ${SOURCES})
2627

Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
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

Comments
 (0)