/** * Copyright (c) 2015-present, Facebook, Inc. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "RCTTextShadowView.h" #import #import #import #import #import "NSTextStorage+FontScaling.h" #import "RCTTextView.h" @implementation RCTTextShadowView { __weak RCTBridge *_bridge; BOOL _needsUpdateView; NSMapTable *_cachedTextStorages; } - (instancetype)initWithBridge:(RCTBridge *)bridge { if (self = [super init]) { _bridge = bridge; _cachedTextStorages = [NSMapTable strongToStrongObjectsMapTable]; _needsUpdateView = YES; YGNodeSetMeasureFunc(self.yogaNode, RCTTextShadowViewMeasure); YGNodeSetBaselineFunc(self.yogaNode, RCTTextShadowViewBaseline); } return self; } - (BOOL)isYogaLeafNode { return YES; } - (void)dirtyLayout { [super dirtyLayout]; YGNodeMarkDirty(self.yogaNode); [self invalidateCache]; } - (void)invalidateCache { [_cachedTextStorages removeAllObjects]; _needsUpdateView = YES; } #pragma mark - RCTUIManagerObserver - (void)uiManagerWillPerformMounting { if (YGNodeIsDirty(self.yogaNode)) { return; } if (!_needsUpdateView) { return; } _needsUpdateView = NO; CGRect contentFrame = self.contentFrame; NSTextStorage *textStorage = [self textStorageAndLayoutManagerThatFitsSize:self.contentFrame.size exclusiveOwnership:YES]; NSNumber *tag = self.reactTag; NSMutableArray *descendantViewTags = [NSMutableArray new]; [textStorage enumerateAttribute:RCTBaseTextShadowViewEmbeddedShadowViewAttributeName inRange:NSMakeRange(0, textStorage.length) options:0 usingBlock: ^(RCTShadowView *shadowView, NSRange range, __unused BOOL *stop) { if (!shadowView) { return; } [descendantViewTags addObject:shadowView.reactTag]; } ]; [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { RCTTextView *textView = (RCTTextView *)viewRegistry[tag]; if (!textView) { return; } NSMutableArray *descendantViews = [NSMutableArray arrayWithCapacity:descendantViewTags.count]; [descendantViewTags enumerateObjectsUsingBlock:^(NSNumber *_Nonnull descendantViewTag, NSUInteger index, BOOL *_Nonnull stop) { UIView *descendantView = viewRegistry[descendantViewTag]; if (!descendantView) { return; } [descendantViews addObject:descendantView]; }]; // Removing all references to Shadow Views to avoid unnececery retainning. [textStorage removeAttribute:RCTBaseTextShadowViewEmbeddedShadowViewAttributeName range:NSMakeRange(0, textStorage.length)]; [textView setTextStorage:textStorage contentFrame:contentFrame descendantViews:descendantViews]; }]; } - (void)postprocessAttributedText:(NSMutableAttributedString *)attributedText { __block CGFloat maximumLineHeight = 0; [attributedText enumerateAttribute:NSParagraphStyleAttributeName inRange:NSMakeRange(0, attributedText.length) options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock: ^(NSParagraphStyle *paragraphStyle, __unused NSRange range, __unused BOOL *stop) { if (!paragraphStyle) { return; } maximumLineHeight = MAX(paragraphStyle.maximumLineHeight, maximumLineHeight); } ]; if (maximumLineHeight == 0) { // `lineHeight` was not specified, nothing to do. return; } [attributedText beginEditing]; [attributedText enumerateAttribute:NSFontAttributeName inRange:NSMakeRange(0, attributedText.length) options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock: ^(UIFont *font, NSRange range, __unused BOOL *stop) { if (!font) { return; } if (maximumLineHeight <= font.lineHeight) { return; } CGFloat baseLineOffset = maximumLineHeight / 2.0 - font.lineHeight / 2.0; [attributedText addAttribute:NSBaselineOffsetAttributeName value:@(baseLineOffset) range:range]; } ]; [attributedText endEditing]; } - (NSAttributedString *)attributedTextWithMeasuredAttachmentsThatFitSize:(CGSize)size { NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithAttributedString:[self attributedTextWithBaseTextAttributes:nil]]; [attributedText beginEditing]; [attributedText enumerateAttribute:RCTBaseTextShadowViewEmbeddedShadowViewAttributeName inRange:NSMakeRange(0, attributedText.length) options:0 usingBlock: ^(RCTShadowView *shadowView, NSRange range, __unused BOOL *stop) { if (!shadowView) { return; } CGSize fittingSize = [shadowView sizeThatFitsMinimumSize:CGSizeZero maximumSize:size]; NSTextAttachment *attachment = [NSTextAttachment new]; attachment.bounds = (CGRect){CGPointZero, fittingSize}; [attributedText addAttribute:NSAttachmentAttributeName value:attachment range:range]; } ]; [attributedText endEditing]; return [attributedText copy]; } - (NSTextStorage *)textStorageAndLayoutManagerThatFitsSize:(CGSize)size exclusiveOwnership:(BOOL)exclusiveOwnership { NSValue *key = [NSValue valueWithCGSize:size]; NSTextStorage *cachedTextStorage = [_cachedTextStorages objectForKey:key]; if (cachedTextStorage) { if (exclusiveOwnership) { [_cachedTextStorages removeObjectForKey:key]; } return cachedTextStorage; } NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:size]; textContainer.lineFragmentPadding = 0.0; // Note, the default value is 5. textContainer.lineBreakMode = _maximumNumberOfLines > 0 ? _lineBreakMode : NSLineBreakByClipping; textContainer.maximumNumberOfLines = _maximumNumberOfLines; NSLayoutManager *layoutManager = [NSLayoutManager new]; [layoutManager addTextContainer:textContainer]; NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:[self attributedTextWithMeasuredAttachmentsThatFitSize:size]]; [self postprocessAttributedText:textStorage]; [textStorage addLayoutManager:layoutManager]; if (_adjustsFontSizeToFit) { CGFloat minimumFontSize = MAX(_minimumFontScale * (self.textAttributes.effectiveFont.pointSize), 4.0); [textStorage scaleFontSizeToFitSize:size minimumFontSize:minimumFontSize maximumFontSize:self.textAttributes.effectiveFont.pointSize]; } if (!exclusiveOwnership) { [_cachedTextStorages setObject:textStorage forKey:key]; } return textStorage; } - (void)layoutWithMetrics:(RCTLayoutMetrics)layoutMetrics layoutContext:(RCTLayoutContext)layoutContext { // If the view got new `contentFrame`, we have to redraw it because // and sizes of embedded views may change. if (!CGRectEqualToRect(self.layoutMetrics.contentFrame, layoutMetrics.contentFrame)) { _needsUpdateView = YES; } if (self.textAttributes.layoutDirection != layoutMetrics.layoutDirection) { self.textAttributes.layoutDirection = layoutMetrics.layoutDirection; [self invalidateCache]; } [super layoutWithMetrics:layoutMetrics layoutContext:layoutContext]; } - (void)layoutSubviewsWithContext:(RCTLayoutContext)layoutContext { NSTextStorage *textStorage = [self textStorageAndLayoutManagerThatFitsSize:self.availableSize exclusiveOwnership:NO]; NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject; NSTextContainer *textContainer = layoutManager.textContainers.firstObject; NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer]; NSRange characterRange = [layoutManager characterRangeForGlyphRange:glyphRange actualGlyphRange:NULL]; [textStorage enumerateAttribute:RCTBaseTextShadowViewEmbeddedShadowViewAttributeName inRange:characterRange options:0 usingBlock: ^(RCTShadowView *shadowView, NSRange range, BOOL *stop) { if (!shadowView) { return; } CGRect glyphRect = [layoutManager boundingRectForGlyphRange:range inTextContainer:textContainer]; NSTextAttachment *attachment = [textStorage attribute:NSAttachmentAttributeName atIndex:range.location effectiveRange:nil]; CGSize attachmentSize = attachment.bounds.size; UIFont *font = [textStorage attribute:NSFontAttributeName atIndex:range.location effectiveRange:nil]; CGRect frame = {{ RCTRoundPixelValue(glyphRect.origin.x), RCTRoundPixelValue(glyphRect.origin.y + glyphRect.size.height - attachmentSize.height + font.descender) }, { RCTRoundPixelValue(attachmentSize.width), RCTRoundPixelValue(attachmentSize.height) }}; RCTLayoutContext localLayoutContext = layoutContext; localLayoutContext.absolutePosition.x += frame.origin.x; localLayoutContext.absolutePosition.y += frame.origin.y; [shadowView layoutWithMinimumSize:frame.size maximumSize:frame.size layoutDirection:self.layoutMetrics.layoutDirection layoutContext:localLayoutContext]; // Reinforcing a proper frame origin for the Shadow View. RCTLayoutMetrics localLayoutMetrics = shadowView.layoutMetrics; localLayoutMetrics.frame.origin = frame.origin; [shadowView layoutWithMetrics:localLayoutMetrics layoutContext:localLayoutContext]; } ]; } - (CGFloat)lastBaselineForSize:(CGSize)size { NSAttributedString *attributedText = [self textStorageAndLayoutManagerThatFitsSize:size exclusiveOwnership:NO]; __block CGFloat maximumDescender = 0.0; [attributedText enumerateAttribute:NSFontAttributeName inRange:NSMakeRange(0, attributedText.length) options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock: ^(UIFont *font, NSRange range, __unused BOOL *stop) { if (maximumDescender > font.descender) { maximumDescender = font.descender; } } ]; return size.height + maximumDescender; } static YGSize RCTTextShadowViewMeasure(YGNodeRef node, float width, YGMeasureMode widthMode, float height, YGMeasureMode heightMode) { CGSize maximumSize = (CGSize){ widthMode == YGMeasureModeUndefined ? CGFLOAT_MAX : RCTCoreGraphicsFloatFromYogaFloat(width), heightMode == YGMeasureModeUndefined ? CGFLOAT_MAX : RCTCoreGraphicsFloatFromYogaFloat(height), }; RCTTextShadowView *shadowTextView = (__bridge RCTTextShadowView *)YGNodeGetContext(node); NSTextStorage *textStorage = [shadowTextView textStorageAndLayoutManagerThatFitsSize:maximumSize exclusiveOwnership:NO]; NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject; NSTextContainer *textContainer = layoutManager.textContainers.firstObject; [layoutManager ensureLayoutForTextContainer:textContainer]; CGSize size = [layoutManager usedRectForTextContainer:textContainer].size; CGFloat letterSpacing = shadowTextView.textAttributes.letterSpacing; if (!isnan(letterSpacing) && letterSpacing < 0) { size.width -= letterSpacing; } size = (CGSize){ MIN(RCTCeilPixelValue(size.width), maximumSize.width), MIN(RCTCeilPixelValue(size.height), maximumSize.height) }; // Adding epsilon value illuminates problems with converting values from // `double` to `float`, and then rounding them to pixel grid in Yoga. CGFloat epsilon = 0.001; return (YGSize){ RCTYogaFloatFromCoreGraphicsFloat(size.width + epsilon), RCTYogaFloatFromCoreGraphicsFloat(size.height + epsilon) }; } static float RCTTextShadowViewBaseline(YGNodeRef node, const float width, const float height) { RCTTextShadowView *shadowTextView = (__bridge RCTTextShadowView *)YGNodeGetContext(node); CGSize size = (CGSize){ RCTCoreGraphicsFloatFromYogaFloat(width), RCTCoreGraphicsFloatFromYogaFloat(height) }; CGFloat lastBaseline = [shadowTextView lastBaselineForSize:size]; return RCTYogaFloatFromCoreGraphicsFloat(lastBaseline); } @end