mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
8bd3edec88
Reviewed By: aaronabramov Differential Revision: D33367752 fbshipit-source-id: 4ce94d184485e5ee0a62cf67ad2d3ba16e285c8f
283 lines
14 KiB
Plaintext
283 lines
14 KiB
Plaintext
/*
|
|
* 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.
|
|
*/
|
|
|
|
#import "RCTTextLayoutManager.h"
|
|
|
|
#import "NSTextStorage+FontScaling.h"
|
|
#import "RCTAttributedTextUtils.h"
|
|
|
|
#import <React/RCTUtils.h>
|
|
#import <react/utils/ManagedObjectWrapper.h>
|
|
#import <react/utils/SimpleThreadSafeCache.h>
|
|
|
|
using namespace facebook::react;
|
|
|
|
@implementation RCTTextLayoutManager {
|
|
SimpleThreadSafeCache<AttributedString, std::shared_ptr<void>, 256> _cache;
|
|
}
|
|
|
|
static NSLineBreakMode RCTNSLineBreakModeFromEllipsizeMode(EllipsizeMode ellipsizeMode)
|
|
{
|
|
switch (ellipsizeMode) {
|
|
case EllipsizeMode::Clip:
|
|
return NSLineBreakByClipping;
|
|
case EllipsizeMode::Head:
|
|
return NSLineBreakByTruncatingHead;
|
|
case EllipsizeMode::Tail:
|
|
return NSLineBreakByTruncatingTail;
|
|
case EllipsizeMode::Middle:
|
|
return NSLineBreakByTruncatingMiddle;
|
|
}
|
|
}
|
|
|
|
- (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 {};
|
|
}
|
|
|
|
CGSize maximumSize = CGSize{layoutConstraints.maximumSize.width, CGFLOAT_MAX};
|
|
|
|
NSTextStorage *textStorage = [self _textStorageAndLayoutManagerWithAttributesString:attributedString
|
|
paragraphAttributes:paragraphAttributes
|
|
size:maximumSize];
|
|
|
|
NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject;
|
|
NSTextContainer *textContainer = layoutManager.textContainers.firstObject;
|
|
[layoutManager ensureLayoutForTextContainer:textContainer];
|
|
|
|
CGSize size = [layoutManager usedRectForTextContainer:textContainer].size;
|
|
|
|
size = (CGSize){RCTCeilPixelValue(size.width), RCTCeilPixelValue(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};
|
|
}
|
|
|
|
- (TextMeasurement)measureAttributedString:(AttributedString)attributedString
|
|
paragraphAttributes:(ParagraphAttributes)paragraphAttributes
|
|
layoutConstraints:(LayoutConstraints)layoutConstraints
|
|
{
|
|
return [self measureNSAttributedString:[self _nsAttributedStringFromAttributedString:attributedString]
|
|
paragraphAttributes:paragraphAttributes
|
|
layoutConstraints:layoutConstraints];
|
|
}
|
|
|
|
- (void)drawAttributedString:(AttributedString)attributedString
|
|
paragraphAttributes:(ParagraphAttributes)paragraphAttributes
|
|
frame:(CGRect)frame
|
|
{
|
|
NSTextStorage *textStorage = [self
|
|
_textStorageAndLayoutManagerWithAttributesString:[self _nsAttributedStringFromAttributedString:attributedString]
|
|
paragraphAttributes:paragraphAttributes
|
|
size:frame.size];
|
|
NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject;
|
|
NSTextContainer *textContainer = layoutManager.textContainers.firstObject;
|
|
|
|
#if TARGET_OS_MACCATALYST
|
|
CGContextRef context = UIGraphicsGetCurrentContext();
|
|
CGContextSaveGState(context);
|
|
CGContextSetShouldSmoothFonts(context, NO);
|
|
#endif
|
|
|
|
NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer];
|
|
[layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:frame.origin];
|
|
[layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:frame.origin];
|
|
|
|
#if TARGET_OS_MACCATALYST
|
|
CGContextRestoreGState(context);
|
|
#endif
|
|
}
|
|
|
|
- (LinesMeasurements)getLinesForAttributedString:(facebook::react::AttributedString)attributedString
|
|
paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes
|
|
size:(CGSize)size
|
|
{
|
|
NSTextStorage *textStorage = [self
|
|
_textStorageAndLayoutManagerWithAttributesString:[self _nsAttributedStringFromAttributedString:attributedString]
|
|
paragraphAttributes:paragraphAttributes
|
|
size:size];
|
|
NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject;
|
|
NSTextContainer *textContainer = layoutManager.textContainers.firstObject;
|
|
|
|
NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer];
|
|
|
|
std::vector<LineMeasurement> paragraphLines{};
|
|
auto blockParagraphLines = ¶graphLines;
|
|
|
|
[layoutManager enumerateLineFragmentsForGlyphRange:glyphRange
|
|
usingBlock:^(
|
|
CGRect overallRect,
|
|
CGRect usedRect,
|
|
NSTextContainer *_Nonnull usedTextContainer,
|
|
NSRange lineGlyphRange,
|
|
BOOL *_Nonnull stop) {
|
|
NSRange range = [layoutManager characterRangeForGlyphRange:lineGlyphRange
|
|
actualGlyphRange:nil];
|
|
NSString *renderedString = [textStorage.string substringWithRange:range];
|
|
UIFont *font = [[textStorage attributedSubstringFromRange:range]
|
|
attribute:NSFontAttributeName
|
|
atIndex:0
|
|
effectiveRange:nil];
|
|
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,
|
|
font.capHeight,
|
|
font.ascender,
|
|
font.xHeight};
|
|
blockParagraphLines->push_back(line);
|
|
}];
|
|
return paragraphLines;
|
|
}
|
|
|
|
- (NSTextStorage *)_textStorageAndLayoutManagerWithAttributesString:(NSAttributedString *)attributedString
|
|
paragraphAttributes:(ParagraphAttributes)paragraphAttributes
|
|
size:(CGSize)size
|
|
{
|
|
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:size];
|
|
|
|
textContainer.lineFragmentPadding = 0.0; // Note, the default value is 5.
|
|
textContainer.lineBreakMode = paragraphAttributes.maximumNumberOfLines > 0
|
|
? RCTNSLineBreakModeFromEllipsizeMode(paragraphAttributes.ellipsizeMode)
|
|
: NSLineBreakByClipping;
|
|
textContainer.maximumNumberOfLines = paragraphAttributes.maximumNumberOfLines;
|
|
|
|
NSLayoutManager *layoutManager = [NSLayoutManager new];
|
|
layoutManager.usesFontLeading = NO;
|
|
[layoutManager addTextContainer:textContainer];
|
|
|
|
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedString];
|
|
|
|
[textStorage addLayoutManager:layoutManager];
|
|
|
|
if (paragraphAttributes.adjustsFontSizeToFit) {
|
|
CGFloat minimumFontSize = !isnan(paragraphAttributes.minimumFontSize) ? paragraphAttributes.minimumFontSize : 4.0;
|
|
CGFloat maximumFontSize = !isnan(paragraphAttributes.maximumFontSize) ? paragraphAttributes.maximumFontSize : 96.0;
|
|
[textStorage scaleFontSizeToFitSize:size minimumFontSize:minimumFontSize maximumFontSize:maximumFontSize];
|
|
}
|
|
|
|
return textStorage;
|
|
}
|
|
|
|
- (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];
|
|
NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject;
|
|
NSTextContainer *textContainer = layoutManager.textContainers.firstObject;
|
|
|
|
CGFloat fraction;
|
|
NSUInteger characterIndex = [layoutManager characterIndexForPoint:point
|
|
inTextContainer:textContainer
|
|
fractionOfDistanceBetweenInsertionPoints:&fraction];
|
|
|
|
// If the point is not before (fraction == 0.0) the first character and not
|
|
// after (fraction == 1.0) the last character, then the attribute is valid.
|
|
if (textStorage.length > 0 && (fraction > 0 || characterIndex > 0) &&
|
|
(fraction < 1 || characterIndex < textStorage.length - 1)) {
|
|
RCTWeakEventEmitterWrapper *eventEmitterWrapper =
|
|
(RCTWeakEventEmitterWrapper *)[textStorage attribute:RCTAttributedStringEventEmitterKey
|
|
atIndex:characterIndex
|
|
effectiveRange:NULL];
|
|
return eventEmitterWrapper.eventEmitter;
|
|
}
|
|
|
|
return nil;
|
|
}
|
|
|
|
- (NSAttributedString *)_nsAttributedStringFromAttributedString:(AttributedString)attributedString
|
|
{
|
|
auto sharedNSAttributedString = _cache.get(attributedString, [](AttributedString attributedString) {
|
|
return wrapManagedObject(RCTNSAttributedStringFromAttributedString(attributedString));
|
|
});
|
|
|
|
return unwrapManagedObject(sharedNSAttributedString);
|
|
}
|
|
|
|
- (void)getRectWithAttributedString:(AttributedString)attributedString
|
|
paragraphAttributes:(ParagraphAttributes)paragraphAttributes
|
|
enumerateAttribute:(NSString *)enumerateAttribute
|
|
frame:(CGRect)frame
|
|
usingBlock:(RCTTextLayoutFragmentEnumerationBlock)block
|
|
{
|
|
NSTextStorage *textStorage = [self
|
|
_textStorageAndLayoutManagerWithAttributesString:[self _nsAttributedStringFromAttributedString:attributedString]
|
|
paragraphAttributes:paragraphAttributes
|
|
size:frame.size];
|
|
|
|
NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject;
|
|
NSTextContainer *textContainer = layoutManager.textContainers.firstObject;
|
|
[layoutManager ensureLayoutForTextContainer:textContainer];
|
|
|
|
NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer];
|
|
NSRange characterRange = [layoutManager characterRangeForGlyphRange:glyphRange actualGlyphRange:NULL];
|
|
|
|
[textStorage enumerateAttribute:enumerateAttribute
|
|
inRange:characterRange
|
|
options:0
|
|
usingBlock:^(NSString *value, NSRange range, BOOL *pause) {
|
|
if (!value) {
|
|
return;
|
|
}
|
|
|
|
[layoutManager
|
|
enumerateEnclosingRectsForGlyphRange:range
|
|
withinSelectedGlyphRange:range
|
|
inTextContainer:textContainer
|
|
usingBlock:^(CGRect enclosingRect, BOOL *_Nonnull stop) {
|
|
block(
|
|
enclosingRect,
|
|
[textStorage attributedSubstringFromRange:range].string,
|
|
value);
|
|
*stop = YES;
|
|
}];
|
|
}];
|
|
}
|
|
|
|
@end
|