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
119 changes: 119 additions & 0 deletions tests/YGAutoMinSizeTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<const float*>(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();
Expand Down
84 changes: 58 additions & 26 deletions yoga/algorithm/CalculateLayout.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -872,36 +876,48 @@ 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 =
node->style().computeFlexStartPaddingAndBorder(
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);
Expand All @@ -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);

Expand All @@ -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
Expand All @@ -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{};
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1432,7 +1463,8 @@ static void resolveFlexibleLength(
direction,
mainAxisOwnerSize,
availableInnerWidth,
availableInnerHeight);
availableInnerHeight,
generationCount);
}
} else {
for (auto currentLineChild : flexLine.itemsInFlow) {
Expand Down
9 changes: 9 additions & 0 deletions yoga/node/LayoutResults.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint32_t, 2> minContentGeneration = {};
std::array<float, 2> minContentOwnerWidth = {{YGUndefined, YGUndefined}};
std::array<float, 2> 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;
Expand Down
Loading