diff --git a/tests/YGRelayoutTest.cpp b/tests/YGRelayoutTest.cpp index c4c0adde23..dd677a8389 100644 --- a/tests/YGRelayoutTest.cpp +++ b/tests/YGRelayoutTest.cpp @@ -8,6 +8,52 @@ #include #include +#include +#include + +/* + * Behaves like wrapping text: 2000pt of content laid out at the available + * width with a 20pt line height, so a narrower box measures taller. + */ +static YGSize _simulateWrappingText( + YGNodeConstRef /*node*/, + float width, + YGMeasureMode widthMode, + float height, + YGMeasureMode heightMode) { + const float maxWidth = widthMode == YGMeasureModeUndefined + ? std::numeric_limits::infinity() + : width; + const float lines = std::isfinite(maxWidth) && maxWidth > 0 + ? std::ceil(2000.0f / maxWidth) + : 1.0f; + const float naturalHeight = lines * 20.0f; + + YGSize size{std::isfinite(maxWidth) ? maxWidth : 2000.0f, naturalHeight}; + if (heightMode == YGMeasureModeExactly) { + size.height = height; + } else if (heightMode == YGMeasureModeAtMost && naturalHeight > height) { + size.height = height; + } + if (widthMode == YGMeasureModeExactly) { + size.width = width; + } + + return size; +} + +/* + * Pins the root to an exact size, the way a host platform does when the + * available space changes (e.g. a device rotation), then lays out. + */ +static void _layoutAtSize(YGNodeRef root, float width, float height) { + YGNodeStyleSetMinWidth(root, width); + YGNodeStyleSetMaxWidth(root, width); + YGNodeStyleSetMinHeight(root, height); + YGNodeStyleSetMaxHeight(root, height); + YGNodeCalculateLayout(root, width, height, YGDirectionLTR); +} + TEST(YogaTest, dont_cache_computed_flex_basis_between_layouts) { YGConfigRef config = YGConfigNew(); YGConfigSetExperimentalFeatureEnabled( @@ -253,3 +299,60 @@ TEST(YogaTest, has_new_layout_flag_set_static) { YGNodeFreeRecursive(root); } + +/* + * A flex basis measured from a child's content during an earlier layout must + * not be reused once the available size changes. The wrapper levels let an + * ancestor's measurement cache answer for the whole subtree during the + * max-content pass, skipping the measurement that would otherwise refresh the + * stored basis. + */ +TEST(YogaTest, measured_flex_basis_is_not_reused_after_relayout_at_new_size) { + YGConfigRef config = YGConfigNew(); + + YGNodeRef root = YGNodeNewWithConfig(config); + + YGNodeRef scrollView = YGNodeNewWithConfig(config); + YGNodeStyleSetOverflow(scrollView, YGOverflowScroll); + YGNodeStyleSetFlexGrow(scrollView, 1); + YGNodeStyleSetFlexShrink(scrollView, 1); + YGNodeInsertChild(root, scrollView, 0); + + YGNodeRef contentContainer = YGNodeNewWithConfig(config); + YGNodeInsertChild(scrollView, contentContainer, 0); + + YGNodeRef autoHeightColumn = YGNodeNewWithConfig(config); + YGNodeInsertChild(contentContainer, autoHeightColumn, 0); + + YGNodeRef flexColumn = YGNodeNewWithConfig(config); + YGNodeStyleSetFlex(flexColumn, 1); + YGNodeInsertChild(autoHeightColumn, flexColumn, 0); + + YGNodeRef fixedSibling = YGNodeNewWithConfig(config); + YGNodeStyleSetHeight(fixedSibling, 24); + YGNodeInsertChild(flexColumn, fixedSibling, 0); + + YGNodeRef wrapper0 = YGNodeNewWithConfig(config); + YGNodeInsertChild(flexColumn, wrapper0, 1); + + YGNodeRef wrapper1 = YGNodeNewWithConfig(config); + YGNodeInsertChild(wrapper0, wrapper1, 0); + + YGNodeRef text = YGNodeNewWithConfig(config); + YGNodeStyleSetFlex(text, 1); + YGNodeSetMeasureFunc(text, _simulateWrappingText); + YGNodeInsertChild(wrapper1, text, 0); + + _layoutAtSize(root, 400, 800); + const float portraitHeight = YGNodeLayoutGetHeight(text); + + _layoutAtSize(root, 800, 400); + _layoutAtSize(root, 400, 800); + + ASSERT_FLOAT_EQ(portraitHeight, YGNodeLayoutGetHeight(text)); + ASSERT_FLOAT_EQ( + YGNodeLayoutGetHeight(wrapper0), YGNodeLayoutGetHeight(wrapper1)); + + YGNodeFreeRecursive(root); + YGConfigFree(config); +} diff --git a/yoga/algorithm/CalculateLayout.cpp b/yoga/algorithm/CalculateLayout.cpp index 4e7930a92b..f346b99daf 100644 --- a/yoga/algorithm/CalculateLayout.cpp +++ b/yoga/algorithm/CalculateLayout.cpp @@ -266,18 +266,37 @@ static void computeFlexBasisForChild( node->getConfig()->isExperimentalFeatureEnabled( ExperimentalFeature::FixFlexBasisFitContent); - const bool useResolvedFlexBasis = + bool useResolvedFlexBasis = resolvedFlexBasis.isDefined() && yoga::isDefined(mainAxisSize); + // Whether the resolved-flex-basis branch below would keep a basis stored by + // an earlier layout instead of writing a fresh one. + const bool retainsStoredFlexBasis = + child->getLayout().computedFlexBasis.isDefined() && + !(child->getConfig()->isExperimentalFeatureEnabled( + ExperimentalFeature::WebFlexBasis) && + child->getLayout().computedFlexBasisGeneration != generationCount); + + // A flex basis measured from the child's content during an earlier layout + // must not be retained. The max-content pass that would have refreshed it + // can be skipped entirely when an ancestor's measurement cache answers for + // the whole subtree, leaving a value computed against a different available + // size. Measuring again does exactly what that skipped pass would have done + // (and usually hits the child's own measurement cache). A retained + // *resolved* basis is still valid and is left alone. + if (useResolvedFlexBasis && retainsStoredFlexBasis && + child->getLayout().computedFlexBasisIsMeasured && + child->getLayout().computedFlexBasisGeneration != generationCount) { + useResolvedFlexBasis = false; + } + if (useResolvedFlexBasis) { - if (child->getLayout().computedFlexBasis.isUndefined() || - (child->getConfig()->isExperimentalFeatureEnabled( - ExperimentalFeature::WebFlexBasis) && - child->getLayout().computedFlexBasisGeneration != generationCount)) { + if (!retainsStoredFlexBasis) { const FloatOptional paddingAndBorder = FloatOptional( paddingAndBorderForAxis(child, mainAxis, direction, ownerWidth)); child->setLayoutComputedFlexBasis( yoga::maxOrDefined(resolvedFlexBasis, paddingAndBorder)); + child->setLayoutComputedFlexBasisIsMeasured(false); } } else if (isMainAxisRow && isRowStyleDimDefined) { // The width is definite, so use that as the flex basis. @@ -290,6 +309,7 @@ static void computeFlexBasisForChild( child->getResolvedDimension( direction, Dimension::Width, ownerWidth, ownerWidth), paddingAndBorder)); + child->setLayoutComputedFlexBasisIsMeasured(false); } else if (!isMainAxisRow && isColumnStyleDimDefined) { // The height is definite, so use that as the flex basis. const FloatOptional paddingAndBorder = @@ -300,6 +320,7 @@ static void computeFlexBasisForChild( child->getResolvedDimension( direction, Dimension::Height, ownerHeight, ownerWidth), paddingAndBorder)); + child->setLayoutComputedFlexBasisIsMeasured(false); } else { // Compute the flex basis and hypothetical main size (i.e. the clamped flex // basis). @@ -454,6 +475,7 @@ static void computeFlexBasisForChild( yoga::maxOrDefined( child->getLayout().measuredDimension(dimension(mainAxis)), paddingAndBorderForAxis(child, mainAxis, direction, ownerWidth)))); + child->setLayoutComputedFlexBasisIsMeasured(true); } child->setLayoutComputedFlexBasisGeneration(generationCount); } @@ -801,6 +823,7 @@ static float computeFlexBasisForChildren( if (child == singleFlexChild) { child->setLayoutComputedFlexBasisGeneration(generationCount); child->setLayoutComputedFlexBasis(FloatOptional(0)); + child->setLayoutComputedFlexBasisIsMeasured(false); } else { computeFlexBasisForChild( node, diff --git a/yoga/node/LayoutResults.cpp b/yoga/node/LayoutResults.cpp index acc37a1b4a..7631960b13 100644 --- a/yoga/node/LayoutResults.cpp +++ b/yoga/node/LayoutResults.cpp @@ -24,7 +24,8 @@ bool LayoutResults::operator==(LayoutResults layout) const { configVersion == layout.configVersion && nextCachedMeasurementsIndex == layout.nextCachedMeasurementsIndex && cachedLayout == layout.cachedLayout && - computedFlexBasis == layout.computedFlexBasis; + computedFlexBasis == layout.computedFlexBasis && + computedFlexBasisIsMeasured == layout.computedFlexBasisIsMeasured; for (uint32_t i = 0; i < LayoutResults::MaxCachedMeasurements && isEqual; ++i) { diff --git a/yoga/node/LayoutResults.h b/yoga/node/LayoutResults.h index 24d353f501..6ce4e7c0ef 100644 --- a/yoga/node/LayoutResults.h +++ b/yoga/node/LayoutResults.h @@ -27,6 +27,11 @@ struct LayoutResults { uint32_t computedFlexBasisGeneration = 0; FloatOptional computedFlexBasis = {}; + // True when `computedFlexBasis` holds a size measured from the child's own + // content (the max-content pass), false when it was resolved from + // `flex-basis` or from a style dimension. + bool computedFlexBasisIsMeasured = false; + // Per-flex-item floor along the main axis derived from CSS Flexbox ยง4.5 // automatic minimum sizing. Set by `resolveFlexibleLength` when the parent's // config does NOT carry the `MinSizeUndefinedInsteadOfAuto` errata and the diff --git a/yoga/node/Node.cpp b/yoga/node/Node.cpp index 692d33d9c4..e87fea92ef 100644 --- a/yoga/node/Node.cpp +++ b/yoga/node/Node.cpp @@ -269,6 +269,11 @@ void Node::setLayoutComputedFlexBasis(const FloatOptional computedFlexBasis) { layout_.computedFlexBasis = computedFlexBasis; } +void Node::setLayoutComputedFlexBasisIsMeasured( + const bool computedFlexBasisIsMeasured) { + layout_.computedFlexBasisIsMeasured = computedFlexBasisIsMeasured; +} + void Node::setLayoutPosition(float position, PhysicalEdge edge) { layout_.setPosition(edge, position); } @@ -454,6 +459,7 @@ void Node::markDirtyAndPropagate() { if (!isDirty_) { setDirty(true); setLayoutComputedFlexBasis(FloatOptional()); + setLayoutComputedFlexBasisIsMeasured(false); if (owner_ != nullptr) { owner_->markDirtyAndPropagate(); } diff --git a/yoga/node/Node.h b/yoga/node/Node.h index 74fd2c20d4..8338262cbf 100644 --- a/yoga/node/Node.h +++ b/yoga/node/Node.h @@ -286,6 +286,7 @@ class YG_EXPORT Node : public ::YGNode { void setChildren(const std::vector& children); void setLayoutLastOwnerDirection(Direction direction); void setLayoutComputedFlexBasis(FloatOptional computedFlexBasis); + void setLayoutComputedFlexBasisIsMeasured(bool computedFlexBasisIsMeasured); void setLayoutComputedFlexBasisGeneration( uint32_t computedFlexBasisGeneration); void setLayoutMeasuredDimension(float measuredDimension, Dimension dimension);