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 8d4a9b07aec9c8..9c3482b88f8073 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp @@ -163,6 +163,35 @@ Size ParagraphShadowNode::measureContent( .size; } +Float ParagraphShadowNode::baseline( + const LayoutContext& layoutContext, + Size size) const { + auto layoutMetrics = getLayoutMetrics(); + auto layoutConstraints = + LayoutConstraints{size, size, layoutMetrics.layoutDirection}; + auto content = + getContentWithMeasuredAttachments(layoutContext, layoutConstraints); + auto attributedString = content.attributedString; + + if (attributedString.isEmpty()) { + // Note: `zero-width space` is insufficient in some cases (e.g. when we need + // to measure the "height" of the font). + // TODO T67606511: We will redefine the measurement of empty strings as part + // of T67606511 + auto string = BaseTextShadowNode::getEmptyPlaceholder(); + auto textAttributes = TextAttributes::defaultTextAttributes(); + textAttributes.fontSizeMultiplier = layoutContext.fontSizeMultiplier; + textAttributes.apply(getConcreteProps().textAttributes); + attributedString.appendFragment({string, textAttributes, {}}); + } + + return textLayoutManager_ + ->baseline( + attributedString, + getConcreteProps().paragraphAttributes, + size); +} + void ParagraphShadowNode::layout(LayoutContext layoutContext) { ensureUnsealed(); diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.h b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.h index 0a1cffb621c7eb..93b639db23f3f1 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.h +++ b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.h @@ -43,6 +43,7 @@ class ParagraphShadowNode final : public ConcreteViewShadowNode< auto traits = ConcreteViewShadowNode::BaseTraits(); traits.set(ShadowNodeTraits::Trait::LeafYogaNode); traits.set(ShadowNodeTraits::Trait::MeasurableYogaNode); + traits.set(ShadowNodeTraits::Trait::BaselineYogaNode); #ifdef ANDROID // Unsetting `FormsStackingContext` trait is essential on Android where we @@ -69,6 +70,10 @@ class ParagraphShadowNode final : public ConcreteViewShadowNode< const LayoutContext& layoutContext, const LayoutConstraints& layoutConstraints) const override; + Float baseline( + const LayoutContext& layoutContext, + Size size) const override; + /* * Internal representation of the nested content of the node in a format * suitable for future processing. 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 d15c7b32bbcf04..9fd23d97f2fa1f 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 @@ -214,6 +214,28 @@ Size AndroidTextInputShadowNode::measureContent( .size; } +Float AndroidTextInputShadowNode::baseline( + const LayoutContext& layoutContext, + Size size) const { + AttributedString attributedString = getMostRecentAttributedString(); + + if (attributedString.isEmpty()) { + attributedString = getPlaceholderAttributedString(); + } + + // Yoga expects a baseline relative to the Node's border-box edge instead of + // the content, so we need to adjust by the padding and border widths, which + // have already been set by the time of baseline alignment + auto top = YGNodeLayoutGetBorder(&yogaNode_, YGEdgeTop) + + YGNodeLayoutGetPadding(&yogaNode_, YGEdgeTop); + + return textLayoutManager_ + ->baseline( + attributedString, + getConcreteProps().paragraphAttributes, + size) + top; +} + void AndroidTextInputShadowNode::layout(LayoutContext layoutContext) { updateStateIfNeeded(); ConcreteViewShadowNode::layout(layoutContext); diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/android/react/renderer/components/androidtextinput/AndroidTextInputShadowNode.h b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/android/react/renderer/components/androidtextinput/AndroidTextInputShadowNode.h index 43a4df1e9edc1b..bd2fb2e6e39cca 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/android/react/renderer/components/androidtextinput/AndroidTextInputShadowNode.h +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/android/react/renderer/components/androidtextinput/AndroidTextInputShadowNode.h @@ -34,6 +34,7 @@ class AndroidTextInputShadowNode final static ShadowNodeTraits BaseTraits() { auto traits = ConcreteViewShadowNode::BaseTraits(); traits.set(ShadowNodeTraits::Trait::LeafYogaNode); + traits.set(ShadowNodeTraits::Trait::BaselineYogaNode); return traits; } @@ -65,6 +66,10 @@ class AndroidTextInputShadowNode final const LayoutConstraints& layoutConstraints) const override; void layout(LayoutContext layoutContext) override; + Float baseline( + const LayoutContext& layoutContext, + Size size) const override; + private: ContextContainer* contextContainer_{}; 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 b1ceb3eaa87931..9e03093d51ca33 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 @@ -140,6 +140,33 @@ Size TextInputShadowNode::measureContent( .size; } +Float TextInputShadowNode::baseline( + const LayoutContext& layoutContext, + Size size) const { + auto attributedString = getAttributedString(layoutContext); + + if (attributedString.isEmpty()) { + auto placeholderString = !getConcreteProps().placeholder.empty() + ? getConcreteProps().placeholder + : BaseTextShadowNode::getEmptyPlaceholder(); + auto textAttributes = getConcreteProps().getEffectiveTextAttributes( + layoutContext.fontSizeMultiplier); + attributedString.appendFragment({std::move(placeholderString), textAttributes, {}}); + } + + // Yoga expects a baseline relative to the Node's border-box edge instead of + // the content, so we need to adjust by the padding and border widths, which + // have already been set by the time of baseline alignment + auto top = YGNodeLayoutGetBorder(&yogaNode_, YGEdgeTop) + + YGNodeLayoutGetPadding(&yogaNode_, YGEdgeTop); + + return textLayoutManager_ + ->baseline( + attributedString, + getConcreteProps().getEffectiveParagraphAttributes(), + size) + top; +} + void TextInputShadowNode::layout(LayoutContext layoutContext) { updateStateIfNeeded(layoutContext); ConcreteViewShadowNode::layout(layoutContext); diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputShadowNode.h b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputShadowNode.h index 59e2db0ed56183..44204cddbf8a49 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputShadowNode.h +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputShadowNode.h @@ -40,6 +40,7 @@ class TextInputShadowNode final : public ConcreteViewShadowNode< auto traits = ConcreteViewShadowNode::BaseTraits(); traits.set(ShadowNodeTraits::Trait::LeafYogaNode); traits.set(ShadowNodeTraits::Trait::MeasurableYogaNode); + traits.set(ShadowNodeTraits::Trait::BaselineYogaNode); return traits; } @@ -58,6 +59,10 @@ class TextInputShadowNode final : public ConcreteViewShadowNode< const LayoutConstraints& layoutConstraints) const override; void layout(LayoutContext layoutContext) override; + Float baseline( + const LayoutContext& layoutContext, + Size size) const override; + private: /* * Creates a `State` object if needed. diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.cpp index f7ee626491fa20..8ffbfc5ba608dc 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.cpp @@ -81,6 +81,11 @@ YogaLayoutableShadowNode::YogaLayoutableShadowNode( YogaLayoutableShadowNode::yogaNodeMeasureCallbackConnector); } + if (getTraits().check(ShadowNodeTraits::Trait::BaselineYogaNode)) { + yogaNode_.setBaselineFunc( + YogaLayoutableShadowNode::yogaNodeBaselineCallbackConnector); + } + updateYogaProps(); updateYogaChildren(); @@ -845,6 +850,22 @@ YGSize YogaLayoutableShadowNode::yogaNodeMeasureCallbackConnector( yogaFloatFromFloat(size.width), yogaFloatFromFloat(size.height)}; } +float YogaLayoutableShadowNode::yogaNodeBaselineCallbackConnector( + YGNodeConstRef yogaNode, + float width, + float height) { + SystraceSection s( + "YogaLayoutableShadowNode::yogaNodeBaselineCallbackConnector"); + + auto& shadowNode = shadowNodeFromContext(yogaNode); + auto baseline = shadowNode.baseline( + threadLocalLayoutContext, + {.width = floatFromYogaFloat(width), + .height = floatFromYogaFloat(height)}); + + return yogaFloatFromFloat(baseline); +} + YogaLayoutableShadowNode& YogaLayoutableShadowNode::shadowNodeFromContext( YGNodeConstRef yogaNode) { return dynamic_cast( diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.h b/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.h index 6ae363c987ca2f..4c1c5a02a2b702 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.h @@ -164,6 +164,10 @@ class YogaLayoutableShadowNode : public LayoutableShadowNode { YGMeasureMode widthMode, float height, YGMeasureMode heightMode); + static float yogaNodeBaselineCallbackConnector( + YGNodeConstRef yogaNode, + float width, + float height); static YogaLayoutableShadowNode& shadowNodeFromContext( YGNodeConstRef yogaNode); diff --git a/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.cpp index cef90f40b6923c..5e5ccfe6e37697 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.cpp @@ -240,11 +240,7 @@ Size LayoutableShadowNode::measure( return layoutableShadowNode.getLayoutMetrics().frame.size; } -Float LayoutableShadowNode::firstBaseline(Size /*size*/) const { - return 0; -} - -Float LayoutableShadowNode::lastBaseline(Size /*size*/) const { +Float LayoutableShadowNode::baseline(const LayoutContext& /*layoutContext*/, Size /*size*/) const { return 0; } diff --git a/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.h b/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.h index d74a93ee686d53..9c21a144fa027a 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.h +++ b/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.h @@ -153,8 +153,7 @@ class LayoutableShadowNode : public ShadowNode { /* * Unifed methods to access text layout metrics. */ - virtual Float firstBaseline(Size size) const; - virtual Float lastBaseline(Size size) const; + virtual Float baseline(const LayoutContext& layoutContext, Size size) const; virtual bool canBeTouchTarget() const; virtual bool canChildrenBeTouchTarget() const; diff --git a/packages/react-native/ReactCommon/react/renderer/core/ShadowNodeTraits.h b/packages/react-native/ReactCommon/react/renderer/core/ShadowNodeTraits.h index 25157f64167184..729207447c51ce 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/ShadowNodeTraits.h +++ b/packages/react-native/ReactCommon/react/renderer/core/ShadowNodeTraits.h @@ -75,6 +75,9 @@ class ShadowNodeTraits { // Indicates that direct children of the node should not be collapsed ChildrenFormStackingContext = 1 << 10, + + // Inherits `YogaLayoutableShadowNode` and has a custom baseline function. + BaselineYogaNode = 1 << 11, }; /* diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/TextMeasureCache.h b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/TextMeasureCache.h index 01a981ba0c37f6..4a0fd56be48720 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/TextMeasureCache.h +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/TextMeasureCache.h @@ -66,6 +66,16 @@ class TextMeasureCacheKey final { LayoutConstraints layoutConstraints{}; }; +// The Key type that is used for Line Measure Cache. +// The equivalence and hashing operations of this are defined to respect the +// nature of text measuring. +class LineMeasureCacheKey final { + public: + AttributedString attributedString{}; + ParagraphAttributes paragraphAttributes{}; + Size size{}; +}; + /* * Maximum size of the Cache. * The number was empirically chosen based on approximation of an average amount @@ -82,6 +92,15 @@ using TextMeasureCache = SimpleThreadSafeCache< TextMeasurement, kSimpleThreadSafeCacheSizeCap>; +/* + * Thread-safe, evicting hash table designed to store line measurement + * information. + */ +using LineMeasureCache = SimpleThreadSafeCache< + LineMeasureCacheKey, + LinesMeasurements, + kSimpleThreadSafeCacheSizeCap>; + inline bool areTextAttributesEquivalentLayoutWise( const TextAttributes& lhs, const TextAttributes& rhs) { @@ -199,6 +218,21 @@ inline bool operator!=( return !(lhs == rhs); } +inline bool operator==( + const LineMeasureCacheKey& lhs, + const LineMeasureCacheKey& rhs) { + return areAttributedStringsEquivalentLayoutWise( + lhs.attributedString, rhs.attributedString) && + lhs.paragraphAttributes == rhs.paragraphAttributes && + lhs.size == rhs.size; +} + +inline bool operator!=( + const LineMeasureCacheKey& lhs, + const LineMeasureCacheKey& rhs) { + return !(lhs == rhs); +} + } // namespace facebook::react namespace std { @@ -213,4 +247,14 @@ struct hash { } }; +template <> +struct hash { + size_t operator()(const facebook::react::LineMeasureCacheKey& key) const { + return facebook::react::hash_combine( + attributedStringHashLayoutWise(key.attributedString), + key.paragraphAttributes, + key.size); + } +}; + } // namespace std diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp index e3f4f08d384df2..45a2e8c91bd613 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp @@ -89,7 +89,8 @@ Size measureAndroidComponent( TextLayoutManager::TextLayoutManager( const ContextContainer::Shared& contextContainer) : contextContainer_(contextContainer), - measureCache_(kSimpleThreadSafeCacheSizeCap) {} + textMeasureCache_(kSimpleThreadSafeCacheSizeCap), + lineMeasureCache_(kSimpleThreadSafeCacheSizeCap) {} void* TextLayoutManager::getNativeTextLayoutManager() const { return self_; @@ -102,7 +103,7 @@ TextMeasurement TextLayoutManager::measure( LayoutConstraints layoutConstraints) const { auto& attributedString = attributedStringBox.getValue(); - auto measurement = measureCache_.get( + auto measurement = textMeasureCache_.get( {attributedString, paragraphAttributes, layoutConstraints}, [&](const TextMeasureCacheKey& /*key*/) { auto telemetry = TransactionTelemetry::threadLocalTelemetry(); @@ -164,41 +165,61 @@ LinesMeasurements TextLayoutManager::measureLines( const AttributedString& attributedString, const ParagraphAttributes& paragraphAttributes, Size size) const { - const jni::global_ref& fabricUIManager = - contextContainer_->at>("FabricUIManager"); - static auto measureLines = - jni::findClassStatic("com/facebook/react/fabric/FabricUIManager") - ->getMethod("measureLines"); - - auto attributedStringMB = - JReadableMapBuffer::createWithContents(toMapBuffer(attributedString)); - auto paragraphAttributesMB = - JReadableMapBuffer::createWithContents(toMapBuffer(paragraphAttributes)); + auto lineMeasurements = lineMeasureCache_.get( + {attributedString, paragraphAttributes, size}, + [&](const LineMeasureCacheKey& /*key*/) { + const jni::global_ref& fabricUIManager = + contextContainer_->at>("FabricUIManager"); + static auto measureLines = + jni::findClassStatic("com/facebook/react/fabric/FabricUIManager") + ->getMethod("measureLines"); + + auto attributedStringMB = + JReadableMapBuffer::createWithContents(toMapBuffer(attributedString)); + auto paragraphAttributesMB = + JReadableMapBuffer::createWithContents(toMapBuffer(paragraphAttributes)); + + auto array = measureLines( + fabricUIManager, + attributedStringMB.get(), + paragraphAttributesMB.get(), + size.width, + size.height); + + auto dynamicArray = cthis(array)->consume(); + LinesMeasurements lineMeasurements; + lineMeasurements.reserve(dynamicArray.size()); + + for (const auto& data : dynamicArray) { + lineMeasurements.push_back(LineMeasurement(data)); + } - auto array = measureLines( - fabricUIManager, - attributedStringMB.get(), - paragraphAttributesMB.get(), - size.width, - size.height); + // Explicitly release smart pointers to free up space faster in JNI tables + attributedStringMB.reset(); + paragraphAttributesMB.reset(); - auto dynamicArray = cthis(array)->consume(); - LinesMeasurements lineMeasurements; - lineMeasurements.reserve(dynamicArray.size()); + return lineMeasurements; + }); - for (const auto& data : dynamicArray) { - lineMeasurements.push_back(LineMeasurement(data)); - } + return lineMeasurements; +} - // Explicitly release smart pointers to free up space faster in JNI tables - attributedStringMB.reset(); - paragraphAttributesMB.reset(); +Float TextLayoutManager::baseline( + AttributedString attributedString, + ParagraphAttributes paragraphAttributes, + Size size) const { + auto lines = this + ->measureLines(attributedString, paragraphAttributes, size); - return lineMeasurements; + if (!lines.empty()) { + return lines[0].ascender; + } else { + return 0; + } } TextMeasurement TextLayoutManager::doMeasure( diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.h b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.h index ac5206985d3792..103962aafab045 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.h +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.h @@ -67,6 +67,15 @@ class TextLayoutManager { const ParagraphAttributes& paragraphAttributes, Size size) const; + /* + * Calculates baseline of `attributedString` using native text rendering + * infrastructure. + */ + Float baseline( + AttributedString attributedString, + ParagraphAttributes paragraphAttributes, + 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. @@ -81,7 +90,8 @@ class TextLayoutManager { void* self_{}; ContextContainer::Shared contextContainer_; - TextMeasureCache measureCache_; + TextMeasureCache textMeasureCache_; + LineMeasureCache lineMeasureCache_; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/cxx/TextLayoutManager.cpp b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/cxx/TextLayoutManager.cpp index 294e886d3c9cab..285c384e9f44d1 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/cxx/TextLayoutManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/cxx/TextLayoutManager.cpp @@ -42,4 +42,11 @@ LinesMeasurements TextLayoutManager::measureLines( return {}; }; +Float TextLayoutManager::baseline( + AttributedString attributedString, + ParagraphAttributes paragraphAttributes, + Size size) const { + return 0; +} + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/cxx/TextLayoutManager.h b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/cxx/TextLayoutManager.h index 4b5eb7d1ff4692..180605602ccb99 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/cxx/TextLayoutManager.h +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/cxx/TextLayoutManager.h @@ -59,6 +59,15 @@ class TextLayoutManager { ParagraphAttributes paragraphAttributes, Size size) const; + /* + * Calculates baseline of `attributedString` using native text rendering + * infrastructure. + */ + virtual Float baseline( + AttributedString attributedString, + ParagraphAttributes paragraphAttributes, + 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/RCTTextLayoutManager.mm b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm index a50065a56cf416..b361f9adb68261 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 @@ -152,15 +152,16 @@ - (LinesMeasurements)getLinesForAttributedString:(facebook::react::AttributedStr attribute:NSFontAttributeName atIndex:0 effectiveRange:nil]; + CGFloat baseline = [layoutManager locationForGlyphAtIndex:range.location].y; auto rect = facebook::react::Rect{ facebook::react::Point{usedRect.origin.x, usedRect.origin.y}, facebook::react::Size{usedRect.size.width, usedRect.size.height}}; auto line = LineMeasurement{ std::string([renderedString UTF8String]), rect, - -font.descender, + overallRect.size.height - baseline, font.capHeight, - font.ascender, + baseline, font.xHeight}; blockParagraphLines->push_back(line); }]; @@ -303,12 +304,11 @@ - (TextMeasurement)_measureTextStorage:(NSTextStorage *)textStorage CGSize attachmentSize = attachment.bounds.size; CGRect glyphRect = [layoutManager boundingRectForGlyphRange:range inTextContainer:textContainer]; - - UIFont *font = [textStorage attribute:NSFontAttributeName atIndex:range.location effectiveRange:nil]; + CGFloat baseline = [layoutManager locationForGlyphAtIndex:range.location].y; CGRect frame = { {glyphRect.origin.x, - glyphRect.origin.y + glyphRect.size.height - attachmentSize.height + font.descender}, + baseline - attachmentSize.height}, attachmentSize}; auto rect = facebook::react::Rect{ 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 30fd816eb59ebe..5c380ab41c7a0a 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 @@ -45,6 +45,15 @@ class TextLayoutManager { ParagraphAttributes paragraphAttributes, Size size) const; + /* + * Calculates baseline of `attributedString` using native text rendering + * infrastructure. + */ + Float baseline( + AttributedString attributedString, + ParagraphAttributes paragraphAttributes, + 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. @@ -53,7 +62,8 @@ class TextLayoutManager { private: std::shared_ptr self_; - TextMeasureCache measureCache_{}; + TextMeasureCache textMeasureCache_{}; + LineMeasureCache lineMeasureCache_{}; }; } // namespace facebook::react 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 4d9b0f3f89f8b4..b64aca412b6f0c 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 @@ -38,7 +38,7 @@ case AttributedStringBox::Mode::Value: { auto &attributedString = attributedStringBox.getValue(); - measurement = measureCache_.get( + measurement = textMeasureCache_.get( {attributedString, paragraphAttributes, layoutConstraints}, [&](const TextMeasureCacheKey &key) { auto telemetry = TransactionTelemetry::threadLocalTelemetry(); if (telemetry) { @@ -90,9 +90,31 @@ Size size) const { RCTTextLayoutManager *textLayoutManager = (RCTTextLayoutManager *)unwrapManagedObject(self_); - return [textLayoutManager getLinesForAttributedString:attributedString - paragraphAttributes:paragraphAttributes - size:{size.width, size.height}]; + + auto measurement = lineMeasureCache_.get( + {attributedString, paragraphAttributes, size}, [&](const LineMeasureCacheKey &key) { + auto measurement = [textLayoutManager getLinesForAttributedString:attributedString + paragraphAttributes:paragraphAttributes + size:{size.width, size.height}]; + return measurement; + }); + + return measurement; +} + +Float TextLayoutManager::baseline( + AttributedString attributedString, + ParagraphAttributes paragraphAttributes, + Size size) const +{ + auto lines = this + ->measureLines(attributedString, paragraphAttributes, size); + + if (!lines.empty()) { + return lines[0].ascender; + } else { + return 0; + } } } // namespace facebook::react diff --git a/packages/rn-tester/js/examples/Text/TextExample.android.js b/packages/rn-tester/js/examples/Text/TextExample.android.js index c8acafe10e96a8..308a0c45af5bb5 100644 --- a/packages/rn-tester/js/examples/Text/TextExample.android.js +++ b/packages/rn-tester/js/examples/Text/TextExample.android.js @@ -18,7 +18,13 @@ import TextInlineViewsExample from './TextInlineViewsExample'; const TextInlineView = require('../../components/TextInlineView'); const React = require('react'); -const {LayoutAnimation, StyleSheet, Text, View} = require('react-native'); +const { + LayoutAnimation, + StyleSheet, + Text, + TextInput, + View, +} = require('react-native'); class Entity extends React.Component<{|children: React.Node|}> { render(): React.Node { @@ -958,6 +964,56 @@ function TextBaseLineLayoutExample(props: {}): React.Node { {marker} + + + {'Multi-line interleaved and :'} + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris + venenatis,{' '} + + mauris eu commodo maximus + {' '} + , ante arcu vestibulum ligula, et scelerisque diam. + + + + {'Multi-line alignment'} + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do + eiusmod tempor incididunt ut labore et dolore magna aliqua. + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do + eiusmod tempor incididunt ut labore et dolore magna aliqua. + + + + + {':'} + + {marker} + {texts} + {marker} + + + {':'} + + {marker} + + {texts} + + {marker} + ); } diff --git a/packages/rn-tester/js/examples/Text/TextExample.ios.js b/packages/rn-tester/js/examples/Text/TextExample.ios.js index 59d13fe8959a2a..f454c2c02554e4 100644 --- a/packages/rn-tester/js/examples/Text/TextExample.ios.js +++ b/packages/rn-tester/js/examples/Text/TextExample.ios.js @@ -308,9 +308,6 @@ class TextBaseLineLayoutExample extends React.Component<{}, mixed> { {marker} - {/* iOS-only because it relies on inline views being able to size to content. - * Android's implementation requires that a width and height be specified - * on the inline view. */} {'Interleaving and :'} {marker} @@ -347,6 +344,23 @@ class TextBaseLineLayoutExample extends React.Component<{}, mixed> { + {'Multi-line alignment'} + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do + eiusmod tempor incididunt ut labore et dolore magna aliqua. + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do + eiusmod tempor incididunt ut labore et dolore magna aliqua. + + + + {':'} {marker}