diff --git a/tests/EventsTest.cpp b/tests/EventsTest.cpp index b852ba2ac6..9ea0947daf 100644 --- a/tests/EventsTest.cpp +++ b/tests/EventsTest.cpp @@ -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().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().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().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(); diff --git a/tests/YGRelayoutTest.cpp b/tests/YGRelayoutTest.cpp index b37b905d23..04b44f1edb 100644 --- a/tests/YGRelayoutTest.cpp +++ b/tests/YGRelayoutTest.cpp @@ -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(); @@ -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); +} diff --git a/yoga/algorithm/CalculateLayout.cpp b/yoga/algorithm/CalculateLayout.cpp index 401efaaee2..241527e72a 100644 --- a/yoga/algorithm/CalculateLayout.cpp +++ b/yoga/algorithm/CalculateLayout.cpp @@ -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 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) || @@ -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) { @@ -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( diff --git a/yoga/node/Node.cpp b/yoga/node/Node.cpp index 161c1900ec..8165ee17f3 100644 --- a/yoga/node/Node.cpp +++ b/yoga/node/Node.cpp @@ -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_), @@ -208,6 +209,9 @@ void Node::setDirty(bool isDirty) { return; } isDirty_ = isDirty; + if (isDirty) { + resolvesHeightPercentage_ = true; + } if (isDirty && (dirtiedFunc_ != nullptr)) { dirtiedFunc_(this); } diff --git a/yoga/node/Node.h b/yoga/node/Node.h index 74fd2c20d4..3fa3b56abf 100644 --- a/yoga/node/Node.h +++ b/yoga/node/Node.h @@ -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(dimension)]; } @@ -232,6 +243,10 @@ class YG_EXPORT Node : public ::YGNode { hasNewLayout_ = hasNewLayout; } + void setResolvesHeightPercentage(bool resolvesHeightPercentage) { + resolvesHeightPercentage_ = resolvesHeightPercentage; + } + void setNodeType(NodeType nodeType) { nodeType_ = nodeType; } @@ -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::Default; void* context_ = nullptr; YGMeasureFunc measureFunc_ = nullptr;