From fd0a52ce691efb67dfdd26b8eaf7c5e49f8bff00 Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Tue, 29 Oct 2024 04:27:20 -0700 Subject: [PATCH] Merge RawText sequences Summary: When we have multiple spans of text inside a element, React will emit these as separate RawText ShadowNodes. RawText shadow nodes cannot have any properties beyond the text they contain, yet our current AttributedText logic will generate a separate span for each and duplicate all the relevant properties. This can be particularly inefficient when JSX is used to interpolate strings, e.g. `Example {i}/{count}` results in 4 raw text elements with duplicated properties. Changelog: [General] Improved AttributedText generation for raw text nodes. Reviewed By: NickGerleman Differential Revision: D65134912 --- .../components/text/BaseTextShadowNode.cpp | 27 ++++-- .../text/tests/BaseTextShadowNodeTest.cpp | 93 +++++++++++++++++++ 2 files changed, 111 insertions(+), 9 deletions(-) create mode 100644 packages/react-native/ReactCommon/react/renderer/components/text/tests/BaseTextShadowNodeTest.cpp diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/BaseTextShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/components/text/BaseTextShadowNode.cpp index 2ff46e65dc87..77ede813aedd 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/BaseTextShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/text/BaseTextShadowNode.cpp @@ -29,24 +29,33 @@ void BaseTextShadowNode::buildAttributedString( const ShadowNode& parentNode, AttributedString& outAttributedString, Attachments& outAttachments) { + bool lastFragmentWasRawText = false; for (const auto& childNode : parentNode.getChildren()) { // RawShadowNode auto rawTextShadowNode = dynamic_cast(childNode.get()); if (rawTextShadowNode != nullptr) { - auto fragment = AttributedString::Fragment{}; - fragment.string = rawTextShadowNode->getConcreteProps().text; - fragment.textAttributes = baseTextAttributes; + const auto& rawText = rawTextShadowNode->getConcreteProps().text; + if (lastFragmentWasRawText) { + outAttributedString.getFragments().back().string += rawText; + } else { + auto fragment = AttributedString::Fragment{}; + fragment.string = rawText; + fragment.textAttributes = baseTextAttributes; - // Storing a retaining pointer to `ParagraphShadowNode` inside - // `attributedString` causes a retain cycle (besides that fact that we - // don't need it at all). Storing a `ShadowView` instance instead of - // `ShadowNode` should properly fix this problem. - fragment.parentShadowView = shadowViewFromShadowNode(parentNode); - outAttributedString.appendFragment(fragment); + // Storing a retaining pointer to `ParagraphShadowNode` inside + // `attributedString` causes a retain cycle (besides that fact that we + // don't need it at all). Storing a `ShadowView` instance instead of + // `ShadowNode` should properly fix this problem. + fragment.parentShadowView = shadowViewFromShadowNode(parentNode); + outAttributedString.appendFragment(fragment); + lastFragmentWasRawText = true; + } continue; } + lastFragmentWasRawText = false; + // TextShadowNode auto textShadowNode = dynamic_cast(childNode.get()); if (textShadowNode != nullptr) { diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/tests/BaseTextShadowNodeTest.cpp b/packages/react-native/ReactCommon/react/renderer/components/text/tests/BaseTextShadowNodeTest.cpp new file mode 100644 index 000000000000..6a9e4858d1f4 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/text/tests/BaseTextShadowNodeTest.cpp @@ -0,0 +1,93 @@ +/* + * 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 + +#include +#include +#include + +namespace facebook::react { + +namespace { + +Element rawTextElement(const char* text) { + auto rawTextProps = std::make_shared(); + rawTextProps->text = text; + return Element().props(rawTextProps); +} + +} // namespace + +TEST(BaseTextShadowNodeTest, fragmentsWithDifferentAttributes) { + ContextContainer contextContainer{}; + PropsParserContext parserContext{-1, contextContainer}; + + auto builder = simpleComponentBuilder(); + auto shadowNode = builder.build(Element().children({ + Element() + .props([]() { + auto props = std::make_shared(); + props->textAttributes.fontSize = 12; + return props; + }) + .children({ + rawTextElement("First fragment. "), + }), + Element() + .props([]() { + auto props = std::make_shared(); + props->textAttributes.fontSize = 24; + return props; + }) + .children({ + rawTextElement("Second fragment"), + }), + })); + + auto baseTextAttributes = TextAttributes::defaultTextAttributes(); + AttributedString output; + BaseTextShadowNode::Attachments attachments; + BaseTextShadowNode::buildAttributedString( + baseTextAttributes, *shadowNode, output, attachments); + + EXPECT_EQ(output.getString(), "First fragment. Second fragment"); + + const auto& fragments = output.getFragments(); + EXPECT_EQ(fragments.size(), 2); + EXPECT_EQ(fragments[0].textAttributes.fontSize, 12); + EXPECT_EQ( + fragments[0].parentShadowView.tag, + shadowNode->getChildren()[0]->getTag()); + EXPECT_EQ(fragments[1].textAttributes.fontSize, 24); + EXPECT_EQ( + fragments[1].parentShadowView.tag, + shadowNode->getChildren()[1]->getTag()); +} + +TEST(BaseTextShadowNodeTest, rawTextIsMerged) { + ContextContainer contextContainer{}; + PropsParserContext parserContext{-1, contextContainer}; + + auto builder = simpleComponentBuilder(); + auto shadowNode = builder.build(Element().children({ + rawTextElement("Hello "), + rawTextElement("World"), + })); + + auto baseTextAttributes = TextAttributes::defaultTextAttributes(); + AttributedString output; + BaseTextShadowNode::Attachments attachments; + BaseTextShadowNode::buildAttributedString( + baseTextAttributes, *shadowNode, output, attachments); + + EXPECT_EQ(output.getString(), "Hello World"); + EXPECT_EQ(output.getFragments().size(), 1); +} + +} // namespace facebook::react