/** * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ #import "RCTText.h" #import "RCTShadowText.h" #import "RCTUtils.h" #import "UIView+React.h" @implementation RCTText { NSTextStorage *_textStorage; NSMutableArray *_reactSubviews; CAShapeLayer *_highlightLayer; } - (instancetype)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { _textStorage = [NSTextStorage new]; _reactSubviews = [NSMutableArray array]; self.isAccessibilityElement = YES; self.accessibilityTraits |= UIAccessibilityTraitStaticText; self.opaque = NO; self.contentMode = UIViewContentModeRedraw; } return self; } - (NSString *)description { NSString *superDescription = super.description; NSRange semicolonRange = [superDescription rangeOfString:@";"]; NSString *replacement = [NSString stringWithFormat:@"; reactTag: %@; text: %@", self.reactTag, self.textStorage.string]; return [superDescription stringByReplacingCharactersInRange:semicolonRange withString:replacement]; } - (void)reactSetFrame:(CGRect)frame { // Text looks super weird if its frame is animated. // This disables the frame animation, without affecting opacity, etc. [UIView performWithoutAnimation:^{ [super reactSetFrame:frame]; }]; } - (void)reactSetInheritedBackgroundColor:(UIColor *)inheritedBackgroundColor { self.backgroundColor = inheritedBackgroundColor; } - (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex { [_reactSubviews insertObject:subview atIndex:atIndex]; } - (void)removeReactSubview:(UIView *)subview { [_reactSubviews removeObject:subview]; } - (NSArray *)reactSubviews { return _reactSubviews; } - (void)setTextStorage:(NSTextStorage *)textStorage { if (_textStorage != textStorage) { _textStorage = textStorage; [self setNeedsDisplay]; } } - (void)drawRect:(CGRect)rect { NSLayoutManager *layoutManager = _textStorage.layoutManagers.firstObject; NSTextContainer *textContainer = layoutManager.textContainers.firstObject; CGRect textFrame = UIEdgeInsetsInsetRect(self.bounds, _contentInset); NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer]; [layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:textFrame.origin]; [layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:textFrame.origin]; __block UIBezierPath *highlightPath = nil; NSRange characterRange = [layoutManager characterRangeForGlyphRange:glyphRange actualGlyphRange:NULL]; [layoutManager.textStorage enumerateAttribute:RCTIsHighlightedAttributeName inRange:characterRange options:0 usingBlock:^(NSNumber *value, NSRange range, BOOL *_) { if (!value.boolValue) { return; } [layoutManager enumerateEnclosingRectsForGlyphRange:range withinSelectedGlyphRange:range inTextContainer:textContainer usingBlock:^(CGRect enclosingRect, __unused BOOL *__) { UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(enclosingRect, -2, -2) cornerRadius:2]; if (highlightPath) { [highlightPath appendPath:path]; } else { highlightPath = path; } }]; }]; if (highlightPath) { if (!_highlightLayer) { _highlightLayer = [CAShapeLayer layer]; _highlightLayer.fillColor = [UIColor colorWithWhite:0 alpha:0.25].CGColor; [self.layer addSublayer:_highlightLayer]; } _highlightLayer.position = (CGPoint){_contentInset.left, _contentInset.top}; _highlightLayer.path = highlightPath.CGPath; } else { [_highlightLayer removeFromSuperlayer]; _highlightLayer = nil; } } - (NSNumber *)reactTagAtPoint:(CGPoint)point { NSNumber *reactTag = self.reactTag; CGFloat fraction; NSLayoutManager *layoutManager = _textStorage.layoutManagers.firstObject; NSTextContainer *textContainer = layoutManager.textContainers.firstObject; 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)) { reactTag = [_textStorage attribute:RCTReactTagAttributeName atIndex:characterIndex effectiveRange:NULL]; } return reactTag; } - (void)didMoveToWindow { [super didMoveToWindow]; if (!self.window) { self.layer.contents = nil; if (_highlightLayer) { [_highlightLayer removeFromSuperlayer]; _highlightLayer = nil; } } else if (_textStorage.length) { [self setNeedsDisplay]; } } #pragma mark - Accessibility - (NSString *)accessibilityLabel { return _textStorage.string; } @end