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
86 changes: 86 additions & 0 deletions tests/YGFlexWrapRoundingTest.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#include <gtest/gtest.h>
#include <yoga/Yoga.h>

namespace {

// Reports the fractional width supplied through the node's context, standing in
// for a text measurement whose result is not representable on the pixel grid.
YGSize measureFractionalWidth(
YGNodeConstRef node,
float /*width*/,
YGMeasureMode /*widthMode*/,
float /*height*/,
YGMeasureMode /*heightMode*/) {
const float* measuredWidth =
static_cast<const float*>(YGNodeGetContext(const_cast<YGNodeRef>(node)));
return YGSize{*measuredWidth, 20.0f};
}

} // namespace

// A content-sized wrapping row sizes itself to the max-content sum of its
// items, then recovers the space available to those items by subtracting its
// padding and border back off. When a measure function contributes a fractional
// width, that float32 round-trip can land a fraction of an ulp below the sum
// it came from, while the measurement cache still returns the max-content
// measurement unchanged because it treats the difference as equal
// (oldSizeIsMaxContentAndStillFits). The line must not break in that case: the
// container has already been sized for a single line, so breaking leaves its
// items laid out on two lines inside a box only tall enough for one.
//
// The widths swept here put the row's content sum just under 128 and its outer
// width just over it, so adding padding and border crosses a float32 binade and
// the subtraction cannot always recover the original value.
TEST(YogaTest, content_sized_wrap_row_does_not_break_line_on_rounding) {
for (int step = 0; step < 100; step++) {
float measuredWidth = 84.0f + static_cast<float>(step) / 100.0f;
SCOPED_TRACE(
"measured width " + std::to_string(measuredWidth) + " (step " +
std::to_string(step) + ")");

YGNodeRef root = YGNodeNew();
YGNodeStyleSetWidth(root, 400.0f);
YGNodeStyleSetHeight(root, 400.0f);

// Shrink-to-fit in the cross axis, so its width comes from its content.
YGNodeRef wrapper = YGNodeNew();
YGNodeStyleSetAlignSelf(wrapper, YGAlignFlexStart);
YGNodeStyleSetBorder(wrapper, YGEdgeAll, 1.0f);
YGNodeInsertChild(root, wrapper, 0);

YGNodeRef row = YGNodeNew();
YGNodeStyleSetFlexDirection(row, YGFlexDirectionRow);
YGNodeStyleSetFlexWrap(row, YGWrapWrap);
YGNodeStyleSetPadding(row, YGEdgeAll, 16.0f);
YGNodeInsertChild(wrapper, row, 0);

YGNodeRef measured = YGNodeNew();
YGNodeSetContext(measured, &measuredWidth);
YGNodeSetMeasureFunc(measured, measureFractionalWidth);
YGNodeInsertChild(row, measured, 0);

YGNodeRef rigid = YGNodeNew();
YGNodeStyleSetWidth(rigid, 20.0f);
YGNodeStyleSetHeight(rigid, 20.0f);
YGNodeInsertChild(row, rigid, 1);

YGNodeCalculateLayout(root, 400.0f, 400.0f, YGDirectionLTR);

// Both items fit on one line by construction: the row is content-sized and
// the root is far wider than the content needs.
EXPECT_EQ(YGNodeLayoutGetTop(rigid), YGNodeLayoutGetTop(measured));

// A single line of 20pt content plus 16pt of padding on each side. If the
// line broke, the items occupy two lines while the box keeps this height.
EXPECT_EQ(52.0f, YGNodeLayoutGetHeight(row));

YGNodeFreeRecursive(root);
}
}
21 changes: 18 additions & 3 deletions yoga/algorithm/FlexLine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <yoga/algorithm/BoundAxis.h>
#include <yoga/algorithm/FlexDirection.h>
#include <yoga/algorithm/FlexLine.h>
#include <yoga/numeric/Comparison.h>

namespace facebook::yoga {

Expand Down Expand Up @@ -74,12 +75,26 @@ FlexLine calculateFlexLine(
ownerWidth)
.unwrap();

const float requiredMainDim = sizeConsumedIncludingMinConstraint +
flexBasisWithMinAndMaxConstraints + childMarginMainAxis +
childLeadingGapMainAxis;

// If this is a multi-line flow and this item pushes us over the available
// size, we've hit the end of the current line. Break out of the loop and
// lay out the current line.
if (sizeConsumedIncludingMinConstraint + flexBasisWithMinAndMaxConstraints +
childMarginMainAxis + childLeadingGapMainAxis >
availableInnerMainDim &&
//
// The overflow test is tolerant of the same epsilon the measurement cache
// uses when it decides a cached measurement "still fits" (see
// oldSizeIsMaxContentAndStillFits in Cache.cpp). A content-sized wrap
// container adds padding and border to the max-content sum to size itself
// and then subtracts them again to recover availableInnerMainDim, and in
// float32 that round-trip can land a few ulps low. The cache accepts a
// basis up to the epsilon wider than the space now available and hands
// back the max-content measurement unchanged; without the same tolerance
// here, that basis reads as overflow and the line breaks, leaving the
// container sized for one line with its items laid out on two.
if (requiredMainDim > availableInnerMainDim &&
!yoga::inexactEquals(requiredMainDim, availableInnerMainDim) &&
isNodeFlexWrap && !itemsInFlow.empty()) {
break;
}
Expand Down