diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp index 7602b616e798..a14ce0a01e94 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp @@ -59,6 +59,23 @@ ParagraphShadowNode::ParagraphShadowNode( const ShadowNodeFragment& fragment) : ConcreteViewShadowNode(sourceShadowNode, fragment) { initialize(); + // A revision with neither new props nor new children (e.g. one Yoga cloned + // because a sibling changed) renders the same content, so keep the + // attributed string and the layouts measured for it instead of rebuilding + // and re-measuring them. `getContent` drops both if the layout context they + // were built for no longer matches. + // + // Content with attachments is not kept: they point at the children the + // content was built from, and `layout` replaces those children with laid-out + // clones, so in a later revision the pointers would outlive the nodes. + if (fragment.props == nullptr && fragment.children == nullptr) { + const auto& source = + static_cast(sourceShadowNode); + if (source.content_.has_value() && source.content_->attachments.empty()) { + content_ = source.content_; + measuredLayouts_ = source.measuredLayouts_; + } + } } bool ParagraphShadowNode::shouldNewRevisionDirtyMeasurement( @@ -69,8 +86,26 @@ bool ParagraphShadowNode::shouldNewRevisionDirtyMeasurement( const Content& ParagraphShadowNode::getContent( const LayoutContext& layoutContext) const { + auto layoutDirection = YGNodeLayoutGetDirection(&yogaNode_) == YGDirectionRTL + ? LayoutDirection::RightToLeft + : LayoutDirection::LeftToRight; if (content_.has_value()) { - return content_.value(); + const auto& baseTextAttributes = + content_->attributedString.getBaseTextAttributes(); + // The content is built from this node's props and children, and it is + // only carried over to revisions that have neither new props nor new + // children (see the copy constructor). The only other inputs are the + // font size multiplier from the layout context and the layout direction + // Yoga resolves from the parent, and those can change without new props + // or children, so they are the two things to check here. + if (baseTextAttributes.fontSizeMultiplier == + layoutContext.fontSizeMultiplier && + baseTextAttributes.layoutDirection == layoutDirection) { + return content_.value(); + } + // Carried over from a previous revision, but built for another context. + content_.reset(); + measuredLayouts_.clear(); } ensureUnsealed(); @@ -78,10 +113,7 @@ const Content& ParagraphShadowNode::getContent( auto textAttributes = TextAttributes::defaultTextAttributes(); textAttributes.fontSizeMultiplier = layoutContext.fontSizeMultiplier; textAttributes.apply(getConcreteProps().textAttributes); - textAttributes.layoutDirection = - YGNodeLayoutGetDirection(&yogaNode_) == YGDirectionRTL - ? LayoutDirection::RightToLeft - : LayoutDirection::LeftToRight; + textAttributes.layoutDirection = layoutDirection; auto attributedString = AttributedString{}; auto attachments = Attachments{}; buildAttributedString(textAttributes, *this, attributedString, attachments); @@ -189,7 +221,8 @@ void ParagraphShadowNode::updateStateIfNeeded(const Content& content) { textLayoutManager_}); } -MeasuredPreparedTextLayout* ParagraphShadowNode::findUsableLayout() { +MeasuredPreparedTextLayout* ParagraphShadowNode::findUsableLayout( + Float pointScaleFactor) { MeasuredPreparedTextLayout* ret = nullptr; if constexpr (TextLayoutManagerExtended::supportsPreparedTextLayout()) { @@ -201,7 +234,8 @@ MeasuredPreparedTextLayout* ParagraphShadowNode::findUsableLayout() { // adjusted by a constraint (like min-size). auto expectedSize = rawContentSize(); for (auto& prevLayout : measuredLayouts_) { - if (floatEquality( + if (prevLayout.pointScaleFactor == pointScaleFactor && + floatEquality( prevLayout.measurement.size.width, expectedSize.width) && floatEquality( prevLayout.measurement.size.height, expectedSize.height)) { @@ -230,8 +264,11 @@ Size ParagraphShadowNode::measureContent( const LayoutContext& layoutContext, const LayoutConstraints& layoutConstraints) const { if constexpr (TextLayoutManagerExtended::supportsPreparedTextLayout()) { + // Layouts carried over from a previous revision may have been prepared + // at another pixel density, which they can't be reused for. for (const auto& layout : measuredLayouts_) { - if (layout.layoutConstraints == layoutConstraints) { + if (layout.layoutConstraints == layoutConstraints && + layout.pointScaleFactor == layoutContext.pointScaleFactor) { return layout.measurement.size; } } @@ -321,7 +358,7 @@ void ParagraphShadowNode::layout(LayoutContext layoutContext) { auto content = getContentWithMeasuredAttachments(layoutContext, layoutConstraints); - auto measuredLayout = findUsableLayout(); + auto measuredLayout = findUsableLayout(layoutContext.pointScaleFactor); if constexpr ( TextLayoutManagerExtended::supportsPreparedTextLayout() && @@ -338,7 +375,7 @@ void ParagraphShadowNode::layout(LayoutContext layoutContext) { // culled if (measuredLayout == nullptr) { measureContent(layoutContext, layoutConstraints); - measuredLayout = findUsableLayout(); + measuredLayout = findUsableLayout(layoutContext.pointScaleFactor); } react_native_assert(measuredLayout); updateStateIfNeeded(content, *measuredLayout); diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.h b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.h index 5d0537d363df..6378b268d3df 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.h +++ b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.h @@ -117,9 +117,10 @@ class ParagraphShadowNode /** * Returns any previously prepared layout, generated for measurement, which - * may be used, given the currently assigned layout to the ShadowNode + * may be used, given the currently assigned layout to the ShadowNode and + * the current pixel density */ - MeasuredPreparedTextLayout *findUsableLayout(); + MeasuredPreparedTextLayout *findUsableLayout(Float pointScaleFactor); /** * Returns the final measure of the content frame, before layout rounding