diff --git a/tests/YGAutoMinSizeTest.cpp b/tests/YGAutoMinSizeTest.cpp index 750f6df1c2..92fd4a2c95 100644 --- a/tests/YGAutoMinSizeTest.cpp +++ b/tests/YGAutoMinSizeTest.cpp @@ -539,6 +539,125 @@ TEST(YogaAutoMinSize, static_min_content_short_circuits_container_recursion) { YGConfigFree(config); } +static YGSize measureMinContentWord( + YGNodeConstRef node, + float /*width*/, + YGMeasureMode /*widthMode*/, + float /*height*/, + YGMeasureMode /*heightMode*/) { + ++gMinContentCalls; + const auto* wordWidth = static_cast(YGNodeGetContext(node)); + return YGSize{wordWidth != nullptr ? *wordWidth : kWordWidth, kLineHeight}; +} + +// Container recursion asks every child for both axes, and every ancestor's +// layout probes its children again, so an unmemoized leaf N containers deep is +// probed O(2^N) times per layout. It must be probed at most once per axis. +TEST(YogaAutoMinSize, nested_leaf_probed_at_most_once_per_axis_per_layout) { + YGConfigRef config = makeWebConfig(/*useAutoMinSize=*/true); + YGNodeRef root = YGNodeNewWithConfig(config); + YGNodeStyleSetFlexDirection(root, YGFlexDirectionRow); + YGNodeStyleSetWidth(root, 20); + YGNodeStyleSetHeight(root, 50); + + YGNodeRef item = YGNodeNewWithConfig(config); + YGNodeInsertChild(root, item, 0); + YGNodeRef parent = item; + for (int depth = 0; depth < 10; ++depth) { + YGNodeRef container = YGNodeNewWithConfig(config); + YGNodeStyleSetFlexDirection( + container, depth % 2 == 0 ? YGFlexDirectionColumn : YGFlexDirectionRow); + YGNodeInsertChild(parent, container, 0); + parent = container; + } + YGNodeRef leaf = YGNodeNewWithConfig(config); + YGNodeSetMeasureFunc(leaf, measureWordWrappingText); + YGNodeSetMinContentMeasureFunc(leaf, measureMinContentWord); + YGNodeInsertChild(parent, leaf, 0); + + YGNodeRef spacer = YGNodeNewWithConfig(config); + YGNodeStyleSetWidth(spacer, 10); + YGNodeStyleSetFlexShrink(spacer, 0); + YGNodeInsertChild(root, spacer, 1); + + gMinContentCalls = 0; + YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR); + + EXPECT_LE(gMinContentCalls, 2); + // The memoized floor still reaches the outermost item. + EXPECT_FLOAT_EQ(kWordWidth, YGNodeLayoutGetWidth(item)); + + YGNodeFreeRecursive(root); + YGConfigFree(config); +} + +// A container's min-content depends on the owner width it is probed with, +// through percentage padding, and each ancestor probes with its own inner +// width. The memo must not hand one ancestor's result to another. +TEST(YogaAutoMinSize, container_min_content_memo_respects_owner_width) { + YGConfigRef config = makeWebConfig(/*useAutoMinSize=*/true); + YGNodeRef root = YGNodeNewWithConfig(config); + YGNodeStyleSetFlexDirection(root, YGFlexDirectionRow); + YGNodeStyleSetWidth(root, 200); + YGNodeStyleSetHeight(root, 50); + + YGNodeRef outer = YGNodeNewWithConfig(config); + YGNodeStyleSetFlexDirection(outer, YGFlexDirectionRow); + YGNodeStyleSetFlexBasis(outer, 300); + YGNodeInsertChild(root, outer, 0); + + YGNodeRef rootSpacer = YGNodeNewWithConfig(config); + YGNodeStyleSetWidth(rootSpacer, 150); + YGNodeStyleSetFlexShrink(rootSpacer, 0); + YGNodeInsertChild(root, rootSpacer, 1); + + YGNodeRef inner = YGNodeNewWithConfig(config); + YGNodeStyleSetFlexDirection(inner, YGFlexDirectionColumn); + YGNodeStyleSetPaddingPercent(inner, YGEdgeLeft, 10); + YGNodeStyleSetPaddingPercent(inner, YGEdgeRight, 10); + YGNodeInsertChild(outer, inner, 0); + + YGNodeRef leaf = YGNodeNewWithConfig(config); + YGNodeSetMeasureFunc(leaf, measureWordWrappingText); + YGNodeSetMinContentMeasureFunc(leaf, measureMinContentWord); + YGNodeInsertChild(inner, leaf, 0); + + YGNodeRef outerSpacer = YGNodeNewWithConfig(config); + YGNodeStyleSetWidth(outerSpacer, 20); + YGNodeStyleSetFlexShrink(outerSpacer, 0); + YGNodeInsertChild(outer, outerSpacer, 1); + + YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR); + + // Root probes inner against 200: 30 + 2 * 20 = 70, flooring outer at 70. + EXPECT_FLOAT_EQ(70.0f, YGNodeLayoutGetWidth(outer)); + // Outer probes inner against 70: 30 + 2 * 7 = 44, so inner takes the 50 + // left beside the spacer. Reusing the root's 70 would give 70. + EXPECT_FLOAT_EQ(50.0f, YGNodeLayoutGetWidth(inner)); + + YGNodeFreeRecursive(root); + YGConfigFree(config); +} + +// Min-content is memoized for a single layout only: the next layout probes +// again, so a leaf whose min-content changed gets its new floor. +TEST(YogaAutoMinSize, min_content_reprobed_on_next_layout) { + ShrinkRow row(/*useAutoMinSize=*/true, /*containerWidth=*/20); + float wordWidth = kWordWidth; + YGNodeSetContext(row.text, &wordWidth); + YGNodeSetMinContentMeasureFunc(row.text, measureMinContentWord); + row.layout(); + EXPECT_FLOAT_EQ(kWordWidth, YGNodeLayoutGetWidth(row.text)); + + wordWidth = 40.0f; + YGNodeMarkDirty(row.text); + gMinContentCalls = 0; + row.layout(); + + EXPECT_GT(gMinContentCalls, 0); + EXPECT_FLOAT_EQ(40.0f, YGNodeLayoutGetWidth(row.text)); +} + // Static min-content getter / setter round-trip smoke test. TEST(YogaAutoMinSize, static_min_content_getter_setter_round_trip) { YGNodeRef node = YGNodeNew(); diff --git a/yoga/algorithm/CalculateLayout.cpp b/yoga/algorithm/CalculateLayout.cpp index e8998c0693..a3a70f895a 100644 --- a/yoga/algorithm/CalculateLayout.cpp +++ b/yoga/algorithm/CalculateLayout.cpp @@ -850,15 +850,19 @@ static float computeFlexBasisForChildren( // Container-level recursion does no layout writes (no positions, no // alignment, no flex distribution); only the descendant leaf measure // callbacks observe state changes (the same ones a normal layout pass -// would invoke). Roughly equivalent to FlexLayout's dedicated -// `computeMinContentSize` cost: one measure call per leaf + linear walk -// per container. +// would invoke). +// +// Results are memoized per node and axis for the current layout generation +// (see `LayoutResults::minContent`): each leaf's measure callback runs at +// most once per axis per layout, and each container is walked once per +// distinct `ownerWidth`. static float computeMinContentMainSize( yoga::Node* const node, const FlexDirection requestedAxis, const Direction ownerDirection, const float ownerWidth, - const float ownerHeight) { + const float ownerHeight, + const uint32_t generationCount) { const bool wantRow = isRow(requestedAxis); // 1. Static value wins for any node (leaf or container). Short-circuits @@ -872,22 +876,29 @@ static float computeMinContentMainSize( return staticMin.unwrap(); } + LayoutResults& layout = node->getLayout(); + const auto axisIndex = yoga::to_underlying(dimension(requestedAxis)); + if (node->hasMeasureFunc()) { - // 2. Dynamic min-content callback if set (for Primitives whose - // min-content depends on state). Otherwise fall back to the regular - // measure function with `AtMost 0`, which text measurers naturally - // answer with longest-word width. - const YGSize size = node->hasMinContentMeasureFunc() - ? node->measureMinContent( - wantRow ? 0.0f : YGUndefined, - wantRow ? MeasureMode::AtMost : MeasureMode::Undefined, - wantRow ? YGUndefined : 0.0f, - wantRow ? MeasureMode::Undefined : MeasureMode::AtMost) - : node->measure( - wantRow ? 0.0f : YGUndefined, - wantRow ? MeasureMode::AtMost : MeasureMode::Undefined, - wantRow ? YGUndefined : 0.0f, - wantRow ? MeasureMode::Undefined : MeasureMode::AtMost); + if (layout.minContentGeneration[axisIndex] != generationCount) { + // 2. Dynamic min-content callback if set (for Primitives whose + // min-content depends on state). Otherwise fall back to the regular + // measure function with `AtMost 0`, which text measurers naturally + // answer with longest-word width. + const YGSize size = node->hasMinContentMeasureFunc() + ? node->measureMinContent( + wantRow ? 0.0f : YGUndefined, + wantRow ? MeasureMode::AtMost : MeasureMode::Undefined, + wantRow ? YGUndefined : 0.0f, + wantRow ? MeasureMode::Undefined : MeasureMode::AtMost) + : node->measure( + wantRow ? 0.0f : YGUndefined, + wantRow ? MeasureMode::AtMost : MeasureMode::Undefined, + wantRow ? YGUndefined : 0.0f, + wantRow ? MeasureMode::Undefined : MeasureMode::AtMost); + layout.minContent[axisIndex] = wantRow ? size.width : size.height; + layout.minContentGeneration[axisIndex] = generationCount; + } // Add the leaf's own padding and border, like the container branch below. const Direction leafDirection = node->resolveDirection(ownerDirection); const float paddingAndBorder = @@ -895,13 +906,18 @@ static float computeMinContentMainSize( requestedAxis, leafDirection, ownerWidth) + node->style().computeFlexEndPaddingAndBorder( requestedAxis, leafDirection, ownerWidth); - return (wantRow ? size.width : size.height) + paddingAndBorder; + return layout.minContent[axisIndex] + paddingAndBorder; } if (node->getChildCount() == 0) { return 0.0f; } + if (layout.minContentGeneration[axisIndex] == generationCount && + yoga::inexactEquals(layout.minContentOwnerWidth[axisIndex], ownerWidth)) { + return layout.minContent[axisIndex]; + } + const Direction direction = node->resolveDirection(ownerDirection); const FlexDirection nodeMainAxis = resolveDirection(node->style().flexDirection(), direction); @@ -919,11 +935,21 @@ static float computeMinContentMainSize( } float childMain = computeMinContentMainSize( - child, nodeMainAxis, direction, ownerWidth, ownerHeight); + child, + nodeMainAxis, + direction, + ownerWidth, + ownerHeight, + generationCount); childMain += child->style().computeMarginForAxis(nodeMainAxis, ownerWidth); float childCross = computeMinContentMainSize( - child, nodeCrossAxis, direction, ownerWidth, ownerHeight); + child, + nodeCrossAxis, + direction, + ownerWidth, + ownerHeight, + generationCount); childCross += child->style().computeMarginForAxis(nodeCrossAxis, ownerWidth); @@ -943,7 +969,11 @@ static float computeMinContentMainSize( const bool nodeMainIsRow = isRow(nodeMainAxis); const float widthMin = nodeMainIsRow ? mainTotal : crossMax; const float heightMin = nodeMainIsRow ? crossMax : mainTotal; - return wantRow ? widthMin : heightMin; + const float minContent = wantRow ? widthMin : heightMin; + layout.minContent[axisIndex] = minContent; + layout.minContentOwnerWidth[axisIndex] = ownerWidth; + layout.minContentGeneration[axisIndex] = generationCount; + return minContent; } // Computes the CSS Flexbox §4.5 automatic minimum main-axis size for @@ -962,7 +992,8 @@ static FloatOptional computeAutoMinMainSize( const Direction direction, const float ownerMainAxisSize, const float ownerWidth, - const float ownerHeight) { + const float ownerHeight, + const uint32_t generationCount) { if (child->hasErrata(Errata::MinSizeUndefinedInsteadOfAuto)) { return FloatOptional{}; } @@ -1007,7 +1038,7 @@ static FloatOptional computeAutoMinMainSize( // Content size suggestion: probe via min-content recursion. const FloatOptional contentMain = FloatOptional{computeMinContentMainSize( - child, mainAxis, direction, ownerWidth, ownerHeight)}; + child, mainAxis, direction, ownerWidth, ownerHeight, generationCount)}; // Combine per §4.5: floor = min(content, specified) when specified is // definite; otherwise floor = min(content, transferred) when transferred @@ -1432,7 +1463,8 @@ static void resolveFlexibleLength( direction, mainAxisOwnerSize, availableInnerWidth, - availableInnerHeight); + availableInnerHeight, + generationCount); } } else { for (auto currentLineChild : flexLine.itemsInFlow) { diff --git a/yoga/node/LayoutResults.h b/yoga/node/LayoutResults.h index 24d353f501..5a0111eb74 100644 --- a/yoga/node/LayoutResults.h +++ b/yoga/node/LayoutResults.h @@ -35,6 +35,15 @@ struct LayoutResults { // means "no auto-min applies." FloatOptional computedAutoMinMainSize = {}; + // Memoized min-content sizes for §4.5, indexed by dimension and valid only + // during layout generation `minContentGeneration`. A leaf stores its measured + // min-content without padding and border, which doesn't depend on the owner. + // A container stores its full min-content, which depends on + // `minContentOwnerWidth` through percentage padding and margins. + std::array minContentGeneration = {}; + std::array minContentOwnerWidth = {{YGUndefined, YGUndefined}}; + std::array minContent = {{YGUndefined, YGUndefined}}; + // Instead of recomputing the entire layout every single time, we cache some // information to break early when nothing changed uint32_t generationCount = 0;