From f41d855638276a68797e1e40f9fd2a82dde605c1 Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Thu, 3 Oct 2024 08:43:00 -0700 Subject: [PATCH] Keep base TextAttributes as part of AttributedString (#46612) Summary: This information is lost by the time the AttributedString makes its way to be measured. Pretty sure I've seen code do incorrect heuristics like grabbing first span for this (IIRC adjustFontSizeToFit does this)? Another case where we hit this on Android is when the last character is a newline (which is part of the Spannable), and layout needs to predict the size of the line which will come next (where it uses the EditText's underlying TextPaint, who is using these base attributes). Changelog: [Internal] Reviewed By: sammy-SC Differential Revision: D63303709 --- .../react/renderer/attributedstring/AttributedString.cpp | 9 +++++++++ .../react/renderer/attributedstring/AttributedString.h | 9 +++++++++ .../react/renderer/attributedstring/conversions.h | 4 ++++ .../renderer/components/text/ParagraphShadowNode.cpp | 1 + .../androidtextinput/AndroidTextInputShadowNode.cpp | 1 + .../components/iostextinput/TextInputShadowNode.cpp | 1 + 6 files changed, 25 insertions(+) diff --git a/packages/react-native/ReactCommon/react/renderer/attributedstring/AttributedString.cpp b/packages/react-native/ReactCommon/react/renderer/attributedstring/AttributedString.cpp index d90407738aab04..6ce203b415c520 100644 --- a/packages/react-native/ReactCommon/react/renderer/attributedstring/AttributedString.cpp +++ b/packages/react-native/ReactCommon/react/renderer/attributedstring/AttributedString.cpp @@ -91,6 +91,11 @@ void AttributedString::prependAttributedString( attributedString.fragments_.end()); } +void AttributedString::setBaseTextAttributes( + const TextAttributes& defaultAttributes) { + baseAttributes_ = defaultAttributes; +} + const Fragments& AttributedString::getFragments() const { return fragments_; } @@ -107,6 +112,10 @@ std::string AttributedString::getString() const { return string; } +const TextAttributes& AttributedString::getBaseTextAttributes() const { + return baseAttributes_; +} + bool AttributedString::isEmpty() const { return fragments_.empty(); } diff --git a/packages/react-native/ReactCommon/react/renderer/attributedstring/AttributedString.h b/packages/react-native/ReactCommon/react/renderer/attributedstring/AttributedString.h index 45f351ce18a411..4499aefad453ed 100644 --- a/packages/react-native/ReactCommon/react/renderer/attributedstring/AttributedString.h +++ b/packages/react-native/ReactCommon/react/renderer/attributedstring/AttributedString.h @@ -75,6 +75,12 @@ class AttributedString : public Sealable, public DebugStringConvertible { void appendAttributedString(const AttributedString& attributedString); void prependAttributedString(const AttributedString& attributedString); + /* + * Sets attributes which would apply to hypothetical text not included in the + * AttributedString. + */ + void setBaseTextAttributes(const TextAttributes& defaultAttributes); + /* * Returns a read-only reference to a list of fragments. */ @@ -90,6 +96,8 @@ class AttributedString : public Sealable, public DebugStringConvertible { */ std::string getString() const; + const TextAttributes& getBaseTextAttributes() const; + /* * Returns `true` if the string is empty (has no any fragments). */ @@ -113,6 +121,7 @@ class AttributedString : public Sealable, public DebugStringConvertible { private: Fragments fragments_; + TextAttributes baseAttributes_; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h b/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h index 8e365c52523f9e..de8676939e4630 100644 --- a/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h +++ b/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h @@ -871,6 +871,7 @@ constexpr static MapBuffer::Key AS_KEY_HASH = 0; constexpr static MapBuffer::Key AS_KEY_STRING = 1; constexpr static MapBuffer::Key AS_KEY_FRAGMENTS = 2; constexpr static MapBuffer::Key AS_KEY_CACHE_ID = 3; +constexpr static MapBuffer::Key AS_KEY_BASE_ATTRIBUTES = 4; // constants for Fragment serialization constexpr static MapBuffer::Key FR_KEY_STRING = 0; @@ -1117,6 +1118,9 @@ inline MapBuffer toMapBuffer(const AttributedString& attributedString) { // TODO: This truncates half the hash builder.putInt(AS_KEY_HASH, static_cast(hash)); builder.putString(AS_KEY_STRING, attributedString.getString()); + builder.putMapBuffer( + AS_KEY_BASE_ATTRIBUTES, + toMapBuffer(attributedString.getBaseTextAttributes())); auto fragmentsMap = fragmentsBuilder.build(); builder.putMapBuffer(AS_KEY_FRAGMENTS, fragmentsMap); return builder.build(); 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 95b2e2de19f71f..3be6198d73cd77 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp @@ -58,6 +58,7 @@ const Content& ParagraphShadowNode::getContent( auto attributedString = AttributedString{}; auto attachments = Attachments{}; buildAttributedString(textAttributes, *this, attributedString, attachments); + attributedString.setBaseTextAttributes(textAttributes); content_ = Content{ attributedString, getConcreteProps().paragraphAttributes, attachments}; diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/android/react/renderer/components/androidtextinput/AndroidTextInputShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/android/react/renderer/components/androidtextinput/AndroidTextInputShadowNode.cpp index 694b789ba17dfd..5c2b81d86c49cc 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/android/react/renderer/components/androidtextinput/AndroidTextInputShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/android/react/renderer/components/androidtextinput/AndroidTextInputShadowNode.cpp @@ -64,6 +64,7 @@ AttributedString AndroidTextInputShadowNode::getAttributedString() const { auto attachments = BaseTextShadowNode::Attachments{}; BaseTextShadowNode::buildAttributedString( childTextAttributes, *this, attributedString, attachments); + attributedString.setBaseTextAttributes(childTextAttributes); // BaseTextShadowNode only gets children. We must detect and prepend text // value attributes manually. diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputShadowNode.cpp index b6c7a889093f59..b0a58614ac8725 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputShadowNode.cpp @@ -88,6 +88,7 @@ AttributedString TextInputShadowNode::getAttributedString( auto attachments = Attachments{}; BaseTextShadowNode::buildAttributedString( textAttributes, *this, attributedString, attachments); + attributedString.setBaseTextAttributes(textAttributes); return attributedString; }