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 fb72421213ab..f7adbe14ef60 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.cpp @@ -480,6 +480,7 @@ void YogaLayoutableShadowNode::updateYogaProps() { void YogaLayoutableShadowNode::configureYogaTree( float pointScaleFactor, + Float fontSizeMultiplier, YGErrata defaultErrata, bool swapLeftAndRight) { ensureUnsealed(); @@ -489,6 +490,18 @@ void YogaLayoutableShadowNode::configureYogaTree( YGConfigSetErrata(&yogaConfig_, errata); YGConfigSetPointScaleFactor(&yogaConfig_, pointScaleFactor); + // A measurable node's measurement depends on `fontSizeMultiplier`, but unlike + // `pointScaleFactor` it is not part of the Yoga config, so a change does not + // invalidate Yoga's layout cache. Dirty the node when it changes to force + // re-measurement. We propagate up to the root so an unchanged, still-cached + // ancestor isn't skipped by `calculateLayoutInternal` before reaching us. + if (ReactNativeFeatureFlags::enableFontScaleChangesUpdatingLayout() && + getTraits().check(ShadowNodeTraits::Trait::MeasurableYogaNode) && + !floatEquality( + getLayoutMetrics().fontSizeMultiplier, fontSizeMultiplier)) { + yogaNode_.markDirtyAndPropagate(); + } + // TODO: `swapLeftAndRight` modified backing props and cannot be undone if (swapLeftAndRight) { swapStyleLeftAndRight(); @@ -507,6 +520,8 @@ void YogaLayoutableShadowNode::configureYogaTree( if (child.yogaTreeHasBeenConfigured_ && childLayoutMetrics.pointScaleFactor == pointScaleFactor && + floatEquality( + childLayoutMetrics.fontSizeMultiplier, fontSizeMultiplier) && childLayoutMetrics.wasLeftAndRightSwapped == swapLeftAndRight && childErrata == child.resolveErrata(errata)) { continue; @@ -515,10 +530,13 @@ void YogaLayoutableShadowNode::configureYogaTree( if (doesOwn(child)) { auto& mutableChild = const_cast(child); mutableChild.configureYogaTree( - pointScaleFactor, child.resolveErrata(errata), swapLeftAndRight); + pointScaleFactor, + fontSizeMultiplier, + child.resolveErrata(errata), + swapLeftAndRight); } else { cloneChildInPlace(i).configureYogaTree( - pointScaleFactor, errata, swapLeftAndRight); + pointScaleFactor, fontSizeMultiplier, errata, swapLeftAndRight); } } } @@ -627,6 +645,7 @@ void YogaLayoutableShadowNode::layoutTree( TraceSection s2("YogaLayoutableShadowNode::configureYogaTree"); configureYogaTree( layoutContext.pointScaleFactor, + layoutContext.fontSizeMultiplier, YGErrataAll /*defaultErrata*/, swapLeftAndRight); } @@ -692,6 +711,7 @@ void YogaLayoutableShadowNode::layoutTree( if (yogaNode_.getHasNewLayout()) { auto layoutMetrics = layoutMetricsFromYogaNode(yogaNode_); layoutMetrics.pointScaleFactor = layoutContext.pointScaleFactor; + layoutMetrics.fontSizeMultiplier = layoutContext.fontSizeMultiplier; layoutMetrics.wasLeftAndRightSwapped = swapLeftAndRight; setLayoutMetrics(layoutMetrics); yogaNode_.setHasNewLayout(false); @@ -739,6 +759,7 @@ void YogaLayoutableShadowNode::layout(LayoutContext layoutContext) { auto newLayoutMetrics = layoutMetricsFromYogaNode(*childYogaNode); newLayoutMetrics.pointScaleFactor = layoutContext.pointScaleFactor; + newLayoutMetrics.fontSizeMultiplier = layoutContext.fontSizeMultiplier; newLayoutMetrics.wasLeftAndRightSwapped = layoutContext.swapLeftAndRightInRTL && newLayoutMetrics.layoutDirection == LayoutDirection::RightToLeft; diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.h b/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.h index 6ec5f43b4b08..905b98ecaa66 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.h @@ -134,7 +134,8 @@ class YogaLayoutableShadowNode : public LayoutableShadowNode { * ShadowTree has been constructed, but before it has been is laid out or * committed. */ - void configureYogaTree(float pointScaleFactor, YGErrata defaultErrata, bool swapLeftAndRight); + void + configureYogaTree(float pointScaleFactor, Float fontSizeMultiplier, YGErrata defaultErrata, bool swapLeftAndRight); /** * Return an errata based on a `layoutConformance` prop if given, otherwise diff --git a/packages/react-native/ReactCommon/react/renderer/core/LayoutMetrics.h b/packages/react-native/ReactCommon/react/renderer/core/LayoutMetrics.h index 794f899eacb6..cc20fae6e0f1 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/LayoutMetrics.h +++ b/packages/react-native/ReactCommon/react/renderer/core/LayoutMetrics.h @@ -40,6 +40,8 @@ struct LayoutMetrics { bool wasLeftAndRightSwapped{false}; // Pixel density. Number of device pixels per density-independent pixel. Float pointScaleFactor{1.0}; + // Surface font scale this node was last laid out with. + Float fontSizeMultiplier{1.0}; // How much the children of the node actually overflow in each direction. // Positive values indicate that children are overflowing outside of the node. // Negative values indicate that children are clipped inside the node @@ -120,6 +122,7 @@ struct hash { layoutMetrics.displayType, layoutMetrics.layoutDirection, layoutMetrics.pointScaleFactor, + layoutMetrics.fontSizeMultiplier, layoutMetrics.overflowInset); } }; diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/SurfaceHandler.cpp b/packages/react-native/ReactCommon/react/renderer/scheduler/SurfaceHandler.cpp index 3d4adc6dbdfe..930472f096e9 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/SurfaceHandler.cpp +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/SurfaceHandler.cpp @@ -9,7 +9,6 @@ #include #include -#include #include namespace facebook::react { @@ -213,78 +212,6 @@ Size SurfaceHandler::measure( return rootShadowNode->getLayoutMetrics().frame.size; } -std::shared_ptr SurfaceHandler::dirtyMeasurableNodesRecursive( - std::shared_ptr node) const { - const auto nodeHasChildren = !node->getChildren().empty(); - const auto isMeasurableYogaNode = - node->getTraits().check(ShadowNodeTraits::Trait::MeasurableYogaNode); - - // Node is not measurable and has no children, its layout will not be affected - if (!nodeHasChildren && !isMeasurableYogaNode) { - return nullptr; - } - - std::shared_ptr>> - newChildren = ShadowNodeFragment::childrenPlaceholder(); - - if (nodeHasChildren) { - std::shared_ptr>> - newChildrenMutable = nullptr; - for (size_t i = 0; i < node->getChildren().size(); i++) { - const auto& child = node->getChildren()[i]; - - if (const auto& layoutableNode = - std::dynamic_pointer_cast( - child)) { - auto newChild = dirtyMeasurableNodesRecursive(layoutableNode); - - if (newChild != nullptr) { - if (newChildrenMutable == nullptr) { - newChildrenMutable = std::make_shared< - std::vector>>( - node->getChildren()); - newChildren = newChildrenMutable; - } - - (*newChildrenMutable)[i] = newChild; - } - } - } - - // Node is not measurable and its children were not dirtied, its layout will - // not be affected - if (!isMeasurableYogaNode && newChildrenMutable == nullptr) { - return nullptr; - } - } - - const auto newNode = node->getComponentDescriptor().cloneShadowNode( - *node, - { - .children = newChildren, - // Preserve the original state of the node - .state = node->getState(), - }); - - if (isMeasurableYogaNode) { - std::static_pointer_cast(newNode)->dirtyLayout(); - } - - return newNode; -} - -void SurfaceHandler::dirtyMeasurableNodes(ShadowNode& root) const { - for (const auto& child : root.getChildren()) { - if (const auto& layoutableNode = - std::dynamic_pointer_cast(child)) { - const auto newChild = dirtyMeasurableNodesRecursive(layoutableNode); - if (newChild != nullptr) { - root.replaceChild(*child, newChild); - } - } - } -} - void SurfaceHandler::constraintLayout( const LayoutConstraints& layoutConstraints, const LayoutContext& layoutContext) const { @@ -315,19 +242,8 @@ void SurfaceHandler::constraintLayout( link_.shadowTree && "`link_.shadowTree` must not be null."); link_.shadowTree->commit( [&](const RootShadowNode& oldRootShadowNode) { - auto newRoot = oldRootShadowNode.clone( + return oldRootShadowNode.clone( propsParserContext, layoutConstraints, layoutContext); - - // Dirty all measurable nodes when the fontSizeMultiplier changes to - // trigger re-measurement. - if (ReactNativeFeatureFlags::enableFontScaleChangesUpdatingLayout() && - layoutContext.fontSizeMultiplier != - oldRootShadowNode.getConcreteProps() - .layoutContext.fontSizeMultiplier) { - dirtyMeasurableNodes(*newRoot); - } - - return newRoot; }, {/* default commit options */}); } diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/SurfaceHandler.h b/packages/react-native/ReactCommon/react/renderer/scheduler/SurfaceHandler.h index b031a51c5455..93cfb22fef84 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/SurfaceHandler.h +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/SurfaceHandler.h @@ -151,12 +151,6 @@ class SurfaceHandler { void applyDisplayMode(DisplayMode displayMode) const; - /* - * An utility for dirtying all measurable shadow nodes present in the tree. - */ - void dirtyMeasurableNodes(ShadowNode &root) const; - std::shared_ptr dirtyMeasurableNodesRecursive(std::shared_ptr node) const; - #pragma mark - Link & Parameters /* diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index 347d20311324..23bd905ad1d1 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -7415,6 +7415,7 @@ struct facebook::react::LayoutMetrics { public facebook::react::EdgeInsets borderWidth; public facebook::react::EdgeInsets contentInsets; public facebook::react::EdgeInsets overflowInset; + public facebook::react::Float fontSizeMultiplier; public facebook::react::Float pointScaleFactor; public facebook::react::LayoutDirection layoutDirection; public facebook::react::PositionType positionType; diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api index c1a244f13f66..124d979c0de7 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api @@ -7198,6 +7198,7 @@ struct facebook::react::LayoutMetrics { public facebook::react::EdgeInsets borderWidth; public facebook::react::EdgeInsets contentInsets; public facebook::react::EdgeInsets overflowInset; + public facebook::react::Float fontSizeMultiplier; public facebook::react::Float pointScaleFactor; public facebook::react::LayoutDirection layoutDirection; public facebook::react::PositionType positionType; diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api index 658f8065fbb0..294cd38c3cc7 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -7406,6 +7406,7 @@ struct facebook::react::LayoutMetrics { public facebook::react::EdgeInsets borderWidth; public facebook::react::EdgeInsets contentInsets; public facebook::react::EdgeInsets overflowInset; + public facebook::react::Float fontSizeMultiplier; public facebook::react::Float pointScaleFactor; public facebook::react::LayoutDirection layoutDirection; public facebook::react::PositionType positionType; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index 9cfe36b68de6..32ccd9253c47 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -9496,6 +9496,7 @@ struct facebook::react::LayoutMetrics { public facebook::react::EdgeInsets borderWidth; public facebook::react::EdgeInsets contentInsets; public facebook::react::EdgeInsets overflowInset; + public facebook::react::Float fontSizeMultiplier; public facebook::react::Float pointScaleFactor; public facebook::react::LayoutDirection layoutDirection; public facebook::react::PositionType positionType; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index 05cedbce3f5a..34d4b58eab60 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -9331,6 +9331,7 @@ struct facebook::react::LayoutMetrics { public facebook::react::EdgeInsets borderWidth; public facebook::react::EdgeInsets contentInsets; public facebook::react::EdgeInsets overflowInset; + public facebook::react::Float fontSizeMultiplier; public facebook::react::Float pointScaleFactor; public facebook::react::LayoutDirection layoutDirection; public facebook::react::PositionType positionType; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index 5b98d91d467f..08e53f0245b0 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -9487,6 +9487,7 @@ struct facebook::react::LayoutMetrics { public facebook::react::EdgeInsets borderWidth; public facebook::react::EdgeInsets contentInsets; public facebook::react::EdgeInsets overflowInset; + public facebook::react::Float fontSizeMultiplier; public facebook::react::Float pointScaleFactor; public facebook::react::LayoutDirection layoutDirection; public facebook::react::PositionType positionType; diff --git a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api index 029bdefe3fa0..8c67922913dd 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api @@ -5603,6 +5603,7 @@ struct facebook::react::LayoutContext { } struct facebook::react::LayoutMetrics { + public Float fontSizeMultiplier; public Float pointScaleFactor; public bool operator==(const facebook::react::LayoutMetrics& rhs) const = default; public bool wasLeftAndRightSwapped; diff --git a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api index bf58f8941d07..a94b56d5c676 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api @@ -5450,6 +5450,7 @@ struct facebook::react::LayoutContext { } struct facebook::react::LayoutMetrics { + public Float fontSizeMultiplier; public Float pointScaleFactor; public bool operator==(const facebook::react::LayoutMetrics& rhs) const = default; public bool wasLeftAndRightSwapped; diff --git a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api index ddeb42c6e2a6..c98085e1106d 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api @@ -5594,6 +5594,7 @@ struct facebook::react::LayoutContext { } struct facebook::react::LayoutMetrics { + public Float fontSizeMultiplier; public Float pointScaleFactor; public bool operator==(const facebook::react::LayoutMetrics& rhs) const = default; public bool wasLeftAndRightSwapped;