Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions tests/EventsTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,99 @@ TEST_F(EventTest, layout_events_counts_cache_hits_multi_node_layout) {
ASSERT_EQ(layoutData.cachedMeasures, 4);
}

TEST_F(EventTest, unconstrained_fit_content_height_measure_is_reused) {
// A child measured with a FitContent height that does not constrain it
// (e.g. a row in a scroll view's content, whose height grows) keeps its
// cached measurement when only that available height changes.
auto root = YGNodeNew();
YGNodeStyleSetWidth(root, 400);
YGNodeStyleSetHeight(root, 1000);
auto container = YGNodeNew();
YGNodeInsertChild(root, container, 0);
auto content = YGNodeNew();
YGNodeStyleSetHeight(content, 50);
YGNodeInsertChild(container, content, 0);

YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR);

YGNodeStyleSetHeight(root, 1200);
YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR);

LayoutData layoutData =
lastEvent().eventTestData<Event::LayoutPassEnd>().layoutData;

ASSERT_EQ(layoutData.measures, 0);
ASSERT_EQ(YGNodeLayoutGetHeight(container), 50);

YGNodeFreeRecursive(root);
}

TEST_F(EventTest, percent_height_under_definite_height_measure_is_reused) {
// A percentage below a node with a definite height resolves against that
// height, not against the available height of the measured container.
auto root = YGNodeNew();
YGNodeStyleSetWidth(root, 400);
YGNodeStyleSetHeight(root, 1000);
auto container = YGNodeNew();
YGNodeInsertChild(root, container, 0);
auto fixedBox = YGNodeNew();
YGNodeStyleSetHeight(fixedBox, 50);
YGNodeInsertChild(container, fixedBox, 0);
auto fixedBoxChild = YGNodeNew();
YGNodeStyleSetHeightPercent(fixedBoxChild, 100);
YGNodeInsertChild(fixedBox, fixedBoxChild, 0);
auto aspectRatioBox = YGNodeNew();
YGNodeStyleSetWidthPercent(aspectRatioBox, 100);
YGNodeStyleSetAspectRatio(aspectRatioBox, 4);
YGNodeInsertChild(container, aspectRatioBox, 1);
auto aspectRatioBoxChild = YGNodeNew();
YGNodeStyleSetHeightPercent(aspectRatioBoxChild, 100);
YGNodeInsertChild(aspectRatioBox, aspectRatioBoxChild, 0);

YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR);

YGNodeStyleSetHeight(root, 1200);
YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR);

LayoutData layoutData =
lastEvent().eventTestData<Event::LayoutPassEnd>().layoutData;

ASSERT_EQ(layoutData.measures, 0);
ASSERT_EQ(YGNodeLayoutGetHeight(container), 150);

YGNodeFreeRecursive(root);
}

TEST_F(EventTest, absolute_percent_height_child_measure_is_reused) {
// An absolutely positioned child doesn't affect the container's size.
auto root = YGNodeNew();
YGNodeStyleSetWidth(root, 400);
YGNodeStyleSetHeight(root, 1000);
auto container = YGNodeNew();
YGNodeInsertChild(root, container, 0);
auto content = YGNodeNew();
YGNodeStyleSetHeight(content, 50);
YGNodeInsertChild(container, content, 0);
auto overlay = YGNodeNew();
YGNodeStyleSetPositionType(overlay, YGPositionTypeAbsolute);
YGNodeStyleSetHeightPercent(overlay, 100);
YGNodeInsertChild(container, overlay, 1);

YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR);

YGNodeStyleSetHeight(root, 1200);
YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR);

LayoutData layoutData =
lastEvent().eventTestData<Event::LayoutPassEnd>().layoutData;

ASSERT_EQ(layoutData.measures, 0);
ASSERT_EQ(YGNodeLayoutGetHeight(container), 50);
ASSERT_EQ(YGNodeLayoutGetHeight(overlay), 50);

YGNodeFreeRecursive(root);
}

