Files
yoga/yoga/algorithm/AbsoluteLayout.cpp
Dawid 6d6f69bee7 Fix handling 'auto' checks in absolute layout (#1689)
Summary:
X-link: https://github.com/facebook/react-native/pull/46216

Regarding [issue](https://github.com/facebook/react-native/issues/45817) with incorrect layout when `left` is set to `auto`. This PR introduces handling `auto` whenever inline or flex position is checked to be defined and it fixes above issue.

Changelog:
[General][Fixed] - Fix handling 'auto' checks in absolute layout

## Tests:
 I have run the provided unit tests and everything passes.

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

Reviewed By: cipolleschi

Differential Revision: D61737876

Pulled By: NickGerleman

fbshipit-source-id: 531199a91c5e122b930b49725ea567cbb1d592ce
2024-08-27 06:00:34 -07:00

630 lines
24 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) {
child->setLayoutPosition(
child->style().computeFlexStartMargin(
axis, direction, containingBlockWidth) +
parent->getLayout().border(flexStartEdge(axis)) +
parent->getLayout().padding(flexStartEdge(axis)),
flexStartEdge(axis));
}
static inline void setFlexEndLayoutPosition(
const yoga::Node* const parent,
yoga::Node* child,
const Direction direction,
const FlexDirection axis,
const float containingBlockWidth) {
child->setLayoutPosition(
getPositionOfOppositeEdge(
parent->getLayout().border(flexEndEdge(axis)) +
parent->getLayout().padding(flexEndEdge(axis)) +
child->style().computeFlexEndMargin(
axis, direction, containingBlockWidth),
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) {
const float parentContentBoxSize =
parent->getLayout().measuredDimension(dimension(axis)) -
parent->getLayout().border(flexStartEdge(axis)) -
parent->getLayout().border(flexEndEdge(axis)) -
parent->getLayout().padding(flexStartEdge(axis)) -
parent->getLayout().padding(flexEndEdge(axis));
const float childOuterSize =
child->getLayout().measuredDimension(dimension(axis)) +
child->style().computeMarginForAxis(axis, containingBlockWidth);
child->setLayoutPosition(
(parentContentBoxSize - childOuterSize) / 2.0f +
parent->getLayout().border(flexStartEdge(axis)) +
parent->getLayout().padding(flexStartEdge(axis)) +
child->style().computeFlexStartMargin(
axis, direction, containingBlockWidth),
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;
}
}
// To ensure no breaking changes, we preserve the legacy way of positioning
// absolute children and determine if we should use it using an errata.
static void positionAbsoluteChildLegacy(
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 bool shouldCenter = isMainAxis
? parent->style().justifyContent() == Justify::Center
: resolveChildAlignment(parent, child) == Align::Center;
const bool shouldFlexEnd = isMainAxis
? parent->style().justifyContent() == Justify::FlexEnd
: ((resolveChildAlignment(parent, child) == Align::FlexEnd) ^
(parent->style().flexWrap() == Wrap::WrapReverse));
if (child->style().isFlexEndPositionDefined(axis, direction) &&
(!child->style().isFlexStartPositionDefined(axis, direction) ||
child->style().isFlexStartPositionAuto(axis, direction))) {
child->setLayoutPosition(
containingNode->getLayout().measuredDimension(dimension(axis)) -
child->getLayout().measuredDimension(dimension(axis)) -
containingNode->style().computeFlexEndBorder(axis, direction) -
child->style().computeFlexEndMargin(
axis,
direction,
isAxisRow ? containingBlockWidth : containingBlockHeight) -
child->style().computeFlexEndPosition(
axis,
direction,
isAxisRow ? containingBlockWidth : containingBlockHeight),
flexStartEdge(axis));
} else if (
(!child->style().isFlexStartPositionDefined(axis, direction) ||
child->style().isFlexStartPositionAuto(axis, direction)) &&
shouldCenter) {
child->setLayoutPosition(
(parent->getLayout().measuredDimension(dimension(axis)) -
child->getLayout().measuredDimension(dimension(axis))) /
2.0f,
flexStartEdge(axis));
} else if (
(!child->style().isFlexStartPositionDefined(axis, direction) ||
child->style().isFlexStartPositionAuto(axis, direction)) &&
shouldFlexEnd) {
child->setLayoutPosition(
(parent->getLayout().measuredDimension(dimension(axis)) -
child->getLayout().measuredDimension(dimension(axis))),
flexStartEdge(axis));
}
}
/*
* 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 positionAbsoluteChildImpl(
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);
}
}
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) {
child->hasErrata(Errata::AbsolutePositioningIncorrect)
? positionAbsoluteChildLegacy(
containingNode,
parent,
child,
direction,
axis,
isMainAxis,
containingBlockWidth,
containingBlockHeight)
: positionAbsoluteChildImpl(
containingNode,
parent,
child,
direction,
axis,
isMainAxis,
containingBlockWidth,
containingBlockHeight);
}
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(Dimension::Width)
.resolve(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(Dimension::Height)
.resolve(containingBlockHeight)
.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->getChildren()) {
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