diff --git a/Source/ASTextNode2.h b/Source/ASTextNode2.h index 5848adc37..177751610 100644 --- a/Source/ASTextNode2.h +++ b/Source/ASTextNode2.h @@ -7,7 +7,6 @@ // #import -#import #import @protocol ASTextLinePositionModifier; @@ -209,23 +208,9 @@ NS_ASSUME_NONNULL_BEGIN /** @abstract if YES will not intercept touches for non-link areas of the text. Default is NO. - @discussion If you still want to handle tap truncation action when passthroughNonlinkTouches is YES, - you should set the alwaysHandleTruncationTokenTap to YES. */ @property (nonatomic) BOOL passthroughNonlinkTouches; -/** - @abstract Always handle tap truncationAction, even the passthroughNonlinkTouches is YES. Default is NO. - @discussion if this is set to YES, the [ASTextNodeDelegate textNodeTappedTruncationToken:] callback will be called. - */ -@property (nonatomic) BOOL alwaysHandleTruncationTokenTap; - -/** - @abstract if YES will use the value of `self.tintColor` if the foreground color of text is not defined. - @discussion This is mainly used from ASButtonNode since by default text nodes do not respect tintColor settings unless contained within a interactive control - */ -@property (nonatomic) BOOL textColorFollowsTintColor; - + (void)enableDebugging; #pragma mark - Layout and Sizing diff --git a/Source/ASTextNode2.mm b/Source/ASTextNode2.mm index e268eca2d..4eb753857 100644 --- a/Source/ASTextNode2.mm +++ b/Source/ASTextNode2.mm @@ -20,9 +20,15 @@ #import #import +#import #import +#import + +#import +#import #import +#import @interface ASTextCacheValue : NSObject { @package @@ -48,11 +54,11 @@ @implementation ASTextCacheValue * NOTE: Be careful to copy `text` if needed. */ static NS_RETURNS_RETAINED ASTextLayout *ASTextNodeCompatibleLayoutWithContainerAndText(ASTextContainer *container, NSAttributedString *text) { - static dispatch_once_t onceToken; - static AS::Mutex *layoutCacheLock; + // Allocate layoutCacheLock on the heap to prevent destruction at app exit (https://github.com/TextureGroup/Texture/issues/136) + static auto *layoutCacheLock = new AS::Mutex; static NSCache *textLayoutCache; + static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - layoutCacheLock = new AS::Mutex(); textLayoutCache = [[NSCache alloc] init]; }); @@ -131,8 +137,6 @@ @implementation ASTextCacheValue return layout; } -static const NSTimeInterval ASTextNodeHighlightFadeOutDuration = 0.15; -static const NSTimeInterval ASTextNodeHighlightFadeInDuration = 0.1; static const CGFloat ASTextNodeHighlightLightOpacity = 0.11; static const CGFloat ASTextNodeHighlightDarkOpacity = 0.22; static NSString *ASTextNodeTruncationTokenAttributeName = @"ASTextNodeTruncationAttribute"; @@ -158,31 +162,22 @@ @implementation AS_TN2_CLASSNAME { NSAttributedString *_attributedText; NSAttributedString *_truncationAttributedText; NSAttributedString *_additionalTruncationMessage; + NSAttributedString *_composedTruncationText; NSArray *_pointSizeScaleFactors; NSLineBreakMode _truncationMode; NSString *_highlightedLinkAttributeName; id _highlightedLinkAttributeValue; + ASTextNodeHighlightStyle _highlightStyle; NSRange _highlightRange; ASHighlightOverlayLayer *_activeHighlightLayer; UIColor *_placeholderColor; UILongPressGestureRecognizer *_longPressGestureRecognizer; - ASTextNodeHighlightStyle _highlightStyle; - BOOL _longPressCancelsTouches; - BOOL _passthroughNonlinkTouches; - BOOL _alwaysHandleTruncationTokenTap; } @dynamic placeholderEnabled; -static NSArray *DefaultLinkAttributeNames() { - static NSArray *names; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - names = @[ NSLinkAttributeName ]; - }); - return names; -} +static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; - (instancetype)init { @@ -203,9 +198,9 @@ - (instancetype)init // The common case is for a text node to be non-opaque and blended over some background. self.opaque = NO; self.backgroundColor = [UIColor clearColor]; - - self.linkAttributeNames = DefaultLinkAttributeNames(); - + + self.linkAttributeNames = DefaultLinkAttributeNames; + // Accessibility self.isAccessibilityElement = YES; self.accessibilityTraits = self.defaultAccessibilityTraits; @@ -223,6 +218,12 @@ - (instancetype)init - (void)dealloc { CGColorRelease(_shadowColor); + + if (_longPressGestureRecognizer) { + _longPressGestureRecognizer.delegate = nil; + [_longPressGestureRecognizer removeTarget:nil action:NULL]; + [self.view removeGestureRecognizer:_longPressGestureRecognizer]; + } } #pragma mark - Description @@ -286,10 +287,7 @@ - (BOOL)supportsLayerBacking for (NSString *linkAttributeName in _linkAttributeNames) { __block BOOL hasLink = NO; [attributedText enumerateAttribute:linkAttributeName inRange:range options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop) { - if (value == nil) { - return; - } - hasLink = YES; + hasLink = (value != nil); *stop = YES; }]; if (hasLink) { @@ -395,7 +393,6 @@ - (void)setAttributedText:(NSAttributedString *)attributedText // Holding it for the duration of the method is more efficient in this case. ASLockScopeSelf(); - NSAttributedString *oldAttributedText = _attributedText; if (!ASCompareAssignCopy(_attributedText, attributedText)) { return; } @@ -418,12 +415,7 @@ - (void)setAttributedText:(NSAttributedString *)attributedText // Accessiblity self.accessibilityLabel = self.defaultAccessibilityLabel; - - // We update the isAccessibilityElement setting if this node is not switching between strings. - if (oldAttributedText.length == 0 || length == 0) { - // We're an accessibility element by default if there is a string. - self.isAccessibilityElement = (length != 0); - } + self.isAccessibilityElement = (length != 0); // We're an accessibility element by default if there is a string. #if AS_TEXTNODE2_RECORD_ATTRIBUTED_STRINGS [ASTextNode _registerAttributedText:_attributedText]; @@ -517,45 +509,21 @@ - (void)prepareAttributedString:(NSMutableAttributedString *)attributedString is - (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer { - ASTextContainer *copiedContainer; - NSMutableAttributedString *mutableText; - BOOL needsTintColor; - id bgColor; - { - // Wrapping all the other access here, because we can't lock while accessing tintColor. - ASLockScopeSelf(); - [self _ensureTruncationText]; - - // Unlike layout, here we must copy the container since drawing is asynchronous. - copiedContainer = [_textContainer copy]; - copiedContainer.size = self.bounds.size; - [copiedContainer makeImmutable]; - mutableText = [_attributedText mutableCopy] ?: [[NSMutableAttributedString alloc] init]; - - [self prepareAttributedString:mutableText isForIntrinsicSize:NO]; - needsTintColor = self.textColorFollowsTintColor && mutableText.length > 0; - bgColor = self.backgroundColor ?: [NSNull null]; - } - - // After all other attributes are set, apply tint color if needed and foreground color is not already specified - if (needsTintColor) { - // Apply tint color if specified and if foreground color is undefined for attributedString - NSRange limit = NSMakeRange(0, mutableText.length); - // Look for previous attributes that define foreground color - UIColor *attributeValue = (UIColor *)[mutableText attribute:NSForegroundColorAttributeName atIndex:limit.location effectiveRange:NULL]; - - // we need to unlock before accessing tintColor - UIColor *tintColor = self.tintColor; - if (attributeValue == nil && tintColor) { - // None are found, apply tint color if available. Fallback to "black" text color - [mutableText addAttributes:@{ NSForegroundColorAttributeName : tintColor } range:limit]; - } - } + ASLockScopeSelf(); + [self _ensureTruncationText]; + + // Unlike layout, here we must copy the container since drawing is asynchronous. + ASTextContainer *copiedContainer = [_textContainer copy]; + copiedContainer.size = self.bounds.size; + [copiedContainer makeImmutable]; + NSMutableAttributedString *mutableText = [_attributedText mutableCopy] ?: [[NSMutableAttributedString alloc] init]; + [self prepareAttributedString:mutableText isForIntrinsicSize:NO]; + return @{ @"container": copiedContainer, @"text": mutableText, - @"bgColor": bgColor + @"bgColor": self.backgroundColor ?: [NSNull null] }; } @@ -588,38 +556,6 @@ + (void)drawRect:(CGRect)bounds withParameters:(NSDictionary *)layoutDict isCanc [layout drawInContext:context size:bounds.size point:bounds.origin view:nil layer:nil debug:[ASTextDebugOption sharedDebugOption] cancel:isCancelledBlock]; } -#pragma mark - Tint Color - -- (void)tintColorDidChange -{ - [super tintColorDidChange]; - - [self _setNeedsDisplayOnTintedTextColor]; -} - -- (void)_setNeedsDisplayOnTintedTextColor -{ - BOOL textColorFollowsTintColor = NO; - { - AS::MutexLocker l(__instanceLock__); - textColorFollowsTintColor = _textColorFollowsTintColor; - } - - if (textColorFollowsTintColor) { - [self setNeedsDisplay]; - } -} - - -#pragma mark Interface State - -- (void)didEnterHierarchy -{ - [super didEnterHierarchy]; - - [self _setNeedsDisplayOnTintedTextColor]; -} - #pragma mark - Attributes - (id)linkAttributeValueAtPoint:(CGPoint)point @@ -656,41 +592,25 @@ - (id)_linkAttributeValueAtPoint:(CGPoint)point NSRange visibleRange = layout.visibleRange; NSRange clampedRange = NSIntersectionRange(visibleRange, NSMakeRange(0, _attributedText.length)); - - // Search the 9 points of a 44x44 square around the touch until we find a link. - // Start from center, then do sides, then do top/bottom, then do corners. - static constexpr CGSize kRectOffsets[9] = { - { 0, 0 }, - { -22, 0 }, { 22, 0 }, - { 0, -22 }, { 0, 22 }, - { -22, -22 }, { -22, 22 }, - { 22, -22 }, { 22, 22 } - }; - - for (const CGSize &offset : kRectOffsets) { - const CGPoint testPoint = CGPointMake(point.x + offset.width, - point.y + offset.height); - ASTextPosition *pos = [layout closestPositionToPoint:testPoint]; - if (!pos || !NSLocationInRange(pos.offset, clampedRange)) { + ASTextRange *range = [layout closestTextRangeAtPoint:point]; + NSRange effectiveRange = NSMakeRange(0, 0); + for (__strong NSString *attributeName in self.linkAttributeNames) { + id value = [self.attributedText attribute:attributeName atIndex:range.start.offset longestEffectiveRange:&effectiveRange inRange:clampedRange]; + if (value == nil) { + // Didn't find any links specified with this attribute. continue; } - for (NSString *attributeName in _linkAttributeNames) { - NSRange effectiveRange = NSMakeRange(0, 0); - id value = [_attributedText attribute:attributeName atIndex:pos.offset - longestEffectiveRange:&effectiveRange inRange:clampedRange]; - if (value == nil) { - // Didn't find any links specified with this attribute. - continue; - } - // If highlighting, check with delegate first. If not implemented, assume YES. - if (highlighting - && [_delegate respondsToSelector:@selector(textNode:shouldHighlightLinkAttribute:value:atPoint:)] - && ![_delegate textNode:(ASTextNode *)self shouldHighlightLinkAttribute:attributeName - value:value atPoint:point]) { - continue; - } + // If highlighting, check with delegate first. If not implemented, assume YES. + id delegate = self.delegate; + if (highlighting + && [delegate respondsToSelector:@selector(textNode:shouldHighlightLinkAttribute:value:atPoint:)] + && ![delegate textNode:(ASTextNode *)self shouldHighlightLinkAttribute:attributeName value:value atPoint:point]) { + value = nil; + attributeName = nil; + } + if (value != nil || attributeName != nil) { *rangeOut = NSIntersectionRange(visibleRange, effectiveRange); if (attributeNameOut != NULL) { @@ -718,17 +638,9 @@ - (BOOL)_locked_pointInsideAdditionalTruncationMessage:(CGPoint)point withLayout CTLineRef truncationTokenLine = CTLineCreateWithAttributedString((CFAttributedStringRef)_truncationAttributedText); CFIndex truncationTokenLineGlyphCount = truncationTokenLine ? CTLineGetGlyphCount(truncationTokenLine) : 0; - if (truncationTokenLine) { - CFRelease(truncationTokenLine); - } - CTLineRef additionalTruncationTokenLine = CTLineCreateWithAttributedString((CFAttributedStringRef)_additionalTruncationMessage); - CFIndex additionalTruncationTokenLineGlyphCount = additionalTruncationTokenLine ? CTLineGetGlyphCount(additionalTruncationTokenLine) : 0; + CFIndex additionalTruncationTokenLineGlyphCount = additionalTruncationTokenLine ? CTLineGetGlyphCount(additionalTruncationTokenLine) : 0; - if (additionalTruncationTokenLine) { - CFRelease(additionalTruncationTokenLine); - } - switch (_textContainer.truncationType) { case ASTextTruncationTypeStart: { CFIndex composedTruncationTextLineGlyphCount = truncationTokenLineGlyphCount + additionalTruncationTokenLineGlyphCount; @@ -837,114 +749,17 @@ - (void)setHighlightRange:(NSRange)highlightRange animated:(BOOL)animated - (void)_setHighlightRange:(NSRange)highlightRange forAttributeName:(NSString *)highlightedAttributeName value:(id)highlightedAttributeValue animated:(BOOL)animated { - ASDisplayNodeAssertMainThread(); ASLockScopeSelf(); // Protect usage of _highlight* ivars. // Set these so that link tapping works. _highlightedLinkAttributeName = highlightedAttributeName; _highlightedLinkAttributeValue = highlightedAttributeValue; + _highlightRange = highlightRange; - if (!NSEqualRanges(highlightRange, _highlightRange) && ((0 != highlightRange.length) || (0 != _highlightRange.length))) { - - _highlightRange = highlightRange; - - if (_activeHighlightLayer) { - if (animated) { - __weak CALayer *weakHighlightLayer = _activeHighlightLayer; - _activeHighlightLayer = nil; - - weakHighlightLayer.opacity = 0.0; - - CFTimeInterval beginTime = CACurrentMediaTime(); - CABasicAnimation *possibleFadeIn = (CABasicAnimation *)[weakHighlightLayer animationForKey:@"opacity"]; - if (possibleFadeIn) { - // Calculate when we should begin fading out based on the end of the fade in animation, - // Also check to make sure that the new begin time hasn't already passed - CGFloat newBeginTime = (possibleFadeIn.beginTime + possibleFadeIn.duration); - if (newBeginTime > beginTime) { - beginTime = newBeginTime; - } - } - - CABasicAnimation *fadeOut = [CABasicAnimation animationWithKeyPath:@"opacity"]; - fadeOut.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; - fadeOut.fromValue = possibleFadeIn.toValue ? : @(((CALayer *)weakHighlightLayer.presentationLayer).opacity); - fadeOut.toValue = @0.0; - fadeOut.fillMode = kCAFillModeBoth; - fadeOut.duration = ASTextNodeHighlightFadeOutDuration; - fadeOut.beginTime = beginTime; - - dispatch_block_t prev = [CATransaction completionBlock]; - [CATransaction setCompletionBlock:^{ - [weakHighlightLayer removeFromSuperlayer]; - }]; - - [weakHighlightLayer addAnimation:fadeOut forKey:fadeOut.keyPath]; - - [CATransaction setCompletionBlock:prev]; - - } else { - [_activeHighlightLayer removeFromSuperlayer]; - _activeHighlightLayer = nil; - } - } - if (0 != highlightRange.length) { - // Find layer in hierarchy that allows us to draw highlighting on. - CALayer *highlightTargetLayer = self.layer; - while (highlightTargetLayer != nil) { - if (highlightTargetLayer.as_allowsHighlightDrawing) { - break; - } - highlightTargetLayer = highlightTargetLayer.superlayer; - } - - if (highlightTargetLayer != nil) { - // TODO: The copy and application of size shouldn't be required, but it is currently. - // See discussion in https://github.com/TextureGroup/Texture/pull/396 - ASTextContainer *textContainerCopy = [_textContainer copy]; - textContainerCopy.size = self.calculatedSize; - ASTextLayout *layout = ASTextNodeCompatibleLayoutWithContainerAndText(textContainerCopy, _attributedText); - - NSArray *highlightRects = [layout selectionRectsWithoutStartAndEndForRange:[ASTextRange rangeWithRange:highlightRange]]; - NSMutableArray *converted = [NSMutableArray arrayWithCapacity:highlightRects.count]; - - CALayer *layer = self.layer; - UIEdgeInsets shadowPadding = self.shadowPadding; - for (ASTextSelectionRect *rectValue in highlightRects) { - // Adjust shadow padding - CGRect rendererRect = ASTextNodeAdjustRenderRectForShadowPadding(rectValue.rect, shadowPadding); - CGRect highlightedRect = [layer convertRect:rendererRect toLayer:highlightTargetLayer]; - - // We set our overlay layer's frame to the bounds of the highlight target layer. - // Offset highlight rects to avoid double-counting target layer's bounds.origin. - highlightedRect.origin.x -= highlightTargetLayer.bounds.origin.x; - highlightedRect.origin.y -= highlightTargetLayer.bounds.origin.y; - [converted addObject:[NSValue valueWithCGRect:highlightedRect]]; - } - - ASHighlightOverlayLayer *overlayLayer = [[ASHighlightOverlayLayer alloc] initWithRects:converted]; - overlayLayer.highlightColor = [[self class] _highlightColorForStyle:self.highlightStyle]; - overlayLayer.frame = highlightTargetLayer.bounds; - overlayLayer.masksToBounds = NO; - overlayLayer.opacity = [[self class] _highlightOpacityForStyle:self.highlightStyle]; - [highlightTargetLayer addSublayer:overlayLayer]; - - if (animated) { - CABasicAnimation *fadeIn = [CABasicAnimation animationWithKeyPath:@"opacity"]; - fadeIn.fromValue = @0.0; - fadeIn.toValue = @(overlayLayer.opacity); - fadeIn.duration = ASTextNodeHighlightFadeInDuration; - fadeIn.beginTime = CACurrentMediaTime(); - - [overlayLayer addAnimation:fadeIn forKey:fadeIn.keyPath]; - } - - [overlayLayer setNeedsDisplay]; + AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE(); + // Much of the code from original ASTextNode is probably usable here. - _activeHighlightLayer = overlayLayer; - } - } - } + return; } - (void)_clearHighlightIfNecessary @@ -968,12 +783,6 @@ + (CGFloat)_highlightOpacityForStyle:(ASTextNodeHighlightStyle)style #pragma mark - Text rects -static CGRect ASTextNodeAdjustRenderRectForShadowPadding(CGRect rendererRect, UIEdgeInsets shadowPadding) { - rendererRect.origin.x -= shadowPadding.left; - rendererRect.origin.y -= shadowPadding.top; - return rendererRect; -} - - (NSArray *)rectsForTextRange:(NSRange)textRange { AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE(); @@ -1024,15 +833,10 @@ - (UIImage *)placeholderImage - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { ASDisplayNodeAssertMainThread(); - ASLockScopeSelf(); // Protect usage of _passthroughNonlinkTouches and _alwaysHandleTruncationTokenTap ivars. - + if (!_passthroughNonlinkTouches) { return [super pointInside:point withEvent:event]; } - - if (_alwaysHandleTruncationTokenTap) { - return YES; - } NSRange range = NSMakeRange(0, 0); NSString *linkAttributeName = nil; @@ -1179,18 +983,6 @@ - (BOOL)_pendingTruncationTap return [ASLockedSelf(_highlightedLinkAttributeName) isEqualToString:ASTextNodeTruncationTokenAttributeName]; } -- (BOOL)alwaysHandleTruncationTokenTap -{ - ASLockScopeSelf(); - return _alwaysHandleTruncationTokenTap; -} - -- (void)setAlwaysHandleTruncationTokenTap:(BOOL)alwaysHandleTruncationTokenTap -{ - ASLockScopeSelf(); - _alwaysHandleTruncationTokenTap = alwaysHandleTruncationTokenTap; -} - #pragma mark - Shadow Properties /** @@ -1351,7 +1143,7 @@ - (void)setTruncationMode:(NSLineBreakMode)truncationMode - (BOOL)isTruncated { - return ASLockedSelf([self locked_textLayoutForSize:[self _locked_threadSafeBounds].size].truncatedLine != nil); + return ASLockedSelf([self locked_textLayoutForSize:[self _locked_threadSafeBounds].size].truncatedLine == nil); } - (BOOL)shouldTruncateForConstrainedSize:(ASSizeRange)constrainedSize @@ -1431,20 +1223,22 @@ - (NSRange)_additionalTruncationMessageRangeWithVisibleRange:(NSRange)visibleRan - (NSAttributedString *)_locked_composedTruncationText { DISABLED_ASAssertLocked(__instanceLock__); - NSAttributedString *composedTruncationText = nil; - if (_truncationAttributedText != nil && _additionalTruncationMessage != nil) { - NSMutableAttributedString *newComposedTruncationString = [[NSMutableAttributedString alloc] initWithAttributedString:_truncationAttributedText]; - [newComposedTruncationString.mutableString appendString:@" "]; - [newComposedTruncationString appendAttributedString:_additionalTruncationMessage]; - composedTruncationText = newComposedTruncationString; - } else if (_truncationAttributedText != nil) { - composedTruncationText = _truncationAttributedText; - } else if (_additionalTruncationMessage != nil) { - composedTruncationText = _additionalTruncationMessage; - } else { - composedTruncationText = DefaultTruncationAttributedString(); + if (_composedTruncationText == nil) { + if (_truncationAttributedText != nil && _additionalTruncationMessage != nil) { + NSMutableAttributedString *newComposedTruncationString = [[NSMutableAttributedString alloc] initWithAttributedString:_truncationAttributedText]; + [newComposedTruncationString.mutableString appendString:@" "]; + [newComposedTruncationString appendAttributedString:_additionalTruncationMessage]; + _composedTruncationText = newComposedTruncationString; + } else if (_truncationAttributedText != nil) { + _composedTruncationText = _truncationAttributedText; + } else if (_additionalTruncationMessage != nil) { + _composedTruncationText = _additionalTruncationMessage; + } else { + _composedTruncationText = DefaultTruncationAttributedString(); + } + _composedTruncationText = [self _locked_prepareTruncationStringForDrawing:_composedTruncationText]; } - return [self _locked_prepareTruncationStringForDrawing:composedTruncationText]; + return _composedTruncationText; } /** diff --git a/Source/TextExperiment/Component/ASTextLayout.mm b/Source/TextExperiment/Component/ASTextLayout.mm index f01c25908..0e44b9dc7 100644 --- a/Source/TextExperiment/Component/ASTextLayout.mm +++ b/Source/TextExperiment/Component/ASTextLayout.mm @@ -16,6 +16,8 @@ #import #import +#import + const CGSize ASTextContainerMaxSize = (CGSize){0x100000, 0x100000}; typedef struct { @@ -381,7 +383,7 @@ - (instancetype)_init { - (NSString *)description { return [NSString stringWithFormat:@"lines: %ld, visibleRange:%@, textBoundingRect:%@", - (long)[self.lines count], + [self.lines count], NSStringFromRange(self.visibleRange), NSStringFromCGRect(self.textBoundingRect)]; } @@ -832,8 +834,7 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri } } int i = 0; - if (type != kCTLineTruncationStart) { // Middle or End/Tail wants to collect some text (at least one line's - // worth) preceding the truncated content, with which to construct a "truncated line". + if (type != kCTLineTruncationStart) { // Middle or End/Tail wants text preceding truncated content. i = (int)removedLines.count - 1; while (atLeastOneLine < truncatedWidth && i >= 0) { if (lastLineText.length > 0 && [lastLineText.string characterAtIndex:lastLineText.string.length - 1] == '\n') { // Explicit newlines are always "long enough". @@ -845,8 +846,7 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri } [lastLineText appendAttributedString:truncationToken]; } - if (type != kCTLineTruncationEnd && removedLines.count > 0) { // Middle or Start/Head wants to collect some - // text following the truncated content. + if (type != kCTLineTruncationEnd && removedLines.count > 0) { // Middle or Start/Head wants text following truncated content. i = 0; atLeastOneLine = removedLines[i].width; while (atLeastOneLine < truncatedWidth && i < removedLines.count) { @@ -860,9 +860,7 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri [lastLineText appendAttributedString:nextLine]; } } - if (type == kCTLineTruncationStart) { - [lastLineText insertAttributedString:truncationToken atIndex:0]; - } + [lastLineText insertAttributedString:truncationToken atIndex:0]; } CTLineRef ctLastLineExtend = CTLineCreateWithAttributedString((CFAttributedStringRef) lastLineText); @@ -969,16 +967,15 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri [truncationToken enumerateAttributesInRange:NSMakeRange(0, truncationToken.length) options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:block]; } } + + attachments = [NSMutableArray new]; + attachmentRanges = [NSMutableArray new]; + attachmentRects = [NSMutableArray new]; + attachmentContentsSet = [NSMutableSet new]; for (NSUInteger i = 0, max = lines.count; i < max; i++) { ASTextLine *line = lines[i]; if (truncatedLine && line.index == truncatedLine.index) line = truncatedLine; if (line.attachments.count > 0) { - if (!attachments) { - attachments = [[NSMutableArray alloc] init]; - attachmentRanges = [[NSMutableArray alloc] init]; - attachmentRects = [[NSMutableArray alloc] init]; - attachmentContentsSet = [[NSMutableSet alloc] init]; - } [attachments addObjectsFromArray:line.attachments]; [attachmentRanges addObjectsFromArray:line.attachmentRanges]; [attachmentRects addObjectsFromArray:line.attachmentRects]; @@ -989,6 +986,9 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri } } } + if (attachments.count == 0) { + attachments = attachmentRanges = attachmentRects = nil; + } layout.frame = ctFrame; layout.lines = lines; @@ -2142,9 +2142,7 @@ - (NSArray *)selectionRectsForRange:(ASTextRange *)range { if (isVertical) { topRect.rect = CGRectMake(startLine.left, topOffset, startLine.width, (_container.path ? startLine.bottom : _container.size.height - _container.insets.bottom) - topOffset); } else { - // TODO: Fixes highlighting first row only to the end of the text and not highlight - // the while line to the end. Needs to brought over to multiline support - topRect.rect = CGRectMake(topOffset, startLine.top, (_container.path ? startLine.right : _container.size.width - _container.insets.right) - topOffset - (_container.size.width - _container.insets.right - startLine.right), startLine.height); + topRect.rect = CGRectMake(topOffset, startLine.top, (_container.path ? startLine.right : _container.size.width - _container.insets.right) - topOffset, startLine.height); } } [rects addObject:topRect]; diff --git a/Source/TextExperiment/Component/ASTextLine.mm b/Source/TextExperiment/Component/ASTextLine.mm index 37c68e02f..a01311d83 100644 --- a/Source/TextExperiment/Component/ASTextLine.mm +++ b/Source/TextExperiment/Component/ASTextLine.mm @@ -76,9 +76,9 @@ - (void)reloadBounds { NSUInteger runCount = CFArrayGetCount(runs); if (runCount == 0) return; - NSMutableArray *attachments = nil; - NSMutableArray *attachmentRanges = nil; - NSMutableArray *attachmentRects = nil; + NSMutableArray *attachments = [NSMutableArray new]; + NSMutableArray *attachmentRanges = [NSMutableArray new]; + NSMutableArray *attachmentRects = [NSMutableArray new]; for (NSUInteger r = 0; r < runCount; r++) { CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runs, r); CFIndex glyphCount = CTRunGetGlyphCount(run); @@ -104,19 +104,14 @@ - (void)reloadBounds { } NSRange runRange = ASTextNSRangeFromCFRange(CTRunGetStringRange(run)); - if (!attachments) { - attachments = [[NSMutableArray alloc] init]; - attachmentRanges = [[NSMutableArray alloc] init]; - attachmentRects = [[NSMutableArray alloc] init]; - } [attachments addObject:attachment]; [attachmentRanges addObject:[NSValue valueWithRange:runRange]]; [attachmentRects addObject:[NSValue valueWithCGRect:runTypoBounds]]; } } - _attachments = attachments; - _attachmentRanges = attachmentRanges; - _attachmentRects = attachmentRects; + _attachments = attachments.count ? attachments : nil; + _attachmentRanges = attachmentRanges.count ? attachmentRanges : nil; + _attachmentRects = attachmentRects.count ? attachmentRects : nil; } - (CGSize)size { diff --git a/Source/TextExperiment/String/ASTextAttribute.mm b/Source/TextExperiment/String/ASTextAttribute.mm index 8af432271..d1abadbe1 100644 --- a/Source/TextExperiment/String/ASTextAttribute.mm +++ b/Source/TextExperiment/String/ASTextAttribute.mm @@ -8,6 +8,7 @@ // #import "ASTextAttribute.h" +#import #import #import diff --git a/Source/TextExperiment/String/ASTextRunDelegate.h b/Source/TextExperiment/String/ASTextRunDelegate.h index 973dc5ffe..3d3bf11c2 100644 --- a/Source/TextExperiment/String/ASTextRunDelegate.h +++ b/Source/TextExperiment/String/ASTextRunDelegate.h @@ -43,7 +43,7 @@ NS_ASSUME_NONNULL_BEGIN /** Additional information about the the run delegate. */ -@property (nullable, nonatomic, copy) NSDictionary *userInfo; +@property (nullable, nonatomic) NSDictionary *userInfo; /** The typographic ascent of glyphs in the run. diff --git a/Source/TextExperiment/Utility/ASTextUtilities.h b/Source/TextExperiment/Utility/ASTextUtilities.h index 2ba0f09f5..f21a931ba 100644 --- a/Source/TextExperiment/Utility/ASTextUtilities.h +++ b/Source/TextExperiment/Utility/ASTextUtilities.h @@ -132,6 +132,7 @@ static inline CGFloat ASTextEmojiGetDescentWithFontSize(CGFloat fontSize) { } else { return 0.3125 * fontSize; } + return 0; } /** diff --git a/Source/TextExperiment/Utility/ASTextUtilities.mm b/Source/TextExperiment/Utility/ASTextUtilities.mm index 7194dadd4..8d0137718 100644 --- a/Source/TextExperiment/Utility/ASTextUtilities.mm +++ b/Source/TextExperiment/Utility/ASTextUtilities.mm @@ -7,6 +7,7 @@ // #import "ASTextUtilities.h" +#import NSCharacterSet *ASTextVerticalFormRotateCharacterSet() { static NSMutableCharacterSet *set; diff --git a/Tests/ASTextNode2Tests.mm b/Tests/ASTextNode2Tests.mm index 70d25cb26..2bc513af0 100644 --- a/Tests/ASTextNode2Tests.mm +++ b/Tests/ASTextNode2Tests.mm @@ -8,13 +8,12 @@ #import -#import +#import "ASTestCase.h" #import #import -#import -#import "ASTestCase.h" +#import @interface ASTextNode2Tests : XCTestCase @@ -64,14 +63,6 @@ - (void)setUp _textNode.attributedText = _attributedText; } -- (void)testTruncation -{ - XCTAssertTrue([(ASTextNode *)_textNode shouldTruncateForConstrainedSize:ASSizeRangeMake(CGSizeMake(100, 100))], @"Text Node should truncate"); - - _textNode.frame = CGRectMake(0, 0, 100, 100); - XCTAssertTrue(_textNode.isTruncated, @"Text Node should be truncated"); -} - - (void)testAccessibility { XCTAssertTrue(_textNode.isAccessibilityElement, @"Should be an accessibility element"); @@ -91,43 +82,4 @@ - (void)testAccessibility _textNode.defaultAccessibilityLabel, _attributedText.string); } -- (void)testRespectingAccessibilitySetting -{ - ASTextNode2 *textNode = [[ASTextNode2 alloc] init]; - textNode.attributedText = _attributedText; - textNode.isAccessibilityElement = NO; - - textNode.attributedText = [[NSAttributedString alloc] initWithString:@"new string"]; - XCTAssertFalse(textNode.isAccessibilityElement); - - // Ensure removing string on an accessible text node updates the setting. - ASTextNode2 *accessibleTextNode = [ASTextNode2 new]; - accessibleTextNode.attributedText = _attributedText; - accessibleTextNode.attributedText = nil; - XCTAssertFalse(accessibleTextNode.isAccessibilityElement); -} - -- (void)testSupportsLayerBacking -{ - ASTextNode2 *textNode = [[ASTextNode2 alloc] init]; - textNode.attributedText = [[NSAttributedString alloc] initWithString:@"new string"]; - XCTAssertTrue(textNode.supportsLayerBacking); - - NSString *link = @"https://texturegroup.com"; - NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"Texture Website: %@", link]]; - NSRange linkRange = [attributedText.string rangeOfString:link]; - [attributedText addAttribute:NSLinkAttributeName value:link range:linkRange]; - textNode.attributedText = attributedText; - XCTAssertFalse(textNode.supportsLayerBacking); -} - -- (void)testEmptyStringSize -{ - CGSize constrainedSize = CGSizeMake(100, CGFLOAT_MAX); - _textNode.attributedText = [[NSAttributedString alloc] initWithString:@""]; - CGSize sizeWithEmptyString = [_textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, constrainedSize)].size; - XCTAssertTrue(ASIsCGSizeValidForSize(sizeWithEmptyString)); - XCTAssertTrue(sizeWithEmptyString.width == 0); -} - @end