TEST_F(EventTest, layout_events_has_max_measure_cache) {
auto root = YGNodeNew();
auto a = YGNodeNew();
Expand Down
63 changes: 63 additions & 0 deletions tests/YGRelayoutTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,36 @@ TEST(YogaTest, has_new_layout_flag_set_static) {
YGNodeFreeRecursive(root);
}

TEST(
YogaTest,
percent_height_in_auto_container_relayout_on_root_height_change) {
YGConfigRef config = YGConfigNew();

YGNodeRef root = YGNodeNewWithConfig(config);
YGNodeStyleSetWidth(root, 400);
YGNodeStyleSetHeight(root, 1000);

YGNodeRef container = YGNodeNewWithConfig(config);
YGNodeInsertChild(root, container, 0);

YGNodeRef child = YGNodeNewWithConfig(config);
YGNodeStyleSetHeightPercent(child, 50);
YGNodeInsertChild(container, child, 0);

YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR);
ASSERT_FLOAT_EQ(500, YGNodeLayoutGetHeight(container));
ASSERT_FLOAT_EQ(250, YGNodeLayoutGetHeight(child));

YGNodeStyleSetHeight(root, 1200);
YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR);
ASSERT_FLOAT_EQ(600, YGNodeLayoutGetHeight(container));
ASSERT_FLOAT_EQ(300, YGNodeLayoutGetHeight(child));

YGNodeFreeRecursive(root);

YGConfigFree(config);
}

TEST(YogaTest, percent_padding_follows_owner_size_on_relayout) {
YGConfigRef config = YGConfigNew();

Expand Down Expand Up @@ -329,3 +359,36 @@ TEST(YogaTest, percent_min_height_follows_owner_size_on_relayout) {

YGConfigFree(config);
}

TEST(
YogaTest,
percent_padding_follows_owner_size_on_relayout_with_taller_fit_content) {
YGConfigRef config = YGConfigNew();

YGNodeRef root = YGNodeNewWithConfig(config);
YGNodeStyleSetWidth(root, 100);
YGNodeStyleSetHeight(root, 100);

YGNodeRef root_child0 = YGNodeNewWithConfig(config);
YGNodeStyleSetWidth(root_child0, 50);
YGNodeStyleSetPaddingPercent(root_child0, YGEdgeTop, 50);
YGNodeInsertChild(root, root_child0, 0);

YGNodeRef root_child0_child0 = YGNodeNewWithConfig(config);
YGNodeStyleSetHeight(root_child0_child0, 10);
YGNodeInsertChild(root_child0, root_child0_child0, 0);

YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR);
ASSERT_FLOAT_EQ(50, YGNodeLayoutGetPadding(root_child0, YGEdgeTop));
ASSERT_FLOAT_EQ(60, YGNodeLayoutGetHeight(root_child0));

YGNodeStyleSetWidth(root, 60);
YGNodeStyleSetHeight(root, 120);
YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR);
ASSERT_FLOAT_EQ(30, YGNodeLayoutGetPadding(root_child0, YGEdgeTop));
ASSERT_FLOAT_EQ(40, YGNodeLayoutGetHeight(root_child0));

YGNodeFreeRecursive(root);

YGConfigFree(config);
}
76 changes: 76 additions & 0 deletions yoga/algorithm/CalculateLayout.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,56 @@

