diff --git a/tests/YGRoundingMeasureFuncTest.cpp b/tests/YGRoundingMeasureFuncTest.cpp index c654a280ec..20180824a3 100644 --- a/tests/YGRoundingMeasureFuncTest.cpp +++ b/tests/YGRoundingMeasureFuncTest.cpp @@ -5,6 +5,8 @@ * LICENSE file in the root directory of this source tree. */ +#include + #include #include @@ -137,3 +139,83 @@ TEST( YGConfigFree(config); } + +// 23 physical pixels on a 2.75 density screen, as a float. Measurement on +// Android reports whole physical pixels converted to points, so the product +// with the scale factor lands a hair under 23 rather than exactly on it. +static YGSize _measureTwentyThreePhysicalPixels( + YGNodeConstRef /*node*/, + float /*width*/, + YGMeasureMode /*widthMode*/, + float /*height*/, + YGMeasureMode /*heightMode*/) { + return YGSize{ + .width = 8.363636016845703f, + .height = 8.363636016845703f, + }; +} + +static long widthInPhysicalPixels(YGNodeConstRef node, float pointScaleFactor) { + return std::lround( + static_cast(YGNodeLayoutGetWidth(node)) * pointScaleFactor); +} + +// A node with a measure function must never be rounded below the size it +// measured. Here the left edge lands on 103.9999008 scaled units, inside the +// tolerance used to decide a value already sits on the pixel grid, and is +// snapped up to 104; the right edge lands on 126.9998999, just outside it, and +// is floored to 126. Rounding the edges independently yields 22 pixels for 23 +// pixels of content. +TEST( + YogaTest, + rounding_feature_with_custom_measure_never_rounds_below_measure) { + const float pointScaleFactor = 2.75f; + + YGConfigRef config = YGConfigNew(); + YGConfigSetPointScaleFactor(config, pointScaleFactor); + + YGNodeRef root = YGNodeNewWithConfig(config); + YGNodeStyleSetPositionType(root, YGPositionTypeAbsolute); + + YGNodeRef root_child0 = YGNodeNewWithConfig(config); + YGNodeStyleSetPositionType(root_child0, YGPositionTypeRelative); + YGNodeStyleSetPosition(root_child0, YGEdgeLeft, 37.818145751953125f); + YGNodeSetMeasureFunc(root_child0, _measureTwentyThreePhysicalPixels); + YGNodeInsertChild(root, root_child0, 0); + + YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR); + + ASSERT_EQ(23, widthInPhysicalPixels(root_child0, pointScaleFactor)); + + YGNodeFreeRecursive(root); + YGConfigFree(config); +} + +// The counterpart: a node that already rounds to the size it measured must not +// gain a pixel. The left edge lands on 28.875 scaled units, well clear of the +// tolerance, and both edges floor consistently. Forcing the right edge to ceil +// unconditionally would widen this node to 24 pixels. +TEST( + YogaTest, + rounding_feature_with_custom_measure_does_not_widen_when_it_fits) { + const float pointScaleFactor = 2.75f; + + YGConfigRef config = YGConfigNew(); + YGConfigSetPointScaleFactor(config, pointScaleFactor); + + YGNodeRef root = YGNodeNewWithConfig(config); + YGNodeStyleSetPositionType(root, YGPositionTypeAbsolute); + + YGNodeRef root_child0 = YGNodeNewWithConfig(config); + YGNodeStyleSetPositionType(root_child0, YGPositionTypeRelative); + YGNodeStyleSetPosition(root_child0, YGEdgeLeft, 10.5f); + YGNodeSetMeasureFunc(root_child0, _measureTwentyThreePhysicalPixels); + YGNodeInsertChild(root, root_child0, 0); + + YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR); + + ASSERT_EQ(23, widthInPhysicalPixels(root_child0, pointScaleFactor)); + + YGNodeFreeRecursive(root); + YGConfigFree(config); +} diff --git a/yoga/algorithm/PixelGrid.cpp b/yoga/algorithm/PixelGrid.cpp index 5c5a3b7ffb..5175695db7 100644 --- a/yoga/algorithm/PixelGrid.cpp +++ b/yoga/algorithm/PixelGrid.cpp @@ -106,15 +106,37 @@ void roundLayoutResultsToPixelGrid( const bool hasFractionalHeight = !yoga::inexactEquals(round(scaledNodeHeight), scaledNodeHeight); - node->getLayout().setDimension( - Dimension::Width, - roundValueToPixelGrid( - absoluteNodeRight, - pointScaleFactor, - (textRounding && hasFractionalWidth), - (textRounding && !hasFractionalWidth)) - - roundValueToPixelGrid( - absoluteNodeLeft, pointScaleFactor, false, textRounding)); + const float roundedNodeLeft = roundValueToPixelGrid( + absoluteNodeLeft, pointScaleFactor, false, textRounding); + + float roundedNodeWidth = roundValueToPixelGrid( + absoluteNodeRight, + pointScaleFactor, + (textRounding && hasFractionalWidth), + (textRounding && !hasFractionalWidth)) - + roundedNodeLeft; + + // Rounding the two edges independently can still narrow a node below the + // size it measured, which is what the comment above means to prevent. The + // left and right edge can fall on opposite sides of `inexactEquals`' + // tolerance: for a node measured as 23 physical pixels on a 2.75 density + // screen the left edge lands on 103.9999008 scaled units and is snapped up + // to 104, while the right edge lands on 126.9998999, misses the tolerance, + // and is floored to 126 - a 22 pixel wide box for 23 pixels of content. + // + // Recompute the right edge with `forceCeil` in exactly those cases. Nodes + // that already round to at least the size they measured keep the width + // computed above. + const double scaledRoundedNodeWidth = + static_cast(roundedNodeWidth) * pointScaleFactor; + if (textRounding && scaledRoundedNodeWidth < scaledNodeWith && + !yoga::inexactEquals(scaledRoundedNodeWidth, scaledNodeWith)) { + roundedNodeWidth = roundValueToPixelGrid( + absoluteNodeRight, pointScaleFactor, true, false) - + roundedNodeLeft; + } + + node->getLayout().setDimension(Dimension::Width, roundedNodeWidth); node->getLayout().setDimension( Dimension::Height,