From d41e95fb1a75514a10434b9dc39ba14979faf8bd Mon Sep 17 00:00:00 2001 From: Samuel Susla Date: Wed, 8 Mar 2023 08:36:08 -0800 Subject: [PATCH] Cache NSTextStorage Summary: changelog: [internal] This diff introduces a mechanism to cache NSTextStorage on Paragraph's state. The old renderer already has caching for NSTextStorage: https://github.com/facebook/react-native/blob/main/Libraries/Text/Text/RCTTextShadowView.m#L21 Fabric will store `NSTextStorage` inside `ParagraphLayoutManager` which is owned by state. There is one notable change which is not gated: Paragraph's state strongly owns `TextLayoutManager`, not `ParagraphShadowNode` like previously. This shouldn't change anything Reviewed By: mdvacca Differential Revision: D43692171 fbshipit-source-id: efe6077222a30ab6d89abd9916534b9a96e745d4 --- .../Text/RCTParagraphComponentView.mm | 37 +++++------ React/Fabric/RCTSurfacePresenter.mm | 4 ++ .../text/ParagraphLayoutManager.cpp | 59 ++++++++++++++++++ .../components/text/ParagraphLayoutManager.h | 59 ++++++++++++++++++ .../components/text/ParagraphShadowNode.cpp | 23 +++---- .../components/text/ParagraphShadowNode.h | 7 +-- .../renderer/components/text/ParagraphState.h | 19 +++--- .../AndroidTextInputShadowNode.cpp | 3 +- .../iostextinput/TextInputShadowNode.cpp | 3 +- .../react/renderer/core/CoreFeatures.cpp | 1 + .../react/renderer/core/CoreFeatures.h | 5 ++ .../textlayoutmanager/TextMeasureCache.h | 1 + .../textlayoutmanager/TextLayoutManager.cpp | 11 +++- .../textlayoutmanager/TextLayoutManager.h | 8 ++- .../platform/cxx/TextLayoutManager.cpp | 10 ++- .../platform/cxx/TextLayoutManager.h | 8 ++- .../textlayoutmanager/RCTTextLayoutManager.h | 13 +++- .../textlayoutmanager/RCTTextLayoutManager.mm | 61 ++++++++++++------- .../textlayoutmanager/TextLayoutManager.h | 8 ++- .../textlayoutmanager/TextLayoutManager.mm | 29 +++++++-- 20 files changed, 279 insertions(+), 90 deletions(-) create mode 100644 ReactCommon/react/renderer/components/text/ParagraphLayoutManager.cpp create mode 100644 ReactCommon/react/renderer/components/text/ParagraphLayoutManager.h diff --git a/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm b/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm index 5903f966375..ac45711c97b 100644 --- a/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm +++ b/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm @@ -116,11 +116,8 @@ using namespace facebook::react; return; } - auto textLayoutManager = _state->getData().layoutManager.lock(); - - if (!textLayoutManager) { - return; - } + auto textLayoutManager = _state->getData().paragraphLayoutManager.getTextLayoutManager(); + auto nsTextStorage = _state->getData().paragraphLayoutManager.getHostTextStorage(); RCTTextLayoutManager *nativeTextLayoutManager = (RCTTextLayoutManager *)unwrapManagedObject(textLayoutManager->getNativeTextLayoutManager()); @@ -129,7 +126,8 @@ using namespace facebook::react; [nativeTextLayoutManager drawAttributedString:_state->getData().attributedString paragraphAttributes:_paragraphAttributes - frame:frame]; + frame:frame + textStorage:unwrapManagedObject(nsTextStorage)]; } #pragma mark - Accessibility @@ -161,18 +159,15 @@ using namespace facebook::react; auto &data = _state->getData(); if (![_accessibilityProvider isUpToDate:data.attributedString]) { - auto textLayoutManager = data.layoutManager.lock(); - if (textLayoutManager) { - RCTTextLayoutManager *nativeTextLayoutManager = - (RCTTextLayoutManager *)unwrapManagedObject(textLayoutManager->getNativeTextLayoutManager()); - CGRect frame = RCTCGRectFromRect(_layoutMetrics.getContentFrame()); - _accessibilityProvider = - [[RCTParagraphComponentAccessibilityProvider alloc] initWithString:data.attributedString - layoutManager:nativeTextLayoutManager - paragraphAttributes:data.paragraphAttributes - frame:frame - view:self]; - } + auto textLayoutManager = data.paragraphLayoutManager.getTextLayoutManager(); + RCTTextLayoutManager *nativeTextLayoutManager = + (RCTTextLayoutManager *)unwrapManagedObject(textLayoutManager->getNativeTextLayoutManager()); + CGRect frame = RCTCGRectFromRect(_layoutMetrics.getContentFrame()); + _accessibilityProvider = [[RCTParagraphComponentAccessibilityProvider alloc] initWithString:data.attributedString + layoutManager:nativeTextLayoutManager + paragraphAttributes:data.paragraphAttributes + frame:frame + view:self]; } return _accessibilityProvider.accessibilityElements; @@ -191,11 +186,7 @@ using namespace facebook::react; return _eventEmitter; } - auto textLayoutManager = _state->getData().layoutManager.lock(); - - if (!textLayoutManager) { - return _eventEmitter; - } + auto textLayoutManager = _state->getData().paragraphLayoutManager.getTextLayoutManager(); RCTTextLayoutManager *nativeTextLayoutManager = (RCTTextLayoutManager *)unwrapManagedObject(textLayoutManager->getNativeTextLayoutManager()); diff --git a/React/Fabric/RCTSurfacePresenter.mm b/React/Fabric/RCTSurfacePresenter.mm index 23ec85a5111..d52cbdfe763 100644 --- a/React/Fabric/RCTSurfacePresenter.mm +++ b/React/Fabric/RCTSurfacePresenter.mm @@ -281,6 +281,10 @@ static BackgroundExecutor RCTGetBackgroundExecutor() CoreFeatures::useNativeState = true; } + if (reactNativeConfig && reactNativeConfig->getBool("react_fabric:enable_nstextstorage_caching")) { + CoreFeatures::cacheNSTextStorage = true; + } + auto componentRegistryFactory = [factory = wrapManagedObject(_mountingManager.componentViewRegistry.componentViewFactory)]( EventDispatcher::Weak const &eventDispatcher, ContextContainer::Shared const &contextContainer) { diff --git a/ReactCommon/react/renderer/components/text/ParagraphLayoutManager.cpp b/ReactCommon/react/renderer/components/text/ParagraphLayoutManager.cpp new file mode 100644 index 00000000000..fb55485d037 --- /dev/null +++ b/ReactCommon/react/renderer/components/text/ParagraphLayoutManager.cpp @@ -0,0 +1,59 @@ +/* + * 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 "ParagraphLayoutManager.h" +#include +#include + +namespace facebook::react { + +TextMeasurement ParagraphLayoutManager::measure( + AttributedString const &attributedString, + ParagraphAttributes const ¶graphAttributes, + LayoutConstraints layoutConstraints) const { + if (CoreFeatures::cacheNSTextStorage) { + size_t newHash = folly::hash::hash_combine( + 0, + textAttributedStringHashLayoutWise(attributedString), + paragraphAttributes); + + if (!hostTextStorage_ || newHash != hash_) { + hostTextStorage_ = textLayoutManager_->getHostTextStorage( + attributedString, paragraphAttributes, layoutConstraints); + hash_ = newHash; + } + } + + return textLayoutManager_->measure( + AttributedStringBox(attributedString), + paragraphAttributes, + layoutConstraints, + hostTextStorage_); +} + +LinesMeasurements ParagraphLayoutManager::measureLines( + AttributedString const &attributedString, + ParagraphAttributes const ¶graphAttributes, + Size size) const { + return textLayoutManager_->measureLines( + attributedString, paragraphAttributes, size); +} + +void ParagraphLayoutManager::setTextLayoutManager( + std::shared_ptr textLayoutManager) const { + textLayoutManager_ = std::move(textLayoutManager); +} + +std::shared_ptr +ParagraphLayoutManager::getTextLayoutManager() const { + return textLayoutManager_; +} + +std::shared_ptr ParagraphLayoutManager::getHostTextStorage() const { + return hostTextStorage_; +} +} // namespace facebook::react diff --git a/ReactCommon/react/renderer/components/text/ParagraphLayoutManager.h b/ReactCommon/react/renderer/components/text/ParagraphLayoutManager.h new file mode 100644 index 00000000000..c5ed8b7f5b8 --- /dev/null +++ b/ReactCommon/react/renderer/components/text/ParagraphLayoutManager.h @@ -0,0 +1,59 @@ +/* + * 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. + */ + +#pragma once + +#include +#include +#include +#include + +namespace facebook::react { + +/* + * Serves as a middle man between `ParagraphShadowNode` and `TextLayoutManager`. + * On iOS, caches `NSTextStorage` for individual `ParagraphShadowNode` to make + * sure only one `NSTextStorage` is created for every string. `NSTextStorage` + * can be re created on native views layer but it is expensive. On Android, this + * class does not cache anything. + */ +class ParagraphLayoutManager { + public: + TextMeasurement measure( + AttributedString const &attributedString, + ParagraphAttributes const ¶graphAttributes, + LayoutConstraints layoutConstraints) const; + + LinesMeasurements measureLines( + AttributedString const &attributedString, + ParagraphAttributes const ¶graphAttributes, + Size size) const; + + void setTextLayoutManager( + std::shared_ptr textLayoutManager) const; + + /* + * Returns an opaque pointer to platform-specific `TextLayoutManager`. + * Is used on a native views layer to delegate text rendering to the manager. + */ + std::shared_ptr getTextLayoutManager() const; + + /* + * Returns opaque shared_ptr holding `NSTextStorage`. + * May be nullptr. + * Is used on a native views layer to prevent `NSTextStorage` from being + * created twice. + */ + std::shared_ptr getHostTextStorage() const; + + private: + std::shared_ptr mutable textLayoutManager_{}; + std::shared_ptr mutable hostTextStorage_{}; + + size_t mutable hash_{}; +}; +} // namespace facebook::react diff --git a/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp b/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp index 7efe83c9e3e..17cb7dd34ad 100644 --- a/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp +++ b/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp @@ -95,7 +95,8 @@ Content ParagraphShadowNode::getContentWithMeasuredAttachments( void ParagraphShadowNode::setTextLayoutManager( std::shared_ptr textLayoutManager) { ensureUnsealed(); - textLayoutManager_ = std::move(textLayoutManager); + getStateData().paragraphLayoutManager.setTextLayoutManager( + std::move(textLayoutManager)); } void ParagraphShadowNode::updateStateIfNeeded(Content const &content) { @@ -103,7 +104,7 @@ void ParagraphShadowNode::updateStateIfNeeded(Content const &content) { auto &state = getStateData(); - react_native_assert(textLayoutManager_); + react_native_assert(state.paragraphLayoutManager.getTextLayoutManager()); if (state.attributedString == content.attributedString) { return; @@ -112,7 +113,7 @@ void ParagraphShadowNode::updateStateIfNeeded(Content const &content) { setStateData(ParagraphState{ content.attributedString, content.paragraphAttributes, - textLayoutManager_}); + state.paragraphLayoutManager}); } #pragma mark - LayoutableShadowNode @@ -136,11 +137,9 @@ Size ParagraphShadowNode::measureContent( attributedString.appendFragment({string, textAttributes, {}}); } - return textLayoutManager_ - ->measure( - AttributedStringBox{attributedString}, - content.paragraphAttributes, - layoutConstraints) + return getStateData() + .paragraphLayoutManager + .measure(attributedString, content.paragraphAttributes, layoutConstraints) .size; } @@ -157,13 +156,11 @@ void ParagraphShadowNode::layout(LayoutContext layoutContext) { updateStateIfNeeded(content); - auto measurement = textLayoutManager_->measure( - AttributedStringBox{content.attributedString}, - content.paragraphAttributes, - layoutConstraints); + auto measurement = getStateData().paragraphLayoutManager.measure( + content.attributedString, content.paragraphAttributes, layoutConstraints); if (getConcreteProps().onTextLayout) { - auto linesMeasurements = textLayoutManager_->measureLines( + auto linesMeasurements = getStateData().paragraphLayoutManager.measureLines( content.attributedString, content.paragraphAttributes, measurement.size); diff --git a/ReactCommon/react/renderer/components/text/ParagraphShadowNode.h b/ReactCommon/react/renderer/components/text/ParagraphShadowNode.h index 7a9e3adfa5b..b24063766e6 100644 --- a/ReactCommon/react/renderer/components/text/ParagraphShadowNode.h +++ b/ReactCommon/react/renderer/components/text/ParagraphShadowNode.h @@ -7,15 +7,14 @@ #pragma once +#include #include +#include #include #include -#include #include -#include #include #include -#include namespace facebook { namespace react { @@ -96,8 +95,6 @@ class ParagraphShadowNode final : public ConcreteViewShadowNode< */ void updateStateIfNeeded(Content const &content); - std::shared_ptr textLayoutManager_; - /* * Cached content of the subtree started from the node. */ diff --git a/ReactCommon/react/renderer/components/text/ParagraphState.h b/ReactCommon/react/renderer/components/text/ParagraphState.h index 24a8e1258e8..f5a0408b938 100644 --- a/ReactCommon/react/renderer/components/text/ParagraphState.h +++ b/ReactCommon/react/renderer/components/text/ParagraphState.h @@ -10,15 +10,14 @@ #include #include #include -#include +#include #ifdef ANDROID #include #include #endif -namespace facebook { -namespace react { +namespace facebook::react { #ifdef ANDROID // constants for Text State serialization @@ -33,8 +32,7 @@ constexpr static MapBuffer::Key TX_STATE_KEY_MOST_RECENT_EVENT_COUNT = 3; * State for component. * Represents what to render and how to render. */ -class ParagraphState final { - public: +struct ParagraphState { /* * All content of component represented as an `AttributedString`. */ @@ -47,22 +45,22 @@ class ParagraphState final { ParagraphAttributes paragraphAttributes; /* - * `TextLayoutManager` provides a connection to platform-specific + * `ParagraphLayoutManager` provides a connection to platform-specific * text rendering infrastructure which is capable to render the * `AttributedString`. * This is not on every platform. This is not used on Android, but is * used on the iOS mounting layer. */ - std::weak_ptr layoutManager; + ParagraphLayoutManager paragraphLayoutManager; #ifdef ANDROID ParagraphState( AttributedString const &attributedString, ParagraphAttributes const ¶graphAttributes, - std::weak_ptr const &layoutManager) + ParagraphLayoutManager const ¶graphLayoutManager) : attributedString(attributedString), paragraphAttributes(paragraphAttributes), - layoutManager(layoutManager) {} + paragraphLayoutManager(paragraphLayoutManager) {} ParagraphState() = default; ParagraphState( ParagraphState const &previousState, @@ -74,5 +72,4 @@ class ParagraphState final { #endif }; -} // namespace react -} // namespace facebook +} // namespace facebook::react diff --git a/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputShadowNode.cpp b/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputShadowNode.cpp index 2c9a4a58186..7bd83c701bf 100644 --- a/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputShadowNode.cpp +++ b/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputShadowNode.cpp @@ -197,7 +197,8 @@ Size AndroidTextInputShadowNode::measureContent( ->measure( AttributedStringBox{attributedString}, getConcreteProps().paragraphAttributes, - layoutConstraints) + layoutConstraints, + nullptr) .size; } diff --git a/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputShadowNode.cpp b/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputShadowNode.cpp index b35b802f60f..6e4771a94f8 100644 --- a/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputShadowNode.cpp +++ b/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputShadowNode.cpp @@ -110,7 +110,8 @@ Size TextInputShadowNode::measureContent( ->measure( attributedStringBoxToMeasure(layoutContext), getConcreteProps().getEffectiveParagraphAttributes(), - layoutConstraints) + layoutConstraints, + nullptr) .size; } diff --git a/ReactCommon/react/renderer/core/CoreFeatures.cpp b/ReactCommon/react/renderer/core/CoreFeatures.cpp index 8685a1890ad..129bc7c2a68 100644 --- a/ReactCommon/react/renderer/core/CoreFeatures.cpp +++ b/ReactCommon/react/renderer/core/CoreFeatures.cpp @@ -14,6 +14,7 @@ bool CoreFeatures::enablePropIteratorSetter = false; bool CoreFeatures::enableMapBuffer = false; bool CoreFeatures::blockPaintForUseLayoutEffect = false; bool CoreFeatures::useNativeState = false; +bool CoreFeatures::cacheNSTextStorage = false; } // namespace react } // namespace facebook diff --git a/ReactCommon/react/renderer/core/CoreFeatures.h b/ReactCommon/react/renderer/core/CoreFeatures.h index 3064784eea2..a3a9625e82b 100644 --- a/ReactCommon/react/renderer/core/CoreFeatures.h +++ b/ReactCommon/react/renderer/core/CoreFeatures.h @@ -34,6 +34,11 @@ class CoreFeatures { // Whether to use Hermes' NativeState instead of HostObject // in simple data passing scenarios with JS static bool useNativeState; + + // Creating NSTextStorage is relatively expensive operation and we were + // creating it twice. Once when measuring text and once when rendering it. + // This flag caches it inside ParagraphState. + static bool cacheNSTextStorage; }; } // namespace react diff --git a/ReactCommon/react/renderer/textlayoutmanager/TextMeasureCache.h b/ReactCommon/react/renderer/textlayoutmanager/TextMeasureCache.h index 23486173c68..6eb4cdcf1bf 100644 --- a/ReactCommon/react/renderer/textlayoutmanager/TextMeasureCache.h +++ b/ReactCommon/react/renderer/textlayoutmanager/TextMeasureCache.h @@ -7,6 +7,7 @@ #pragma once +#include #include #include #include diff --git a/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp b/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp index fa51c0a92c2..a8b186a5eb8 100644 --- a/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp +++ b/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp @@ -154,8 +154,9 @@ void *TextLayoutManager::getNativeTextLayoutManager() const { TextMeasurement TextLayoutManager::measure( AttributedStringBox const &attributedStringBox, - ParagraphAttributes paragraphAttributes, - LayoutConstraints layoutConstraints) const { + ParagraphAttributes const ¶graphAttributes, + LayoutConstraints layoutConstraints, + std::shared_ptr /* hostTextStorage */) const { auto &attributedString = attributedStringBox.getValue(); auto measurement = measureCache_.get( @@ -179,6 +180,12 @@ TextMeasurement TextLayoutManager::measure( measurement.size = layoutConstraints.clamp(measurement.size); return measurement; } +std::shared_ptr TextLayoutManager::getHostTextStorage( + AttributedString const & /* attributedStringBox */, + ParagraphAttributes const & /* paragraphAttributes */, + LayoutConstraints /* layoutConstraints */) const { + return nullptr; +} TextMeasurement TextLayoutManager::measureCachedSpannableById( int64_t cacheId, diff --git a/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.h b/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.h index 63694bc1bbc..0f5e070b942 100644 --- a/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.h +++ b/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.h @@ -45,7 +45,13 @@ class TextLayoutManager { */ TextMeasurement measure( AttributedStringBox const &attributedStringBox, - ParagraphAttributes paragraphAttributes, + ParagraphAttributes const ¶graphAttributes, + LayoutConstraints layoutConstraints, + std::shared_ptr /* hostTextStorage */) const; + + std::shared_ptr getHostTextStorage( + AttributedString const &attributedString, + ParagraphAttributes const ¶graphAttributes, LayoutConstraints layoutConstraints) const; /** diff --git a/ReactCommon/react/renderer/textlayoutmanager/platform/cxx/TextLayoutManager.cpp b/ReactCommon/react/renderer/textlayoutmanager/platform/cxx/TextLayoutManager.cpp index e0214a6dab0..1284f3edfdd 100644 --- a/ReactCommon/react/renderer/textlayoutmanager/platform/cxx/TextLayoutManager.cpp +++ b/ReactCommon/react/renderer/textlayoutmanager/platform/cxx/TextLayoutManager.cpp @@ -17,7 +17,8 @@ void *TextLayoutManager::getNativeTextLayoutManager() const { TextMeasurement TextLayoutManager::measure( AttributedStringBox attributedStringBox, ParagraphAttributes paragraphAttributes, - LayoutConstraints layoutConstraints) const { + LayoutConstraints layoutConstraints, + std::shared_ptr) const { TextMeasurement::Attachments attachments; for (auto const &fragment : attributedStringBox.getValue().getFragments()) { if (fragment.isAttachment()) { @@ -35,5 +36,12 @@ LinesMeasurements TextLayoutManager::measureLines( return {}; }; +std::shared_ptr TextLayoutManager::getHostTextStorage( + AttributedString attributedString, + ParagraphAttributes paragraphAttributes, + LayoutConstraints layoutConstraints) const { + return nullptr; +} + } // namespace react } // namespace facebook diff --git a/ReactCommon/react/renderer/textlayoutmanager/platform/cxx/TextLayoutManager.h b/ReactCommon/react/renderer/textlayoutmanager/platform/cxx/TextLayoutManager.h index 25233cabea2..cb2b9ebcf0a 100644 --- a/ReactCommon/react/renderer/textlayoutmanager/platform/cxx/TextLayoutManager.h +++ b/ReactCommon/react/renderer/textlayoutmanager/platform/cxx/TextLayoutManager.h @@ -36,7 +36,8 @@ class TextLayoutManager { TextMeasurement measure( AttributedStringBox attributedStringBox, ParagraphAttributes paragraphAttributes, - LayoutConstraints layoutConstraints) const; + LayoutConstraints layoutConstraints, + std::shared_ptr) const; /* * Measures lines of `attributedString` using native text rendering @@ -52,6 +53,11 @@ class TextLayoutManager { * Is used on a native views layer to delegate text rendering to the manager. */ void *getNativeTextLayoutManager() const; + + std::shared_ptr getHostTextStorage( + AttributedString attributedStringBox, + ParagraphAttributes paragraphAttributes, + LayoutConstraints layoutConstraints) const; }; } // namespace react diff --git a/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.h b/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.h index 0c2dc67921e..b0d906ca10c 100644 --- a/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.h +++ b/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.h @@ -28,15 +28,22 @@ using RCTTextLayoutFragmentEnumerationBlock = - (facebook::react::TextMeasurement)measureAttributedString:(facebook::react::AttributedString)attributedString paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes - layoutConstraints:(facebook::react::LayoutConstraints)layoutConstraints; + layoutConstraints:(facebook::react::LayoutConstraints)layoutConstraints + textStorage:(NSTextStorage *_Nullable)textStorage; - (facebook::react::TextMeasurement)measureNSAttributedString:(NSAttributedString *)attributedString paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes - layoutConstraints:(facebook::react::LayoutConstraints)layoutConstraints; + layoutConstraints:(facebook::react::LayoutConstraints)layoutConstraints + textStorage:(NSTextStorage *_Nullable)textStorage; + +- (NSTextStorage *)textStorageForAttributesString:(facebook::react::AttributedString)attributedString + paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes + size:(CGSize)size; - (void)drawAttributedString:(facebook::react::AttributedString)attributedString paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes - frame:(CGRect)frame; + frame:(CGRect)frame + textStorage:(NSTextStorage *_Nullable)textStorage; - (facebook::react::LinesMeasurements)getLinesForAttributedString:(facebook::react::AttributedString)attributedString paragraphAttributes: diff --git a/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm b/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm index 844fcb22851..c877e0a1e78 100644 --- a/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm +++ b/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm @@ -37,6 +37,7 @@ static NSLineBreakMode RCTNSLineBreakModeFromEllipsizeMode(EllipsizeMode ellipsi - (TextMeasurement)measureNSAttributedString:(NSAttributedString *)attributedString paragraphAttributes:(ParagraphAttributes)paragraphAttributes layoutConstraints:(LayoutConstraints)layoutConstraints + textStorage:(NSTextStorage *_Nullable)textStorage { if (attributedString.length == 0) { // This is not really an optimization because that should be checked much earlier on the call stack. @@ -47,9 +48,13 @@ static NSLineBreakMode RCTNSLineBreakModeFromEllipsizeMode(EllipsizeMode ellipsi CGSize maximumSize = CGSize{layoutConstraints.maximumSize.width, CGFLOAT_MAX}; - NSTextStorage *textStorage = [self _textStorageAndLayoutManagerWithAttributesString:attributedString - paragraphAttributes:paragraphAttributes - size:maximumSize]; + if (!textStorage) { + textStorage = [self _textStorageForNSAttributesString:attributedString + paragraphAttributes:paragraphAttributes + size:maximumSize]; + } else { + textStorage.layoutManagers.firstObject.textContainers.firstObject.size = maximumSize; + } NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject; NSTextContainer *textContainer = layoutManager.textContainers.firstObject; @@ -93,20 +98,24 @@ static NSLineBreakMode RCTNSLineBreakModeFromEllipsizeMode(EllipsizeMode ellipsi - (TextMeasurement)measureAttributedString:(AttributedString)attributedString paragraphAttributes:(ParagraphAttributes)paragraphAttributes layoutConstraints:(LayoutConstraints)layoutConstraints + textStorage:(NSTextStorage *_Nullable)textStorage { return [self measureNSAttributedString:[self _nsAttributedStringFromAttributedString:attributedString] paragraphAttributes:paragraphAttributes - layoutConstraints:layoutConstraints]; + layoutConstraints:layoutConstraints + textStorage:textStorage]; } - (void)drawAttributedString:(AttributedString)attributedString paragraphAttributes:(ParagraphAttributes)paragraphAttributes frame:(CGRect)frame + textStorage:(NSTextStorage *_Nullable)textStorage { - NSTextStorage *textStorage = [self - _textStorageAndLayoutManagerWithAttributesString:[self _nsAttributedStringFromAttributedString:attributedString] + if (!textStorage) { + textStorage = [self textStorageForAttributesString:attributedString paragraphAttributes:paragraphAttributes size:frame.size]; + } NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject; NSTextContainer *textContainer = layoutManager.textContainers.firstObject; @@ -125,14 +134,13 @@ static NSLineBreakMode RCTNSLineBreakModeFromEllipsizeMode(EllipsizeMode ellipsi #endif } -- (LinesMeasurements)getLinesForAttributedString:(facebook::react::AttributedString)attributedString - paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes +- (LinesMeasurements)getLinesForAttributedString:(AttributedString)attributedString + paragraphAttributes:(ParagraphAttributes)paragraphAttributes size:(CGSize)size { - NSTextStorage *textStorage = [self - _textStorageAndLayoutManagerWithAttributesString:[self _nsAttributedStringFromAttributedString:attributedString] - paragraphAttributes:paragraphAttributes - size:size]; + NSTextStorage *textStorage = [self textStorageForAttributesString:attributedString + paragraphAttributes:paragraphAttributes + size:size]; NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject; NSTextContainer *textContainer = layoutManager.textContainers.firstObject; @@ -170,9 +178,9 @@ static NSLineBreakMode RCTNSLineBreakModeFromEllipsizeMode(EllipsizeMode ellipsi return paragraphLines; } -- (NSTextStorage *)_textStorageAndLayoutManagerWithAttributesString:(NSAttributedString *)attributedString - paragraphAttributes:(ParagraphAttributes)paragraphAttributes - size:(CGSize)size +- (NSTextStorage *)_textStorageForNSAttributesString:(NSAttributedString *)attributedString + paragraphAttributes:(ParagraphAttributes)paragraphAttributes + size:(CGSize)size { NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:size]; @@ -199,15 +207,23 @@ static NSLineBreakMode RCTNSLineBreakModeFromEllipsizeMode(EllipsizeMode ellipsi return textStorage; } +- (NSTextStorage *)textStorageForAttributesString:(AttributedString)attributedString + paragraphAttributes:(ParagraphAttributes)paragraphAttributes + size:(CGSize)size +{ + return [self _textStorageForNSAttributesString:[self _nsAttributedStringFromAttributedString:attributedString] + paragraphAttributes:paragraphAttributes + size:size]; +} + - (SharedEventEmitter)getEventEmitterWithAttributeString:(AttributedString)attributedString paragraphAttributes:(ParagraphAttributes)paragraphAttributes frame:(CGRect)frame atPoint:(CGPoint)point { - NSTextStorage *textStorage = [self - _textStorageAndLayoutManagerWithAttributesString:[self _nsAttributedStringFromAttributedString:attributedString] - paragraphAttributes:paragraphAttributes - size:frame.size]; + NSTextStorage *textStorage = [self textStorageForAttributesString:attributedString + paragraphAttributes:paragraphAttributes + size:frame.size]; NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject; NSTextContainer *textContainer = layoutManager.textContainers.firstObject; @@ -245,10 +261,9 @@ static NSLineBreakMode RCTNSLineBreakModeFromEllipsizeMode(EllipsizeMode ellipsi frame:(CGRect)frame usingBlock:(RCTTextLayoutFragmentEnumerationBlock)block { - NSTextStorage *textStorage = [self - _textStorageAndLayoutManagerWithAttributesString:[self _nsAttributedStringFromAttributedString:attributedString] - paragraphAttributes:paragraphAttributes - size:frame.size]; + NSTextStorage *textStorage = [self textStorageForAttributesString:attributedString + paragraphAttributes:paragraphAttributes + size:frame.size]; NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject; NSTextContainer *textContainer = layoutManager.textContainers.firstObject; diff --git a/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/TextLayoutManager.h b/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/TextLayoutManager.h index ec101a3cb20..095fae58850 100644 --- a/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/TextLayoutManager.h +++ b/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/TextLayoutManager.h @@ -33,7 +33,8 @@ class TextLayoutManager { TextMeasurement measure( AttributedStringBox attributedStringBox, ParagraphAttributes paragraphAttributes, - LayoutConstraints layoutConstraints) const; + LayoutConstraints layoutConstraints, + std::shared_ptr hostTextStorage) const; /* * Measures lines of `attributedString` using native text rendering @@ -44,6 +45,11 @@ class TextLayoutManager { ParagraphAttributes paragraphAttributes, Size size) const; + std::shared_ptr getHostTextStorage( + AttributedString attributedString, + ParagraphAttributes paragraphAttributes, + LayoutConstraints layoutConstraints) const; + /* * Returns an opaque pointer to platform-specific TextLayoutManager. * Is used on a native views layer to delegate text rendering to the manager. diff --git a/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/TextLayoutManager.mm b/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/TextLayoutManager.mm index 74e905bbecd..4f2e8719565 100644 --- a/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/TextLayoutManager.mm +++ b/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/TextLayoutManager.mm @@ -25,12 +25,31 @@ std::shared_ptr TextLayoutManager::getNativeTextLayoutManager() const return self_; } -TextMeasurement TextLayoutManager::measure( - AttributedStringBox attributedStringBox, +std::shared_ptr TextLayoutManager::getHostTextStorage( + AttributedString attributedString, ParagraphAttributes paragraphAttributes, LayoutConstraints layoutConstraints) const { RCTTextLayoutManager *textLayoutManager = (RCTTextLayoutManager *)unwrapManagedObject(self_); + CGSize maximumSize = CGSize{layoutConstraints.maximumSize.width, CGFLOAT_MAX}; + + NSTextStorage *textStorage = [textLayoutManager textStorageForAttributesString:attributedString + paragraphAttributes:paragraphAttributes + size:maximumSize]; + return wrapManagedObject(textStorage); +} + +TextMeasurement TextLayoutManager::measure( + AttributedStringBox attributedStringBox, + ParagraphAttributes paragraphAttributes, + LayoutConstraints layoutConstraints, + std::shared_ptr hostTextStorage) const +{ + RCTTextLayoutManager *textLayoutManager = (RCTTextLayoutManager *)unwrapManagedObject(self_); + NSTextStorage *textStorage; + if (hostTextStorage) { + textStorage = unwrapManagedObject(hostTextStorage); + } auto measurement = TextMeasurement{}; @@ -47,7 +66,8 @@ TextMeasurement TextLayoutManager::measure( auto measurement = [textLayoutManager measureAttributedString:attributedString paragraphAttributes:paragraphAttributes - layoutConstraints:layoutConstraints]; + layoutConstraints:layoutConstraints + textStorage:textStorage]; if (telemetry) { telemetry->didMeasureText(); @@ -69,7 +89,8 @@ TextMeasurement TextLayoutManager::measure( measurement = [textLayoutManager measureNSAttributedString:nsAttributedString paragraphAttributes:paragraphAttributes - layoutConstraints:layoutConstraints]; + layoutConstraints:layoutConstraints + textStorage:textStorage]; if (telemetry) { telemetry->didMeasureText();