From 5482fb200e45af900086cde36c34f1349eace98a Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Sat, 21 Feb 2026 21:54:05 -0800 Subject: [PATCH 1/7] CSS Grid 1/9: Grid style types and public API Summary: Add the foundational data types, enums, style properties, and C API for expressing CSS Grid layouts in Yoga. Includes: - Grid style types (GridLine.h, GridTrack.h, GridTrackType.h) - Updated enums (Display::Grid, Align::Start/End, Justify::Auto/Stretch/Start/End) - Grid event (LayoutPassReason::kGridLayout) - Style property accessors and member variables - Public C API (YGGridTrackList.h/cpp, YGNodeStyle grid setters/getters) - Layout helpers updated for new enum values (Align.h, AbsoluteLayout.cpp, CalculateLayout.cpp/h partial) - Node.h: relativePosition made public - React Native mirror of all C++ changes Differential Revision: D93946262 --- enums.py | 9 +- java/com/facebook/yoga/YogaAlign.java | 6 +- java/com/facebook/yoga/YogaDisplay.java | 4 +- java/com/facebook/yoga/YogaGridTrackType.java | 39 ++++ java/com/facebook/yoga/YogaJustify.java | 32 ++-- javascript/src/generated/YGEnums.ts | 39 +++- yoga/YGEnums.cpp | 30 +++ yoga/YGEnums.h | 21 +- yoga/YGGridTrackList.cpp | 179 ++++++++++++++++++ yoga/YGGridTrackList.h | 96 ++++++++++ yoga/YGNodeStyle.cpp | 100 ++++++++++ yoga/YGNodeStyle.h | 32 +++- yoga/Yoga.h | 1 + yoga/algorithm/AbsoluteLayout.cpp | 48 ++++- yoga/algorithm/Align.h | 13 +- yoga/algorithm/CalculateLayout.cpp | 24 ++- yoga/algorithm/CalculateLayout.h | 22 +++ yoga/enums/Align.h | 4 +- yoga/enums/Display.h | 3 +- yoga/enums/GridTrackType.h | 43 +++++ yoga/enums/Justify.h | 6 +- yoga/event/event.cpp | 2 + yoga/event/event.h | 1 + yoga/node/Node.h | 8 +- yoga/style/GridLine.h | 61 ++++++ yoga/style/GridTrack.h | 65 +++++++ yoga/style/Style.h | 112 ++++++++++- yoga/style/StyleSizeLength.h | 8 +- 28 files changed, 960 insertions(+), 48 deletions(-) create mode 100644 java/com/facebook/yoga/YogaGridTrackType.java create mode 100644 yoga/YGGridTrackList.cpp create mode 100644 yoga/YGGridTrackList.h create mode 100644 yoga/enums/GridTrackType.h create mode 100644 yoga/style/GridLine.h create mode 100644 yoga/style/GridTrack.h diff --git a/enums.py b/enums.py index d43dd6c147..2c53672a2e 100755 --- a/enums.py +++ b/enums.py @@ -19,12 +19,16 @@ ], "FlexDirection": ["Column", "ColumnReverse", "Row", "RowReverse"], "Justify": [ + "Auto", "FlexStart", "Center", "FlexEnd", "SpaceBetween", "SpaceAround", "SpaceEvenly", + "Stretch", + "Start", + "End", ], "Overflow": ["Visible", "Hidden", "Scroll"], "Align": [ @@ -37,9 +41,11 @@ "SpaceBetween", "SpaceAround", "SpaceEvenly", + "Start", + "End", ], "PositionType": ["Static", "Relative", "Absolute"], - "Display": ["Flex", "None", "Contents"], + "Display": ["Flex", "None", "Contents", "Grid"], "Wrap": ["NoWrap", "Wrap", "WrapReverse"], "BoxSizing": ["BorderBox", "ContentBox"], "MeasureMode": ["Undefined", "Exactly", "AtMost"], @@ -62,6 +68,7 @@ "WebFlexBasis", ], "Gutter": ["Column", "Row", "All"], + "GridTrackType": ["Auto", "Points", "Percent", "Fr", "Minmax"], # Known incorrect behavior which can be enabled for compatibility "Errata": [ # Default: Standards conformant mode diff --git a/java/com/facebook/yoga/YogaAlign.java b/java/com/facebook/yoga/YogaAlign.java index 00535154fc..6f1c456581 100644 --- a/java/com/facebook/yoga/YogaAlign.java +++ b/java/com/facebook/yoga/YogaAlign.java @@ -18,7 +18,9 @@ public enum YogaAlign { BASELINE(5), SPACE_BETWEEN(6), SPACE_AROUND(7), - SPACE_EVENLY(8); + SPACE_EVENLY(8), + START(9), + END(10); private final int mIntValue; @@ -41,6 +43,8 @@ public static YogaAlign fromInt(int value) { case 6: return SPACE_BETWEEN; case 7: return SPACE_AROUND; case 8: return SPACE_EVENLY; + case 9: return START; + case 10: return END; default: throw new IllegalArgumentException("Unknown enum value: " + value); } } diff --git a/java/com/facebook/yoga/YogaDisplay.java b/java/com/facebook/yoga/YogaDisplay.java index 4dae871936..8e7e0f83cd 100644 --- a/java/com/facebook/yoga/YogaDisplay.java +++ b/java/com/facebook/yoga/YogaDisplay.java @@ -12,7 +12,8 @@ public enum YogaDisplay { FLEX(0), NONE(1), - CONTENTS(2); + CONTENTS(2), + GRID(3); private final int mIntValue; @@ -29,6 +30,7 @@ public static YogaDisplay fromInt(int value) { case 0: return FLEX; case 1: return NONE; case 2: return CONTENTS; + case 3: return GRID; default: throw new IllegalArgumentException("Unknown enum value: " + value); } } diff --git a/java/com/facebook/yoga/YogaGridTrackType.java b/java/com/facebook/yoga/YogaGridTrackType.java new file mode 100644 index 0000000000..e3d22d25be --- /dev/null +++ b/java/com/facebook/yoga/YogaGridTrackType.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// @generated by enums.py + +package com.facebook.yoga; + +public enum YogaGridTrackType { + AUTO(0), + POINTS(1), + PERCENT(2), + FR(3), + MINMAX(4); + + private final int mIntValue; + + YogaGridTrackType(int intValue) { + mIntValue = intValue; + } + + public int intValue() { + return mIntValue; + } + + public static YogaGridTrackType fromInt(int value) { + switch (value) { + case 0: return AUTO; + case 1: return POINTS; + case 2: return PERCENT; + case 3: return FR; + case 4: return MINMAX; + default: throw new IllegalArgumentException("Unknown enum value: " + value); + } + } +} diff --git a/java/com/facebook/yoga/YogaJustify.java b/java/com/facebook/yoga/YogaJustify.java index 4be1ed71d3..778238ec66 100644 --- a/java/com/facebook/yoga/YogaJustify.java +++ b/java/com/facebook/yoga/YogaJustify.java @@ -10,12 +10,16 @@ package com.facebook.yoga; public enum YogaJustify { - FLEX_START(0), - CENTER(1), - FLEX_END(2), - SPACE_BETWEEN(3), - SPACE_AROUND(4), - SPACE_EVENLY(5); + AUTO(0), + FLEX_START(1), + CENTER(2), + FLEX_END(3), + SPACE_BETWEEN(4), + SPACE_AROUND(5), + SPACE_EVENLY(6), + STRETCH(7), + START(8), + END(9); private final int mIntValue; @@ -29,12 +33,16 @@ public int intValue() { public static YogaJustify fromInt(int value) { switch (value) { - case 0: return FLEX_START; - case 1: return CENTER; - case 2: return FLEX_END; - case 3: return SPACE_BETWEEN; - case 4: return SPACE_AROUND; - case 5: return SPACE_EVENLY; + case 0: return AUTO; + case 1: return FLEX_START; + case 2: return CENTER; + case 3: return FLEX_END; + case 4: return SPACE_BETWEEN; + case 5: return SPACE_AROUND; + case 6: return SPACE_EVENLY; + case 7: return STRETCH; + case 8: return START; + case 9: return END; default: throw new IllegalArgumentException("Unknown enum value: " + value); } } diff --git a/javascript/src/generated/YGEnums.ts b/javascript/src/generated/YGEnums.ts index f389fe2fdf..f50aa88a73 100644 --- a/javascript/src/generated/YGEnums.ts +++ b/javascript/src/generated/YGEnums.ts @@ -17,6 +17,8 @@ export enum Align { SpaceBetween = 6, SpaceAround = 7, SpaceEvenly = 8, + Start = 9, + End = 10, } export enum BoxSizing { @@ -39,6 +41,7 @@ export enum Display { Flex = 0, None = 1, Contents = 2, + Grid = 3, } export enum Edge { @@ -73,6 +76,14 @@ export enum FlexDirection { RowReverse = 3, } +export enum GridTrackType { + Auto = 0, + Points = 1, + Percent = 2, + Fr = 3, + Minmax = 4, +} + export enum Gutter { Column = 0, Row = 1, @@ -80,12 +91,16 @@ export enum Gutter { } export enum Justify { - FlexStart = 0, - Center = 1, - FlexEnd = 2, - SpaceBetween = 3, - SpaceAround = 4, - SpaceEvenly = 5, + Auto = 0, + FlexStart = 1, + Center = 2, + FlexEnd = 3, + SpaceBetween = 4, + SpaceAround = 5, + SpaceEvenly = 6, + Stretch = 7, + Start = 8, + End = 9, } export enum LogLevel { @@ -146,6 +161,8 @@ const constants = { ALIGN_SPACE_BETWEEN: Align.SpaceBetween, ALIGN_SPACE_AROUND: Align.SpaceAround, ALIGN_SPACE_EVENLY: Align.SpaceEvenly, + ALIGN_START: Align.Start, + ALIGN_END: Align.End, BOX_SIZING_BORDER_BOX: BoxSizing.BorderBox, BOX_SIZING_CONTENT_BOX: BoxSizing.ContentBox, DIMENSION_WIDTH: Dimension.Width, @@ -156,6 +173,7 @@ const constants = { DISPLAY_FLEX: Display.Flex, DISPLAY_NONE: Display.None, DISPLAY_CONTENTS: Display.Contents, + DISPLAY_GRID: Display.Grid, EDGE_LEFT: Edge.Left, EDGE_TOP: Edge.Top, EDGE_RIGHT: Edge.Right, @@ -176,15 +194,24 @@ const constants = { FLEX_DIRECTION_COLUMN_REVERSE: FlexDirection.ColumnReverse, FLEX_DIRECTION_ROW: FlexDirection.Row, FLEX_DIRECTION_ROW_REVERSE: FlexDirection.RowReverse, + GRID_TRACK_TYPE_AUTO: GridTrackType.Auto, + GRID_TRACK_TYPE_POINTS: GridTrackType.Points, + GRID_TRACK_TYPE_PERCENT: GridTrackType.Percent, + GRID_TRACK_TYPE_FR: GridTrackType.Fr, + GRID_TRACK_TYPE_MINMAX: GridTrackType.Minmax, GUTTER_COLUMN: Gutter.Column, GUTTER_ROW: Gutter.Row, GUTTER_ALL: Gutter.All, + JUSTIFY_AUTO: Justify.Auto, JUSTIFY_FLEX_START: Justify.FlexStart, JUSTIFY_CENTER: Justify.Center, JUSTIFY_FLEX_END: Justify.FlexEnd, JUSTIFY_SPACE_BETWEEN: Justify.SpaceBetween, JUSTIFY_SPACE_AROUND: Justify.SpaceAround, JUSTIFY_SPACE_EVENLY: Justify.SpaceEvenly, + JUSTIFY_STRETCH: Justify.Stretch, + JUSTIFY_START: Justify.Start, + JUSTIFY_END: Justify.End, LOG_LEVEL_ERROR: LogLevel.Error, LOG_LEVEL_WARN: LogLevel.Warn, LOG_LEVEL_INFO: LogLevel.Info, diff --git a/yoga/YGEnums.cpp b/yoga/YGEnums.cpp index 4bdace6b7a..9fc4a83a82 100644 --- a/yoga/YGEnums.cpp +++ b/yoga/YGEnums.cpp @@ -29,6 +29,10 @@ const char* YGAlignToString(const YGAlign value) { return "space-around"; case YGAlignSpaceEvenly: return "space-evenly"; + case YGAlignStart: + return "start"; + case YGAlignEnd: + return "end"; } return "unknown"; } @@ -73,6 +77,8 @@ const char* YGDisplayToString(const YGDisplay value) { return "none"; case YGDisplayContents: return "contents"; + case YGDisplayGrid: + return "grid"; } return "unknown"; } @@ -141,6 +147,22 @@ const char* YGFlexDirectionToString(const YGFlexDirection value) { return "unknown"; } +const char* YGGridTrackTypeToString(const YGGridTrackType value) { + switch (value) { + case YGGridTrackTypeAuto: + return "auto"; + case YGGridTrackTypePoints: + return "points"; + case YGGridTrackTypePercent: + return "percent"; + case YGGridTrackTypeFr: + return "fr"; + case YGGridTrackTypeMinmax: + return "minmax"; + } + return "unknown"; +} + const char* YGGutterToString(const YGGutter value) { switch (value) { case YGGutterColumn: @@ -155,6 +177,8 @@ const char* YGGutterToString(const YGGutter value) { const char* YGJustifyToString(const YGJustify value) { switch (value) { + case YGJustifyAuto: + return "auto"; case YGJustifyFlexStart: return "flex-start"; case YGJustifyCenter: @@ -167,6 +191,12 @@ const char* YGJustifyToString(const YGJustify value) { return "space-around"; case YGJustifySpaceEvenly: return "space-evenly"; + case YGJustifyStretch: + return "stretch"; + case YGJustifyStart: + return "start"; + case YGJustifyEnd: + return "end"; } return "unknown"; } diff --git a/yoga/YGEnums.h b/yoga/YGEnums.h index bb83bcfac9..1b69f09318 100644 --- a/yoga/YGEnums.h +++ b/yoga/YGEnums.h @@ -22,7 +22,9 @@ YG_ENUM_DECL( YGAlignBaseline, YGAlignSpaceBetween, YGAlignSpaceAround, - YGAlignSpaceEvenly) + YGAlignSpaceEvenly, + YGAlignStart, + YGAlignEnd) YG_ENUM_DECL( YGBoxSizing, @@ -44,7 +46,8 @@ YG_ENUM_DECL( YGDisplay, YGDisplayFlex, YGDisplayNone, - YGDisplayContents) + YGDisplayContents, + YGDisplayGrid) YG_ENUM_DECL( YGEdge, @@ -79,6 +82,14 @@ YG_ENUM_DECL( YGFlexDirectionRow, YGFlexDirectionRowReverse) +YG_ENUM_DECL( + YGGridTrackType, + YGGridTrackTypeAuto, + YGGridTrackTypePoints, + YGGridTrackTypePercent, + YGGridTrackTypeFr, + YGGridTrackTypeMinmax) + YG_ENUM_DECL( YGGutter, YGGutterColumn, @@ -87,12 +98,16 @@ YG_ENUM_DECL( YG_ENUM_DECL( YGJustify, + YGJustifyAuto, YGJustifyFlexStart, YGJustifyCenter, YGJustifyFlexEnd, YGJustifySpaceBetween, YGJustifySpaceAround, - YGJustifySpaceEvenly) + YGJustifySpaceEvenly, + YGJustifyStretch, + YGJustifyStart, + YGJustifyEnd) YG_ENUM_DECL( YGLogLevel, diff --git a/yoga/YGGridTrackList.cpp b/yoga/YGGridTrackList.cpp new file mode 100644 index 0000000000..7417656758 --- /dev/null +++ b/yoga/YGGridTrackList.cpp @@ -0,0 +1,179 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include +#include +#include +#include +#include + +using namespace facebook::yoga; + +// Internal representation of a grid track value +struct YGGridTrackValue { + enum class Type { Points, Percent, Fr, Auto, MinMax }; + + Type type; + float value; + YGGridTrackValue* minValue; + YGGridTrackValue* maxValue; + + YGGridTrackValue(Type t, float v = 0.0f) + : type(t), value(v), minValue(nullptr), maxValue(nullptr) {} + + YGGridTrackValue(YGGridTrackValue* min, YGGridTrackValue* max) + : type(Type::MinMax), value(0.0f), minValue(min), maxValue(max) {} + + ~YGGridTrackValue() { + // MinMax owns its min/max values + if (type == Type::MinMax) { + delete minValue; + delete maxValue; + } + } + + StyleSizeLength toStyleSizeLength() const { + switch (type) { + case Type::Points: + return StyleSizeLength::points(value); + case Type::Percent: + return StyleSizeLength::percent(value); + case Type::Fr: + return StyleSizeLength::stretch(value); + case Type::Auto: + return StyleSizeLength::ofAuto(); + case Type::MinMax: + // MinMax should not call this, it needs special handling + return StyleSizeLength::ofAuto(); + } + return StyleSizeLength::ofAuto(); + } +}; + +// Internal representation of a grid track list +struct YGGridTrackList { + std::vector tracks; + + YGGridTrackList() = default; + YGGridTrackList(const YGGridTrackList&) = delete; + YGGridTrackList& operator=(const YGGridTrackList&) = delete; + YGGridTrackList(YGGridTrackList&&) = delete; + YGGridTrackList& operator=(YGGridTrackList&&) = delete; + + ~YGGridTrackList() { + for (auto* track : tracks) { + delete track; + } + } + + GridTrackList toGridTrackList() const { + GridTrackList result; + result.reserve(tracks.size()); + + for (auto* track : tracks) { + if (track->type == YGGridTrackValue::Type::MinMax) { + auto min = track->minValue->toStyleSizeLength(); + auto max = track->maxValue->toStyleSizeLength(); + result.push_back(GridTrackSize::minmax(min, max)); + } else { + switch (track->type) { + case YGGridTrackValue::Type::Points: + result.push_back(GridTrackSize::length(track->value)); + break; + case YGGridTrackValue::Type::Percent: + result.push_back(GridTrackSize::percent(track->value)); + break; + case YGGridTrackValue::Type::Fr: + result.push_back(GridTrackSize::fr(track->value)); + break; + case YGGridTrackValue::Type::Auto: + result.push_back(GridTrackSize::auto_()); + break; + case YGGridTrackValue::Type::MinMax: + // Already handled above + break; + } + } + } + + return result; + } +}; + +YGGridTrackListRef YGGridTrackListCreate() { + return new YGGridTrackList(); +} + +void YGGridTrackListFree(YGGridTrackListRef list) { + delete list; +} + +void YGGridTrackListAddTrack( + YGGridTrackListRef list, + YGGridTrackValueRef trackValue) { + if (list && trackValue) { + list->tracks.push_back(trackValue); + } +} + +YGGridTrackValueRef YGPoints(float points) { + return new YGGridTrackValue(YGGridTrackValue::Type::Points, points); +} + +YGGridTrackValueRef YGPercent(float percent) { + return new YGGridTrackValue(YGGridTrackValue::Type::Percent, percent); +} + +YGGridTrackValueRef YGFr(float fr) { + return new YGGridTrackValue(YGGridTrackValue::Type::Fr, fr); +} + +YGGridTrackValueRef YGAuto() { + return new YGGridTrackValue(YGGridTrackValue::Type::Auto); +} + +YGGridTrackValueRef YGMinMax(YGGridTrackValueRef min, YGGridTrackValueRef max) { + return new YGGridTrackValue(min, max); +} + +void YGNodeStyleSetGridTemplateRows( + YGNodeRef node, + YGGridTrackListRef trackList) { + if (node && trackList) { + auto* n = resolveRef(node); + n->style().setGridTemplateRows(trackList->toGridTrackList()); + n->markDirtyAndPropagate(); + } +} + +void YGNodeStyleSetGridTemplateColumns( + YGNodeRef node, + YGGridTrackListRef trackList) { + if (node && trackList) { + auto* n = resolveRef(node); + n->style().setGridTemplateColumns(trackList->toGridTrackList()); + n->markDirtyAndPropagate(); + } +} + +void YGNodeStyleSetGridAutoRows(YGNodeRef node, YGGridTrackListRef trackList) { + if (node && trackList) { + auto* n = resolveRef(node); + n->style().setGridAutoRows(trackList->toGridTrackList()); + n->markDirtyAndPropagate(); + } +} + +void YGNodeStyleSetGridAutoColumns( + YGNodeRef node, + YGGridTrackListRef trackList) { + if (node && trackList) { + auto* n = resolveRef(node); + n->style().setGridAutoColumns(trackList->toGridTrackList()); + n->markDirtyAndPropagate(); + } +} diff --git a/yoga/YGGridTrackList.h b/yoga/YGGridTrackList.h new file mode 100644 index 0000000000..a89e92cca6 --- /dev/null +++ b/yoga/YGGridTrackList.h @@ -0,0 +1,96 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +YG_EXTERN_C_BEGIN + +/** + * Opaque handle to a grid track list for building grid-template-rows/columns. + */ +typedef struct YGGridTrackList* YGGridTrackListRef; + +/** + * Opaque handle to a grid track value. + */ +typedef struct YGGridTrackValue* YGGridTrackValueRef; + +/** + * Create a new grid track list. + */ +YG_EXPORT YGGridTrackListRef YGGridTrackListCreate(void); + +/** + * Free a grid track list. + */ +YG_EXPORT void YGGridTrackListFree(YGGridTrackListRef list); + +/** + * Add a track to the grid track list. + */ +YG_EXPORT void YGGridTrackListAddTrack( + YGGridTrackListRef list, + YGGridTrackValueRef trackValue); + +/** + * Create a grid track value with a points (px) length. + */ +YG_EXPORT YGGridTrackValueRef YGPoints(float points); + +/** + * Create a grid track value with a percentage length. + */ +YG_EXPORT YGGridTrackValueRef YGPercent(float percent); + +/** + * Create a grid track value with a flexible (fr) length. + */ +YG_EXPORT YGGridTrackValueRef YGFr(float fr); + +/** + * Create a grid track value with auto sizing. + */ +YG_EXPORT YGGridTrackValueRef YGAuto(void); + +/** + * Create a grid track value with minmax(min, max) sizing. + */ +YG_EXPORT YGGridTrackValueRef +YGMinMax(YGGridTrackValueRef min, YGGridTrackValueRef max); + +/** + * Set the grid-template-rows property on a node. + */ +YG_EXPORT void YGNodeStyleSetGridTemplateRows( + YGNodeRef node, + YGGridTrackListRef trackList); + +/** + * Set the grid-template-columns property on a node. + */ +YG_EXPORT void YGNodeStyleSetGridTemplateColumns( + YGNodeRef node, + YGGridTrackListRef trackList); + +/** + * Set the grid-auto-rows property on a node. + */ +YG_EXPORT void YGNodeStyleSetGridAutoRows( + YGNodeRef node, + YGGridTrackListRef trackList); + +/** + * Set the grid-auto-columns property on a node. + */ +YG_EXPORT void YGNodeStyleSetGridAutoColumns( + YGNodeRef node, + YGGridTrackListRef trackList); + +YG_EXTERN_C_END diff --git a/yoga/YGNodeStyle.cpp b/yoga/YGNodeStyle.cpp index 4e77405b6c..f859c8427a 100644 --- a/yoga/YGNodeStyle.cpp +++ b/yoga/YGNodeStyle.cpp @@ -74,6 +74,24 @@ YGJustify YGNodeStyleGetJustifyContent(const YGNodeConstRef node) { return unscopedEnum(resolveRef(node)->style().justifyContent()); } +void YGNodeStyleSetJustifyItems(YGNodeRef node, const YGJustify justifyItems) { + updateStyle<&Style::justifyItems, &Style::setJustifyItems>( + node, scopedEnum(justifyItems)); +} + +YGJustify YGNodeStyleGetJustifyItems(const YGNodeConstRef node) { + return unscopedEnum(resolveRef(node)->style().justifyItems()); +} + +void YGNodeStyleSetJustifySelf(YGNodeRef node, const YGJustify justifySelf) { + updateStyle<&Style::justifySelf, &Style::setJustifySelf>( + node, scopedEnum(justifySelf)); +} + +YGJustify YGNodeStyleGetJustifySelf(const YGNodeConstRef node) { + return unscopedEnum(resolveRef(node)->style().justifySelf()); +} + void YGNodeStyleSetAlignContent( const YGNodeRef node, const YGAlign alignContent) { @@ -503,3 +521,85 @@ void YGNodeStyleSetMaxHeightStretch(const YGNodeRef node) { YGValue YGNodeStyleGetMaxHeight(const YGNodeConstRef node) { return (YGValue)resolveRef(node)->style().maxDimension(Dimension::Height); } + +// Grid Item Placement Properties + +void YGNodeStyleSetGridColumnStart(YGNodeRef node, int32_t gridColumnStart) { + updateStyle<&Style::gridColumnStart, &Style::setGridColumnStart>( + node, GridLine::fromInteger(gridColumnStart)); +} + +void YGNodeStyleSetGridColumnStartAuto(YGNodeRef node) { + updateStyle<&Style::gridColumnStart, &Style::setGridColumnStart>( + node, GridLine::auto_()); +} + +void YGNodeStyleSetGridColumnStartSpan(YGNodeRef node, int32_t span) { + updateStyle<&Style::gridColumnStart, &Style::setGridColumnStart>( + node, GridLine::span(span)); +} + +int32_t YGNodeStyleGetGridColumnStart(YGNodeConstRef node) { + const auto& gridLine = resolveRef(node)->style().gridColumnStart(); + return gridLine.isInteger() ? gridLine.integer : 0; +} + +void YGNodeStyleSetGridColumnEnd(YGNodeRef node, int32_t gridColumnEnd) { + updateStyle<&Style::gridColumnEnd, &Style::setGridColumnEnd>( + node, GridLine::fromInteger(gridColumnEnd)); +} + +void YGNodeStyleSetGridColumnEndAuto(YGNodeRef node) { + updateStyle<&Style::gridColumnEnd, &Style::setGridColumnEnd>( + node, GridLine::auto_()); +} + +void YGNodeStyleSetGridColumnEndSpan(YGNodeRef node, int32_t span) { + updateStyle<&Style::gridColumnEnd, &Style::setGridColumnEnd>( + node, GridLine::span(span)); +} + +int32_t YGNodeStyleGetGridColumnEnd(YGNodeConstRef node) { + const auto& gridLine = resolveRef(node)->style().gridColumnEnd(); + return gridLine.isInteger() ? gridLine.integer : 0; +} + +void YGNodeStyleSetGridRowStart(YGNodeRef node, int32_t gridRowStart) { + updateStyle<&Style::gridRowStart, &Style::setGridRowStart>( + node, GridLine::fromInteger(gridRowStart)); +} + +void YGNodeStyleSetGridRowStartAuto(YGNodeRef node) { + updateStyle<&Style::gridRowStart, &Style::setGridRowStart>( + node, GridLine::auto_()); +} + +void YGNodeStyleSetGridRowStartSpan(YGNodeRef node, int32_t span) { + updateStyle<&Style::gridRowStart, &Style::setGridRowStart>( + node, GridLine::span(span)); +} + +int32_t YGNodeStyleGetGridRowStart(YGNodeConstRef node) { + const auto& gridLine = resolveRef(node)->style().gridRowStart(); + return gridLine.isInteger() ? gridLine.integer : 0; +} + +void YGNodeStyleSetGridRowEnd(YGNodeRef node, int32_t gridRowEnd) { + updateStyle<&Style::gridRowEnd, &Style::setGridRowEnd>( + node, GridLine::fromInteger(gridRowEnd)); +} + +void YGNodeStyleSetGridRowEndAuto(YGNodeRef node) { + updateStyle<&Style::gridRowEnd, &Style::setGridRowEnd>( + node, GridLine::auto_()); +} + +void YGNodeStyleSetGridRowEndSpan(YGNodeRef node, int32_t span) { + updateStyle<&Style::gridRowEnd, &Style::setGridRowEnd>( + node, GridLine::span(span)); +} + +int32_t YGNodeStyleGetGridRowEnd(YGNodeConstRef node) { + const auto& gridLine = resolveRef(node)->style().gridRowEnd(); + return gridLine.isInteger() ? gridLine.integer : 0; +} diff --git a/yoga/YGNodeStyle.h b/yoga/YGNodeStyle.h index 21b8326d85..d1afd5c8ae 100644 --- a/yoga/YGNodeStyle.h +++ b/yoga/YGNodeStyle.h @@ -8,7 +8,6 @@ #pragma once #include - #include #include @@ -29,6 +28,14 @@ YG_EXPORT void YGNodeStyleSetJustifyContent( YGJustify justifyContent); YG_EXPORT YGJustify YGNodeStyleGetJustifyContent(YGNodeConstRef node); +YG_EXPORT void YGNodeStyleSetJustifyItems( + YGNodeRef node, + YGJustify justifyItems); +YG_EXPORT YGJustify YGNodeStyleGetJustifyItems(YGNodeConstRef node); + +YG_EXPORT void YGNodeStyleSetJustifySelf(YGNodeRef node, YGJustify justifySelf); +YG_EXPORT YGJustify YGNodeStyleGetJustifySelf(YGNodeConstRef node); + YG_EXPORT void YGNodeStyleSetAlignContent(YGNodeRef node, YGAlign alignContent); YG_EXPORT YGAlign YGNodeStyleGetAlignContent(YGNodeConstRef node); @@ -148,4 +155,27 @@ YG_EXPORT YGValue YGNodeStyleGetMaxHeight(YGNodeConstRef node); YG_EXPORT void YGNodeStyleSetAspectRatio(YGNodeRef node, float aspectRatio); YG_EXPORT float YGNodeStyleGetAspectRatio(YGNodeConstRef node); +// Grid Item Properties +YG_EXPORT void YGNodeStyleSetGridColumnStart( + YGNodeRef node, + int gridColumnStart); +YG_EXPORT void YGNodeStyleSetGridColumnStartAuto(YGNodeRef node); +YG_EXPORT void YGNodeStyleSetGridColumnStartSpan(YGNodeRef node, int span); +YG_EXPORT int YGNodeStyleGetGridColumnStart(YGNodeConstRef node); + +YG_EXPORT void YGNodeStyleSetGridColumnEnd(YGNodeRef node, int gridColumnEnd); +YG_EXPORT void YGNodeStyleSetGridColumnEndAuto(YGNodeRef node); +YG_EXPORT void YGNodeStyleSetGridColumnEndSpan(YGNodeRef node, int span); +YG_EXPORT int YGNodeStyleGetGridColumnEnd(YGNodeConstRef node); + +YG_EXPORT void YGNodeStyleSetGridRowStart(YGNodeRef node, int gridRowStart); +YG_EXPORT void YGNodeStyleSetGridRowStartAuto(YGNodeRef node); +YG_EXPORT void YGNodeStyleSetGridRowStartSpan(YGNodeRef node, int span); +YG_EXPORT int YGNodeStyleGetGridRowStart(YGNodeConstRef node); + +YG_EXPORT void YGNodeStyleSetGridRowEnd(YGNodeRef node, int gridRowEnd); +YG_EXPORT void YGNodeStyleSetGridRowEndAuto(YGNodeRef node); +YG_EXPORT void YGNodeStyleSetGridRowEndSpan(YGNodeRef node, int span); +YG_EXPORT int YGNodeStyleGetGridRowEnd(YGNodeConstRef node); + YG_EXTERN_C_END diff --git a/yoga/Yoga.h b/yoga/Yoga.h index 97f05ed3ea..25b7a8e83c 100644 --- a/yoga/Yoga.h +++ b/yoga/Yoga.h @@ -13,6 +13,7 @@ #include // IWYU pragma: export #include // IWYU pragma: export +#include // IWYU pragma: export #include // IWYU pragma: export #include // IWYU pragma: export #include // IWYU pragma: export diff --git a/yoga/algorithm/AbsoluteLayout.cpp b/yoga/algorithm/AbsoluteLayout.cpp index b2d8d8dbef..181dfcb1b0 100644 --- a/yoga/algorithm/AbsoluteLayout.cpp +++ b/yoga/algorithm/AbsoluteLayout.cpp @@ -23,7 +23,11 @@ static inline void setFlexStartLayoutPosition( axis, direction, containingBlockWidth) + parent->getLayout().border(flexStartEdge(axis)); - if (!child->hasErrata(Errata::AbsolutePositionWithoutInsetsExcludesPadding)) { + // https://www.w3.org/TR/css-grid-1/#abspos + // absolute positioned grid items are positioned relative to the padding edge + // of the grid container + if (!child->hasErrata(Errata::AbsolutePositionWithoutInsetsExcludesPadding) && + parent->style().display() != Display::Grid) { position += parent->getLayout().padding(flexStartEdge(axis)); } @@ -40,7 +44,11 @@ static inline void setFlexEndLayoutPosition( child->style().computeFlexEndMargin( axis, direction, containingBlockWidth); - if (!child->hasErrata(Errata::AbsolutePositionWithoutInsetsExcludesPadding)) { + // https://www.w3.org/TR/css-grid-1/#abspos + // absolute positioned grid items are positioned relative to the padding edge + // of the grid container + if (!child->hasErrata(Errata::AbsolutePositionWithoutInsetsExcludesPadding) && + parent->style().display() != Display::Grid) { flexEndPosition += parent->getLayout().padding(flexEndEdge(axis)); } @@ -60,7 +68,11 @@ static inline void setCenterLayoutPosition( parent->getLayout().border(flexStartEdge(axis)) - parent->getLayout().border(flexEndEdge(axis)); - if (!child->hasErrata(Errata::AbsolutePositionWithoutInsetsExcludesPadding)) { + // https://www.w3.org/TR/css-grid-1/#abspos + // absolute positioned grid items are positioned relative to the padding edge + // of the grid container + if (!child->hasErrata(Errata::AbsolutePositionWithoutInsetsExcludesPadding) && + parent->style().display() != Display::Grid) { parentContentBoxSize -= parent->getLayout().padding(flexStartEdge(axis)); parentContentBoxSize -= parent->getLayout().padding(flexEndEdge(axis)); } @@ -74,7 +86,11 @@ static inline void setCenterLayoutPosition( child->style().computeFlexStartMargin( axis, direction, containingBlockWidth); - if (!child->hasErrata(Errata::AbsolutePositionWithoutInsetsExcludesPadding)) { + // https://www.w3.org/TR/css-grid-1/#abspos + // absolute positioned grid items are positioned relative to the padding edge + // of the grid container + if (!child->hasErrata(Errata::AbsolutePositionWithoutInsetsExcludesPadding) && + parent->style().display() != Display::Grid) { position += parent->getLayout().padding(flexStartEdge(axis)); } @@ -87,13 +103,19 @@ static void justifyAbsoluteChild( const Direction direction, const FlexDirection mainAxis, const float containingBlockWidth) { - const Justify parentJustifyContent = parent->style().justifyContent(); - switch (parentJustifyContent) { + const Justify justify = parent->style().display() == Display::Grid + ? resolveChildJustification(parent, child) + : parent->style().justifyContent(); + switch (justify) { + case Justify::Start: + case Justify::Auto: + case Justify::Stretch: case Justify::FlexStart: case Justify::SpaceBetween: setFlexStartLayoutPosition( parent, child, direction, mainAxis, containingBlockWidth); break; + case Justify::End: case Justify::FlexEnd: setFlexEndLayoutPosition( parent, child, direction, mainAxis, containingBlockWidth); @@ -124,6 +146,7 @@ static void alignAbsoluteChild( } switch (itemAlign) { + case Align::Start: case Align::Auto: case Align::FlexStart: case Align::Baseline: @@ -134,6 +157,7 @@ static void alignAbsoluteChild( setFlexStartLayoutPosition( parent, child, direction, crossAxis, containingBlockWidth); break; + case Align::End: case Align::FlexEnd: setFlexEndLayoutPosition( parent, child, direction, crossAxis, containingBlockWidth); @@ -234,9 +258,15 @@ void layoutAbsoluteChild( LayoutData& layoutMarkerData, const uint32_t depth, const uint32_t generationCount) { - const FlexDirection mainAxis = - resolveDirection(node->style().flexDirection(), direction); - const FlexDirection crossAxis = resolveCrossDirection(mainAxis, direction); + // For grid containers, use inline (Row) and block (Column) axes for + // positioning, since grid alignment properties (justify-self, align-self) + // operate on inline/block axes, not main/cross axes based on flex-direction. + const FlexDirection mainAxis = node->style().display() == Display::Grid + ? resolveDirection(FlexDirection::Row, direction) + : resolveDirection(node->style().flexDirection(), direction); + const FlexDirection crossAxis = node->style().display() == Display::Grid + ? FlexDirection::Column + : resolveCrossDirection(mainAxis, direction); const bool isMainAxisRow = isRow(mainAxis); float childWidth = YGUndefined; diff --git a/yoga/algorithm/Align.h b/yoga/algorithm/Align.h index bb21fe5dca..b12ae7fd53 100644 --- a/yoga/algorithm/Align.h +++ b/yoga/algorithm/Align.h @@ -20,12 +20,23 @@ inline Align resolveChildAlignment( const Align align = child->style().alignSelf() == Align::Auto ? node->style().alignItems() : child->style().alignSelf(); - if (align == Align::Baseline && isColumn(node->style().flexDirection())) { + + if (node->style().display() == Display::Flex && align == Align::Baseline && + isColumn(node->style().flexDirection())) { return Align::FlexStart; } + return align; } +inline Justify resolveChildJustification( + const yoga::Node* node, + const yoga::Node* child) { + return child->style().justifySelf() == Justify::Auto + ? node->style().justifyItems() + : child->style().justifySelf(); +} + /** * Fallback alignment to use on overflow * https://www.w3.org/TR/css-align-3/#distribution-values diff --git a/yoga/algorithm/CalculateLayout.cpp b/yoga/algorithm/CalculateLayout.cpp index 81a5f3b8a0..95a2ba513b 100644 --- a/yoga/algorithm/CalculateLayout.cpp +++ b/yoga/algorithm/CalculateLayout.cpp @@ -35,7 +35,7 @@ namespace facebook::yoga { std::atomic gCurrentGenerationCount(0); -static void constrainMaxSizeForMode( +void constrainMaxSizeForMode( const yoga::Node* node, Direction direction, FlexDirection axis, @@ -468,7 +468,7 @@ static bool measureNodeWithFixedSize( return false; } -static void zeroOutLayoutRecursively(yoga::Node* const node) { +void zeroOutLayoutRecursively(yoga::Node* const node) { node->getLayout() = {}; node->setLayoutDimension(0, Dimension::Width); node->setLayoutDimension(0, Dimension::Height); @@ -480,7 +480,7 @@ static void zeroOutLayoutRecursively(yoga::Node* const node) { } } -static void cleanupContentsNodesRecursively(yoga::Node* const node) { +void cleanupContentsNodesRecursively(yoga::Node* const node) { if (node->hasContentsChildren()) [[unlikely]] { node->cloneContentsChildrenIfNeeded(); for (auto child : node->getChildren()) { @@ -498,7 +498,7 @@ static void cleanupContentsNodesRecursively(yoga::Node* const node) { } } -static float calculateAvailableInnerDimension( +float calculateAvailableInnerDimension( const yoga::Node* const node, const Direction direction, const Dimension dimension, @@ -1046,6 +1046,14 @@ static void justifyMainAxis( if (flexLine.numberOfAutoMargins == 0) { switch (justifyContent) { + case Justify::Start: + case Justify::End: + case Justify::Auto: + // No-Op + break; + case Justify::Stretch: + // No-Op + break; case Justify::Center: leadingMainDim = flexLine.layout.remainingFreeSpace / 2; break; @@ -1799,6 +1807,10 @@ static void calculateLayoutImpl( : fallbackAlignment(node->style().alignContent()); switch (alignContent) { + case Align::Start: + case Align::End: + // No-Op + break; case Align::FlexEnd: currentLead += remainingAlignContentDim; break; @@ -1886,6 +1898,10 @@ static void calculateLayoutImpl( } if (child->style().positionType() != PositionType::Absolute) { switch (resolveChildAlignment(node, child)) { + case Align::Start: + case Align::End: + // No-Op + break; case Align::FlexStart: { child->setLayoutPosition( currentLead + diff --git a/yoga/algorithm/CalculateLayout.h b/yoga/algorithm/CalculateLayout.h index 5e6884ec1a..86b3c61afe 100644 --- a/yoga/algorithm/CalculateLayout.h +++ b/yoga/algorithm/CalculateLayout.h @@ -35,4 +35,26 @@ bool calculateLayoutInternal( uint32_t depth, uint32_t generationCount); +void constrainMaxSizeForMode( + const yoga::Node* node, + Direction direction, + FlexDirection axis, + float ownerAxisSize, + float ownerWidth, + /*in_out*/ SizingMode* mode, + /*in_out*/ float* size); + +float calculateAvailableInnerDimension( + const yoga::Node* const node, + const Direction direction, + const Dimension dimension, + const float availableDim, + const float paddingAndBorder, + const float ownerDim, + const float ownerWidth); + +void zeroOutLayoutRecursively(yoga::Node* const node); + +void cleanupContentsNodesRecursively(yoga::Node* const node); + } // namespace facebook::yoga diff --git a/yoga/enums/Align.h b/yoga/enums/Align.h index 3896fe2b8b..e1b8b29bd7 100644 --- a/yoga/enums/Align.h +++ b/yoga/enums/Align.h @@ -25,11 +25,13 @@ enum class Align : uint8_t { SpaceBetween = YGAlignSpaceBetween, SpaceAround = YGAlignSpaceAround, SpaceEvenly = YGAlignSpaceEvenly, + Start = YGAlignStart, + End = YGAlignEnd, }; template <> constexpr int32_t ordinalCount() { - return 9; + return 11; } constexpr Align scopedEnum(YGAlign unscoped) { diff --git a/yoga/enums/Display.h b/yoga/enums/Display.h index 9bf23c0ac7..9edbee80b5 100644 --- a/yoga/enums/Display.h +++ b/yoga/enums/Display.h @@ -19,11 +19,12 @@ enum class Display : uint8_t { Flex = YGDisplayFlex, None = YGDisplayNone, Contents = YGDisplayContents, + Grid = YGDisplayGrid, }; template <> constexpr int32_t ordinalCount() { - return 3; + return 4; } constexpr Display scopedEnum(YGDisplay unscoped) { diff --git a/yoga/enums/GridTrackType.h b/yoga/enums/GridTrackType.h new file mode 100644 index 0000000000..eb6e8332bc --- /dev/null +++ b/yoga/enums/GridTrackType.h @@ -0,0 +1,43 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// @generated by enums.py +// clang-format off +#pragma once + +#include +#include +#include + +namespace facebook::yoga { + +enum class GridTrackType : uint8_t { + Auto = YGGridTrackTypeAuto, + Points = YGGridTrackTypePoints, + Percent = YGGridTrackTypePercent, + Fr = YGGridTrackTypeFr, + Minmax = YGGridTrackTypeMinmax, +}; + +template <> +constexpr int32_t ordinalCount() { + return 5; +} + +constexpr GridTrackType scopedEnum(YGGridTrackType unscoped) { + return static_cast(unscoped); +} + +constexpr YGGridTrackType unscopedEnum(GridTrackType scoped) { + return static_cast(scoped); +} + +inline const char* toString(GridTrackType e) { + return YGGridTrackTypeToString(unscopedEnum(e)); +} + +} // namespace facebook::yoga diff --git a/yoga/enums/Justify.h b/yoga/enums/Justify.h index 255baa6e27..db4afece9a 100644 --- a/yoga/enums/Justify.h +++ b/yoga/enums/Justify.h @@ -16,17 +16,21 @@ namespace facebook::yoga { enum class Justify : uint8_t { + Auto = YGJustifyAuto, FlexStart = YGJustifyFlexStart, Center = YGJustifyCenter, FlexEnd = YGJustifyFlexEnd, SpaceBetween = YGJustifySpaceBetween, SpaceAround = YGJustifySpaceAround, SpaceEvenly = YGJustifySpaceEvenly, + Stretch = YGJustifyStretch, + Start = YGJustifyStart, + End = YGJustifyEnd, }; template <> constexpr int32_t ordinalCount() { - return 6; + return 10; } constexpr Justify scopedEnum(YGJustify unscoped) { diff --git a/yoga/event/event.cpp b/yoga/event/event.cpp index e286ded055..ac6735d6d4 100644 --- a/yoga/event/event.cpp +++ b/yoga/event/event.cpp @@ -29,6 +29,8 @@ const char* LayoutPassReasonToString(const LayoutPassReason value) { return "abs_measure"; case LayoutPassReason::kFlexMeasure: return "flex_measure"; + case LayoutPassReason::kGridLayout: + return "grid_layout"; default: return "unknown"; } diff --git a/yoga/event/event.h b/yoga/event/event.h index 587c1cd063..0d032240dd 100644 --- a/yoga/event/event.h +++ b/yoga/event/event.h @@ -32,6 +32,7 @@ enum struct LayoutPassReason : int { kMeasureChild = 5, kAbsMeasureChild = 6, kFlexMeasure = 7, + kGridLayout = 8, COUNT }; diff --git a/yoga/node/Node.h b/yoga/node/Node.h index 8068c81497..d22c4c1ef0 100644 --- a/yoga/node/Node.h +++ b/yoga/node/Node.h @@ -294,15 +294,15 @@ class YG_EXPORT Node : public ::YGNode { bool isNodeFlexible(); void reset(); - private: - // Used to allow resetting the node - Node& operator=(Node&&) noexcept = default; - float relativePosition( FlexDirection axis, Direction direction, float axisSize) const; + private: + // Used to allow resetting the node + Node& operator=(Node&&) noexcept = default; + void useWebDefaults() { style_.setFlexDirection(FlexDirection::Row); style_.setAlignContent(Align::Stretch); diff --git a/yoga/style/GridLine.h b/yoga/style/GridLine.h new file mode 100644 index 0000000000..a330fe1b02 --- /dev/null +++ b/yoga/style/GridLine.h @@ -0,0 +1,61 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include + +namespace facebook::yoga { + +// https://www.w3.org/TR/css-grid-1/#typedef-grid-row-start-grid-line +enum class GridLineType : uint8_t { + Auto, + Integer, + Span, +}; + +struct GridLine { + GridLineType type; + // Line position (1, 2, -1, -2, etc) + int32_t integer; + + static GridLine auto_() { + return GridLine{GridLineType::Auto, 0}; + } + + static GridLine fromInteger(int32_t value) { + return GridLine{GridLineType::Integer, value}; + } + + static GridLine span(int32_t value) { + return GridLine{GridLineType::Span, value}; + } + + bool isAuto() const { + return type == GridLineType::Auto; + } + + bool isInteger() const { + return type == GridLineType::Integer; + } + + bool isSpan() const { + return type == GridLineType::Span; + } + + bool operator==(const GridLine& other) const { + return type == other.type && integer == other.integer; + } + + bool operator!=(const GridLine& other) const { + return !(*this == other); + } +}; + +} // namespace facebook::yoga diff --git a/yoga/style/GridTrack.h b/yoga/style/GridTrack.h new file mode 100644 index 0000000000..4b4c1c1a19 --- /dev/null +++ b/yoga/style/GridTrack.h @@ -0,0 +1,65 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +namespace facebook::yoga { +// https://www.w3.org/TR/css-grid-1/#typedef-track-size +struct GridTrackSize { + StyleSizeLength minSizingFunction; + StyleSizeLength maxSizingFunction; + + // These are used in the grid layout algorithm when distributing spaces among + // tracks + // TODO: maybe move them to TrackSizing since these are track states + float baseSize = 0.0f; + float growthLimit = 0.0f; + bool infinitelyGrowable = false; + + // Static factory methods for common cases + static GridTrackSize auto_() { + return GridTrackSize{StyleSizeLength::ofAuto(), StyleSizeLength::ofAuto()}; + } + + static GridTrackSize length(float points) { + auto len = StyleSizeLength::points(points); + return GridTrackSize{len, len}; + } + + static GridTrackSize fr(float fraction) { + // Flex sizing function is always a max sizing function + return GridTrackSize{ + StyleSizeLength::ofAuto(), StyleSizeLength::stretch(fraction)}; + } + + static GridTrackSize percent(float percentage) { + return GridTrackSize{ + StyleSizeLength::percent(percentage), + StyleSizeLength::percent(percentage)}; + } + + static GridTrackSize minmax(StyleSizeLength min, StyleSizeLength max) { + return GridTrackSize{min, max}; + } + + bool operator==(const GridTrackSize& other) const { + return minSizingFunction == other.minSizingFunction && + maxSizingFunction == other.maxSizingFunction; + } + + bool operator!=(const GridTrackSize& other) const { + return !(*this == other); + } +}; + +// Grid track list for grid-template-rows/columns properties +using GridTrackList = std::vector; + +} // namespace facebook::yoga diff --git a/yoga/style/Style.h b/yoga/style/Style.h index 566b6934e6..728cc283b2 100644 --- a/yoga/style/Style.h +++ b/yoga/style/Style.h @@ -29,6 +29,8 @@ #include #include #include +#include +#include #include #include #include @@ -65,6 +67,20 @@ class YG_EXPORT Style { justifyContent_ = value; } + Justify justifyItems() const { + return justifyItems_; + } + void setJustifyItems(Justify value) { + justifyItems_ = value; + } + + Justify justifySelf() const { + return justifySelf_; + } + void setJustifySelf(Justify value) { + justifySelf_ = value; + } + Align alignContent() const { return alignContent_; } @@ -191,6 +207,64 @@ class YG_EXPORT Style { pool_.store(minDimensions_[yoga::to_underlying(axis)], value); } + // Grid Container Properties + const GridTrackList& gridTemplateColumns() const { + return gridTemplateColumns_; + } + void setGridTemplateColumns(GridTrackList value) { + gridTemplateColumns_ = std::move(value); + } + + const GridTrackList& gridTemplateRows() const { + return gridTemplateRows_; + } + void setGridTemplateRows(GridTrackList value) { + gridTemplateRows_ = std::move(value); + } + + const GridTrackList& gridAutoColumns() const { + return gridAutoColumns_; + } + void setGridAutoColumns(GridTrackList value) { + gridAutoColumns_ = std::move(value); + } + + const GridTrackList& gridAutoRows() const { + return gridAutoRows_; + } + void setGridAutoRows(GridTrackList value) { + gridAutoRows_ = std::move(value); + } + + // Grid Item Properties + const GridLine& gridColumnStart() const { + return gridColumnStart_; + } + void setGridColumnStart(GridLine value) { + gridColumnStart_ = std::move(value); + } + + const GridLine& gridColumnEnd() const { + return gridColumnEnd_; + } + void setGridColumnEnd(GridLine value) { + gridColumnEnd_ = std::move(value); + } + + const GridLine& gridRowStart() const { + return gridRowStart_; + } + void setGridRowStart(GridLine value) { + gridRowStart_ = std::move(value); + } + + const GridLine& gridRowEnd() const { + return gridRowEnd_; + } + void setGridRowEnd(GridLine value) { + gridRowEnd_ = std::move(value); + } + FloatOptional resolvedMinDimension( Direction direction, Dimension axis, @@ -515,6 +589,12 @@ class YG_EXPORT Style { return maxOrDefined(gap.resolve(ownerSize).unwrap(), 0.0f); } + float computeGapForDimension(Dimension dimension, float ownerSize) const { + auto gap = + dimension == Dimension::Width ? computeColumnGap() : computeRowGap(); + return maxOrDefined(gap.resolve(ownerSize).unwrap(), 0.0f); + } + bool flexStartMarginIsAuto(FlexDirection axis, Direction direction) const { return computeMargin(flexStartEdge(axis), direction).isAuto(); } @@ -523,10 +603,20 @@ class YG_EXPORT Style { return computeMargin(flexEndEdge(axis), direction).isAuto(); } + bool inlineStartMarginIsAuto(FlexDirection axis, Direction direction) const { + return computeMargin(inlineStartEdge(axis, direction), direction).isAuto(); + } + + bool inlineEndMarginIsAuto(FlexDirection axis, Direction direction) const { + return computeMargin(inlineEndEdge(axis, direction), direction).isAuto(); + } + bool operator==(const Style& other) const { return direction_ == other.direction_ && flexDirection_ == other.flexDirection_ && justifyContent_ == other.justifyContent_ && + justifyItems_ == other.justifyItems_ && + justifySelf_ == other.justifySelf_ && alignContent_ == other.alignContent_ && alignItems_ == other.alignItems_ && alignSelf_ == other.alignSelf_ && positionType_ == other.positionType_ && flexWrap_ == other.flexWrap_ && @@ -545,7 +635,15 @@ class YG_EXPORT Style { minDimensions_, pool_, other.minDimensions_, other.pool_) && lengthsEqual( maxDimensions_, pool_, other.maxDimensions_, other.pool_) && - numbersEqual(aspectRatio_, pool_, other.aspectRatio_, other.pool_); + numbersEqual(aspectRatio_, pool_, other.aspectRatio_, other.pool_) && + gridTemplateColumns_ == other.gridTemplateColumns_ && + gridTemplateRows_ == other.gridTemplateRows_ && + gridAutoColumns_ == other.gridAutoColumns_ && + gridAutoRows_ == other.gridAutoRows_ && + gridColumnStart_ == other.gridColumnStart_ && + gridColumnEnd_ == other.gridColumnEnd_ && + gridRowStart_ == other.gridRowStart_ && + gridRowEnd_ == other.gridRowEnd_; } bool operator!=(const Style& other) const { @@ -727,6 +825,8 @@ class YG_EXPORT Style { FlexDirection flexDirection_ : bitCount() = FlexDirection::Column; Justify justifyContent_ : bitCount() = Justify::FlexStart; + Justify justifyItems_ : bitCount() = Justify::Stretch; + Justify justifySelf_ : bitCount() = Justify::Auto; Align alignContent_ : bitCount() = Align::FlexStart; Align alignItems_ : bitCount() = Align::Stretch; Align alignSelf_ : bitCount() = Align::Auto; @@ -753,6 +853,16 @@ class YG_EXPORT Style { Dimensions maxDimensions_{}; StyleValueHandle aspectRatio_{}; + // Grid properties + GridTrackList gridTemplateColumns_{}; + GridTrackList gridTemplateRows_{}; + GridTrackList gridAutoColumns_{}; + GridTrackList gridAutoRows_{}; + GridLine gridColumnStart_{}; + GridLine gridColumnEnd_{}; + GridLine gridRowStart_{}; + GridLine gridRowEnd_{}; + StyleValuePool pool_; }; diff --git a/yoga/style/StyleSizeLength.h b/yoga/style/StyleSizeLength.h index fc4d371e42..76e079b2da 100644 --- a/yoga/style/StyleSizeLength.h +++ b/yoga/style/StyleSizeLength.h @@ -42,6 +42,12 @@ class StyleSizeLength { : StyleSizeLength{FloatOptional{value}, Unit::Percent}; } + constexpr static StyleSizeLength stretch(float fraction) { + return yoga::isUndefined(fraction) || yoga::isinf(fraction) + ? undefined() + : StyleSizeLength{FloatOptional{fraction}, Unit::Stretch}; + } + constexpr static StyleSizeLength ofAuto() { return StyleSizeLength{{}, Unit::Auto}; } @@ -98,7 +104,7 @@ class StyleSizeLength { return value_; } - constexpr FloatOptional resolve(float referenceLength) { + constexpr FloatOptional resolve(float referenceLength) const { #ifdef __clang__ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wswitch-enum" From fc9216f8907dba4c9b39464d26d13aa3f81ec0aa Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Sat, 21 Feb 2026 22:11:09 -0800 Subject: [PATCH 2/7] CSS Grid 2/9: Grid layout algorithm Summary: Add the core grid layout computation and integrate it into the layout dispatcher. Includes: - AutoPlacement.h: auto-placement algorithm for grid items - GridLayout.h/cpp: grid layout entry point - TrackSizing.h: track sizing algorithm - CalculateLayout.cpp: grid dispatch block and #include - CMakeLists.txt: add algorithm/grid/*.cpp glob - React Native mirror of all C++ changes Differential Revision: D93946253 --- javascript/CMakeLists.txt | 2 +- yoga/CMakeLists.txt | 3 +- yoga/algorithm/CalculateLayout.cpp | 19 + yoga/algorithm/grid/AutoPlacement.h | 585 ++++++++ yoga/algorithm/grid/GridLayout.cpp | 514 +++++++ yoga/algorithm/grid/GridLayout.h | 42 + yoga/algorithm/grid/TrackSizing.h | 2052 +++++++++++++++++++++++++++ 7 files changed, 3215 insertions(+), 2 deletions(-) create mode 100644 yoga/algorithm/grid/AutoPlacement.h create mode 100644 yoga/algorithm/grid/GridLayout.cpp create mode 100644 yoga/algorithm/grid/GridLayout.h create mode 100644 yoga/algorithm/grid/TrackSizing.h diff --git a/javascript/CMakeLists.txt b/javascript/CMakeLists.txt index d3ddf3a6f2..d3919a90a9 100644 --- a/javascript/CMakeLists.txt +++ b/javascript/CMakeLists.txt @@ -7,7 +7,7 @@ cmake_minimum_required(VERSION 3.13...3.26) set(CMAKE_VERBOSE_MAKEFILE on) project(yoga) -file(GLOB SOURCES CONFIGURE_DEPENDS +file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/../yoga/*.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../yoga/**/*.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp) diff --git a/yoga/CMakeLists.txt b/yoga/CMakeLists.txt index b6eca1ac72..594600f702 100644 --- a/yoga/CMakeLists.txt +++ b/yoga/CMakeLists.txt @@ -20,7 +20,8 @@ include(${YOGA_ROOT}/cmake/project-defaults.cmake) file(GLOB SOURCES CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/**/*.cpp) + ${CMAKE_CURRENT_SOURCE_DIR}/**/*.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/algorithm/grid/*.cpp) add_library(yogacore STATIC ${SOURCES}) diff --git a/yoga/algorithm/CalculateLayout.cpp b/yoga/algorithm/CalculateLayout.cpp index 95a2ba513b..b9b3d90d2a 100644 --- a/yoga/algorithm/CalculateLayout.cpp +++ b/yoga/algorithm/CalculateLayout.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -1375,6 +1376,24 @@ static void calculateLayoutImpl( // current node as they will not be traversed cleanupContentsNodesRecursively(node); + if (node->style().display() == Display::Grid) { + calculateGridLayoutInternal( + node, + availableWidth, + availableHeight, + ownerDirection, + widthSizingMode, + heightSizingMode, + ownerWidth, + ownerHeight, + performLayout, + reason, + layoutMarkerData, + depth, + generationCount); + return; + } + // STEP 1: CALCULATE VALUES FOR REMAINDER OF ALGORITHM const FlexDirection mainAxis = resolveDirection(node->style().flexDirection(), direction); diff --git a/yoga/algorithm/grid/AutoPlacement.h b/yoga/algorithm/grid/AutoPlacement.h new file mode 100644 index 0000000000..0245c2fbd6 --- /dev/null +++ b/yoga/algorithm/grid/AutoPlacement.h @@ -0,0 +1,585 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace facebook::yoga { + +struct OccupancyGrid { + std::unordered_map>> + rowIntervals; + + void markOccupied( + int32_t rowStart, + int32_t rowEnd, + int32_t colStart, + int32_t colEnd) { + for (int32_t row = rowStart; row < rowEnd; row++) { + rowIntervals[row].emplace_back(colStart, colEnd); + } + } + + bool hasOverlap( + int32_t rowStart, + int32_t rowEnd, + int32_t colStart, + int32_t colEnd) const { + for (int32_t row = rowStart; row < rowEnd; row++) { + auto it = rowIntervals.find(row); + if (it == rowIntervals.end()) { + continue; + } + for (const auto& interval : it->second) { + if (interval.first < colEnd && interval.second > colStart) { + return true; + } + } + } + return false; + } +}; + +struct GridItemTrackPlacement { + int32_t start = 0; + int32_t end = 0; + int32_t span = 1; + // https://www.w3.org/TR/css-grid-1/#grid-placement-errors + static GridItemTrackPlacement resolveLinePlacement( + const GridLine& startLine, + const GridLine& endLine, + int32_t explicitLineCount) { + GridItemTrackPlacement placement; + + auto resolveNegativeLineValue = [](int32_t lineValue, + int32_t explicitLineCount) -> int32_t { + return lineValue < 0 ? explicitLineCount + lineValue + 1 : lineValue; + }; + + // If the placement for a grid item contains two lines + if (startLine.type == GridLineType::Integer && + endLine.type == GridLineType::Integer) { + // if lines are negative, we count it from the last line. e.g. -1 is the + // last line + auto normalizedStartLine = + resolveNegativeLineValue(startLine.integer, explicitLineCount); + auto normalizedEndLine = + resolveNegativeLineValue(endLine.integer, explicitLineCount); + // and the start line is further end-ward than the end line, swap the two + // lines. + if (normalizedStartLine > normalizedEndLine) { + placement.start = normalizedEndLine; + placement.end = normalizedStartLine; + placement.span = placement.end - placement.start; + } + // If the start line is equal to the end line, remove the end line. + else if (normalizedStartLine == normalizedEndLine) { + placement.start = normalizedStartLine; + placement.end = normalizedStartLine + 1; + placement.span = 1; + } else { + placement.start = normalizedStartLine; + placement.end = normalizedEndLine; + placement.span = placement.end - placement.start; + } + } + // If the placement contains two spans, remove the one contributed by the + // end grid-placement property. + else if ( + startLine.type == GridLineType::Span && + endLine.type == GridLineType::Span) { + placement.start = 0; + placement.end = 0; + placement.span = startLine.integer; + } + + else if ( + startLine.type == GridLineType::Integer && + endLine.type == GridLineType::Span) { + auto normalizedStartLine = + resolveNegativeLineValue(startLine.integer, explicitLineCount); + placement.start = normalizedStartLine; + placement.span = endLine.integer; + placement.end = placement.start + placement.span; + } + + else if ( + startLine.type == GridLineType::Span && + endLine.type == GridLineType::Integer) { + auto normalizedEndLine = + resolveNegativeLineValue(endLine.integer, explicitLineCount); + placement.end = normalizedEndLine; + placement.span = startLine.integer; + placement.start = placement.end - placement.span; + } + + else if (startLine.type == GridLineType::Integer) { + auto normalizedStartLine = + resolveNegativeLineValue(startLine.integer, explicitLineCount); + placement.start = normalizedStartLine; + placement.span = 1; + placement.end = placement.start + placement.span; + } + + else if (startLine.type == GridLineType::Span) { + placement.span = startLine.integer; + placement.start = 0; + placement.end = 0; + } + + else if (endLine.type == GridLineType::Integer) { + auto normalizedEndLine = + resolveNegativeLineValue(endLine.integer, explicitLineCount); + placement.end = normalizedEndLine; + placement.span = 1; + placement.start = placement.end - placement.span; + } + + else if (endLine.type == GridLineType::Span) { + placement.span = endLine.integer; + placement.start = 0; + placement.end = 0; + } + + else { + placement.start = 0; + placement.end = 0; + placement.span = 1; + } + + // we want 0 based indexing, so we subtract 1. Negative values will imply + // auto implicit grid lines + placement.start = placement.start - 1; + placement.end = placement.end - 1; + + return placement; + } +}; + +struct AutoPlacement { + struct AutoPlacementItem { + int32_t columnStart; + int32_t columnEnd; + int32_t rowStart; + int32_t rowEnd; + + yoga::Node* node; + + bool overlaps(const AutoPlacementItem& other) const { + return columnStart < other.columnEnd && columnEnd > other.columnStart && + rowStart < other.rowEnd && rowEnd > other.rowStart; + } + }; + + std::vector gridItems; + int32_t minColumnStart; + int32_t minRowStart; + int32_t maxColumnEnd; + int32_t maxRowEnd; + + static AutoPlacement performAutoPlacement(yoga::Node* node) { + std::vector gridItems; + gridItems.reserve(node->getChildCount()); + std::unordered_set placedItems; + placedItems.reserve(node->getChildCount()); + int32_t minColumnStart = 0; + int32_t minRowStart = 0; + int32_t maxColumnEnd = + static_cast(node->style().gridTemplateColumns().size()); + int32_t maxRowEnd = + static_cast(node->style().gridTemplateRows().size()); + OccupancyGrid occupancy; + + // function to push back a grid item placement and record the min/max + // column/row start/end + auto recordGridArea = [&](AutoPlacementItem& gridItemArea) { + yoga::assertFatal( + gridItemArea.columnEnd > gridItemArea.columnStart, + "Grid item column end must be greater than column start"); + yoga::assertFatal( + gridItemArea.rowEnd > gridItemArea.rowStart, + "Grid item row end must be greater than row start"); + gridItems.push_back(gridItemArea); + placedItems.insert(gridItemArea.node); + occupancy.markOccupied( + gridItemArea.rowStart, + gridItemArea.rowEnd, + gridItemArea.columnStart, + gridItemArea.columnEnd); + minColumnStart = std::min(minColumnStart, gridItemArea.columnStart); + minRowStart = std::min(minRowStart, gridItemArea.rowStart); + maxColumnEnd = std::max(maxColumnEnd, gridItemArea.columnEnd); + maxRowEnd = std::max(maxRowEnd, gridItemArea.rowEnd); + }; + + auto explicitColumnLineCount = + static_cast(node->style().gridTemplateColumns().size() + 1); + auto explicitRowLineCount = + static_cast(node->style().gridTemplateRows().size() + 1); + + // Step 1: Position anything that's not auto-positioned. + // In spec level 1, span is always definite. Default is 1. + // So for grid position to be definite, we need either start or end to be + // definite. + for (auto child : node->getLayoutChildren()) { + if (child->style().positionType() == PositionType::Absolute || + child->style().display() == Display::None) { + continue; + } + + auto gridItemColumnStart = child->style().gridColumnStart(); + auto gridItemColumnEnd = child->style().gridColumnEnd(); + auto gridItemRowStart = child->style().gridRowStart(); + auto gridItemRowEnd = child->style().gridRowEnd(); + auto hasDefiniteColumn = + gridItemColumnStart.type == GridLineType::Integer || + gridItemColumnEnd.type == GridLineType::Integer; + auto hasDefiniteRow = gridItemRowStart.type == GridLineType::Integer || + gridItemRowEnd.type == GridLineType::Integer; + + auto hasDefinitePosition = hasDefiniteColumn && hasDefiniteRow; + + if (hasDefinitePosition) { + auto columnPlacement = GridItemTrackPlacement::resolveLinePlacement( + gridItemColumnStart, gridItemColumnEnd, explicitColumnLineCount); + auto rowPlacement = GridItemTrackPlacement::resolveLinePlacement( + gridItemRowStart, gridItemRowEnd, explicitRowLineCount); + + auto columnStart = columnPlacement.start; + auto columnEnd = columnPlacement.end; + + auto rowStart = rowPlacement.start; + auto rowEnd = rowPlacement.end; + + auto gridItemArea = AutoPlacementItem{ + .columnStart = columnStart, + .columnEnd = columnEnd, + .rowStart = rowStart, + .rowEnd = rowEnd, + .node = child}; + recordGridArea(gridItemArea); + } + } + + // Step 2: Process the items locked to a given row. + // Definite row positions only, exclude items with definite column + // positions. + std::unordered_map rowStartToColumnStartCache; + for (auto child : node->getLayoutChildren()) { + if (child->style().positionType() == PositionType::Absolute || + child->style().display() == Display::None) { + continue; + } + + auto gridItemColumnStart = child->style().gridColumnStart(); + auto gridItemColumnEnd = child->style().gridColumnEnd(); + auto gridItemRowStart = child->style().gridRowStart(); + auto gridItemRowEnd = child->style().gridRowEnd(); + auto hasDefiniteRow = gridItemRowStart.type == GridLineType::Integer || + gridItemRowEnd.type == GridLineType::Integer; + auto hasDefiniteColumn = + gridItemColumnStart.type == GridLineType::Integer || + gridItemColumnEnd.type == GridLineType::Integer; + + if (hasDefiniteRow && !hasDefiniteColumn) { + auto rowPlacement = GridItemTrackPlacement::resolveLinePlacement( + gridItemRowStart, gridItemRowEnd, explicitRowLineCount); + + auto rowStart = rowPlacement.start; + auto rowEnd = rowPlacement.end; + + auto columnStart = rowStartToColumnStartCache.contains(rowStart) + ? rowStartToColumnStartCache[rowStart] + : minColumnStart; + + auto columnPlacement = GridItemTrackPlacement::resolveLinePlacement( + gridItemColumnStart, gridItemColumnEnd, explicitColumnLineCount); + auto columnSpan = columnPlacement.span; + auto columnEnd = columnStart + columnSpan; + + bool placed = false; + while (!placed) { + auto gridItemArea = AutoPlacementItem{ + .columnStart = columnStart, + .columnEnd = columnEnd, + .rowStart = rowStart, + .rowEnd = rowEnd, + .node = child}; + if (occupancy.hasOverlap(rowStart, rowEnd, columnStart, columnEnd)) { + columnStart++; + columnEnd = columnStart + columnSpan; + } else { + recordGridArea(gridItemArea); + rowStartToColumnStartCache[rowStart] = columnEnd; + placed = true; + } + } + } + } + + // Step 3: Determine the columns in the implicit grid. + // TODO: we dont need this loop. we can do it in above steps. But keeping it + // for now, to match the spec. + auto largestColumnSpan = 1; + for (auto child : node->getLayoutChildren()) { + if (child->style().positionType() == PositionType::Absolute || + child->style().display() == Display::None) { + continue; + } + + auto gridItemColumnStart = child->style().gridColumnStart(); + auto gridItemColumnEnd = child->style().gridColumnEnd(); + + auto hasDefiniteColumn = + gridItemColumnStart.type == GridLineType::Integer || + gridItemColumnEnd.type == GridLineType::Integer; + + if (hasDefiniteColumn) { + auto columnPlacement = GridItemTrackPlacement::resolveLinePlacement( + gridItemColumnStart, gridItemColumnEnd, explicitColumnLineCount); + + auto columnStart = columnPlacement.start; + auto columnEnd = columnPlacement.end; + + minColumnStart = std::min(minColumnStart, columnStart); + maxColumnEnd = std::max(maxColumnEnd, columnEnd); + } else { + auto columnPlacement = GridItemTrackPlacement::resolveLinePlacement( + gridItemColumnStart, gridItemColumnEnd, explicitColumnLineCount); + largestColumnSpan = std::max(largestColumnSpan, columnPlacement.span); + } + } + + // If largest span is larger than current grid width, extend the end + auto currentGridWidth = maxColumnEnd - minColumnStart; + if (largestColumnSpan > currentGridWidth) { + maxColumnEnd = minColumnStart + largestColumnSpan; + } + + // Step 4: Position the remaining grid items. + std::array autoPlacementCursor = {minColumnStart, minRowStart}; + for (auto child : node->getLayoutChildren()) { + if (child->style().positionType() == PositionType::Absolute || + child->style().display() == Display::None) { + continue; + } + + if (!placedItems.contains(child)) { + auto gridItemColumnStart = child->style().gridColumnStart(); + auto gridItemColumnEnd = child->style().gridColumnEnd(); + auto hasDefiniteColumn = + gridItemColumnStart.type == GridLineType::Integer || + gridItemColumnEnd.type == GridLineType::Integer; + + auto gridItemRowStart = child->style().gridRowStart(); + auto gridItemRowEnd = child->style().gridRowEnd(); + auto hasDefiniteRow = gridItemRowStart.type == GridLineType::Integer || + gridItemRowEnd.type == GridLineType::Integer; + + auto columnPlacement = GridItemTrackPlacement::resolveLinePlacement( + gridItemColumnStart, gridItemColumnEnd, explicitColumnLineCount); + auto rowPlacement = GridItemTrackPlacement::resolveLinePlacement( + gridItemRowStart, gridItemRowEnd, explicitRowLineCount); + + // If the item has a definite column position: + if (hasDefiniteColumn) { + auto columnStart = columnPlacement.start; + auto columnEnd = columnPlacement.end; + + // Set cursor column position to item's column-start line + auto previousColumnPosition = autoPlacementCursor[0]; + autoPlacementCursor[0] = columnStart; + + // If this is less than previous column position, increment row + if (autoPlacementCursor[0] < previousColumnPosition) { + autoPlacementCursor[1]++; + } + + // Find a row position where the item doesn't overlap occupied cells + bool foundPosition = false; + auto rowSpan = rowPlacement.span; + while (!foundPosition) { + auto proposedRowStart = autoPlacementCursor[1]; + auto proposedRowEnd = proposedRowStart + rowSpan; + + // Check for overlaps with already placed items + AutoPlacementItem proposedPlacement{ + .columnStart = columnStart, + .columnEnd = columnEnd, + .rowStart = proposedRowStart, + .rowEnd = proposedRowEnd, + .node = child}; + + if (occupancy.hasOverlap( + proposedRowStart, proposedRowEnd, columnStart, columnEnd)) { + autoPlacementCursor[1]++; + } else { + recordGridArea(proposedPlacement); + foundPosition = true; + } + } + } + + // If the item has an automatic grid position in both axes: + else if (!hasDefiniteRow && !hasDefiniteColumn) { + auto itemColumnSpan = columnPlacement.span; + auto itemRowSpan = rowPlacement.span; + + bool foundPosition = false; + while (!foundPosition) { + // Try to find a position starting from current cursor position + while (autoPlacementCursor[0] + itemColumnSpan <= maxColumnEnd) { + auto columnStart = autoPlacementCursor[0]; + auto columnEnd = columnStart + itemColumnSpan; + auto rowStart = autoPlacementCursor[1]; + auto rowEnd = rowStart + itemRowSpan; + + AutoPlacementItem proposedPlacement{ + .columnStart = columnStart, + .columnEnd = columnEnd, + .rowStart = rowStart, + .rowEnd = rowEnd, + .node = child}; + + if (occupancy.hasOverlap( + rowStart, rowEnd, columnStart, columnEnd)) { + autoPlacementCursor[0]++; + } else { + recordGridArea(proposedPlacement); + foundPosition = true; + break; + } + } + + if (!foundPosition) { + // Cursor column position + span would overflow, move to next row + autoPlacementCursor[1]++; + autoPlacementCursor[0] = minColumnStart; + } + } + } + } + } + + return AutoPlacement{ + std::move(gridItems), + minColumnStart, + minRowStart, + maxColumnEnd, + maxRowEnd}; + } +}; + +struct GridItem { + size_t columnStart; + size_t columnEnd; + size_t rowStart; + size_t rowEnd; + yoga::Node* node; + // additional space added to align baselines + // https://www.w3.org/TR/css-grid-1/#algo-baseline-shims + float baselineShim = 0.0f; + // Flags used for optimisations in TrackSizing + bool crossesIntrinsicRow = false; + bool crossesIntrinsicColumn = false; + bool crossesFlexibleRow = false; + bool crossesFlexibleColumn = false; + + GridItem( + size_t columnStart, + size_t columnEnd, + size_t rowStart, + size_t rowEnd, + yoga::Node* node, + float baselineShim = 0.0f) + : columnStart(columnStart), + columnEnd(columnEnd), + rowStart(rowStart), + rowEnd(rowEnd), + node(node), + baselineShim(baselineShim) {} + + bool crossesIntrinsicTrack(Dimension dimension) const { + return dimension == Dimension::Width ? crossesIntrinsicColumn + : crossesIntrinsicRow; + } + bool crossesFlexibleTrack(Dimension dimension) const { + return dimension == Dimension::Width ? crossesFlexibleColumn + : crossesFlexibleRow; + } +}; + +// Baseline sharing groups - items grouped by their starting row for resolve +// intrinsic size step in TrackSizing +// https://www.w3.org/TR/css-grid-1/#algo-baseline-shims +using BaselineItemGroups = std::map>; + +struct ResolvedAutoPlacement { + std::vector gridItems; + BaselineItemGroups baselineItemGroups; + int32_t minColumnStart; + int32_t minRowStart; + int32_t maxColumnEnd; + int32_t maxRowEnd; + + // Offset column and row so they starts at 0 index + // also casts start and end values from int32_t to size_t + static ResolvedAutoPlacement resolveGridItemPlacements(Node* node) { + auto autoPlacement = AutoPlacement::performAutoPlacement(node); + + auto minColumnStart = autoPlacement.minColumnStart; + auto minRowStart = autoPlacement.minRowStart; + auto maxColumnEnd = autoPlacement.maxColumnEnd; + auto maxRowEnd = autoPlacement.maxRowEnd; + + std::vector resolvedAreas; + resolvedAreas.reserve(autoPlacement.gridItems.size()); + + BaselineItemGroups baselineGroups; + auto alignItems = node->style().alignItems(); + + for (auto& placement : autoPlacement.gridItems) { + resolvedAreas.emplace_back( + static_cast(placement.columnStart - minColumnStart), + static_cast(placement.columnEnd - minColumnStart), + static_cast(placement.rowStart - minRowStart), + static_cast(placement.rowEnd - minRowStart), + placement.node); + + auto& item = resolvedAreas.back(); + auto alignSelf = item.node->style().alignSelf(); + if (alignSelf == Align::Auto) { + alignSelf = alignItems; + } + bool spansOneRow = (item.rowEnd - item.rowStart) == 1; + if (alignSelf == Align::Baseline && spansOneRow) { + baselineGroups[item.rowStart].push_back(&item); + } + + // TODO: find a better place to call this + placement.node->processDimensions(); + } + + return ResolvedAutoPlacement{ + .gridItems = std::move(resolvedAreas), + .baselineItemGroups = std::move(baselineGroups), + .minColumnStart = minColumnStart, + .minRowStart = minRowStart, + .maxColumnEnd = maxColumnEnd, + .maxRowEnd = maxRowEnd}; + } +}; + +} // namespace facebook::yoga diff --git a/yoga/algorithm/grid/GridLayout.cpp b/yoga/algorithm/grid/GridLayout.cpp new file mode 100644 index 0000000000..14ee6e7d0a --- /dev/null +++ b/yoga/algorithm/grid/GridLayout.cpp @@ -0,0 +1,514 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include +#include +#include +#include +#include + +namespace facebook::yoga { + +void calculateGridLayoutInternal( + Node* node, + float availableWidth, + float availableHeight, + Direction ownerDirection, + SizingMode widthSizingMode, + SizingMode heightSizingMode, + float ownerWidth, + float ownerHeight, + bool performLayout, + LayoutPassReason reason, + LayoutData& layoutMarkerData, + uint32_t depth, + uint32_t generationCount) { + (void)reason; // Unused parameter + + const auto& nodeStyle = node->style(); + const Direction direction = node->resolveDirection(ownerDirection); + const float marginInline = + nodeStyle.computeMarginForAxis(FlexDirection::Row, ownerWidth); + const float marginBlock = + nodeStyle.computeMarginForAxis(FlexDirection::Column, ownerWidth); + const float paddingAndBorderInline = + paddingAndBorderForAxis(node, FlexDirection::Row, direction, ownerWidth); + const float paddingAndBorderBlock = paddingAndBorderForAxis( + node, FlexDirection::Column, direction, ownerWidth); + const float availableInnerWidth = calculateAvailableInnerDimension( + node, + direction, + Dimension::Width, + availableWidth - marginInline, + paddingAndBorderInline, + ownerWidth, + ownerWidth); + const float availableInnerHeight = calculateAvailableInnerDimension( + node, + direction, + Dimension::Height, + availableHeight - marginBlock, + paddingAndBorderBlock, + ownerHeight, + ownerWidth); + auto widthIsDefinite = + (widthSizingMode == SizingMode::StretchFit && + yoga::isDefined(availableWidth)); + auto heightIsDefinite = + (heightSizingMode == SizingMode::StretchFit && + yoga::isDefined(availableHeight)); + + // 11. Grid Layout Algorithm + // Step 1: Run the Grid Item Placement Algorithm to resolve the placement of + // all grid items in the grid. + auto autoPlacement = ResolvedAutoPlacement::resolveGridItemPlacements(node); + // Create the grid tracks (auto and explicit = implicit grid) + auto gridTracks = createGridTracks(node, autoPlacement); + // At this point, we have grid items final positions and implicit grid tracks + + // Step 2: Find the size of the grid container, per § 5.2 Sizing Grid + // Containers. If grid container size is not definite, we have to run the + // track sizing algorithm to find the size of the grid container. Note: During + // this phase, cyclic s in track sizes are treated as auto. + float containerInnerWidth = + widthIsDefinite ? availableInnerWidth : YGUndefined; + float containerInnerHeight = + heightIsDefinite ? availableInnerHeight : YGUndefined; + auto& rowTracks = gridTracks.rowTracks; + auto& columnTracks = gridTracks.columnTracks; + auto& gridItems = autoPlacement.gridItems; + auto& baselineItemGroups = autoPlacement.baselineItemGroups; + bool needsSecondTrackSizingPass = true; + + if (!widthIsDefinite || !heightIsDefinite) { + auto trackSizing = TrackSizing( + node, + columnTracks, + rowTracks, + containerInnerWidth, + containerInnerHeight, + gridItems, + widthSizingMode, + heightSizingMode, + direction, + ownerWidth, + ownerHeight, + layoutMarkerData, + depth, + generationCount, + baselineItemGroups); + + trackSizing.runGridSizingAlgorithm(); + + bool containerSizeChanged = false; + + if (!widthIsDefinite) { + auto totalTrackWidth = trackSizing.getTotalBaseSize(Dimension::Width); + containerInnerWidth = boundAxis( + node, + FlexDirection::Row, + direction, + totalTrackWidth, + ownerWidth, + ownerWidth); + if (containerInnerWidth != totalTrackWidth) { + containerSizeChanged = true; + } + } + + if (!heightIsDefinite) { + auto totalTrackHeight = trackSizing.getTotalBaseSize(Dimension::Height); + containerInnerHeight = boundAxis( + node, + FlexDirection::Column, + direction, + totalTrackHeight, + ownerHeight, + ownerWidth); + if (containerInnerHeight != totalTrackHeight) { + containerSizeChanged = true; + } + } + + // We need to run track sizing again if: + // 1. The container size changed due to min/max bounds or + // 2. There are percentage tracks in indefinite dimensions that need + // resolution + bool hasPercentageTracksNeedingResolution = + (!widthIsDefinite && + trackSizing.hasPercentageTracks(Dimension::Width)) || + (!heightIsDefinite && + trackSizing.hasPercentageTracks(Dimension::Height)); + needsSecondTrackSizingPass = + containerSizeChanged || hasPercentageTracksNeedingResolution; + } + + node->setLayoutMeasuredDimension( + boundAxis( + node, + FlexDirection::Row, + direction, + containerInnerWidth + paddingAndBorderInline, + ownerWidth, + ownerWidth), + Dimension::Width); + + node->setLayoutMeasuredDimension( + boundAxis( + node, + FlexDirection::Column, + direction, + containerInnerHeight + paddingAndBorderBlock, + ownerHeight, + ownerWidth), + Dimension::Height); + + // If we are not performing layout, we can return early after sizing the grid + // container. + if (!performLayout) { + return; + } + + // Inititialize track sizing with the final container size + auto trackSizing = TrackSizing( + node, + columnTracks, + rowTracks, + containerInnerWidth, + containerInnerHeight, + gridItems, + widthSizingMode, + heightSizingMode, + direction, + ownerWidth, + ownerHeight, + layoutMarkerData, + depth, + generationCount, + baselineItemGroups); + + // Step 3: Given the resulting grid container size, run the Grid Sizing + // Algorithm to size the grid. Run track sizing with the new container + // dimensions Note: During this phase, s in track sizes are + // resolved against the grid container size. + + // We only need to run track sizing again if: + // 1. Both dimensions were definite or + // 2. The container size changed due to min/max constraints in Step 2, or + // 3. There are percentage tracks in indefinite dimensions that need + // resolution + if (needsSecondTrackSizingPass) { + trackSizing.runGridSizingAlgorithm(); + } + + // Step 4: Lay out the grid items into their respective containing blocks. + // Each grid area’s width and height are considered definite for this purpose. + auto gridWidth = trackSizing.getTotalBaseSize(Dimension::Width); + auto gridHeight = trackSizing.getTotalBaseSize(Dimension::Height); + + float leadingPaddingAndBorderInline = + nodeStyle.computeInlineStartPadding( + FlexDirection::Row, direction, ownerWidth) + + nodeStyle.computeInlineStartBorder(FlexDirection::Row, direction); + float leadingPaddingAndBorderBlock = + nodeStyle.computeInlineStartPadding( + FlexDirection::Column, direction, ownerWidth) + + nodeStyle.computeInlineStartBorder(FlexDirection::Column, direction); + + // Align content/Justify content + float freeSpaceInlineAxis = containerInnerWidth - gridWidth; + auto inlineDistribution = trackSizing.calculateContentDistribution( + Dimension::Width, freeSpaceInlineAxis); + float freeSpaceBlockAxis = containerInnerHeight - gridHeight; + auto blockDistribution = trackSizing.calculateContentDistribution( + Dimension::Height, freeSpaceBlockAxis); + + if (freeSpaceInlineAxis < 0.0f || freeSpaceBlockAxis < 0.0f) { + node->setLayoutHadOverflow(true); + } + + auto gridInlineStartOffset = inlineDistribution.startOffset; + auto gridBlockStartOffset = blockDistribution.startOffset; + auto finalEffectiveColumnGap = inlineDistribution.effectiveGap; + auto finalEffectiveRowGap = blockDistribution.effectiveGap; + + std::vector columnGridLineOffsets; + columnGridLineOffsets.reserve(columnTracks.size() + 1); + columnGridLineOffsets.push_back(0.0f); + for (size_t i = 0; i < columnTracks.size(); i++) { + float offset = columnGridLineOffsets[i] + columnTracks[i].baseSize; + if (i < columnTracks.size() - 1) { + offset += finalEffectiveColumnGap; + } + columnGridLineOffsets.push_back(offset); + } + + std::vector rowGridLineOffsets; + rowGridLineOffsets.reserve(rowTracks.size() + 1); + rowGridLineOffsets.push_back(0.0f); + for (size_t i = 0; i < rowTracks.size(); i++) { + float offset = rowGridLineOffsets[i] + rowTracks[i].baseSize; + if (i < rowTracks.size() - 1) { + offset += finalEffectiveRowGap; + } + rowGridLineOffsets.push_back(offset); + } + + for (auto& item : gridItems) { + // grid line offsets include the gap after each track (except the last). + // so we subtract the trailing gap for items that do not end at the last + // track. + float containingBlockWidth = columnGridLineOffsets[item.columnEnd] - + columnGridLineOffsets[item.columnStart]; + if (item.columnEnd < columnTracks.size()) { + containingBlockWidth -= finalEffectiveColumnGap; + } + float containingBlockHeight = + rowGridLineOffsets[item.rowEnd] - rowGridLineOffsets[item.rowStart]; + if (item.rowEnd < rowTracks.size()) { + containingBlockHeight -= finalEffectiveRowGap; + } + float gridItemInlineStart = columnGridLineOffsets[item.columnStart]; + float gridItemBlockStart = rowGridLineOffsets[item.rowStart]; + const auto& itemStyle = item.node->style(); + + const auto marginInlineStart = itemStyle.computeInlineStartMargin( + FlexDirection::Row, direction, containingBlockWidth); + const auto marginInlineEnd = itemStyle.computeInlineEndMargin( + FlexDirection::Row, direction, containingBlockWidth); + const auto marginBlockStart = itemStyle.computeInlineStartMargin( + FlexDirection::Column, direction, containingBlockWidth); + const auto marginBlockEnd = itemStyle.computeInlineEndMargin( + FlexDirection::Column, direction, containingBlockWidth); + + auto itemConstraints = trackSizing.calculateItemConstraints( + item, containingBlockWidth, containingBlockHeight); + + calculateLayoutInternal( + item.node, + itemConstraints.width, + itemConstraints.height, + direction, + itemConstraints.widthSizingMode, + itemConstraints.heightSizingMode, + containingBlockWidth, + containingBlockHeight, + true, + LayoutPassReason::kGridLayout, + layoutMarkerData, + depth, + generationCount); + + auto justifySelf = resolveChildJustification(node, item.node); + auto alignSelf = resolveChildAlignment(node, item.node); + + // since we know the item width and grid width, we can do the alignment + // here. alignment of grid items happen in the grid area measured dimension + // includes padding and border + float actualItemWidth = + item.node->getLayout().measuredDimension(Dimension::Width); + auto freeSpaceInlineAxisItem = containingBlockWidth - actualItemWidth - + marginInlineStart - marginInlineEnd; + float startAutoMarginOffset = 0.0f; + // https://www.w3.org/TR/css-grid-1/#auto-margins + // auto margins in either axis absorb positive free space prior to alignment + // via the box alignment properties, thereby disabling the effects of any + // self-alignment properties in that axis. + if (freeSpaceInlineAxisItem > 0.0f) { + if (itemStyle.inlineStartMarginIsAuto(FlexDirection::Row, direction) && + itemStyle.inlineEndMarginIsAuto(FlexDirection::Row, direction)) { + startAutoMarginOffset = freeSpaceInlineAxisItem / 2; + freeSpaceInlineAxisItem = 0.0f; + } else if (itemStyle.inlineStartMarginIsAuto( + FlexDirection::Row, direction)) { + startAutoMarginOffset = freeSpaceInlineAxisItem; + freeSpaceInlineAxisItem = 0.0f; + } else if (itemStyle.inlineEndMarginIsAuto( + FlexDirection::Row, direction)) { + startAutoMarginOffset = 0.0f; + freeSpaceInlineAxisItem = 0.0f; + } + } + + float justifySelfOffset = 0.0f; + if (justifySelf == Justify::End) { + justifySelfOffset = freeSpaceInlineAxisItem; + } else if (justifySelf == Justify::Center) { + justifySelfOffset = freeSpaceInlineAxisItem / 2; + } + + float finalLeft = leadingPaddingAndBorderInline + gridItemInlineStart + + marginInlineStart + startAutoMarginOffset + justifySelfOffset + + gridInlineStartOffset; + + if (direction == Direction::RTL) { + finalLeft = getPositionOfOppositeEdge( + finalLeft, FlexDirection::Row, node, item.node); + } + + // Add relative position offset for relatively positioned items. + // For RTL, the relative position is in logical coordinates so we subtract + // it from the physical left. + float relativePositionInline = item.node->relativePosition( + FlexDirection::Row, direction, containingBlockWidth); + if (direction == Direction::RTL) { + item.node->setLayoutPosition( + finalLeft - relativePositionInline, PhysicalEdge::Left); + } else { + item.node->setLayoutPosition( + finalLeft + relativePositionInline, PhysicalEdge::Left); + } + + float actualItemHeight = + item.node->getLayout().measuredDimension(Dimension::Height); + auto freeSpaceBlockAxisItem = containingBlockHeight - actualItemHeight - + marginBlockStart - marginBlockEnd; + float topAutoMarginOffset = 0.0f; + if (freeSpaceBlockAxisItem > 0.0f) { + if (itemStyle.inlineStartMarginIsAuto(FlexDirection::Column, direction) && + itemStyle.inlineEndMarginIsAuto(FlexDirection::Column, direction)) { + topAutoMarginOffset = freeSpaceBlockAxisItem / 2; + freeSpaceBlockAxisItem = 0.0f; + } else if (itemStyle.inlineStartMarginIsAuto( + FlexDirection::Column, direction)) { + topAutoMarginOffset = freeSpaceBlockAxisItem; + freeSpaceBlockAxisItem = 0.0f; + } else if (itemStyle.inlineEndMarginIsAuto( + FlexDirection::Column, direction)) { + freeSpaceBlockAxisItem = 0.0f; + } + } + + float alignSelfOffset = 0.0f; + if (alignSelf == Align::End) { + alignSelfOffset = freeSpaceBlockAxisItem; + } else if (alignSelf == Align::Center) { + alignSelfOffset = freeSpaceBlockAxisItem / 2; + } else if (alignSelf == Align::Baseline) { + alignSelfOffset = item.baselineShim; + } + + float finalTop = gridItemBlockStart + marginBlockStart + + topAutoMarginOffset + alignSelfOffset + gridBlockStartOffset + + leadingPaddingAndBorderBlock; + + // Add relative position offset for relatively positioned items + float relativePositionBlock = item.node->relativePosition( + FlexDirection::Column, direction, containingBlockHeight); + item.node->setLayoutPosition( + finalTop + relativePositionBlock, PhysicalEdge::Top); + } + + // Perform layout of absolute children + // https://www.w3.org/TR/css-grid-1/#abspos + // TODO: support grid-[row|column]-[start|end] as containing blocks + if (nodeStyle.positionType() != PositionType::Static || + node->alwaysFormsContainingBlock() || depth == 1) { + for (auto child : node->getLayoutChildren()) { + if (child->style().display() == Display::None) { + zeroOutLayoutRecursively(child); + child->setHasNewLayout(true); + child->setDirty(false); + continue; + } + + if (child->style().positionType() == PositionType::Absolute) { + child->processDimensions(); + } + } + + layoutAbsoluteDescendants( + node, + node, + widthSizingMode, + direction, + layoutMarkerData, + depth, + generationCount, + 0.0f, + 0.0f, + availableInnerWidth, + availableInnerHeight); + } +} + +GridTracks createGridTracks( + yoga::Node* node, + const ResolvedAutoPlacement& autoPlacement) { + auto gridExplicitColumns = node->style().gridTemplateColumns(); + auto gridExplicitRows = node->style().gridTemplateRows(); + + std::vector columnTracks; + std::vector rowTracks; + columnTracks.reserve( + static_cast( + autoPlacement.maxColumnEnd - autoPlacement.minColumnStart)); + rowTracks.reserve( + static_cast(autoPlacement.maxRowEnd - autoPlacement.minRowStart)); + + // https://www.w3.org/TR/css-grid-1/#auto-tracks + auto autoRowTracks = node->style().gridAutoRows().empty() + ? GridTrackList{GridTrackSize{ + .minSizingFunction = StyleSizeLength::ofAuto(), + .maxSizingFunction = StyleSizeLength::ofAuto()}} + : node->style().gridAutoRows(); + auto autoColumnTracks = node->style().gridAutoColumns().empty() + ? GridTrackList{GridTrackSize{ + .minSizingFunction = StyleSizeLength::ofAuto(), + .maxSizingFunction = StyleSizeLength::ofAuto()}} + : node->style().gridAutoColumns(); + + // The last implicit grid track before the explicit grid receives the last + // specified size, and so on backwards. i.e. The pattern repeats backwards + auto negativeImplicitGridColumnTrackCount = -autoPlacement.minColumnStart; + auto autoColumnTracksSize = autoColumnTracks.size(); + for (auto i = 0; i < negativeImplicitGridColumnTrackCount; i++) { + auto currentColumnTrackIndex = + static_cast(negativeImplicitGridColumnTrackCount - i - 1) % + autoColumnTracksSize; + auto autoColumnTrack = + autoColumnTracks[autoColumnTracksSize - currentColumnTrackIndex - 1]; + columnTracks.push_back(autoColumnTrack); + } + + for (size_t i = 0; i < gridExplicitColumns.size(); i++) { + columnTracks.push_back(gridExplicitColumns[i]); + } + + // The first track after the last explicitly-sized track receives the first + // specified size i.e. the pattern repeats forwards + for (size_t i = 0; i < static_cast(autoPlacement.maxColumnEnd) - + gridExplicitColumns.size(); + i++) { + auto autoColumnTrack = autoColumnTracks[i % autoColumnTracksSize]; + columnTracks.push_back(autoColumnTrack); + } + + auto negativeImplicitGridRowTrackCount = -autoPlacement.minRowStart; + auto autoRowTracksSize = autoRowTracks.size(); + for (auto i = 0; i < negativeImplicitGridRowTrackCount; i++) { + auto currentRowTrackIndex = + static_cast(negativeImplicitGridRowTrackCount - i - 1) % + autoRowTracksSize; + auto autoRowTrack = + autoRowTracks[autoRowTracksSize - currentRowTrackIndex - 1]; + rowTracks.push_back(autoRowTrack); + } + for (const auto& explicitRow : gridExplicitRows) { + rowTracks.push_back(explicitRow); + } + for (size_t i = 0; i < + static_cast(autoPlacement.maxRowEnd) - gridExplicitRows.size(); + i++) { + auto autoRowTrack = autoRowTracks[i % autoRowTracksSize]; + rowTracks.push_back(autoRowTrack); + } + + return {std::move(columnTracks), std::move(rowTracks)}; +} + +} // namespace facebook::yoga diff --git a/yoga/algorithm/grid/GridLayout.h b/yoga/algorithm/grid/GridLayout.h new file mode 100644 index 0000000000..f6699c1366 --- /dev/null +++ b/yoga/algorithm/grid/GridLayout.h @@ -0,0 +1,42 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace facebook::yoga { + +void calculateGridLayoutInternal( + yoga::Node* node, + float availableWidth, + float availableHeight, + Direction ownerDirection, + SizingMode widthSizingMode, + SizingMode heightSizingMode, + float ownerWidth, + float ownerHeight, + bool performLayout, + LayoutPassReason reason, + LayoutData& layoutMarkerData, + uint32_t depth, + uint32_t generationCount); + +struct GridTracks { + std::vector columnTracks; + std::vector rowTracks; +}; +// Creates implicit grid tracks based on the auto placement result +GridTracks createGridTracks( + yoga::Node* node, + const ResolvedAutoPlacement& autoPlacement); + +} // namespace facebook::yoga diff --git a/yoga/algorithm/grid/TrackSizing.h b/yoga/algorithm/grid/TrackSizing.h new file mode 100644 index 0000000000..77802b2d9c --- /dev/null +++ b/yoga/algorithm/grid/TrackSizing.h @@ -0,0 +1,2052 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace facebook::yoga { + +struct TrackSizing { + enum class AffectedSize { BaseSize, GrowthLimit }; + + struct ContentDistribution { + float startOffset = 0.0f; + float betweenTracksOffset = 0.0f; + float effectiveGap = 0.0f; + }; + + struct ItemConstraint { + float width; + float height; + SizingMode widthSizingMode; + SizingMode heightSizingMode; + float containingBlockWidth; + float containingBlockHeight; + }; + + using CrossDimensionEstimator = std::function; + + struct ItemSizeContribution { + const GridItem* item; + std::vector affectedTracks; + float sizeContribution; + + ItemSizeContribution( + const GridItem* item, + const std::vector& affectedTracks, + float sizeContribution) + : item(item), + affectedTracks(affectedTracks), + sizeContribution(sizeContribution) {} + }; + + Node* node; + std::vector& + columnTracks; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) + std::vector& + rowTracks; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) + float containerInnerWidth; + float containerInnerHeight; + std::vector& + gridItems; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) + SizingMode widthSizingMode; + SizingMode heightSizingMode; + Direction direction; + float ownerWidth; + float ownerHeight; + LayoutData& + layoutMarkerData; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) + uint32_t depth; + uint32_t generationCount; + CrossDimensionEstimator crossDimensionEstimator; + + // below flags are used for optimization purposes + bool hasPercentageColumnTracks = false; + bool hasPercentageRowTracks = false; + bool hasOnlyFixedTracks = false; + bool hasIntrinsicTracks = false; + bool hasFlexibleTracks = false; + + // Pre-computed baseline sharing groups + BaselineItemGroups& + baselineItemGroups; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) + + TrackSizing( + yoga::Node* node, + std::vector& columnTracks, + std::vector& rowTracks, + float containerInnerWidth, + float containerInnerHeight, + std::vector& gridItems, + SizingMode widthSizingMode, + SizingMode heightSizingMode, + Direction direction, + float ownerWidth, + float ownerHeight, + LayoutData& layoutMarkerData, + uint32_t depth, + uint32_t generationCount, + BaselineItemGroups& baselineItemGroups) + : node(node), + columnTracks(columnTracks), + rowTracks(rowTracks), + containerInnerWidth(containerInnerWidth), + containerInnerHeight(containerInnerHeight), + gridItems(gridItems), + widthSizingMode(widthSizingMode), + heightSizingMode(heightSizingMode), + direction(direction), + ownerWidth(ownerWidth), + ownerHeight(ownerHeight), + layoutMarkerData(layoutMarkerData), + depth(depth), + generationCount(generationCount), + baselineItemGroups(baselineItemGroups) {} + + // 11.1. Grid Sizing Algorithm + // https://www.w3.org/TR/css-grid-1/#algo-grid-sizing + void runGridSizingAlgorithm() { + computeItemTrackCrossingFlags(); + + // 1. First, the track sizing algorithm is used to resolve the sizes of the + // grid columns. + auto rowHeightFromFixedTracks = makeRowHeightEstimatorUsingFixedTracks( + calculateEffectiveRowGapForEstimation()); + runTrackSizing(Dimension::Width, rowHeightFromFixedTracks); + + // 2. Next, the track sizing algorithm resolves the sizes of the grid rows. + auto columnWidthFromBaseSizes = makeCrossDimensionEstimatorUsingBaseSize( + Dimension::Width, calculateEffectiveGapFromBaseSizes(Dimension::Width)); + runTrackSizing(Dimension::Height, columnWidthFromBaseSizes); + + // 3. Then, if the min-content contribution of any grid item has changed + // Only intrinsic tracks can affect the cross track size in above steps, so + // this step is only needed if there are intrinsic tracks + if (hasIntrinsicTracks) { + auto rowHeightFromBaseSizes = makeCrossDimensionEstimatorUsingBaseSize( + Dimension::Height, + calculateEffectiveGapFromBaseSizes(Dimension::Height)); + if (contributionsChanged( + Dimension::Width, + rowHeightFromFixedTracks, + rowHeightFromBaseSizes)) { + runTrackSizing(Dimension::Width, rowHeightFromBaseSizes); + // 4. Next, if the min-content contribution of any grid item has changed + auto newColumnWidthFromBaseSizes = + makeCrossDimensionEstimatorUsingBaseSize( + Dimension::Width, + calculateEffectiveGapFromBaseSizes(Dimension::Width)); + if (contributionsChanged( + Dimension::Height, + columnWidthFromBaseSizes, + newColumnWidthFromBaseSizes)) { + runTrackSizing(Dimension::Height, newColumnWidthFromBaseSizes); + } + } + } + } + + // 11.3. Track Sizing Algorithm + // https://www.w3.org/TR/css-grid-1/#algo-track-sizing + void runTrackSizing( + Dimension dimension, + CrossDimensionEstimator estimator = nullptr) { + // Store the estimator for use in calculateItemConstraints + crossDimensionEstimator = estimator; + + // Step 1: Initialize Track Sizes + initializeTrackSizes(dimension); + // Step 2: Resolve Intrinsic Track Sizes + resolveIntrinsicTrackSizes(dimension); + // Step 3: Maximize Track Sizes + maximizeTrackSizes(dimension); + // Step 4: Expand Flexible Tracks + expandFlexibleTracks(dimension); + // Step 5: Stretch Auto Tracks + stretchAutoTracks(dimension); + } + + // 11.4 Initialize Track Sizes + // https://www.w3.org/TR/css-grid-1/#algo-init + // Also sets some flags (hasPercentageTracks, hasOnlyFixedTracks) for + // optimization purposes + void initializeTrackSizes(Dimension dimension) { + auto& tracks = dimension == Dimension::Width ? columnTracks : rowTracks; + auto containerSize = dimension == Dimension::Width ? containerInnerWidth + : containerInnerHeight; + bool& hasPercentageTracks = dimension == Dimension::Width + ? hasPercentageColumnTracks + : hasPercentageRowTracks; + hasOnlyFixedTracks = true; + hasIntrinsicTracks = false; + hasFlexibleTracks = false; + + for (size_t i = 0; i < tracks.size(); i++) { + auto& track = tracks[i]; + + // detect percentage tracks for optimization purposes + if (isPercentageSizingFunction(track.minSizingFunction) || + isPercentageSizingFunction(track.maxSizingFunction)) { + hasPercentageTracks = true; + } + + if (isFixedSizingFunction(track.minSizingFunction, containerSize)) { + auto resolved = track.minSizingFunction.resolve(containerSize); + track.baseSize = resolved.unwrap(); + } else if (isIntrinsicSizingFunction( + track.minSizingFunction, containerSize)) { + track.baseSize = 0; + hasOnlyFixedTracks = false; + hasIntrinsicTracks = true; + } else { + // THIS SHOULD NEVER HAPPEN + track.baseSize = 0; + } + + if (isFixedSizingFunction(track.maxSizingFunction, containerSize)) { + auto resolved = track.maxSizingFunction.resolve(containerSize); + track.growthLimit = resolved.unwrap(); + } else if (isIntrinsicSizingFunction( + track.maxSizingFunction, containerSize)) { + track.growthLimit = INFINITY; + hasOnlyFixedTracks = false; + hasIntrinsicTracks = true; + } else if (isFlexibleSizingFunction(track.maxSizingFunction)) { + track.growthLimit = INFINITY; + hasOnlyFixedTracks = false; + hasFlexibleTracks = true; + } else { + // THIS SHOULD NEVER HAPPEN + track.growthLimit = INFINITY; + } + + // In all cases, if the growth limit is less than the base size, increase + // the growth limit to match the base size. + if (track.growthLimit < track.baseSize) { + track.growthLimit = track.baseSize; + } + + // minmax(20px, 40px) type of tracks are not fixed tracks + if (track.baseSize < track.growthLimit) { + hasOnlyFixedTracks = false; + } + } + } + + // 11.5 Resolve Intrinsic Track Sizes + // https://www.w3.org/TR/css-grid-1/#algo-content + void resolveIntrinsicTrackSizes(Dimension dimension) { + auto& tracks = dimension == Dimension::Width ? columnTracks : rowTracks; + + // Step 1: Shim baseline-aligned items (only for height dimension i.e. + // align-items/align-self) + if (dimension == Dimension::Height) { + shimBaselineAlignedItems(); + } + + // Fast path - if tracks are fixed-sized, skip below steps + if (hasOnlyFixedTracks) { + return; + } + + // Step 2. and Step 3 Increase sizes to accommodate spanning items + accomodateSpanningItemsCrossingContentSizedTracks(dimension); + // Step 4. Increase sizes to accommodate spanning items crossing flexible + // tracks + accomodateSpanningItemsCrossingFlexibleTracks(dimension); + // Step 5. If any track still has an infinite growth limit (because, for + // example, it had no items placed in it or it is a flexible track), set its + // growth limit to its base size. + for (auto& track : tracks) { + if (track.growthLimit == INFINITY) { + track.growthLimit = track.baseSize; + } + } + } + + // https://www.w3.org/TR/css-grid-1/#algo-baseline-shims + void shimBaselineAlignedItems() { + for (const auto& [rowIndex, items] : baselineItemGroups) { + float maxBaselineWithMargin = 0.0f; + std::vector> itemBaselines; + itemBaselines.reserve(items.size()); + + for (auto* itemPtr : items) { + const auto& item = *itemPtr; + + if (itemSizeDependsOnIntrinsicTracks(item)) { + continue; + } + + float containingBlockWidth = crossDimensionEstimator + ? crossDimensionEstimator(item) + : YGUndefined; + float containingBlockHeight = YGUndefined; + + auto itemConstraints = calculateItemConstraints( + item, containingBlockWidth, containingBlockHeight); + + calculateLayoutInternal( + item.node, + itemConstraints.width, + itemConstraints.height, + node->getLayout().direction(), + SizingMode::MaxContent, + itemConstraints.heightSizingMode, + itemConstraints.containingBlockWidth, + itemConstraints.containingBlockHeight, + true, + LayoutPassReason::kGridLayout, + layoutMarkerData, + depth + 1, + generationCount); + + const float baseline = calculateBaseline(item.node); + const float marginTop = item.node->style().computeInlineStartMargin( + FlexDirection::Column, + direction, + itemConstraints.containingBlockWidth); + const float baselineWithMargin = baseline + marginTop; + + itemBaselines.emplace_back(itemPtr, baselineWithMargin); + maxBaselineWithMargin = + std::max(maxBaselineWithMargin, baselineWithMargin); + } + + for (auto& [itemPtr, baselineWithMargin] : itemBaselines) { + itemPtr->baselineShim = maxBaselineWithMargin - baselineWithMargin; + } + } + } + + // https://www.w3.org/TR/css-grid-1/#algo-single-span-items + // https://www.w3.org/TR/css-grid-1/#algo-spanning-items + void accomodateSpanningItemsCrossingContentSizedTracks(Dimension dimension) { + if (!hasIntrinsicTracks) { + return; + } + auto& tracks = dimension == Dimension::Width ? columnTracks : rowTracks; + auto containerSize = dimension == Dimension::Width ? containerInnerWidth + : containerInnerHeight; + auto sizingMode = + dimension == Dimension::Width ? widthSizingMode : heightSizingMode; + + auto startIndexKey = dimension == Dimension::Width ? &GridItem::columnStart + : &GridItem::rowStart; + auto endIndexKey = dimension == Dimension::Width ? &GridItem::columnEnd + : &GridItem::rowEnd; + + // 2. Size tracks to fit non-spanning items (span = 1 items) + // https://www.w3.org/TR/css-grid-1/#algo-single-span-items + std::vector spanningItemIndices; + spanningItemIndices.reserve(gridItems.size()); + for (size_t index = 0; index < gridItems.size(); index++) { + const auto& item = gridItems[index]; + if (item.crossesFlexibleTrack(dimension)) { + continue; + } + auto startIndex = item.*startIndexKey; + auto endIndex = item.*endIndexKey; + size_t span = endIndex - startIndex; + if (span == 1) { + auto& track = tracks[startIndex]; + auto itemConstraints = calculateItemConstraints(item, dimension); + // For auto minimums: + if (isAutoSizingFunction(track.minSizingFunction, containerSize)) { + float contribution = sizingMode == SizingMode::MaxContent + ? limitedMinContentContribution(item, dimension, itemConstraints) + : minimumContribution(item, dimension, itemConstraints); + track.baseSize = std::max(track.baseSize, contribution); + } + + // For max-content maximums: + if (isAutoSizingFunction(track.maxSizingFunction, containerSize)) { + float contribution = + maxContentContribution(item, dimension, itemConstraints); + if (track.growthLimit == INFINITY) { + track.growthLimit = contribution; + } else { + track.growthLimit = std::max(track.growthLimit, contribution); + } + } + // In all cases, if a track's growth limit is now less than its base + // size, increase the growth limit to match the base size. + if (track.growthLimit < track.baseSize) { + track.growthLimit = track.baseSize; + } + } else { + spanningItemIndices.push_back(index); + } + } + + // 3. Increase sizes to accommodate spanning items crossing content-sized + // tracks: https://www.w3.org/TR/css-grid-1/#algo-spanning-items + if (spanningItemIndices.empty()) { + return; + } + + std::sort( + spanningItemIndices.begin(), + spanningItemIndices.end(), + [&](size_t i, size_t j) { + const auto& a = gridItems[i]; + const auto& b = gridItems[j]; + return (a.*endIndexKey - a.*startIndexKey) < + (b.*endIndexKey - b.*startIndexKey); + }); + + size_t previousSpan = 1; + std::vector itemsForIntrinsicMin; + std::vector itemsForIntrinsicMax; + std::vector itemsForMaxContentMax; + + auto distributeSpaceToTracksForItemsWithTheSameSpan = [&]() { + // Step 1: For intrinsic minimums + if (!itemsForIntrinsicMin.empty()) { + distributeExtraSpaceAcrossSpannedTracks( + dimension, itemsForIntrinsicMin, AffectedSize::BaseSize); + itemsForIntrinsicMin.clear(); + } + + // Step 2 and Step 3 are skipped since we're not supporting min-content + // and max-content yet + + // Step 4: If at this point any track's growth limit is now less than its + // base size, increase its growth limit to match its base size + for (auto& track : tracks) { + if (track.growthLimit < track.baseSize) { + track.growthLimit = track.baseSize; + } + + // https://www.w3.org/TR/css-grid-1/#infinitely-growable + // reset infinitely growable flag for each track + // This flag gets set in Step 5 and used in Step 6, so we need to reset + // it before running Step 5. + track.infinitelyGrowable = false; + } + + // Step 5: For intrinsic maximums + if (!itemsForIntrinsicMax.empty()) { + distributeExtraSpaceAcrossSpannedTracks( + dimension, itemsForIntrinsicMax, AffectedSize::GrowthLimit); + itemsForIntrinsicMax.clear(); + } + + // Step 6: For max-content maximums + if (!itemsForMaxContentMax.empty()) { + distributeExtraSpaceAcrossSpannedTracks( + dimension, itemsForMaxContentMax, AffectedSize::GrowthLimit); + itemsForMaxContentMax.clear(); + } + }; + + for (auto& index : spanningItemIndices) { + const auto& item = gridItems[index]; + + if (item.crossesFlexibleTrack(dimension)) { + continue; + } + + auto startIndex = item.*startIndexKey; + auto endIndex = item.*endIndexKey; + size_t span = endIndex - startIndex; + + if (span > previousSpan) { + distributeSpaceToTracksForItemsWithTheSameSpan(); + previousSpan = span; + } + + std::vector intrinsicMinimumSizingFunctionTracks; + std::vector intrinsicMaximumSizingFunctionTracks; + std::vector maxContentMaximumSizingFunctionTracks; + + for (size_t i = startIndex; i < endIndex; i++) { + if (isIntrinsicSizingFunction( + tracks[i].minSizingFunction, containerSize)) { + intrinsicMinimumSizingFunctionTracks.push_back(&tracks[i]); + } + + if (isIntrinsicSizingFunction( + tracks[i].maxSizingFunction, containerSize)) { + intrinsicMaximumSizingFunctionTracks.push_back(&tracks[i]); + } + + // auto as max sizing function is treated as max-content sizing function + if (isAutoSizingFunction(tracks[i].maxSizingFunction, containerSize)) { + maxContentMaximumSizingFunctionTracks.push_back(&tracks[i]); + } + } + + auto itemConstraints = calculateItemConstraints(item, dimension); + if (!intrinsicMinimumSizingFunctionTracks.empty()) { + auto minContribution = sizingMode == SizingMode::MaxContent + ? limitedMinContentContribution(item, dimension, itemConstraints) + : minimumContribution(item, dimension, itemConstraints); + itemsForIntrinsicMin.emplace_back( + &item, + std::move(intrinsicMinimumSizingFunctionTracks), + minContribution); + } + + if (!intrinsicMaximumSizingFunctionTracks.empty()) { + auto minContentContrib = + minContentContribution(item, dimension, itemConstraints); + itemsForIntrinsicMax.emplace_back( + &item, + std::move(intrinsicMaximumSizingFunctionTracks), + minContentContrib); + } + + if (!maxContentMaximumSizingFunctionTracks.empty()) { + auto maxContentContrib = + maxContentContribution(item, dimension, itemConstraints); + itemsForMaxContentMax.emplace_back( + &item, + std::move(maxContentMaximumSizingFunctionTracks), + maxContentContrib); + } + } + + // Process last span + distributeSpaceToTracksForItemsWithTheSameSpan(); + }; + + // https://www.w3.org/TR/css-grid-1/#algo-spanning-flex-items + void accomodateSpanningItemsCrossingFlexibleTracks(Dimension dimension) { + if (!hasFlexibleTracks) { + return; + } + auto& tracks = dimension == Dimension::Width ? columnTracks : rowTracks; + auto sizingMode = + dimension == Dimension::Width ? widthSizingMode : heightSizingMode; + auto startIndexkey = dimension == Dimension::Width ? &GridItem::columnStart + : &GridItem::rowStart; + auto endIndexKey = dimension == Dimension::Width ? &GridItem::columnEnd + : &GridItem::rowEnd; + + std::vector itemsSpanningFlexible; + + for (const auto& item : gridItems) { + if (!item.crossesFlexibleTrack(dimension)) { + continue; + } + + auto start = item.*startIndexkey; + auto end = item.*endIndexKey; + std::vector flexibleTracks; + + for (size_t i = start; i < end && i < tracks.size(); i++) { + auto& track = tracks[i]; + if (isFlexibleSizingFunction(track.maxSizingFunction)) { + flexibleTracks.push_back(&track); + } + } + + if (!flexibleTracks.empty()) { + auto itemConstraints = calculateItemConstraints(item, dimension); + auto minContribution = sizingMode == SizingMode::MaxContent + ? limitedMinContentContribution(item, dimension, itemConstraints) + : minimumContribution(item, dimension, itemConstraints); + + itemsSpanningFlexible.emplace_back( + &item, std::move(flexibleTracks), minContribution); + } + } + + if (!itemsSpanningFlexible.empty()) { + distributeSpaceToFlexibleTracksForItems(dimension, itemsSpanningFlexible); + } + }; + + // https://www.w3.org/TR/css-grid-1/#extra-space + void distributeExtraSpaceAcrossSpannedTracks( + Dimension dimension, + std::vector& gridItemSizeContributions, + AffectedSize affectedSizeType) { + auto& tracks = dimension == Dimension::Width ? columnTracks : rowTracks; + auto startIndexKey = dimension == Dimension::Width ? &GridItem::columnStart + : &GridItem::rowStart; + auto endIndexKey = dimension == Dimension::Width ? &GridItem::columnEnd + : &GridItem::rowEnd; + auto containerSize = dimension == Dimension::Width ? containerInnerWidth + : containerInnerHeight; + auto gap = node->style().computeGapForDimension(dimension, containerSize); + std::unordered_map plannedIncrease; + plannedIncrease.reserve(gridItemSizeContributions.size()); + + // 1. Maintain separately for each affected track a planned increase, + // initially set to 0. (This prevents the size increases from becoming + // order-dependent.) + for (const auto& itemSizeContribution : gridItemSizeContributions) { + for (auto& track : itemSizeContribution.affectedTracks) { + plannedIncrease[track] = 0.0f; + } + } + + // 2. For each accommodated item, considering only tracks the item spans: + for (const auto& itemSizeContribution : gridItemSizeContributions) { + std::unordered_map itemIncurredIncrease; + itemIncurredIncrease.reserve(itemSizeContribution.affectedTracks.size()); + for (auto& track : itemSizeContribution.affectedTracks) { + itemIncurredIncrease[track] = 0.0f; + } + + // 2.1 Find the space to distribute + auto start = itemSizeContribution.item->*startIndexKey; + auto end = itemSizeContribution.item->*endIndexKey; + float totalSpannedTracksSize = 0.0f; + for (size_t i = start; i < end && i < tracks.size(); i++) { + auto& track = tracks[i]; + if (affectedSizeType == AffectedSize::BaseSize) { + totalSpannedTracksSize += track.baseSize; + } else { + // For infinite growth limits, substitute the track's base size + totalSpannedTracksSize += track.growthLimit == INFINITY + ? track.baseSize + : track.growthLimit; + } + if (i < end - 1) { + // gaps are treated as tracks of fixed size. Item can span over gaps. + totalSpannedTracksSize += gap; + } + } + + float spaceToDistribute = std::max( + 0.0f, itemSizeContribution.sizeContribution - totalSpannedTracksSize); + std::unordered_set frozenTracks; + frozenTracks.reserve(itemSizeContribution.affectedTracks.size()); + + // 2.2. Distribute space up to limits + while (frozenTracks.size() < itemSizeContribution.affectedTracks.size() && + spaceToDistribute > 0.0f && + !yoga::inexactEquals(spaceToDistribute, 0.0f)) { + auto unfrozenTrackCount = + itemSizeContribution.affectedTracks.size() - frozenTracks.size(); + auto distributionPerTrack = + spaceToDistribute / static_cast(unfrozenTrackCount); + + for (auto& track : itemSizeContribution.affectedTracks) { + if (frozenTracks.contains(track)) { + continue; + } + + float limit; + float affectedSize; + + if (affectedSizeType == AffectedSize::BaseSize) { + affectedSize = track->baseSize; + limit = track->growthLimit; + } else { + affectedSize = track->growthLimit; + limit = INFINITY; + if (track->growthLimit != INFINITY && !track->infinitelyGrowable) { + limit = track->growthLimit; + } + + // If the affected size was a growth limit and the track is not + // marked infinitely growable, then each item-incurred increase will + // be zero. + if (!track->infinitelyGrowable) { + frozenTracks.insert(track); + continue; + } + } + + if (affectedSize + distributionPerTrack + + itemIncurredIncrease[track] > + limit) { + frozenTracks.insert(track); + auto increase = limit - affectedSize - itemIncurredIncrease[track]; + itemIncurredIncrease[track] += increase; + spaceToDistribute -= increase; + } else { + itemIncurredIncrease[track] += distributionPerTrack; + spaceToDistribute -= distributionPerTrack; + } + } + } + + // 2.3. Distribute space to non-affected tracks: + // Currently, browsers do not implement this step. + // https://github.com/w3c/csswg-drafts/issues/3648 + + // 2.4. Distribute space beyond limits + if (spaceToDistribute > 0.0f && + !yoga::inexactEquals(spaceToDistribute, 0.0f)) { + std::vector tracksToGrowBeyondLimits; + for (auto& track : itemSizeContribution.affectedTracks) { + if (isIntrinsicSizingFunction( + track->maxSizingFunction, containerSize)) { + tracksToGrowBeyondLimits.push_back(track); + } + } + + // if there are no such tracks, then all affected tracks. + if (affectedSizeType == AffectedSize::BaseSize && + tracksToGrowBeyondLimits.empty()) { + tracksToGrowBeyondLimits = itemSizeContribution.affectedTracks; + } + + while (spaceToDistribute > 0.0f && + !yoga::inexactEquals(spaceToDistribute, 0.0f) && + !tracksToGrowBeyondLimits.empty()) { + auto unfrozenTrackCount = tracksToGrowBeyondLimits.size(); + auto distributionPerTrack = + spaceToDistribute / static_cast(unfrozenTrackCount); + for (auto& track : tracksToGrowBeyondLimits) { + itemIncurredIncrease[track] += distributionPerTrack; + spaceToDistribute -= distributionPerTrack; + } + } + } + + // 2.5. For each affected track, if the track's item-incurred increase is + // larger than the track's planned increase set the track's planned + // increase to that value. + for (auto& track : itemSizeContribution.affectedTracks) { + if (itemIncurredIncrease[track] > plannedIncrease[track]) { + plannedIncrease[track] = itemIncurredIncrease[track]; + } + } + } + + // 3. Update the tracks affected sizes + for (const auto& [track, increase] : plannedIncrease) { + if (affectedSizeType == AffectedSize::BaseSize) { + track->baseSize += increase; + } else { + if (track->growthLimit == INFINITY) { + track->growthLimit = track->baseSize + increase; + track->infinitelyGrowable = true; + } else { + track->growthLimit += increase; + } + } + } + } + + // https://www.w3.org/TR/css-grid-1/#extra-space + // Similar to distribute extra space for content sized tracks, but distributes + // space considering flex factors. + void distributeSpaceToFlexibleTracksForItems( + Dimension dimension, + const std::vector& gridItemSizeContributions) { + auto& tracks = dimension == Dimension::Width ? columnTracks : rowTracks; + auto containerSize = dimension == Dimension::Width ? containerInnerWidth + : containerInnerHeight; + auto gap = node->style().computeGapForDimension(dimension, containerSize); + auto startIndexKey = dimension == Dimension::Width ? &GridItem::columnStart + : &GridItem::rowStart; + auto endIndexKey = dimension == Dimension::Width ? &GridItem::columnEnd + : &GridItem::rowEnd; + + // Step 1: Maintain planned increase for each affected track + std::unordered_map plannedIncrease; + for (const auto& itemSizeContribution : gridItemSizeContributions) { + for (auto& track : itemSizeContribution.affectedTracks) { + plannedIncrease[track] = 0.0f; + } + } + + // Step 2: For each item + for (const auto& itemSizeContribution : gridItemSizeContributions) { + std::unordered_map itemIncurredIncrease; + for (auto& track : itemSizeContribution.affectedTracks) { + itemIncurredIncrease[track] = 0.0f; + } + + // 2.1 Find space to distribute + auto start = itemSizeContribution.item->*startIndexKey; + auto end = itemSizeContribution.item->*endIndexKey; + float totalSpannedTracksSize = 0.0f; + for (size_t i = start; i < end && i < tracks.size(); i++) { + totalSpannedTracksSize += tracks[i].baseSize; + if (i < end - 1) { + // gaps are treated as tracks of fixed size. Item can span over gaps. + totalSpannedTracksSize += gap; + } + } + + float spaceToDistribute = std::max( + 0.0f, itemSizeContribution.sizeContribution - totalSpannedTracksSize); + + float sumOfFlexFactors = 0.0f; + for (auto& track : itemSizeContribution.affectedTracks) { + sumOfFlexFactors += track->maxSizingFunction.value().unwrap(); + } + + if (sumOfFlexFactors > 0.0f) { + // Distribute space by flex ratios (normalized) + for (auto& track : itemSizeContribution.affectedTracks) { + auto flexFactor = track->maxSizingFunction.value().unwrap(); + auto increase = spaceToDistribute * flexFactor / sumOfFlexFactors; + itemIncurredIncrease[track] += increase; + } + } else { + // All flex factors are zero, distribute equally + auto equalShare = spaceToDistribute / + static_cast(itemSizeContribution.affectedTracks.size()); + for (auto& track : itemSizeContribution.affectedTracks) { + itemIncurredIncrease[track] += equalShare; + } + } + + for (auto& track : itemSizeContribution.affectedTracks) { + if (itemIncurredIncrease[track] > plannedIncrease[track]) { + plannedIncrease[track] = itemIncurredIncrease[track]; + } + } + } + + // Step 3: Update the tracks' affected sizes by adding in the planned + // increase + for (const auto& [track, increase] : plannedIncrease) { + track->baseSize += increase; + } + }; + + // 11.6. Maximize Tracks + // https://www.w3.org/TR/css-grid-1/#algo-grow-tracks + void maximizeTrackSizes(Dimension dimension) { + // Fast path - if tracks are fixed-sized, skip below steps + if (hasOnlyFixedTracks) { + return; + } + + auto& tracks = dimension == Dimension::Width ? columnTracks : rowTracks; + auto containerSize = dimension == Dimension::Width ? containerInnerWidth + : containerInnerHeight; + + // Save original base sizes before maximization + std::vector originalBaseSizes; + originalBaseSizes.reserve(tracks.size()); + for (auto& track : tracks) { + originalBaseSizes.push_back(track.baseSize); + } + + // First attempt with the original container inner size + distributeFreeSpaceToTracks(dimension, containerSize); + + // Check if this would cause the grid to be larger than the grid container's + // inner size as limited by its max-width/height + auto totalGridSize = getTotalBaseSize(dimension); + + // Get the max constraint for this dimension + const float paddingAndBorder = dimension == Dimension::Width + ? paddingAndBorderForAxis( + node, FlexDirection::Row, direction, ownerWidth) + : paddingAndBorderForAxis( + node, FlexDirection::Column, direction, ownerWidth); + + auto maxContainerBorderBoxSize = node->style().resolvedMaxDimension( + direction, + dimension, + dimension == Dimension::Width ? ownerWidth : ownerHeight, + ownerWidth); + + auto maxContainerInnerSize = maxContainerBorderBoxSize.isDefined() + ? maxContainerBorderBoxSize.unwrap() - paddingAndBorder + : YGUndefined; + + if (yoga::isDefined(maxContainerInnerSize)) { + if (totalGridSize > maxContainerInnerSize) { + // Redo this step, treating the available grid space as equal to the + // grid container's inner size when it's sized to its max-width/height + // Reset base sizes to their values before this maximize step + for (size_t i = 0; i < tracks.size(); i++) { + tracks[i].baseSize = originalBaseSizes[i]; + } + + distributeFreeSpaceToTracks(dimension, maxContainerInnerSize); + } + } + } + + // Distribute space in maximizeTrackSizes step + // https://www.w3.org/TR/css-grid-1/#algo-grow-tracks + void distributeFreeSpaceToTracks( + Dimension dimension, + float targetAvailableSize) { + auto& tracks = dimension == Dimension::Width ? columnTracks : rowTracks; + auto sizingMode = + dimension == Dimension::Width ? widthSizingMode : heightSizingMode; + float freeSpace = 0.0f; + if (yoga::isDefined(targetAvailableSize)) { + auto totalBaseSize = getTotalBaseSize(dimension); + freeSpace = std::max(0.0f, targetAvailableSize - totalBaseSize); + } + + // For the purpose of this step: if sizing the grid container under a + // max-content constraint, the free space is infinite; if sizing under a + // min-content constraint, the free space is zero. + if (sizingMode == SizingMode::MaxContent) { + freeSpace = INFINITY; + } + + // If the free space is positive, distribute it equally to the base sizes of + // all tracks, freezing tracks as they reach their growth limits (and + // continuing to grow the unfrozen tracks as needed). + if (freeSpace > 0.0f && !yoga::inexactEquals(freeSpace, 0.0f)) { + // growth limit will not be Infinite in maximizeTrackSizes step since we + // had set Infinite growth limit to base size in + // resolveIntrinsicTrackSizes's last step - + // https://www.w3.org/TR/css-grid-1/#algo-finite-growth + if (freeSpace == INFINITY) { + for (auto& track : tracks) { + track.baseSize = track.growthLimit; + } + } else { + std::unordered_set frozenTracks; + frozenTracks.reserve(tracks.size()); + auto extraSpace = freeSpace; + + while (frozenTracks.size() < tracks.size() && extraSpace > 0.0f && + !yoga::inexactEquals(extraSpace, 0.0f)) { + auto unfrozenTrackCount = tracks.size() - frozenTracks.size(); + auto distributionPerTrack = + extraSpace / static_cast(unfrozenTrackCount); + + for (auto& track : tracks) { + GridTrackSize* trackPtr = &track; + if (frozenTracks.contains(trackPtr)) { + continue; + } + + // Check if adding this distribution would exceed the growth limit + if (track.baseSize + distributionPerTrack > track.growthLimit) { + auto increase = + std::max(0.0f, track.growthLimit - track.baseSize); + track.baseSize += increase; + extraSpace -= increase; + frozenTracks.insert(trackPtr); + } else { + track.baseSize += distributionPerTrack; + extraSpace -= distributionPerTrack; + } + } + } + } + } + } + + // 11.7. Expand Flexible Tracks + // https://www.w3.org/TR/css-grid-1/#algo-flex-tracks + void expandFlexibleTracks(Dimension dimension) { + if (!hasFlexibleTracks) { + return; + } + + auto& gridTracks = dimension == Dimension::Width ? columnTracks : rowTracks; + auto containerSize = dimension == Dimension::Width ? containerInnerWidth + : containerInnerHeight; + auto gap = node->style().computeGapForDimension(dimension, containerSize); + + float freeSpace = calculateFreeSpace(dimension); + + float flexFraction = 0.0f; + // If the free space is zero or if sizing the grid container under a + // min-content constraint: + if (yoga::inexactEquals(freeSpace, 0.0f)) { + flexFraction = 0.0f; + } + // Otherwise, if the free space is a definite length: + // The used flex fraction is the result of finding the size of an fr using + // all of the grid tracks and a space to fill of the available grid space. + else if (yoga::isDefined(freeSpace)) { + flexFraction = findFrSize( + dimension, + 0, + gridTracks.size(), + containerSize, + std::unordered_set()); + } + // Otherwise, if the free space is an indefinite length: + // The used flex fraction is the maximum of: + // For each flexible track, if the flexible track's flex factor is greater + // than one, the result of dividing the track's base size by its flex + // factor; otherwise, the track's base size. For each grid item that crosses + // a flexible track, the result of finding the size of an fr using all the + // grid tracks that the item crosses and a space to fill of the item’s + // max-content contribution. + else { + for (auto& track : gridTracks) { + if (isFlexibleSizingFunction(track.maxSizingFunction) && + track.maxSizingFunction.value().isDefined()) { + float flexFactor = track.maxSizingFunction.value().unwrap(); + if (flexFactor > 1.0f) { + flexFraction = std::max(flexFraction, track.baseSize / flexFactor); + } else { + flexFraction = std::max(flexFraction, track.baseSize); + } + } + } + + auto startIndexKey = dimension == Dimension::Width + ? &GridItem::columnStart + : &GridItem::rowStart; + auto endIndexKey = dimension == Dimension::Width ? &GridItem::columnEnd + : &GridItem::rowEnd; + + for (auto& item : gridItems) { + if (!item.crossesFlexibleTrack(dimension)) { + continue; + } + auto itemConstraints = calculateItemConstraints(item, dimension); + auto itemMaxContentContribution = + maxContentContribution(item, dimension, itemConstraints); + flexFraction = std::max( + flexFraction, + findFrSize( + dimension, + item.*startIndexKey, + item.*endIndexKey, + itemMaxContentContribution, + std::unordered_set())); + } + } + + // If using this flex fraction would cause the grid to be smaller than the + // grid container's min-width/height (or larger than the grid container's + // max-width/height), then redo this step, treating the free space as + // definite and the available grid space as equal to the grid container's + // inner size when it's sized to its min-width/height (max-width/height). + + // Calculate what the grid size would be with this flex fraction + float newTotalSize = 0.0f; + for (size_t i = 0; i < gridTracks.size(); i++) { + auto& track = gridTracks[i]; + if (isFlexibleSizingFunction(track.maxSizingFunction) && + track.maxSizingFunction.value().isDefined()) { + float flexFactor = track.maxSizingFunction.value().unwrap(); + newTotalSize += std::max(track.baseSize, flexFraction * flexFactor); + } else { + newTotalSize += track.baseSize; + } + if (i < gridTracks.size() - 1) { + newTotalSize += gap; + } + } + + // Check min constraint for this dimension + const float paddingAndBorder = dimension == Dimension::Width + ? paddingAndBorderForAxis( + node, FlexDirection::Row, direction, ownerWidth) + : paddingAndBorderForAxis( + node, FlexDirection::Column, direction, ownerWidth); + auto minContainerOuter = node->style().resolvedMinDimension( + direction, + dimension, + dimension == Dimension::Width ? ownerWidth : ownerHeight, + ownerWidth); + auto minContainerSize = minContainerOuter.isDefined() + ? minContainerOuter.unwrap() - paddingAndBorder + : YGUndefined; + + if (yoga::isDefined(minContainerSize)) { + if (newTotalSize < minContainerSize) { + // Redo with min constraint + flexFraction = findFrSize( + dimension, + 0, + gridTracks.size(), + minContainerSize, + std::unordered_set()); + } + } + + // Get the max constraint for this dimension + auto maxContainerOuter = node->style().resolvedMaxDimension( + direction, + dimension, + dimension == Dimension::Width ? ownerWidth : ownerHeight, + ownerWidth); + + auto maxContainerSize = maxContainerOuter.isDefined() + ? maxContainerOuter.unwrap() - paddingAndBorder + : YGUndefined; + + if (yoga::isDefined(maxContainerSize)) { + if (newTotalSize > maxContainerSize) { + // Redo with max constraint + flexFraction = findFrSize( + dimension, + 0, + gridTracks.size(), + maxContainerSize, + std::unordered_set()); + } + } + + // For each flexible track, if the product of the used flex fraction and the + // track's flex factor is greater than the track's base size, set its base + // size to that product. + for (auto& track : gridTracks) { + if (isFlexibleSizingFunction(track.maxSizingFunction) && + track.maxSizingFunction.value().isDefined()) { + float flexFactor = track.maxSizingFunction.value().unwrap(); + float newSize = flexFraction * flexFactor; + if (newSize > track.baseSize) { + track.baseSize = newSize; + } + } + } + }; + + // 11.7.1. Find the Size of an fr + // https://www.w3.org/TR/css-grid-1/#algo-find-fr-size + float findFrSize( + Dimension dimension, + size_t startIndex, + size_t endIndex, + float spaceToFill, + const std::unordered_set& nonFlexibleTracks) { + auto containerSize = dimension == Dimension::Width ? containerInnerWidth + : containerInnerHeight; + auto gap = node->style().computeGapForDimension(dimension, containerSize); + auto leftoverSpace = spaceToFill; + auto flexFactorSum = 0.0f; + std::vector flexibleTracks; + auto& tracks = dimension == Dimension::Width ? columnTracks : rowTracks; + + for (size_t i = startIndex; i < endIndex; i++) { + auto& track = tracks[i]; + // Let leftover space be the space to fill minus the base sizes of the + // non-flexible grid tracks. + if (i < endIndex - 1) { + // gap is treated as a non-flexible track + leftoverSpace -= gap; + } + + if (!isFlexibleSizingFunction(track.maxSizingFunction) || + nonFlexibleTracks.contains(&track)) { + leftoverSpace -= track.baseSize; + } + // Let flex factor sum be the sum of the flex factors of the flexible + // tracks. + else if ( + track.maxSizingFunction.isStretch() && + track.maxSizingFunction.value().isDefined()) { + flexFactorSum += track.maxSizingFunction.value().unwrap(); + flexibleTracks.push_back(&track); + } + } + + // If this value is less than 1, set it to 1 instead. + if (flexFactorSum < 1.0f) { + flexFactorSum = 1.0f; + } + + // Let the hypothetical fr size be the leftover space divided by the flex + // factor sum. + auto hypotheticalFrSize = leftoverSpace / flexFactorSum; + // If the product of the hypothetical fr size and a flexible track's flex + // factor is less than the track's base size, restart this algorithm + // treating all such tracks as inflexible. + std::unordered_set inflexibleTracks; + for (auto& track : flexibleTracks) { + if (track->maxSizingFunction.isStretch() && + track->maxSizingFunction.value().isDefined()) { + float flexFactor = track->maxSizingFunction.value().unwrap(); + if (hypotheticalFrSize * flexFactor < track->baseSize) { + inflexibleTracks.insert(track); + } + } + } + + // restart this algorithm treating all such tracks as inflexible. + if (!inflexibleTracks.empty()) { + inflexibleTracks.insert( + nonFlexibleTracks.begin(), nonFlexibleTracks.end()); + return findFrSize( + dimension, startIndex, endIndex, spaceToFill, inflexibleTracks); + } + + return hypotheticalFrSize; + } + + // 11.8. Stretch auto Tracks + // https://www.w3.org/TR/css-grid-1/#algo-stretch + void stretchAutoTracks(Dimension dimension) { + // Fast path - if tracks are fixed-sized, skip below steps + if (hasOnlyFixedTracks) { + return; + } + + auto& gridTracks = dimension == Dimension::Width ? columnTracks : rowTracks; + auto containerSize = dimension == Dimension::Width ? containerInnerWidth + : containerInnerHeight; + + // When the content-distribution property of the grid container is normal or + // stretch in this axis, this step expands tracks that have an auto max + // track sizing function by dividing any remaining positive, definite free + // space equally amongst them. If the free space is indefinite, but the grid + // container has a definite min-width/height, use that size to calculate the + // free space for this step instead. + auto shouldStretch = false; + if (dimension == Dimension::Width) { + shouldStretch = node->style().justifyContent() == Justify::Stretch; + } else { + shouldStretch = node->style().alignContent() == Align::Stretch; + } + + if (shouldStretch) { + // Count only auto tracks for distribution + std::vector autoTracks; + for (auto& track : gridTracks) { + if (isAutoSizingFunction(track.maxSizingFunction, containerSize)) { + autoTracks.push_back(&track); + } + } + + if (autoTracks.empty()) { + return; + } + + float freeSpace = calculateFreeSpace(dimension); + + // If the free space is indefinite, but the grid container has a definite + // min-width/height, use that size to calculate the free space for this + // step instead. + if (!yoga::isDefined(freeSpace)) { + const float paddingAndBorder = dimension == Dimension::Width + ? paddingAndBorderForAxis( + node, FlexDirection::Row, direction, ownerWidth) + : paddingAndBorderForAxis( + node, FlexDirection::Column, direction, ownerWidth); + + auto minContainerBorderBoxSize = node->style().resolvedMinDimension( + direction, + dimension, + dimension == Dimension::Width ? ownerWidth : ownerHeight, + ownerWidth); + auto minContainerInnerSize = minContainerBorderBoxSize.isDefined() + ? minContainerBorderBoxSize.unwrap() - paddingAndBorder + : YGUndefined; + + if (yoga::isDefined(minContainerInnerSize)) { + auto totalBaseSize = getTotalBaseSize(dimension); + freeSpace = std::max(0.0f, minContainerInnerSize - totalBaseSize); + } + } + + if (yoga::isDefined(freeSpace) && freeSpace > 0.0f && + !yoga::inexactEquals(freeSpace, 0.0f)) { + // Divide free space equally among auto tracks only + auto freeSpacePerAutoTrack = + freeSpace / static_cast(autoTracks.size()); + for (auto& track : autoTracks) { + track->baseSize += freeSpacePerAutoTrack; + } + } + } + }; + + // https://www.w3.org/TR/css-grid-1/#free-space + float calculateFreeSpace(Dimension dimension) { + float freeSpace = YGUndefined; + auto containerSize = dimension == Dimension::Width ? containerInnerWidth + : containerInnerHeight; + if (yoga::isDefined(containerSize)) { + auto totalBaseSize = getTotalBaseSize(dimension); + freeSpace = std::max(0.0f, containerSize - totalBaseSize); + } + + return freeSpace; + } + + float measureItem( + const GridItem& item, + Dimension dimension, + const ItemConstraint& constraints) { + calculateLayoutInternal( + item.node, + constraints.width, + constraints.height, + node->getLayout().direction(), + constraints.widthSizingMode, + constraints.heightSizingMode, + constraints.containingBlockWidth, + constraints.containingBlockHeight, + false, + LayoutPassReason::kMeasureChild, + layoutMarkerData, + depth + 1, + generationCount); + + return item.node->getLayout().measuredDimension(dimension); + } + + // There are 4 size contribution types used for intrinsic track sizing + // 1. minContentContribution - item's min-content size + margins + // 2. maxContentContribution - item's max-content size + margins + // 3. minimumContribution - smallest outer size + // 4. limitedMinContentContribution - min-content clamped by fixed track + // limits + + // TODO: Yoga does not support min-content constraint yet so we use the + // max-content size contributions here + float minContentContribution( + const GridItem& item, + Dimension dimension, + const ItemConstraint& itemConstraints) { + auto marginForAxis = item.node->style().computeMarginForAxis( + dimension == Dimension::Width ? FlexDirection::Row + : FlexDirection::Column, + itemConstraints.containingBlockWidth); + + float contribution = + measureItem(item, dimension, itemConstraints) + marginForAxis; + + if (dimension == Dimension::Height) { + contribution += item.baselineShim; + } + return contribution; + } + + float maxContentContribution( + const GridItem& item, + Dimension dimension, + const ItemConstraint& itemConstraints) { + auto marginForAxis = item.node->style().computeMarginForAxis( + dimension == Dimension::Width ? FlexDirection::Row + : FlexDirection::Column, + itemConstraints.containingBlockWidth); + + float contribution = + measureItem(item, dimension, itemConstraints) + marginForAxis; + + if (dimension == Dimension::Height) { + contribution += item.baselineShim; + } + return contribution; + } + + // Minimum contribution: the smallest outer size the item can have + // https://www.w3.org/TR/css-grid-1/#minimum-contribution + float minimumContribution( + const GridItem& item, + Dimension dimension, + const ItemConstraint& itemConstraints) { + auto& tracks = dimension == Dimension::Width ? columnTracks : rowTracks; + float containerSize = dimension == Dimension::Width ? containerInnerWidth + : containerInnerHeight; + auto containingBlockSize = dimension == Dimension::Width + ? itemConstraints.containingBlockWidth + : itemConstraints.containingBlockHeight; + + auto marginForAxis = item.node->style().computeMarginForAxis( + dimension == Dimension::Width ? FlexDirection::Row + : FlexDirection::Column, + itemConstraints.containingBlockWidth); + + auto preferredSize = item.node->style().dimension(dimension); + auto minSize = item.node->style().minDimension(dimension); + + float contribution = 0.0f; + + // If preferred size is definite (not auto/percent), use min-content + // contribution + if (!preferredSize.isAuto() && !preferredSize.isPercent()) { + return minContentContribution(item, dimension, itemConstraints); + } + + // If explicit min-size is set, use it + if (minSize.isDefined() && !minSize.isAuto()) { + auto resolvedMinSize = minSize.resolve(containingBlockSize); + contribution = resolvedMinSize.unwrap() + marginForAxis; + } + // Otherwise compute automatic minimum size + else { + contribution = + automaticMinimumSize( + item, dimension, itemConstraints, tracks, containerSize) + + marginForAxis; + } + + if (dimension == Dimension::Height) { + contribution += item.baselineShim; + } + return contribution; + } + + // https://www.w3.org/TR/css-grid-1/#min-size-auto + float automaticMinimumSize( + const GridItem& item, + Dimension dimension, + const ItemConstraint& itemConstraints, + const std::vector& tracks, + float containerSize) { + auto overflow = item.node->style().overflow(); + size_t startIndex = + dimension == Dimension::Width ? item.columnStart : item.rowStart; + size_t endIndex = + dimension == Dimension::Width ? item.columnEnd : item.rowEnd; + + // Check its computed overflow is not a scrollable overflow value + bool isScrollContainer = + overflow == Overflow::Scroll || overflow == Overflow::Hidden; + if (isScrollContainer) { + return 0.0f; + } + + // Check if it spans at least one track in that axis whose min track sizing + // function is auto + bool spansAutoMinTrack = false; + for (size_t i = startIndex; i < endIndex; i++) { + // TODO: check if this should also consider percentage auto behaving + // tracks + if (tracks[i].minSizingFunction.isAuto()) { + spansAutoMinTrack = true; + break; + } + } + if (!spansAutoMinTrack) { + return 0.0f; + } + + // Check if it spans more than one track in that axis, none of those tracks + // are flexible + bool spansMultipleTracks = (endIndex - startIndex) > 1; + if (spansMultipleTracks && item.crossesFlexibleTrack(dimension)) { + return 0.0f; + } + + return contentBasedMinimum( + item, dimension, itemConstraints, tracks, containerSize); + } + + // https://www.w3.org/TR/css-grid-1/#content-based-minimum-size + float contentBasedMinimum( + const GridItem& item, + Dimension dimension, + const ItemConstraint& itemConstraints, + const std::vector& tracks, + float containerSize) { + float result = measureItem(item, dimension, itemConstraints); + // Clamp by fixed track limit if all spanned tracks have fixed max sizing + // function + auto fixedLimit = + computeFixedTracksLimit(item, dimension, tracks, containerSize); + if (yoga::isDefined(fixedLimit)) { + result = std::min(result, fixedLimit); + } + + // Clamp by max-size if definite + auto containingBlockSize = dimension == Dimension::Width + ? itemConstraints.containingBlockWidth + : itemConstraints.containingBlockHeight; + auto maxSize = item.node->style().maxDimension(dimension); + if (maxSize.isDefined()) { + auto resolvedMaxSize = maxSize.resolve(containingBlockSize); + if (resolvedMaxSize.isDefined()) { + result = std::min(result, resolvedMaxSize.unwrap()); + } + } + + return result; + } + + // https://www.w3.org/TR/css-grid-1/#limited-contribution + float limitedMinContentContribution( + const GridItem& item, + Dimension dimension, + const ItemConstraint& itemConstraints) { + auto& tracks = dimension == Dimension::Width ? columnTracks : rowTracks; + float containerSize = dimension == Dimension::Width ? containerInnerWidth + : containerInnerHeight; + + auto fixedLimit = + computeFixedTracksLimit(item, dimension, tracks, containerSize); + auto minContent = minContentContribution(item, dimension, itemConstraints); + auto minimum = minimumContribution(item, dimension, itemConstraints); + + if (yoga::isDefined(fixedLimit)) { + return std::max(std::min(minContent, fixedLimit), minimum); + } + + return std::max(minContent, minimum); + } + + float computeFixedTracksLimit( + const GridItem& item, + Dimension dimension, + const std::vector& tracks, + float containerSize) { + size_t startIndex = + dimension == Dimension::Width ? item.columnStart : item.rowStart; + size_t endIndex = + dimension == Dimension::Width ? item.columnEnd : item.rowEnd; + auto gap = node->style().computeGapForDimension(dimension, containerSize); + + float limit = 0.0f; + for (size_t i = startIndex; i < endIndex; i++) { + if (!isFixedSizingFunction(tracks[i].maxSizingFunction, containerSize)) { + return YGUndefined; + } + auto resolved = tracks[i].maxSizingFunction.resolve(containerSize); + if (resolved.isDefined()) { + limit += resolved.unwrap(); + } + if (i < endIndex - 1) { + limit += gap; + } + } + return limit; + } + + static bool isFixedSizingFunction( + const StyleSizeLength& sizingFunction, + float referenceLength) { + return sizingFunction.isDefined() && + sizingFunction.resolve(referenceLength).isDefined(); + } + + static bool isIntrinsicSizingFunction( + const StyleSizeLength& sizingFunction, + float referenceLength) { + return isAutoSizingFunction(sizingFunction, referenceLength); + } + + static bool isAutoSizingFunction( + const StyleSizeLength& sizingFunction, + float referenceLength) { + return sizingFunction.isAuto() || + (sizingFunction.isPercent() && !yoga::isDefined(referenceLength)); + } + + static bool isFlexibleSizingFunction(const StyleSizeLength& sizingFunction) { + return sizingFunction.isStretch(); + } + + static bool isPercentageSizingFunction( + const StyleSizeLength& sizingFunction) { + return sizingFunction.isPercent(); + } + + float getTotalBaseSize(Dimension dimension) { + auto containerSize = dimension == Dimension::Width ? containerInnerWidth + : containerInnerHeight; + const auto& tracks = + dimension == Dimension::Width ? columnTracks : rowTracks; + auto gap = node->style().computeGapForDimension(dimension, containerSize); + + float totalBaseSize = 0.0f; + for (size_t i = 0; i < tracks.size(); i++) { + totalBaseSize += tracks[i].baseSize; + if (i < tracks.size() - 1) { + totalBaseSize += gap; + } + } + return totalBaseSize; + } + + bool hasPercentageTracks(Dimension dimension) const { + return dimension == Dimension::Width ? hasPercentageColumnTracks + : hasPercentageRowTracks; + } + + ContentDistribution calculateContentDistribution( + Dimension dimension, + float freeSpace) { + auto numTracks = static_cast( + dimension == Dimension::Width ? columnTracks.size() : rowTracks.size()); + auto containerSize = dimension == Dimension::Width ? containerInnerWidth + : containerInnerHeight; + auto baseGap = + node->style().computeGapForDimension(dimension, containerSize); + + ContentDistribution result; + result.effectiveGap = baseGap; + + if (yoga::inexactEquals(freeSpace, 0.0f)) { + return result; + } + + if (dimension == Dimension::Width) { + const Justify justifyContent = freeSpace > 0.0f + ? node->style().justifyContent() + : fallbackAlignment(node->style().justifyContent()); + + switch (justifyContent) { + case Justify::Center: + result.startOffset = freeSpace / 2.0f; + break; + + case Justify::End: + result.startOffset = freeSpace; + break; + + case Justify::SpaceBetween: + if (numTracks > 1) { + result.betweenTracksOffset = freeSpace / (numTracks - 1); + } + break; + + case Justify::SpaceAround: + if (numTracks > 0) { + result.betweenTracksOffset = freeSpace / numTracks; + result.startOffset = result.betweenTracksOffset / 2.0f; + } + break; + + case Justify::SpaceEvenly: + result.betweenTracksOffset = freeSpace / (numTracks + 1); + result.startOffset = result.betweenTracksOffset; + break; + + case Justify::Start: + case Justify::FlexStart: + case Justify::FlexEnd: + case Justify::Stretch: + case Justify::Auto: + default: + break; + } + } else { + const auto alignContent = freeSpace > 0.0f + ? node->style().alignContent() + : fallbackAlignment(node->style().alignContent()); + switch (alignContent) { + case Align::Center: + // content center works with negative free space too + // refer grid_align_content_center_negative_space_gap fixture + result.startOffset = freeSpace / 2.0f; + break; + case Align::End: + result.startOffset = freeSpace; + break; + case Align::SpaceBetween: + if (numTracks > 1) { + // negative free space is not distributed with space between, + // checkout grid_align_content_space_between_negative_space_gap + // fixture + result.betweenTracksOffset = + std::max(0.0f, freeSpace / (numTracks - 1)); + } + break; + + case Align::SpaceAround: + if (numTracks > 0) { + result.betweenTracksOffset = freeSpace / numTracks; + result.startOffset = result.betweenTracksOffset / 2.0f; + } + break; + + case Align::SpaceEvenly: + result.betweenTracksOffset = freeSpace / (numTracks + 1); + result.startOffset = result.betweenTracksOffset; + break; + + case Align::Auto: + case Align::FlexStart: + case Align::FlexEnd: + case Align::Stretch: + case Align::Baseline: + case Align::Start: + default: + break; + } + } + + result.effectiveGap = baseGap + result.betweenTracksOffset; + return result; + } + + ItemConstraint calculateItemConstraints( + const GridItem& item, + Dimension dimension) { + float containingBlockWidth = YGUndefined; + float containingBlockHeight = YGUndefined; + if (dimension == Dimension::Width) { + containingBlockHeight = crossDimensionEstimator(item); + } else { + containingBlockWidth = crossDimensionEstimator(item); + } + + return calculateItemConstraints( + item, containingBlockWidth, containingBlockHeight); + } + + ItemConstraint calculateItemConstraints( + const GridItem& item, + float containingBlockWidth, + float containingBlockHeight) { + auto availableWidth = YGUndefined; + auto availableHeight = YGUndefined; + auto itemWidthSizingMode = SizingMode::MaxContent; + auto itemHeightSizingMode = SizingMode::MaxContent; + auto hasDefiniteWidth = + item.node->hasDefiniteLength(Dimension::Width, containingBlockWidth); + auto hasDefiniteHeight = + item.node->hasDefiniteLength(Dimension::Height, containingBlockHeight); + + if (yoga::isDefined(containingBlockWidth)) { + itemWidthSizingMode = SizingMode::FitContent; + availableWidth = containingBlockWidth; + } + + if (yoga::isDefined(containingBlockHeight)) { + itemHeightSizingMode = SizingMode::FitContent; + availableHeight = containingBlockHeight; + } + + const auto marginInline = item.node->style().computeMarginForAxis( + FlexDirection::Row, containingBlockWidth); + if (hasDefiniteWidth) { + itemWidthSizingMode = SizingMode::StretchFit; + auto resolvedWidth = item.node + ->getResolvedDimension( + direction, + Dimension::Width, + containingBlockWidth, + containingBlockWidth) + .unwrap(); + resolvedWidth = boundAxis( + item.node, + FlexDirection::Row, + direction, + resolvedWidth, + containingBlockWidth, + containingBlockWidth); + availableWidth = resolvedWidth + marginInline; + } + + const auto marginBlock = item.node->style().computeMarginForAxis( + FlexDirection::Column, containingBlockWidth); + if (hasDefiniteHeight) { + itemHeightSizingMode = SizingMode::StretchFit; + auto resolvedHeight = item.node + ->getResolvedDimension( + direction, + Dimension::Height, + containingBlockHeight, + containingBlockWidth) + .unwrap(); + resolvedHeight = boundAxis( + item.node, + FlexDirection::Column, + direction, + resolvedHeight, + containingBlockHeight, + containingBlockWidth); + availableHeight = resolvedHeight + marginBlock; + } + + auto justifySelf = resolveChildJustification(node, item.node); + auto alignSelf = resolveChildAlignment(node, item.node); + + bool hasMarginInlineAuto = item.node->style().inlineStartMarginIsAuto( + FlexDirection::Row, direction) || + item.node->style().inlineEndMarginIsAuto(FlexDirection::Row, direction); + bool hasMarginBlockAuto = item.node->style().inlineStartMarginIsAuto( + FlexDirection::Column, direction) || + item.node->style().inlineEndMarginIsAuto( + FlexDirection::Column, direction); + + // For stretch-aligned items with a definite containing block size and no + // auto margins, treat the item as having a definite size in that axis (it + // will stretch to fill). + const auto& itemStyle = item.node->style(); + + if (yoga::isDefined(containingBlockWidth) && !hasDefiniteWidth && + justifySelf == Justify::Stretch && !hasMarginInlineAuto) { + itemWidthSizingMode = SizingMode::StretchFit; + availableWidth = containingBlockWidth; + } + + if (yoga::isDefined(containingBlockHeight) && + !item.node->hasDefiniteLength( + Dimension::Height, containingBlockHeight) && + alignSelf == Align::Stretch && !hasMarginBlockAuto) { + itemHeightSizingMode = SizingMode::StretchFit; + availableHeight = containingBlockHeight; + } + + if (itemStyle.aspectRatio().isDefined() && + !yoga::inexactEquals(itemStyle.aspectRatio().unwrap(), 0.0f)) { + const float aspectRatio = itemStyle.aspectRatio().unwrap(); + if (itemWidthSizingMode == SizingMode::StretchFit && + itemHeightSizingMode == SizingMode::StretchFit) { + if (!hasDefiniteWidth && !hasDefiniteHeight) { + auto resolvedWidth = (availableHeight - marginBlock) * aspectRatio; + resolvedWidth = boundAxis( + item.node, + FlexDirection::Row, + direction, + resolvedWidth, + containingBlockWidth, + containingBlockWidth); + availableWidth = resolvedWidth + marginInline; + } + } else if ( + itemWidthSizingMode == SizingMode::StretchFit && + itemHeightSizingMode != SizingMode::StretchFit) { + auto resolvedHeight = (availableWidth - marginInline) / aspectRatio; + resolvedHeight = boundAxis( + item.node, + FlexDirection::Column, + direction, + resolvedHeight, + containingBlockHeight, + containingBlockWidth); + availableHeight = resolvedHeight + marginBlock; + itemHeightSizingMode = SizingMode::StretchFit; + } else if ( + itemHeightSizingMode == SizingMode::StretchFit && + itemWidthSizingMode != SizingMode::StretchFit) { + auto resolvedWidth = (availableHeight - marginBlock) * aspectRatio; + resolvedWidth = boundAxis( + item.node, + FlexDirection::Row, + direction, + resolvedWidth, + containingBlockWidth, + containingBlockWidth); + availableWidth = resolvedWidth + marginInline; + itemWidthSizingMode = SizingMode::StretchFit; + } + } + + constrainMaxSizeForMode( + item.node, + direction, + FlexDirection::Row, + containingBlockWidth, + containingBlockWidth, + &itemWidthSizingMode, + &availableWidth); + constrainMaxSizeForMode( + item.node, + direction, + FlexDirection::Column, + containingBlockHeight, + containingBlockWidth, + &itemHeightSizingMode, + &availableHeight); + + return ItemConstraint{ + .width = availableWidth, + .height = availableHeight, + .widthSizingMode = itemWidthSizingMode, + .heightSizingMode = itemHeightSizingMode, + .containingBlockWidth = containingBlockWidth, + .containingBlockHeight = containingBlockHeight}; + } + + float calculateEffectiveRowGapForEstimation() { + auto rowGap = node->style().computeGapForDimension( + Dimension::Height, containerInnerHeight); + + if (!yoga::isDefined(containerInnerHeight)) { + return rowGap; + } + + float totalTrackSize = 0.0f; + for (auto& track : rowTracks) { + if (isFixedSizingFunction( + track.maxSizingFunction, containerInnerHeight)) { + totalTrackSize += + track.maxSizingFunction.resolve(containerInnerHeight).unwrap(); + } else { + return rowGap; + } + } + + float totalGapSize = rowTracks.size() > 1 + ? rowGap * static_cast(rowTracks.size() - 1) + : 0.0f; + float freeSpace = containerInnerHeight - totalTrackSize - totalGapSize; + + auto distribution = + calculateContentDistribution(Dimension::Height, freeSpace); + + return distribution.effectiveGap; + } + + float calculateEffectiveGapFromBaseSizes(Dimension dimension) { + auto containerSize = dimension == Dimension::Width ? containerInnerWidth + : containerInnerHeight; + auto gap = node->style().computeGapForDimension(dimension, containerSize); + const auto& tracks = + dimension == Dimension::Width ? columnTracks : rowTracks; + + if (!yoga::isDefined(containerSize)) { + return gap; + } + + float totalTrackSize = 0.0f; + for (auto& track : tracks) { + totalTrackSize += track.baseSize; + } + + float totalGapSize = + tracks.size() > 1 ? gap * static_cast(tracks.size() - 1) : 0.0f; + float freeSpace = containerSize - totalTrackSize - totalGapSize; + + auto distribution = calculateContentDistribution(dimension, freeSpace); + + return distribution.effectiveGap; + } + + bool contributionsChanged( + Dimension dimension, + CrossDimensionEstimator estimatorBefore, + CrossDimensionEstimator estimatorAfter) { + for (const auto& item : gridItems) { + if (!item.crossesIntrinsicTrack(dimension)) { + continue; + } + + float crossDimBefore = + estimatorBefore ? estimatorBefore(item) : YGUndefined; + float crossDimAfter = estimatorAfter ? estimatorAfter(item) : YGUndefined; + + // If cross dimension hasn't changed, contribution depending on it won't + // change + if (yoga::inexactEquals(crossDimBefore, crossDimAfter)) { + continue; + } + + float containingBlockWidth = + dimension == Dimension::Width ? YGUndefined : crossDimBefore; + float containingBlockHeight = + dimension == Dimension::Width ? crossDimBefore : YGUndefined; + auto constraintsBefore = calculateItemConstraints( + item, containingBlockWidth, containingBlockHeight); + float contributionBefore = + minContentContribution(item, dimension, constraintsBefore); + + containingBlockWidth = + dimension == Dimension::Width ? YGUndefined : crossDimAfter; + containingBlockHeight = + dimension == Dimension::Width ? crossDimAfter : YGUndefined; + auto constraintsAfter = calculateItemConstraints( + item, containingBlockWidth, containingBlockHeight); + float contributionAfter = + minContentContribution(item, dimension, constraintsAfter); + + if (!yoga::inexactEquals(contributionBefore, contributionAfter)) { + return true; + } + } + return false; + } + + bool itemSizeDependsOnIntrinsicTracks(const GridItem& item) const { + auto heightStyle = item.node->style().dimension(Dimension::Height); + if (heightStyle.isPercent()) { + for (size_t i = item.rowStart; i < item.rowEnd && i < rowTracks.size(); + i++) { + if (isIntrinsicSizingFunction( + rowTracks[i].minSizingFunction, containerInnerHeight) || + isIntrinsicSizingFunction( + rowTracks[i].maxSizingFunction, containerInnerHeight)) { + return true; + } + } + } + return false; + } + + void computeItemTrackCrossingFlags() { + for (auto& item : gridItems) { + item.crossesFlexibleColumn = false; + item.crossesIntrinsicColumn = false; + item.crossesFlexibleRow = false; + item.crossesIntrinsicRow = false; + + for (size_t i = item.columnStart; i < item.columnEnd; i++) { + if (isFlexibleSizingFunction(columnTracks[i].maxSizingFunction)) { + item.crossesFlexibleColumn = true; + } else if (isIntrinsicSizingFunction( + columnTracks[i].maxSizingFunction, + containerInnerWidth)) { + item.crossesIntrinsicColumn = true; + } + if (isIntrinsicSizingFunction( + columnTracks[i].minSizingFunction, containerInnerWidth)) { + item.crossesIntrinsicColumn = true; + } + } + + for (size_t i = item.rowStart; i < item.rowEnd; i++) { + if (isFlexibleSizingFunction(rowTracks[i].maxSizingFunction)) { + item.crossesFlexibleRow = true; + } else if (isIntrinsicSizingFunction( + rowTracks[i].maxSizingFunction, containerInnerHeight)) { + item.crossesIntrinsicRow = true; + } + if (isIntrinsicSizingFunction( + rowTracks[i].minSizingFunction, containerInnerHeight)) { + item.crossesIntrinsicRow = true; + } + } + } + } + + CrossDimensionEstimator makeRowHeightEstimatorUsingFixedTracks(float gap) { + auto& tracks = rowTracks; + auto containerHeight = containerInnerHeight; + return [&tracks, containerHeight, gap](const GridItem& item) -> float { + float height = 0.0f; + for (size_t i = item.rowStart; i < item.rowEnd && i < tracks.size(); + i++) { + if (isFixedSizingFunction( + tracks[i].maxSizingFunction, containerHeight)) { + height += + tracks[i].maxSizingFunction.resolve(containerHeight).unwrap(); + if (i < item.rowEnd - 1) { + height += gap; + } + } else { + return YGUndefined; + } + } + return height; + }; + } + + CrossDimensionEstimator makeCrossDimensionEstimatorUsingBaseSize( + Dimension dimension, + float gap) { + std::vector baseSizes; + auto& tracks = dimension == Dimension::Width ? columnTracks : rowTracks; + baseSizes.reserve(tracks.size()); + for (const auto& track : tracks) { + baseSizes.push_back(track.baseSize); + } + auto startIndexKey = dimension == Dimension::Width ? &GridItem::columnStart + : &GridItem::rowStart; + auto endIndexKey = dimension == Dimension::Width ? &GridItem::columnEnd + : &GridItem::rowEnd; + return [baseSizes = std::move(baseSizes), gap, startIndexKey, endIndexKey]( + const GridItem& item) -> float { + float width = 0.0f; + for (size_t i = item.*startIndexKey; + i < item.*endIndexKey && i < baseSizes.size(); + i++) { + width += baseSizes[i]; + if (i < item.*endIndexKey - 1) { + width += gap; + } + } + return width; + }; + } +}; + +} // namespace facebook::yoga From 788955d42b3c5e09fbf1c8750b9b6042eadc736f Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Sat, 21 Feb 2026 22:11:09 -0800 Subject: [PATCH 3/7] CSS Grid 3/9: Grid benchmark Summary: Add grid layout benchmarks. Includes: - YGGridBenchmark.c: 14 benchmark scenarios - benchmarkgrid: shell script to run benchmarks - CMakeLists.txt: benchmarkgrid target Differential Revision: D93946260 --- benchmark/CMakeLists.txt | 6 +- benchmark/YGGridBenchmark.c | 597 ++++++++++++++++++++++++++++++++++++ benchmark/benchmarkgrid | 10 + 3 files changed, 610 insertions(+), 3 deletions(-) create mode 100644 benchmark/YGGridBenchmark.c create mode 100755 benchmark/benchmarkgrid diff --git a/benchmark/CMakeLists.txt b/benchmark/CMakeLists.txt index 4df524b023..895f82f719 100644 --- a/benchmark/CMakeLists.txt +++ b/benchmark/CMakeLists.txt @@ -12,16 +12,16 @@ include(${YOGA_ROOT}/cmake/project-defaults.cmake) add_subdirectory(${YOGA_ROOT}/yoga ${CMAKE_CURRENT_BINARY_DIR}/yoga) -file(GLOB SOURCES_LEGACY CONFIGURE_DEPENDS - ${CMAKE_CURRENT_SOURCE_DIR}/*.c) file(GLOB SOURCES CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp) add_executable(benchmark ${SOURCES}) -add_executable(benchmarklegacy ${SOURCES_LEGACY}) +add_executable(benchmarklegacy ${CMAKE_CURRENT_SOURCE_DIR}/YGBenchmark.c) +add_executable(benchmarkgrid ${CMAKE_CURRENT_SOURCE_DIR}/YGGridBenchmark.c) target_link_libraries(benchmark yogacore) target_link_libraries(benchmarklegacy yogacore) +target_link_libraries(benchmarkgrid yogacore) target_include_directories(benchmark PRIVATE $) diff --git a/benchmark/YGGridBenchmark.c b/benchmark/YGGridBenchmark.c new file mode 100644 index 0000000000..c8b2c291b7 --- /dev/null +++ b/benchmark/YGGridBenchmark.c @@ -0,0 +1,597 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include +#include +#include +#include +#include + +#include + +#define NUM_REPETITIONS 1000 + +#define YGBENCHMARKS(BLOCK) \ + int main(int argc, char const* argv[]) { \ + (void)argc; \ + (void)argv; \ + clock_t __start; \ + clock_t __endTimes[NUM_REPETITIONS]; \ + { \ + BLOCK \ + } \ + return 0; \ + } + +#define YGBENCHMARK(NAME, BLOCK) \ + __start = clock(); \ + for (uint32_t __i = 0; __i < NUM_REPETITIONS; __i++) { \ + {BLOCK} __endTimes[__i] = clock(); \ + } \ + __printBenchmarkResult(NAME, __start, __endTimes); + +static int __compareDoubles(const void* a, const void* b) { + double arg1 = *(const double*)a; + double arg2 = *(const double*)b; + + if (arg1 < arg2) { + return -1; + } + + if (arg1 > arg2) { + return 1; + } + + return 0; +} + +static void +__printBenchmarkResult(char* name, clock_t start, const clock_t* endTimes) { + double timesInMs[NUM_REPETITIONS]; + double mean = 0; + clock_t lastEnd = start; + for (uint32_t i = 0; i < NUM_REPETITIONS; i++) { + timesInMs[i] = + ((double)(endTimes[i] - lastEnd)) / (double)CLOCKS_PER_SEC * 1000; + lastEnd = endTimes[i]; + mean += timesInMs[i]; + } + mean /= NUM_REPETITIONS; + + qsort(timesInMs, NUM_REPETITIONS, sizeof(double), __compareDoubles); + double median = timesInMs[NUM_REPETITIONS / 2]; + + double variance = 0; + for (uint32_t i = 0; i < NUM_REPETITIONS; i++) { + variance += pow(timesInMs[i] - mean, 2); + } + variance /= NUM_REPETITIONS; + double stddev = sqrt(variance); + + printf("%s: median: %lf ms, stddev: %lf ms\n", name, median, stddev); +} + +static YGSize _measure( + YGNodeConstRef node, + float width, + YGMeasureMode widthMode, + float height, + YGMeasureMode heightMode) { + (void)node; + return (YGSize){ + .width = widthMode == YGMeasureModeUndefined ? 10 : width, + .height = heightMode == YGMeasureModeUndefined ? 10 : height, + }; +} + +static YGSize _measureFixed( + YGNodeConstRef node, + float width, + YGMeasureMode widthMode, + float height, + YGMeasureMode heightMode) { + (void)node; + (void)width; + (void)widthMode; + (void)height; + (void)heightMode; + return (YGSize){ + .width = 50, + .height = 50, + }; +} + +static YGGridTrackListRef createFixed3x100Tracks(void) { + YGGridTrackListRef tracks = YGGridTrackListCreate(); + YGGridTrackListAddTrack(tracks, YGPoints(100)); + YGGridTrackListAddTrack(tracks, YGPoints(100)); + YGGridTrackListAddTrack(tracks, YGPoints(100)); + return tracks; +} + +static YGGridTrackListRef createAuto3Tracks(void) { + YGGridTrackListRef tracks = YGGridTrackListCreate(); + YGGridTrackListAddTrack(tracks, YGAuto()); + YGGridTrackListAddTrack(tracks, YGAuto()); + YGGridTrackListAddTrack(tracks, YGAuto()); + return tracks; +} + +static YGGridTrackListRef createFr3Tracks(void) { + YGGridTrackListRef tracks = YGGridTrackListCreate(); + YGGridTrackListAddTrack(tracks, YGFr(1)); + YGGridTrackListAddTrack(tracks, YGFr(1)); + YGGridTrackListAddTrack(tracks, YGFr(1)); + return tracks; +} + +static YGGridTrackListRef createFr4Tracks(void) { + YGGridTrackListRef tracks = YGGridTrackListCreate(); + YGGridTrackListAddTrack(tracks, YGFr(1)); + YGGridTrackListAddTrack(tracks, YGFr(1)); + YGGridTrackListAddTrack(tracks, YGFr(1)); + YGGridTrackListAddTrack(tracks, YGFr(1)); + return tracks; +} + +static YGGridTrackListRef createFr5Tracks(void) { + YGGridTrackListRef tracks = YGGridTrackListCreate(); + YGGridTrackListAddTrack(tracks, YGFr(1)); + YGGridTrackListAddTrack(tracks, YGFr(1)); + YGGridTrackListAddTrack(tracks, YGFr(1)); + YGGridTrackListAddTrack(tracks, YGFr(1)); + YGGridTrackListAddTrack(tracks, YGFr(1)); + return tracks; +} + +static YGGridTrackListRef createFr2Tracks(void) { + YGGridTrackListRef tracks = YGGridTrackListCreate(); + YGGridTrackListAddTrack(tracks, YGFr(1)); + YGGridTrackListAddTrack(tracks, YGFr(1)); + return tracks; +} + +static YGGridTrackListRef createFixed3x80Tracks(void) { + YGGridTrackListRef tracks = YGGridTrackListCreate(); + YGGridTrackListAddTrack(tracks, YGPoints(80)); + YGGridTrackListAddTrack(tracks, YGPoints(80)); + YGGridTrackListAddTrack(tracks, YGPoints(80)); + return tracks; +} + +static YGGridTrackListRef createAuto4Tracks(void) { + YGGridTrackListRef tracks = YGGridTrackListCreate(); + YGGridTrackListAddTrack(tracks, YGAuto()); + YGGridTrackListAddTrack(tracks, YGAuto()); + YGGridTrackListAddTrack(tracks, YGAuto()); + YGGridTrackListAddTrack(tracks, YGAuto()); + return tracks; +} + +static YGGridTrackListRef createAuto2Tracks(void) { + YGGridTrackListRef tracks = YGGridTrackListCreate(); + YGGridTrackListAddTrack(tracks, YGAuto()); + YGGridTrackListAddTrack(tracks, YGAuto()); + return tracks; +} + +static YGGridTrackListRef createPercent3Tracks(void) { + YGGridTrackListRef tracks = YGGridTrackListCreate(); + YGGridTrackListAddTrack(tracks, YGPercent(25)); + YGGridTrackListAddTrack(tracks, YGPercent(50)); + YGGridTrackListAddTrack(tracks, YGPercent(25)); + return tracks; +} + +static YGGridTrackListRef createPercent3RowTracks(void) { + YGGridTrackListRef tracks = YGGridTrackListCreate(); + YGGridTrackListAddTrack(tracks, YGPercent(33.33f)); + YGGridTrackListAddTrack(tracks, YGPercent(33.33f)); + YGGridTrackListAddTrack(tracks, YGPercent(33.33f)); + return tracks; +} + +static YGGridTrackListRef createMixedColumnTracks(void) { + YGGridTrackListRef tracks = YGGridTrackListCreate(); + YGGridTrackListAddTrack(tracks, YGPoints(200)); + YGGridTrackListAddTrack(tracks, YGFr(1)); + YGGridTrackListAddTrack(tracks, YGPoints(200)); + return tracks; +} + +static YGGridTrackListRef createMixedRowTracks(void) { + YGGridTrackListRef tracks = YGGridTrackListCreate(); + YGGridTrackListAddTrack(tracks, YGPoints(60)); + YGGridTrackListAddTrack(tracks, YGFr(1)); + YGGridTrackListAddTrack(tracks, YGPoints(40)); + return tracks; +} + +static YGGridTrackListRef createMinmaxColumnTracks(void) { + YGGridTrackListRef tracks = YGGridTrackListCreate(); + YGGridTrackListAddTrack(tracks, YGMinMax(YGPoints(100), YGFr(1))); + YGGridTrackListAddTrack(tracks, YGMinMax(YGPoints(100), YGFr(1))); + YGGridTrackListAddTrack(tracks, YGMinMax(YGPoints(100), YGFr(1))); + return tracks; +} + +static YGGridTrackListRef createMinmaxRowTracks(void) { + YGGridTrackListRef tracks = YGGridTrackListCreate(); + YGGridTrackListAddTrack(tracks, YGMinMax(YGPoints(50), YGAuto())); + YGGridTrackListAddTrack(tracks, YGMinMax(YGPoints(50), YGAuto())); + YGGridTrackListAddTrack(tracks, YGMinMax(YGPoints(50), YGAuto())); + return tracks; +} + +static YGGridTrackListRef createMixed20ColumnTracks(void) { + YGGridTrackListRef tracks = YGGridTrackListCreate(); + for (int i = 0; i < 5; i++) { + YGGridTrackListAddTrack(tracks, YGPoints(100)); + YGGridTrackListAddTrack(tracks, YGFr(1)); + YGGridTrackListAddTrack(tracks, YGAuto()); + YGGridTrackListAddTrack(tracks, YGMinMax(YGPoints(50), YGFr(1))); + } + return tracks; +} + +static YGGridTrackListRef createMixed50RowTracks(void) { + YGGridTrackListRef tracks = YGGridTrackListCreate(); + for (int i = 0; i < 10; i++) { + YGGridTrackListAddTrack(tracks, YGPoints(40)); + YGGridTrackListAddTrack(tracks, YGFr(1)); + YGGridTrackListAddTrack(tracks, YGAuto()); + YGGridTrackListAddTrack(tracks, YGMinMax(YGPoints(30), YGAuto())); + YGGridTrackListAddTrack(tracks, YGFr(2)); + } + return tracks; +} + +YGBENCHMARKS({ + // Scenario 1: Basic fixed-size grid + YGBENCHMARK("Grid 3x3 fixed tracks", { + YGNodeRef root = YGNodeNew(); + YGNodeStyleSetDisplay(root, YGDisplayGrid); + YGNodeStyleSetWidth(root, 300); + YGNodeStyleSetHeight(root, 300); + YGNodeStyleSetGridTemplateColumns(root, createFixed3x100Tracks()); + YGNodeStyleSetGridTemplateRows(root, createFixed3x100Tracks()); + + for (uint32_t i = 0; i < 9; i++) { + YGNodeRef child = YGNodeNew(); + YGNodeInsertChild(root, child, i); + } + + YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR); + YGNodeFreeRecursive(root); + }); + + // Scenario 2: Grid with auto-sized items + YGBENCHMARK("Grid 3x3 auto tracks", { + YGNodeRef root = YGNodeNew(); + YGNodeStyleSetDisplay(root, YGDisplayGrid); + YGNodeStyleSetGridTemplateColumns(root, createAuto3Tracks()); + YGNodeStyleSetGridTemplateRows(root, createAuto3Tracks()); + + for (uint32_t i = 0; i < 9; i++) { + YGNodeRef child = YGNodeNew(); + YGNodeSetMeasureFunc(child, _measureFixed); + YGNodeInsertChild(root, child, i); + } + + YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR); + YGNodeFreeRecursive(root); + }); + + // Scenario 3: Grid with fr units + YGBENCHMARK("Grid 3x3 fr tracks", { + YGNodeRef root = YGNodeNew(); + YGNodeStyleSetDisplay(root, YGDisplayGrid); + YGNodeStyleSetWidth(root, 300); + YGNodeStyleSetHeight(root, 300); + YGNodeStyleSetGridTemplateColumns(root, createFr3Tracks()); + YGNodeStyleSetGridTemplateRows(root, createFr3Tracks()); + + for (uint32_t i = 0; i < 9; i++) { + YGNodeRef child = YGNodeNew(); + YGNodeInsertChild(root, child, i); + } + + YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR); + YGNodeFreeRecursive(root); + }); + + // Scenario 4: Grid with gaps + YGBENCHMARK("Grid 4x4 with gaps", { + YGNodeRef root = YGNodeNew(); + YGNodeStyleSetDisplay(root, YGDisplayGrid); + YGNodeStyleSetWidth(root, 400); + YGNodeStyleSetHeight(root, 400); + YGNodeStyleSetGap(root, YGGutterAll, 10); + YGNodeStyleSetGridTemplateColumns(root, createFr4Tracks()); + YGNodeStyleSetGridTemplateRows(root, createFr4Tracks()); + + for (uint32_t i = 0; i < 16; i++) { + YGNodeRef child = YGNodeNew(); + YGNodeInsertChild(root, child, i); + } + + YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR); + YGNodeFreeRecursive(root); + }); + + // Scenario 5: Mixed fixed and flexible tracks + YGBENCHMARK("Grid mixed tracks (fixed + fr)", { + YGNodeRef root = YGNodeNew(); + YGNodeStyleSetDisplay(root, YGDisplayGrid); + YGNodeStyleSetWidth(root, 800); + YGNodeStyleSetHeight(root, 600); + YGNodeStyleSetGridTemplateColumns(root, createMixedColumnTracks()); + YGNodeStyleSetGridTemplateRows(root, createMixedRowTracks()); + + for (uint32_t i = 0; i < 9; i++) { + YGNodeRef child = YGNodeNew(); + YGNodeInsertChild(root, child, i); + } + + YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR); + YGNodeFreeRecursive(root); + }); + + // Scenario 6: Grid with spanning items + YGBENCHMARK("Grid with spanning items", { + YGNodeRef root = YGNodeNew(); + YGNodeStyleSetDisplay(root, YGDisplayGrid); + YGNodeStyleSetWidth(root, 400); + YGNodeStyleSetHeight(root, 400); + YGNodeStyleSetGap(root, YGGutterAll, 8); + YGNodeStyleSetGridTemplateColumns(root, createFr4Tracks()); + YGNodeStyleSetGridTemplateRows(root, createFr4Tracks()); + + YGNodeRef child1 = YGNodeNew(); + YGNodeStyleSetGridColumnStart(child1, 1); + YGNodeStyleSetGridColumnEndSpan(child1, 2); + YGNodeInsertChild(root, child1, 0); + + YGNodeRef child2 = YGNodeNew(); + YGNodeStyleSetGridRowStart(child2, 1); + YGNodeStyleSetGridRowEndSpan(child2, 2); + YGNodeInsertChild(root, child2, 1); + + YGNodeRef child3 = YGNodeNew(); + YGNodeStyleSetGridColumnStart(child3, 3); + YGNodeStyleSetGridColumnEndSpan(child3, 2); + YGNodeStyleSetGridRowStart(child3, 3); + YGNodeStyleSetGridRowEndSpan(child3, 2); + YGNodeInsertChild(root, child3, 2); + + for (uint32_t i = 0; i < 8; i++) { + YGNodeRef child = YGNodeNew(); + YGNodeInsertChild(root, child, 3 + i); + } + + YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR); + YGNodeFreeRecursive(root); + }); + + // Scenario 7: Auto-placement + YGBENCHMARK("Grid auto-placement 5x5", { + YGNodeRef root = YGNodeNew(); + YGNodeStyleSetDisplay(root, YGDisplayGrid); + YGNodeStyleSetWidth(root, 500); + YGNodeStyleSetHeight(root, 500); + YGNodeStyleSetGridTemplateColumns(root, createFr5Tracks()); + YGNodeStyleSetGridTemplateRows(root, createFr5Tracks()); + + for (uint32_t i = 0; i < 25; i++) { + YGNodeRef child = YGNodeNew(); + YGNodeInsertChild(root, child, i); + } + + YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR); + YGNodeFreeRecursive(root); + }); + + // Scenario 8: Nested grids + YGBENCHMARK("Nested grids 3x3 with 2x2 children", { + YGNodeRef root = YGNodeNew(); + YGNodeStyleSetDisplay(root, YGDisplayGrid); + YGNodeStyleSetWidth(root, 600); + YGNodeStyleSetHeight(root, 600); + YGNodeStyleSetGap(root, YGGutterAll, 10); + YGNodeStyleSetGridTemplateColumns(root, createFr3Tracks()); + YGNodeStyleSetGridTemplateRows(root, createFr3Tracks()); + + for (uint32_t i = 0; i < 9; i++) { + YGNodeRef child = YGNodeNew(); + YGNodeStyleSetDisplay(child, YGDisplayGrid); + YGNodeStyleSetGap(child, YGGutterAll, 4); + YGNodeStyleSetGridTemplateColumns(child, createFr2Tracks()); + YGNodeStyleSetGridTemplateRows(child, createFr2Tracks()); + YGNodeInsertChild(root, child, i); + + for (uint32_t j = 0; j < 4; j++) { + YGNodeRef grandChild = YGNodeNew(); + YGNodeInsertChild(child, grandChild, j); + } + } + + YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR); + YGNodeFreeRecursive(root); + }); + + // Scenario 9: Grid with alignment + YGBENCHMARK("Grid with alignment", { + YGNodeRef root = YGNodeNew(); + YGNodeStyleSetDisplay(root, YGDisplayGrid); + YGNodeStyleSetWidth(root, 400); + YGNodeStyleSetHeight(root, 400); + YGNodeStyleSetJustifyContent(root, YGJustifyCenter); + YGNodeStyleSetAlignContent(root, YGAlignCenter); + YGNodeStyleSetGap(root, YGGutterAll, 10); + YGNodeStyleSetGridTemplateColumns(root, createFixed3x80Tracks()); + YGNodeStyleSetGridTemplateRows(root, createFixed3x80Tracks()); + + for (uint32_t i = 0; i < 9; i++) { + YGNodeRef child = YGNodeNew(); + YGNodeStyleSetAlignSelf(child, YGAlignCenter); + YGNodeStyleSetWidth(child, 60); + YGNodeStyleSetHeight(child, 60); + YGNodeInsertChild(root, child, i); + } + + YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR); + YGNodeFreeRecursive(root); + }); + + // Scenario 10: Grid with intrinsic sizing and measure functions + YGBENCHMARK("Grid auto tracks with measure", { + YGNodeRef root = YGNodeNew(); + YGNodeStyleSetDisplay(root, YGDisplayGrid); + YGNodeStyleSetWidth(root, 400); + YGNodeStyleSetGridTemplateColumns(root, createAuto4Tracks()); + YGNodeStyleSetGridTemplateRows(root, createAuto4Tracks()); + + for (uint32_t i = 0; i < 16; i++) { + YGNodeRef child = YGNodeNew(); + YGNodeSetMeasureFunc(child, _measure); + YGNodeInsertChild(root, child, i); + } + + YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR); + YGNodeFreeRecursive(root); + }); + + // Scenario 11: minmax tracks + YGBENCHMARK("Grid minmax tracks", { + YGNodeRef root = YGNodeNew(); + YGNodeStyleSetDisplay(root, YGDisplayGrid); + YGNodeStyleSetWidth(root, 600); + YGNodeStyleSetHeight(root, 400); + YGNodeStyleSetGap(root, YGGutterAll, 10); + YGNodeStyleSetGridTemplateColumns(root, createMinmaxColumnTracks()); + YGNodeStyleSetGridTemplateRows(root, createMinmaxRowTracks()); + + for (uint32_t i = 0; i < 9; i++) { + YGNodeRef child = YGNodeNew(); + YGNodeSetMeasureFunc(child, _measureFixed); + YGNodeInsertChild(root, child, i); + } + + YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR); + YGNodeFreeRecursive(root); + }); + + // Scenario 12: Indefinite container size + YGBENCHMARK("Grid indefinite container", { + YGNodeRef root = YGNodeNew(); + YGNodeStyleSetDisplay(root, YGDisplayGrid); + YGNodeStyleSetGridTemplateColumns(root, createAuto3Tracks()); + YGNodeStyleSetGridTemplateRows(root, createAuto2Tracks()); + + for (uint32_t i = 0; i < 6; i++) { + YGNodeRef child = YGNodeNew(); + YGNodeStyleSetWidth(child, 80); + YGNodeStyleSetHeight(child, 60); + YGNodeInsertChild(root, child, i); + } + + YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR); + YGNodeFreeRecursive(root); + }); + + // Scenario 13: Grid with percentage tracks + YGBENCHMARK("Grid percentage tracks", { + YGNodeRef root = YGNodeNew(); + YGNodeStyleSetDisplay(root, YGDisplayGrid); + YGNodeStyleSetWidth(root, 400); + YGNodeStyleSetHeight(root, 300); + YGNodeStyleSetGridTemplateColumns(root, createPercent3Tracks()); + YGNodeStyleSetGridTemplateRows(root, createPercent3RowTracks()); + + for (uint32_t i = 0; i < 9; i++) { + YGNodeRef child = YGNodeNew(); + YGNodeInsertChild(root, child, i); + } + + YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR); + YGNodeFreeRecursive(root); + }); + + // Scenario 14: Stress test - 1000 items with mixed tracks and spanning + YGBENCHMARK("Stress test 1000 items mixed", { + YGNodeRef root = YGNodeNew(); + YGNodeStyleSetDisplay(root, YGDisplayGrid); + YGNodeStyleSetWidth(root, 2000); + YGNodeStyleSetHeight(root, 5000); + YGNodeStyleSetGap(root, YGGutterColumn, 8); + YGNodeStyleSetGap(root, YGGutterRow, 4); + YGNodeStyleSetGridTemplateColumns(root, createMixed20ColumnTracks()); + YGNodeStyleSetGridTemplateRows(root, createMixed50RowTracks()); + + uint32_t childIndex = 0; + + for (uint32_t row = 1; row <= 50; row++) { + for (uint32_t col = 1; col <= 20; col++) { + YGNodeRef child = YGNodeNew(); + + if (childIndex % 30 == 0 && col <= 17) { + YGNodeStyleSetGridColumnStart(child, (int)col); + YGNodeStyleSetGridColumnEndSpan(child, 3); + YGNodeStyleSetGridRowStart(child, (int)row); + } else if (childIndex % 40 == 0 && col <= 16) { + YGNodeStyleSetGridColumnStart(child, (int)col); + YGNodeStyleSetGridColumnEndSpan(child, 4); + YGNodeStyleSetGridRowStart(child, (int)row); + } else if (childIndex % 50 == 0 && row <= 48) { + YGNodeStyleSetGridColumnStart(child, (int)col); + YGNodeStyleSetGridRowStart(child, (int)row); + YGNodeStyleSetGridRowEndSpan(child, 2); + } else if (childIndex % 70 == 0 && row <= 47) { + YGNodeStyleSetGridColumnStart(child, (int)col); + YGNodeStyleSetGridRowStart(child, (int)row); + YGNodeStyleSetGridRowEndSpan(child, 3); + } else if (childIndex % 100 == 0 && col <= 18 && row <= 48) { + YGNodeStyleSetGridColumnStart(child, (int)col); + YGNodeStyleSetGridColumnEndSpan(child, 2); + YGNodeStyleSetGridRowStart(child, (int)row); + YGNodeStyleSetGridRowEndSpan(child, 2); + } else if (childIndex % 150 == 0 && col <= 17 && row <= 48) { + YGNodeStyleSetGridColumnStart(child, (int)col); + YGNodeStyleSetGridColumnEndSpan(child, 3); + YGNodeStyleSetGridRowStart(child, (int)row); + YGNodeStyleSetGridRowEndSpan(child, 2); + } else if (childIndex % 200 == 0 && col <= 18 && row <= 47) { + YGNodeStyleSetGridColumnStart(child, (int)col); + YGNodeStyleSetGridColumnEndSpan(child, 2); + YGNodeStyleSetGridRowStart(child, (int)row); + YGNodeStyleSetGridRowEndSpan(child, 3); + } else if (childIndex % 300 == 0 && col <= 16 && row <= 47) { + YGNodeStyleSetGridColumnStart(child, (int)col); + YGNodeStyleSetGridColumnEndSpan(child, 4); + YGNodeStyleSetGridRowStart(child, (int)row); + YGNodeStyleSetGridRowEndSpan(child, 3); + } else if (childIndex % 500 == 0 && col <= 15 && row <= 46) { + YGNodeStyleSetGridColumnStart(child, (int)col); + YGNodeStyleSetGridColumnEndSpan(child, 5); + YGNodeStyleSetGridRowStart(child, (int)row); + YGNodeStyleSetGridRowEndSpan(child, 4); + } + + YGNodeInsertChild(root, child, childIndex); + childIndex++; + + if (childIndex >= 1000) + break; + } + if (childIndex >= 1000) + break; + } + + YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR); + YGNodeFreeRecursive(root); + }); +}); diff --git a/benchmark/benchmarkgrid b/benchmark/benchmarkgrid new file mode 100755 index 0000000000..7bc73b4c7b --- /dev/null +++ b/benchmark/benchmarkgrid @@ -0,0 +1,10 @@ +#!/bin/sh +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. +cd "$(dirname "$0")" || exit + +cmake -B build -S . -D CMAKE_BUILD_TYPE=Release +cmake --build build --target benchmarkgrid +build/benchmarkgrid From 59601af2fd3478de62c62fef6088aa635d954de5 Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Sat, 21 Feb 2026 22:11:09 -0800 Subject: [PATCH 4/7] CSS Grid 4/9: Grid C++ tests Summary: Add hand-written C++ tests for the grid layout algorithm. Includes: - Hand-written tests: AutoPlacementTest.cpp, CreateGridTrackTest.cpp - Modified existing tests: YGAlignBaselineTest.cpp (+3 grid baseline tests), YGHadOverflowTest.cpp (+1 grid overflow test) Differential Revision: D93946259 --- tests/YGAlignBaselineTest.cpp | 167 +++++++++ tests/YGHadOverflowTest.cpp | 26 ++ tests/grid/AutoPlacementTest.cpp | 578 +++++++++++++++++++++++++++++ tests/grid/CreateGridTrackTest.cpp | 292 +++++++++++++++ 4 files changed, 1063 insertions(+) create mode 100644 tests/grid/AutoPlacementTest.cpp create mode 100644 tests/grid/CreateGridTrackTest.cpp diff --git a/tests/YGAlignBaselineTest.cpp b/tests/YGAlignBaselineTest.cpp index e8c896ce39..e210ba8dc1 100644 --- a/tests/YGAlignBaselineTest.cpp +++ b/tests/YGAlignBaselineTest.cpp @@ -6,6 +6,7 @@ */ #include +#include #include static float _baselineFunc( @@ -835,3 +836,169 @@ TEST( YGConfigFree(config); } + +TEST(YogaTest, grid_align_baseline_with_margin) { + YGConfigRef config = YGConfigNew(); + + YGNodeRef root = YGNodeNewWithConfig(config); + YGNodeStyleSetAlignItems(root, YGAlignBaseline); + YGNodeStyleSetPositionType(root, YGPositionTypeAbsolute); + YGNodeStyleSetDisplay(root, YGDisplayGrid); + auto root_gridTemplateColumns = YGGridTrackListCreate(); + YGGridTrackListAddTrack(root_gridTemplateColumns, YGPoints(100)); + YGGridTrackListAddTrack(root_gridTemplateColumns, YGPoints(100)); + YGGridTrackListAddTrack(root_gridTemplateColumns, YGPoints(100)); + YGNodeStyleSetGridTemplateColumns(root, root_gridTemplateColumns); + YGGridTrackListFree(root_gridTemplateColumns); + + YGNodeRef root_child0 = YGNodeNewWithConfig(config); + YGNodeStyleSetMargin(root_child0, YGEdgeTop, 10); + YGNodeStyleSetWidth(root_child0, 100); + YGNodeStyleSetHeight(root_child0, 50); + YGNodeSetBaselineFunc(root_child0, _baselineFunc); + YGNodeInsertChild(root, root_child0, 0); + // child0 marginTop (10) + baseline (25) = 35 + + YGNodeRef root_child1 = YGNodeNewWithConfig(config); + YGNodeStyleSetWidth(root_child1, 100); + YGNodeStyleSetHeight(root_child1, 30); + YGNodeSetBaselineFunc(root_child1, _baselineFunc); + YGNodeInsertChild(root, root_child1, 1); + // child1 baseline (15) + // child1 baselineShim = 35 - 15 = 20 + + YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR); + + ASSERT_FLOAT_EQ(0, YGNodeLayoutGetLeft(root)); + ASSERT_FLOAT_EQ(0, YGNodeLayoutGetTop(root)); + ASSERT_FLOAT_EQ(300, YGNodeLayoutGetWidth(root)); + ASSERT_FLOAT_EQ(60, YGNodeLayoutGetHeight(root)); + + ASSERT_FLOAT_EQ(0, YGNodeLayoutGetLeft(root_child0)); + ASSERT_FLOAT_EQ(10, YGNodeLayoutGetTop(root_child0)); + ASSERT_FLOAT_EQ(100, YGNodeLayoutGetWidth(root_child0)); + ASSERT_FLOAT_EQ(50, YGNodeLayoutGetHeight(root_child0)); + + ASSERT_FLOAT_EQ(100, YGNodeLayoutGetLeft(root_child1)); + ASSERT_FLOAT_EQ(20, YGNodeLayoutGetTop(root_child1)); + ASSERT_FLOAT_EQ(100, YGNodeLayoutGetWidth(root_child1)); + ASSERT_FLOAT_EQ(30, YGNodeLayoutGetHeight(root_child1)); + + YGNodeFreeRecursive(root); + + YGConfigFree(config); +} + +TEST(YogaTest, grid_align_baseline_with_padding) { + YGConfigRef config = YGConfigNew(); + + YGNodeRef root = YGNodeNewWithConfig(config); + YGNodeStyleSetAlignItems(root, YGAlignBaseline); + YGNodeStyleSetPositionType(root, YGPositionTypeAbsolute); + YGNodeStyleSetPadding(root, YGEdgeTop, 20); + YGNodeStyleSetPadding(root, YGEdgeBottom, 20); + YGNodeStyleSetDisplay(root, YGDisplayGrid); + auto root_gridTemplateColumns = YGGridTrackListCreate(); + YGGridTrackListAddTrack(root_gridTemplateColumns, YGPoints(100)); + YGGridTrackListAddTrack(root_gridTemplateColumns, YGPoints(100)); + YGNodeStyleSetGridTemplateColumns(root, root_gridTemplateColumns); + YGGridTrackListFree(root_gridTemplateColumns); + + YGNodeRef root_child0 = YGNodeNewWithConfig(config); + YGNodeStyleSetWidth(root_child0, 100); + YGNodeStyleSetHeight(root_child0, 60); + YGNodeSetBaselineFunc(root_child0, _baselineFunc); + YGNodeInsertChild(root, root_child0, 0); + // child0 baseline (30) + + YGNodeRef root_child1 = YGNodeNewWithConfig(config); + YGNodeStyleSetWidth(root_child1, 100); + YGNodeStyleSetHeight(root_child1, 40); + YGNodeSetBaselineFunc(root_child1, _baselineFunc); + YGNodeInsertChild(root, root_child1, 1); + // child1 baseline (20) + // child1 baselineShim = 30 - 20 = 10 + + YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR); + + ASSERT_FLOAT_EQ(0, YGNodeLayoutGetLeft(root)); + ASSERT_FLOAT_EQ(0, YGNodeLayoutGetTop(root)); + ASSERT_FLOAT_EQ(200, YGNodeLayoutGetWidth(root)); + ASSERT_FLOAT_EQ(100, YGNodeLayoutGetHeight(root)); + + ASSERT_FLOAT_EQ(0, YGNodeLayoutGetLeft(root_child0)); + // container padding (20) + child0 baseline (0) = 20 + ASSERT_FLOAT_EQ(20, YGNodeLayoutGetTop(root_child0)); + ASSERT_FLOAT_EQ(100, YGNodeLayoutGetWidth(root_child0)); + ASSERT_FLOAT_EQ(60, YGNodeLayoutGetHeight(root_child0)); + + ASSERT_FLOAT_EQ(100, YGNodeLayoutGetLeft(root_child1)); + // container padding (20) + child1 baselineShim (10) = 30 + ASSERT_FLOAT_EQ(30, YGNodeLayoutGetTop(root_child1)); + ASSERT_FLOAT_EQ(100, YGNodeLayoutGetWidth(root_child1)); + ASSERT_FLOAT_EQ(40, YGNodeLayoutGetHeight(root_child1)); + + YGNodeFreeRecursive(root); + + YGConfigFree(config); +} + +TEST(YogaTest, grid_align_baseline_nested_child) { + YGConfigRef config = YGConfigNew(); + + YGNodeRef root = YGNodeNewWithConfig(config); + YGNodeStyleSetAlignItems(root, YGAlignBaseline); + YGNodeStyleSetPositionType(root, YGPositionTypeAbsolute); + YGNodeStyleSetDisplay(root, YGDisplayGrid); + auto root_gridTemplateColumns = YGGridTrackListCreate(); + YGGridTrackListAddTrack(root_gridTemplateColumns, YGPoints(100)); + YGGridTrackListAddTrack(root_gridTemplateColumns, YGPoints(100)); + YGNodeStyleSetGridTemplateColumns(root, root_gridTemplateColumns); + YGGridTrackListFree(root_gridTemplateColumns); + + YGNodeRef root_child0 = YGNodeNewWithConfig(config); + YGNodeStyleSetMargin(root_child0, YGEdgeTop, 10); + YGNodeInsertChild(root, root_child0, 0); + + YGNodeRef root_child0_child0 = YGNodeNewWithConfig(config); + YGNodeStyleSetWidth(root_child0_child0, 80); + YGNodeStyleSetHeight(root_child0_child0, 60); + YGNodeSetBaselineFunc(root_child0_child0, _baselineFunc); + YGNodeInsertChild(root_child0, root_child0_child0, 0); + // child0 baseline (30) + marginTop (10) = 40 + + YGNodeRef root_child1 = YGNodeNewWithConfig(config); + YGNodeStyleSetWidth(root_child1, 100); + YGNodeStyleSetHeight(root_child1, 20); + YGNodeSetBaselineFunc(root_child1, _baselineFunc); + YGNodeInsertChild(root, root_child1, 1); + // child1 baseline (10) + // child1 baselineShim = 40 - 10 = 30 + + YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR); + + ASSERT_FLOAT_EQ(0, YGNodeLayoutGetLeft(root)); + ASSERT_FLOAT_EQ(0, YGNodeLayoutGetTop(root)); + ASSERT_FLOAT_EQ(200, YGNodeLayoutGetWidth(root)); + ASSERT_FLOAT_EQ(70, YGNodeLayoutGetHeight(root)); + + ASSERT_FLOAT_EQ(0, YGNodeLayoutGetLeft(root_child0)); + ASSERT_FLOAT_EQ(10, YGNodeLayoutGetTop(root_child0)); + ASSERT_FLOAT_EQ(100, YGNodeLayoutGetWidth(root_child0)); + ASSERT_FLOAT_EQ(60, YGNodeLayoutGetHeight(root_child0)); + + ASSERT_FLOAT_EQ(0, YGNodeLayoutGetLeft(root_child0_child0)); + ASSERT_FLOAT_EQ(0, YGNodeLayoutGetTop(root_child0_child0)); + ASSERT_FLOAT_EQ(80, YGNodeLayoutGetWidth(root_child0_child0)); + ASSERT_FLOAT_EQ(60, YGNodeLayoutGetHeight(root_child0_child0)); + + ASSERT_FLOAT_EQ(100, YGNodeLayoutGetLeft(root_child1)); + // baselineShim (30) + ASSERT_FLOAT_EQ(30, YGNodeLayoutGetTop(root_child1)); + ASSERT_FLOAT_EQ(100, YGNodeLayoutGetWidth(root_child1)); + ASSERT_FLOAT_EQ(20, YGNodeLayoutGetHeight(root_child1)); + + YGNodeFreeRecursive(root); + + YGConfigFree(config); +} diff --git a/tests/YGHadOverflowTest.cpp b/tests/YGHadOverflowTest.cpp index bbeefe7ee4..aed6f58277 100644 --- a/tests/YGHadOverflowTest.cpp +++ b/tests/YGHadOverflowTest.cpp @@ -133,3 +133,29 @@ TEST_F(YogaTest_HadOverflowTests, spacing_overflow_in_nested_nodes) { ASSERT_TRUE(YGNodeLayoutGetHadOverflow(root)); } + +TEST(YogaTest, grid_hadoverflow_when_content_exceeds_container) { + YGConfigRef config = YGConfigNew(); + + YGNodeRef root = YGNodeNewWithConfig(config); + YGNodeStyleSetDisplay(root, YGDisplayGrid); + YGNodeStyleSetWidth(root, 200); + YGNodeStyleSetHeight(root, 100); + + YGNodeRef child0 = YGNodeNewWithConfig(config); + YGNodeStyleSetWidth(child0, 150); + YGNodeStyleSetHeight(child0, 80); + YGNodeInsertChild(root, child0, 0); + + YGNodeRef child1 = YGNodeNewWithConfig(config); + YGNodeStyleSetWidth(child1, 150); + YGNodeStyleSetHeight(child1, 80); + YGNodeInsertChild(root, child1, 1); + + YGNodeCalculateLayout(root, 200, 100, YGDirectionLTR); + + ASSERT_TRUE(YGNodeLayoutGetHadOverflow(root)); + + YGNodeFreeRecursive(root); + YGConfigFree(config); +} diff --git a/tests/grid/AutoPlacementTest.cpp b/tests/grid/AutoPlacementTest.cpp new file mode 100644 index 0000000000..354938030f --- /dev/null +++ b/tests/grid/AutoPlacementTest.cpp @@ -0,0 +1,578 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include +#include +#include +#include +#include +#include +#include + +using namespace facebook::yoga; + +class GridAutoplacementTest : public ::testing::Test { + protected: + void SetUp() override { + gridContainer = new Node(); + gridContainer->style().setDisplay(Display::Grid); + } + + void TearDown() override { + delete gridContainer; + } + + Node* gridContainer{nullptr}; + + Node* createGridItem( + GridLine columnStart = GridLine::auto_(), + GridLine columnEnd = GridLine::auto_(), + GridLine rowStart = GridLine::auto_(), + GridLine rowEnd = GridLine::auto_()) { + auto item = new Node(); + item->style().setGridColumnStart(columnStart); + item->style().setGridColumnEnd(columnEnd); + item->style().setGridRowStart(rowStart); + item->style().setGridRowEnd(rowEnd); + gridContainer->insertChild(item, gridContainer->getChildren().size()); + return item; + } +}; + +TEST_F(GridAutoplacementTest, places_items_with_definite_positions) { + createGridItem( + GridLine::fromInteger(1), + GridLine::fromInteger(3), + GridLine::fromInteger(1), + GridLine::fromInteger(2)); + createGridItem( + GridLine::fromInteger(2), + GridLine::fromInteger(4), + GridLine::fromInteger(2), + GridLine::fromInteger(3)); + + auto autoPlacementResult = AutoPlacement::performAutoPlacement(gridContainer); + auto& placements = autoPlacementResult.gridItems; + ASSERT_EQ(placements.size(), 2); + + EXPECT_EQ(placements[0].columnStart, 0); + EXPECT_EQ(placements[0].columnEnd, 2); + EXPECT_EQ(placements[0].rowStart, 0); + EXPECT_EQ(placements[0].rowEnd, 1); + + EXPECT_EQ(placements[1].columnStart, 1); + EXPECT_EQ(placements[1].columnEnd, 3); + EXPECT_EQ(placements[1].rowStart, 1); + EXPECT_EQ(placements[1].rowEnd, 2); +} + +TEST_F(GridAutoplacementTest, places_items_with_definite_row_auto_column) { + createGridItem( + GridLine::fromInteger(1), + GridLine::fromInteger(3), + GridLine::fromInteger(1), + GridLine::fromInteger(2)); + + createGridItem( + GridLine::auto_(), + GridLine::auto_(), + GridLine::fromInteger(1), + GridLine::fromInteger(2)); + + auto autoPlacementResult = AutoPlacement::performAutoPlacement(gridContainer); + auto& placements = autoPlacementResult.gridItems; + ASSERT_EQ(placements.size(), 2); + + EXPECT_EQ(placements[0].columnStart, 0); + EXPECT_EQ(placements[0].columnEnd, 2); + EXPECT_EQ(placements[0].rowStart, 0); + EXPECT_EQ(placements[0].rowEnd, 1); + EXPECT_EQ(placements[1].columnStart, 2); + EXPECT_EQ(placements[1].columnEnd, 3); + EXPECT_EQ(placements[1].rowStart, 0); + EXPECT_EQ(placements[1].rowEnd, 1); +} + +TEST_F(GridAutoplacementTest, handles_overlapping_definite_row_items) { + createGridItem( + GridLine::auto_(), + GridLine::span(2), + GridLine::fromInteger(1), + GridLine::fromInteger(2)); + createGridItem( + GridLine::auto_(), + GridLine::span(2), + GridLine::fromInteger(1), + GridLine::fromInteger(2)); + + auto autoPlacementResult = AutoPlacement::performAutoPlacement(gridContainer); + auto& placements = autoPlacementResult.gridItems; + + ASSERT_EQ(placements.size(), 2); + + EXPECT_EQ(placements[0].columnStart, 0); + EXPECT_EQ(placements[0].columnEnd, 2); + EXPECT_EQ(placements[0].rowStart, 0); + EXPECT_EQ(placements[0].rowEnd, 1); + + EXPECT_EQ(placements[1].columnStart, 2); + EXPECT_EQ(placements[1].columnEnd, 4); + EXPECT_EQ(placements[1].rowStart, 0); + EXPECT_EQ(placements[1].rowEnd, 1); +} + +TEST_F(GridAutoplacementTest, places_auto_positioned_items) { + createGridItem(); + createGridItem(); + + auto autoPlacementResult = AutoPlacement::performAutoPlacement(gridContainer); + auto& placements = autoPlacementResult.gridItems; + + ASSERT_EQ(placements.size(), 2); + + EXPECT_EQ(placements[0].columnStart, 0); + EXPECT_EQ(placements[0].columnEnd, 1); + EXPECT_EQ(placements[0].rowStart, 0); + EXPECT_EQ(placements[0].rowEnd, 1); + + EXPECT_EQ(placements[1].columnStart, 0); + EXPECT_EQ(placements[1].columnEnd, 1); + EXPECT_EQ(placements[1].rowStart, 1); + EXPECT_EQ(placements[1].rowEnd, 2); +} + +TEST_F(GridAutoplacementTest, handles_large_spans) { + createGridItem(GridLine::auto_(), GridLine::span(5)); + + auto autoPlacementResult = AutoPlacement::performAutoPlacement(gridContainer); + auto& placements = autoPlacementResult.gridItems; + + ASSERT_EQ(placements.size(), 1); + + EXPECT_EQ(placements[0].columnStart, 0); + EXPECT_EQ(placements[0].columnEnd, 5); + EXPECT_EQ(placements[0].rowStart, 0); + EXPECT_EQ(placements[0].rowEnd, 1); +} + +TEST_F(GridAutoplacementTest, places_items_with_definite_column_auto_row) { + createGridItem( + GridLine::fromInteger(2), + GridLine::fromInteger(4), + GridLine::auto_(), + GridLine::auto_()); + + createGridItem( + GridLine::fromInteger(1), + GridLine::fromInteger(2), + GridLine::auto_(), + GridLine::auto_()); + + auto autoPlacementResult = AutoPlacement::performAutoPlacement(gridContainer); + auto& placements = autoPlacementResult.gridItems; + + ASSERT_EQ(placements.size(), 2); + + EXPECT_EQ(placements[0].columnStart, 1); + EXPECT_EQ(placements[0].columnEnd, 3); + EXPECT_EQ(placements[0].rowStart, 0); + EXPECT_EQ(placements[0].rowEnd, 1); + + EXPECT_EQ(placements[1].columnStart, 0); + EXPECT_EQ(placements[1].columnEnd, 1); + EXPECT_EQ(placements[1].rowStart, 1); + EXPECT_EQ(placements[1].rowEnd, 2); +} + +TEST_F(GridAutoplacementTest, avoids_overlaps_with_definite_column_items) { + createGridItem( + GridLine::fromInteger(1), + GridLine::fromInteger(3), + GridLine::fromInteger(1), + GridLine::fromInteger(3)); + + createGridItem( + GridLine::fromInteger(2), + GridLine::fromInteger(3), + GridLine::auto_(), + GridLine::auto_()); + + auto autoPlacementResult = AutoPlacement::performAutoPlacement(gridContainer); + auto& placements = autoPlacementResult.gridItems; + + ASSERT_EQ(placements.size(), 2); + + EXPECT_EQ(placements[0].columnStart, 0); + EXPECT_EQ(placements[0].columnEnd, 2); + EXPECT_EQ(placements[0].rowStart, 0); + EXPECT_EQ(placements[0].rowEnd, 2); + + EXPECT_EQ(placements[1].columnStart, 1); + EXPECT_EQ(placements[1].columnEnd, 2); + EXPECT_EQ(placements[1].rowStart, 2); + EXPECT_EQ(placements[1].rowEnd, 3); +} + +TEST_F(GridAutoplacementTest, handles_mixed_positioning_strategies) { + createGridItem( + GridLine::fromInteger(1), + GridLine::fromInteger(2), + GridLine::fromInteger(1), + GridLine::fromInteger(2)); + + createGridItem( + GridLine::auto_(), + GridLine::span(2), + GridLine::fromInteger(1), + GridLine::fromInteger(2)); + + createGridItem(); + + auto autoPlacementResult = AutoPlacement::performAutoPlacement(gridContainer); + auto& placements = autoPlacementResult.gridItems; + + ASSERT_EQ(placements.size(), 3); + + EXPECT_EQ(placements[0].columnStart, 0); + EXPECT_EQ(placements[0].columnEnd, 1); + EXPECT_EQ(placements[0].rowStart, 0); + EXPECT_EQ(placements[0].rowEnd, 1); + + EXPECT_EQ(placements[1].columnStart, 1); + EXPECT_EQ(placements[1].columnEnd, 3); + EXPECT_EQ(placements[1].rowStart, 0); + EXPECT_EQ(placements[1].rowEnd, 1); + + EXPECT_EQ(placements[2].columnStart, 0); + EXPECT_EQ(placements[2].columnEnd, 1); + EXPECT_EQ(placements[2].rowStart, 1); + EXPECT_EQ(placements[2].rowEnd, 2); +} + +TEST_F(GridAutoplacementTest, handles_negative_grid_line_references_simple) { + std::vector columns = { + GridTrackSize::length(20.0f), + GridTrackSize::length(20.0f), + GridTrackSize::length(20.0f)}; + gridContainer->style().setGridTemplateColumns(std::move(columns)); + createGridItem(GridLine::fromInteger(-1), GridLine::fromInteger(-2)); + + auto autoPlacementResult = AutoPlacement::performAutoPlacement(gridContainer); + auto& placements = autoPlacementResult.gridItems; + + ASSERT_EQ(placements.size(), 1); + EXPECT_EQ(placements[0].columnStart, 2); + EXPECT_EQ(placements[0].columnEnd, 3); + EXPECT_EQ(placements[0].rowStart, 0); + EXPECT_EQ(placements[0].rowEnd, 1); +} + +TEST_F(GridAutoplacementTest, handles_negative_grid_line_references) { + std::vector columns = { + GridTrackSize::length(20.0f), + GridTrackSize::length(20.0f), + GridTrackSize::length(20.0f)}; + gridContainer->style().setGridTemplateColumns(std::move(columns)); + + // .one { grid-column-start: -5; grid-column-end: -4; } + auto node1 = + createGridItem(GridLine::fromInteger(-5), GridLine::fromInteger(-4)); + + // .two { grid-column-start: 1; grid-column-end: 2; grid-row-start: 1; + // grid-row-end: 2; } + auto node2 = createGridItem( + GridLine::fromInteger(1), + GridLine::fromInteger(2), + GridLine::fromInteger(1), + GridLine::fromInteger(2)); + + auto node3 = createGridItem(); + auto node4 = createGridItem(); + auto node5 = createGridItem(); + + auto autoPlacementResult = AutoPlacement::performAutoPlacement(gridContainer); + auto& placements = autoPlacementResult.gridItems; + + ASSERT_EQ(placements.size(), 5); + + for (auto placement : placements) { + if (placement.node == node1) { + EXPECT_EQ(placement.columnStart, -1); + EXPECT_EQ(placement.columnEnd, 0); + EXPECT_EQ(placement.rowStart, 0); + EXPECT_EQ(placement.rowEnd, 1); + } else if (placement.node == node2) { + EXPECT_EQ(placement.columnStart, 0); + EXPECT_EQ(placement.columnEnd, 1); + EXPECT_EQ(placement.rowStart, 0); + EXPECT_EQ(placement.rowEnd, 1); + } else if (placement.node == node3) { + EXPECT_EQ(placement.columnStart, 1); + EXPECT_EQ(placement.columnEnd, 2); + EXPECT_EQ(placement.rowStart, 0); + EXPECT_EQ(placement.rowEnd, 1); + } else if (placement.node == node4) { + EXPECT_EQ(placement.columnStart, 2); + EXPECT_EQ(placement.columnEnd, 3); + EXPECT_EQ(placement.rowStart, 0); + EXPECT_EQ(placement.rowEnd, 1); + } else if (placement.node == node5) { + EXPECT_EQ(placement.columnStart, -1); + EXPECT_EQ(placement.columnEnd, 0); + EXPECT_EQ(placement.rowStart, 1); + EXPECT_EQ(placement.rowEnd, 2); + } + } +} + +TEST_F(GridAutoplacementTest, same_start_and_end_line_spans_one_track) { + createGridItem( + GridLine::fromInteger(1), + GridLine::fromInteger(1), + GridLine::fromInteger(1), + GridLine::fromInteger(2)); + + auto autoPlacementResult = AutoPlacement::performAutoPlacement(gridContainer); + auto& placements = autoPlacementResult.gridItems; + + ASSERT_EQ(placements.size(), 1); + EXPECT_EQ(placements[0].columnStart, 0); + EXPECT_EQ(placements[0].columnEnd, 1); + EXPECT_EQ(placements[0].rowStart, 0); + EXPECT_EQ(placements[0].rowEnd, 1); +} + +TEST_F( + GridAutoplacementTest, + handles_negative_grid_lines_with_row_positioning) { + std::vector columns = { + GridTrackSize::length(20.0f), + GridTrackSize::length(20.0f), + GridTrackSize::length(20.0f)}; + gridContainer->style().setGridTemplateColumns(std::move(columns)); + + // .one { grid-column-start: -5; grid-column-end: -4; grid-row-start: 2; + // grid-row-end: 3; } + auto node1 = createGridItem( + GridLine::fromInteger(-5), + GridLine::fromInteger(-4), + GridLine::fromInteger(2), + GridLine::fromInteger(3)); + + // .two { grid-row-start: 1; grid-row-end: 2; } + auto node2 = createGridItem( + GridLine::auto_(), + GridLine::auto_(), + GridLine::fromInteger(1), + GridLine::fromInteger(2)); + + auto node3 = createGridItem(); + auto node4 = createGridItem(); + auto node5 = createGridItem(); + + auto autoPlacementResult = AutoPlacement::performAutoPlacement(gridContainer); + auto& placements = autoPlacementResult.gridItems; + ; + + ASSERT_EQ(placements.size(), 5); + + for (auto placement : placements) { + if (placement.node == node1) { + EXPECT_EQ(placement.columnStart, -1); + EXPECT_EQ(placement.columnEnd, 0); + EXPECT_EQ(placement.rowStart, 1); + EXPECT_EQ(placement.rowEnd, 2); + } else if (placement.node == node2) { + EXPECT_EQ(placement.columnStart, -1); + EXPECT_EQ(placement.columnEnd, 0); + EXPECT_EQ(placement.rowStart, 0); + EXPECT_EQ(placement.rowEnd, 1); + } else if (placement.node == node3) { + EXPECT_EQ(placement.columnStart, 0); + EXPECT_EQ(placement.columnEnd, 1); + EXPECT_EQ(placement.rowStart, 0); + EXPECT_EQ(placement.rowEnd, 1); + } else if (placement.node == node4) { + EXPECT_EQ(placement.columnStart, 1); + EXPECT_EQ(placement.columnEnd, 2); + EXPECT_EQ(placement.rowStart, 0); + EXPECT_EQ(placement.rowEnd, 1); + } else if (placement.node == node5) { + EXPECT_EQ(placement.columnStart, 2); + EXPECT_EQ(placement.columnEnd, 3); + EXPECT_EQ(placement.rowStart, 0); + EXPECT_EQ(placement.rowEnd, 1); + } + } +} + +TEST_F(GridAutoplacementTest, skips_past_large_blocking_items) { + auto largeItem = createGridItem( + GridLine::fromInteger(1), + GridLine::fromInteger(6), + GridLine::fromInteger(1), + GridLine::fromInteger(2)); + + auto autoItem1 = createGridItem( + GridLine::auto_(), + GridLine::auto_(), + GridLine::fromInteger(1), + GridLine::fromInteger(2)); + auto autoItem2 = createGridItem( + GridLine::auto_(), + GridLine::auto_(), + GridLine::fromInteger(1), + GridLine::fromInteger(2)); + + auto autoPlacementResult = AutoPlacement::performAutoPlacement(gridContainer); + auto& placements = autoPlacementResult.gridItems; + + ASSERT_EQ(placements.size(), 3); + + for (auto placement : placements) { + if (placement.node == largeItem) { + EXPECT_EQ(placement.columnStart, 0); + EXPECT_EQ(placement.columnEnd, 5); + EXPECT_EQ(placement.rowStart, 0); + EXPECT_EQ(placement.rowEnd, 1); + } else if (placement.node == autoItem1) { + EXPECT_EQ(placement.columnStart, 5); + EXPECT_EQ(placement.columnEnd, 6); + EXPECT_EQ(placement.rowStart, 0); + EXPECT_EQ(placement.rowEnd, 1); + } else if (placement.node == autoItem2) { + EXPECT_EQ(placement.columnStart, 6); + EXPECT_EQ(placement.columnEnd, 7); + EXPECT_EQ(placement.rowStart, 0); + EXPECT_EQ(placement.rowEnd, 1); + } + } +} + +TEST_F(GridAutoplacementTest, skips_past_large_blocking_rows) { + createGridItem( + GridLine::fromInteger(1), + GridLine::fromInteger(2), + GridLine::fromInteger(1), + GridLine::fromInteger(6)); + + auto autoItem = createGridItem( + GridLine::fromInteger(1), + GridLine::fromInteger(2), + GridLine::auto_(), + GridLine::auto_()); + + auto autoPlacementResult = AutoPlacement::performAutoPlacement(gridContainer); + auto& placements = autoPlacementResult.gridItems; + + ASSERT_EQ(placements.size(), 2); + + for (auto placement : placements) { + if (placement.node == autoItem) { + EXPECT_EQ(placement.columnStart, 0); + EXPECT_EQ(placement.columnEnd, 1); + EXPECT_EQ(placement.rowStart, 5); + EXPECT_EQ(placement.rowEnd, 6); + } + } +} + +TEST_F(GridAutoplacementTest, handles_nested_overlapping_items) { + createGridItem( + GridLine::fromInteger(1), + GridLine::fromInteger(8), + GridLine::fromInteger(1), + GridLine::fromInteger(2)); + + createGridItem( + GridLine::fromInteger(3), + GridLine::fromInteger(5), + GridLine::fromInteger(2), + GridLine::fromInteger(3)); + + auto autoItem = createGridItem( + GridLine::auto_(), + GridLine::auto_(), + GridLine::fromInteger(1), + GridLine::fromInteger(2)); + + auto autoPlacementResult = AutoPlacement::performAutoPlacement(gridContainer); + auto& placements = autoPlacementResult.gridItems; + + ASSERT_EQ(placements.size(), 3); + + for (auto placement : placements) { + if (placement.node == autoItem) { + EXPECT_EQ(placement.columnStart, 7); + EXPECT_EQ(placement.columnEnd, 8); + EXPECT_EQ(placement.rowStart, 0); + EXPECT_EQ(placement.rowEnd, 1); + } + } +} + +TEST_F( + GridAutoplacementTest, + handles_negative_and_positive_grid_lines_auto_row) { + std::vector columns = { + GridTrackSize::length(20.0f), + GridTrackSize::length(20.0f), + GridTrackSize::length(20.0f)}; + gridContainer->style().setGridTemplateColumns(std::move(columns)); + + // .one { grid-column-start: -5; grid-column-end: -4; grid-row-start: 1; + // grid-row-end: 2; } + auto node1 = createGridItem( + GridLine::fromInteger(-5), + GridLine::fromInteger(-4), + GridLine::fromInteger(1), + GridLine::fromInteger(2)); + + // .two { grid-column-start: 1; grid-column-end: 2; } (auto rows) + auto node2 = createGridItem( + GridLine::fromInteger(1), + GridLine::fromInteger(2), + GridLine::auto_(), + GridLine::auto_()); + + auto node3 = createGridItem(); + auto node4 = createGridItem(); + auto node5 = createGridItem(); + + auto autoPlacementResult = AutoPlacement::performAutoPlacement(gridContainer); + auto& placements = autoPlacementResult.gridItems; + ASSERT_EQ(placements.size(), 5); + + for (auto placement : placements) { + if (placement.node == node1) { + EXPECT_EQ(placement.columnStart, -1); + EXPECT_EQ(placement.columnEnd, 0); + EXPECT_EQ(placement.rowStart, 0); + EXPECT_EQ(placement.rowEnd, 1); + } else if (placement.node == node2) { + EXPECT_EQ(placement.columnStart, 0); + EXPECT_EQ(placement.columnEnd, 1); + EXPECT_EQ(placement.rowStart, 0); + EXPECT_EQ(placement.rowEnd, 1); + } else if (placement.node == node3) { + EXPECT_EQ(placement.columnStart, 1); + EXPECT_EQ(placement.columnEnd, 2); + EXPECT_EQ(placement.rowStart, 0); + EXPECT_EQ(placement.rowEnd, 1); + } else if (placement.node == node4) { + EXPECT_EQ(placement.columnStart, 2); + EXPECT_EQ(placement.columnEnd, 3); + EXPECT_EQ(placement.rowStart, 0); + EXPECT_EQ(placement.rowEnd, 1); + } else if (placement.node == node5) { + EXPECT_EQ(placement.columnStart, -1); + EXPECT_EQ(placement.columnEnd, 0); + EXPECT_EQ(placement.rowStart, 1); + EXPECT_EQ(placement.rowEnd, 2); + } + } +} diff --git a/tests/grid/CreateGridTrackTest.cpp b/tests/grid/CreateGridTrackTest.cpp new file mode 100644 index 0000000000..ceae7c494c --- /dev/null +++ b/tests/grid/CreateGridTrackTest.cpp @@ -0,0 +1,292 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include +#include +#include +#include +#include +#include +#include + +using namespace facebook::yoga; + +class GridCreateTracksTest : public ::testing::Test { + protected: + void SetUp() override { + gridContainer = new Node(); + gridContainer->style().setDisplay(Display::Grid); + } + + void TearDown() override { + delete gridContainer; + } + + Node* gridContainer{nullptr}; + + Node* createGridItem( + GridLine columnStart = GridLine::auto_(), + GridLine columnEnd = GridLine::auto_(), + GridLine rowStart = GridLine::auto_(), + GridLine rowEnd = GridLine::auto_()) { + auto item = new Node(); + item->style().setGridColumnStart(columnStart); + item->style().setGridColumnEnd(columnEnd); + item->style().setGridRowStart(rowStart); + item->style().setGridRowEnd(rowEnd); + gridContainer->insertChild(item, gridContainer->getChildren().size()); + return item; + } +}; + +TEST_F(GridCreateTracksTest, only_explicit_tracks) { + std::vector columns = { + GridTrackSize::length(100.0f), GridTrackSize::length(150.0f)}; + std::vector rows = { + GridTrackSize::length(80.0f), GridTrackSize::fr(1.0f)}; + + gridContainer->style().setGridTemplateColumns(std::move(columns)); + gridContainer->style().setGridTemplateRows(std::move(rows)); + + createGridItem( + GridLine::fromInteger(1), + GridLine::fromInteger(2), + GridLine::fromInteger(1), + GridLine::fromInteger(2)); + createGridItem( + GridLine::fromInteger(2), + GridLine::fromInteger(3), + GridLine::fromInteger(2), + GridLine::fromInteger(3)); + + auto autoPlacementResult = + ResolvedAutoPlacement::resolveGridItemPlacements(gridContainer); + GridTracks tracks = createGridTracks(gridContainer, autoPlacementResult); + + EXPECT_EQ(tracks.columnTracks.size(), 2); + EXPECT_TRUE(tracks.columnTracks[0].minSizingFunction.isPoints()); + EXPECT_TRUE(tracks.columnTracks[1].minSizingFunction.isPoints()); + + EXPECT_EQ(tracks.rowTracks.size(), 2); + EXPECT_TRUE(tracks.rowTracks[0].minSizingFunction.isPoints()); + EXPECT_TRUE(tracks.rowTracks[1].maxSizingFunction.isStretch()); +} + +TEST_F(GridCreateTracksTest, implicit_tracks_after_explicit_grid) { + std::vector columns = { + GridTrackSize::length(100.0f), GridTrackSize::length(150.0f)}; + + gridContainer->style().setGridTemplateColumns(std::move(columns)); + + createGridItem( + GridLine::fromInteger(1), + GridLine::fromInteger(5), + GridLine::fromInteger(1), + GridLine::fromInteger(2)); + + auto autoPlacementResult = + ResolvedAutoPlacement::resolveGridItemPlacements(gridContainer); + GridTracks tracks = createGridTracks(gridContainer, autoPlacementResult); + + EXPECT_EQ(tracks.columnTracks.size(), 4); + + EXPECT_TRUE(tracks.columnTracks[0].minSizingFunction.isPoints()); + EXPECT_TRUE(tracks.columnTracks[1].minSizingFunction.isPoints()); + + EXPECT_TRUE(tracks.columnTracks[2].minSizingFunction.isAuto()); + EXPECT_TRUE(tracks.columnTracks[3].minSizingFunction.isAuto()); +} + +TEST_F( + GridCreateTracksTest, + implicit_tracks_before_explicit_grid_negative_indices) { + std::vector columns = { + GridTrackSize::length(20.0f), + GridTrackSize::length(20.0f), + GridTrackSize::length(20.0f)}; + + gridContainer->style().setGridTemplateColumns(std::move(columns)); + + createGridItem(GridLine::fromInteger(-5), GridLine::fromInteger(-4)); + + auto autoPlacementResult = + ResolvedAutoPlacement::resolveGridItemPlacements(gridContainer); + GridTracks tracks = createGridTracks(gridContainer, autoPlacementResult); + + EXPECT_EQ(tracks.columnTracks.size(), 4); + + EXPECT_TRUE(tracks.columnTracks[0].minSizingFunction.isAuto()); +} + +TEST_F(GridCreateTracksTest, implicit_tracks_before_and_after) { + std::vector columns = { + GridTrackSize::length(100.0f), GridTrackSize::fr(1.0f)}; + + // creates 3 grid lines 1, 2, 3 + gridContainer->style().setGridTemplateColumns(std::move(columns)); + // -4 index = 0 index. 1 new track before. and 5 index = 1 new track after. + // total 5 tracks + createGridItem(GridLine::fromInteger(-4), GridLine::fromInteger(5)); + + auto autoPlacementResult = + ResolvedAutoPlacement::resolveGridItemPlacements(gridContainer); + GridTracks tracks = createGridTracks(gridContainer, autoPlacementResult); + + EXPECT_EQ(tracks.columnTracks.size(), 5); + EXPECT_TRUE(tracks.columnTracks[0].minSizingFunction.isAuto()); + EXPECT_TRUE(tracks.columnTracks[1].minSizingFunction.isPoints()); + EXPECT_TRUE(tracks.columnTracks[2].maxSizingFunction.isStretch()); + EXPECT_TRUE(tracks.columnTracks[4].minSizingFunction.isAuto()); + EXPECT_TRUE(tracks.columnTracks[4].minSizingFunction.isAuto()); +} + +TEST_F(GridCreateTracksTest, no_explicit_tracks_only_implicit) { + createGridItem(); + createGridItem(); + createGridItem(); + + auto autoPlacementResult = + ResolvedAutoPlacement::resolveGridItemPlacements(gridContainer); + GridTracks tracks = createGridTracks(gridContainer, autoPlacementResult); + + for (const auto& track : tracks.columnTracks) { + EXPECT_TRUE(track.minSizingFunction.isAuto()); + } + + for (const auto& track : tracks.rowTracks) { + EXPECT_TRUE(track.minSizingFunction.isAuto()); + } +} + +TEST_F(GridCreateTracksTest, empty_grid_no_items) { + std::vector columns = { + GridTrackSize::length(100.0f), GridTrackSize::length(150.0f)}; + + gridContainer->style().setGridTemplateColumns(std::move(columns)); + + auto autoPlacementResult = + ResolvedAutoPlacement::resolveGridItemPlacements(gridContainer); + GridTracks tracks = createGridTracks(gridContainer, autoPlacementResult); + EXPECT_EQ(tracks.columnTracks.size(), 2); +} + +TEST_F(GridCreateTracksTest, large_span_creating_many_implicit_tracks) { + createGridItem(GridLine::auto_(), GridLine::span(10)); + + auto autoPlacementResult = + ResolvedAutoPlacement::resolveGridItemPlacements(gridContainer); + GridTracks tracks = createGridTracks(gridContainer, autoPlacementResult); + + EXPECT_EQ(tracks.columnTracks.size(), 10); + + for (const auto& track : tracks.columnTracks) { + EXPECT_TRUE(track.minSizingFunction.isAuto()); + } +} + +TEST_F(GridCreateTracksTest, grid_auto_columns_pattern_repeats_backward) { + std::vector columns = { + GridTrackSize::length(100.0f), GridTrackSize::length(150.0f)}; + gridContainer->style().setGridTemplateColumns(std::move(columns)); + + std::vector autoColumns = { + GridTrackSize::length(50.0f), + GridTrackSize::fr(1.0f), + GridTrackSize::length(75.0f)}; + gridContainer->style().setGridAutoColumns(std::move(autoColumns)); + + createGridItem(GridLine::fromInteger(-8), GridLine::fromInteger(-7)); + createGridItem(GridLine::fromInteger(3), GridLine::fromInteger(6)); + + auto autoPlacementResult = + ResolvedAutoPlacement::resolveGridItemPlacements(gridContainer); + GridTracks tracks = createGridTracks(gridContainer, autoPlacementResult); + + // 5 negative implicit + 2 explicit + 3 positive implicit = 10 tracks + EXPECT_EQ(tracks.columnTracks.size(), 10); + + // Implicit tracks before explicit grid + EXPECT_TRUE(tracks.columnTracks[4].minSizingFunction.isPoints()); + EXPECT_EQ( + tracks.columnTracks[4].minSizingFunction.resolve(0.0f).unwrap(), 75.0f); + EXPECT_TRUE(tracks.columnTracks[3].maxSizingFunction.isStretch()); + EXPECT_TRUE(tracks.columnTracks[2].minSizingFunction.isPoints()); + EXPECT_EQ( + tracks.columnTracks[2].minSizingFunction.resolve(0.0f).unwrap(), 50.0f); + EXPECT_TRUE(tracks.columnTracks[1].minSizingFunction.isPoints()); + EXPECT_EQ( + tracks.columnTracks[1].minSizingFunction.resolve(0.0f).unwrap(), 75.0f); + EXPECT_TRUE(tracks.columnTracks[0].maxSizingFunction.isStretch()); + + // Explicit grid + EXPECT_TRUE(tracks.columnTracks[5].minSizingFunction.isPoints()); + EXPECT_EQ( + tracks.columnTracks[5].minSizingFunction.resolve(0.0f).unwrap(), 100.0f); + EXPECT_TRUE(tracks.columnTracks[6].minSizingFunction.isPoints()); + EXPECT_EQ( + tracks.columnTracks[6].minSizingFunction.resolve(0.0f).unwrap(), 150.0f); + + // Implicit tracks after explicit grid + EXPECT_TRUE(tracks.columnTracks[7].minSizingFunction.isPoints()); + EXPECT_EQ( + tracks.columnTracks[7].minSizingFunction.resolve(0.0f).unwrap(), 50.0f); + EXPECT_TRUE(tracks.columnTracks[8].maxSizingFunction.isStretch()); + EXPECT_TRUE(tracks.columnTracks[9].minSizingFunction.isPoints()); + EXPECT_EQ( + tracks.columnTracks[9].minSizingFunction.resolve(0.0f).unwrap(), 75.0f); +} + +TEST_F(GridCreateTracksTest, grid_auto_rows_pattern_repeats_both_directions) { + std::vector rows = {GridTrackSize::length(200.0f)}; + gridContainer->style().setGridTemplateRows(std::move(rows)); + + std::vector autoRows = { + GridTrackSize::length(60.0f), GridTrackSize::length(80.0f)}; + gridContainer->style().setGridAutoRows(std::move(autoRows)); + + createGridItem( + GridLine::auto_(), + GridLine::auto_(), + GridLine::fromInteger(-4), + GridLine::fromInteger(-2)); + + createGridItem( + GridLine::auto_(), + GridLine::auto_(), + GridLine::fromInteger(3), + GridLine::fromInteger(5)); + + auto autoPlacementResult = + ResolvedAutoPlacement::resolveGridItemPlacements(gridContainer); + GridTracks tracks = createGridTracks(gridContainer, autoPlacementResult); + + // 2 implicit before + 1 explicit + 3 implicit after = 7 tracks + EXPECT_EQ(tracks.rowTracks.size(), 6); + + // Implicit tracks before explicit grid + EXPECT_TRUE(tracks.rowTracks[1].minSizingFunction.isPoints()); + EXPECT_EQ( + tracks.rowTracks[1].minSizingFunction.resolve(0.0f).unwrap(), 80.0f); + EXPECT_TRUE(tracks.rowTracks[0].minSizingFunction.isPoints()); + EXPECT_EQ( + tracks.rowTracks[0].minSizingFunction.resolve(0.0f).unwrap(), 60.0f); + + // Explicit grid + EXPECT_EQ( + tracks.rowTracks[2].minSizingFunction.resolve(0.0f).unwrap(), 200.0f); + + // Implicit tracks after explicit grid + EXPECT_TRUE(tracks.rowTracks[3].minSizingFunction.isPoints()); + EXPECT_EQ( + tracks.rowTracks[3].minSizingFunction.resolve(0.0f).unwrap(), 60.0f); + EXPECT_TRUE(tracks.rowTracks[4].minSizingFunction.isPoints()); + EXPECT_EQ( + tracks.rowTracks[4].minSizingFunction.resolve(0.0f).unwrap(), 80.0f); + EXPECT_TRUE(tracks.rowTracks[5].minSizingFunction.isPoints()); + EXPECT_EQ( + tracks.rowTracks[5].minSizingFunction.resolve(0.0f).unwrap(), 60.0f); +} From 981275ffb2054e7c86a999b404510a39f9dc2a76 Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Sat, 21 Feb 2026 22:11:09 -0800 Subject: [PATCH 5/7] CSS Grid 5/9: Java/Kotlin bindings Summary: Add Java/Kotlin bindings for CSS Grid support. Includes grid API classes (YogaGridTrackList, YogaGridTrackValue, YogaGridTrackType), JNI bridge updates, enum changes (YogaDisplay, YogaAlign, YogaJustify). Also includes React Native Android mirror of all Java/Kotlin changes. Differential Revision: D93946256 --- java/com/facebook/yoga/YogaGridTrackList.java | 39 +++ .../com/facebook/yoga/YogaGridTrackValue.java | 76 +++++ java/com/facebook/yoga/YogaNative.kt | 64 ++++ java/com/facebook/yoga/YogaNode.kt | 24 ++ java/com/facebook/yoga/YogaNodeJNIBase.java | 136 ++++++++ java/jni/YGJNIVanilla.cpp | 308 ++++++++++++++++++ 6 files changed, 647 insertions(+) create mode 100644 java/com/facebook/yoga/YogaGridTrackList.java create mode 100644 java/com/facebook/yoga/YogaGridTrackValue.java diff --git a/java/com/facebook/yoga/YogaGridTrackList.java b/java/com/facebook/yoga/YogaGridTrackList.java new file mode 100644 index 0000000000..ff39ccf899 --- /dev/null +++ b/java/com/facebook/yoga/YogaGridTrackList.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.yoga; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Represents a list of grid tracks for use with grid-template-rows/columns. + */ +public class YogaGridTrackList { + private final List tracks; + + public YogaGridTrackList() { + this.tracks = new ArrayList<>(); + } + + public void addTrack(YogaGridTrackValue track) { + tracks.add(track); + } + + public List getTracks() { + return Collections.unmodifiableList(tracks); + } + + public int size() { + return tracks.size(); + } + + public YogaGridTrackValue get(int index) { + return tracks.get(index); + } +} diff --git a/java/com/facebook/yoga/YogaGridTrackValue.java b/java/com/facebook/yoga/YogaGridTrackValue.java new file mode 100644 index 0000000000..b5285e54c2 --- /dev/null +++ b/java/com/facebook/yoga/YogaGridTrackValue.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.yoga; + +/** + * Represents a grid track value for use with grid-template-rows/columns. + */ +public class YogaGridTrackValue { + public enum Type { + AUTO, + POINTS, + PERCENT, + FR, + MINMAX + } + + private final Type type; + private final float value; + private final YogaGridTrackValue minValue; + private final YogaGridTrackValue maxValue; + + private YogaGridTrackValue(Type type, float value) { + this.type = type; + this.value = value; + this.minValue = null; + this.maxValue = null; + } + + private YogaGridTrackValue(YogaGridTrackValue min, YogaGridTrackValue max) { + this.type = Type.MINMAX; + this.value = 0; + this.minValue = min; + this.maxValue = max; + } + + public static YogaGridTrackValue auto() { + return new YogaGridTrackValue(Type.AUTO, 0); + } + + public static YogaGridTrackValue points(float points) { + return new YogaGridTrackValue(Type.POINTS, points); + } + + public static YogaGridTrackValue percent(float percent) { + return new YogaGridTrackValue(Type.PERCENT, percent); + } + + public static YogaGridTrackValue fr(float fr) { + return new YogaGridTrackValue(Type.FR, fr); + } + + public static YogaGridTrackValue minMax(YogaGridTrackValue min, YogaGridTrackValue max) { + return new YogaGridTrackValue(min, max); + } + + public Type getType() { + return type; + } + + public float getValue() { + return value; + } + + public YogaGridTrackValue getMinValue() { + return minValue; + } + + public YogaGridTrackValue getMaxValue() { + return maxValue; + } +} diff --git a/java/com/facebook/yoga/YogaNative.kt b/java/com/facebook/yoga/YogaNative.kt index 5ce6a02872..45e28e5aa4 100644 --- a/java/com/facebook/yoga/YogaNative.kt +++ b/java/com/facebook/yoga/YogaNative.kt @@ -328,4 +328,68 @@ public object YogaNative { nativePointer: Long, alwaysFormContainingBlock: Boolean, ) + + @JvmStatic + public external fun jni_YGNodeStyleSetGridTemplateColumnsJNI( + nativePointer: Long, + types: IntArray, + values: FloatArray, + minTypes: IntArray, + minValues: FloatArray, + maxTypes: IntArray, + maxValues: FloatArray, + ) + + @JvmStatic + public external fun jni_YGNodeStyleSetGridTemplateRowsJNI( + nativePointer: Long, + types: IntArray, + values: FloatArray, + minTypes: IntArray, + minValues: FloatArray, + maxTypes: IntArray, + maxValues: FloatArray, + ) + + @JvmStatic + public external fun jni_YGNodeStyleSetGridAutoColumnsJNI( + nativePointer: Long, + types: IntArray, + values: FloatArray, + minTypes: IntArray, + minValues: FloatArray, + maxTypes: IntArray, + maxValues: FloatArray, + ) + + @JvmStatic + public external fun jni_YGNodeStyleSetGridAutoRowsJNI( + nativePointer: Long, + types: IntArray, + values: FloatArray, + minTypes: IntArray, + minValues: FloatArray, + maxTypes: IntArray, + maxValues: FloatArray, + ) + + @JvmStatic + public external fun jni_YGNodeStyleSetGridColumnStartJNI(nativePointer: Long, value: Int) + + @JvmStatic + public external fun jni_YGNodeStyleSetGridColumnStartSpanJNI(nativePointer: Long, span: Int) + + @JvmStatic public external fun jni_YGNodeStyleSetGridColumnEndJNI(nativePointer: Long, value: Int) + + @JvmStatic + public external fun jni_YGNodeStyleSetGridColumnEndSpanJNI(nativePointer: Long, span: Int) + + @JvmStatic public external fun jni_YGNodeStyleSetGridRowStartJNI(nativePointer: Long, value: Int) + + @JvmStatic + public external fun jni_YGNodeStyleSetGridRowStartSpanJNI(nativePointer: Long, span: Int) + + @JvmStatic public external fun jni_YGNodeStyleSetGridRowEndJNI(nativePointer: Long, value: Int) + + @JvmStatic public external fun jni_YGNodeStyleSetGridRowEndSpanJNI(nativePointer: Long, span: Int) } diff --git a/java/com/facebook/yoga/YogaNode.kt b/java/com/facebook/yoga/YogaNode.kt index fbd06f2e1b..4867faa4ad 100644 --- a/java/com/facebook/yoga/YogaNode.kt +++ b/java/com/facebook/yoga/YogaNode.kt @@ -210,6 +210,30 @@ public abstract class YogaNode : YogaProps { public abstract fun setGapPercent(gutter: YogaGutter, gapLength: Float) + public abstract fun setGridTemplateColumns(trackList: YogaGridTrackList) + + public abstract fun setGridTemplateRows(trackList: YogaGridTrackList) + + public abstract fun setGridAutoColumns(trackList: YogaGridTrackList) + + public abstract fun setGridAutoRows(trackList: YogaGridTrackList) + + public abstract fun setGridColumnStart(value: Int) + + public abstract fun setGridColumnStartSpan(span: Int) + + public abstract fun setGridColumnEnd(value: Int) + + public abstract fun setGridColumnEndSpan(span: Int) + + public abstract fun setGridRowStart(value: Int) + + public abstract fun setGridRowStartSpan(span: Int) + + public abstract fun setGridRowEnd(value: Int) + + public abstract fun setGridRowEndSpan(span: Int) + public abstract val layoutX: Float public abstract val layoutY: Float diff --git a/java/com/facebook/yoga/YogaNodeJNIBase.java b/java/com/facebook/yoga/YogaNodeJNIBase.java index 9fb0e51954..999c4bc98c 100644 --- a/java/com/facebook/yoga/YogaNodeJNIBase.java +++ b/java/com/facebook/yoga/YogaNodeJNIBase.java @@ -824,4 +824,140 @@ public void setGap(YogaGutter gutter, float gapLength) { public void setGapPercent(YogaGutter gutter, float gapLength) { YogaNative.jni_YGNodeStyleSetGapPercentJNI(mNativePointer, gutter.intValue(), gapLength); } + + @Override + public void setGridTemplateColumns(YogaGridTrackList trackList) { + int[] types = new int[trackList.size()]; + float[] values = new float[trackList.size()]; + int[] minTypes = new int[trackList.size()]; + float[] minValues = new float[trackList.size()]; + int[] maxTypes = new int[trackList.size()]; + float[] maxValues = new float[trackList.size()]; + + for (int i = 0; i < trackList.size(); i++) { + YogaGridTrackValue track = trackList.get(i); + types[i] = track.getType().ordinal(); + values[i] = track.getValue(); + if (track.getType() == YogaGridTrackValue.Type.MINMAX) { + minTypes[i] = track.getMinValue().getType().ordinal(); + minValues[i] = track.getMinValue().getValue(); + maxTypes[i] = track.getMaxValue().getType().ordinal(); + maxValues[i] = track.getMaxValue().getValue(); + } + } + YogaNative.jni_YGNodeStyleSetGridTemplateColumnsJNI( + mNativePointer, types, values, minTypes, minValues, maxTypes, maxValues); + } + + @Override + public void setGridTemplateRows(YogaGridTrackList trackList) { + int[] types = new int[trackList.size()]; + float[] values = new float[trackList.size()]; + int[] minTypes = new int[trackList.size()]; + float[] minValues = new float[trackList.size()]; + int[] maxTypes = new int[trackList.size()]; + float[] maxValues = new float[trackList.size()]; + + for (int i = 0; i < trackList.size(); i++) { + YogaGridTrackValue track = trackList.get(i); + types[i] = track.getType().ordinal(); + values[i] = track.getValue(); + if (track.getType() == YogaGridTrackValue.Type.MINMAX) { + minTypes[i] = track.getMinValue().getType().ordinal(); + minValues[i] = track.getMinValue().getValue(); + maxTypes[i] = track.getMaxValue().getType().ordinal(); + maxValues[i] = track.getMaxValue().getValue(); + } + } + YogaNative.jni_YGNodeStyleSetGridTemplateRowsJNI( + mNativePointer, types, values, minTypes, minValues, maxTypes, maxValues); + } + + @Override + public void setGridAutoColumns(YogaGridTrackList trackList) { + int[] types = new int[trackList.size()]; + float[] values = new float[trackList.size()]; + int[] minTypes = new int[trackList.size()]; + float[] minValues = new float[trackList.size()]; + int[] maxTypes = new int[trackList.size()]; + float[] maxValues = new float[trackList.size()]; + + for (int i = 0; i < trackList.size(); i++) { + YogaGridTrackValue track = trackList.get(i); + types[i] = track.getType().ordinal(); + values[i] = track.getValue(); + if (track.getType() == YogaGridTrackValue.Type.MINMAX) { + minTypes[i] = track.getMinValue().getType().ordinal(); + minValues[i] = track.getMinValue().getValue(); + maxTypes[i] = track.getMaxValue().getType().ordinal(); + maxValues[i] = track.getMaxValue().getValue(); + } + } + YogaNative.jni_YGNodeStyleSetGridAutoColumnsJNI( + mNativePointer, types, values, minTypes, minValues, maxTypes, maxValues); + } + + @Override + public void setGridAutoRows(YogaGridTrackList trackList) { + int[] types = new int[trackList.size()]; + float[] values = new float[trackList.size()]; + int[] minTypes = new int[trackList.size()]; + float[] minValues = new float[trackList.size()]; + int[] maxTypes = new int[trackList.size()]; + float[] maxValues = new float[trackList.size()]; + + for (int i = 0; i < trackList.size(); i++) { + YogaGridTrackValue track = trackList.get(i); + types[i] = track.getType().ordinal(); + values[i] = track.getValue(); + if (track.getType() == YogaGridTrackValue.Type.MINMAX) { + minTypes[i] = track.getMinValue().getType().ordinal(); + minValues[i] = track.getMinValue().getValue(); + maxTypes[i] = track.getMaxValue().getType().ordinal(); + maxValues[i] = track.getMaxValue().getValue(); + } + } + YogaNative.jni_YGNodeStyleSetGridAutoRowsJNI( + mNativePointer, types, values, minTypes, minValues, maxTypes, maxValues); + } + + @Override + public void setGridColumnStart(int value) { + YogaNative.jni_YGNodeStyleSetGridColumnStartJNI(mNativePointer, value); + } + + @Override + public void setGridColumnStartSpan(int span) { + YogaNative.jni_YGNodeStyleSetGridColumnStartSpanJNI(mNativePointer, span); + } + + @Override + public void setGridColumnEnd(int value) { + YogaNative.jni_YGNodeStyleSetGridColumnEndJNI(mNativePointer, value); + } + + @Override + public void setGridColumnEndSpan(int span) { + YogaNative.jni_YGNodeStyleSetGridColumnEndSpanJNI(mNativePointer, span); + } + + @Override + public void setGridRowStart(int value) { + YogaNative.jni_YGNodeStyleSetGridRowStartJNI(mNativePointer, value); + } + + @Override + public void setGridRowStartSpan(int span) { + YogaNative.jni_YGNodeStyleSetGridRowStartSpanJNI(mNativePointer, span); + } + + @Override + public void setGridRowEnd(int value) { + YogaNative.jni_YGNodeStyleSetGridRowEndJNI(mNativePointer, value); + } + + @Override + public void setGridRowEndSpan(int span) { + YogaNative.jni_YGNodeStyleSetGridRowEndSpanJNI(mNativePointer, span); + } } diff --git a/java/jni/YGJNIVanilla.cpp b/java/jni/YGJNIVanilla.cpp index c5fdd8c791..8aeed52d2e 100644 --- a/java/jni/YGJNIVanilla.cpp +++ b/java/jni/YGJNIVanilla.cpp @@ -6,6 +6,7 @@ */ #include "YGJNIVanilla.h" +#include #include #include #include @@ -762,6 +763,277 @@ static void jni_YGNodeStyleSetGapPercentJNI( // Yoga specific properties, not compatible with flexbox specification YG_NODE_JNI_STYLE_PROP(jfloat, float, AspectRatio); +enum GridTrackType { + GRID_TRACK_AUTO = 0, + GRID_TRACK_POINTS = 1, + GRID_TRACK_PERCENT = 2, + GRID_TRACK_FR = 3, + GRID_TRACK_MINMAX = 4 +}; + +static YGGridTrackValueRef createTrackValue(int type, float value) { + switch (type) { + case GRID_TRACK_AUTO: + return YGAuto(); + case GRID_TRACK_POINTS: + return YGPoints(value); + case GRID_TRACK_PERCENT: + return YGPercent(value); + case GRID_TRACK_FR: + return YGFr(value); + default: + return YGAuto(); + } +} + +static void jni_YGNodeStyleSetGridTemplateColumnsJNI( + JNIEnv* env, + jobject /*obj*/, + jlong nativePointer, + jintArray types, + jfloatArray values, + jintArray minTypes, + jfloatArray minValues, + jintArray maxTypes, + jfloatArray maxValues) { + YGNodeRef node = _jlong2YGNodeRef(nativePointer); + YGGridTrackListRef trackList = YGGridTrackListCreate(); + + jint* typesArr = env->GetIntArrayElements(types, nullptr); + jfloat* valuesArr = env->GetFloatArrayElements(values, nullptr); + jint* minTypesArr = env->GetIntArrayElements(minTypes, nullptr); + jfloat* minValuesArr = env->GetFloatArrayElements(minValues, nullptr); + jint* maxTypesArr = env->GetIntArrayElements(maxTypes, nullptr); + jfloat* maxValuesArr = env->GetFloatArrayElements(maxValues, nullptr); + jsize length = env->GetArrayLength(types); + + for (jsize i = 0; i < length; i++) { + YGGridTrackValueRef trackValue; + if (typesArr[i] == GRID_TRACK_MINMAX) { + YGGridTrackValueRef minVal = + createTrackValue(minTypesArr[i], minValuesArr[i]); + YGGridTrackValueRef maxVal = + createTrackValue(maxTypesArr[i], maxValuesArr[i]); + trackValue = YGMinMax(minVal, maxVal); + } else { + trackValue = createTrackValue(typesArr[i], valuesArr[i]); + } + YGGridTrackListAddTrack(trackList, trackValue); + } + + env->ReleaseIntArrayElements(types, typesArr, JNI_ABORT); + env->ReleaseFloatArrayElements(values, valuesArr, JNI_ABORT); + env->ReleaseIntArrayElements(minTypes, minTypesArr, JNI_ABORT); + env->ReleaseFloatArrayElements(minValues, minValuesArr, JNI_ABORT); + env->ReleaseIntArrayElements(maxTypes, maxTypesArr, JNI_ABORT); + env->ReleaseFloatArrayElements(maxValues, maxValuesArr, JNI_ABORT); + + YGNodeStyleSetGridTemplateColumns(node, trackList); + YGGridTrackListFree(trackList); +} + +static void jni_YGNodeStyleSetGridTemplateRowsJNI( + JNIEnv* env, + jobject /*obj*/, + jlong nativePointer, + jintArray types, + jfloatArray values, + jintArray minTypes, + jfloatArray minValues, + jintArray maxTypes, + jfloatArray maxValues) { + YGNodeRef node = _jlong2YGNodeRef(nativePointer); + YGGridTrackListRef trackList = YGGridTrackListCreate(); + + jint* typesArr = env->GetIntArrayElements(types, nullptr); + jfloat* valuesArr = env->GetFloatArrayElements(values, nullptr); + jint* minTypesArr = env->GetIntArrayElements(minTypes, nullptr); + jfloat* minValuesArr = env->GetFloatArrayElements(minValues, nullptr); + jint* maxTypesArr = env->GetIntArrayElements(maxTypes, nullptr); + jfloat* maxValuesArr = env->GetFloatArrayElements(maxValues, nullptr); + jsize length = env->GetArrayLength(types); + + for (jsize i = 0; i < length; i++) { + YGGridTrackValueRef trackValue; + if (typesArr[i] == GRID_TRACK_MINMAX) { + YGGridTrackValueRef minVal = + createTrackValue(minTypesArr[i], minValuesArr[i]); + YGGridTrackValueRef maxVal = + createTrackValue(maxTypesArr[i], maxValuesArr[i]); + trackValue = YGMinMax(minVal, maxVal); + } else { + trackValue = createTrackValue(typesArr[i], valuesArr[i]); + } + YGGridTrackListAddTrack(trackList, trackValue); + } + + env->ReleaseIntArrayElements(types, typesArr, JNI_ABORT); + env->ReleaseFloatArrayElements(values, valuesArr, JNI_ABORT); + env->ReleaseIntArrayElements(minTypes, minTypesArr, JNI_ABORT); + env->ReleaseFloatArrayElements(minValues, minValuesArr, JNI_ABORT); + env->ReleaseIntArrayElements(maxTypes, maxTypesArr, JNI_ABORT); + env->ReleaseFloatArrayElements(maxValues, maxValuesArr, JNI_ABORT); + + YGNodeStyleSetGridTemplateRows(node, trackList); + YGGridTrackListFree(trackList); +} + +static void jni_YGNodeStyleSetGridAutoColumnsJNI( + JNIEnv* env, + jobject /*obj*/, + jlong nativePointer, + jintArray types, + jfloatArray values, + jintArray minTypes, + jfloatArray minValues, + jintArray maxTypes, + jfloatArray maxValues) { + YGNodeRef node = _jlong2YGNodeRef(nativePointer); + YGGridTrackListRef trackList = YGGridTrackListCreate(); + + jint* typesArr = env->GetIntArrayElements(types, nullptr); + jfloat* valuesArr = env->GetFloatArrayElements(values, nullptr); + jint* minTypesArr = env->GetIntArrayElements(minTypes, nullptr); + jfloat* minValuesArr = env->GetFloatArrayElements(minValues, nullptr); + jint* maxTypesArr = env->GetIntArrayElements(maxTypes, nullptr); + jfloat* maxValuesArr = env->GetFloatArrayElements(maxValues, nullptr); + jsize length = env->GetArrayLength(types); + + for (jsize i = 0; i < length; i++) { + YGGridTrackValueRef trackValue; + if (typesArr[i] == GRID_TRACK_MINMAX) { + YGGridTrackValueRef minVal = + createTrackValue(minTypesArr[i], minValuesArr[i]); + YGGridTrackValueRef maxVal = + createTrackValue(maxTypesArr[i], maxValuesArr[i]); + trackValue = YGMinMax(minVal, maxVal); + } else { + trackValue = createTrackValue(typesArr[i], valuesArr[i]); + } + YGGridTrackListAddTrack(trackList, trackValue); + } + + env->ReleaseIntArrayElements(types, typesArr, JNI_ABORT); + env->ReleaseFloatArrayElements(values, valuesArr, JNI_ABORT); + env->ReleaseIntArrayElements(minTypes, minTypesArr, JNI_ABORT); + env->ReleaseFloatArrayElements(minValues, minValuesArr, JNI_ABORT); + env->ReleaseIntArrayElements(maxTypes, maxTypesArr, JNI_ABORT); + env->ReleaseFloatArrayElements(maxValues, maxValuesArr, JNI_ABORT); + + YGNodeStyleSetGridAutoColumns(node, trackList); + YGGridTrackListFree(trackList); +} + +static void jni_YGNodeStyleSetGridAutoRowsJNI( + JNIEnv* env, + jobject /*obj*/, + jlong nativePointer, + jintArray types, + jfloatArray values, + jintArray minTypes, + jfloatArray minValues, + jintArray maxTypes, + jfloatArray maxValues) { + YGNodeRef node = _jlong2YGNodeRef(nativePointer); + YGGridTrackListRef trackList = YGGridTrackListCreate(); + + jint* typesArr = env->GetIntArrayElements(types, nullptr); + jfloat* valuesArr = env->GetFloatArrayElements(values, nullptr); + jint* minTypesArr = env->GetIntArrayElements(minTypes, nullptr); + jfloat* minValuesArr = env->GetFloatArrayElements(minValues, nullptr); + jint* maxTypesArr = env->GetIntArrayElements(maxTypes, nullptr); + jfloat* maxValuesArr = env->GetFloatArrayElements(maxValues, nullptr); + jsize length = env->GetArrayLength(types); + + for (jsize i = 0; i < length; i++) { + YGGridTrackValueRef trackValue; + if (typesArr[i] == GRID_TRACK_MINMAX) { + YGGridTrackValueRef minVal = + createTrackValue(minTypesArr[i], minValuesArr[i]); + YGGridTrackValueRef maxVal = + createTrackValue(maxTypesArr[i], maxValuesArr[i]); + trackValue = YGMinMax(minVal, maxVal); + } else { + trackValue = createTrackValue(typesArr[i], valuesArr[i]); + } + YGGridTrackListAddTrack(trackList, trackValue); + } + + env->ReleaseIntArrayElements(types, typesArr, JNI_ABORT); + env->ReleaseFloatArrayElements(values, valuesArr, JNI_ABORT); + env->ReleaseIntArrayElements(minTypes, minTypesArr, JNI_ABORT); + env->ReleaseFloatArrayElements(minValues, minValuesArr, JNI_ABORT); + env->ReleaseIntArrayElements(maxTypes, maxTypesArr, JNI_ABORT); + env->ReleaseFloatArrayElements(maxValues, maxValuesArr, JNI_ABORT); + + YGNodeStyleSetGridAutoRows(node, trackList); + YGGridTrackListFree(trackList); +} + +static void jni_YGNodeStyleSetGridColumnStartJNI( + JNIEnv* /*env*/, + jobject /*obj*/, + jlong nativePointer, + jint value) { + YGNodeStyleSetGridColumnStart(_jlong2YGNodeRef(nativePointer), value); +} + +static void jni_YGNodeStyleSetGridColumnStartSpanJNI( + JNIEnv* /*env*/, + jobject /*obj*/, + jlong nativePointer, + jint span) { + YGNodeStyleSetGridColumnStartSpan(_jlong2YGNodeRef(nativePointer), span); +} + +static void jni_YGNodeStyleSetGridColumnEndJNI( + JNIEnv* /*env*/, + jobject /*obj*/, + jlong nativePointer, + jint value) { + YGNodeStyleSetGridColumnEnd(_jlong2YGNodeRef(nativePointer), value); +} + +static void jni_YGNodeStyleSetGridColumnEndSpanJNI( + JNIEnv* /*env*/, + jobject /*obj*/, + jlong nativePointer, + jint span) { + YGNodeStyleSetGridColumnEndSpan(_jlong2YGNodeRef(nativePointer), span); +} + +static void jni_YGNodeStyleSetGridRowStartJNI( + JNIEnv* /*env*/, + jobject /*obj*/, + jlong nativePointer, + jint value) { + YGNodeStyleSetGridRowStart(_jlong2YGNodeRef(nativePointer), value); +} + +static void jni_YGNodeStyleSetGridRowStartSpanJNI( + JNIEnv* /*env*/, + jobject /*obj*/, + jlong nativePointer, + jint span) { + YGNodeStyleSetGridRowStartSpan(_jlong2YGNodeRef(nativePointer), span); +} + +static void jni_YGNodeStyleSetGridRowEndJNI( + JNIEnv* /*env*/, + jobject /*obj*/, + jlong nativePointer, + jint value) { + YGNodeStyleSetGridRowEnd(_jlong2YGNodeRef(nativePointer), value); +} + +static void jni_YGNodeStyleSetGridRowEndSpanJNI( + JNIEnv* /*env*/, + jobject /*obj*/, + jlong nativePointer, + jint span) { + YGNodeStyleSetGridRowEndSpan(_jlong2YGNodeRef(nativePointer), span); +} + static JNINativeMethod methods[] = { {"jni_YGConfigNewJNI", "()J", (void*)jni_YGConfigNewJNI}, {"jni_YGConfigFreeJNI", "(J)V", (void*)jni_YGConfigFreeJNI}, @@ -1070,6 +1342,42 @@ static JNINativeMethod methods[] = { "(JZ)V", (void*)jni_YGNodeSetAlwaysFormsContainingBlockJNI}, {"jni_YGNodeCloneJNI", "(J)J", (void*)jni_YGNodeCloneJNI}, + {"jni_YGNodeStyleSetGridTemplateColumnsJNI", + "(J[I[F[I[F[I[F)V", + (void*)jni_YGNodeStyleSetGridTemplateColumnsJNI}, + {"jni_YGNodeStyleSetGridTemplateRowsJNI", + "(J[I[F[I[F[I[F)V", + (void*)jni_YGNodeStyleSetGridTemplateRowsJNI}, + {"jni_YGNodeStyleSetGridAutoColumnsJNI", + "(J[I[F[I[F[I[F)V", + (void*)jni_YGNodeStyleSetGridAutoColumnsJNI}, + {"jni_YGNodeStyleSetGridAutoRowsJNI", + "(J[I[F[I[F[I[F)V", + (void*)jni_YGNodeStyleSetGridAutoRowsJNI}, + {"jni_YGNodeStyleSetGridColumnStartJNI", + "(JI)V", + (void*)jni_YGNodeStyleSetGridColumnStartJNI}, + {"jni_YGNodeStyleSetGridColumnStartSpanJNI", + "(JI)V", + (void*)jni_YGNodeStyleSetGridColumnStartSpanJNI}, + {"jni_YGNodeStyleSetGridColumnEndJNI", + "(JI)V", + (void*)jni_YGNodeStyleSetGridColumnEndJNI}, + {"jni_YGNodeStyleSetGridColumnEndSpanJNI", + "(JI)V", + (void*)jni_YGNodeStyleSetGridColumnEndSpanJNI}, + {"jni_YGNodeStyleSetGridRowStartJNI", + "(JI)V", + (void*)jni_YGNodeStyleSetGridRowStartJNI}, + {"jni_YGNodeStyleSetGridRowStartSpanJNI", + "(JI)V", + (void*)jni_YGNodeStyleSetGridRowStartSpanJNI}, + {"jni_YGNodeStyleSetGridRowEndJNI", + "(JI)V", + (void*)jni_YGNodeStyleSetGridRowEndJNI}, + {"jni_YGNodeStyleSetGridRowEndSpanJNI", + "(JI)V", + (void*)jni_YGNodeStyleSetGridRowEndSpanJNI}, }; void YGJNIVanilla::registerNatives(JNIEnv* env) { From 3a6fbab7a5fb8ff12151751a35c898458b893d5f Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Sat, 21 Feb 2026 22:11:09 -0800 Subject: [PATCH 6/7] CSS Grid 6/9: JavaScript bindings Summary: Add JavaScript/WASM bindings for CSS Grid support. Includes embind bindings, TypeScript types (YGEnums.ts), Node wrapper updates (wrapAssembly.ts). Existing generated JS tests are updated to import GridTrackType. Differential Revision: D93946255 --- javascript/CMakeLists.txt | 1 - javascript/src/Node.cpp | 109 +++++++++++++++++++++++++++++++++ javascript/src/Node.h | 16 +++++ javascript/src/embind.cpp | 16 +++++ javascript/src/wrapAssembly.ts | 22 +++++++ 5 files changed, 163 insertions(+), 1 deletion(-) diff --git a/javascript/CMakeLists.txt b/javascript/CMakeLists.txt index d3919a90a9..4aed9b8578 100644 --- a/javascript/CMakeLists.txt +++ b/javascript/CMakeLists.txt @@ -9,7 +9,6 @@ project(yoga) file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/../yoga/*.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/../yoga/**/*.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp) include_directories(..) diff --git a/javascript/src/Node.cpp b/javascript/src/Node.cpp index 42f6d82f63..d7df47854f 100644 --- a/javascript/src/Node.cpp +++ b/javascript/src/Node.cpp @@ -6,7 +6,9 @@ */ #include +#include +#include #include #include "./Config.h" @@ -124,6 +126,14 @@ void Node::setJustifyContent(int justifyContent) { YGNodeStyleSetJustifyContent(m_node, static_cast(justifyContent)); } +void Node::setJustifyItems(int justifyItems) { + YGNodeStyleSetJustifyItems(m_node, static_cast(justifyItems)); +} + +void Node::setJustifySelf(int justifySelf) { + YGNodeStyleSetJustifySelf(m_node, static_cast(justifySelf)); +} + void Node::setMargin(int edge, double margin) { YGNodeStyleSetMargin(m_node, static_cast(edge), margin); } @@ -590,3 +600,102 @@ void Node::setAlwaysFormsContainingBlock(bool alwaysFormsContainingBlock) { return YGNodeSetAlwaysFormsContainingBlock( m_node, alwaysFormsContainingBlock); } + +// Helper function to convert JS track value to YGGridTrackValueRef +static YGGridTrackValueRef convertTrackValue(emscripten::val track) { + int type = track["type"].as(); + + switch (type) { + case YGGridTrackTypeAuto: + return YGAuto(); + case YGGridTrackTypePoints: { + float value = track["value"].as(); + return YGPoints(value); + } + case YGGridTrackTypePercent: { + float value = track["value"].as(); + return YGPercent(value); + } + case YGGridTrackTypeFr: { + float value = track["value"].as(); + return YGFr(value); + } + case YGGridTrackTypeMinmax: { + YGGridTrackValueRef minVal = convertTrackValue(track["min"]); + YGGridTrackValueRef maxVal = convertTrackValue(track["max"]); + return YGMinMax(minVal, maxVal); + } + default: + return YGAuto(); + } +} + +// Helper function to build grid track list from JS array +static YGGridTrackListRef buildGridTrackList(emscripten::val tracks) { + YGGridTrackListRef trackList = YGGridTrackListCreate(); + + unsigned length = tracks["length"].as(); + for (unsigned i = 0; i < length; i++) { + emscripten::val track = tracks[i]; + YGGridTrackValueRef trackValue = convertTrackValue(track); + YGGridTrackListAddTrack(trackList, trackValue); + } + + return trackList; +} + +void Node::setGridTemplateColumns(emscripten::val tracks) { + YGGridTrackListRef trackList = buildGridTrackList(tracks); + YGNodeStyleSetGridTemplateColumns(m_node, trackList); + YGGridTrackListFree(trackList); +} + +void Node::setGridTemplateRows(emscripten::val tracks) { + YGGridTrackListRef trackList = buildGridTrackList(tracks); + YGNodeStyleSetGridTemplateRows(m_node, trackList); + YGGridTrackListFree(trackList); +} + +void Node::setGridAutoColumns(emscripten::val tracks) { + YGGridTrackListRef trackList = buildGridTrackList(tracks); + YGNodeStyleSetGridAutoColumns(m_node, trackList); + YGGridTrackListFree(trackList); +} + +void Node::setGridAutoRows(emscripten::val tracks) { + YGGridTrackListRef trackList = buildGridTrackList(tracks); + YGNodeStyleSetGridAutoRows(m_node, trackList); + YGGridTrackListFree(trackList); +} + +void Node::setGridColumnStart(int value) { + YGNodeStyleSetGridColumnStart(m_node, value); +} + +void Node::setGridColumnStartSpan(int span) { + YGNodeStyleSetGridColumnStartSpan(m_node, span); +} + +void Node::setGridColumnEnd(int value) { + YGNodeStyleSetGridColumnEnd(m_node, value); +} + +void Node::setGridColumnEndSpan(int span) { + YGNodeStyleSetGridColumnEndSpan(m_node, span); +} + +void Node::setGridRowStart(int value) { + YGNodeStyleSetGridRowStart(m_node, value); +} + +void Node::setGridRowStartSpan(int span) { + YGNodeStyleSetGridRowStartSpan(m_node, span); +} + +void Node::setGridRowEnd(int value) { + YGNodeStyleSetGridRowEnd(m_node, value); +} + +void Node::setGridRowEndSpan(int span) { + YGNodeStyleSetGridRowEndSpan(m_node, span); +} diff --git a/javascript/src/Node.h b/javascript/src/Node.h index e101b436db..44c13b19df 100644 --- a/javascript/src/Node.h +++ b/javascript/src/Node.h @@ -84,6 +84,8 @@ class Node { void setFlexDirection(int flexDirection); void setFlexWrap(int flexWrap); void setJustifyContent(int justifyContent); + void setJustifyItems(int justifyItems); + void setJustifySelf(int justifySelf); void setDirection(int direction); void setMargin(int edge, double margin); @@ -150,6 +152,20 @@ class Node { void setBoxSizing(int boxSizing); + // Grid setters + void setGridTemplateColumns(emscripten::val tracks); + void setGridTemplateRows(emscripten::val tracks); + void setGridAutoColumns(emscripten::val tracks); + void setGridAutoRows(emscripten::val tracks); + void setGridColumnStart(int value); + void setGridColumnStartSpan(int span); + void setGridColumnEnd(int value); + void setGridColumnEndSpan(int span); + void setGridRowStart(int value); + void setGridRowStartSpan(int span); + void setGridRowEnd(int value); + void setGridRowEndSpan(int span); + public: // Style getters int getPositionType(void) const; Value getPosition(int edge) const; diff --git a/javascript/src/embind.cpp b/javascript/src/embind.cpp index a2f7202961..2e53981b71 100644 --- a/javascript/src/embind.cpp +++ b/javascript/src/embind.cpp @@ -78,6 +78,8 @@ EMSCRIPTEN_BINDINGS(YOGA_LAYOUT) { .function("setFlexDirection", &Node::setFlexDirection) .function("setFlexWrap", &Node::setFlexWrap) .function("setJustifyContent", &Node::setJustifyContent) + .function("setJustifyItems", &Node::setJustifyItems) + .function("setJustifySelf", &Node::setJustifySelf) .function("setMargin", &Node::setMargin) .function("setMarginPercent", &Node::setMarginPercent) @@ -192,6 +194,20 @@ EMSCRIPTEN_BINDINGS(YOGA_LAYOUT) { .function( "setAlwaysFormsContainingBlock", &Node::setAlwaysFormsContainingBlock) + // Grid setters + .function("setGridTemplateColumns", &Node::setGridTemplateColumns) + .function("setGridTemplateRows", &Node::setGridTemplateRows) + .function("setGridAutoColumns", &Node::setGridAutoColumns) + .function("setGridAutoRows", &Node::setGridAutoRows) + .function("setGridColumnStart", &Node::setGridColumnStart) + .function("setGridColumnStartSpan", &Node::setGridColumnStartSpan) + .function("setGridColumnEnd", &Node::setGridColumnEnd) + .function("setGridColumnEndSpan", &Node::setGridColumnEndSpan) + .function("setGridRowStart", &Node::setGridRowStart) + .function("setGridRowStartSpan", &Node::setGridRowStartSpan) + .function("setGridRowEnd", &Node::setGridRowEnd) + .function("setGridRowEndSpan", &Node::setGridRowEndSpan) + .function("isReferenceBaseline", &Node::isReferenceBaseline) .function("setIsReferenceBaseline", &Node::setIsReferenceBaseline) diff --git a/javascript/src/wrapAssembly.ts b/javascript/src/wrapAssembly.ts index 9b2dc1e6ec..01fb8bf5a9 100644 --- a/javascript/src/wrapAssembly.ts +++ b/javascript/src/wrapAssembly.ts @@ -20,6 +20,7 @@ import type { Errata, ExperimentalFeature, FlexDirection, + GridTrackType, Gutter, Justify, MeasureMode, @@ -47,6 +48,13 @@ type Value = { value: number; }; +type GridTrackValue = { + type: GridTrackType; + value?: number; + min?: GridTrackValue; + max?: GridTrackValue; +}; + export type Config = { free(): void; isExperimentalFeatureEnabled(feature: ExperimentalFeature): boolean; @@ -170,6 +178,8 @@ export type Node = { setHeightPercent(height: number | undefined): void; setHeightStretch(): void; setJustifyContent(justifyContent: Justify): void; + setJustifyItems(justifyItems: Justify): void; + setJustifySelf(justifySelf: Justify): void; setGap(gutter: Gutter, gapLength: number | `${number}%` | undefined): Value; setGapPercent(gutter: Gutter, gapLength: number | undefined): Value; setMargin( @@ -258,6 +268,18 @@ export type Node = { unsetDirtiedFunc(): void; unsetMeasureFunc(): void; setAlwaysFormsContainingBlock(alwaysFormsContainingBlock: boolean): void; + setGridTemplateColumns(tracks: GridTrackValue[]): void; + setGridTemplateRows(tracks: GridTrackValue[]): void; + setGridAutoColumns(tracks: GridTrackValue[]): void; + setGridAutoRows(tracks: GridTrackValue[]): void; + setGridColumnStart(value: number): void; + setGridColumnStartSpan(span: number): void; + setGridColumnEnd(value: number): void; + setGridColumnEndSpan(span: number): void; + setGridRowStart(value: number): void; + setGridRowStartSpan(span: number): void; + setGridRowEnd(value: number): void; + setGridRowEndSpan(span: number): void; }; export type Yoga = { From 61be1f44350b90dd6c082a7b1cf61beba3331703 Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Sun, 22 Feb 2026 03:10:05 -0800 Subject: [PATCH 7/7] CSS Grid 7/9: Test generation infrastructure (#1885) Summary: Pull Request resolved: https://github.com/facebook/yoga/pull/1885 Update gentest scripts to support CSS Grid properties. Adds grid style extraction, grid property setup, display:grid handling, and grid-aware LTR/RTL fixture support to the C++, Java, and JavaScript test emitters. Differential Revision: D93946258 --- gentest/gentest-cpp.js | 295 ++++++++++++++++++ gentest/gentest-driver.ts | 46 ++- gentest/gentest-java.js | 197 ++++++++++++ gentest/gentest-javascript.js | 255 +++++++++++++++ gentest/gentest.js | 134 +++++++- .../generated/YGAbsolutePositionTest.test.ts | 3 +- .../generated/YGAlignContentTest.test.ts | 3 +- .../tests/generated/YGAlignItemsTest.test.ts | 3 +- .../tests/generated/YGAlignSelfTest.test.ts | 3 +- .../tests/generated/YGAndroidNewsFeed.test.ts | 3 +- .../tests/generated/YGAspectRatioTest.test.ts | 3 +- javascript/tests/generated/YGAutoTest.test.ts | 3 +- .../tests/generated/YGBorderTest.test.ts | 3 +- .../tests/generated/YGBoxSizingTest.test.ts | 3 +- .../tests/generated/YGDimensionTest.test.ts | 3 +- .../tests/generated/YGDisplayTest.test.ts | 3 +- .../generated/YGFlexDirectionTest.test.ts | 3 +- javascript/tests/generated/YGFlexTest.test.ts | 3 +- .../tests/generated/YGFlexWrapTest.test.ts | 3 +- javascript/tests/generated/YGGapTest.test.ts | 3 +- .../generated/YGIntrinsicSizeTest.test.ts | 3 +- .../generated/YGJustifyContentTest.test.ts | 3 +- .../tests/generated/YGMarginTest.test.ts | 3 +- .../generated/YGMinMaxDimensionTest.test.ts | 3 +- .../tests/generated/YGPaddingTest.test.ts | 3 +- .../tests/generated/YGPercentageTest.test.ts | 3 +- .../tests/generated/YGRoundingTest.test.ts | 3 +- .../generated/YGSizeOverflowTest.test.ts | 3 +- .../generated/YGStaticPositionTest.test.ts | 3 +- 29 files changed, 958 insertions(+), 41 deletions(-) diff --git a/gentest/gentest-cpp.js b/gentest/gentest-cpp.js index d063080491..78c0027f3b 100644 --- a/gentest/gentest-cpp.js +++ b/gentest/gentest-cpp.js @@ -125,6 +125,8 @@ CPPEmitter.prototype = Object.create(Emitter.prototype, { YGAlignSpaceAround: {value: 'YGAlignSpaceAround'}, YGAlignSpaceEvenly: {value: 'YGAlignSpaceEvenly'}, YGAlignBaseline: {value: 'YGAlignBaseline'}, + YGAlignStart: {value: 'YGAlignStart'}, + YGAlignEnd: {value: 'YGAlignEnd'}, YGDirectionInherit: {value: 'YGDirectionInherit'}, YGDirectionLTR: {value: 'YGDirectionLTR'}, @@ -152,6 +154,10 @@ CPPEmitter.prototype = Object.create(Emitter.prototype, { YGJustifySpaceAround: {value: 'YGJustifySpaceAround'}, YGJustifySpaceBetween: {value: 'YGJustifySpaceBetween'}, YGJustifySpaceEvenly: {value: 'YGJustifySpaceEvenly'}, + YGJustifyStretch: {value: 'YGJustifyStretch'}, + YGJustifyStart: {value: 'YGJustifyStart'}, + YGJustifyEnd: {value: 'YGJustifyEnd'}, + YGJustifyAuto: {value: 'YGJustifyAuto'}, YGOverflowHidden: {value: 'YGOverflowHidden'}, YGOverflowVisible: {value: 'YGOverflowVisible'}, @@ -179,6 +185,8 @@ CPPEmitter.prototype = Object.create(Emitter.prototype, { YGFitContent: {value: 'FitContent'}, YGStretch: {value: 'Stretch'}, + YGDisplayGrid: {value: 'YGDisplayGrid'}, + YGNodeCalculateLayout: { value: function (node, dir, _experiments) { this.push( @@ -363,6 +371,30 @@ CPPEmitter.prototype = Object.create(Emitter.prototype, { }, }, + YGNodeStyleSetJustifyItems: { + value: function (nodeName, value) { + this.push( + 'YGNodeStyleSetJustifyItems(' + + nodeName + + ', ' + + toValueCpp(value) + + ');', + ); + }, + }, + + YGNodeStyleSetJustifySelf: { + value: function (nodeName, value) { + this.push( + 'YGNodeStyleSetJustifySelf(' + + nodeName + + ', ' + + toValueCpp(value) + + ');', + ); + }, + }, + YGNodeStyleSetMargin: { value: function (nodeName, edge, value) { let valueStr = toValueCpp(value); @@ -509,4 +541,267 @@ CPPEmitter.prototype = Object.create(Emitter.prototype, { ); }, }, + + YGNodeStyleSetGridTemplateRows: { + value: function (nodeName, value) { + if (!value) { + return; + } + + const tracks = parseGridTrackList(this, value); + if (!tracks || tracks.length === 0) { + return; + } + + this.push(`auto ${nodeName}_gridTemplateRows = YGGridTrackListCreate();`); + + for (const track of tracks) { + if (track.type === 'minmax') { + const minVal = this.formatGridTrackValue(track.min); + const maxVal = this.formatGridTrackValue(track.max); + this.push( + `YGGridTrackListAddTrack(${nodeName}_gridTemplateRows, YGMinMax(${minVal}, ${maxVal}));`, + ); + } else { + const val = this.formatGridTrackValue(track); + this.push( + `YGGridTrackListAddTrack(${nodeName}_gridTemplateRows, ${val});`, + ); + } + } + + this.push( + `YGNodeStyleSetGridTemplateRows(${nodeName}, ${nodeName}_gridTemplateRows);`, + ); + this.push(`YGGridTrackListFree(${nodeName}_gridTemplateRows);`); + }, + }, + + YGNodeStyleSetGridTemplateColumns: { + value: function (nodeName, value) { + if (!value) { + return; + } + + const tracks = parseGridTrackList(this, value); + if (!tracks || tracks.length === 0) { + return; + } + + this.push( + `auto ${nodeName}_gridTemplateColumns = YGGridTrackListCreate();`, + ); + + for (const track of tracks) { + if (track.type === 'minmax') { + const minVal = this.formatGridTrackValue(track.min); + const maxVal = this.formatGridTrackValue(track.max); + this.push( + `YGGridTrackListAddTrack(${nodeName}_gridTemplateColumns, YGMinMax(${minVal}, ${maxVal}));`, + ); + } else { + const val = this.formatGridTrackValue(track); + this.push( + `YGGridTrackListAddTrack(${nodeName}_gridTemplateColumns, ${val});`, + ); + } + } + + this.push( + `YGNodeStyleSetGridTemplateColumns(${nodeName}, ${nodeName}_gridTemplateColumns);`, + ); + this.push(`YGGridTrackListFree(${nodeName}_gridTemplateColumns);`); + }, + }, + + YGNodeStyleSetGridColumnStart: { + value: function (nodeName, value) { + this.push(`YGNodeStyleSetGridColumnStart(${nodeName}, ${value});`); + }, + }, + + YGNodeStyleSetGridColumnStartSpan: { + value: function (nodeName, value) { + this.push(`YGNodeStyleSetGridColumnStartSpan(${nodeName}, ${value});`); + }, + }, + + YGNodeStyleSetGridColumnEnd: { + value: function (nodeName, value) { + this.push(`YGNodeStyleSetGridColumnEnd(${nodeName}, ${value});`); + }, + }, + + YGNodeStyleSetGridColumnEndSpan: { + value: function (nodeName, value) { + this.push(`YGNodeStyleSetGridColumnEndSpan(${nodeName}, ${value});`); + }, + }, + + YGNodeStyleSetGridRowStart: { + value: function (nodeName, value) { + this.push(`YGNodeStyleSetGridRowStart(${nodeName}, ${value});`); + }, + }, + + YGNodeStyleSetGridRowStartSpan: { + value: function (nodeName, value) { + this.push(`YGNodeStyleSetGridRowStartSpan(${nodeName}, ${value});`); + }, + }, + + YGNodeStyleSetGridRowEnd: { + value: function (nodeName, value) { + this.push(`YGNodeStyleSetGridRowEnd(${nodeName}, ${value});`); + }, + }, + + YGNodeStyleSetGridRowEndSpan: { + value: function (nodeName, value) { + this.push(`YGNodeStyleSetGridRowEndSpan(${nodeName}, ${value});`); + }, + }, + + YGNodeStyleSetGridAutoColumns: { + value: function (nodeName, value) { + if (!value) { + return; + } + + const tracks = parseGridTrackList(this, value); + if (!tracks || tracks.length === 0) { + return; + } + + this.push(`auto ${nodeName}_gridAutoColumns = YGGridTrackListCreate();`); + + for (const track of tracks) { + if (track.type === 'minmax') { + const minVal = this.formatGridTrackValue(track.min); + const maxVal = this.formatGridTrackValue(track.max); + this.push( + `YGGridTrackListAddTrack(${nodeName}_gridAutoColumns, YGMinMax(${minVal}, ${maxVal}));`, + ); + } else { + const val = this.formatGridTrackValue(track); + this.push( + `YGGridTrackListAddTrack(${nodeName}_gridAutoColumns, ${val});`, + ); + } + } + + this.push( + `YGNodeStyleSetGridAutoColumns(${nodeName}, ${nodeName}_gridAutoColumns);`, + ); + this.push(`YGGridTrackListFree(${nodeName}_gridAutoColumns);`); + }, + }, + + YGNodeStyleSetGridAutoRows: { + value: function (nodeName, value) { + if (!value) { + return; + } + + const tracks = parseGridTrackList(this, value); + if (!tracks || tracks.length === 0) { + return; + } + + this.push(`auto ${nodeName}_gridAutoRows = YGGridTrackListCreate();`); + + for (const track of tracks) { + if (track.type === 'minmax') { + const minVal = this.formatGridTrackValue(track.min); + const maxVal = this.formatGridTrackValue(track.max); + this.push( + `YGGridTrackListAddTrack(${nodeName}_gridAutoRows, YGMinMax(${minVal}, ${maxVal}));`, + ); + } else { + const val = this.formatGridTrackValue(track); + this.push( + `YGGridTrackListAddTrack(${nodeName}_gridAutoRows, ${val});`, + ); + } + } + + this.push( + `YGNodeStyleSetGridAutoRows(${nodeName}, ${nodeName}_gridAutoRows);`, + ); + this.push(`YGGridTrackListFree(${nodeName}_gridAutoRows);`); + }, + }, + + formatGridTrackValue: { + value: function (track) { + switch (track.type) { + case 'auto': + return 'YGAuto()'; + case 'points': + return `YGPoints(${toValueCpp(track.value)})`; + case 'percent': + return `YGPercent(${toValueCpp(track.value)})`; + case 'fr': + return `YGFr(${toValueCpp(track.value)})`; + default: + return 'YGAuto()'; + } + }, + }, }); + +function parseGridTrackList(e, value) { + if (!value || value === 'none') { + return null; + } + + // Parse space-separated track values + // Examples: "100px 100px 100px", "100px 100% minmax(100px, 200px) auto" + const tracks = []; + const parts = value.trim().split(/\s+/); + + let i = 0; + while (i < parts.length) { + const part = parts[i]; + + if (part.startsWith('minmax(')) { + // Handle minmax function - may span multiple parts if there are spaces + let minmaxStr = part; + while (!minmaxStr.includes(')') && i < parts.length - 1) { + i++; + minmaxStr += ' ' + parts[i]; + } + + // Extract min and max values from minmax(min, max) + const match = minmaxStr.match(/minmax\(([^,]+),\s*([^)]+)\)/); + if (match) { + const min = match[1].trim(); + const max = match[2].trim(); + tracks.push({ + type: 'minmax', + min: parseGridTrackValue(e, min), + max: parseGridTrackValue(e, max), + }); + } + } else { + // Simple track value + tracks.push(parseGridTrackValue(e, part)); + } + i++; + } + + return tracks; +} + +function parseGridTrackValue(e, value) { + if (value === 'auto') { + return {type: 'auto'}; + } else if (value.endsWith('px')) { + return {type: 'points', value: parseFloat(value)}; + } else if (value.endsWith('%')) { + return {type: 'percent', value: parseFloat(value)}; + } else if (value.endsWith('fr')) { + return {type: 'fr', value: parseFloat(value)}; + } + return {type: 'auto'}; +} diff --git a/gentest/gentest-driver.ts b/gentest/gentest-driver.ts index a0acd6e642..c74a2eb0dd 100644 --- a/gentest/gentest-driver.ts +++ b/gentest/gentest-driver.ts @@ -9,7 +9,7 @@ import * as fs from 'node:fs/promises'; import {format} from 'node:util'; -import {parse, dirname} from 'path'; +import {parse, dirname, relative} from 'path'; import * as process from 'node:process'; import {Builder, logging} from 'selenium-webdriver'; import {Options} from 'selenium-webdriver/chrome.js'; @@ -18,6 +18,7 @@ import {stdin, stdout} from 'node:process'; import minimist from 'minimist'; import readline from 'node:readline/promises'; import signedsource from 'signedsource'; +import {glob} from 'glob'; function addSignatureToSourceCode(sourceCode: string): string { const codeWithToken = sourceCode.replace( @@ -35,18 +36,23 @@ const headless = argv.h || argv.headless; const gentestDir = dirname(fileURLToPath(import.meta.url)); const yogaDir = dirname(gentestDir); +const fixturesDir = `${gentestDir}/fixtures`; -let fixtures = await fs.readdir(`${gentestDir}/fixtures`); +let fixtures: string[]; try { if (specificFixture != null) { - await fs.access(`fixtures/${specificFixture}.html`, fs.constants.F_OK); - fixtures = [specificFixture + '.html']; + const fixturePath = `${fixturesDir}/${specificFixture}.html`; + await fs.access(fixturePath, fs.constants.F_OK); + fixtures = [fixturePath]; + } else { + fixtures = await glob(`${fixturesDir}/**/*.html`); } } catch (e) { const errorMessage = e instanceof Error ? e.message : ''; console.log( `Trying to access ${specificFixture}.html threw an exception. Executing against all fixtures. ${errorMessage}`, ); + fixtures = await glob(`${fixturesDir}/**/*.html`); } const options = new Options(); @@ -65,25 +71,37 @@ const driver = await new Builder() .setChromeOptions(options) .build(); -for (const fileName of fixtures) { - const fixture = await fs.readFile( - `${gentestDir}/fixtures/${fileName}`, - 'utf8', - ); - const fileNameNoExtension = parse(fileName).name; +for (const fixturePath of fixtures) { + const fixture = await fs.readFile(fixturePath, 'utf8'); + const relativePath = relative(fixturesDir, fixturePath); + const fileNameNoExtension = parse(relativePath).name; console.log('Generate', fileNameNoExtension); // TODO: replace this with something more robust than just blindly replacing // start/end in the entire fixture const ltrFixture = fixture - .replaceAll('start', 'left') - .replaceAll('end', 'right') + // prevent replacing in grid properties and alignment properties (justify/align-*) + .replaceAll( + /(? { - map[key] = - node.style[key] || getComputedStyle(node, null).getPropertyValue(key); + // For grid template properties, only use inline styles to avoid capturing + // computed implicit grid tracks + if (gridTemplateProperties.has(key)) { + map[key] = node.style[key] || ''; + } else { + map[key] = + node.style[key] || getComputedStyle(node, null).getPropertyValue(key); + } return map; }, {}); } diff --git a/javascript/tests/generated/YGAbsolutePositionTest.test.ts b/javascript/tests/generated/YGAbsolutePositionTest.test.ts index 694086e23c..34102ab20d 100644 --- a/javascript/tests/generated/YGAbsolutePositionTest.test.ts +++ b/javascript/tests/generated/YGAbsolutePositionTest.test.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<9deb466dfc94bb340137a9e6b5d5c63d>> + * @generated SignedSource<> * generated by gentest/gentest-driver.ts from gentest/fixtures/YGAbsolutePositionTest.html */ @@ -19,6 +19,7 @@ import { Errata, ExperimentalFeature, FlexDirection, + GridTrackType, Gutter, Justify, MeasureMode, diff --git a/javascript/tests/generated/YGAlignContentTest.test.ts b/javascript/tests/generated/YGAlignContentTest.test.ts index aecef1a641..0d9a664741 100644 --- a/javascript/tests/generated/YGAlignContentTest.test.ts +++ b/javascript/tests/generated/YGAlignContentTest.test.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<82fd535509f8091cc2ea8503097520f1>> * generated by gentest/gentest-driver.ts from gentest/fixtures/YGAlignContentTest.html */ @@ -19,6 +19,7 @@ import { Errata, ExperimentalFeature, FlexDirection, + GridTrackType, Gutter, Justify, MeasureMode, diff --git a/javascript/tests/generated/YGAlignItemsTest.test.ts b/javascript/tests/generated/YGAlignItemsTest.test.ts index ae4dc63585..cdf3f9be9b 100644 --- a/javascript/tests/generated/YGAlignItemsTest.test.ts +++ b/javascript/tests/generated/YGAlignItemsTest.test.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<70b8fab25ecc3cd2ac855fc05c0ae528>> * generated by gentest/gentest-driver.ts from gentest/fixtures/YGAlignItemsTest.html */ @@ -19,6 +19,7 @@ import { Errata, ExperimentalFeature, FlexDirection, + GridTrackType, Gutter, Justify, MeasureMode, diff --git a/javascript/tests/generated/YGAlignSelfTest.test.ts b/javascript/tests/generated/YGAlignSelfTest.test.ts index 5d2e49ecc9..509366341b 100644 --- a/javascript/tests/generated/YGAlignSelfTest.test.ts +++ b/javascript/tests/generated/YGAlignSelfTest.test.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<1badc9ed5a0cb8d9a4a1b23653cfbe58>> + * @generated SignedSource<> * generated by gentest/gentest-driver.ts from gentest/fixtures/YGAlignSelfTest.html */ @@ -19,6 +19,7 @@ import { Errata, ExperimentalFeature, FlexDirection, + GridTrackType, Gutter, Justify, MeasureMode, diff --git a/javascript/tests/generated/YGAndroidNewsFeed.test.ts b/javascript/tests/generated/YGAndroidNewsFeed.test.ts index 488ed8d531..af16f244d3 100644 --- a/javascript/tests/generated/YGAndroidNewsFeed.test.ts +++ b/javascript/tests/generated/YGAndroidNewsFeed.test.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> * generated by gentest/gentest-driver.ts from gentest/fixtures/YGAndroidNewsFeed.html */ @@ -19,6 +19,7 @@ import { Errata, ExperimentalFeature, FlexDirection, + GridTrackType, Gutter, Justify, MeasureMode, diff --git a/javascript/tests/generated/YGAspectRatioTest.test.ts b/javascript/tests/generated/YGAspectRatioTest.test.ts index 69a7427809..31de4d5773 100644 --- a/javascript/tests/generated/YGAspectRatioTest.test.ts +++ b/javascript/tests/generated/YGAspectRatioTest.test.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<57064c3213fc22e0637ae5768ce6fea5>> + * @generated SignedSource<> * generated by gentest/gentest-driver.ts from gentest/fixtures/YGAspectRatioTest.html */ @@ -19,6 +19,7 @@ import { Errata, ExperimentalFeature, FlexDirection, + GridTrackType, Gutter, Justify, MeasureMode, diff --git a/javascript/tests/generated/YGAutoTest.test.ts b/javascript/tests/generated/YGAutoTest.test.ts index 5e9557dc0f..81653007fd 100644 --- a/javascript/tests/generated/YGAutoTest.test.ts +++ b/javascript/tests/generated/YGAutoTest.test.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<234cdb7f76ac586e034a5b6ca2033fa4>> + * @generated SignedSource<<77991f9b9a3b5330ab0985fcbc7dc2be>> * generated by gentest/gentest-driver.ts from gentest/fixtures/YGAutoTest.html */ @@ -19,6 +19,7 @@ import { Errata, ExperimentalFeature, FlexDirection, + GridTrackType, Gutter, Justify, MeasureMode, diff --git a/javascript/tests/generated/YGBorderTest.test.ts b/javascript/tests/generated/YGBorderTest.test.ts index ddbdd2aad1..d88d740e4f 100644 --- a/javascript/tests/generated/YGBorderTest.test.ts +++ b/javascript/tests/generated/YGBorderTest.test.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> * generated by gentest/gentest-driver.ts from gentest/fixtures/YGBorderTest.html */ @@ -19,6 +19,7 @@ import { Errata, ExperimentalFeature, FlexDirection, + GridTrackType, Gutter, Justify, MeasureMode, diff --git a/javascript/tests/generated/YGBoxSizingTest.test.ts b/javascript/tests/generated/YGBoxSizingTest.test.ts index d967870518..4f1d038021 100644 --- a/javascript/tests/generated/YGBoxSizingTest.test.ts +++ b/javascript/tests/generated/YGBoxSizingTest.test.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> * generated by gentest/gentest-driver.ts from gentest/fixtures/YGBoxSizingTest.html */ @@ -19,6 +19,7 @@ import { Errata, ExperimentalFeature, FlexDirection, + GridTrackType, Gutter, Justify, MeasureMode, diff --git a/javascript/tests/generated/YGDimensionTest.test.ts b/javascript/tests/generated/YGDimensionTest.test.ts index 39c140df0e..c6c7f1f3a9 100644 --- a/javascript/tests/generated/YGDimensionTest.test.ts +++ b/javascript/tests/generated/YGDimensionTest.test.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<719c8f3b9fea672881a47f593f4aabd0>> + * @generated SignedSource<<0269633bd4c55343906f703147336507>> * generated by gentest/gentest-driver.ts from gentest/fixtures/YGDimensionTest.html */ @@ -19,6 +19,7 @@ import { Errata, ExperimentalFeature, FlexDirection, + GridTrackType, Gutter, Justify, MeasureMode, diff --git a/javascript/tests/generated/YGDisplayTest.test.ts b/javascript/tests/generated/YGDisplayTest.test.ts index 9c6cd8d773..414d5296e3 100644 --- a/javascript/tests/generated/YGDisplayTest.test.ts +++ b/javascript/tests/generated/YGDisplayTest.test.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> * generated by gentest/gentest-driver.ts from gentest/fixtures/YGDisplayTest.html */ @@ -19,6 +19,7 @@ import { Errata, ExperimentalFeature, FlexDirection, + GridTrackType, Gutter, Justify, MeasureMode, diff --git a/javascript/tests/generated/YGFlexDirectionTest.test.ts b/javascript/tests/generated/YGFlexDirectionTest.test.ts index 2999befc5e..05208847a2 100644 --- a/javascript/tests/generated/YGFlexDirectionTest.test.ts +++ b/javascript/tests/generated/YGFlexDirectionTest.test.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<285a0c9dd4b646e7b1af77658fc41d99>> + * @generated SignedSource<> * generated by gentest/gentest-driver.ts from gentest/fixtures/YGFlexDirectionTest.html */ @@ -19,6 +19,7 @@ import { Errata, ExperimentalFeature, FlexDirection, + GridTrackType, Gutter, Justify, MeasureMode, diff --git a/javascript/tests/generated/YGFlexTest.test.ts b/javascript/tests/generated/YGFlexTest.test.ts index 6eee8e8640..d19104df11 100644 --- a/javascript/tests/generated/YGFlexTest.test.ts +++ b/javascript/tests/generated/YGFlexTest.test.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<60e19ea0b6b108cd6b0d4062c0b0316d>> + * @generated SignedSource<<38b3c3ba2b4d7f3b82504101e60ba0b5>> * generated by gentest/gentest-driver.ts from gentest/fixtures/YGFlexTest.html */ @@ -19,6 +19,7 @@ import { Errata, ExperimentalFeature, FlexDirection, + GridTrackType, Gutter, Justify, MeasureMode, diff --git a/javascript/tests/generated/YGFlexWrapTest.test.ts b/javascript/tests/generated/YGFlexWrapTest.test.ts index 28ee4cee2e..a8d9ece2c5 100644 --- a/javascript/tests/generated/YGFlexWrapTest.test.ts +++ b/javascript/tests/generated/YGFlexWrapTest.test.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<5c4e3faf7c401d359b341c41a3055313>> + * @generated SignedSource<<88720c733aee14a331371d80be8ff492>> * generated by gentest/gentest-driver.ts from gentest/fixtures/YGFlexWrapTest.html */ @@ -19,6 +19,7 @@ import { Errata, ExperimentalFeature, FlexDirection, + GridTrackType, Gutter, Justify, MeasureMode, diff --git a/javascript/tests/generated/YGGapTest.test.ts b/javascript/tests/generated/YGGapTest.test.ts index d38f592eaa..fe1238cba9 100644 --- a/javascript/tests/generated/YGGapTest.test.ts +++ b/javascript/tests/generated/YGGapTest.test.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<3fad56933a314f0a81b6ed504040ffae>> + * @generated SignedSource<> * generated by gentest/gentest-driver.ts from gentest/fixtures/YGGapTest.html */ @@ -19,6 +19,7 @@ import { Errata, ExperimentalFeature, FlexDirection, + GridTrackType, Gutter, Justify, MeasureMode, diff --git a/javascript/tests/generated/YGIntrinsicSizeTest.test.ts b/javascript/tests/generated/YGIntrinsicSizeTest.test.ts index 97f43f46ee..882ba77dc1 100644 --- a/javascript/tests/generated/YGIntrinsicSizeTest.test.ts +++ b/javascript/tests/generated/YGIntrinsicSizeTest.test.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<9e83eda6c04c1dcec0dfa49c44dd51de>> * generated by gentest/gentest-driver.ts from gentest/fixtures/YGIntrinsicSizeTest.html */ @@ -19,6 +19,7 @@ import { Errata, ExperimentalFeature, FlexDirection, + GridTrackType, Gutter, Justify, MeasureMode, diff --git a/javascript/tests/generated/YGJustifyContentTest.test.ts b/javascript/tests/generated/YGJustifyContentTest.test.ts index 63f814f1a2..3c9588de78 100644 --- a/javascript/tests/generated/YGJustifyContentTest.test.ts +++ b/javascript/tests/generated/YGJustifyContentTest.test.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> * generated by gentest/gentest-driver.ts from gentest/fixtures/YGJustifyContentTest.html */ @@ -19,6 +19,7 @@ import { Errata, ExperimentalFeature, FlexDirection, + GridTrackType, Gutter, Justify, MeasureMode, diff --git a/javascript/tests/generated/YGMarginTest.test.ts b/javascript/tests/generated/YGMarginTest.test.ts index 8046de55d0..8cce28e0e1 100644 --- a/javascript/tests/generated/YGMarginTest.test.ts +++ b/javascript/tests/generated/YGMarginTest.test.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<3a4aaa192f950239104218a9666654c1>> + * @generated SignedSource<> * generated by gentest/gentest-driver.ts from gentest/fixtures/YGMarginTest.html */ @@ -19,6 +19,7 @@ import { Errata, ExperimentalFeature, FlexDirection, + GridTrackType, Gutter, Justify, MeasureMode, diff --git a/javascript/tests/generated/YGMinMaxDimensionTest.test.ts b/javascript/tests/generated/YGMinMaxDimensionTest.test.ts index dfe3773f5e..66562577ad 100644 --- a/javascript/tests/generated/YGMinMaxDimensionTest.test.ts +++ b/javascript/tests/generated/YGMinMaxDimensionTest.test.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<1bd782301afbab34ed3c9a296c3ecaa6>> + * @generated SignedSource<<176c24534db638bb3cd82db0b622e46f>> * generated by gentest/gentest-driver.ts from gentest/fixtures/YGMinMaxDimensionTest.html */ @@ -19,6 +19,7 @@ import { Errata, ExperimentalFeature, FlexDirection, + GridTrackType, Gutter, Justify, MeasureMode, diff --git a/javascript/tests/generated/YGPaddingTest.test.ts b/javascript/tests/generated/YGPaddingTest.test.ts index 474a80821e..ba1076aed1 100644 --- a/javascript/tests/generated/YGPaddingTest.test.ts +++ b/javascript/tests/generated/YGPaddingTest.test.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<07e65137c3055db0090d46067ec69d5e>> + * @generated SignedSource<<5c511f68cad541d18b25ad5a13c4a332>> * generated by gentest/gentest-driver.ts from gentest/fixtures/YGPaddingTest.html */ @@ -19,6 +19,7 @@ import { Errata, ExperimentalFeature, FlexDirection, + GridTrackType, Gutter, Justify, MeasureMode, diff --git a/javascript/tests/generated/YGPercentageTest.test.ts b/javascript/tests/generated/YGPercentageTest.test.ts index 4f95fb28cb..222dd45933 100644 --- a/javascript/tests/generated/YGPercentageTest.test.ts +++ b/javascript/tests/generated/YGPercentageTest.test.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<091fb5c6d9004e40211bba58ac19d686>> + * @generated SignedSource<> * generated by gentest/gentest-driver.ts from gentest/fixtures/YGPercentageTest.html */ @@ -19,6 +19,7 @@ import { Errata, ExperimentalFeature, FlexDirection, + GridTrackType, Gutter, Justify, MeasureMode, diff --git a/javascript/tests/generated/YGRoundingTest.test.ts b/javascript/tests/generated/YGRoundingTest.test.ts index ca653f138c..a7125a5e91 100644 --- a/javascript/tests/generated/YGRoundingTest.test.ts +++ b/javascript/tests/generated/YGRoundingTest.test.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<8119d2703019b25513720167cb4dea4e>> + * @generated SignedSource<> * generated by gentest/gentest-driver.ts from gentest/fixtures/YGRoundingTest.html */ @@ -19,6 +19,7 @@ import { Errata, ExperimentalFeature, FlexDirection, + GridTrackType, Gutter, Justify, MeasureMode, diff --git a/javascript/tests/generated/YGSizeOverflowTest.test.ts b/javascript/tests/generated/YGSizeOverflowTest.test.ts index 325dd26db1..f143d08b20 100644 --- a/javascript/tests/generated/YGSizeOverflowTest.test.ts +++ b/javascript/tests/generated/YGSizeOverflowTest.test.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> * generated by gentest/gentest-driver.ts from gentest/fixtures/YGSizeOverflowTest.html */ @@ -19,6 +19,7 @@ import { Errata, ExperimentalFeature, FlexDirection, + GridTrackType, Gutter, Justify, MeasureMode, diff --git a/javascript/tests/generated/YGStaticPositionTest.test.ts b/javascript/tests/generated/YGStaticPositionTest.test.ts index 0c2bd37327..cee342889a 100644 --- a/javascript/tests/generated/YGStaticPositionTest.test.ts +++ b/javascript/tests/generated/YGStaticPositionTest.test.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> * generated by gentest/gentest-driver.ts from gentest/fixtures/YGStaticPositionTest.html */ @@ -19,6 +19,7 @@ import { Errata, ExperimentalFeature, FlexDirection, + GridTrackType, Gutter, Justify, MeasureMode,