From 90ada5abb157c8767e2fed9af0dc0cdfa5a0fe30 Mon Sep 17 00:00:00 2001 From: Valentin Shergin Date: Mon, 9 Mar 2020 16:32:33 -0700 Subject: [PATCH] Fabric: Calculating positions of attachments as part of text measurement Summary: This diff changes API we use to measure text. Previously, a platform-specific measure infra returned just the size of the text, now it returns the size and an array of frames that describe where attachments are placed. Changelog: [Internal] Fabric-specific internal change. Reviewed By: sammy-SC Differential Revision: D20268041 fbshipit-source-id: 7c065607b6af18a36318db0aab24dad0f171d33a --- .../text/paragraph/ParagraphShadowNode.cpp | 10 +++-- .../AndroidTextInputShadowNode.cpp | 10 +++-- .../iostextinput/TextInputShadowNode.cpp | 10 +++-- .../textlayoutmanager/TextMeasureCache.h | 19 ++++++++- .../platform/android/TextLayoutManager.cpp | 8 ++-- .../platform/android/TextLayoutManager.h | 4 +- .../platform/ios/RCTTextLayoutManager.h | 13 +++--- .../platform/ios/RCTTextLayoutManager.mm | 42 +++++++++++++++---- .../platform/ios/TextLayoutManager.h | 2 +- .../platform/ios/TextLayoutManager.mm | 16 +++---- 10 files changed, 94 insertions(+), 40 deletions(-) diff --git a/ReactCommon/fabric/components/text/paragraph/ParagraphShadowNode.cpp b/ReactCommon/fabric/components/text/paragraph/ParagraphShadowNode.cpp index c4835e55d4f..72f5ddff291 100644 --- a/ReactCommon/fabric/components/text/paragraph/ParagraphShadowNode.cpp +++ b/ReactCommon/fabric/components/text/paragraph/ParagraphShadowNode.cpp @@ -70,10 +70,12 @@ Size ParagraphShadowNode::measure(LayoutConstraints layoutConstraints) const { return layoutConstraints.clamp({0, 0}); } - return textLayoutManager_->measure( - AttributedStringBox{content.attributedString}, - content.paragraphAttributes, - layoutConstraints); + return textLayoutManager_ + ->measure( + AttributedStringBox{content.attributedString}, + content.paragraphAttributes, + layoutConstraints) + .size; } void ParagraphShadowNode::layout(LayoutContext layoutContext) { diff --git a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.cpp b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.cpp index 8f2c00cf3a0..0d90987f511 100644 --- a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.cpp +++ b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.cpp @@ -180,10 +180,12 @@ Size AndroidTextInputShadowNode::measure( return {0, 0}; } - return textLayoutManager_->measure( - AttributedStringBox{attributedString}, - getConcreteProps().paragraphAttributes, - layoutConstraints); + return textLayoutManager_ + ->measure( + AttributedStringBox{attributedString}, + getConcreteProps().paragraphAttributes, + layoutConstraints) + .size; } void AndroidTextInputShadowNode::layout(LayoutContext layoutContext) { diff --git a/ReactCommon/fabric/components/textinput/iostextinput/TextInputShadowNode.cpp b/ReactCommon/fabric/components/textinput/iostextinput/TextInputShadowNode.cpp index f426f1ed9b1..0912e891324 100644 --- a/ReactCommon/fabric/components/textinput/iostextinput/TextInputShadowNode.cpp +++ b/ReactCommon/fabric/components/textinput/iostextinput/TextInputShadowNode.cpp @@ -81,10 +81,12 @@ void TextInputShadowNode::updateStateIfNeeded() { #pragma mark - LayoutableShadowNode Size TextInputShadowNode::measure(LayoutConstraints layoutConstraints) const { - return textLayoutManager_->measure( - attributedStringBoxToMeasure(), - getConcreteProps().getEffectiveParagraphAttributes(), - layoutConstraints); + return textLayoutManager_ + ->measure( + attributedStringBoxToMeasure(), + getConcreteProps().getEffectiveParagraphAttributes(), + layoutConstraints) + .size; } void TextInputShadowNode::layout(LayoutContext layoutContext) { diff --git a/ReactCommon/fabric/textlayoutmanager/TextMeasureCache.h b/ReactCommon/fabric/textlayoutmanager/TextMeasureCache.h index 25c13ef2ca0..71b1c07a3d0 100644 --- a/ReactCommon/fabric/textlayoutmanager/TextMeasureCache.h +++ b/ReactCommon/fabric/textlayoutmanager/TextMeasureCache.h @@ -16,6 +16,23 @@ namespace facebook { namespace react { +/* + * Describes a result of text measuring. + */ +class TextMeasurement final { + public: + class Attachment final { + public: + Rect frame; + bool isClipped; + }; + + using Attachments = std::vector; + + Size size; + Attachments attachments; +}; + // The Key type that is used for Text Measure Cache. // The equivalence and hashing operations of this are defined to respect the // nature of text measuring. @@ -39,7 +56,7 @@ constexpr auto kSimpleThreadSafeCacheSizeCap = size_t{256}; */ using TextMeasureCache = SimpleThreadSafeCache< TextMeasureCacheKey, - Size, + TextMeasurement, kSimpleThreadSafeCacheSizeCap>; inline bool areTextAttributesEquivalentLayoutWise( diff --git a/ReactCommon/fabric/textlayoutmanager/platform/android/TextLayoutManager.cpp b/ReactCommon/fabric/textlayoutmanager/platform/android/TextLayoutManager.cpp index 93456a2a9bd..c89eefe552f 100644 --- a/ReactCommon/fabric/textlayoutmanager/platform/android/TextLayoutManager.cpp +++ b/ReactCommon/fabric/textlayoutmanager/platform/android/TextLayoutManager.cpp @@ -22,7 +22,7 @@ void *TextLayoutManager::getNativeTextLayoutManager() const { return self_; } -Size TextLayoutManager::measure( +TextMeasurement TextLayoutManager::measure( AttributedStringBox attributedStringBox, ParagraphAttributes paragraphAttributes, LayoutConstraints layoutConstraints) const { @@ -36,7 +36,7 @@ Size TextLayoutManager::measure( }); } -Size TextLayoutManager::doMeasure( +TextMeasurement TextLayoutManager::doMeasure( AttributedString attributedString, ParagraphAttributes paragraphAttributes, LayoutConstraints layoutConstraints) const { @@ -69,7 +69,7 @@ Size TextLayoutManager::doMeasure( reinterpret_cast(attributedStringRNM.get())); local_ref paragraphAttributesRM = make_local( reinterpret_cast(paragraphAttributesRNM.get())); - return yogaMeassureToSize(measure( + auto size = yogaMeassureToSize(measure( fabricUIManager, -1, componentName.get(), @@ -80,6 +80,8 @@ Size TextLayoutManager::doMeasure( maximumSize.width, minimumSize.height, maximumSize.height)); + + return TextMeasurement{size, {}}; } } // namespace react diff --git a/ReactCommon/fabric/textlayoutmanager/platform/android/TextLayoutManager.h b/ReactCommon/fabric/textlayoutmanager/platform/android/TextLayoutManager.h index 215ef8adab3..c8939d3f075 100644 --- a/ReactCommon/fabric/textlayoutmanager/platform/android/TextLayoutManager.h +++ b/ReactCommon/fabric/textlayoutmanager/platform/android/TextLayoutManager.h @@ -32,7 +32,7 @@ class TextLayoutManager { /* * Measures `attributedString` using native text rendering infrastructure. */ - Size measure( + TextMeasurement measure( AttributedStringBox attributedStringBox, ParagraphAttributes paragraphAttributes, LayoutConstraints layoutConstraints) const; @@ -44,7 +44,7 @@ class TextLayoutManager { void *getNativeTextLayoutManager() const; private: - Size doMeasure( + TextMeasurement doMeasure( AttributedString attributedString, ParagraphAttributes paragraphAttributes, LayoutConstraints layoutConstraints) const; diff --git a/ReactCommon/fabric/textlayoutmanager/platform/ios/RCTTextLayoutManager.h b/ReactCommon/fabric/textlayoutmanager/platform/ios/RCTTextLayoutManager.h index a88eb2902a2..0e10ce2a085 100644 --- a/ReactCommon/fabric/textlayoutmanager/platform/ios/RCTTextLayoutManager.h +++ b/ReactCommon/fabric/textlayoutmanager/platform/ios/RCTTextLayoutManager.h @@ -11,6 +11,7 @@ #import #import #import +#import NS_ASSUME_NONNULL_BEGIN @@ -19,13 +20,13 @@ NS_ASSUME_NONNULL_BEGIN */ @interface RCTTextLayoutManager : NSObject -- (facebook::react::Size)measureAttributedString:(facebook::react::AttributedString)attributedString - paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes - layoutConstraints:(facebook::react::LayoutConstraints)layoutConstraints; +- (facebook::react::TextMeasurement)measureAttributedString:(facebook::react::AttributedString)attributedString + paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes + layoutConstraints:(facebook::react::LayoutConstraints)layoutConstraints; -- (facebook::react::Size)measureNSAttributedString:(NSAttributedString *)attributedString - paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes - layoutConstraints:(facebook::react::LayoutConstraints)layoutConstraints; +- (facebook::react::TextMeasurement)measureNSAttributedString:(NSAttributedString *)attributedString + paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes + layoutConstraints:(facebook::react::LayoutConstraints)layoutConstraints; - (void)drawAttributedString:(facebook::react::AttributedString)attributedString paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes diff --git a/ReactCommon/fabric/textlayoutmanager/platform/ios/RCTTextLayoutManager.mm b/ReactCommon/fabric/textlayoutmanager/platform/ios/RCTTextLayoutManager.mm index 875ee084090..1029a87ad4a 100644 --- a/ReactCommon/fabric/textlayoutmanager/platform/ios/RCTTextLayoutManager.mm +++ b/ReactCommon/fabric/textlayoutmanager/platform/ios/RCTTextLayoutManager.mm @@ -32,15 +32,15 @@ static NSLineBreakMode RCTNSLineBreakModeFromEllipsizeMode(EllipsizeMode ellipsi } } -- (facebook::react::Size)measureNSAttributedString:(NSAttributedString *)attributedString - paragraphAttributes:(ParagraphAttributes)paragraphAttributes - layoutConstraints:(LayoutConstraints)layoutConstraints +- (TextMeasurement)measureNSAttributedString:(NSAttributedString *)attributedString + paragraphAttributes:(ParagraphAttributes)paragraphAttributes + layoutConstraints:(LayoutConstraints)layoutConstraints { if (attributedString.length == 0) { // This is not really an optimization because that should be checked much earlier on the call stack. // Sometimes, very irregularly, measuring an empty string crashes/freezes iOS internal text infrastructure. // This is our last line of defense. - return {0, 0}; + return {}; } CGSize maximumSize = CGSize{layoutConstraints.maximumSize.width, CGFLOAT_MAX}; @@ -55,12 +55,38 @@ static NSLineBreakMode RCTNSLineBreakModeFromEllipsizeMode(EllipsizeMode ellipsi CGSize size = [layoutManager usedRectForTextContainer:textContainer].size; - return {size.width, size.height}; + __block auto attachments = TextMeasurement::Attachments{}; + + [textStorage + enumerateAttribute:NSAttachmentAttributeName + inRange:NSMakeRange(0, textStorage.length) + options:0 + usingBlock:^(NSTextAttachment *attachment, NSRange range, BOOL *stop) { + if (!attachment) { + return; + } + + CGSize attachmentSize = attachment.bounds.size; + CGRect glyphRect = [layoutManager boundingRectForGlyphRange:range inTextContainer:textContainer]; + + UIFont *font = [textStorage attribute:NSFontAttributeName atIndex:range.location effectiveRange:nil]; + + CGRect frame = {{glyphRect.origin.x, + glyphRect.origin.y + glyphRect.size.height - attachmentSize.height + font.descender}, + attachmentSize}; + + auto rect = facebook::react::Rect{facebook::react::Point{frame.origin.x, frame.origin.y}, + facebook::react::Size{frame.size.width, frame.size.height}}; + + attachments.push_back(TextMeasurement::Attachment{rect, false}); + }]; + + return TextMeasurement{{size.width, size.height}, attachments}; } -- (facebook::react::Size)measureAttributedString:(AttributedString)attributedString - paragraphAttributes:(ParagraphAttributes)paragraphAttributes - layoutConstraints:(LayoutConstraints)layoutConstraints +- (TextMeasurement)measureAttributedString:(AttributedString)attributedString + paragraphAttributes:(ParagraphAttributes)paragraphAttributes + layoutConstraints:(LayoutConstraints)layoutConstraints { return [self measureNSAttributedString:[self _nsAttributedStringFromAttributedString:attributedString] paragraphAttributes:paragraphAttributes diff --git a/ReactCommon/fabric/textlayoutmanager/platform/ios/TextLayoutManager.h b/ReactCommon/fabric/textlayoutmanager/platform/ios/TextLayoutManager.h index 57478586edb..02a9ff7a9e6 100644 --- a/ReactCommon/fabric/textlayoutmanager/platform/ios/TextLayoutManager.h +++ b/ReactCommon/fabric/textlayoutmanager/platform/ios/TextLayoutManager.h @@ -34,7 +34,7 @@ class TextLayoutManager { /* * Measures `attributedString` using native text rendering infrastructure. */ - Size measure( + TextMeasurement measure( AttributedStringBox attributedStringBox, ParagraphAttributes paragraphAttributes, LayoutConstraints layoutConstraints) const; diff --git a/ReactCommon/fabric/textlayoutmanager/platform/ios/TextLayoutManager.mm b/ReactCommon/fabric/textlayoutmanager/platform/ios/TextLayoutManager.mm index 6c28e51ff46..c3333e1d9da 100644 --- a/ReactCommon/fabric/textlayoutmanager/platform/ios/TextLayoutManager.mm +++ b/ReactCommon/fabric/textlayoutmanager/platform/ios/TextLayoutManager.mm @@ -25,20 +25,20 @@ std::shared_ptr TextLayoutManager::getNativeTextLayoutManager() const return self_; } -Size TextLayoutManager::measure( +TextMeasurement TextLayoutManager::measure( AttributedStringBox attributedStringBox, ParagraphAttributes paragraphAttributes, LayoutConstraints layoutConstraints) const { RCTTextLayoutManager *textLayoutManager = (RCTTextLayoutManager *)unwrapManagedObject(self_); - auto size = Size{}; + auto measurement = TextMeasurement{}; switch (attributedStringBox.getMode()) { case AttributedStringBox::Mode::Value: { auto &attributedString = attributedStringBox.getValue(); - size = measureCache_.get( + measurement = measureCache_.get( {attributedString, paragraphAttributes, layoutConstraints}, [&](TextMeasureCacheKey const &key) { return [textLayoutManager measureAttributedString:attributedString paragraphAttributes:paragraphAttributes @@ -51,14 +51,16 @@ Size TextLayoutManager::measure( NSAttributedString *nsAttributedString = (NSAttributedString *)unwrapManagedObject(attributedStringBox.getOpaquePointer()); - size = [textLayoutManager measureNSAttributedString:nsAttributedString - paragraphAttributes:paragraphAttributes - layoutConstraints:layoutConstraints]; + measurement = [textLayoutManager measureNSAttributedString:nsAttributedString + paragraphAttributes:paragraphAttributes + layoutConstraints:layoutConstraints]; break; } } - return layoutConstraints.clamp(size); + measurement.size = layoutConstraints.clamp(measurement.size); + + return measurement; } } // namespace react