namespace facebook::yoga {

// Margin used to tell an unconstrained FitContent measurement from one that
// filled its available size.
constexpr float kUnconstrainedEpsilon = 0.5f;

std::atomic<uint32_t> gCurrentGenerationCount(0);

// Whether laying out `node` resolves a percentage against a height it doesn't
// define itself: a height, min/max height or flex basis in percent on the
// node, or on a descendant whose percentage reaches the node's available
// height. Below a node with a definite height (in points, or from an aspect
// ratio and a defined width) percentages resolve against that height, and
// absolutely positioned descendants don't affect the node's size, so neither
// counts. Children are read from the flags their own layouts left behind,
// except `display: contents` ones, which are not laid out themselves.
static bool resolvesHeightPercentage(const yoga::Node* node) {
const auto& style = node->style();
if (style.dimension(Dimension::Height).isPercent() ||
style.minDimension(Dimension::Height).isPercent() ||
style.maxDimension(Dimension::Height).isPercent() ||
style.flexBasis().isPercent()) {
return true;
}
if (style.dimension(Dimension::Height).isPoints() ||
(style.aspectRatio().isDefined() &&
(style.dimension(Dimension::Width).isPoints() ||
style.dimension(Dimension::Width).isPercent()))) {
return false;
}
for (const auto* child : node->getChildren()) {
if (child->style().positionType() == PositionType::Absolute) {
continue;
}
switch (child->style().display()) {
case Display::None:
break;
case Display::Contents:
if (resolvesHeightPercentage(child)) {
return true;
}
break;
default:
if (child->resolvesHeightPercentage()) {
return true;
}
break;
}
}
return false;
}

static bool hasAutoHorizontalMargin(const Style& style) {
for (const auto direction : {Direction::LTR, Direction::RTL}) {
if (style.flexStartMarginIsAuto(FlexDirection::Row, direction) ||
Expand Down Expand Up @@ -2804,6 +2852,33 @@ bool calculateLayoutInternal(
break;
}
}
// A FitContent height that did not constrain an earlier measurement (the
// measured height plus margin stayed below it) does not change the result
// as long as the new available height still fits that measured height.
// Content in a scroll view is measured with the container's growing
// height, so without this every clean child misses the cache whenever the
// content gets taller. That doesn't hold if a percentage in the subtree
// is resolved against the available height.
if (cachedResults == nullptr && !node->resolvesHeightPercentage() &&
heightSizingMode == SizingMode::FitContent &&
yoga::isDefined(availableHeight)) {
const float marginColumn =
node->style().computeMarginForAxis(FlexDirection::Column, ownerWidth);
for (uint32_t i = 0; i < layout->nextCachedMeasurementsIndex; i++) {
const auto& cached = layout->cachedMeasurements[i];
if (cached.widthSizingMode == widthSizingMode &&
yoga::inexactEquals(cached.availableWidth, availableWidth) &&
cached.heightSizingMode == SizingMode::FitContent &&
yoga::isDefined(cached.availableHeight) &&
cached.computedHeight + marginColumn <
cached.availableHeight - kUnconstrainedEpsilon &&
cached.computedHeight + marginColumn <= availableHeight &&
isCachedForOwnerSize(node, cached, ownerWidth, ownerHeight)) {
cachedResults = &layout->cachedMeasurements[i];
break;
}
}
}
}

if (!needToVisitNode && cachedResults != nullptr) {
Expand Down Expand Up @@ -2832,6 +2907,7 @@ bool calculateLayoutInternal(

layout->lastOwnerDirection = ownerDirection;
layout->configVersion = node->getConfig()->getVersion();
node->setResolvesHeightPercentage(resolvesHeightPercentage(node));

if (cachedResults == nullptr) {
layoutMarkerData.maxMeasureCache = std::max(
Expand Down
4 changes: 4 additions & 0 deletions yoga/node/Node.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Node::Node(Node&& node) noexcept
isReferenceBaseline_(node.isReferenceBaseline_),
isDirty_(node.isDirty_),
alwaysFormsContainingBlock_(node.alwaysFormsContainingBlock_),
resolvesHeightPercentage_(node.resolvesHeightPercentage_),
nodeType_(node.nodeType_),
context_(node.context_),
measureFunc_(node.measureFunc_),
Expand Down Expand Up @@ -208,6 +209,9 @@ void Node::setDirty(bool isDirty) {
return;
}
isDirty_ = isDirty;
if (isDirty) {
resolvesHeightPercentage_ = true;
}
if (isDirty && (dirtiedFunc_ != nullptr)) {
dirtiedFunc_(this);
}
Expand Down
16 changes: 16 additions & 0 deletions yoga/node/Node.h
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,17 @@ class YG_EXPORT Node : public ::YGNode {
return isDirty_;
}

/**
* Whether the last layout of this node resolved a percentage against a
* height, on the node itself or on a descendant that takes part in its
* layout. Measurements of such a node depend on the available height even
* when they don't fill it. True until the node has been laid out, and again
* once it is dirtied.
*/
bool resolvesHeightPercentage() const {
return resolvesHeightPercentage_;
}

Style::SizeLength getProcessedDimension(Dimension dimension) const {
return processedDimensions_[static_cast<size_t>(dimension)];
}
Expand Down Expand Up @@ -232,6 +243,10 @@ class YG_EXPORT Node : public ::YGNode {
hasNewLayout_ = hasNewLayout;
}

void setResolvesHeightPercentage(bool resolvesHeightPercentage) {
resolvesHeightPercentage_ = resolvesHeightPercentage;
}

void setNodeType(NodeType nodeType) {
nodeType_ = nodeType;
}
Expand Down Expand Up @@ -342,6 +357,7 @@ class YG_EXPORT Node : public ::YGNode {
bool isReferenceBaseline_ : 1 = false;
bool isDirty_ : 1 = true;
bool alwaysFormsContainingBlock_ : 1 = false;
bool resolvesHeightPercentage_ : 1 = true;
NodeType nodeType_ : bitCount<NodeType>() = NodeType::Default;
void* context_ = nullptr;
YGMeasureFunc measureFunc_ = nullptr;
Expand Down
Loading