From 9e2f8859c4d237ae2bee4679da45038405b7074e Mon Sep 17 00:00:00 2001 From: zhongwuzw Date: Mon, 15 Jul 2024 10:53:35 -0700 Subject: [PATCH] Reland: [iOS] Fixes ellipsis carries background from trimmed text (#45412) Summary: Reland https://github.com/facebook/react-native/issues/39408 . Try to fix the crash https://github.com/facebook/react-native/issues/37926#issuecomment-2225113557. cc. javache I changed the range from glyph to character because all attributes related APIs are character range. ## Changelog: [IOS] [FIXED] - Fixes ellipsis carries background from trimmed text Pull Request resolved: https://github.com/facebook/react-native/pull/45412 Test Plan: https://github.com/facebook/react-native/issues/37926 . Reviewed By: cipolleschi Differential Revision: D59681679 Pulled By: javache fbshipit-source-id: de4cb73e0304b8c0b0a40f4f6838b2c679747009 --- .../Libraries/Text/Text/RCTTextShadowView.mm | 46 +++++++++++++++++++ .../textlayoutmanager/RCTTextLayoutManager.mm | 46 +++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/packages/react-native/Libraries/Text/Text/RCTTextShadowView.mm b/packages/react-native/Libraries/Text/Text/RCTTextShadowView.mm index 363170e617e..6c815d267ee 100644 --- a/packages/react-native/Libraries/Text/Text/RCTTextShadowView.mm +++ b/packages/react-native/Libraries/Text/Text/RCTTextShadowView.mm @@ -237,6 +237,8 @@ maximumFontSize:self.textAttributes.effectiveFont.pointSize]; } + [self processTruncatedAttributedText:textStorage textContainer:textContainer layoutManager:layoutManager]; + if (!exclusiveOwnership) { [_cachedTextStorages setObject:textStorage forKey:key]; } @@ -244,6 +246,50 @@ return textStorage; } +- (void)processTruncatedAttributedText:(NSTextStorage *)textStorage + textContainer:(NSTextContainer *)textContainer + layoutManager:(NSLayoutManager *)layoutManager +{ + if (_maximumNumberOfLines > 0) { + [layoutManager ensureLayoutForTextContainer:textContainer]; + NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer]; + __block int line = 0; + [layoutManager + enumerateLineFragmentsForGlyphRange:glyphRange + usingBlock:^( + CGRect rect, + CGRect usedRect, + NSTextContainer *_Nonnull _, + NSRange lineGlyphRange, + BOOL *_Nonnull stop) { + if (line == textContainer.maximumNumberOfLines - 1) { + NSRange truncatedRange = [layoutManager + truncatedGlyphRangeInLineFragmentForGlyphAtIndex:lineGlyphRange.location]; + + if (truncatedRange.location != NSNotFound) { + NSRange characterRange = + [layoutManager characterRangeForGlyphRange:truncatedRange + actualGlyphRange:nil]; + if (characterRange.location > 0 && characterRange.length > 0) { + // Remove color attributes for truncated range + for (NSAttributedStringKey key in + @[ NSForegroundColorAttributeName, NSBackgroundColorAttributeName ]) { + [textStorage removeAttribute:key range:characterRange]; + id attribute = [textStorage attribute:key + atIndex:characterRange.location - 1 + effectiveRange:nil]; + if (attribute) { + [textStorage addAttribute:key value:attribute range:characterRange]; + } + } + } + } + } + line++; + }]; + } +} + - (void)layoutWithMetrics:(RCTLayoutMetrics)layoutMetrics layoutContext:(RCTLayoutContext)layoutContext { // If the view got new `contentFrame`, we have to redraw it because 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 b62dc9b536b..0cc55820b7d 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 @@ -83,6 +83,9 @@ static NSLineBreakMode RCTNSLineBreakModeFromEllipsizeMode(EllipsizeMode ellipsi #endif NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer]; + + [self processTruncatedAttributedText:textStorage textContainer:textContainer layoutManager:layoutManager]; + [layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:frame.origin]; [layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:frame.origin]; @@ -123,6 +126,49 @@ static NSLineBreakMode RCTNSLineBreakModeFromEllipsizeMode(EllipsizeMode ellipsi } } +- (void)processTruncatedAttributedText:(NSTextStorage *)textStorage + textContainer:(NSTextContainer *)textContainer + layoutManager:(NSLayoutManager *)layoutManager +{ + if (textContainer.maximumNumberOfLines > 0) { + [layoutManager ensureLayoutForTextContainer:textContainer]; + NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer]; + __block int line = 0; + [layoutManager + enumerateLineFragmentsForGlyphRange:glyphRange + usingBlock:^( + CGRect rect, + CGRect usedRect, + NSTextContainer *_Nonnull _, + NSRange lineGlyphRange, + BOOL *_Nonnull stop) { + if (line == textContainer.maximumNumberOfLines - 1) { + NSRange truncatedRange = [layoutManager + truncatedGlyphRangeInLineFragmentForGlyphAtIndex:lineGlyphRange.location]; + if (truncatedRange.location != NSNotFound) { + NSRange characterRange = + [layoutManager characterRangeForGlyphRange:truncatedRange + actualGlyphRange:nil]; + if (characterRange.location > 0 && characterRange.length > 0) { + // Remove color attributes for truncated range + for (NSAttributedStringKey key in + @[ NSForegroundColorAttributeName, NSBackgroundColorAttributeName ]) { + [textStorage removeAttribute:key range:characterRange]; + id attribute = [textStorage attribute:key + atIndex:characterRange.location - 1 + effectiveRange:nil]; + if (attribute) { + [textStorage addAttribute:key value:attribute range:characterRange]; + } + } + } + } + } + line++; + }]; + } +} + - (LinesMeasurements)getLinesForAttributedString:(facebook::react::AttributedString)attributedString paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes size:(CGSize)size