Files
yoga/yoga/algorithm/AbsoluteLayout.cpp
Jakub Piasecki 68bb2343d2 Add support for display: contents style (#1726)
Summary:
X-link: https://github.com/facebook/react-native/pull/47035

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
<div id="node1">
  <div id="node2" style="display: contents;">
    <div id="node3" />
  </div>
</div>
```

`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

Changelog: [Internal]

Pull Request resolved: https://github.com/facebook/yoga/pull/1726

Test Plan: Added tests for `display: contents` based on existing tests for `display: none` and ensured that all the tests were passing.

Reviewed By: joevilches

Differential Revision: D64404340

Pulled By: NickGerleman

fbshipit-source-id: f6f6e9a6fad82873f18c8a0ead58aad897df5d09
2024-10-18 22:05:41 -07:00

564 lines
22 KiB
C++

/*
* 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 <yoga/algorithm/AbsoluteLayout.h>
#include <yoga/algorithm/Align.h>
#include <yoga/algorithm/BoundAxis.h>
#include <yoga/algorithm/CalculateLayout.h>
#include <yoga/algorithm/TrailingPosition.h>
namespace facebook::yoga {
static inline void setFlexStartLayoutPosition(
const yoga::Node* const parent,
yoga::Node* child,
const Direction direction,
const FlexDirection axis,
const float containingBlockWidth) {
float position = child->style().computeFlexStartMargin(
axis, direction, containingBlockWidth) +
parent->getLayout().border(flexStartEdge(axis));
if (!child->hasErrata(Errata::AbsolutePositionWithoutInsetsExcludesPadding)) {
position += parent->getLayout().padding(flexStartEdge(axis));
}
child->setLayoutPosition(position, flexStartEdge(axis));
}
static inline void setFlexEndLayoutPosition(
const yoga::Node* const parent,
yoga::Node* child,
const Direction direction,
const FlexDirection axis,
const float containingBlockWidth) {
float flexEndPosition = parent->getLayout().border(flexEndEdge(axis)) +
child->style().computeFlexEndMargin(
axis, direction, containingBlockWidth);
if (!child->hasErrata(Errata::AbsolutePositionWithoutInsetsExcludesPadding)) {
flexEndPosition += parent->getLayout().padding(flexEndEdge(axis));
}
child->setLayoutPosition(
getPositionOfOppositeEdge(flexEndPosition, axis, parent, child),
flexStartEdge(axis));
}
static inline void setCenterLayoutPosition(
const yoga::Node* const parent,
yoga::Node* child,
const Direction direction,
const FlexDirection axis,
const float containingBlockWidth) {
float parentContentBoxSize =
parent->getLayout().measuredDimension(dimension(axis)) -
parent->getLayout().border(flexStartEdge(axis)) -
parent->getLayout().border(flexEndEdge(axis));
if (!child->hasErrata(Errata::AbsolutePositionWithoutInsetsExcludesPadding)) {
parentContentBoxSize -= parent->getLayout().padding(flexStartEdge(axis));
parentContentBoxSize -= parent->getLayout().padding(flexEndEdge(axis));
}
const float childOuterSize =
child->getLayout().measuredDimension(dimension(axis)) +
child->style().computeMarginForAxis(axis, containingBlockWidth);
float position = (parentContentBoxSize - childOuterSize) / 2.0f +
parent->getLayout().border(flexStartEdge(axis)) +
child->style().computeFlexStartMargin(
axis, direction, containingBlockWidth);
if (!child->hasErrata(Errata::AbsolutePositionWithoutInsetsExcludesPadding)) {
position += parent->getLayout().padding(flexStartEdge(axis));
}
child->setLayoutPosition(position, flexStartEdge(axis));
}
static void justifyAbsoluteChild(
const yoga::Node* const parent,
yoga::Node* child,
const Direction direction,
const FlexDirection mainAxis,
const float containingBlockWidth) {
const Justify parentJustifyContent = parent->style().justifyContent();
switch (parentJustifyContent) {
case Justify::FlexStart:
case Justify::SpaceBetween:
setFlexStartLayoutPosition(
parent, child, direction, mainAxis, containingBlockWidth);
break;
case Justify::FlexEnd:
setFlexEndLayoutPosition(
parent, child, direction, mainAxis, containingBlockWidth);
break;
case Justify::Center:
case Justify::SpaceAround:
case Justify::SpaceEvenly:
setCenterLayoutPosition(
parent, child, direction, mainAxis, containingBlockWidth);
break;
}
}
static void alignAbsoluteChild(
const yoga::Node* const parent,
yoga::Node* child,
const Direction direction,
const FlexDirection crossAxis,
const float containingBlockWidth) {
Align itemAlign = resolveChildAlignment(parent, child);
const Wrap parentWrap = parent->style().flexWrap();
if (parentWrap == Wrap::WrapReverse) {
if (itemAlign == Align::FlexEnd) {
itemAlign = Align::FlexStart;
} else if (itemAlign != Align::Center) {
itemAlign = Align::FlexEnd;
}
}
switch (itemAlign) {
case Align::Auto:
case Align::FlexStart:
case Align::Baseline:
case Align::SpaceAround:
case Align::SpaceBetween:
case Align::Stretch:
case Align::SpaceEvenly:
setFlexStartLayoutPosition(
parent, child, direction, crossAxis, containingBlockWidth);
break;
case Align::FlexEnd:
setFlexEndLayoutPosition(
parent, child, direction, crossAxis, containingBlockWidth);
break;
case Align::Center:
setCenterLayoutPosition(
parent, child, direction, crossAxis, containingBlockWidth);
break;
}
}
/*
* Absolutely positioned nodes do not participate in flex layout and thus their
* positions can be determined independently from the rest of their siblings.
* For each axis there are essentially two cases:
*
* 1) The node has insets defined. In this case we can just use these to
* determine the position of the node.
* 2) The node does not have insets defined. In this case we look at the style
* of the parent to position the node. Things like justify content and
* align content will move absolute children around. If none of these
* special properties are defined, the child is positioned at the start
* (defined by flex direction) of the leading flex line.
*
* This function does that positioning for the given axis. The spec has more
* information on this topic: https://www.w3.org/TR/css-flexbox-1/#abspos-items
*/
static void positionAbsoluteChild(
const yoga::Node* const containingNode,
const yoga::Node* const parent,
yoga::Node* child,
const Direction direction,
const FlexDirection axis,
const bool isMainAxis,
const float containingBlockWidth,
const float containingBlockHeight) {
const bool isAxisRow = isRow(axis);
const float containingBlockSize =
isAxisRow ? containingBlockWidth : containingBlockHeight;
// The inline-start position takes priority over the end position in the case
// that they are both set and the node has a fixed width. Thus we only have 2
// cases here: if inline-start is defined and if inline-end is defined.
//
// Despite checking inline-start to honor prioritization of insets, we write
// to the flex-start edge because this algorithm works by positioning on the
// flex-start edge and then filling in the flex-end direction at the end if
// necessary.
if (child->style().isInlineStartPositionDefined(axis, direction) &&
!child->style().isInlineStartPositionAuto(axis, direction)) {
const float positionRelativeToInlineStart =
child->style().computeInlineStartPosition(
axis, direction, containingBlockSize) +
containingNode->style().computeInlineStartBorder(axis, direction) +
child->style().computeInlineStartMargin(
axis, direction, containingBlockSize);
const float positionRelativeToFlexStart =
inlineStartEdge(axis, direction) != flexStartEdge(axis)
? getPositionOfOppositeEdge(
positionRelativeToInlineStart, axis, containingNode, child)
: positionRelativeToInlineStart;
child->setLayoutPosition(positionRelativeToFlexStart, flexStartEdge(axis));
} else if (
child->style().isInlineEndPositionDefined(axis, direction) &&
!child->style().isInlineEndPositionAuto(axis, direction)) {
const float positionRelativeToInlineStart =
containingNode->getLayout().measuredDimension(dimension(axis)) -
child->getLayout().measuredDimension(dimension(axis)) -
containingNode->style().computeInlineEndBorder(axis, direction) -
child->style().computeInlineEndMargin(
axis, direction, containingBlockSize) -
child->style().computeInlineEndPosition(
axis, direction, containingBlockSize);
const float positionRelativeToFlexStart =
inlineStartEdge(axis, direction) != flexStartEdge(axis)
? getPositionOfOppositeEdge(
positionRelativeToInlineStart, axis, containingNode, child)
: positionRelativeToInlineStart;
child->setLayoutPosition(positionRelativeToFlexStart, flexStartEdge(axis));
} else {
isMainAxis ? justifyAbsoluteChild(
parent, child, direction, axis, containingBlockWidth)
: alignAbsoluteChild(
parent, child, direction, axis, containingBlockWidth);
}
}
void layoutAbsoluteChild(
const yoga::Node* const containingNode,
const yoga::Node* const node,
yoga::Node* const child,
const float containingBlockWidth,
const float containingBlockHeight,
const SizingMode widthMode,
const Direction direction,
LayoutData& layoutMarkerData,
const uint32_t depth,
const uint32_t generationCount) {
const FlexDirection mainAxis =
resolveDirection(node->style().flexDirection(), direction);
const FlexDirection crossAxis = resolveCrossDirection(mainAxis, direction);
const bool isMainAxisRow = isRow(mainAxis);
float childWidth = YGUndefined;
float childHeight = YGUndefined;
SizingMode childWidthSizingMode = SizingMode::MaxContent;
SizingMode childHeightSizingMode = SizingMode::MaxContent;
auto marginRow = child->style().computeMarginForAxis(
FlexDirection::Row, containingBlockWidth);
auto marginColumn = child->style().computeMarginForAxis(
FlexDirection::Column, containingBlockWidth);
if (child->hasDefiniteLength(Dimension::Width, containingBlockWidth)) {
childWidth = child
->getResolvedDimension(
direction,
Dimension::Width,
containingBlockWidth,
containingBlockWidth)
.unwrap() +
marginRow;
} else {
// If the child doesn't have a specified width, compute the width based on
// the left/right offsets if they're defined.
if (child->style().isFlexStartPositionDefined(
FlexDirection::Row, direction) &&
child->style().isFlexEndPositionDefined(
FlexDirection::Row, direction) &&
!child->style().isFlexStartPositionAuto(
FlexDirection::Row, direction) &&
!child->style().isFlexEndPositionAuto(FlexDirection::Row, direction)) {
childWidth =
containingNode->getLayout().measuredDimension(Dimension::Width) -
(containingNode->style().computeFlexStartBorder(
FlexDirection::Row, direction) +
containingNode->style().computeFlexEndBorder(
FlexDirection::Row, direction)) -
(child->style().computeFlexStartPosition(
FlexDirection::Row, direction, containingBlockWidth) +
child->style().computeFlexEndPosition(
FlexDirection::Row, direction, containingBlockWidth));
childWidth = boundAxis(
child,
FlexDirection::Row,
direction,
childWidth,
containingBlockWidth,
containingBlockWidth);
}
}
if (child->hasDefiniteLength(Dimension::Height, containingBlockHeight)) {
childHeight = child
->getResolvedDimension(
direction,
Dimension::Height,
containingBlockHeight,
containingBlockWidth)
.unwrap() +
marginColumn;
} else {
// If the child doesn't have a specified height, compute the height based
// on the top/bottom offsets if they're defined.
if (child->style().isFlexStartPositionDefined(
FlexDirection::Column, direction) &&
child->style().isFlexEndPositionDefined(
FlexDirection::Column, direction) &&
!child->style().isFlexStartPositionAuto(
FlexDirection::Column, direction) &&
!child->style().isFlexEndPositionAuto(
FlexDirection::Column, direction)) {
childHeight =
containingNode->getLayout().measuredDimension(Dimension::Height) -
(containingNode->style().computeFlexStartBorder(
FlexDirection::Column, direction) +
containingNode->style().computeFlexEndBorder(
FlexDirection::Column, direction)) -
(child->style().computeFlexStartPosition(
FlexDirection::Column, direction, containingBlockHeight) +
child->style().computeFlexEndPosition(
FlexDirection::Column, direction, containingBlockHeight));
childHeight = boundAxis(
child,
FlexDirection::Column,
direction,
childHeight,
containingBlockHeight,
containingBlockWidth);
}
}
// Exactly one dimension needs to be defined for us to be able to do aspect
// ratio calculation. One dimension being the anchor and the other being
// flexible.
const auto& childStyle = child->style();
if (yoga::isUndefined(childWidth) ^ yoga::isUndefined(childHeight)) {
if (childStyle.aspectRatio().isDefined()) {
if (yoga::isUndefined(childWidth)) {
childWidth = marginRow +
(childHeight - marginColumn) * childStyle.aspectRatio().unwrap();
} else if (yoga::isUndefined(childHeight)) {
childHeight = marginColumn +
(childWidth - marginRow) / childStyle.aspectRatio().unwrap();
}
}
}
// If we're still missing one or the other dimension, measure the content.
if (yoga::isUndefined(childWidth) || yoga::isUndefined(childHeight)) {
childWidthSizingMode = yoga::isUndefined(childWidth)
? SizingMode::MaxContent
: SizingMode::StretchFit;
childHeightSizingMode = yoga::isUndefined(childHeight)
? SizingMode::MaxContent
: SizingMode::StretchFit;
// If the size of the owner is defined then try to constrain the absolute
// child to that size as well. This allows text within the absolute child
// to wrap to the size of its owner. This is the same behavior as many
// browsers implement.
if (!isMainAxisRow && yoga::isUndefined(childWidth) &&
widthMode != SizingMode::MaxContent &&
yoga::isDefined(containingBlockWidth) && containingBlockWidth > 0) {
childWidth = containingBlockWidth;
childWidthSizingMode = SizingMode::FitContent;
}
calculateLayoutInternal(
child,
childWidth,
childHeight,
direction,
childWidthSizingMode,
childHeightSizingMode,
containingBlockWidth,
containingBlockHeight,
false,
LayoutPassReason::kAbsMeasureChild,
layoutMarkerData,
depth,
generationCount);
childWidth = child->getLayout().measuredDimension(Dimension::Width) +
child->style().computeMarginForAxis(
FlexDirection::Row, containingBlockWidth);
childHeight = child->getLayout().measuredDimension(Dimension::Height) +
child->style().computeMarginForAxis(
FlexDirection::Column, containingBlockWidth);
}
calculateLayoutInternal(
child,
childWidth,
childHeight,
direction,
SizingMode::StretchFit,
SizingMode::StretchFit,
containingBlockWidth,
containingBlockHeight,
true,
LayoutPassReason::kAbsLayout,
layoutMarkerData,
depth,
generationCount);
positionAbsoluteChild(
containingNode,
node,
child,
direction,
mainAxis,
true /*isMainAxis*/,
containingBlockWidth,
containingBlockHeight);
positionAbsoluteChild(
containingNode,
node,
child,
direction,
crossAxis,
false /*isMainAxis*/,
containingBlockWidth,
containingBlockHeight);
}
bool layoutAbsoluteDescendants(
yoga::Node* containingNode,
yoga::Node* currentNode,
SizingMode widthSizingMode,
Direction currentNodeDirection,
LayoutData& layoutMarkerData,
uint32_t currentDepth,
uint32_t generationCount,
float currentNodeLeftOffsetFromContainingBlock,
float currentNodeTopOffsetFromContainingBlock,
float containingNodeAvailableInnerWidth,
float containingNodeAvailableInnerHeight) {
bool hasNewLayout = false;
for (auto child : currentNode->getLayoutChildren()) {
if (child->style().display() == Display::None) {
continue;
} else if (child->style().positionType() == PositionType::Absolute) {
const bool absoluteErrata =
currentNode->hasErrata(Errata::AbsolutePercentAgainstInnerSize);
const float containingBlockWidth = absoluteErrata
? containingNodeAvailableInnerWidth
: containingNode->getLayout().measuredDimension(Dimension::Width) -
containingNode->style().computeBorderForAxis(FlexDirection::Row);
const float containingBlockHeight = absoluteErrata
? containingNodeAvailableInnerHeight
: containingNode->getLayout().measuredDimension(Dimension::Height) -
containingNode->style().computeBorderForAxis(
FlexDirection::Column);
layoutAbsoluteChild(
containingNode,
currentNode,
child,
containingBlockWidth,
containingBlockHeight,
widthSizingMode,
currentNodeDirection,
layoutMarkerData,
currentDepth,
generationCount);
hasNewLayout = hasNewLayout || child->getHasNewLayout();
/*
* At this point the child has its position set but only on its the
* parent's flexStart edge. Additionally, this position should be
* interpreted relative to the containing block of the child if it had
* insets defined. So we need to adjust the position by subtracting the
* the parents offset from the containing block. However, getting that
* offset is complicated since the two nodes can have different main/cross
* axes.
*/
const FlexDirection parentMainAxis = resolveDirection(
currentNode->style().flexDirection(), currentNodeDirection);
const FlexDirection parentCrossAxis =
resolveCrossDirection(parentMainAxis, currentNodeDirection);
if (needsTrailingPosition(parentMainAxis)) {
const bool mainInsetsDefined = isRow(parentMainAxis)
? child->style().horizontalInsetsDefined()
: child->style().verticalInsetsDefined();
setChildTrailingPosition(
mainInsetsDefined ? containingNode : currentNode,
child,
parentMainAxis);
}
if (needsTrailingPosition(parentCrossAxis)) {
const bool crossInsetsDefined = isRow(parentCrossAxis)
? child->style().horizontalInsetsDefined()
: child->style().verticalInsetsDefined();
setChildTrailingPosition(
crossInsetsDefined ? containingNode : currentNode,
child,
parentCrossAxis);
}
/*
* At this point we know the left and top physical edges of the child are
* set with positions that are relative to the containing block if insets
* are defined
*/
const float childLeftPosition =
child->getLayout().position(PhysicalEdge::Left);
const float childTopPosition =
child->getLayout().position(PhysicalEdge::Top);
const float childLeftOffsetFromParent =
child->style().horizontalInsetsDefined()
? (childLeftPosition - currentNodeLeftOffsetFromContainingBlock)
: childLeftPosition;
const float childTopOffsetFromParent =
child->style().verticalInsetsDefined()
? (childTopPosition - currentNodeTopOffsetFromContainingBlock)
: childTopPosition;
child->setLayoutPosition(childLeftOffsetFromParent, PhysicalEdge::Left);
child->setLayoutPosition(childTopOffsetFromParent, PhysicalEdge::Top);
} else if (
child->style().positionType() == PositionType::Static &&
!child->alwaysFormsContainingBlock()) {
// We may write new layout results for absolute descendants of "child"
// which are positioned relative to the current containing block instead
// of their parent. "child" may not be dirty, or have new constraints, so
// absolute positioning may be the first time during this layout pass that
// we need to mutate these descendents. Make sure the path of
// nodes to them is mutable before positioning.
child->cloneChildrenIfNeeded();
const Direction childDirection =
child->resolveDirection(currentNodeDirection);
// By now all descendants of the containing block that are not absolute
// will have their positions set for left and top.
const float childLeftOffsetFromContainingBlock =
currentNodeLeftOffsetFromContainingBlock +
child->getLayout().position(PhysicalEdge::Left);
const float childTopOffsetFromContainingBlock =
currentNodeTopOffsetFromContainingBlock +
child->getLayout().position(PhysicalEdge::Top);
hasNewLayout = layoutAbsoluteDescendants(
containingNode,
child,
widthSizingMode,
childDirection,
layoutMarkerData,
currentDepth + 1,
generationCount,
childLeftOffsetFromContainingBlock,
childTopOffsetFromContainingBlock,
containingNodeAvailableInnerWidth,
containingNodeAvailableInnerHeight) ||
hasNewLayout;
if (hasNewLayout) {
child->setHasNewLayout(hasNewLayout);
}
}
}
return hasNewLayout;
}
} // namespace facebook::yoga