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 9009071df8d..dbc4a58e3af 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp @@ -195,6 +195,14 @@ void ParagraphShadowNode::updateStateIfNeeded(const Content& content) { return; } + if constexpr (TextLayoutManagerExtended::supportsPrepareForDrawing()) { + // The text view lays its text out on the main thread the first time it + // draws. Lay it out here instead, for the frame that draw uses. + TextLayoutManagerExtended(*textLayoutManager_) + .prepareForDrawing( + content.attributedString, content.paragraphAttributes, layoutSize); + } + setStateData( ParagraphState{ .attributedString = content.attributedString, diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/TextLayoutManagerExtended.h b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/TextLayoutManagerExtended.h index 93677133e64..84ecb262d9d 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/TextLayoutManagerExtended.h +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/TextLayoutManagerExtended.h @@ -71,6 +71,15 @@ class TextLayoutManagerExtended { return TextLayoutManagerWithPreparedTextLayout; } + static constexpr bool supportsPrepareForDrawing() + { + return requires( + const TextLayoutManagerT &textLayoutManager, + AttributedString attributedString, + ParagraphAttributes paragraphAttributes, + Size size) { textLayoutManager.prepareForDrawing(attributedString, paragraphAttributes, size); }; + } + using PreparedTextLayout = typename PreparedTextLayoutT::type; TextLayoutManagerExtended(const TextLayoutManagerT &textLayoutManager) : textLayoutManager_(textLayoutManager) {} @@ -98,6 +107,18 @@ class TextLayoutManagerExtended { LOG(FATAL) << "Platform TextLayoutManager does not support prepareLayout"; } + void prepareForDrawing( + const AttributedString &attributedString, + const ParagraphAttributes ¶graphAttributes, + const Size &size) const + { + if constexpr (supportsPrepareForDrawing()) { + textLayoutManager_.prepareForDrawing(attributedString, paragraphAttributes, size); + } else { + LOG(FATAL) << "Platform TextLayoutManager does not support prepareForDrawing"; + } + } + TextMeasurement measurePreparedLayout( const PreparedTextLayout &layout, const TextLayoutContext &layoutContext, diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.h b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.h index 2cd7f1bce24..d982f6bfefb 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.h +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.h @@ -39,6 +39,15 @@ using RCTTextLayoutFragmentEnumerationBlock = layoutContext:(facebook::react::TextLayoutContext)layoutContext layoutConstraints:(facebook::react::LayoutConstraints)layoutConstraints; +/** + * Lays `attributedString` out for a text view that draws it in a frame of `size`, + * so that the draw does not lay it out again. + * Can be called from any thread. + */ +- (void)prepareAttributedStringForDrawing:(const facebook::react::AttributedString &)attributedString + paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes + size:(CGSize)size; + - (void)drawAttributedString:(const facebook::react::AttributedString &)attributedString paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes frame:(CGRect)frame diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm index 3e93b60e64b..a222ba57511 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm @@ -15,14 +15,74 @@ #import #import #import +#import #import #import #import using namespace facebook::react; +namespace facebook::react { + +/* + * Identifies the text storage a text view draws: what it draws, and the size of + * the frame it draws it in. Two paragraphs that are equivalent this way draw + * the same, so they share a text storage. Equivalence is the one prepared text + * layouts use on Android: everything drawing looks at, and nothing that only + * matters for hit testing, such as a fragment's event emitter. + * + * The frame size is all a layout depends on: it is the size the view draws in, + * already rounded to the pixel grid by Yoga, so the pixel scale factor is not + * part of the key. + */ +class RCTPreparedTextLayoutKey final { + public: + AttributedString attributedString; + ParagraphAttributes paragraphAttributes; + Size size; + + bool operator==(const RCTPreparedTextLayoutKey &rhs) const + { + return areAttributedStringsEquivalentDisplayWise(attributedString, rhs.attributedString) && + paragraphAttributes == rhs.paragraphAttributes && size == rhs.size; + } +}; + +inline RCTPreparedTextLayoutKey RCTPreparedTextLayoutKeyFor( + const AttributedString &attributedString, + const ParagraphAttributes ¶graphAttributes, + CGSize size) +{ + return RCTPreparedTextLayoutKey{ + .attributedString = attributedString, + .paragraphAttributes = paragraphAttributes, + .size = facebook::react::Size{ + .width = static_cast(size.width), + .height = static_cast(size.height)}}; +} + +} // namespace facebook::react + +namespace std { + +template <> +struct hash { + size_t operator()(const facebook::react::RCTPreparedTextLayoutKey &key) const + { + return facebook::react::hash_combine( + facebook::react::attributedStringHashDisplayWise(key.attributedString), key.paragraphAttributes, key.size); + } +}; + +} // namespace std + @implementation RCTTextLayoutManager { SimpleThreadSafeCache, 256> _cache; + /* + * Text laid out for the first draw of a text view. The text storage is the + * prepared layout: it owns the layout manager that holds the lines. + */ + SimpleThreadSafeCache, 256> _preparedTextLayouts; } static NSLineBreakMode RCTNSLineBreakModeFromEllipsizeMode(EllipsizeMode ellipsizeMode) @@ -128,15 +188,36 @@ - (CGRect)drawingFrameForAttributedString:(const AttributedString &)attributedSt return drawingFrame; } +- (void)prepareAttributedStringForDrawing:(const AttributedString &)attributedString + paragraphAttributes:(ParagraphAttributes)paragraphAttributes + size:(CGSize)size +{ + if (attributedString.isEmpty() || size.width <= 0 || size.height <= 0) { + return; + } + + // Laying the text out here holds the cache for the whole layout, which + // serializes preparations. Drawing only looks layouts up, so it never waits on + // this. + (void)_preparedTextLayouts.get(RCTPreparedTextLayoutKeyFor(attributedString, paragraphAttributes, size), [&]() { + NSAttributedString *nsAttributedString = [self _nsAttributedStringFromAttributedString:attributedString]; + NSTextStorage *textStorage = [self _textStorageAndLayoutManagerWithAttributesString:nsAttributedString + paragraphAttributes:paragraphAttributes + size:size]; + NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject; + [layoutManager ensureLayoutForTextContainer:layoutManager.textContainers.firstObject]; + return wrapManagedObject(textStorage); + }); +} + - (void)drawAttributedString:(const AttributedString &)attributedString paragraphAttributes:(ParagraphAttributes)paragraphAttributes frame:(CGRect)frame drawHighlightPath:(void (^_Nullable)(UIBezierPath *highlightPath))block { - NSTextStorage *textStorage = [self - _textStorageAndLayoutManagerWithAttributesString:[self _nsAttributedStringFromAttributedString:attributedString] - paragraphAttributes:paragraphAttributes - size:frame.size]; + NSTextStorage *textStorage = [self _textStorageForDrawingWithAttributedString:attributedString + paragraphAttributes:paragraphAttributes + size:frame.size]; NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject; NSTextContainer *textContainer = layoutManager.textContainers.firstObject; @@ -431,6 +512,29 @@ - (CGFloat)_maximumFontSizeInAttributedString:(NSAttributedString *)attributedSt return maximumFontSize; } +/* + * The text storage to draw `attributedString` with in a frame of `size`: the one + * prepared for exactly that, or a new one when none was prepared. + */ +- (NSTextStorage *)_textStorageForDrawingWithAttributedString:(const AttributedString &)attributedString + paragraphAttributes:(ParagraphAttributes)paragraphAttributes + size:(CGSize)size +{ + auto preparedTextLayout = + _preparedTextLayouts.get(RCTPreparedTextLayoutKeyFor(attributedString, paragraphAttributes, size)); + NSTextStorage *textStorage = preparedTextLayout.has_value() ? unwrapManagedObject(*preparedTextLayout) : nil; + if (textStorage != nil) { + return textStorage; + } + + // Not prepared: lay the text out here, as before. It is not added to the + // cache, so that drawing never holds it. + return [self + _textStorageAndLayoutManagerWithAttributesString:[self _nsAttributedStringFromAttributedString:attributedString] + paragraphAttributes:paragraphAttributes + size:size]; +} + - (NSTextStorage *)_textStorageAndLayoutManagerWithAttributesString:(NSAttributedString *)attributedString paragraphAttributes:(ParagraphAttributes)paragraphAttributes size:(CGSize)size diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/TextLayoutManager.h b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/TextLayoutManager.h index c97994b6068..7fb5d06760a 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/TextLayoutManager.h +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/TextLayoutManager.h @@ -57,6 +57,16 @@ class TextLayoutManager { const ParagraphAttributes ¶graphAttributes, const Size &size) const; + /* + * Lays `attributedString` out for a text view that draws it in a frame of + * `size`, so that the view's first draw does not lay it out again. + * Can be called from any thread. + */ + void prepareForDrawing( + const AttributedString &attributedString, + const ParagraphAttributes ¶graphAttributes, + const Size &size) 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/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/TextLayoutManager.mm b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/TextLayoutManager.mm index bf4bc16c654..1e7f1f6c139 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/TextLayoutManager.mm +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/TextLayoutManager.mm @@ -113,6 +113,18 @@ return measurement; } +void TextLayoutManager::prepareForDrawing( + const AttributedString &attributedString, + const ParagraphAttributes ¶graphAttributes, + const Size &size) const +{ + RCTTextLayoutManager *textLayoutManager = (RCTTextLayoutManager *)unwrapManagedObject(nativeTextLayoutManager_); + + [textLayoutManager prepareAttributedStringForDrawing:attributedString + paragraphAttributes:paragraphAttributes + size:CGSizeMake(size.width, size.height)]; +} + LinesMeasurements TextLayoutManager::measureLines( const AttributedStringBox &attributedStringBox, const ParagraphAttributes ¶graphAttributes, diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api index 69efb6e2de0..997ba5e98a8 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api @@ -8403,8 +8403,10 @@ class facebook::react::detail::TextLayoutManagerExtended { public facebook::react::TextMeasurement measurePreparedLayout(const facebook::react::detail::TextLayoutManagerExtended::PreparedTextLayout& layout, const facebook::react::TextLayoutContext& layoutContext, const facebook::react::LayoutConstraints& layoutConstraints) const; public facebook::react::detail::TextLayoutManagerExtended::PreparedTextLayout prepareLayout(const facebook::react::AttributedString& attributedString, const facebook::react::ParagraphAttributes& paragraphAttributes, const facebook::react::TextLayoutContext& layoutContext, const facebook::react::LayoutConstraints& layoutConstraints) const; public static constexpr bool supportsLineMeasurement(); + public static constexpr bool supportsPrepareForDrawing(); public static constexpr bool supportsPreparedTextLayout(); public using PreparedTextLayout = typename facebook::react::detail::PreparedTextLayoutT::type; + public void prepareForDrawing(const facebook::react::AttributedString& attributedString, const facebook::react::ParagraphAttributes& paragraphAttributes, const facebook::react::Size& size) const; } template diff --git a/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api index a21ebf06823..451dff229ef 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api @@ -1936,6 +1936,7 @@ interface RCTTextLayoutManager : public NSObject { public virtual std::shared_ptr getEventEmitterWithAttributeString:paragraphAttributes:frame:atPoint:(const facebook::react::AttributedString& attributedString, facebook::react::ParagraphAttributes paragraphAttributes, CGRect frame, CGPoint point); public virtual void drawAttributedString:paragraphAttributes:frame:drawHighlightPath:(const facebook::react::AttributedString& attributedString, facebook::react::ParagraphAttributes paragraphAttributes, CGRect frame, void(^)(UIBezierPath* highlightPath) block); public virtual void getRectWithAttributedString:paragraphAttributes:enumerateAttribute:frame:usingBlock:(const facebook::react::AttributedString& attributedString, facebook::react::ParagraphAttributes paragraphAttributes, NSString* enumerateAttribute, CGRect frame, RCTTextLayoutFragmentEnumerationBlock block); + public virtual void prepareAttributedStringForDrawing:paragraphAttributes:size:(const facebook::react::AttributedString& attributedString, facebook::react::ParagraphAttributes paragraphAttributes, CGSize size); } interface RCTTextSelection : public NSObject { @@ -6213,6 +6214,7 @@ class facebook::react::TextLayoutManager { public facebook::react::TextLayoutManager& operator=(facebook::react::TextLayoutManager&&) = delete; public facebook::react::TextMeasurement measure(const facebook::react::AttributedStringBox& attributedStringBox, const facebook::react::ParagraphAttributes& paragraphAttributes, const facebook::react::TextLayoutContext& layoutContext, const facebook::react::LayoutConstraints& layoutConstraints) const; public std::shared_ptr getNativeTextLayoutManager() const; + public void prepareForDrawing(const facebook::react::AttributedString& attributedString, const facebook::react::ParagraphAttributes& paragraphAttributes, const facebook::react::Size& size) const; } class facebook::react::TextMeasureCacheKey { @@ -10312,8 +10314,10 @@ class facebook::react::detail::TextLayoutManagerExtended { public facebook::react::TextMeasurement measurePreparedLayout(const facebook::react::detail::TextLayoutManagerExtended::PreparedTextLayout& layout, const facebook::react::TextLayoutContext& layoutContext, const facebook::react::LayoutConstraints& layoutConstraints) const; public facebook::react::detail::TextLayoutManagerExtended::PreparedTextLayout prepareLayout(const facebook::react::AttributedString& attributedString, const facebook::react::ParagraphAttributes& paragraphAttributes, const facebook::react::TextLayoutContext& layoutContext, const facebook::react::LayoutConstraints& layoutConstraints) const; public static constexpr bool supportsLineMeasurement(); + public static constexpr bool supportsPrepareForDrawing(); public static constexpr bool supportsPreparedTextLayout(); public using PreparedTextLayout = typename facebook::react::detail::PreparedTextLayoutT::type; + public void prepareForDrawing(const facebook::react::AttributedString& attributedString, const facebook::react::ParagraphAttributes& paragraphAttributes, const facebook::react::Size& size) const; } template diff --git a/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api index 7933bdb1a43..d1cae469690 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api @@ -5647,8 +5647,10 @@ class facebook::react::detail::TextLayoutManagerExtended { public facebook::react::TextMeasurement measurePreparedLayout(const facebook::react::detail::TextLayoutManagerExtended::PreparedTextLayout& layout, const facebook::react::TextLayoutContext& layoutContext, const facebook::react::LayoutConstraints& layoutConstraints) const; public facebook::react::detail::TextLayoutManagerExtended::PreparedTextLayout prepareLayout(const facebook::react::AttributedString& attributedString, const facebook::react::ParagraphAttributes& paragraphAttributes, const facebook::react::TextLayoutContext& layoutContext, const facebook::react::LayoutConstraints& layoutConstraints) const; public static constexpr bool supportsLineMeasurement(); + public static constexpr bool supportsPrepareForDrawing(); public static constexpr bool supportsPreparedTextLayout(); public using PreparedTextLayout = typename facebook::react::detail::PreparedTextLayoutT::type; + public void prepareForDrawing(const facebook::react::AttributedString& attributedString, const facebook::react::ParagraphAttributes& paragraphAttributes, const facebook::react::Size& size) const; } template