369 lines
13 KiB
Objective-C
369 lines
13 KiB
Objective-C
/**
|
|
* 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 "RCTShadowText.h"
|
|
|
|
#import "RCTAccessibilityManager.h"
|
|
#import "RCTUIManager.h"
|
|
#import "RCTBridge.h"
|
|
#import "RCTConvert.h"
|
|
#import "RCTLog.h"
|
|
#import "RCTShadowRawText.h"
|
|
#import "RCTSparseArray.h"
|
|
#import "RCTText.h"
|
|
#import "RCTUtils.h"
|
|
|
|
NSString *const RCTIsHighlightedAttributeName = @"IsHighlightedAttributeName";
|
|
NSString *const RCTReactTagAttributeName = @"ReactTagAttributeName";
|
|
|
|
@implementation RCTShadowText
|
|
{
|
|
NSTextStorage *_cachedTextStorage;
|
|
CGFloat _cachedTextStorageWidth;
|
|
NSAttributedString *_cachedAttributedString;
|
|
CGFloat _effectiveLetterSpacing;
|
|
}
|
|
|
|
static css_dim_t RCTMeasure(void *context, float width)
|
|
{
|
|
RCTShadowText *shadowText = (__bridge RCTShadowText *)context;
|
|
NSTextStorage *textStorage = [shadowText buildTextStorageForWidth:width];
|
|
NSLayoutManager *layoutManager = [textStorage.layoutManagers firstObject];
|
|
NSTextContainer *textContainer = [layoutManager.textContainers firstObject];
|
|
CGSize computedSize = [layoutManager usedRectForTextContainer:textContainer].size;
|
|
|
|
css_dim_t result;
|
|
result.dimensions[CSS_WIDTH] = RCTCeilPixelValue(computedSize.width);
|
|
if (shadowText->_effectiveLetterSpacing < 0) {
|
|
result.dimensions[CSS_WIDTH] -= shadowText->_effectiveLetterSpacing;
|
|
}
|
|
result.dimensions[CSS_HEIGHT] = RCTCeilPixelValue(computedSize.height);
|
|
return result;
|
|
}
|
|
|
|
- (instancetype)init
|
|
{
|
|
if ((self = [super init])) {
|
|
_fontSize = NAN;
|
|
_letterSpacing = NAN;
|
|
_isHighlighted = NO;
|
|
_textDecorationStyle = NSUnderlineStyleSingle;
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(contentSizeMultiplierDidChange:)
|
|
name:RCTUIManagerWillUpdateViewsDueToContentSizeMultiplierChangeNotification
|
|
object:nil];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
}
|
|
|
|
- (NSString *)description
|
|
{
|
|
NSString *superDescription = super.description;
|
|
return [[superDescription substringToIndex:superDescription.length - 1] stringByAppendingFormat:@"; text: %@>", [self attributedString].string];
|
|
}
|
|
|
|
- (void)contentSizeMultiplierDidChange:(NSNotification *)note
|
|
{
|
|
[self dirtyLayout];
|
|
[self dirtyText];
|
|
}
|
|
|
|
- (NSDictionary *)processUpdatedProperties:(NSMutableSet *)applierBlocks
|
|
parentProperties:(NSDictionary *)parentProperties
|
|
{
|
|
parentProperties = [super processUpdatedProperties:applierBlocks
|
|
parentProperties:parentProperties];
|
|
|
|
NSTextStorage *textStorage = [self buildTextStorageForWidth:self.frame.size.width];
|
|
[applierBlocks addObject:^(RCTSparseArray *viewRegistry) {
|
|
RCTText *view = viewRegistry[self.reactTag];
|
|
view.textStorage = textStorage;
|
|
}];
|
|
|
|
return parentProperties;
|
|
}
|
|
|
|
- (void)applyLayoutNode:(css_node_t *)node
|
|
viewsWithNewFrame:(NSMutableSet *)viewsWithNewFrame
|
|
absolutePosition:(CGPoint)absolutePosition
|
|
{
|
|
[super applyLayoutNode:node viewsWithNewFrame:viewsWithNewFrame absolutePosition:absolutePosition];
|
|
[self dirtyPropagation];
|
|
}
|
|
|
|
- (NSTextStorage *)buildTextStorageForWidth:(CGFloat)width
|
|
{
|
|
UIEdgeInsets padding = self.paddingAsInsets;
|
|
width -= (padding.left + padding.right);
|
|
|
|
if (_cachedTextStorage && width == _cachedTextStorageWidth) {
|
|
return _cachedTextStorage;
|
|
}
|
|
|
|
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
|
|
|
|
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:self.attributedString];
|
|
[textStorage addLayoutManager:layoutManager];
|
|
|
|
NSTextContainer *textContainer = [[NSTextContainer alloc] init];
|
|
textContainer.lineFragmentPadding = 0.0;
|
|
textContainer.lineBreakMode = _numberOfLines > 0 ? NSLineBreakByTruncatingTail : NSLineBreakByClipping;
|
|
textContainer.maximumNumberOfLines = _numberOfLines;
|
|
textContainer.size = (CGSize){isnan(width) ? CGFLOAT_MAX : width, CGFLOAT_MAX};
|
|
|
|
[layoutManager addTextContainer:textContainer];
|
|
[layoutManager ensureLayoutForTextContainer:textContainer];
|
|
|
|
_cachedTextStorageWidth = width;
|
|
_cachedTextStorage = textStorage;
|
|
|
|
return textStorage;
|
|
}
|
|
|
|
- (void)dirtyText
|
|
{
|
|
[super dirtyText];
|
|
_cachedTextStorage = nil;
|
|
}
|
|
|
|
- (void)recomputeText
|
|
{
|
|
[self attributedString];
|
|
[self setTextComputed];
|
|
[self dirtyPropagation];
|
|
}
|
|
|
|
- (NSAttributedString *)attributedString
|
|
{
|
|
return [self _attributedStringWithFontFamily:nil
|
|
fontSize:nil
|
|
fontWeight:nil
|
|
fontStyle:nil
|
|
letterSpacing:nil
|
|
useBackgroundColor:NO];
|
|
}
|
|
|
|
- (NSAttributedString *)_attributedStringWithFontFamily:(NSString *)fontFamily
|
|
fontSize:(NSNumber *)fontSize
|
|
fontWeight:(NSString *)fontWeight
|
|
fontStyle:(NSString *)fontStyle
|
|
letterSpacing:(NSNumber *)letterSpacing
|
|
useBackgroundColor:(BOOL)useBackgroundColor
|
|
{
|
|
if (![self isTextDirty] && _cachedAttributedString) {
|
|
return _cachedAttributedString;
|
|
}
|
|
|
|
if (_fontSize && !isnan(_fontSize)) {
|
|
fontSize = @(_fontSize);
|
|
}
|
|
if (_fontWeight) {
|
|
fontWeight = _fontWeight;
|
|
}
|
|
if (_fontStyle) {
|
|
fontStyle = _fontStyle;
|
|
}
|
|
if (_fontFamily) {
|
|
fontFamily = _fontFamily;
|
|
}
|
|
if (!isnan(_letterSpacing)) {
|
|
letterSpacing = @(_letterSpacing);
|
|
}
|
|
|
|
_effectiveLetterSpacing = letterSpacing.doubleValue;
|
|
|
|
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] init];
|
|
for (RCTShadowView *child in [self reactSubviews]) {
|
|
if ([child isKindOfClass:[RCTShadowText class]]) {
|
|
RCTShadowText *shadowText = (RCTShadowText *)child;
|
|
[attributedString appendAttributedString:[shadowText _attributedStringWithFontFamily:fontFamily fontSize:fontSize fontWeight:fontWeight fontStyle:fontStyle letterSpacing:letterSpacing useBackgroundColor:YES]];
|
|
} else if ([child isKindOfClass:[RCTShadowRawText class]]) {
|
|
RCTShadowRawText *shadowRawText = (RCTShadowRawText *)child;
|
|
[attributedString appendAttributedString:[[NSAttributedString alloc] initWithString:[shadowRawText text] ?: @""]];
|
|
} else {
|
|
RCTLogError(@"<Text> can't have any children except <Text> or raw strings");
|
|
}
|
|
|
|
[child setTextComputed];
|
|
}
|
|
|
|
if (_color) {
|
|
[self _addAttribute:NSForegroundColorAttributeName withValue:_color toAttributedString:attributedString];
|
|
}
|
|
if (_isHighlighted) {
|
|
[self _addAttribute:RCTIsHighlightedAttributeName withValue:@YES toAttributedString:attributedString];
|
|
}
|
|
if (useBackgroundColor && self.backgroundColor) {
|
|
[self _addAttribute:NSBackgroundColorAttributeName withValue:self.backgroundColor toAttributedString:attributedString];
|
|
}
|
|
|
|
UIFont *font = [RCTConvert UIFont:nil withFamily:fontFamily
|
|
size:fontSize weight:fontWeight style:fontStyle
|
|
scaleMultiplier:(_allowFontScaling && _fontSizeMultiplier > 0.0 ? _fontSizeMultiplier : 1.0)];
|
|
[self _addAttribute:NSFontAttributeName withValue:font toAttributedString:attributedString];
|
|
[self _addAttribute:NSKernAttributeName withValue:letterSpacing toAttributedString:attributedString];
|
|
[self _addAttribute:RCTReactTagAttributeName withValue:self.reactTag toAttributedString:attributedString];
|
|
[self _setParagraphStyleOnAttributedString:attributedString];
|
|
|
|
// create a non-mutable attributedString for use by the Text system which avoids copies down the line
|
|
_cachedAttributedString = [[NSAttributedString alloc] initWithAttributedString:attributedString];
|
|
[self dirtyLayout];
|
|
|
|
return _cachedAttributedString;
|
|
}
|
|
|
|
- (void)_addAttribute:(NSString *)attribute withValue:(id)attributeValue toAttributedString:(NSMutableAttributedString *)attributedString
|
|
{
|
|
[attributedString enumerateAttribute:attribute inRange:NSMakeRange(0, [attributedString length]) options:0 usingBlock:^(id value, NSRange range, BOOL *stop) {
|
|
if (!value && attributeValue) {
|
|
[attributedString addAttribute:attribute value:attributeValue range:range];
|
|
}
|
|
}];
|
|
}
|
|
|
|
/*
|
|
* LineHeight works the same way line-height works in the web: if children and self have
|
|
* varying lineHeights, we simply take the max.
|
|
*/
|
|
- (void)_setParagraphStyleOnAttributedString:(NSMutableAttributedString *)attributedString
|
|
{
|
|
// check if we have lineHeight set on self
|
|
__block BOOL hasParagraphStyle = NO;
|
|
if (_lineHeight || _textAlign) {
|
|
hasParagraphStyle = YES;
|
|
}
|
|
|
|
if (!_lineHeight) {
|
|
self.lineHeight = 0.0;
|
|
}
|
|
|
|
// check for lineHeight on each of our children, update the max as we go (in self.lineHeight)
|
|
[attributedString enumerateAttribute:NSParagraphStyleAttributeName inRange:NSMakeRange(0, [attributedString length]) options:0 usingBlock:^(id value, NSRange range, BOOL *stop) {
|
|
if (value) {
|
|
NSParagraphStyle *paragraphStyle = (NSParagraphStyle *)value;
|
|
CGFloat maximumLineHeight = round([paragraphStyle maximumLineHeight] / self.fontSizeMultiplier);
|
|
if (maximumLineHeight > self.lineHeight) {
|
|
self.lineHeight = maximumLineHeight;
|
|
}
|
|
hasParagraphStyle = YES;
|
|
}
|
|
}];
|
|
|
|
self.textAlign = _textAlign ?: NSTextAlignmentNatural;
|
|
self.writingDirection = _writingDirection ?: NSWritingDirectionNatural;
|
|
|
|
// if we found anything, set it :D
|
|
if (hasParagraphStyle) {
|
|
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
|
|
paragraphStyle.alignment = _textAlign;
|
|
paragraphStyle.baseWritingDirection = _writingDirection;
|
|
CGFloat lineHeight = round(_lineHeight * self.fontSizeMultiplier);
|
|
paragraphStyle.minimumLineHeight = lineHeight;
|
|
paragraphStyle.maximumLineHeight = lineHeight;
|
|
[attributedString addAttribute:NSParagraphStyleAttributeName
|
|
value:paragraphStyle
|
|
range:(NSRange){0, attributedString.length}];
|
|
}
|
|
|
|
// Text decoration
|
|
if(_textDecorationLine == RCTTextDecorationLineTypeUnderline ||
|
|
_textDecorationLine == RCTTextDecorationLineTypeUnderlineStrikethrough) {
|
|
[self _addAttribute:NSUnderlineStyleAttributeName withValue:@(_textDecorationStyle)
|
|
toAttributedString:attributedString];
|
|
}
|
|
if(_textDecorationLine == RCTTextDecorationLineTypeStrikethrough ||
|
|
_textDecorationLine == RCTTextDecorationLineTypeUnderlineStrikethrough){
|
|
[self _addAttribute:NSStrikethroughStyleAttributeName withValue:@(_textDecorationStyle)
|
|
toAttributedString:attributedString];
|
|
}
|
|
if(_textDecorationColor) {
|
|
[self _addAttribute:NSStrikethroughColorAttributeName withValue:_textDecorationColor
|
|
toAttributedString:attributedString];
|
|
[self _addAttribute:NSUnderlineColorAttributeName withValue:_textDecorationColor
|
|
toAttributedString:attributedString];
|
|
}
|
|
}
|
|
|
|
- (void)fillCSSNode:(css_node_t *)node
|
|
{
|
|
[super fillCSSNode:node];
|
|
node->measure = RCTMeasure;
|
|
node->children_count = 0;
|
|
}
|
|
|
|
- (void)insertReactSubview:(RCTShadowView *)subview atIndex:(NSInteger)atIndex
|
|
{
|
|
[super insertReactSubview:subview atIndex:atIndex];
|
|
[self cssNode]->children_count = 0;
|
|
}
|
|
|
|
- (void)removeReactSubview:(RCTShadowView *)subview
|
|
{
|
|
[super removeReactSubview:subview];
|
|
[self cssNode]->children_count = 0;
|
|
}
|
|
|
|
- (void)setBackgroundColor:(UIColor *)backgroundColor
|
|
{
|
|
super.backgroundColor = backgroundColor;
|
|
[self dirtyText];
|
|
}
|
|
|
|
#define RCT_TEXT_PROPERTY(setProp, ivar, type) \
|
|
- (void)set##setProp:(type)value; \
|
|
{ \
|
|
ivar = value; \
|
|
[self dirtyText]; \
|
|
}
|
|
|
|
RCT_TEXT_PROPERTY(Color, _color, UIColor *)
|
|
RCT_TEXT_PROPERTY(FontFamily, _fontFamily, NSString *)
|
|
RCT_TEXT_PROPERTY(FontSize, _fontSize, CGFloat)
|
|
RCT_TEXT_PROPERTY(FontWeight, _fontWeight, NSString *)
|
|
RCT_TEXT_PROPERTY(FontStyle, _fontStyle, NSString *)
|
|
RCT_TEXT_PROPERTY(IsHighlighted, _isHighlighted, BOOL)
|
|
RCT_TEXT_PROPERTY(LetterSpacing, _letterSpacing, CGFloat)
|
|
RCT_TEXT_PROPERTY(LineHeight, _lineHeight, CGFloat)
|
|
RCT_TEXT_PROPERTY(NumberOfLines, _numberOfLines, NSUInteger)
|
|
RCT_TEXT_PROPERTY(ShadowOffset, _shadowOffset, CGSize)
|
|
RCT_TEXT_PROPERTY(TextAlign, _textAlign, NSTextAlignment)
|
|
RCT_TEXT_PROPERTY(TextDecorationColor, _textDecorationColor, UIColor *);
|
|
RCT_TEXT_PROPERTY(TextDecorationLine, _textDecorationLine, RCTTextDecorationLineType);
|
|
RCT_TEXT_PROPERTY(TextDecorationStyle, _textDecorationStyle, NSUnderlineStyle);
|
|
RCT_TEXT_PROPERTY(WritingDirection, _writingDirection, NSWritingDirection)
|
|
|
|
- (void)setAllowFontScaling:(BOOL)allowFontScaling
|
|
{
|
|
_allowFontScaling = allowFontScaling;
|
|
for (RCTShadowView *child in [self reactSubviews]) {
|
|
if ([child isKindOfClass:[RCTShadowText class]]) {
|
|
[(RCTShadowText *)child setAllowFontScaling:allowFontScaling];
|
|
}
|
|
}
|
|
[self dirtyText];
|
|
}
|
|
|
|
- (void)setFontSizeMultiplier:(CGFloat)fontSizeMultiplier
|
|
{
|
|
_fontSizeMultiplier = fontSizeMultiplier;
|
|
for (RCTShadowView *child in [self reactSubviews]) {
|
|
if ([child isKindOfClass:[RCTShadowText class]]) {
|
|
[(RCTShadowText *)child setFontSizeMultiplier:fontSizeMultiplier];
|
|
}
|
|
}
|
|
[self dirtyText];
|
|
}
|
|
|
|
@end
|