diff --git a/packages/react-native/Libraries/Components/View/__tests__/View-flexboxAutoMinSize-itest.js b/packages/react-native/Libraries/Components/View/__tests__/View-flexboxAutoMinSize-itest.js new file mode 100644 index 000000000000..6a2b6dc787a2 --- /dev/null +++ b/packages/react-native/Libraries/Components/View/__tests__/View-flexboxAutoMinSize-itest.js @@ -0,0 +1,90 @@ +/** + * 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. + * + * @flow strict-local + * @fantom_flags enableFlexboxAutoMinSizeInStrictMode:true + * @format + */ + +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + +import type {HostInstance} from 'react-native'; + +import * as Fantom from '@react-native/fantom'; +import nullthrows from 'nullthrows'; +import * as React from 'react'; +import {createRef} from 'react'; +import { + View, + experimental_LayoutConformance as LayoutConformance, +} from 'react-native'; + +type ColumnProps = Readonly<{ + contentPadding: number, + itemRef: React.RefSetter, + minHeight?: number, +}>; + +// Two items asking for 200 in a column of 100 shrink to 50 each, unless the +// first one's automatic minimum size, the padding of the view it holds, is +// larger. +function Column({contentPadding, itemRef, minHeight}: ColumnProps): React.Node { + return ( + + + + + + + + + + + ); +} + +describe('the automatic minimum size of a flex item under strict layout conformance', () => { + it('follows a change to its content', () => { + const root = Fantom.createRoot(); + const itemRef = createRef(); + + const render = (contentPadding: number) => { + Fantom.runTask(() => { + root.render( + , + ); + }); + return nullthrows(itemRef.current).getBoundingClientRect().height; + }; + + expect(render(30)).toBe(50); + expect(render(80)).toBe(80); + }); + + it('follows a change to its content laid out under an explicit minimum height', () => { + const root = Fantom.createRoot(); + const itemRef = createRef(); + + const render = (contentPadding: number, minHeight?: number) => { + Fantom.runTask(() => { + root.render( + , + ); + }); + return nullthrows(itemRef.current).getBoundingClientRect().height; + }; + + expect(render(30)).toBe(50); + // With explicit minimum heights nothing asks for the content's minimum + // size, so the change is laid out without it. + expect(render(80, 0)).toBe(50); + expect(render(80)).toBe(80); + }); +}); diff --git a/packages/react-native/Libraries/Text/__tests__/Text-flexboxAutoMinSize-itest.js b/packages/react-native/Libraries/Text/__tests__/Text-flexboxAutoMinSize-itest.js new file mode 100644 index 000000000000..86822602d8ba --- /dev/null +++ b/packages/react-native/Libraries/Text/__tests__/Text-flexboxAutoMinSize-itest.js @@ -0,0 +1,116 @@ +/** + * 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. + * + * @flow strict-local + * @fantom_flags enableFlexboxAutoMinSizeInStrictMode:true + * @format + */ + +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + +import type {HostInstance} from 'react-native'; + +import * as Fantom from '@react-native/fantom'; +import nullthrows from 'nullthrows'; +import * as React from 'react'; +import {createRef} from 'react'; +import { + Text, + View, + experimental_LayoutConformance as LayoutConformance, +} from 'react-native'; + +const paragraph = ( + + Open + +); + +const paragraphWithMinHeight = ( + + Open + +); + +type FooterScreenProps = Readonly<{ + footerHeight: number, + footerRef: React.RefSetter, +}>; + +function FooterScreen({ + footerHeight, + footerRef, +}: FooterScreenProps): React.Node { + return ( + + + {paragraph} + + + + ); +} + +type ItemScreenProps = Readonly<{ + itemMinHeight?: number, + itemRef: React.RefSetter, +}>; + +function ItemScreen({itemMinHeight, itemRef}: ItemScreenProps): React.Node { + return ( + + + + {paragraphWithMinHeight} + + + + ); +} + +describe(' holding a under strict layout conformance', () => { + it('lays out again after a sibling of the view holding it changes', () => { + const root = Fantom.createRoot(); + const footerRef = createRef(); + + const render = (footerHeight: number) => { + Fantom.runTask(() => { + root.render( + , + ); + }); + }; + + render(10); + // Only the footer changes, so the paragraph stays as the first render + // committed it while its container is laid out again. + render(20); + + expect(nullthrows(footerRef.current).getBoundingClientRect().height).toBe( + 20, + ); + }); + + it('lays out again after the view holding it drops its minimum height', () => { + const root = Fantom.createRoot(); + const itemRef = createRef(); + + const render = (itemMinHeight?: number) => { + Fantom.runTask(() => { + root.render( + , + ); + }); + }; + + // Explicit minimum heights keep the min-content pass out of the paragraph, + // so the pass first reaches it once the first render has committed it. + render(0); + render(); + + expect(nullthrows(itemRef.current).getBoundingClientRect().height).toBe(30); + }); +}); diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.cpp index 226385a6ec31..91892b2a878f 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.cpp @@ -881,6 +881,8 @@ YGSize YogaLayoutableShadowNode::yogaNodeMeasureCallbackConnector( TraceSection s("YogaLayoutableShadowNode::yogaNodeMeasureCallbackConnector"); auto& shadowNode = shadowNodeFromContext(yogaNode); + // Measuring may write to the node, so it must not be a committed one. + shadowNode.ensureUnsealed(); auto minimumSize = Size{.width = 0, .height = 0}; auto maximumSize = Size{ diff --git a/packages/react-native/ReactCommon/yoga/test/MinContentTest.cpp b/packages/react-native/ReactCommon/yoga/test/MinContentTest.cpp new file mode 100644 index 000000000000..ac49cf49c21b --- /dev/null +++ b/packages/react-native/ReactCommon/yoga/test/MinContentTest.cpp @@ -0,0 +1,202 @@ +/* + * 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 + +#include + +namespace facebook::yoga { +namespace { + +struct MeasureCalls { + YGNodeConstRef original = nullptr; + int ofOriginal = 0; + int ofCopies = 0; +}; + +YGSize measureWord( + YGNodeConstRef node, + float /*width*/, + YGMeasureMode /*widthMode*/, + float /*height*/, + YGMeasureMode /*heightMode*/) { + auto* calls = static_cast(YGNodeGetContext(node)); + ++(node == calls->original ? calls->ofOriginal : calls->ofCopies); + return YGSize{.width = 30.0f, .height = 10.0f}; +} + +YGConfigRef autoMinSizeConfig() { + YGConfigRef config = YGConfigNew(); + YGConfigSetErrata(config, YGErrataNone); + return config; +} + +YGNodeRef measuredLeaf(YGConfigRef config, MeasureCalls* calls) { + YGNodeRef leaf = YGNodeNewWithConfig(config); + YGNodeSetMeasureFunc(leaf, measureWord); + YGNodeSetContext(leaf, calls); + calls->original = leaf; + return leaf; +} + +TEST(MinContentTest, testWalkClonesChildrenOfAnotherTree) { + YGConfigRef config = autoMinSizeConfig(); + MeasureCalls calls; + YGNodeRef root = YGNodeNewWithConfig(config); + YGNodeStyleSetWidth(root, 100); + YGNodeStyleSetHeight(root, 100); + YGNodeRef item = YGNodeNewWithConfig(config); + YGNodeStyleSetHeight(item, 30); + YGNodeStyleSetMinHeight(item, 0); + YGNodeInsertChild(root, item, 0); + // Explicit minimum heights keep every min-content walk away from the leaf. + YGNodeRef leaf = measuredLeaf(config, &calls); + YGNodeStyleSetMinHeight(leaf, 0); + YGNodeInsertChild(item, leaf, 0); + YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR); + + // A second tree shares the leaf, under a copy of the item that drops its + // minimum height, so the walk first reaches the leaf in that tree. + YGNodeRef rootCopy = YGNodeClone(root); + YGNodeRef itemCopy = YGNodeClone(item); + YGNodeSwapChild(rootCopy, itemCopy, 0); + YGNodeStyleSetMinHeight(itemCopy, YGUndefined); + calls.ofOriginal = 0; + calls.ofCopies = 0; + YGNodeCalculateLayout(rootCopy, YGUndefined, YGUndefined, YGDirectionLTR); + + EXPECT_EQ(0, calls.ofOriginal); + EXPECT_GT(calls.ofCopies, 0); + + YGNodeFreeRecursive(rootCopy); + YGNodeFreeRecursive(root); + YGConfigFree(config); +} + +TEST(MinContentTest, testUnchangedItemIsNotMeasuredAgain) { + YGConfigRef config = autoMinSizeConfig(); + MeasureCalls calls; + YGNodeRef root = YGNodeNewWithConfig(config); + YGNodeStyleSetWidth(root, 100); + YGNodeStyleSetHeight(root, 100); + YGNodeRef item = YGNodeNewWithConfig(config); + YGNodeInsertChild(root, item, 0); + YGNodeInsertChild(item, measuredLeaf(config, &calls), 0); + YGNodeRef sibling = YGNodeNewWithConfig(config); + YGNodeStyleSetHeight(sibling, 10); + YGNodeInsertChild(root, sibling, 1); + YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR); + + YGNodeStyleSetHeight(sibling, 20); + calls.ofOriginal = 0; + YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR); + + EXPECT_EQ(0, calls.ofOriginal); + EXPECT_FLOAT_EQ(20.0f, YGNodeLayoutGetHeight(sibling)); + + YGNodeFreeRecursive(root); + YGConfigFree(config); +} + +TEST(MinContentTest, testStaticMinContentChangeReachesNextLayout) { + YGConfigRef config = autoMinSizeConfig(); + YGNodeRef root = YGNodeNewWithConfig(config); + YGNodeStyleSetFlexDirection(root, YGFlexDirectionRow); + YGNodeStyleSetWidth(root, 20); + YGNodeStyleSetHeight(root, 50); + YGNodeRef item = YGNodeNewWithConfig(config); + YGNodeStyleSetFlexBasis(item, 90); + YGNodeStyleSetFlexShrink(item, 1); + YGNodeInsertChild(root, item, 0); + YGNodeRef scroller = YGNodeNewWithConfig(config); + YGNodeSetMinContentWidth(scroller, 10); + YGNodeInsertChild(item, scroller, 0); + YGNodeRef spacer = YGNodeNewWithConfig(config); + YGNodeStyleSetWidth(spacer, 10); + YGNodeInsertChild(root, spacer, 1); + YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR); + ASSERT_FLOAT_EQ(10.0f, YGNodeLayoutGetWidth(item)); + + YGNodeSetMinContentWidth(scroller, 30); + YGNodeStyleSetWidth(spacer, 5); + YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR); + + EXPECT_FLOAT_EQ(30.0f, YGNodeLayoutGetWidth(item)); + + YGNodeFreeRecursive(root); + YGConfigFree(config); +} + +TEST(MinContentTest, testNewWidthDoesNotCloneItemContent) { + YGConfigRef config = autoMinSizeConfig(); + MeasureCalls calls; + YGNodeRef root = YGNodeNewWithConfig(config); + YGNodeStyleSetAlignItems(root, YGAlignFlexStart); + YGNodeStyleSetWidth(root, 300); + YGNodeStyleSetHeight(root, 100); + YGNodeRef item = YGNodeNewWithConfig(config); + YGNodeStyleSetWidth(item, 50); + YGNodeStyleSetHeight(item, 200); + YGNodeStyleSetFlexShrink(item, 1); + YGNodeInsertChild(root, item, 0); + YGNodeRef content = YGNodeNewWithConfig(config); + YGNodeInsertChild(content, measuredLeaf(config, &calls), 0); + YGNodeInsertChild(item, content, 0); + YGNodeRef sibling = YGNodeNewWithConfig(config); + YGNodeStyleSetHeight(sibling, 200); + YGNodeStyleSetFlexShrink(sibling, 1); + YGNodeInsertChild(root, sibling, 1); + YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR); + + // A narrower second tree shares the item's content. The item keeps its + // size, so its layout comes from cache, and nothing below it resolves a + // percentage against the new width. + YGNodeRef rootCopy = YGNodeClone(root); + YGNodeStyleSetWidth(rootCopy, 290); + YGNodeCalculateLayout(rootCopy, YGUndefined, YGUndefined, YGDirectionLTR); + + EXPECT_EQ(content, YGNodeGetChild(YGNodeGetChild(rootCopy, 0), 0)); + EXPECT_FLOAT_EQ(50.0f, YGNodeLayoutGetHeight(YGNodeGetChild(rootCopy, 0))); + + YGNodeFreeRecursive(rootCopy); + YGNodeFreeRecursive(root); + YGConfigFree(config); +} + +TEST(MinContentTest, testPercentagePaddingFollowsNewWidth) { + YGConfigRef config = autoMinSizeConfig(); + MeasureCalls calls; + YGNodeRef root = YGNodeNewWithConfig(config); + YGNodeStyleSetFlexDirection(root, YGFlexDirectionRow); + YGNodeStyleSetWidth(root, 100); + YGNodeStyleSetHeight(root, 50); + YGNodeRef item = YGNodeNewWithConfig(config); + YGNodeStyleSetFlexBasis(item, 100); + YGNodeStyleSetFlexShrink(item, 1); + YGNodeInsertChild(root, item, 0); + YGNodeRef content = YGNodeNewWithConfig(config); + YGNodeStyleSetPaddingPercent(content, YGEdgeLeft, 10); + YGNodeInsertChild(content, measuredLeaf(config, &calls), 0); + YGNodeInsertChild(item, content, 0); + YGNodeRef sibling = YGNodeNewWithConfig(config); + YGNodeStyleSetFlexBasis(sibling, 300); + YGNodeStyleSetFlexShrink(sibling, 1); + YGNodeInsertChild(root, sibling, 1); + YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR); + ASSERT_FLOAT_EQ(40.0f, YGNodeLayoutGetWidth(item)); + + YGNodeStyleSetWidth(root, 150); + YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR); + + EXPECT_FLOAT_EQ(45.0f, YGNodeLayoutGetWidth(item)); + + YGNodeFreeRecursive(root); + YGConfigFree(config); +} + +} // namespace +} // namespace facebook::yoga diff --git a/packages/react-native/ReactCommon/yoga/yoga/YGNode.cpp b/packages/react-native/ReactCommon/yoga/yoga/YGNode.cpp index 7e66845fc173..afadea4c89c9 100644 --- a/packages/react-native/ReactCommon/yoga/yoga/YGNode.cpp +++ b/packages/react-native/ReactCommon/yoga/yoga/YGNode.cpp @@ -320,12 +320,22 @@ bool YGNodeHasMinContentMeasureFunc(YGNodeConstRef node) { return resolveRef(node)->hasMinContentMeasureFunc(); } -void YGNodeSetMinContentWidth(YGNodeRef node, float minContentWidth) { - resolveRef(node)->setMinContentWidth(FloatOptional{minContentWidth}); +void YGNodeSetMinContentWidth(YGNodeRef nodeRef, float minContentWidth) { + const auto node = resolveRef(nodeRef); + const FloatOptional value{minContentWidth}; + if (node->getMinContentWidth() != value) { + node->setMinContentWidth(value); + node->markDirtyAndPropagate(); + } } -void YGNodeSetMinContentHeight(YGNodeRef node, float minContentHeight) { - resolveRef(node)->setMinContentHeight(FloatOptional{minContentHeight}); +void YGNodeSetMinContentHeight(YGNodeRef nodeRef, float minContentHeight) { + const auto node = resolveRef(nodeRef); + const FloatOptional value{minContentHeight}; + if (node->getMinContentHeight() != value) { + node->setMinContentHeight(value); + node->markDirtyAndPropagate(); + } } float YGNodeGetMinContentWidth(YGNodeConstRef node) { diff --git a/packages/react-native/ReactCommon/yoga/yoga/YGNode.h b/packages/react-native/ReactCommon/yoga/yoga/YGNode.h index 7a9b0e1c3f12..4f8cdf81a521 100644 --- a/packages/react-native/ReactCommon/yoga/yoga/YGNode.h +++ b/packages/react-native/ReactCommon/yoga/yoga/YGNode.h @@ -235,7 +235,8 @@ YG_EXPORT bool YGNodeHasMeasureFunc(YGNodeConstRef node); * invoked with the same `AtMost 0` constraint — preserving today's * behavior. Setting this is a perf opt-in for Primitives whose min-content * size is cheap to compute or differs from what the regular measure would - * return. + * return. Its results are kept, as a measure function's are, until the node is + * marked dirty. */ typedef YGSize (*YGMinContentMeasureFunc)( YGNodeConstRef node, @@ -259,7 +260,8 @@ YG_EXPORT bool YGNodeHasMinContentMeasureFunc(YGNodeConstRef node); /** * Static per-axis min-content size used by the CSS Flexbox §4.5 automatic - * minimum sizing probe. Pass `YGUndefined` to clear. + * minimum sizing probe. Pass `YGUndefined` to clear. A new value marks the node + * dirty, as a style change does. * * When set, the algorithm's probe short-circuits at this node along the * matching axis — skipping both the regular `YGMeasureFunc` / `YGMinContent diff --git a/packages/react-native/ReactCommon/yoga/yoga/algorithm/CalculateLayout.cpp b/packages/react-native/ReactCommon/yoga/yoga/algorithm/CalculateLayout.cpp index 192a6af77da9..5ac58810f8d1 100644 --- a/packages/react-native/ReactCommon/yoga/yoga/algorithm/CalculateLayout.cpp +++ b/packages/react-native/ReactCommon/yoga/yoga/algorithm/CalculateLayout.cpp @@ -825,6 +825,41 @@ static float computeFlexBasisForChildren( return totalOuterFlexBasis; } +// Whether what an earlier layout cached for `node` no longer holds: the node or +// a descendant changed and has not been visited in this layout yet, or its +// config or the direction it inherits changed. +static bool isCacheStale( + const yoga::Node* const node, + const Direction ownerDirection, + const uint32_t generationCount) { + const LayoutResults& layout = node->getLayout(); + return (node->isDirty() && layout.generationCount != generationCount) || + layout.configVersion != node->getConfig()->getVersion() || + layout.lastOwnerDirection != ownerDirection; +} + +// A min-content size memoized in this layout holds for the rest of it, and one +// memoized in an earlier layout holds for as long as the node's cached +// measurements do. +static bool hasMemoizedMinContent( + const yoga::Node* const node, + const Dimension axisDimension, + const Direction ownerDirection, + const uint32_t generationCount) { + const uint32_t memoizedGeneration = + node->getLayout().minContentGeneration[yoga::to_underlying(axisDimension)]; + return memoizedGeneration == generationCount || + (memoizedGeneration != 0 && + !isCacheStale(node, ownerDirection, generationCount)); +} + +// A min-content size, and whether a percentage padding or margin resolved +// against the owner's width went into it. +struct MinContentSize { + float size; + bool resolvesAgainstOwnerWidth; +}; + // Returns the min-content size of `node` along `requestedAxis`, used by CSS // Flexbox §4.5 automatic minimum sizing. // @@ -850,13 +885,16 @@ 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). +// would invoke). Like a layout pass, it first clones the children a container +// does not own, since the measure callbacks and the memoized results below +// write to them. // -// 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( +// Results are memoized per node and axis (see `LayoutResults::minContent`) for +// as long as the node's cached measurements hold, so a later layout walks only +// the subtrees that changed: a leaf's measure callback runs again only once the +// leaf is marked dirty, and a container is walked again for a new `ownerWidth` +// only when a percentage padding or margin below it resolves against it. +static MinContentSize computeMinContentMainSize( yoga::Node* const node, const FlexDirection requestedAxis, const Direction ownerDirection, @@ -873,14 +911,16 @@ static float computeMinContentMainSize( const FloatOptional staticMin = wantRow ? node->getMinContentWidth() : node->getMinContentHeight(); if (staticMin.isDefined()) { - return staticMin.unwrap(); + return {staticMin.unwrap(), node->style().hasPercentMarginOrPadding()}; } LayoutResults& layout = node->getLayout(); - const auto axisIndex = yoga::to_underlying(dimension(requestedAxis)); + const Dimension axisDimension = dimension(requestedAxis); + const auto axisIndex = yoga::to_underlying(axisDimension); if (node->hasMeasureFunc()) { - if (layout.minContentGeneration[axisIndex] != generationCount) { + if (!hasMemoizedMinContent( + node, axisDimension, ownerDirection, 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 @@ -906,16 +946,23 @@ static float computeMinContentMainSize( requestedAxis, leafDirection, ownerWidth) + node->style().computeFlexEndPaddingAndBorder( requestedAxis, leafDirection, ownerWidth); - return layout.minContent[axisIndex] + paddingAndBorder; + return { + layout.minContent[axisIndex] + paddingAndBorder, + node->style().hasPercentMarginOrPadding()}; } if (node->getChildCount() == 0) { - return 0.0f; + return {0.0f, node->style().hasPercentMarginOrPadding()}; } - if (layout.minContentGeneration[axisIndex] == generationCount && - yoga::inexactEquals(layout.minContentOwnerWidth[axisIndex], ownerWidth)) { - return layout.minContent[axisIndex]; + if (hasMemoizedMinContent( + node, axisDimension, ownerDirection, generationCount) && + (!layout.minContentResolvesAgainstOwnerWidth[axisIndex] || + yoga::inexactEquals( + layout.minContentOwnerWidth[axisIndex], ownerWidth))) { + return { + layout.minContent[axisIndex], + layout.minContentResolvesAgainstOwnerWidth[axisIndex]}; } const Direction direction = node->resolveDirection(ownerDirection); @@ -926,7 +973,9 @@ static float computeMinContentMainSize( float mainTotal = 0.0f; float crossMax = 0.0f; + bool resolvesAgainstOwnerWidth = node->style().hasPercentMarginOrPadding(); + node->cloneChildrenIfNeeded(); for (size_t i = 0; i < node->getChildCount(); i++) { auto* const child = node->getChild(i); if (child->style().display() == Display::None || @@ -934,27 +983,30 @@ static float computeMinContentMainSize( continue; } - float childMain = computeMinContentMainSize( + const MinContentSize childMain = computeMinContentMainSize( child, nodeMainAxis, direction, ownerWidth, ownerHeight, generationCount); - childMain += child->style().computeMarginForAxis(nodeMainAxis, ownerWidth); - - float childCross = computeMinContentMainSize( + const MinContentSize childCross = computeMinContentMainSize( child, nodeCrossAxis, direction, ownerWidth, ownerHeight, generationCount); - childCross += - child->style().computeMarginForAxis(nodeCrossAxis, ownerWidth); - mainTotal += childMain; - crossMax = std::max(crossMax, childCross); + mainTotal += childMain.size + + child->style().computeMarginForAxis(nodeMainAxis, ownerWidth); + crossMax = std::max( + crossMax, + childCross.size + + child->style().computeMarginForAxis(nodeCrossAxis, ownerWidth)); + resolvesAgainstOwnerWidth = resolvesAgainstOwnerWidth || + childMain.resolvesAgainstOwnerWidth || + childCross.resolvesAgainstOwnerWidth; } mainTotal += node->style().computeFlexStartPaddingAndBorder( @@ -972,8 +1024,10 @@ static float computeMinContentMainSize( const float minContent = wantRow ? widthMin : heightMin; layout.minContent[axisIndex] = minContent; layout.minContentOwnerWidth[axisIndex] = ownerWidth; + layout.minContentResolvesAgainstOwnerWidth[axisIndex] = + resolvesAgainstOwnerWidth; layout.minContentGeneration[axisIndex] = generationCount; - return minContent; + return {minContent, resolvesAgainstOwnerWidth}; } // Computes the CSS Flexbox §4.5 automatic minimum main-axis size for @@ -1037,8 +1091,10 @@ static FloatOptional computeAutoMinMainSize( } // Content size suggestion: probe via min-content recursion. - const FloatOptional contentMain = FloatOptional{computeMinContentMainSize( - child, mainAxis, direction, ownerWidth, ownerHeight, generationCount)}; + const FloatOptional contentMain = FloatOptional{ + computeMinContentMainSize( + child, mainAxis, direction, ownerWidth, ownerHeight, generationCount) + .size}; // Combine per §4.5: floor = min(content, specified) when specified is // definite; otherwise floor = min(content, transferred) when transferred @@ -2740,9 +2796,7 @@ bool calculateLayoutInternal( depth++; const bool needToVisitNode = - (node->isDirty() && layout->generationCount != generationCount) || - layout->configVersion != node->getConfig()->getVersion() || - layout->lastOwnerDirection != ownerDirection; + isCacheStale(node, ownerDirection, generationCount); if (needToVisitNode) { // Invalidate the cached results. @@ -2755,6 +2809,13 @@ bool calculateLayoutInternal( layout->cachedLayout.computedHeight = -1; layout->cachedLayout.ownerWidth = -1; layout->cachedLayout.ownerHeight = -1; + // A min-content size memoized earlier in this layout already reflects the + // change. + for (auto& minContentGeneration : layout->minContentGeneration) { + if (minContentGeneration != generationCount) { + minContentGeneration = 0; + } + } } CachedMeasurement* cachedResults = nullptr; diff --git a/packages/react-native/ReactCommon/yoga/yoga/node/LayoutResults.h b/packages/react-native/ReactCommon/yoga/yoga/node/LayoutResults.h index 5a0111eb74cb..19a7e08cc467 100644 --- a/packages/react-native/ReactCommon/yoga/yoga/node/LayoutResults.h +++ b/packages/react-native/ReactCommon/yoga/yoga/node/LayoutResults.h @@ -35,13 +35,16 @@ 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. + // Memoized min-content sizes for §4.5, indexed by dimension, with the layout + // generation each was computed in (0 for none). Like `cachedMeasurements`, + // they hold until the node, its config or its direction changes. 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` when a percentage padding or margin in + // the subtree it walked resolves against it. std::array minContentGeneration = {}; std::array minContentOwnerWidth = {{YGUndefined, YGUndefined}}; + std::array minContentResolvesAgainstOwnerWidth = {}; std::array minContent = {{YGUndefined, YGUndefined}}; // Instead of recomputing the entire layout every single time, we cache some diff --git a/packages/react-native/ReactCommon/yoga/yoga/style/Style.h b/packages/react-native/ReactCommon/yoga/yoga/style/Style.h index 9ce453b7fc39..858a6f549b9c 100644 --- a/packages/react-native/ReactCommon/yoga/yoga/style/Style.h +++ b/packages/react-native/ReactCommon/yoga/yoga/style/Style.h @@ -346,7 +346,7 @@ class YG_EXPORT Style { // axes, and a percentage size or size bound against the owner's size on its // own axis. bool resolvesAgainstOwnerWidth() const { - return anyIsPercent(margin_) || anyIsPercent(padding_) || + return hasPercentMarginOrPadding() || dimensions_[yoga::to_underlying(Dimension::Width)].isPercent() || minDimensions_[yoga::to_underlying(Dimension::Width)].isPercent() || maxDimensions_[yoga::to_underlying(Dimension::Width)].isPercent(); @@ -358,6 +358,10 @@ class YG_EXPORT Style { maxDimensions_[yoga::to_underlying(Dimension::Height)].isPercent(); } + bool hasPercentMarginOrPadding() const { + return anyIsPercent(margin_) || anyIsPercent(padding_); + } + FloatOptional aspectRatio() const { return pool_.getNumber(aspectRatio_); } diff --git a/private/react-native-fantom/tester/src/stubs/StubComponentRegistryFactory.h b/private/react-native-fantom/tester/src/stubs/StubComponentRegistryFactory.h index b68623060fa9..9fc4f6048568 100644 --- a/private/react-native-fantom/tester/src/stubs/StubComponentRegistryFactory.h +++ b/private/react-native-fantom/tester/src/stubs/StubComponentRegistryFactory.h @@ -16,6 +16,7 @@ #include #include #include +#include #include namespace facebook::react { @@ -27,6 +28,7 @@ inline ComponentRegistryFactory getDefaultComponentRegistryFactory() static auto providerRegistry = []() { auto providerRegistry = std::make_shared(); providerRegistry->add(concreteComponentDescriptorProvider()); + providerRegistry->add(concreteComponentDescriptorProvider()); providerRegistry->add(concreteComponentDescriptorProvider()); providerRegistry->add(concreteComponentDescriptorProvider()); providerRegistry->add(concreteComponentDescriptorProvider()); diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index 675923cec7c0..ff8f03c21353 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -6691,6 +6691,7 @@ class facebook::yoga::Node : public YGNode { class facebook::yoga::Style { public bool flexEndMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; public bool flexStartMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; + public bool hasPercentMarginOrPadding() const; public bool horizontalInsetsDefined() const; public bool inlineEndMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; public bool inlineStartMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; @@ -7217,6 +7218,7 @@ struct facebook::yoga::LayoutResults { public float position(facebook::yoga::PhysicalEdge physicalEdge) const; public float rawDimension(facebook::yoga::Dimension axis) const; public static constexpr int32_t MaxCachedMeasurements; + public std::array minContentResolvesAgainstOwnerWidth; public std::array cachedMeasurements; public std::array minContent; public std::array minContentOwnerWidth; diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api index 0d1f0fd94a91..b2e46bc0fd5a 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api @@ -11150,6 +11150,7 @@ class facebook::yoga::Node : public YGNode { class facebook::yoga::Style { public bool flexEndMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; public bool flexStartMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; + public bool hasPercentMarginOrPadding() const; public bool horizontalInsetsDefined() const; public bool inlineEndMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; public bool inlineStartMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; @@ -11676,6 +11677,7 @@ struct facebook::yoga::LayoutResults { public float position(facebook::yoga::PhysicalEdge physicalEdge) const; public float rawDimension(facebook::yoga::Dimension axis) const; public static constexpr int32_t MaxCachedMeasurements; + public std::array minContentResolvesAgainstOwnerWidth; public std::array cachedMeasurements; public std::array minContent; public std::array minContentOwnerWidth; diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api index 4b15d66312e8..1aac991b1e2c 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api @@ -6540,6 +6540,7 @@ class facebook::yoga::Node : public YGNode { class facebook::yoga::Style { public bool flexEndMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; public bool flexStartMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; + public bool hasPercentMarginOrPadding() const; public bool horizontalInsetsDefined() const; public bool inlineEndMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; public bool inlineStartMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; @@ -7066,6 +7067,7 @@ struct facebook::yoga::LayoutResults { public float position(facebook::yoga::PhysicalEdge physicalEdge) const; public float rawDimension(facebook::yoga::Dimension axis) const; public static constexpr int32_t MaxCachedMeasurements; + public std::array minContentResolvesAgainstOwnerWidth; public std::array cachedMeasurements; public std::array minContent; public std::array minContentOwnerWidth; diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api index 4b15d66312e8..1aac991b1e2c 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -6540,6 +6540,7 @@ class facebook::yoga::Node : public YGNode { class facebook::yoga::Style { public bool flexEndMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; public bool flexStartMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; + public bool hasPercentMarginOrPadding() const; public bool horizontalInsetsDefined() const; public bool inlineEndMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; public bool inlineStartMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; @@ -7066,6 +7067,7 @@ struct facebook::yoga::LayoutResults { public float position(facebook::yoga::PhysicalEdge physicalEdge) const; public float rawDimension(facebook::yoga::Dimension axis) const; public static constexpr int32_t MaxCachedMeasurements; + public std::array minContentResolvesAgainstOwnerWidth; public std::array cachedMeasurements; public std::array minContent; public std::array minContentOwnerWidth; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index cb68d6c58bd7..421f56a3b107 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -8441,6 +8441,7 @@ class facebook::yoga::Node : public YGNode { class facebook::yoga::Style { public bool flexEndMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; public bool flexStartMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; + public bool hasPercentMarginOrPadding() const; public bool horizontalInsetsDefined() const; public bool inlineEndMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; public bool inlineStartMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; @@ -8967,6 +8968,7 @@ struct facebook::yoga::LayoutResults { public float position(facebook::yoga::PhysicalEdge physicalEdge) const; public float rawDimension(facebook::yoga::Dimension axis) const; public static constexpr int32_t MaxCachedMeasurements; + public std::array minContentResolvesAgainstOwnerWidth; public std::array cachedMeasurements; public std::array minContent; public std::array minContentOwnerWidth; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api index b05f7665f7da..f784e978c598 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api @@ -12926,6 +12926,7 @@ class facebook::yoga::Node : public YGNode { class facebook::yoga::Style { public bool flexEndMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; public bool flexStartMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; + public bool hasPercentMarginOrPadding() const; public bool horizontalInsetsDefined() const; public bool inlineEndMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; public bool inlineStartMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; @@ -13452,6 +13453,7 @@ struct facebook::yoga::LayoutResults { public float position(facebook::yoga::PhysicalEdge physicalEdge) const; public float rawDimension(facebook::yoga::Dimension axis) const; public static constexpr int32_t MaxCachedMeasurements; + public std::array minContentResolvesAgainstOwnerWidth; public std::array cachedMeasurements; public std::array minContent; public std::array minContentOwnerWidth; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index 497a3a43a3ee..69b1dddc64b7 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -8293,6 +8293,7 @@ class facebook::yoga::Node : public YGNode { class facebook::yoga::Style { public bool flexEndMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; public bool flexStartMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; + public bool hasPercentMarginOrPadding() const; public bool horizontalInsetsDefined() const; public bool inlineEndMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; public bool inlineStartMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; @@ -8819,6 +8820,7 @@ struct facebook::yoga::LayoutResults { public float position(facebook::yoga::PhysicalEdge physicalEdge) const; public float rawDimension(facebook::yoga::Dimension axis) const; public static constexpr int32_t MaxCachedMeasurements; + public std::array minContentResolvesAgainstOwnerWidth; public std::array cachedMeasurements; public std::array minContent; public std::array minContentOwnerWidth; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index 4f7774be13cd..fe51142d0111 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -8300,6 +8300,7 @@ class facebook::yoga::Node : public YGNode { class facebook::yoga::Style { public bool flexEndMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; public bool flexStartMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; + public bool hasPercentMarginOrPadding() const; public bool horizontalInsetsDefined() const; public bool inlineEndMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; public bool inlineStartMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; @@ -8826,6 +8827,7 @@ struct facebook::yoga::LayoutResults { public float position(facebook::yoga::PhysicalEdge physicalEdge) const; public float rawDimension(facebook::yoga::Dimension axis) const; public static constexpr int32_t MaxCachedMeasurements; + public std::array minContentResolvesAgainstOwnerWidth; public std::array cachedMeasurements; public std::array minContent; public std::array minContentOwnerWidth; diff --git a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api index 507e71d72698..6b6889cb651a 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api @@ -3920,6 +3920,7 @@ class facebook::yoga::Node : public YGNode { class facebook::yoga::Style { public bool flexEndMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; public bool flexStartMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; + public bool hasPercentMarginOrPadding() const; public bool horizontalInsetsDefined() const; public bool inlineEndMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; public bool inlineStartMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; @@ -4446,6 +4447,7 @@ struct facebook::yoga::LayoutResults { public float position(facebook::yoga::PhysicalEdge physicalEdge) const; public float rawDimension(facebook::yoga::Dimension axis) const; public static constexpr int32_t MaxCachedMeasurements; + public std::array minContentResolvesAgainstOwnerWidth; public std::array cachedMeasurements; public std::array minContent; public std::array minContentOwnerWidth; diff --git a/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api index 2772d8e63b9e..b798e96f9f2f 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api @@ -8270,6 +8270,7 @@ class facebook::yoga::Node : public YGNode { class facebook::yoga::Style { public bool flexEndMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; public bool flexStartMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; + public bool hasPercentMarginOrPadding() const; public bool horizontalInsetsDefined() const; public bool inlineEndMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; public bool inlineStartMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; @@ -8796,6 +8797,7 @@ struct facebook::yoga::LayoutResults { public float position(facebook::yoga::PhysicalEdge physicalEdge) const; public float rawDimension(facebook::yoga::Dimension axis) const; public static constexpr int32_t MaxCachedMeasurements; + public std::array minContentResolvesAgainstOwnerWidth; public std::array cachedMeasurements; public std::array minContent; public std::array minContentOwnerWidth; diff --git a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api index 004abd9081df..f99a7be4e026 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api @@ -3913,6 +3913,7 @@ class facebook::yoga::Node : public YGNode { class facebook::yoga::Style { public bool flexEndMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; public bool flexStartMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; + public bool hasPercentMarginOrPadding() const; public bool horizontalInsetsDefined() const; public bool inlineEndMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; public bool inlineStartMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; @@ -4439,6 +4440,7 @@ struct facebook::yoga::LayoutResults { public float position(facebook::yoga::PhysicalEdge physicalEdge) const; public float rawDimension(facebook::yoga::Dimension axis) const; public static constexpr int32_t MaxCachedMeasurements; + public std::array minContentResolvesAgainstOwnerWidth; public std::array cachedMeasurements; public std::array minContent; public std::array minContentOwnerWidth; diff --git a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api index 004abd9081df..f99a7be4e026 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api @@ -3913,6 +3913,7 @@ class facebook::yoga::Node : public YGNode { class facebook::yoga::Style { public bool flexEndMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; public bool flexStartMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; + public bool hasPercentMarginOrPadding() const; public bool horizontalInsetsDefined() const; public bool inlineEndMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; public bool inlineStartMarginIsAuto(facebook::yoga::FlexDirection axis, facebook::yoga::Direction direction) const; @@ -4439,6 +4440,7 @@ struct facebook::yoga::LayoutResults { public float position(facebook::yoga::PhysicalEdge physicalEdge) const; public float rawDimension(facebook::yoga::Dimension axis) const; public static constexpr int32_t MaxCachedMeasurements; + public std::array minContentResolvesAgainstOwnerWidth; public std::array cachedMeasurements; public std::array minContent; public std::array minContentOwnerWidth;