From bd29b9a972d8abe90504079270d16dfbd047fdb4 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Tue, 15 Oct 2024 09:20:02 -0700 Subject: [PATCH] Add support for `display: contents` style Summary: This PR adds support for `display: contents` style by effectively skipping nodes with `display: contents` set during layout. This required changes in the logic related to children traversal - before this PR a node would be always laid out in the context of its direct parent. After this PR that assumption is no longer true - `display: contents` allows nodes to be skipped, i.e.: ```html
``` `node3` will be laid out as if it were a child of `node1`. Because of this, iterating over direct children of a node is no longer correct to achieve the correct layout. This PR introduces `LayoutableChildren::Iterator` which can traverse the subtree of a given node in a way that nodes with `display: contents` are replaced with their concrete children. A tree like this: ```mermaid flowchart TD A((A)) B((B)) C((C)) D((D)) E((E)) F((F)) G((G)) H((H)) I((I)) J((J)) A --> B A --> C B --> D B --> E C --> F D --> G F --> H G --> I H --> J style B fill:https://github.com/facebook/yoga/issues/050 style C fill:https://github.com/facebook/yoga/issues/050 style D fill:https://github.com/facebook/yoga/issues/050 style H fill:https://github.com/facebook/yoga/issues/050 style I fill:https://github.com/facebook/yoga/issues/050 ``` would be laid out as if the green nodes (ones with `display: contents`) did not exist. It also changes the logic where children were accessed by index to use the iterator instead as random access would be non-trivial to implement and it's not really necessary - the iteration was always sequential and indices were only used as boundaries. There's one place where knowledge of layoutable children is required to calculate the gap. An optimization for this is for a node to keep a counter of how many `display: contents` nodes are its children. If there are none, a short path of just returning the size of the children vector can be taken, otherwise it needs to iterate over layoutable children and count them, since the structure may be complex. One more major change this PR introduces is `cleanupContentsNodesRecursively`. Since nodes with `display: contents` would be entirely skipped during the layout pass, they would keep previous metrics, would be kept as dirty, and, in the case of nested `contents` nodes, would not be cloned, breaking `doesOwn` relation. All of this is handled in the new method which clones `contents` nodes recursively, sets empty layout, and marks them as clean and having a new layout so that it can be used on the React Native side. Relies on https://github.com/facebook/yoga/pull/1725 X-link: https://github.com/facebook/yoga/pull/1726 Differential Revision: D64404340 Pulled By: NickGerleman --- .../java/com/facebook/yoga/YogaDisplay.java | 4 +- .../ReactCommon/yoga/yoga/YGEnums.cpp | 2 + .../ReactCommon/yoga/yoga/YGEnums.h | 3 +- .../yoga/yoga/algorithm/AbsoluteLayout.cpp | 35 +- .../yoga/yoga/algorithm/Baseline.cpp | 8 +- .../yoga/yoga/algorithm/CalculateLayout.cpp | 454 ++++++++---------- .../yoga/yoga/algorithm/FlexLine.cpp | 13 +- .../yoga/yoga/algorithm/FlexLine.h | 2 +- .../yoga/yoga/algorithm/PixelGrid.cpp | 2 +- .../ReactCommon/yoga/yoga/enums/Display.h | 3 +- .../yoga/yoga/node/LayoutableChildren.h | 142 ++++++ .../ReactCommon/yoga/yoga/node/Node.cpp | 32 ++ .../ReactCommon/yoga/yoga/node/Node.h | 21 + 13 files changed, 448 insertions(+), 273 deletions(-) create mode 100644 packages/react-native/ReactCommon/yoga/yoga/node/LayoutableChildren.h diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaDisplay.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaDisplay.java index d4c4685fcb220d..4dae871936e65a 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaDisplay.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaDisplay.java @@ -11,7 +11,8 @@ public enum YogaDisplay { FLEX(0), - NONE(1); + NONE(1), + CONTENTS(2); private final int mIntValue; @@ -27,6 +28,7 @@ public static YogaDisplay fromInt(int value) { switch (value) { case 0: return FLEX; case 1: return NONE; + case 2: return CONTENTS; default: throw new IllegalArgumentException("Unknown enum value: " + value); } } diff --git a/packages/react-native/ReactCommon/yoga/yoga/YGEnums.cpp b/packages/react-native/ReactCommon/yoga/yoga/YGEnums.cpp index c0a72918d8cf9e..0588b749906473 100644 --- a/packages/react-native/ReactCommon/yoga/yoga/YGEnums.cpp +++ b/packages/react-native/ReactCommon/yoga/yoga/YGEnums.cpp @@ -71,6 +71,8 @@ const char* YGDisplayToString(const YGDisplay value) { return "flex"; case YGDisplayNone: return "none"; + case YGDisplayContents: + return "contents"; } return "unknown"; } diff --git a/packages/react-native/ReactCommon/yoga/yoga/YGEnums.h b/packages/react-native/ReactCommon/yoga/yoga/YGEnums.h index 83c368a601bcf5..d96ddb055a4d77 100644 --- a/packages/react-native/ReactCommon/yoga/yoga/YGEnums.h +++ b/packages/react-native/ReactCommon/yoga/yoga/YGEnums.h @@ -43,7 +43,8 @@ YG_ENUM_DECL( YG_ENUM_DECL( YGDisplay, YGDisplayFlex, - YGDisplayNone) + YGDisplayNone, + YGDisplayContents) YG_ENUM_DECL( YGEdge, diff --git a/packages/react-native/ReactCommon/yoga/yoga/algorithm/AbsoluteLayout.cpp b/packages/react-native/ReactCommon/yoga/yoga/algorithm/AbsoluteLayout.cpp index de4eef9df97215..ac697dac224c31 100644 --- a/packages/react-native/ReactCommon/yoga/yoga/algorithm/AbsoluteLayout.cpp +++ b/packages/react-native/ReactCommon/yoga/yoga/algorithm/AbsoluteLayout.cpp @@ -153,6 +153,39 @@ static void positionAbsoluteChildLegacy( : ((resolveChildAlignment(parent, child) == Align::FlexEnd) ^ (parent->style().flexWrap() == Wrap::WrapReverse)); + // If the child is absolutely positioned and has a + // top/left/bottom/right set, override all the previously computed + // positions to set it correctly. + const bool isChildLeadingPosDefined = + child->style().isFlexStartPositionDefined(axis, direction) && + !child->style().isFlexStartPositionAuto(axis, direction); + if (isChildLeadingPosDefined) { + child->setLayoutPosition( + child->style().computeFlexStartPosition( + axis, + direction, + isAxisRow ? containingBlockWidth : containingBlockHeight) + + containingNode->style().computeFlexStartBorder(axis, direction) + + child->style().computeFlexStartMargin( + axis, + direction, + isAxisRow ? containingBlockWidth : containingBlockHeight), + flexStartEdge(axis)); + } + + // If leading position is not defined or calculations result in Nan, + // default to border + margin + if (!isChildLeadingPosDefined || + yoga::isUndefined(child->getLayout().position(flexStartEdge(axis)))) { + child->setLayoutPosition( + containingNode->style().computeFlexStartBorder(axis, direction) + + child->style().computeFlexStartMargin( + axis, + direction, + isAxisRow ? containingBlockWidth : containingBlockHeight), + flexStartEdge(axis)); + } + if (child->style().isFlexEndPositionDefined(axis, direction) && (!child->style().isFlexStartPositionDefined(axis, direction) || child->style().isFlexStartPositionAuto(axis, direction))) { @@ -508,7 +541,7 @@ bool layoutAbsoluteDescendants( float containingNodeAvailableInnerWidth, float containingNodeAvailableInnerHeight) { bool hasNewLayout = false; - for (auto child : currentNode->getChildren()) { + for (auto child : currentNode->getLayoutChildren()) { if (child->style().display() == Display::None) { continue; } else if (child->style().positionType() == PositionType::Absolute) { diff --git a/packages/react-native/ReactCommon/yoga/yoga/algorithm/Baseline.cpp b/packages/react-native/ReactCommon/yoga/yoga/algorithm/Baseline.cpp index 61cf7dd1e7eb20..b3002012bc065c 100644 --- a/packages/react-native/ReactCommon/yoga/yoga/algorithm/Baseline.cpp +++ b/packages/react-native/ReactCommon/yoga/yoga/algorithm/Baseline.cpp @@ -32,9 +32,7 @@ float calculateBaseline(const yoga::Node* node) { } yoga::Node* baselineChild = nullptr; - const size_t childCount = node->getChildCount(); - for (size_t i = 0; i < childCount; i++) { - auto child = node->getChild(i); + for (auto child : node->getLayoutChildren()) { if (child->getLineIndex() > 0) { break; } @@ -67,9 +65,7 @@ bool isBaselineLayout(const yoga::Node* node) { if (node->style().alignItems() == Align::Baseline) { return true; } - const auto childCount = node->getChildCount(); - for (size_t i = 0; i < childCount; i++) { - auto child = node->getChild(i); + for (auto child : node->getLayoutChildren()) { if (child->style().positionType() != PositionType::Absolute && child->style().alignSelf() == Align::Baseline) { return true; diff --git a/packages/react-native/ReactCommon/yoga/yoga/algorithm/CalculateLayout.cpp b/packages/react-native/ReactCommon/yoga/yoga/algorithm/CalculateLayout.cpp index 3a98c9232cacaa..40a37bc754b0d7 100644 --- a/packages/react-native/ReactCommon/yoga/yoga/algorithm/CalculateLayout.cpp +++ b/packages/react-native/ReactCommon/yoga/yoga/algorithm/CalculateLayout.cpp @@ -476,6 +476,21 @@ static void zeroOutLayoutRecursively(yoga::Node* const node) { } } +static void cleanupContentsNodesRecursively(yoga::Node* const node) { + for (auto child : node->getChildren()) { + if (child->style().display() == Display::Contents) { + child->getLayout() = {}; + child->setLayoutDimension(0, Dimension::Width); + child->setLayoutDimension(0, Dimension::Height); + child->setHasNewLayout(true); + child->setDirty(false); + child->cloneChildrenIfNeeded(); + + cleanupContentsNodesRecursively(child); + } + } +} + static float calculateAvailableInnerDimension( const yoga::Node* const node, const Direction direction, @@ -525,7 +540,7 @@ static float computeFlexBasisForChildren( const uint32_t generationCount) { float totalOuterFlexBasis = 0.0f; YGNodeRef singleFlexChild = nullptr; - const auto& children = node->getChildren(); + const auto& children = node->getLayoutChildren(); SizingMode sizingModeMainDim = isRow(mainAxis) ? widthSizingMode : heightSizingMode; // If there is only one child with flexGrow + flexShrink it means we can set @@ -981,7 +996,6 @@ static void resolveFlexibleLength( static void justifyMainAxis( yoga::Node* const node, FlexLine& flexLine, - const size_t startOfLineIndex, const FlexDirection mainAxis, const FlexDirection crossAxis, const Direction direction, @@ -1081,102 +1095,69 @@ static void justifyMainAxis( float maxAscentForCurrentLine = 0; float maxDescentForCurrentLine = 0; bool isNodeBaselineLayout = isBaselineLayout(node); - for (size_t i = startOfLineIndex; i < flexLine.endOfLineIndex; i++) { - const auto child = node->getChild(i); - const Style& childStyle = child->style(); + for (auto child : flexLine.itemsInFlow) { const LayoutResults& childLayout = child->getLayout(); - if (childStyle.display() == Display::None) { - continue; + if (child->style().flexStartMarginIsAuto(mainAxis, direction) && + flexLine.layout.remainingFreeSpace > 0.0f) { + flexLine.layout.mainDim += flexLine.layout.remainingFreeSpace / + static_cast(flexLine.numberOfAutoMargins); } - if (childStyle.positionType() == PositionType::Absolute && - child->style().isFlexStartPositionDefined(mainAxis, direction) && - !child->style().isFlexStartPositionAuto(mainAxis, direction)) { - if (performLayout) { - // In case the child is position absolute and has left/top being - // defined, we override the position to whatever the user said (and - // margin/border). - child->setLayoutPosition( - child->style().computeFlexStartPosition( - mainAxis, direction, availableInnerMainDim) + - node->style().computeFlexStartBorder(mainAxis, direction) + - child->style().computeFlexStartMargin( - mainAxis, direction, availableInnerWidth), - flexStartEdge(mainAxis)); - } - } else { - // Now that we placed the element, we need to update the variables. - // We need to do that only for relative elements. Absolute elements do not - // take part in that phase. - if (childStyle.positionType() != PositionType::Absolute) { - if (child->style().flexStartMarginIsAuto(mainAxis, direction) && - flexLine.layout.remainingFreeSpace > 0.0f) { - flexLine.layout.mainDim += flexLine.layout.remainingFreeSpace / - static_cast(flexLine.numberOfAutoMargins); - } - if (performLayout) { - child->setLayoutPosition( - childLayout.position(flexStartEdge(mainAxis)) + - flexLine.layout.mainDim, - flexStartEdge(mainAxis)); - } - - if (child != flexLine.itemsInFlow.back()) { - flexLine.layout.mainDim += betweenMainDim; - } + if (performLayout) { + child->setLayoutPosition( + childLayout.position(flexStartEdge(mainAxis)) + + flexLine.layout.mainDim, + flexStartEdge(mainAxis)); + } - if (child->style().flexEndMarginIsAuto(mainAxis, direction) && - flexLine.layout.remainingFreeSpace > 0.0f) { - flexLine.layout.mainDim += flexLine.layout.remainingFreeSpace / - static_cast(flexLine.numberOfAutoMargins); - } - bool canSkipFlex = - !performLayout && sizingModeCrossDim == SizingMode::StretchFit; - if (canSkipFlex) { - // If we skipped the flex step, then we can't rely on the measuredDims - // because they weren't computed. This means we can't call - // dimensionWithMargin. - flexLine.layout.mainDim += child->style().computeMarginForAxis( - mainAxis, availableInnerWidth) + - childLayout.computedFlexBasis.unwrap(); - flexLine.layout.crossDim = availableInnerCrossDim; - } else { - // The main dimension is the sum of all the elements dimension plus - // the spacing. - flexLine.layout.mainDim += - child->dimensionWithMargin(mainAxis, availableInnerWidth); - - if (isNodeBaselineLayout) { - // If the child is baseline aligned then the cross dimension is - // calculated by adding maxAscent and maxDescent from the baseline. - const float ascent = calculateBaseline(child) + - child->style().computeFlexStartMargin( - FlexDirection::Column, direction, availableInnerWidth); - const float descent = - child->getLayout().measuredDimension(Dimension::Height) + - child->style().computeMarginForAxis( - FlexDirection::Column, availableInnerWidth) - - ascent; + if (child != flexLine.itemsInFlow.back()) { + flexLine.layout.mainDim += betweenMainDim; + } - maxAscentForCurrentLine = - yoga::maxOrDefined(maxAscentForCurrentLine, ascent); - maxDescentForCurrentLine = - yoga::maxOrDefined(maxDescentForCurrentLine, descent); - } else { - // The cross dimension is the max of the elements dimension since - // there can only be one element in that cross dimension in the case - // when the items are not baseline aligned - flexLine.layout.crossDim = yoga::maxOrDefined( - flexLine.layout.crossDim, - child->dimensionWithMargin(crossAxis, availableInnerWidth)); - } - } - } else if (performLayout) { - child->setLayoutPosition( - childLayout.position(flexStartEdge(mainAxis)) + - node->style().computeFlexStartBorder(mainAxis, direction) + - leadingMainDim, - flexStartEdge(mainAxis)); + if (child->style().flexEndMarginIsAuto(mainAxis, direction) && + flexLine.layout.remainingFreeSpace > 0.0f) { + flexLine.layout.mainDim += flexLine.layout.remainingFreeSpace / + static_cast(flexLine.numberOfAutoMargins); + } + bool canSkipFlex = + !performLayout && sizingModeCrossDim == SizingMode::StretchFit; + if (canSkipFlex) { + // If we skipped the flex step, then we can't rely on the measuredDims + // because they weren't computed. This means we can't call + // dimensionWithMargin. + flexLine.layout.mainDim += + child->style().computeMarginForAxis(mainAxis, availableInnerWidth) + + childLayout.computedFlexBasis.unwrap(); + flexLine.layout.crossDim = availableInnerCrossDim; + } else { + // The main dimension is the sum of all the elements dimension plus + // the spacing. + flexLine.layout.mainDim += + child->dimensionWithMargin(mainAxis, availableInnerWidth); + + if (isNodeBaselineLayout) { + // If the child is baseline aligned then the cross dimension is + // calculated by adding maxAscent and maxDescent from the baseline. + const float ascent = calculateBaseline(child) + + child->style().computeFlexStartMargin( + FlexDirection::Column, direction, availableInnerWidth); + const float descent = + child->getLayout().measuredDimension(Dimension::Height) + + child->style().computeMarginForAxis( + FlexDirection::Column, availableInnerWidth) - + ascent; + + maxAscentForCurrentLine = + yoga::maxOrDefined(maxAscentForCurrentLine, ascent); + maxDescentForCurrentLine = + yoga::maxOrDefined(maxDescentForCurrentLine, descent); + } else { + // The cross dimension is the max of the elements dimension since + // there can only be one element in that cross dimension in the case + // when the items are not baseline aligned + flexLine.layout.crossDim = yoga::maxOrDefined( + flexLine.layout.crossDim, + child->dimensionWithMargin(crossAxis, availableInnerWidth)); } } } @@ -1350,7 +1331,7 @@ static void calculateLayoutImpl( return; } - const auto childCount = node->getChildCount(); + const auto childCount = node->getLayoutChildCount(); if (childCount == 0) { measureNodeWithoutChildren( node, @@ -1385,6 +1366,9 @@ static void calculateLayoutImpl( // Reset layout flags, as they could have changed. node->setLayoutHadOverflow(false); + // Clean and update all display: contents nodes with a direct path to the + // current node as they will not be traversed + cleanupContentsNodesRecursively(node); // STEP 1: CALCULATE VALUES FOR REMAINDER OF ALGORITHM const FlexDirection mainAxis = resolveDirection(node->style().flexDirection(), direction); @@ -1470,9 +1454,9 @@ static void calculateLayoutImpl( } // STEP 4: COLLECT FLEX ITEMS INTO FLEX LINES - // Indexes of children that represent the first and last items in the line. - size_t startOfLineIndex = 0; - size_t endOfLineIndex = 0; + // Iterator representing the beginning of the current line + Node::LayoutableChildren::Iterator startOfLineIterator = + node->getLayoutChildren().begin(); // Number of lines. size_t lineCount = 0; @@ -1485,8 +1469,7 @@ static void calculateLayoutImpl( // Max main dimension of all the lines. float maxLineMainDim = 0; - for (; endOfLineIndex < childCount; - lineCount++, startOfLineIndex = endOfLineIndex) { + for (; startOfLineIterator != node->getLayoutChildren().end(); lineCount++) { auto flexLine = calculateFlexLine( node, ownerDirection, @@ -1494,11 +1477,9 @@ static void calculateLayoutImpl( mainAxisOwnerSize, availableInnerWidth, availableInnerMainDim, - startOfLineIndex, + startOfLineIterator, lineCount); - endOfLineIndex = flexLine.endOfLineIndex; - // If we don't need to measure the cross axis, we can skip the entire flex // step. const bool canSkipFlex = @@ -1616,7 +1597,6 @@ static void calculateLayoutImpl( justifyMainAxis( node, flexLine, - startOfLineIndex, mainAxis, crossAxis, direction, @@ -1668,151 +1648,116 @@ static void calculateLayoutImpl( // STEP 7: CROSS-AXIS ALIGNMENT // We can skip child alignment if we're just measuring the container. if (performLayout) { - for (size_t i = startOfLineIndex; i < endOfLineIndex; i++) { - const auto child = node->getChild(i); - if (child->style().display() == Display::None) { - continue; - } - if (child->style().positionType() == PositionType::Absolute) { - // If the child is absolutely positioned and has a - // top/left/bottom/right set, override all the previously computed - // positions to set it correctly. - const bool isChildLeadingPosDefined = - child->style().isFlexStartPositionDefined(crossAxis, direction) && - !child->style().isFlexStartPositionAuto(crossAxis, direction); - if (isChildLeadingPosDefined) { - child->setLayoutPosition( - child->style().computeFlexStartPosition( - crossAxis, direction, availableInnerCrossDim) + - node->style().computeFlexStartBorder(crossAxis, direction) + - child->style().computeFlexStartMargin( - crossAxis, direction, availableInnerWidth), - flexStartEdge(crossAxis)); - } - // If leading position is not defined or calculations result in Nan, - // default to border + margin - if (!isChildLeadingPosDefined || - yoga::isUndefined( - child->getLayout().position(flexStartEdge(crossAxis)))) { - child->setLayoutPosition( - node->style().computeFlexStartBorder(crossAxis, direction) + - child->style().computeFlexStartMargin( - crossAxis, direction, availableInnerWidth), - flexStartEdge(crossAxis)); + for (auto child : flexLine.itemsInFlow) { + float leadingCrossDim = leadingPaddingAndBorderCross; + + // For a relative children, we're either using alignItems (owner) or + // alignSelf (child) in order to determine the position in the cross + // axis + const Align alignItem = resolveChildAlignment(node, child); + + // If the child uses align stretch, we need to lay it out one more + // time, this time forcing the cross-axis size to be the computed + // cross size for the current line. + if (alignItem == Align::Stretch && + !child->style().flexStartMarginIsAuto(crossAxis, direction) && + !child->style().flexEndMarginIsAuto(crossAxis, direction)) { + // If the child defines a definite size for its cross axis, there's + // no need to stretch. + if (!child->hasDefiniteLength( + dimension(crossAxis), availableInnerCrossDim)) { + float childMainSize = + child->getLayout().measuredDimension(dimension(mainAxis)); + const auto& childStyle = child->style(); + float childCrossSize = childStyle.aspectRatio().isDefined() + ? child->style().computeMarginForAxis( + crossAxis, availableInnerWidth) + + (isMainAxisRow + ? childMainSize / childStyle.aspectRatio().unwrap() + : childMainSize * childStyle.aspectRatio().unwrap()) + : flexLine.layout.crossDim; + + childMainSize += child->style().computeMarginForAxis( + mainAxis, availableInnerWidth); + + SizingMode childMainSizingMode = SizingMode::StretchFit; + SizingMode childCrossSizingMode = SizingMode::StretchFit; + constrainMaxSizeForMode( + child, + direction, + mainAxis, + availableInnerMainDim, + availableInnerWidth, + &childMainSizingMode, + &childMainSize); + constrainMaxSizeForMode( + child, + direction, + crossAxis, + availableInnerCrossDim, + availableInnerWidth, + &childCrossSizingMode, + &childCrossSize); + + const float childWidth = + isMainAxisRow ? childMainSize : childCrossSize; + const float childHeight = + !isMainAxisRow ? childMainSize : childCrossSize; + + auto alignContent = node->style().alignContent(); + auto crossAxisDoesNotGrow = + alignContent != Align::Stretch && isNodeFlexWrap; + const SizingMode childWidthSizingMode = + yoga::isUndefined(childWidth) || + (!isMainAxisRow && crossAxisDoesNotGrow) + ? SizingMode::MaxContent + : SizingMode::StretchFit; + const SizingMode childHeightSizingMode = + yoga::isUndefined(childHeight) || + (isMainAxisRow && crossAxisDoesNotGrow) + ? SizingMode::MaxContent + : SizingMode::StretchFit; + + calculateLayoutInternal( + child, + childWidth, + childHeight, + direction, + childWidthSizingMode, + childHeightSizingMode, + availableInnerWidth, + availableInnerHeight, + true, + LayoutPassReason::kStretch, + layoutMarkerData, + depth, + generationCount); } } else { - float leadingCrossDim = leadingPaddingAndBorderCross; - - // For a relative children, we're either using alignItems (owner) or - // alignSelf (child) in order to determine the position in the cross - // axis - const Align alignItem = resolveChildAlignment(node, child); - - // If the child uses align stretch, we need to lay it out one more - // time, this time forcing the cross-axis size to be the computed - // cross size for the current line. - if (alignItem == Align::Stretch && - !child->style().flexStartMarginIsAuto(crossAxis, direction) && - !child->style().flexEndMarginIsAuto(crossAxis, direction)) { - // If the child defines a definite size for its cross axis, there's - // no need to stretch. - if (!child->hasDefiniteLength( - dimension(crossAxis), availableInnerCrossDim)) { - float childMainSize = - child->getLayout().measuredDimension(dimension(mainAxis)); - const auto& childStyle = child->style(); - float childCrossSize = childStyle.aspectRatio().isDefined() - ? child->style().computeMarginForAxis( - crossAxis, availableInnerWidth) + - (isMainAxisRow - ? childMainSize / childStyle.aspectRatio().unwrap() - : childMainSize * childStyle.aspectRatio().unwrap()) - : flexLine.layout.crossDim; - - childMainSize += child->style().computeMarginForAxis( - mainAxis, availableInnerWidth); - - SizingMode childMainSizingMode = SizingMode::StretchFit; - SizingMode childCrossSizingMode = SizingMode::StretchFit; - constrainMaxSizeForMode( - child, - direction, - mainAxis, - availableInnerMainDim, - availableInnerWidth, - &childMainSizingMode, - &childMainSize); - constrainMaxSizeForMode( - child, - direction, - crossAxis, - availableInnerCrossDim, - availableInnerWidth, - &childCrossSizingMode, - &childCrossSize); - - const float childWidth = - isMainAxisRow ? childMainSize : childCrossSize; - const float childHeight = - !isMainAxisRow ? childMainSize : childCrossSize; - - auto alignContent = node->style().alignContent(); - auto crossAxisDoesNotGrow = - alignContent != Align::Stretch && isNodeFlexWrap; - const SizingMode childWidthSizingMode = - yoga::isUndefined(childWidth) || - (!isMainAxisRow && crossAxisDoesNotGrow) - ? SizingMode::MaxContent - : SizingMode::StretchFit; - const SizingMode childHeightSizingMode = - yoga::isUndefined(childHeight) || - (isMainAxisRow && crossAxisDoesNotGrow) - ? SizingMode::MaxContent - : SizingMode::StretchFit; - - calculateLayoutInternal( - child, - childWidth, - childHeight, - direction, - childWidthSizingMode, - childHeightSizingMode, - availableInnerWidth, - availableInnerHeight, - true, - LayoutPassReason::kStretch, - layoutMarkerData, - depth, - generationCount); - } + const float remainingCrossDim = containerCrossAxis - + child->dimensionWithMargin(crossAxis, availableInnerWidth); + + if (child->style().flexStartMarginIsAuto(crossAxis, direction) && + child->style().flexEndMarginIsAuto(crossAxis, direction)) { + leadingCrossDim += yoga::maxOrDefined(0.0f, remainingCrossDim / 2); + } else if (child->style().flexEndMarginIsAuto(crossAxis, direction)) { + // No-Op + } else if (child->style().flexStartMarginIsAuto( + crossAxis, direction)) { + leadingCrossDim += yoga::maxOrDefined(0.0f, remainingCrossDim); + } else if (alignItem == Align::FlexStart) { + // No-Op + } else if (alignItem == Align::Center) { + leadingCrossDim += remainingCrossDim / 2; } else { - const float remainingCrossDim = containerCrossAxis - - child->dimensionWithMargin(crossAxis, availableInnerWidth); - - if (child->style().flexStartMarginIsAuto(crossAxis, direction) && - child->style().flexEndMarginIsAuto(crossAxis, direction)) { - leadingCrossDim += - yoga::maxOrDefined(0.0f, remainingCrossDim / 2); - } else if (child->style().flexEndMarginIsAuto( - crossAxis, direction)) { - // No-Op - } else if (child->style().flexStartMarginIsAuto( - crossAxis, direction)) { - leadingCrossDim += yoga::maxOrDefined(0.0f, remainingCrossDim); - } else if (alignItem == Align::FlexStart) { - // No-Op - } else if (alignItem == Align::Center) { - leadingCrossDim += remainingCrossDim / 2; - } else { - leadingCrossDim += remainingCrossDim; - } + leadingCrossDim += remainingCrossDim; } - // And we apply the position - child->setLayoutPosition( - child->getLayout().position(flexStartEdge(crossAxis)) + - totalLineCrossDim + leadingCrossDim, - flexStartEdge(crossAxis)); } + // And we apply the position + child->setLayoutPosition( + child->getLayout().position(flexStartEdge(crossAxis)) + + totalLineCrossDim + leadingCrossDim, + flexStartEdge(crossAxis)); } } @@ -1886,17 +1831,18 @@ static void calculateLayoutImpl( case Align::Baseline: break; } - size_t endIndex = 0; + Node::LayoutableChildren::Iterator endIterator = + node->getLayoutChildren().begin(); for (size_t i = 0; i < lineCount; i++) { - const size_t startIndex = endIndex; - size_t ii = startIndex; + const Node::LayoutableChildren::Iterator startIterator = endIterator; + auto iterator = startIterator; // compute the line's height and find the endIndex float lineHeight = 0; float maxAscentForCurrentLine = 0; float maxDescentForCurrentLine = 0; - for (; ii < childCount; ii++) { - const auto child = node->getChild(ii); + for (; iterator != node->getLayoutChildren().end(); iterator++) { + const auto child = *iterator; if (child->style().display() == Display::None) { continue; } @@ -1929,11 +1875,11 @@ static void calculateLayoutImpl( } } } - endIndex = ii; + endIterator = iterator; currentLead += i != 0 ? crossAxisGap : 0; - for (ii = startIndex; ii < endIndex; ii++) { - const auto child = node->getChild(ii); + for (iterator = startIterator; iterator != endIterator; iterator++) { + const auto child = *iterator; if (child->style().display() == Display::None) { continue; } @@ -2136,8 +2082,7 @@ static void calculateLayoutImpl( // As we only wrapped in normal direction yet, we need to reverse the // positions on wrap-reverse. if (performLayout && node->style().flexWrap() == Wrap::WrapReverse) { - for (size_t i = 0; i < childCount; i++) { - const auto child = node->getChild(i); + for (auto child : node->getLayoutChildren()) { if (child->style().positionType() != PositionType::Absolute) { child->setLayoutPosition( node->getLayout().measuredDimension(dimension(crossAxis)) - @@ -2154,8 +2099,7 @@ static void calculateLayoutImpl( const bool needsCrossTrailingPos = needsTrailingPosition(crossAxis); if (needsMainTrailingPos || needsCrossTrailingPos) { - for (size_t i = 0; i < childCount; i++) { - const auto child = node->getChild(i); + for (auto child : node->getLayoutChildren()) { // Absolute children will be handled by their containing block since we // cannot guarantee that their positions are set when their parents are // done with layout. diff --git a/packages/react-native/ReactCommon/yoga/yoga/algorithm/FlexLine.cpp b/packages/react-native/ReactCommon/yoga/yoga/algorithm/FlexLine.cpp index 1ad47fa577b53e..aac495f47b7ed8 100644 --- a/packages/react-native/ReactCommon/yoga/yoga/algorithm/FlexLine.cpp +++ b/packages/react-native/ReactCommon/yoga/yoga/algorithm/FlexLine.cpp @@ -20,17 +20,17 @@ FlexLine calculateFlexLine( const float mainAxisownerSize, const float availableInnerWidth, const float availableInnerMainDim, - const size_t startOfLineIndex, + Node::LayoutableChildren::Iterator& iterator, const size_t lineCount) { std::vector itemsInFlow; - itemsInFlow.reserve(node->getChildren().size()); + itemsInFlow.reserve(node->getChildCount()); float sizeConsumed = 0.0f; float totalFlexGrowFactors = 0.0f; float totalFlexShrinkScaledFactors = 0.0f; size_t numberOfAutoMargins = 0; - size_t endOfLineIndex = startOfLineIndex; - size_t firstElementInLineIndex = startOfLineIndex; + size_t endOfLineIndex = iterator.index(); + size_t firstElementInLineIndex = iterator.index(); float sizeConsumedIncludingMinConstraint = 0; const Direction direction = node->resolveDirection(ownerDirection); @@ -41,8 +41,9 @@ FlexLine calculateFlexLine( node->style().computeGapForAxis(mainAxis, availableInnerMainDim); // Add items to the current line until it's full or we run out of items. - for (; endOfLineIndex < node->getChildren().size(); endOfLineIndex++) { - auto child = node->getChild(endOfLineIndex); + for (; iterator != node->getLayoutChildren().end(); + iterator++, endOfLineIndex = iterator.index()) { + auto child = *iterator; if (child->style().display() == Display::None || child->style().positionType() == PositionType::Absolute) { if (firstElementInLineIndex == endOfLineIndex) { diff --git a/packages/react-native/ReactCommon/yoga/yoga/algorithm/FlexLine.h b/packages/react-native/ReactCommon/yoga/yoga/algorithm/FlexLine.h index 14141794a467a6..4dcf7a509d0e24 100644 --- a/packages/react-native/ReactCommon/yoga/yoga/algorithm/FlexLine.h +++ b/packages/react-native/ReactCommon/yoga/yoga/algorithm/FlexLine.h @@ -72,7 +72,7 @@ FlexLine calculateFlexLine( float mainAxisownerSize, float availableInnerWidth, float availableInnerMainDim, - size_t startOfLineIndex, + Node::LayoutableChildren::Iterator& iterator, size_t lineCount); } // namespace facebook::yoga diff --git a/packages/react-native/ReactCommon/yoga/yoga/algorithm/PixelGrid.cpp b/packages/react-native/ReactCommon/yoga/yoga/algorithm/PixelGrid.cpp index 7a694565e9b44e..038994d70e2b08 100644 --- a/packages/react-native/ReactCommon/yoga/yoga/algorithm/PixelGrid.cpp +++ b/packages/react-native/ReactCommon/yoga/yoga/algorithm/PixelGrid.cpp @@ -124,7 +124,7 @@ void roundLayoutResultsToPixelGrid( Dimension::Height); } - for (yoga::Node* child : node->getChildren()) { + for (yoga::Node* child : node->getLayoutChildren()) { roundLayoutResultsToPixelGrid(child, absoluteNodeLeft, absoluteNodeTop); } } diff --git a/packages/react-native/ReactCommon/yoga/yoga/enums/Display.h b/packages/react-native/ReactCommon/yoga/yoga/enums/Display.h index 418ebc1dcd528c..9bf23c0ac7bd1e 100644 --- a/packages/react-native/ReactCommon/yoga/yoga/enums/Display.h +++ b/packages/react-native/ReactCommon/yoga/yoga/enums/Display.h @@ -18,11 +18,12 @@ namespace facebook::yoga { enum class Display : uint8_t { Flex = YGDisplayFlex, None = YGDisplayNone, + Contents = YGDisplayContents, }; template <> constexpr int32_t ordinalCount() { - return 2; + return 3; } constexpr Display scopedEnum(YGDisplay unscoped) { diff --git a/packages/react-native/ReactCommon/yoga/yoga/node/LayoutableChildren.h b/packages/react-native/ReactCommon/yoga/yoga/node/LayoutableChildren.h new file mode 100644 index 00000000000000..c31bc05620a4a0 --- /dev/null +++ b/packages/react-native/ReactCommon/yoga/yoga/node/LayoutableChildren.h @@ -0,0 +1,142 @@ +#include +#include + +#include + +namespace facebook::yoga { + +class Node; + +template +class LayoutableChildren { + public: + using Backtrack = std::vector>; + struct Iterator { + using iterator_category = std::input_iterator_tag; + using difference_type = std::ptrdiff_t; + using value_type = T*; + using pointer = T*; + using reference = T*; + + Iterator(const T* node, size_t childIndex) + : node_(node), childIndex_(childIndex) {} + Iterator(const T* node, size_t childIndex, Backtrack&& backtrack) + : node_(node), + childIndex_(childIndex), + backtrack_(std::move(backtrack)) {} + + T* operator*() const { + return node_->getChild(childIndex_); + } + + Iterator& operator++() { + next(); + currentNodeIndex_++; + return *this; + } + + Iterator operator++(int) { + Iterator tmp = *this; + ++(*this); + return tmp; + } + + size_t index() const { + return currentNodeIndex_; + } + + friend bool operator==(const Iterator& a, const Iterator& b) { + return a.node_ == b.node_ && a.childIndex_ == b.childIndex_; + }; + + friend bool operator!=(const Iterator& a, const Iterator& b) { + return a.node_ != b.node_ || a.childIndex_ != b.childIndex_; + }; + + private: + void next() { + if (childIndex_ + 1 >= node_->getChildCount()) { + // if the current node has no more children, try to backtrack and + // visit its successor + if (backtrack_.empty()) { + // if there are no nodes to backtrack to, the last node has been + // visited + node_ = nullptr; + childIndex_ = SIZE_MAX; + } else { + // pop and restore the latest backtrack entry + const auto back = backtrack_.back(); + backtrack_.pop_back(); + node_ = back.first; + childIndex_ = back.second; + + // go to the next node + next(); + } + } else { + // current node has more children to visit, go to next + ++childIndex_; + // skip all display: contents nodes, possibly going deeper into the + // tree + skipContentsNodes(); + } + } + + void skipContentsNodes() { + // get the node that would be returned from the iterator + auto currentNode = node_->getChild(childIndex_); + while (currentNode->style().display() == Display::Contents && + currentNode->getChildCount() > 0) { + // if it has display: contents set, it shouldn't be returned but its + // children should in its place push the current node and child index + // so that the current state can be restored when backtracking + backtrack_.push_back({node_, childIndex_}); + // traverse the child + node_ = currentNode; + childIndex_ = 0; + + // repeat until a node without display: contents is found in the + // subtree or a leaf is reached + currentNode = currentNode->getChild(childIndex_); + } + + // if no node without display: contents was found, try to backtrack + if (currentNode->style().display() == Display::Contents) { + next(); + } + } + + const T* node_; + size_t childIndex_; + size_t currentNodeIndex_{0}; + Backtrack backtrack_; + + friend LayoutableChildren; + }; + + LayoutableChildren(const T* node) : node_(node) { + static_assert(std::input_iterator::Iterator>); + static_assert( + std::is_base_of::value, + "Type parameter of LayoutableChildren must derive from yoga::Node"); + } + + Iterator begin() const { + if (node_->getChildCount() > 0) { + auto result = Iterator(node_, 0); + result.skipContentsNodes(); + return result; + } else { + return Iterator(nullptr, SIZE_MAX); + } + } + + Iterator end() const { + return Iterator(nullptr, SIZE_MAX); + } + + private: + const T* node_; +}; + +} // namespace facebook::yoga diff --git a/packages/react-native/ReactCommon/yoga/yoga/node/Node.cpp b/packages/react-native/ReactCommon/yoga/yoga/node/Node.cpp index 7c6bea55e3cfe7..cf36ab09a49ea3 100644 --- a/packages/react-native/ReactCommon/yoga/yoga/node/Node.cpp +++ b/packages/react-native/ReactCommon/yoga/yoga/node/Node.cpp @@ -41,6 +41,7 @@ Node::Node(Node&& node) noexcept style_(std::move(node.style_)), layout_(node.layout_), lineIndex_(node.lineIndex_), + contentsChildrenCount_(node.contentsChildrenCount_), owner_(node.owner_), children_(std::move(node.children_)), config_(node.config_), @@ -116,14 +117,37 @@ void Node::setMeasureFunc(YGMeasureFunc measureFunc) { } void Node::replaceChild(Node* child, size_t index) { + auto previousChild = children_[index]; + if (previousChild->style().display() == Display::Contents && + child->style().display() != Display::Contents) { + contentsChildrenCount_--; + } else if ( + previousChild->style().display() != Display::Contents && + child->style().display() == Display::Contents) { + contentsChildrenCount_++; + } + children_[index] = child; } void Node::replaceChild(Node* oldChild, Node* newChild) { + if (oldChild->style().display() == Display::Contents && + newChild->style().display() != Display::Contents) { + contentsChildrenCount_--; + } else if ( + oldChild->style().display() != Display::Contents && + newChild->style().display() == Display::Contents) { + contentsChildrenCount_++; + } + std::replace(children_.begin(), children_.end(), oldChild, newChild); } void Node::insertChild(Node* child, size_t index) { + if (child->style().display() == Display::Contents) { + contentsChildrenCount_++; + } + children_.insert(children_.begin() + static_cast(index), child); } @@ -160,6 +184,10 @@ void Node::setDirty(bool isDirty) { bool Node::removeChild(Node* child) { auto p = std::find(children_.begin(), children_.end(), child); if (p != children_.end()) { + if (child->style().display() == Display::Contents) { + contentsChildrenCount_--; + } + children_.erase(p); return true; } @@ -167,6 +195,10 @@ bool Node::removeChild(Node* child) { } void Node::removeChild(size_t index) { + if (children_[index]->style().display() == Display::Contents) { + contentsChildrenCount_--; + } + children_.erase(children_.begin() + static_cast(index)); } diff --git a/packages/react-native/ReactCommon/yoga/yoga/node/Node.h b/packages/react-native/ReactCommon/yoga/yoga/node/Node.h index 34ef6d955ed8da..ddd34591eb693c 100644 --- a/packages/react-native/ReactCommon/yoga/yoga/node/Node.h +++ b/packages/react-native/ReactCommon/yoga/yoga/node/Node.h @@ -12,6 +12,7 @@ #include #include +#include #include #include @@ -31,6 +32,7 @@ namespace facebook::yoga { class YG_EXPORT Node : public ::YGNode { public: + using LayoutableChildren = yoga::LayoutableChildren; Node(); explicit Node(const Config* config); @@ -144,6 +146,24 @@ class YG_EXPORT Node : public ::YGNode { return children_.size(); } + const LayoutableChildren getLayoutChildren() const { + return LayoutableChildren(this); + } + + size_t getLayoutChildCount() const { + if (contentsChildrenCount_ == 0) { + return children_.size(); + } else { + size_t count = 0; + for (auto iter = getLayoutChildren().begin(); + iter != getLayoutChildren().end(); + iter++) { + count++; + } + return count; + } + } + const Config* getConfig() const { return config_; } @@ -298,6 +318,7 @@ class YG_EXPORT Node : public ::YGNode { Style style_; LayoutResults layout_; size_t lineIndex_ = 0; + size_t contentsChildrenCount_ = 0; Node* owner_ = nullptr; std::vector children_; const Config* config_;